websocket works
This commit is contained in:
parent
15a225e68f
commit
227029c8f6
|
|
@ -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/
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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") {
|
||||||
|
event.preventDefault()
|
||||||
|
runCommand(cmdTextbox.value)
|
||||||
|
clearInput()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInput() {
|
||||||
cmdTextbox.value = ""
|
cmdTextbox.value = ""
|
||||||
cmdTextbox.rows = 1
|
cmdTextbox.rows = 1
|
||||||
cmdLine.dataset.extended = "false"
|
cmdLine.dataset.extended = "false"
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
runCommand(cmdTextbox.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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.
|
// 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")
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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
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