websocket works

This commit is contained in:
Chris Wanstrath 2025-09-20 13:12:28 -07:00
parent 15a225e68f
commit 227029c8f6
13 changed files with 140 additions and 26 deletions

View File

@ -5,3 +5,9 @@
- [ ] Runs one-shot TypeScript commands (via NOSE terminal) - [ ] 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] Has a 960x540 (16:9) virtual screen size that scales to the actual size of the display
- [x] Runs on a Raspberry Pi 5 - [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/

View File

@ -23,7 +23,7 @@ export const Terminal: FC = async () => (
/> />
</div> </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">**** NOSE PLUTO V{new Date().getMonth() + 1}.{new Date().getDate()} ****</li>
<li class="center">VRAM <span id="vram-size">000KB</span></li> <li class="center">VRAM <span id="vram-size">000KB</span></li>
</ul> </ul>

View File

@ -88,9 +88,11 @@ main {
height: 540px; height: 540px;
background: var(--c64-dark-blue); background: var(--c64-dark-blue);
color: var(--c64-light-blue); color: var(--c64-light-blue);
/* nearest-neighbor scaling */ /* nearest-neighbor scaling */
image-rendering: pixelated; image-rendering: pixelated;
transform-origin: center center; transform-origin: center center;
} }
body[data-mode=tall] #content { body[data-mode=tall] #content {

View File

@ -4,6 +4,7 @@
--cli-margin: 10px 0 0 25px; --cli-margin: 10px 0 0 25px;
--cli-font-size: 20px; --cli-font-size: 20px;
--cli-height: 30px; --cli-height: 30px;
--cli-status-width: 8px;
} }
#command-line { #command-line {
@ -71,17 +72,23 @@
border: none; border: none;
} }
#command-history { /* The scrollback shows your previous inputs and outputs. */
#scrollback {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-size: var(--cli-font-size); font-size: var(--cli-font-size);
} }
#command-history li { #scrollback li {
list-style: none; list-style: none;
margin: 0; margin: 0;
} }
.center { #scrollback .center {
text-align: center; text-align: center;
}
#scrollback .input .content {
margin-left: var(--cli-status-width);
} }

View File

@ -1,17 +1,32 @@
//// ////
// DOM helpers and cached elements // DOM helpers and cached elements
// finds an element by ID // elements we know will be there... right?
export const $ = (id: string): HTMLElement | null => export const cmdLine = $("command-line") as HTMLDivElement
document.getElementById(id) export const cmdTextbox = $("command-textbox") as HTMLTextAreaElement
export const scrollback = $("scrollback") as HTMLUListElement
// creates an HTML element // finds an element by ID
export const $$ = (tag: string, innerHTML = ""): HTMLElement => { export function $(id: string): HTMLElement | null {
const el = document.createElement(tag) return document.getElementById(id)
if (innerHTML) el.innerHTML = innerHTML
return el
} }
// elements we know will be there... right? // creates an HTML element, optinally with one or more classes
export const cmdLine = document.getElementById("command-line") as HTMLDivElement // shortcut:
export const cmdTextbox = document.getElementById("command-textbox") as HTMLTextAreaElement // $$("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
}

View File

@ -18,11 +18,14 @@ function inputHandler(event: KeyboardEvent) {
cmdTextbox.rows += 1 cmdTextbox.rows += 1
cmdLine.dataset.extended = "true" cmdLine.dataset.extended = "true"
} else if (event.key === "Enter") { } else if (event.key === "Enter") {
cmdTextbox.value = ""
cmdTextbox.rows = 1
cmdLine.dataset.extended = "false"
event.preventDefault() event.preventDefault()
runCommand(cmdTextbox.value) runCommand(cmdTextbox.value)
clearInput()
} }
}
function clearInput() {
cmdTextbox.value = ""
cmdTextbox.rows = 1
cmdLine.dataset.extended = "false"
} }

48
src/js/scrollback.ts Normal file
View 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
View 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()

View File

@ -1,8 +1,20 @@
//// ////
// The shell runs on the server and processes input, returning output. // 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) { 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")
}

View File

@ -2,6 +2,8 @@
// The terminal communicates with the shell via websockets. // The terminal communicates with the shell via websockets.
import type { Message } from "../shared/message.js" import type { Message } from "../shared/message.js"
import { sessionID } from "./session.js"
import { handleMessage } from "./shell.js"
let ws: WebSocket | null = null let ws: WebSocket | null = null
@ -12,17 +14,25 @@ export function startConnection() {
ws = new WebSocket(url) ws = new WebSocket(url)
ws.onopen = () => console.log('WS connected') 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.onclose = () => setTimeout(startConnection, 1000) // simple retry
ws.onerror = () => ws?.close() ws.onerror = () => ws?.close()
} }
// send any message // send any message
export function send(msg: Message) { export function send(msg: Message) {
if (!msg.session) msg.session = sessionID
ws?.readyState === 1 && ws.send(JSON.stringify(msg)) ws?.readyState === 1 && ws.send(JSON.stringify(msg))
console.log("-> send", msg)
} }
// close it... plz don't do this, though // close it... plz don't do this, though
export function close() { export function close() {
ws?.close(1000, 'bye') ws?.close(1000, 'bye')
} }
// register a local listener
export function onMessage(callback: (e: MessageEvent) => void) {
if (!ws) return
ws.onmessage = callback
}

View File

@ -54,7 +54,8 @@ app.get("/ws", upgradeWebSocket((c) => {
return { return {
onMessage(event, ws) { onMessage(event, ws) {
console.log(`Message from client: ${event.data}`) 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: () => { onClose: () => {
console.log('Connection closed') console.log('Connection closed')

View File

@ -1,6 +1,6 @@
export type Message = { export type Message = {
session: string session?: string
id: string id: string
type: "output" type: "input" | "output"
data: any data: any
} }

3
src/shared/utils.ts Normal file
View File

@ -0,0 +1,3 @@
export function randomID(): string {
return Math.random().toString(36).slice(7)
}