diff --git a/nose/bin/edit.ts b/nose/bin/edit.ts new file mode 100644 index 0000000..4768cc4 --- /dev/null +++ b/nose/bin/edit.ts @@ -0,0 +1,60 @@ +import { escapeHTML } from "bun" +import { readdirSync } from "fs" +import { join, extname } from "path" + +import type { CommandOutput } from "@/shared/types" +import { NOSE_WWW } from "@/config" +import { getState } from "@/state" +import { appPath } from "@/webapp" +import { isBinaryFile } from "@/utils" +import { countChar } from "@/shared/utils" + +export default async function (path: string) { + const state = getState() + if (!state) return { error: "no state" } + + const project = state.project + if (!project) return { error: "no project loaded" } + + const root = appPath(project) + if (!root) return { error: "error loading project" } + + let files: string[] = [] + + for (const file of readdirSync(root, { withFileTypes: true })) { + files.push(file.name) + } + + if (root === NOSE_WWW) { + files = files.filter(file => file.endsWith(`${project}.ts`) || file.endsWith(`${project}.tsx`)) + } + + if (!files.includes(path)) + return { error: `file not found: ${path}` } + + return await readFile(join(root, path)) +} + +async function readFile(path: string): Promise { + const ext = extname(path).slice(1) + const file = Bun.file(path) + + switch (ext) { + case "jpeg": case "jpg": case "png": case "gif": case "webp": + const img = await file.arrayBuffer() + return { html: `` } + case "mp3": case "wav": + case "mp4": case "mov": case "avi": case "mkv": case "webm": + return "Not implemented" + default: + if (await isBinaryFile(path)) + throw "Cannot display binary file" + const text = await file.text() + const rows = countChar(text, "\n") + 1 + return { + html: `` + } + } +} + + diff --git a/src/css/editor.css b/src/css/editor.css index 30e0786..604dd74 100644 --- a/src/css/editor.css +++ b/src/css/editor.css @@ -1,4 +1,9 @@ .editor { width: 100%; - background-color: var(--white); + background-color: transparent; + border: none; +} + +.editor:focus { + outline: none; } \ No newline at end of file diff --git a/src/js/editor.ts b/src/js/editor.ts index 1bfb870..46d2328 100644 --- a/src/js/editor.ts +++ b/src/js/editor.ts @@ -1,4 +1,8 @@ import { scrollback } from "./dom.js" +import { send } from "./websocket.js" +import { focusInput } from "./focus.js" + +const INDENT_SIZE = 2 export function initEditor() { document.addEventListener("input", adjustHeight) @@ -18,10 +22,54 @@ function focusTextareaOnCreation() { for (const m of mutations) for (const node of Array.from(m.addedNodes)) if (node instanceof HTMLElement && node.childNodes[0] instanceof HTMLElement && node.childNodes[0].matches("textarea")) { - node.childNodes[0].focus() + const editor = node.childNodes[0] + editor.focus() + editor.addEventListener("keydown", keydownHandler) return } }) observer.observe(scrollback, { childList: true }) +} + +function keydownHandler(e: KeyboardEvent) { + const target = e.target as HTMLTextAreaElement + if (e.key === "Tab") { + e.preventDefault() + if (e.shiftKey) + removeTab(target) + else + insertTab(target) + } else if (e.ctrlKey && e.key === "c") { + focusInput() + } else if ((e.ctrlKey && e.key === "s") || (e.ctrlKey && e.key === "Enter")) { + e.preventDefault() + send({ + id: target.dataset.path, + type: "save-file", + data: target.value + }) + } +} + +function insertTab(editor: HTMLTextAreaElement) { + const start = editor.selectionStart + const end = editor.selectionEnd + const lineStart = editor.value.lastIndexOf("\n", start - 1) + 1 + + editor.value = editor.value.slice(0, lineStart) + " " + editor.value.slice(lineStart) + editor.selectionStart = start + INDENT_SIZE + editor.selectionEnd = end + INDENT_SIZE +} + +function removeTab(editor: HTMLTextAreaElement) { + const start = editor.selectionStart + const end = editor.selectionEnd + const lineStart = editor.value.lastIndexOf("\n", start - 1) + 1 + + if (editor.value.slice(lineStart, lineStart + 2) === " ") { + editor.value = editor.value.slice(0, lineStart) + editor.value.slice(lineStart + 2) + editor.selectionStart = start - INDENT_SIZE + editor.selectionEnd = end - INDENT_SIZE + } } \ No newline at end of file diff --git a/src/js/shell.ts b/src/js/shell.ts index 5790d44..1be0e14 100644 --- a/src/js/shell.ts +++ b/src/js/shell.ts @@ -34,6 +34,8 @@ export function handleMessage(msg: Message) { handleOutput(msg) } else if (msg.type === "commands") { cacheCommands(msg.data as string[]) + } else if (msg.type === "error") { + console.error(msg.data) } else { console.error("unknown message type", msg) } diff --git a/src/server.tsx b/src/server.tsx index 081cc08..cb0c065 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -1,6 +1,7 @@ import { Hono } from "hono" import { serveStatic, upgradeWebSocket, websocket } from "hono/bun" import { prettyJSON } from "hono/pretty-json" +import { basename } from "path" import color from "kleur" import type { Message } from "./shared/types" @@ -73,8 +74,17 @@ app.get("/ws", upgradeWebSocket(async c => { if (!data) return - const result = await runCommand(data.session || "", data.id || "", data.data as string) - send(ws, { id: data.id, type: "output", data: result }) + if (data.type === "input") { + const result = await runCommand(data.session || "", data.id || "", data.data as string) + send(ws, { id: data.id, type: "output", data: result }) + } else if (data.type === "save-file") { + if (data.id && typeof data.data === "string" && isFile(data.id)) { + await Bun.write(data.id, data.data) + send(ws, { type: "output", data: { status: "ok", output: `saved ${basename(data.id)}` } }) + } + } else { + send(ws, { type: "error", data: `unknown message: ${data.type}` }) + } }, onClose: (event, ws) => { removeWebsocket(ws) diff --git a/src/shared/types.ts b/src/shared/types.ts index 76c66d8..3ae2236 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,10 +1,12 @@ export type Message = { session?: string id?: string - type: "input" | "output" | "commands" | "error" + type: MessageType data: CommandResult | string | string[] } +export type MessageType = "error" | "input" | "output" | "commands" | "save-file" + export type CommandOutput = string | { html: string } export type CommandResult = { diff --git a/src/shell.ts b/src/shell.ts index ad036c0..3225451 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -57,7 +57,7 @@ function processExecOutput(output: string | any): ["ok" | "error", CommandOutput function getState(session: string, id: string): State { let state = sessions.get(session) if (!state) { - state = { session, project: "" } + state = { session, project: "test" } sessions.set(session, state) } state.id = id