websocket works
This commit is contained in:
parent
15a225e68f
commit
227029c8f6
|
|
@ -5,3 +5,9 @@
|
|||
- [ ] Runs one-shot TypeScript commands (via NOSE terminal)
|
||||
- [x] Has a 960x540 (16:9) virtual screen size that scales to the actual size of the display
|
||||
- [x] Runs on a Raspberry Pi 5
|
||||
|
||||
## Fonts
|
||||
|
||||
Use this to examine what's inside the C64 .woff2 font file in public/vendor:
|
||||
|
||||
https://wakamaifondue.com/
|
||||
|
|
@ -23,7 +23,7 @@ export const Terminal: FC = async () => (
|
|||
/>
|
||||
</div>
|
||||
|
||||
<ul id="command-history">
|
||||
<ul id="scrollback">
|
||||
<li class="center">**** NOSE PLUTO V{new Date().getMonth() + 1}.{new Date().getDate()} ****</li>
|
||||
<li class="center">VRAM <span id="vram-size">000KB</span></li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -88,9 +88,11 @@ main {
|
|||
height: 540px;
|
||||
background: var(--c64-dark-blue);
|
||||
color: var(--c64-light-blue);
|
||||
|
||||
/* nearest-neighbor scaling */
|
||||
image-rendering: pixelated;
|
||||
transform-origin: center center;
|
||||
|
||||
}
|
||||
|
||||
body[data-mode=tall] #content {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
--cli-margin: 10px 0 0 25px;
|
||||
--cli-font-size: 20px;
|
||||
--cli-height: 30px;
|
||||
--cli-status-width: 8px;
|
||||
}
|
||||
|
||||
#command-line {
|
||||
|
|
@ -71,17 +72,23 @@
|
|||
border: none;
|
||||
}
|
||||
|
||||
#command-history {
|
||||
/* The scrollback shows your previous inputs and outputs. */
|
||||
|
||||
#scrollback {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: var(--cli-font-size);
|
||||
}
|
||||
|
||||
#command-history li {
|
||||
#scrollback li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.center {
|
||||
#scrollback .center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#scrollback .input .content {
|
||||
margin-left: var(--cli-status-width);
|
||||
}
|
||||
|
|
@ -1,17 +1,32 @@
|
|||
////
|
||||
// DOM helpers and cached elements
|
||||
|
||||
// finds an element by ID
|
||||
export const $ = (id: string): HTMLElement | null =>
|
||||
document.getElementById(id)
|
||||
// elements we know will be there... right?
|
||||
export const cmdLine = $("command-line") as HTMLDivElement
|
||||
export const cmdTextbox = $("command-textbox") as HTMLTextAreaElement
|
||||
export const scrollback = $("scrollback") as HTMLUListElement
|
||||
|
||||
// creates an HTML element
|
||||
export const $$ = (tag: string, innerHTML = ""): HTMLElement => {
|
||||
const el = document.createElement(tag)
|
||||
if (innerHTML) el.innerHTML = innerHTML
|
||||
return el
|
||||
// finds an element by ID
|
||||
export function $(id: string): HTMLElement | null {
|
||||
return document.getElementById(id)
|
||||
}
|
||||
|
||||
// elements we know will be there... right?
|
||||
export const cmdLine = document.getElementById("command-line") as HTMLDivElement
|
||||
export const cmdTextbox = document.getElementById("command-textbox") as HTMLTextAreaElement
|
||||
// creates an HTML element, optinally with one or more classes
|
||||
// shortcut:
|
||||
// $$("span.input.green") = <span class="input green"></span>
|
||||
// $$(".input.green") = <div class="input green"></div>
|
||||
export const $$ = (tag: string, innerHTML = ""): HTMLElement => {
|
||||
let name: string | undefined = tag
|
||||
let classList: string[] = []
|
||||
|
||||
if (tag.startsWith("."))
|
||||
tag = "div" + tag
|
||||
|
||||
if (tag.includes("."))
|
||||
[name, ...classList] = tag.split(".").map(x => x.trim())
|
||||
|
||||
const el = document.createElement(name!)
|
||||
if (innerHTML) el.innerHTML = innerHTML
|
||||
if (classList.length) el.classList.add(...classList)
|
||||
return el
|
||||
}
|
||||
|
|
@ -18,11 +18,14 @@ function inputHandler(event: KeyboardEvent) {
|
|||
cmdTextbox.rows += 1
|
||||
cmdLine.dataset.extended = "true"
|
||||
} else if (event.key === "Enter") {
|
||||
cmdTextbox.value = ""
|
||||
cmdTextbox.rows = 1
|
||||
cmdLine.dataset.extended = "false"
|
||||
event.preventDefault()
|
||||
|
||||
runCommand(cmdTextbox.value)
|
||||
clearInput()
|
||||
}
|
||||
}
|
||||
|
||||
function clearInput() {
|
||||
cmdTextbox.value = ""
|
||||
cmdTextbox.rows = 1
|
||||
cmdLine.dataset.extended = "false"
|
||||
}
|
||||
48
src/js/scrollback.ts
Normal file
48
src/js/scrollback.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
////
|
||||
// The scrollback shows your history of interacting with the shell.
|
||||
// input, output, etc
|
||||
|
||||
import { scrollback, $$ } from "./dom.js"
|
||||
|
||||
type InputStatus = "waiting" | "streaming" | "ok" | "error"
|
||||
|
||||
export function addInput(id: string, input: string) {
|
||||
const parent = $$("li.input")
|
||||
const status = $$("span.status.yellow", "•")
|
||||
const content = $$("span.content", input)
|
||||
parent.append(status, content)
|
||||
parent.dataset.id = id
|
||||
|
||||
scrollback.append(parent)
|
||||
}
|
||||
|
||||
export function setStatus(id: string, status: InputStatus) {
|
||||
const statusEl = document.querySelector(`[data-id="${id}"].input .status`)
|
||||
if (!statusEl) return
|
||||
|
||||
statusEl.className = ""
|
||||
|
||||
switch (status) {
|
||||
case "waiting":
|
||||
statusEl.classList.add("yellow")
|
||||
break
|
||||
case "streaming":
|
||||
statusEl.classList.add("purple")
|
||||
break
|
||||
case "ok":
|
||||
statusEl.classList.add("green")
|
||||
break
|
||||
case "error":
|
||||
statusEl.classList.add("red")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export function addOutput(id: string, output: string) {
|
||||
const item = $$("li", output)
|
||||
item.classList.add("output")
|
||||
item.dataset.id = id
|
||||
|
||||
scrollback.append(item)
|
||||
|
||||
}
|
||||
7
src/js/session.ts
Normal file
7
src/js/session.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
////
|
||||
// Each browser tab is a shell session. This means you can run multiple sessions
|
||||
// in the same browser.
|
||||
|
||||
import { randomID } from "../shared/utils.js"
|
||||
|
||||
export const sessionID = randomID()
|
||||
|
|
@ -1,8 +1,20 @@
|
|||
////
|
||||
// The shell runs on the server and processes input, returning output.
|
||||
|
||||
import type { Message } from "../shared/message.js"
|
||||
import { addInput, setStatus, addOutput } from "./scrollback.js"
|
||||
import { send } from "./websocket.js"
|
||||
import { randomID } from "../shared/utils.js"
|
||||
|
||||
export function runCommand(input: string) {
|
||||
const id = randomID()
|
||||
addInput(id, input)
|
||||
|
||||
}
|
||||
send({ id, type: "input", data: input })
|
||||
}
|
||||
|
||||
// message received from server
|
||||
export function handleMessage(e: MessageEvent) {
|
||||
const data = JSON.parse(e.data)
|
||||
addOutput(data.id, data.data)
|
||||
setStatus(data.id, "ok")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// The terminal communicates with the shell via websockets.
|
||||
|
||||
import type { Message } from "../shared/message.js"
|
||||
import { sessionID } from "./session.js"
|
||||
import { handleMessage } from "./shell.js"
|
||||
|
||||
let ws: WebSocket | null = null
|
||||
|
||||
|
|
@ -12,17 +14,25 @@ export function startConnection() {
|
|||
ws = new WebSocket(url)
|
||||
|
||||
ws.onopen = () => console.log('WS connected')
|
||||
ws.onmessage = e => console.log('WS message:', e.data)
|
||||
ws.onmessage = handleMessage
|
||||
ws.onclose = () => setTimeout(startConnection, 1000) // simple retry
|
||||
ws.onerror = () => ws?.close()
|
||||
}
|
||||
|
||||
// send any message
|
||||
export function send(msg: Message) {
|
||||
if (!msg.session) msg.session = sessionID
|
||||
ws?.readyState === 1 && ws.send(JSON.stringify(msg))
|
||||
console.log("-> send", msg)
|
||||
}
|
||||
|
||||
// close it... plz don't do this, though
|
||||
export function close() {
|
||||
ws?.close(1000, 'bye')
|
||||
}
|
||||
}
|
||||
|
||||
// register a local listener
|
||||
export function onMessage(callback: (e: MessageEvent) => void) {
|
||||
if (!ws) return
|
||||
ws.onmessage = callback
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ app.get("/ws", upgradeWebSocket((c) => {
|
|||
return {
|
||||
onMessage(event, ws) {
|
||||
console.log(`Message from client: ${event.data}`)
|
||||
ws.send('Hello from server!')
|
||||
const data = JSON.parse(event.data.toString())
|
||||
ws.send(JSON.stringify({ id: data.id, data: "Hi there!" }))
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('Connection closed')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export type Message = {
|
||||
session: string
|
||||
session?: string
|
||||
id: string
|
||||
type: "output"
|
||||
type: "input" | "output"
|
||||
data: any
|
||||
}
|
||||
|
|
|
|||
3
src/shared/utils.ts
Normal file
3
src/shared/utils.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function randomID(): string {
|
||||
return Math.random().toString(36).slice(7)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user