Compare commits

...

7 Commits

Author SHA1 Message Date
Chris Wanstrath
907d8b5393 statusline rocks 2025-10-02 14:43:11 -07:00
Chris Wanstrath
7be77d5a69 glorious purpose 2025-10-02 14:38:50 -07:00
Chris Wanstrath
e384bce2ee narrow some Message types 2025-10-02 14:23:09 -07:00
Chris Wanstrath
fb657c5bb1 coming soon 2025-10-02 14:18:50 -07:00
Chris Wanstrath
93a9ce9c8c small cleanup 2025-10-02 14:10:00 -07:00
Chris Wanstrath
d8fd1ae771 no more scrollbars 2025-10-02 14:07:59 -07:00
Chris Wanstrath
3f9db13192 status line! 2025-10-02 14:07:55 -07:00
14 changed files with 142 additions and 20 deletions

View File

@ -6,12 +6,11 @@ import { join, extname } from "path"
import type { CommandOutput } from "@/shared/types"
import { isBinaryFile } from "@/utils"
import { projectName, projectDir } from "@/project"
import { projectDir } from "@/project"
import { sessionGet } from "@/session"
import { highlight } from "../lib/highlight"
export default async function (path: string) {
const project = projectName()
const root = sessionGet("cwd") || projectDir()
let files: string[] = []

View File

@ -10,7 +10,6 @@ import { projectName, projectDir } from "@/project"
import { sessionGet } from "@/session"
export default async function (path: string) {
const project = projectName()
const root = sessionGet("cwd") || projectDir()
let files: string[] = []

View File

@ -1,7 +1,7 @@
// Look around.
import { readdirSync } from "fs"
import { projectName, projectDir } from "@/project"
import { projectDir } from "@/project"
import { sessionGet } from "@/session"
export default function () {

View File

@ -80,6 +80,16 @@ a:visited {
color: var(--purple);
}
/* hide scrollbars, always */
* {
scrollbar-width: none;
-ms-overflow-style: none;
}
*::-webkit-scrollbar {
display: none;
}
html,
body {
font-family: var(--font-family);

View File

@ -160,4 +160,24 @@
#scrollback .output {
white-space: pre-wrap;
}
#statusline {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 2px 8px;
background: var(--c64-light-gray);
color: var(--c64-dark-blue);
font-size: 14px;
display: flex;
justify-content: space-between;
}
#statusline a {
color: inherit;
text-decoration: none;
}

View File

@ -1,8 +1,8 @@
////
// Dispatch Messages received via WebSocket
// Dispatch client->server Messages received via WebSocket
import { basename } from "path"
import type { Message } from "./shared/types"
import type { Message, InputMessage, SaveFileMessage } from "./shared/types"
import { runCommand } from "./shell"
import { send } from "./websocket"
import { setState } from "./state"
@ -11,10 +11,10 @@ export async function dispatchMessage(ws: any, msg: Message) {
console.log("<- receive", msg)
switch (msg.type) {
case "input":
await inputMessage(ws, msg); break
await inputMessage(ws, msg as InputMessage); break
case "save-file":
await saveFileMessage(ws, msg); break
await saveFileMessage(ws, msg as SaveFileMessage); break
case "ui:mode":
setState("ui:mode", msg.data); break
@ -24,8 +24,8 @@ export async function dispatchMessage(ws: any, msg: Message) {
}
}
async function inputMessage(ws: any, msg: Message) {
const result = await runCommand(msg.session || "", msg.id || "", msg.data as string, ws)
async function inputMessage(ws: any, msg: InputMessage) {
const result = await runCommand(msg.session, msg.id, msg.data as string, ws)
if (typeof result.output === "object" && "game" in result.output) {
send(ws, { id: msg.id, type: "game:start", data: result.output.game })
@ -34,7 +34,7 @@ async function inputMessage(ws: any, msg: Message) {
}
}
async function saveFileMessage(ws: any, msg: Message) {
async function saveFileMessage(ws: any, msg: SaveFileMessage) {
if (msg.id && typeof msg.data === "string") {
await Bun.write(msg.id.replace("..", ""), msg.data, { createPath: true })
send(ws, { type: "output", data: { status: "ok", output: `saved ${basename(msg.id)}` } })

View File

@ -34,5 +34,7 @@ export const Terminal: FC = async () => (
<li class="center">**** NOSE PLUTO V{new Date().getMonth() + 1}.{new Date().getDate()} ****</li>
<li class="center">VRAM <span id="vram-size">000KB</span></li>
</ul>
<div id="statusline"><div><a href="#projects" id="project-name">root</a>: <a href="#ls" id="project-cwd">/</a></div></div>
</>
)

View File

@ -38,5 +38,4 @@ export function cacheCommands(cmds: string[]) {
commands.push(...cmds)
commands.push(...Object.keys(browserCommands))
commands.sort()
console.log("CMDS", commands)
}

View File

@ -1,9 +1,12 @@
////
// Dispatch server->client Messages received via WebSocket
import type { Message } from "@/shared/types"
import { cacheCommands } from "./commands"
import { handleOutput } from "./scrollback"
import { handleStreamStart, handleStreamAppend, handleStreamReplace, handleStreamEnd } from "./stream"
import { handleGameStart } from "./game"
import { browserCommands } from "./commands"
import { handleSessionStart, handleSessionUpdate } from "./session"
// message received from server
export async function dispatchMessage(msg: Message) {
@ -24,8 +27,10 @@ export async function dispatchMessage(msg: Message) {
handleStreamReplace(msg); break
case "game:start":
await handleGameStart(msg); break
case "ui:mode":
browserCommands.mode?.(msg.data as string); break
case "session:start":
handleSessionStart(msg); break
case "session:update":
handleSessionUpdate(msg); break
default:
console.error("unknown message type", msg)
}

View File

@ -2,6 +2,47 @@
// Each browser tab is a shell session. This means you can run multiple sessions
// in the same browser.
import type { SessionStartMessage, SessionUpdateMessage } from "@/shared/types"
import { browserCommands } from "./commands"
import { randomId } from "../shared/utils"
import { $ } from "./dom"
export const sessionId = randomId()
export const sessionId = randomId()
export const projectName = $("project-name") as HTMLAnchorElement
export const projectCwd = $("project-cwd") as HTMLAnchorElement
export const sessionStore = new Map<string, string>()
export function handleSessionStart(msg: SessionStartMessage) {
sessionStore.set("NOSE_DIR", msg.data.NOSE_DIR)
updateProjectName(msg.data.project)
updateCwd(msg.data.cwd)
browserCommands.mode?.(msg.data.mode)
}
export function handleSessionUpdate(msg: SessionUpdateMessage) {
const data = msg.data as Record<string, string>
if (data.project)
updateProjectName(data.project)
if (data.cwd)
updateCwd(data.cwd)
}
function updateProjectName(project: string) {
sessionStore.set("project", project)
projectName.textContent = project
}
function updateCwd(cwd: string) {
cwd = displayProjectPath(cwd)
sessionStore.set("cwd", cwd)
projectCwd.textContent = cwd
}
function displayProjectPath(path: string): string {
let prefix = sessionStore.get("NOSE_DIR") || ""
prefix += "/" + sessionStore.get("project")
return path.replace(prefix, "") || "/"
}

View File

@ -29,7 +29,7 @@ export function projectDir(name = projectName()): string {
}
export function projectBin(name = projectName()): string {
return join(projectDir(), "bin")
return join(projectDir(name), "bin")
}
export function projectFiles(name = projectName()): Dirent[] {

View File

@ -7,7 +7,7 @@ import { prettyJSON } from "hono/pretty-json"
import color from "kleur"
import type { Message } from "./shared/types"
import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN } 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 { serveApp } from "./webapp"
import { commands, commandPath, loadCommandModule } from "./commands"
@ -137,8 +137,15 @@ app.get("/ws", c => {
addWebsocket(ws)
send(ws, { type: "commands", data: await commands() })
const mode = getState("ui:mode") || "tall"
send(ws, { type: "ui:mode", data: mode })
send(ws, {
type: "session:start",
data: {
NOSE_DIR: NOSE_DIR,
project: DEFAULT_PROJECT,
cwd: "/",
mode: getState("ui:mode") || "tall"
}
})
},
async onMessage(event, ws) {
let data: Message | undefined

View File

@ -2,6 +2,7 @@
// Session storage. 1 browser tab = 1 session
import { AsyncLocalStorage } from "async_hooks"
import { send } from "./websocket"
export type Session = {
taskId?: string
@ -35,6 +36,12 @@ export function sessionSet(key: keyof Session, value: any) {
const store = ALS.getStore()
if (!store) throw "sessionSet() called outside of ALS.run"
store[key] = value
if (!store.ws) return
send(store.ws, {
type: "session:update",
data: { [key]: value }
})
}
export function sessionStore(sessionId: string, taskId?: string, ws?: any): Session {

View File

@ -4,6 +4,10 @@ export type Message = {
type: MessageType
data?: CommandResult | CommandOutput
}
| InputMessage
| SaveFileMessage
| SessionStartMessage
| SessionUpdateMessage
export type MessageType = "error" | "input" | "output" | "commands" | "save-file"
| "game:start"
@ -20,3 +24,32 @@ export type CommandResult = {
status: "ok" | "error"
output: CommandOutput
}
export type InputMessage = {
type: "input"
id: string
session: string
data: CommandResult | CommandOutput
}
export type SaveFileMessage = {
type: "save-file"
id: string
session: string
data: CommandResult | CommandOutput
}
export type SessionStartMessage = {
type: "session:start"
data: {
NOSE_DIR: string
project: string
cwd: string
mode: string
}
}
export type SessionUpdateMessage = {
type: "session:update"
data: Record<string, string>
}