Compare commits

...

2 Commits

12 changed files with 80 additions and 7 deletions

View File

@ -2,6 +2,7 @@
import { projects } from "@/project" import { projects } from "@/project"
import { sessionGet, sessionSet } from "@/session" import { sessionGet, sessionSet } from "@/session"
import ls from "./ls"
export default function (project: string) { export default function (project: string) {
const state = sessionGet() const state = sessionGet()
@ -10,6 +11,7 @@ export default function (project: string) {
if (state && projects().includes(project)) { if (state && projects().includes(project)) {
sessionSet("project", project) sessionSet("project", project)
sessionSet("cwd", "") sessionSet("cwd", "")
return ls()
} else { } else {
return { error: `failed to load ${project}` } return { error: `failed to load ${project}` }
} }

View File

@ -72,7 +72,6 @@
// Listen for navigation commands from parent // Listen for navigation commands from parent
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
console.log(event)
if (event.data.type === 'NAV_COMMAND') { if (event.data.type === 'NAV_COMMAND') {
switch (event.data.action) { switch (event.data.action) {
case 'back': history.back(); break case 'back': history.back(); break

View File

@ -181,7 +181,8 @@
display: none; display: none;
} }
#statusbar.showing-msg .line-cwd { #statusbar.showing-msg .line-cwd,
#statusbar.showing-msg .line-www {
display: none; display: none;
} }

View File

@ -41,6 +41,7 @@ export const Terminal: FC = async () => (
<div id="statusbar"> <div id="statusbar">
<div class="line-cwd"><a href="#projects" id="project-name">root</a>: <a href="#ls" id="project-cwd">/</a></div> <div class="line-cwd"><a href="#projects" id="project-name">root</a>: <a href="#ls" id="project-cwd">/</a></div>
<div class="line-www"><a id="project-www" href="#">www</a></div>
<div class="line-msg"><span id="statusbar-msg"></span></div> <div class="line-msg"><span id="statusbar-msg"></span></div>
</div> </div>
</> </>

View File

@ -8,6 +8,7 @@ import { focusInput } from "./focus"
import { resize } from "./resize" import { resize } from "./resize"
import { sessionId } from "./session" import { sessionId } from "./session"
import { send } from "./websocket" import { send } from "./websocket"
import { status } from "./statusbar"
export const commands: string[] = [] export const commands: string[] = []
@ -30,6 +31,7 @@ export const browserCommands: Record<string, (...args: string[]) => void | Promi
resize() resize()
focusInput() focusInput()
}, },
status: (msg: string) => status(msg),
reload: () => window.location.reload(), reload: () => window.location.reload(),
} }

View File

@ -3,6 +3,7 @@
import type { Message } from "@/shared/types" import type { Message } from "@/shared/types"
import { cacheCommands } from "./commands" import { cacheCommands } from "./commands"
import { cacheApps } from "./webapp"
import { handleOutput } from "./scrollback" import { handleOutput } from "./scrollback"
import { handleStreamStart, handleStreamAppend, handleStreamReplace, handleStreamEnd } from "./stream" import { handleStreamStart, handleStreamAppend, handleStreamReplace, handleStreamEnd } from "./stream"
import { handleGameStart } from "./game" import { handleGameStart } from "./game"
@ -14,7 +15,9 @@ export async function dispatchMessage(msg: Message) {
case "output": case "output":
handleOutput(msg); break handleOutput(msg); break
case "commands": case "commands":
cacheCommands(msg.data as string[]); break cacheCommands(msg.data); break
case "apps":
cacheApps(msg.data); break
case "error": case "error":
console.error(msg.data); break console.error(msg.data); break
case "stream:start": case "stream:start":

View File

@ -10,6 +10,7 @@ import { initHyperlink } from "./hyperlink"
import { initInput } from "./input" import { initInput } from "./input"
import { initResize } from "./resize" import { initResize } from "./resize"
import { initScrollback } from "./scrollback" import { initScrollback } from "./scrollback"
import { initSession } from "./session"
import { startVramCounter } from "./vram" import { startVramCounter } from "./vram"
import { startConnection } from "./websocket" import { startConnection } from "./websocket"
@ -25,6 +26,7 @@ initHyperlink()
initInput() initInput()
initResize() initResize()
initScrollback() initScrollback()
initSession()
startConnection() startConnection()
startVramCounter() startVramCounter()

View File

@ -5,15 +5,24 @@
import type { SessionStartMessage, SessionUpdateMessage } from "@/shared/types" import type { SessionStartMessage, SessionUpdateMessage } from "@/shared/types"
import { browserCommands } from "./commands" import { browserCommands } from "./commands"
import { randomId } from "../shared/utils" import { randomId } from "../shared/utils"
import { apps } from "./webapp"
import { $ } from "./dom" import { $ } from "./dom"
export const sessionId = randomId() export const sessionId = randomId()
export const projectName = $("project-name") as HTMLAnchorElement export const projectName = $("project-name") as HTMLAnchorElement
export const projectCwd = $("project-cwd") as HTMLAnchorElement export const projectCwd = $("project-cwd") as HTMLAnchorElement
export const projectWww = $("project-www") as HTMLAnchorElement
export const sessionStore = new Map<string, string>() 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) { export function handleSessionStart(msg: SessionStartMessage) {
sessionStore.set("NOSE_DIR", msg.data.NOSE_DIR) sessionStore.set("NOSE_DIR", msg.data.NOSE_DIR)
sessionStore.set("hostname", msg.data.hostname)
updateProjectName(msg.data.project) updateProjectName(msg.data.project)
updateCwd(msg.data.cwd) updateCwd(msg.data.cwd)
browserCommands.mode?.(msg.data.mode) browserCommands.mode?.(msg.data.mode)
@ -32,6 +41,7 @@ export function handleSessionUpdate(msg: SessionUpdateMessage) {
function updateProjectName(project: string) { function updateProjectName(project: string) {
sessionStore.set("project", project) sessionStore.set("project", project)
projectName.textContent = project projectName.textContent = project
updateWww(project)
updateCwd("/") updateCwd("/")
} }
@ -41,6 +51,18 @@ function updateCwd(cwd: string) {
projectCwd.textContent = cwd 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 { function displayProjectPath(path: string): string {
let prefix = sessionStore.get("NOSE_DIR") || "" let prefix = sessionStore.get("NOSE_DIR") || ""
prefix += "/" + sessionStore.get("project") 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 type { Message } from "./shared/types"
import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN, DEFAULT_PROJECT } from "./config" import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN, DEFAULT_PROJECT } from "./config"
import { transpile, isFile, tilde, isDir } from "./utils" import { transpile, isFile, tilde, isDir } from "./utils"
import { serveApp } from "./webapp" import { serveApp, apps, initWebapps } from "./webapp"
import { commands, commandPath, loadCommandModule } from "./commands" import { commands, commandPath, loadCommandModule } from "./commands"
import { runCommandFn } from "./shell" import { runCommandFn } from "./shell"
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket" import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
@ -164,11 +164,14 @@ app.get("/", c => c.html(<Layout><Terminal /></Layout>))
app.get("/ws", c => { app.get("/ws", c => {
const _sessionId = c.req.query("session") const _sessionId = c.req.query("session")
const url = new URL(c.req.url)
let hostname = url.hostname + (url.port === "80" ? "" : `:${url.port}`)
return upgradeWebSocket(c, { return upgradeWebSocket(c, {
async onOpen(_e, ws) { async onOpen(_e, ws) {
addWebsocket(ws) addWebsocket(ws)
send(ws, { type: "commands", data: await commands() }) send(ws, { type: "commands", data: await commands() })
send(ws, { type: "apps", data: apps() })
send(ws, { send(ws, {
type: "session:start", type: "session:start",
@ -176,7 +179,8 @@ app.get("/ws", c => {
NOSE_DIR: NOSE_DIR, NOSE_DIR: NOSE_DIR,
project: DEFAULT_PROJECT, project: DEFAULT_PROJECT,
cwd: "/", cwd: "/",
mode: getState("ui:mode") || "tall" mode: getState("ui:mode") || "tall",
hostname
} }
}) })
}, },
@ -242,6 +246,7 @@ console.log(color.blue("NOSE_ROOT_BIN:"), color.yellow(tilde(NOSE_ROOT_BIN)))
await initNoseDir() await initNoseDir()
initCommands() initCommands()
initWebapps()
initSneakers() initSneakers()
export default { export default {

View File

@ -8,6 +8,7 @@ export type Message =
| GameStartMessage | GameStartMessage
| StreamMessage | StreamMessage
| CommandsMessage | CommandsMessage
| AppsMessage
export type CommandOutput = string | string[] export type CommandOutput = string | string[]
| { text: string, script?: string } | { text: string, script?: string }
@ -30,6 +31,11 @@ export type CommandsMessage = {
data: string[] data: string[]
} }
export type AppsMessage = {
type: "apps"
data: string[]
}
export type OutputMessage = { export type OutputMessage = {
type: "output" type: "output"
id?: string id?: string
@ -57,6 +63,7 @@ export type SessionStartMessage = {
project: string project: string
cwd: string cwd: string
mode: string mode: string
hostname: string
} }
} }

View File

@ -5,14 +5,19 @@ import type { Child } from "hono/jsx"
import { type Context, Hono } from "hono" import { type Context, Hono } from "hono"
import { renderToString } from "hono/jsx/dom/server" import { renderToString } from "hono/jsx/dom/server"
import { join } from "path" 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 { NOSE_DIR } from "./config"
import { isFile, isDir } from "./utils" import { isFile, isDir } from "./utils"
export type Handler = (r: Context) => string | Child | Response | Promise<Response> export type Handler = (r: Context) => string | Child | Response | Promise<Response>
export type App = Hono | Handler export type App = Hono | Handler
export function initWebapps() {
startWatcher()
}
export async function serveApp(c: Context, subdomain: string): Promise<Response> { export async function serveApp(c: Context, subdomain: string): Promise<Response> {
const app = await findApp(subdomain) const app = await findApp(subdomain)
const path = appDir(subdomain) const path = appDir(subdomain)
@ -93,4 +98,16 @@ function serveStatic(path: string): Response {
"Content-Type": file.type "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() })
})
} }