diff --git a/README.md b/README.md index f5c09ba..bbca3ed 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ - [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 +## Local Dev + + bun install + mkdir -p ~/nose/{bin,www} + bun dev + ## Fonts Use this to examine what's inside the C64 .woff2 font file in public/vendor: diff --git a/src/commands.ts b/src/commands.ts index 32df06a..e7e2eba 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,18 +1,28 @@ import { Glob } from "bun" import { watch } from "fs" -import { NOSE_BIN } from "./config" +import { NOSE_SYS_BIN, NOSE_USR_BIN } from "./config" import { sendAll } from "./websocket" +import { expectDir } from "./utils" -const cmdWatcher = watch(NOSE_BIN, async (event, filename) => { +const sysCmdWatcher = watch(NOSE_SYS_BIN, async (event, filename) => + sendAll({ type: "commands", data: await commands() }) +) + +expectDir(NOSE_USR_BIN) +const usrCmdWatcher = watch(NOSE_USR_BIN, async (event, filename) => { sendAll({ type: "commands", data: await commands() }) }) export async function commands(): Promise { + return (await findCommands(NOSE_SYS_BIN)).concat(await findCommands(NOSE_USR_BIN)) +} + +export async function findCommands(path: string): Promise { const glob = new Glob("**/*.{ts,tsx}") let list: string[] = [] - for await (const file of glob.scan(NOSE_BIN)) { - list.push(file.replace(".ts", "")) + for await (const file of glob.scan(path)) { + list.push(file.replace(".ts", "").replace(".tsx", "")) } return list diff --git a/src/config.ts b/src/config.ts index 8224b03..f7d677e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,12 @@ import { resolve, join } from "node:path" export const NOSE_ICON = ` ͡° ͜ʖ ͡°` -export const NOSE_DIR = resolve("./nose") -export const NOSE_BIN = resolve(join(NOSE_DIR, "bin")) -export const NOSE_WWW = resolve(join(NOSE_DIR, "www")) \ No newline at end of file + +export const NOSE_SYS = resolve("./nose") +export const NOSE_SYS_BIN = join(NOSE_SYS, "bin") +export const NOSE_SYS_WWW = join(NOSE_SYS, "www") + +const homedir = process.platform === "darwin" ? `/Users/${process.env.USER}` : `/home/${process.env.USER}` +export const NOSE_USR = resolve(join(homedir, "nose")) +export const NOSE_USR_BIN = join(NOSE_USR, "bin") +export const NOSE_USR_WWW = join(NOSE_USR, "www") \ No newline at end of file diff --git a/src/server.tsx b/src/server.tsx index 33bab2b..9b41cc3 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -4,7 +4,7 @@ import { prettyJSON } from "hono/pretty-json" import color from "kleur" import type { Message } from "./shared/types" -import { NOSE_ICON, NOSE_DIR, NOSE_BIN, NOSE_WWW } from "./config" +import { NOSE_ICON, NOSE_USR_BIN, NOSE_USR_WWW } from "./config" import { transpile, isFile, tilde } from "./utils" import { apps, serveApp, publishDNS } from "./webapp" import { runCommand } from "./shell" @@ -149,9 +149,8 @@ if (process.env.BUN_HOT) { // console.log(color.cyan(NOSE_ICON)) -console.log(color.blue("NOSE_DIR:"), color.yellow(tilde(NOSE_DIR))) -console.log(color.blue("NOSE_BIN:"), color.yellow(tilde(NOSE_BIN))) -console.log(color.blue("NOSE_WWW:"), color.yellow(tilde(NOSE_WWW))) +console.log(color.blue("NOSE_USR_BIN:"), color.yellow(tilde(NOSE_USR_BIN))) +console.log(color.blue("NOSE_USR_WWW:"), color.yellow(tilde(NOSE_USR_WWW))) export default { port: process.env.PORT || 3000, diff --git a/src/shell.ts b/src/shell.ts index 6a95dd5..4ef102f 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -3,7 +3,7 @@ import type { CommandResult } from "./shared/types" import { join } from "path" -import { NOSE_BIN } from "./config" +import { NOSE_SYS_BIN, NOSE_USR_BIN } from "./config" import { isFile } from "./utils" export async function runCommand(input: string): Promise { @@ -36,12 +36,15 @@ async function exec(cmd: string, args: string[]): Promise<["ok" | "error", strin return ["ok", await module.default(...args)] } -function commandPath(cmd: string): string { - return join(NOSE_BIN, cmd + ".ts") +function commandPath(cmd: string): string | undefined { + const sysPath = join(NOSE_SYS_BIN, cmd + ".ts") + const usrPath = join(NOSE_USR_BIN, cmd + ".ts") + + return (isFile(sysPath) && sysPath) || (isFile(usrPath) && usrPath) || undefined } function commandExists(cmd: string): boolean { - return isFile(commandPath(cmd)) + return commandPath(cmd) !== undefined } function errorMessage(error: Error | any): string { diff --git a/src/utils.tsx b/src/utils.tsx index 4f74b90..27e39dc 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -1,7 +1,19 @@ import { Hono } from "hono" -import { statSync } from "node:fs" +import { statSync } from "fs" +import { basename } from "path" import { stat } from "node:fs/promises" import { type Handler, toResponse } from "./webapp" +import { NOSE_ICON } from "./config" + +// End the process with an instructive error if a directory doesn't exist. +export function expectDir(path: string) { + if (!isDir(path)) { + console.error(NOSE_ICON) + console.error(`No ${basename(path)} directory detected. Please run:`) + console.error("\tmkdir -p", path) + process.exit(1) + } +} // Is the given `path` a file? export function isFile(path: string): boolean { diff --git a/src/webapp.ts b/src/webapp.ts index eb4c507..8637a22 100644 --- a/src/webapp.ts +++ b/src/webapp.ts @@ -3,8 +3,8 @@ import type { Child } from "hono/jsx" import { renderToString } from "hono/jsx/dom/server" import { join } from "path" import { readdirSync, watch } from "fs" -import { NOSE_WWW } from "./config" -import { isFile } from "./utils" +import { NOSE_USR_WWW, NOSE_SYS_WWW } from "./config" +import { expectDir } from "./utils" export type Handler = (r: Context) => string | Child | Response | Promise export type App = Hono | Handler @@ -23,34 +23,40 @@ export async function serveApp(c: Context, subdomain: string): Promise export function apps(): string[] { const apps: string[] = [] - for (const entry of readdirSync(NOSE_WWW)) + for (const entry of readdirSync(NOSE_SYS_WWW)) + apps.push(entry.replace(/\.tsx?/, "")) + + for (const entry of readdirSync(NOSE_USR_WWW)) apps.push(entry.replace(/\.tsx?/, "")) return apps } async function findApp(name: string): Promise { - let path = join(NOSE_WWW, `${name}.ts`) - let app = await loadApp(path) - if (app) return app + const paths = [ + `${name}.ts`, + `${name}.tsx`, + join(name, "index.ts"), + join(name, "index.tsx") + ] - path = join(NOSE_WWW, `${name}.tsx`) - app = await loadApp(path) - if (app) return app + let app - path = join(NOSE_WWW, name, "index.ts") - app = await loadApp(path) - if (app) return app + for (const path in paths) { + app = loadApp(join(NOSE_SYS_WWW, path)) + if (app) return app + } - path = join(NOSE_WWW, name, "index.tsx") - app = await loadApp(path) - if (app) return app + for (const path in paths) { + app = loadApp(join(NOSE_USR_WWW, path)) + if (app) return app + } console.error("can't find app:", name) } async function loadApp(path: string): Promise { - if (!isFile(path)) return + if (!await Bun.file(path).exists()) return const mod = await import(path + `?t=${Date.now()}`) if (mod?.default) @@ -104,7 +110,20 @@ function publishAppDNS(app: string) { return dnsEntries[app] } -const appWatcher = watch(NOSE_WWW, (event, filename) => { +const sysAppWatcher = watch(NOSE_SYS_WWW, (event, filename) => { + const www = apps() + www.forEach(publishAppDNS) + for (const name in dnsEntries) + if (!www.includes(name)) { + dnsEntries[name].kill("SIGTERM") + delete dnsEntries[name] + } +}) + +// exit process with error if no WWW dir +expectDir(NOSE_USR_WWW) + +const usrAppWatcher = watch(NOSE_USR_WWW, (event, filename) => { const www = apps() www.forEach(publishAppDNS) for (const name in dnsEntries)