file stuff
This commit is contained in:
parent
cd3c1de04b
commit
320a456fde
|
|
@ -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<CommandOutput> {
|
|||
html: `<div style='white-space: pre;'>${highlight(convertIndent(await file.text()))}</div>`
|
||||
}
|
||||
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<boolean> {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 `<span style="color: var(--red)">${escapeHtml(token.value)}</span>`
|
||||
case "string": return `<span style="color: var(--red)">${escapeHTML(token.value)}</span>`
|
||||
case "number": return `<span style="color: var(--yellow)">${token.value}</span>`
|
||||
case "keyword": return `<span style="color: var(--purple)">${token.value}</span>`
|
||||
case "comment": return `<span style="color: var(--magenta)">${escapeHtml(token.value)}</span>`
|
||||
case "comment": return `<span style="color: var(--magenta)">${escapeHTML(token.value)}</span>`
|
||||
case "null": case "undefined": case "boolean":
|
||||
return `<span style="color: var(--green)">${token.value}</span>`
|
||||
case "punctuation": {
|
||||
// if (token.value === "(" || token.value === ")" || token.value === "{" || token.value === "}" || token.value === "[" || token.value === "]")
|
||||
// return `<span style="color: var(--yellow)">${token.value}</span>`
|
||||
// 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, """)
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import type { FC } from "hono/jsx"
|
|||
export const Terminal: FC = async () => (
|
||||
<>
|
||||
<link rel="stylesheet" href="/css/terminal.css" />
|
||||
<link rel="stylesheet" href="/css/editor.css" />
|
||||
|
||||
<div id="command-line">
|
||||
<span id="command-prompt">></span>
|
||||
|
|
|
|||
4
src/css/editor.css
Normal file
4
src/css/editor.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.editor {
|
||||
width: 100%;
|
||||
background-color: var(--white);
|
||||
}
|
||||
27
src/js/editor.ts
Normal file
27
src/js/editor.ts
Normal file
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -35,6 +35,35 @@ export function isDir(path: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
// is the given file binary?
|
||||
export async function isBinaryFile(path: string): Promise<boolean> {
|
||||
// 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}`), "~")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user