~/nose
This commit is contained in:
parent
027dd0be4e
commit
3762e26166
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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<string[]> {
|
||||
return (await findCommands(NOSE_SYS_BIN)).concat(await findCommands(NOSE_USR_BIN))
|
||||
}
|
||||
|
||||
export async function findCommands(path: string): Promise<string[]> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
||||
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")
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
11
src/shell.ts
11
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<CommandResult> {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<Response>
|
||||
export type App = Hono | Handler
|
||||
|
|
@ -23,34 +23,40 @@ export async function serveApp(c: Context, subdomain: string): Promise<Response>
|
|||
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<App | undefined> {
|
||||
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<App | undefined> {
|
||||
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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user