track apps in the front, link in statusbar

This commit is contained in:
Chris Wanstrath 2025-10-06 21:58:14 -07:00
parent d5d64b88c6
commit 1a97e3721c
9 changed files with 68 additions and 8 deletions

View File

@ -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}` }
}

View File

@ -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

View File

@ -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":

View File

@ -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()

View File

@ -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<string, string>()
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")

12
src/js/webapp.ts Normal file
View File

@ -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"))
}

View File

@ -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 {

View File

@ -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

View File

@ -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<Response>
export type App = Hono | Handler
export function initWebapps() {
startWatcher()
}
export async function serveApp(c: Context, subdomain: string): Promise<Response> {
const app = await findApp(subdomain)
const path = appDir(subdomain)
@ -94,3 +99,15 @@ function serveStatic(path: string): Response {
}
})
}
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() })
})
}