browser commands are now real commands! (again)

This commit is contained in:
Chris Wanstrath 2025-10-07 17:01:38 -07:00
parent 6d05b6888f
commit c872f6c4c4
11 changed files with 69 additions and 28 deletions

16
bin/browse.ts Normal file
View File

@ -0,0 +1,16 @@
/// <reference lib="dom" />
// Open a URL (or the current project) in the NOSEbrowser.
import { openBrowser } from "@/js/browser"
import { setStatus, latestId } from "@/js/scrollback"
import { currentAppUrl } from "@/js/webapp"
export default function (url?: string) {
const currentUrl = url ?? currentAppUrl()
if (currentUrl) {
openBrowser(currentUrl, "command")
} else {
setTimeout(() => setStatus(latestId()!, "error"), 0)
return "usage: browse <url>"
}
}

6
bin/browser-session.ts Normal file
View File

@ -0,0 +1,6 @@
/// <reference lib="dom" />
// Print the browser's session ID
import { sessionId } from "@/js/session"
export default () => sessionId

8
bin/clear.ts Normal file
View File

@ -0,0 +1,8 @@
/// <reference lib="dom" />
// Clear the scrollback history.
import { scrollback } from "@/js/dom"
export default function () {
scrollback.innerHTML = ""
}

8
bin/commands.ts Normal file
View File

@ -0,0 +1,8 @@
/// <reference lib="dom" />
// Print all the commands.
import { commands } from "@/js/commands"
export default function () {
return { html: "<div>" + Object.keys(commands).map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" }
}

4
bin/fullscreen.ts Normal file
View File

@ -0,0 +1,4 @@
/// <reference lib="dom" />
// Toggle fullscreen mode.
export default () => { document.body.requestFullscreen() }

4
bin/reload.ts Normal file
View File

@ -0,0 +1,4 @@
/// <reference lib="dom" />
// Reload the browser window.
export default () => { window.location.reload() }

6
bin/status.ts Normal file
View File

@ -0,0 +1,6 @@
/// <reference lib="dom" />
// Display a status message in the status bar.
import { status } from "@/js/statusbar"
export default (msg: string) => { status(msg) }

View File

@ -43,7 +43,7 @@ export async function findCommands(path: string): Promise<Commands> {
function describeCommand(path: string): Command {
const code = readFileSync(path, "utf8")
let game = /^export const game = true$/mg.test(code)
let browser = /^\/\/\/ ?<reference lib=['"]dom['"]\s*\/mg>$/.test(code)
let browser = /^\/\/\/ ?<reference lib=['"]dom['"]\s*\/>$/mg.test(code)
return {
name: basename(path).replace(/\.tsx?$/, ""),

View File

@ -1,35 +1,16 @@
////
// temporary hack for browser commands
import type { CommandOutput, Commands, Command } from "../shared/types"
import { openBrowser } from "./browser"
import { scrollback, content } from "./dom"
import type { CommandOutput, Commands } from "../shared/types"
import { content } from "./dom"
import { focusInput } from "./focus"
import { resize } from "./resize"
import { sessionId } from "./session"
import { send } from "./websocket"
import { status } from "./statusbar"
import { setStatus, latestId } from "./scrollback"
import { currentAppUrl } from "./webapp"
export const commands: Commands = {}
// TODO: convert to bin/shell "browser" commands
export const browserCommands: Record<string, (...args: string[]) => void | Promise<void> | CommandOutput> = {
browse: (url?: string) => {
const currentUrl = url ?? currentAppUrl()
if (currentUrl) {
openBrowser(currentUrl, "command")
} else {
setTimeout(() => setStatus(latestId()!, "error"), 0)
return "usage: browse <url>"
}
},
"browser-session": () => sessionId,
clear: () => scrollback.innerHTML = "",
commands: () => {
return { html: "<div>" + Object.keys(commands).map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" }
},
fullscreen: () => document.body.requestFullscreen(),
mode: (mode?: string) => {
if (!mode) {
mode = document.body.dataset.mode === "tall" ? "cinema" : "tall"
@ -41,8 +22,6 @@ export const browserCommands: Record<string, (...args: string[]) => void | Promi
resize()
focusInput()
},
status: (msg: string) => status(msg),
reload: () => window.location.reload(),
}
export function cacheCommands(cmds: Commands) {

View File

@ -6,7 +6,7 @@ import { addInput, setStatus, addOutput } from "./scrollback"
import { send } from "./websocket"
import { randomId } from "../shared/utils"
import { addToHistory } from "./history"
import { browserCommands } from "./commands"
import { browserCommands, commands } from "./commands"
export async function runCommand(input: string) {
if (!input.trim()) return
@ -23,7 +23,12 @@ export async function runCommand(input: string) {
const [cmd = "", ...args] = input.split(" ")
if (browserCommands[cmd]) {
if (commands[cmd]?.type === "browser") {
const mod = await import(`/source/${cmd}?t=${Date.now()}`)
const result = mod.default(...args)
if (result) addOutput(id, result)
setStatus(id, "ok")
} else if (browserCommands[cmd]) {
const result = await browserCommands[cmd](...args)
if (result) addOutput(id, result)
setStatus(id, "ok")

View File

@ -133,7 +133,12 @@ app.get("/source/:name", async c => {
const sessionId = c.req.query("session") || "0"
const path = await sessionRun(sessionId, () => commandPath(name))
if (!path) return c.text("Command not found", 404)
return new Response(await transpile(path), {
let code = await transpile(path)
// rewrite imports so they work on the server and in the browser
code = code.replace(/from (["'])@\/js\//g, "from $1../js/")
return new Response(code, {
headers: {
"Content-Type": "text/javascript"
}