save files
This commit is contained in:
parent
4558c157d4
commit
a8b1dc91a3
60
nose/bin/edit.ts
Normal file
60
nose/bin/edit.ts
Normal file
|
|
@ -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<CommandOutput> {
|
||||
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: `<img src="data:image/${ext};base64,${Buffer.from(img).toString('base64')}" />` }
|
||||
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: `<textarea class="editor" spellcheck="false" rows=${rows} data-path="${path}" >${text}</textarea>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,4 +1,9 @@
|
|||
.editor {
|
||||
width: 100%;
|
||||
background-color: var(--white);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.editor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user