Compare commits

...

5 Commits

Author SHA1 Message Date
f828384dba click to replay prompt 2025-09-29 22:36:08 -07:00
d99b4d53e1 better reconnect 2025-09-29 22:14:15 -07:00
877f888c96 games command 2025-09-29 22:08:39 -07:00
40b5bb3df3 cleaner imports 2025-09-29 22:06:34 -07:00
a9ea29287e cruisin 2025-09-29 21:50:27 -07:00
9 changed files with 65 additions and 13 deletions

View File

@ -52,10 +52,10 @@ https://wakamaifondue.com/
## Pluto Goals: Phase 2 ## Pluto Goals: Phase 2
- [ ] public tunnel for your NOSE webapps - [x] public tunnel for your NOSE webapps
- [ ] public tunnel lives through reboots - [x] public tunnel lives through reboots
- [ ] self updating NOSE server - [x] self updating NOSE server
- [ ] `pub/` static hosting in webapps - [x] `pub/` static hosting in webapps
- [ ] game/bin/www cartridges - [ ] game/bin/www cartridges
- [ ] upload files to projects - [ ] upload files to projects
- [ ] pico8-style games - [ ] pico8-style games

View File

@ -4,7 +4,7 @@
export const game = true export const game = true
import type { GameContext, InputState } from "@/shared/game" import type { GameContext, InputState } from "@/shared/game"
import { rng } from "@/shared/utils.ts" import { rng } from "@/shared/utils"
const WIDTH = 960 const WIDTH = 960
const HEIGHT = 540 const HEIGHT = 540

20
bin/games.tsx Normal file
View File

@ -0,0 +1,20 @@
// List all the games installed on the system.
import { readdirSync } from "fs"
import { join } from "path"
import { NOSE_SYS_BIN } from "@/config"
export default async function () {
let games = await Promise.all(readdirSync(NOSE_SYS_BIN, { withFileTypes: true }).map(async file => {
if (!file.isFile()) return
const code = await Bun.file(join(NOSE_SYS_BIN, file.name)).text()
if (/^export const game\s*=\s*true\s*;?\s*$/m.test(code))
return file.name.replace(".tsx", "").replace(".ts", "")
})).then(games => games.filter(file => file))
return <>
{games.map(game => <a href={`#${game}`}>{game}</a>)}
</>
}

View File

@ -2,7 +2,7 @@
export const game = true export const game = true
import type { GameContext, InputState } from "@/shared/game" import type { GameContext, InputState } from "@/shared/game"
import { rng } from "@/shared/utils.ts" import { rng } from "@/shared/utils"
const CELL = 20 const CELL = 20
const WIDTH = 30 const WIDTH = 30

View File

@ -7,7 +7,7 @@
export const game = true export const game = true
import type { GameContext, InputState } from "@/shared/game" import type { GameContext, InputState } from "@/shared/game"
import { randomElement, randomIndex } from "@/shared/utils.ts" import { randomElement, randomIndex } from "@/shared/utils"
const COLS = 10 const COLS = 10
const ROWS = 20 const ROWS = 20

View File

@ -7,6 +7,7 @@ import { initHistory } from "./history.js"
import { initHyperlink } from "./hyperlink.js" import { initHyperlink } from "./hyperlink.js"
import { initInput } from "./input.js" import { initInput } from "./input.js"
import { initResize } from "./resize.js" import { initResize } from "./resize.js"
import { initScrollback } from "./scrollback.js"
import { startVramCounter } from "./vram.js" import { startVramCounter } from "./vram.js"
import { startConnection } from "./websocket.js" import { startConnection } from "./websocket.js"
@ -19,6 +20,7 @@ initHistory()
initHyperlink() initHyperlink()
initInput() initInput()
initResize() initResize()
initScrollback()
startConnection() startConnection()
startVramCounter() startVramCounter()

View File

@ -2,12 +2,16 @@
// The scrollback shows your history of interacting with the shell. // The scrollback shows your history of interacting with the shell.
// input, output, etc // input, output, etc
import { scrollback, $$ } from "./dom.js"
import { randomId } from "../shared/utils.js"
import type { CommandOutput } from "../shared/types.js" import type { CommandOutput } from "../shared/types.js"
import { scrollback, cmdInput, $$ } from "./dom.js"
import { randomId } from "../shared/utils.js"
type InputStatus = "waiting" | "streaming" | "ok" | "error" type InputStatus = "waiting" | "streaming" | "ok" | "error"
export function initScrollback() {
window.addEventListener("click", handleInputClick)
}
export function autoScroll() { export function autoScroll() {
// requestAnimationFrame(() => scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight) // requestAnimationFrame(() => scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight)
// scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight // scrollback.scrollTop = scrollback.scrollHeight - scrollback.clientHeight
@ -63,10 +67,11 @@ export function addOutput(id: string, output: CommandOutput) {
item.textContent = content item.textContent = content
const input = document.querySelector(`[data-id="${id}"].input`) const input = document.querySelector(`[data-id="${id}"].input`)
if (input instanceof HTMLLIElement) if (input instanceof HTMLLIElement) {
input.parentNode!.insertBefore(item, input.nextSibling) input.parentNode!.insertBefore(item, input.nextSibling)
else } else {
insert(item) insert(item)
}
autoScroll() autoScroll()
} }
@ -135,3 +140,13 @@ function processOutput(output: CommandOutput): ["html" | "text", string] {
return [html ? "html" : "text", content] return [html ? "html" : "text", content]
} }
function handleInputClick(e: MouseEvent) {
const target = e.target
if (!(target instanceof HTMLElement)) return
if (target.matches(".input .content")) {
cmdInput.value = target.textContent
}
}

View File

@ -8,6 +8,8 @@ import { addErrorMessage } from "./scrollback.js"
const MAX_RETRIES = 5 const MAX_RETRIES = 5
let retries = 0 let retries = 0
let connected = false
let msgQueue: Message[] = []
let ws: WebSocket | null = null let ws: WebSocket | null = null
@ -20,10 +22,21 @@ export function startConnection() {
ws.onmessage = receive ws.onmessage = receive
ws.onclose = retryConnection ws.onclose = retryConnection
ws.onerror = () => ws?.close() ws.onerror = () => ws?.close()
ws.onopen = () => {
connected = true
msgQueue.forEach(msg => send(msg))
msgQueue.length = 0
}
} }
// send any message // send any message
export function send(msg: Message) { export function send(msg: Message) {
if (!connected) {
msgQueue.push(msg)
startConnection()
return
}
if (!msg.session) msg.session = sessionID 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) console.log("-> send", msg)
@ -41,6 +54,8 @@ export function close() {
} }
function retryConnection() { function retryConnection() {
connected = false
if (retries >= MAX_RETRIES) { if (retries >= MAX_RETRIES) {
addErrorMessage(`!! Failed to reconnect ${retries} times. Server is down.`) addErrorMessage(`!! Failed to reconnect ${retries} times. Server is down.`)
if (ws) ws.onclose = () => { } if (ws) ws.onclose = () => { }

View File

@ -42,10 +42,10 @@ app.use("*", async (c, next) => {
}) })
app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => { app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => {
const path = "./src/" + c.req.path.replace("..", ".") let path = "./src/" + c.req.path.replace("..", ".")
// path must end in .js or .ts // path must end in .js or .ts
if (!path.endsWith(".js") && !path.endsWith(".ts")) return c.text("File not found", 404) if (!path.endsWith(".js") && !path.endsWith(".ts")) path += ".ts"
const ts = path.replace(".js", ".ts") const ts = path.replace(".js", ".ts")
if (isFile(ts)) { if (isFile(ts)) {