diff --git a/nose/bin/cat.ts b/nose/bin/cat.ts index ccba640..c1e43e8 100644 --- a/nose/bin/cat.ts +++ b/nose/bin/cat.ts @@ -6,6 +6,7 @@ import type { CommandOutput } from "@/shared/types" import { NOSE_WWW } from "@/config" import { getState } from "@/state" import { appPath } from "@/webapp" +import { isBinaryFile } from "@/utils" import { highlight } from "../lib/highlight" export default async function (path: string) { @@ -50,7 +51,7 @@ async function readFile(path: string): Promise { html: `
${highlight(convertIndent(await file.text()))}
` } default: - if (await isBinaryContent(file)) + if (await isBinaryFile(path)) throw "Cannot display binary file" return { @@ -77,30 +78,3 @@ function convertIndent(str: string) { .join("\n") } - - -async function isBinaryContent(file: Bun.FileBlob): Promise { - // Create a stream to read just the beginning - const stream = file.stream() - const reader = stream.getReader() - - try { - // Read first chunk (typically 16KB, which is more than enough to detect binary) - const { value } = await reader.read() - if (!value) return false - - // Check first 512 bytes or less - const bytes = new Uint8Array(value) - const checkLength = Math.min(bytes.length, 512) - for (let i = 0; i < checkLength; i++) { - const byte = bytes[i]! - if (byte === 0) return true // null byte - if (byte < 32 && ![9, 10, 13].includes(byte)) return true // control char - } - - return false - } finally { - reader.releaseLock() - stream.cancel() - } -} diff --git a/nose/lib/highlight.ts b/nose/lib/highlight.ts index cc56d30..ba17356 100644 --- a/nose/lib/highlight.ts +++ b/nose/lib/highlight.ts @@ -1,4 +1,4 @@ -// simple-ts-highlighter.ts — regex-only, self-hostable +import { escapeHTML } from "bun" export type TokenType = | "string" | "number" | "keyword" | "boolean" | "null" | "undefined" @@ -110,17 +110,17 @@ export function tokenize(src: string): Program { function tokenToHTML(token: Token): string { switch (token.type) { - case "string": return `${escapeHtml(token.value)}` + case "string": return `${escapeHTML(token.value)}` case "number": return `${token.value}` case "keyword": return `${token.value}` - case "comment": return `${escapeHtml(token.value)}` + case "comment": return `${escapeHTML(token.value)}` case "null": case "undefined": case "boolean": return `${token.value}` case "punctuation": { // if (token.value === "(" || token.value === ")" || token.value === "{" || token.value === "}" || token.value === "[" || token.value === "]") // return `${token.value}` // else - return escapeHtml(token.value) + return escapeHTML(token.value) } case "identifier": { if (token.value[0]?.match(/[A-Z]/) || types.includes(token.value)) @@ -130,15 +130,7 @@ function tokenToHTML(token: Token): string { } case "whitespace": case "unknown": - return `${escapeHtml(token.value)}` + return `${escapeHTML(token.value)}` } } -export function escapeHtml(str: string): string { - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'") -} \ No newline at end of file diff --git a/src/components/terminal.tsx b/src/components/terminal.tsx index 092394e..3cc5b09 100644 --- a/src/components/terminal.tsx +++ b/src/components/terminal.tsx @@ -3,6 +3,7 @@ import type { FC } from "hono/jsx" export const Terminal: FC = async () => ( <> +
> diff --git a/src/css/editor.css b/src/css/editor.css new file mode 100644 index 0000000..30e0786 --- /dev/null +++ b/src/css/editor.css @@ -0,0 +1,4 @@ +.editor { + width: 100%; + background-color: var(--white); +} \ No newline at end of file diff --git a/src/js/editor.ts b/src/js/editor.ts new file mode 100644 index 0000000..1bfb870 --- /dev/null +++ b/src/js/editor.ts @@ -0,0 +1,27 @@ +import { scrollback } from "./dom.js" + +export function initEditor() { + document.addEventListener("input", adjustHeight) + focusTextareaOnCreation() +} + +function adjustHeight(e: Event) { + const target = e.target as HTMLElement + if (target?.matches(".editor")) { + target.style.height = "auto" + target.style.height = target.scrollHeight + "px" + } +} + +function focusTextareaOnCreation() { + const observer = new MutationObserver(mutations => { + 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() + return + } + }) + + observer.observe(scrollback, { childList: true }) +} \ No newline at end of file diff --git a/src/js/focus.ts b/src/js/focus.ts index b696441..985aafc 100644 --- a/src/js/focus.ts +++ b/src/js/focus.ts @@ -5,10 +5,10 @@ import { cmdInput } from "./dom.js" export function initFocus() { window.addEventListener("click", focusHandler) - focusTextbox() + focusInput() } -export function focusTextbox() { +export function focusInput() { cmdInput.focus() } @@ -19,7 +19,7 @@ export function focusHandler(e: MouseEvent) { // who knows where they clicked... just focus the textbox if (!(target instanceof HTMLElement)) { - cmdInput.focus() + focusInput() return } @@ -32,7 +32,7 @@ export function focusHandler(e: MouseEvent) { const selection = window.getSelection() || "" if (selection.toString() === "") - cmdInput.focus() + focusInput() e.preventDefault() return true diff --git a/src/js/main.ts b/src/js/main.ts index c61ab22..a1bff99 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -1,5 +1,6 @@ import { initCompletion } from "./completion.js" import { initCursor } from "./cursor.js" +import { initEditor } from "./editor.js" import { initFocus } from "./focus.js" import { initHistory } from "./history.js" import { initInput } from "./input.js" @@ -10,6 +11,7 @@ import { startConnection } from "./websocket.js" initCompletion() initCursor() initFocus() +initEditor() initHistory() initInput() initResize() diff --git a/src/utils.tsx b/src/utils.tsx index 27e39dc..a4ae6d1 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -35,6 +35,35 @@ export function isDir(path: string): boolean { } } +// is the given file binary? +export async function isBinaryFile(path: string): Promise { + // Create a stream to read just the beginning + const file = Bun.file(path) + const stream = file.stream() + const reader = stream.getReader() + + try { + // Read first chunk (typically 16KB, which is more than enough to detect binary) + const { value } = await reader.read() + if (!value) return false + + // Check first 512 bytes or less + const bytes = new Uint8Array(value) + const checkLength = Math.min(bytes.length, 512) + for (let i = 0; i < checkLength; i++) { + const byte = bytes[i]! + if (byte === 0) return true // null byte + if (byte < 32 && ![9, 10, 13].includes(byte)) return true // control char + } + + return false + } finally { + reader.releaseLock() + stream.cancel() + } +} + + // Convert /Users/$USER or /home/$USER to ~ for simplicity export function tilde(path: string): string { return path.replace(new RegExp(`/(Users|home)/${process.env.USER}`), "~")