From 1a97e3721c4e5da664fe44041174b33d567262f3 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 6 Oct 2025 21:58:14 -0700 Subject: [PATCH] track apps in the front, link in statusbar --- bin/load.ts | 2 ++ public/browser-nav.js | 1 - src/js/dispatch.ts | 5 ++++- src/js/main.ts | 2 ++ src/js/session.ts | 23 ++++++++++++++++++++--- src/js/webapp.ts | 12 ++++++++++++ src/server.tsx | 4 +++- src/shared/types.ts | 6 ++++++ src/webapp.ts | 21 +++++++++++++++++++-- 9 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 src/js/webapp.ts diff --git a/bin/load.ts b/bin/load.ts index bff4e68..8869a24 100644 --- a/bin/load.ts +++ b/bin/load.ts @@ -2,6 +2,7 @@ import { projects } from "@/project" import { sessionGet, sessionSet } from "@/session" +import ls from "./ls" export default function (project: string) { const state = sessionGet() @@ -10,6 +11,7 @@ export default function (project: string) { if (state && projects().includes(project)) { sessionSet("project", project) sessionSet("cwd", "") + return ls() } else { return { error: `failed to load ${project}` } } diff --git a/public/browser-nav.js b/public/browser-nav.js index 5b01da8..c7db605 100644 --- a/public/browser-nav.js +++ b/public/browser-nav.js @@ -72,7 +72,6 @@ // Listen for navigation commands from parent window.addEventListener('message', (event) => { - console.log(event) if (event.data.type === 'NAV_COMMAND') { switch (event.data.action) { case 'back': history.back(); break diff --git a/src/js/dispatch.ts b/src/js/dispatch.ts index 7e5360d..ef31426 100644 --- a/src/js/dispatch.ts +++ b/src/js/dispatch.ts @@ -3,6 +3,7 @@ import type { Message } from "@/shared/types" import { cacheCommands } from "./commands" +import { cacheApps } from "./webapp" import { handleOutput } from "./scrollback" import { handleStreamStart, handleStreamAppend, handleStreamReplace, handleStreamEnd } from "./stream" import { handleGameStart } from "./game" @@ -14,7 +15,9 @@ export async function dispatchMessage(msg: Message) { case "output": handleOutput(msg); break case "commands": - cacheCommands(msg.data as string[]); break + cacheCommands(msg.data); break + case "apps": + cacheApps(msg.data); break case "error": console.error(msg.data); break case "stream:start": diff --git a/src/js/main.ts b/src/js/main.ts index 51a062d..6429c66 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -10,6 +10,7 @@ import { initHyperlink } from "./hyperlink" import { initInput } from "./input" import { initResize } from "./resize" import { initScrollback } from "./scrollback" +import { initSession } from "./session" import { startVramCounter } from "./vram" import { startConnection } from "./websocket" @@ -25,6 +26,7 @@ initHyperlink() initInput() initResize() initScrollback() +initSession() startConnection() startVramCounter() \ No newline at end of file diff --git a/src/js/session.ts b/src/js/session.ts index 4f2b1f0..b9e86cb 100644 --- a/src/js/session.ts +++ b/src/js/session.ts @@ -5,6 +5,7 @@ import type { SessionStartMessage, SessionUpdateMessage } from "@/shared/types" import { browserCommands } from "./commands" import { randomId } from "../shared/utils" +import { apps } from "./webapp" import { $ } from "./dom" export const sessionId = randomId() @@ -13,6 +14,12 @@ export const projectCwd = $("project-cwd") as HTMLAnchorElement export const projectWww = $("project-www") as HTMLAnchorElement export const sessionStore = new Map() +export function initSession() { + window.addEventListener("apps:change", e => + updateWww(sessionStore.get("project") || "root") + ) +} + export function handleSessionStart(msg: SessionStartMessage) { sessionStore.set("NOSE_DIR", msg.data.NOSE_DIR) sessionStore.set("hostname", msg.data.hostname) @@ -34,9 +41,7 @@ export function handleSessionUpdate(msg: SessionUpdateMessage) { function updateProjectName(project: string) { sessionStore.set("project", project) projectName.textContent = project - const hostname = sessionStore.get("hostname") || "localhost" - const s = hostname.startsWith("localhost") ? "" : "s" - projectWww.href = `http${s}://${project}.${hostname}` + updateWww(project) updateCwd("/") } @@ -46,6 +51,18 @@ function updateCwd(cwd: string) { projectCwd.textContent = cwd } +function updateWww(project: string) { + if (!apps.includes(project)) { + projectWww.style.display = "none" + return + } + + projectWww.style.display = "" + const hostname = sessionStore.get("hostname") || "localhost" + const s = hostname.startsWith("localhost") ? "" : "s" + projectWww.href = `http${s}://${project}.${hostname}` +} + function displayProjectPath(path: string): string { let prefix = sessionStore.get("NOSE_DIR") || "" prefix += "/" + sessionStore.get("project") diff --git a/src/js/webapp.ts b/src/js/webapp.ts new file mode 100644 index 0000000..cb1494d --- /dev/null +++ b/src/js/webapp.ts @@ -0,0 +1,12 @@ +//// +// NOSE webapps + +export const apps: string[] = [] + +export function cacheApps(a: string[]) { + apps.length = 0 + apps.unshift(...a) + apps.sort() + + window.dispatchEvent(new CustomEvent("apps:change")) +} \ No newline at end of file diff --git a/src/server.tsx b/src/server.tsx index d931ecc..0ec1f36 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -9,7 +9,7 @@ import color from "kleur" import type { Message } from "./shared/types" import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN, DEFAULT_PROJECT } from "./config" import { transpile, isFile, tilde, isDir } from "./utils" -import { serveApp } from "./webapp" +import { serveApp, apps, initWebapps } from "./webapp" import { commands, commandPath, loadCommandModule } from "./commands" import { runCommandFn } from "./shell" import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket" @@ -171,6 +171,7 @@ app.get("/ws", c => { async onOpen(_e, ws) { addWebsocket(ws) send(ws, { type: "commands", data: await commands() }) + send(ws, { type: "apps", data: apps() }) send(ws, { type: "session:start", @@ -245,6 +246,7 @@ console.log(color.blue("NOSE_ROOT_BIN:"), color.yellow(tilde(NOSE_ROOT_BIN))) await initNoseDir() initCommands() +initWebapps() initSneakers() export default { diff --git a/src/shared/types.ts b/src/shared/types.ts index bd5a51d..7b02c04 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -8,6 +8,7 @@ export type Message = | GameStartMessage | StreamMessage | CommandsMessage + | AppsMessage export type CommandOutput = string | string[] | { text: string, script?: string } @@ -30,6 +31,11 @@ export type CommandsMessage = { data: string[] } +export type AppsMessage = { + type: "apps" + data: string[] +} + export type OutputMessage = { type: "output" id?: string diff --git a/src/webapp.ts b/src/webapp.ts index cf0d1f5..8ef2635 100644 --- a/src/webapp.ts +++ b/src/webapp.ts @@ -5,14 +5,19 @@ import type { Child } from "hono/jsx" import { type Context, Hono } from "hono" import { renderToString } from "hono/jsx/dom/server" import { join } from "path" -import { readdirSync } from "fs" - +import { readdirSync, watch } from "fs" +import { sendAll } from "./websocket" +import { expectDir } from "./utils" import { NOSE_DIR } from "./config" import { isFile, isDir } from "./utils" export type Handler = (r: Context) => string | Child | Response | Promise export type App = Hono | Handler +export function initWebapps() { + startWatcher() +} + export async function serveApp(c: Context, subdomain: string): Promise { const app = await findApp(subdomain) const path = appDir(subdomain) @@ -93,4 +98,16 @@ function serveStatic(path: string): Response { "Content-Type": file.type } }) +} + +let wwwWatcher +function startWatcher() { + if (!expectDir(NOSE_DIR)) return + + wwwWatcher = watch(NOSE_DIR, { recursive: true }, async (event, filename) => { + if (!filename) return + + if (/^.+\/index\.tsx?$/.test(filename)) + sendAll({ type: "apps", data: apps() }) + }) } \ No newline at end of file