diff --git a/README.md b/README.md
index 09f7e90..2fa1dca 100644
--- a/README.md
+++ b/README.md
@@ -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/
\ No newline at end of file
diff --git a/src/components/terminal.tsx b/src/components/terminal.tsx
index 12225ae..4476e0f 100644
--- a/src/components/terminal.tsx
+++ b/src/components/terminal.tsx
@@ -23,7 +23,7 @@ export const Terminal: FC = async () => (
/>
-
+
diff --git a/src/css/main.css b/src/css/main.css
index 29cf059..978b269 100644
--- a/src/css/main.css
+++ b/src/css/main.css
@@ -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 {
diff --git a/src/css/terminal.css b/src/css/terminal.css
index eacd127..962c50a 100644
--- a/src/css/terminal.css
+++ b/src/css/terminal.css
@@ -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);
}
\ No newline at end of file
diff --git a/src/js/dom.ts b/src/js/dom.ts
index 7bc06af..1df502a 100644
--- a/src/js/dom.ts
+++ b/src/js/dom.ts
@@ -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
\ No newline at end of file
+// creates an HTML element, optinally with one or more classes
+// shortcut:
+// $$("span.input.green") =
+// $$(".input.green") =
+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
+}
\ No newline at end of file
diff --git a/src/js/input.ts b/src/js/input.ts
index 5d6eaff..7732f66 100644
--- a/src/js/input.ts
+++ b/src/js/input.ts
@@ -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"
}
\ No newline at end of file
diff --git a/src/js/scrollback.ts b/src/js/scrollback.ts
new file mode 100644
index 0000000..19aa871
--- /dev/null
+++ b/src/js/scrollback.ts
@@ -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)
+
+}
\ No newline at end of file
diff --git a/src/js/session.ts b/src/js/session.ts
new file mode 100644
index 0000000..7759eb9
--- /dev/null
+++ b/src/js/session.ts
@@ -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()
\ No newline at end of file
diff --git a/src/js/shell.ts b/src/js/shell.ts
index d0fe0cb..32cbf9a 100644
--- a/src/js/shell.ts
+++ b/src/js/shell.ts
@@ -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)
-}
\ No newline at end of file
+ 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")
+}
diff --git a/src/js/websocket.ts b/src/js/websocket.ts
index fe921ef..369b2f3 100644
--- a/src/js/websocket.ts
+++ b/src/js/websocket.ts
@@ -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')
-}
\ No newline at end of file
+}
+
+// register a local listener
+export function onMessage(callback: (e: MessageEvent) => void) {
+ if (!ws) return
+ ws.onmessage = callback
+}
diff --git a/src/server.tsx b/src/server.tsx
index bc13609..5d9eb22 100644
--- a/src/server.tsx
+++ b/src/server.tsx
@@ -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')
diff --git a/src/shared/message.ts b/src/shared/message.ts
index f8cfbd4..6d47721 100644
--- a/src/shared/message.ts
+++ b/src/shared/message.ts
@@ -1,6 +1,6 @@
export type Message = {
- session: string
+ session?: string
id: string
- type: "output"
+ type: "input" | "output"
data: any
}
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
new file mode 100644
index 0000000..886c02f
--- /dev/null
+++ b/src/shared/utils.ts
@@ -0,0 +1,3 @@
+export function randomID(): string {
+ return Math.random().toString(36).slice(7)
+}
\ No newline at end of file