diff --git a/README.md b/README.md index c05c319..2781f65 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Scroll Making a text editor in Typescript based on Kilo (Script + Kilo = Scroll). This -runs on [Bun](https://bun.sh/) and [Deno](https://deno.com/). +runs on [Bun](https://bun.sh/) (v1.0 or later) and [Deno](https://deno.com/) +(v1.37 or later). -To simplify running, I'm using [Just](https://github.com/casey/just) +To simplify running, I'm using [Just](https://github.com/casey/just). -- Bun: `just bun-run` -- Deno: `just deno-run` +- Bun: `just bun-run [filename]` +- Deno: `just deno-run [filename]` ## Development Notes diff --git a/src/bun/ffi.ts b/src/bun/ffi.ts index 9bbe5bd..46d6d9f 100644 --- a/src/bun/ffi.ts +++ b/src/bun/ffi.ts @@ -2,7 +2,7 @@ * This is all the nasty ffi setup for the bun runtime */ import { dlopen, ptr, suffix } from 'bun:ffi'; -import { IFFI } from '../common/types.ts'; +import { IFFI } from '../common/runtime.ts'; const getLib = (name: string) => { return dlopen( diff --git a/src/bun/file_io.ts b/src/bun/file_io.ts index 61fc9cc..6415ddd 100644 --- a/src/bun/file_io.ts +++ b/src/bun/file_io.ts @@ -1,6 +1,7 @@ -import { IFIO } from '../common/types.ts'; +import { IFIO } from '../common/runtime.ts'; import { readFileSync } from 'node:fs'; +import { appendFile } from 'node:fs/promises'; const BunFileIO: IFIO = { openFile: async (path: string): Promise => { @@ -10,6 +11,9 @@ const BunFileIO: IFIO = { openFileSync: (path: string): string => { return readFileSync(path).toString(); }, + appendFile: async function (path: string, contents: string): Promise { + await appendFile(path, contents); + }, }; export default BunFileIO; diff --git a/src/bun/mod.ts b/src/bun/mod.ts index 46fddc8..57ecd0e 100644 --- a/src/bun/mod.ts +++ b/src/bun/mod.ts @@ -2,22 +2,18 @@ * The main entrypoint when using Bun as the runtime */ -import { getTermios, IRuntime, RunTimeType } from '../common/mod.ts'; +import { IRuntime, RunTimeType } from '../common/mod.ts'; import BunFFI from './ffi.ts'; import BunTerminalIO from './terminal_io.ts'; import BunFileIO from './file_io.ts'; -process.on('error', async (e) => { - (await getTermios()).disableRawMode(); - console.error(e); - process.exit(); -}); - const BunRuntime: IRuntime = { name: RunTimeType.Bun, file: BunFileIO, ffi: BunFFI, term: BunTerminalIO, + onEvent: (eventName: string, handler: (e: Event) => void) => + process.on(eventName, handler), onExit: (cb: () => void): void => { process.on('beforeExit', cb); process.on('exit', cb); diff --git a/src/common/ansi.ts b/src/common/ansi.ts index 2034d6d..7698c36 100644 --- a/src/common/ansi.ts +++ b/src/common/ansi.ts @@ -1,3 +1,7 @@ +/** + * ANSI/VT terminal escape code handling + */ + export const ANSI_PREFIX = '\x1b['; function esc(pieces: TemplateStringsArray): string { @@ -39,4 +43,33 @@ export const Ansi = { moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`, }; +const decoder = new TextDecoder(); + +export function readKey(raw: Uint8Array): string { + const parsed = decoder.decode(raw); + + // Return the input if it's unambiguous + if (parsed in KeyCommand) { + return parsed; + } + + // Some keycodes have multiple potential inputs + switch (parsed) { + case '\x1bOH': + case '\x1b[7~': + case '\x1b[1~': + case '\x1b[H': + return KeyCommand.Home; + + case '\x1bOF': + case '\x1b[8~': + case '\x1b[4~': + case '\x1b[F': + return KeyCommand.End; + + default: + return parsed; + } +} + export default Ansi; diff --git a/src/common/document.ts b/src/common/document.ts index 5175fc9..b92e0de 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -20,7 +20,7 @@ export class Document { this.#rows = []; } - get numrows(): number { + get numRows(): number { return this.#rows.length; } diff --git a/src/common/editor.ts b/src/common/editor.ts index d76b462..7131233 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -142,7 +142,7 @@ export class Editor { private drawRows(): void { for (let y = 0; y < this.#screen.rows; y++) { - if (this.#document.numrows < y) { + if (this.#document.numRows < y) { this.drawPlaceholderRow(y); } else { this.drawFileRow(y); diff --git a/src/common/main.ts b/src/common/main.ts index deddf96..f7af2d7 100644 --- a/src/common/main.ts +++ b/src/common/main.ts @@ -1,40 +1,11 @@ -import { KeyCommand } from './ansi.ts'; +import { readKey } from './ansi.ts'; import { getRuntime } from './runtime.ts'; import { getTermios } from './termios.ts'; import { Editor } from './editor.ts'; -const decoder = new TextDecoder(); - -function readKey(raw: Uint8Array): string { - const parsed = decoder.decode(raw); - - // Return the input if it's unambiguous - if (parsed in KeyCommand) { - return parsed; - } - - // Some keycodes have multiple potential inputs - switch (parsed) { - case '\x1bOH': - case '\x1b[7~': - case '\x1b[1~': - case '\x1b[H': - return KeyCommand.Home; - - case '\x1bOF': - case '\x1b[8~': - case '\x1b[4~': - case '\x1b[F': - return KeyCommand.End; - - default: - return parsed; - } -} - export async function main() { const runTime = await getRuntime(); - const { term, onExit } = runTime; + const { term, file, onExit, onEvent } = runTime; // Setup raw mode, and tear down on error or normal exit const t = await getTermios(); @@ -43,6 +14,16 @@ export async function main() { t.disableRawMode(); }); + // Setup error handler to log to file + onEvent('error', (error: Event) => { + t.disableRawMode(); + error.preventDefault(); + error.stopPropagation(); + file.appendFile('scroll.err', JSON.stringify(error, null, 2)).then( + () => {}, + ); + }); + const terminalSize = await term.getTerminalSize(); // Create the editor itself @@ -53,6 +34,8 @@ export async function main() { await editor.open(filename); } } + + // Clear the screen await editor.refreshScreen(); // The main event loop diff --git a/src/common/mod.ts b/src/common/mod.ts index 198b147..91e7f84 100644 --- a/src/common/mod.ts +++ b/src/common/mod.ts @@ -2,7 +2,6 @@ export * from './editor.ts'; export * from './runtime.ts'; export * from './termios.ts'; export * from './utils.ts'; -export * from './types.ts'; export type * from './types.ts'; export const VERSION = '0.0.1'; diff --git a/src/common/runtime.ts b/src/common/runtime.ts index f623142..1330b13 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime.ts @@ -1,5 +1,127 @@ import { getTermios } from './termios.ts'; -import { IRuntime, RunTimeType } from './types.ts'; + +// ---------------------------------------------------------------------------- +// Runtime adapter interfaces +// ---------------------------------------------------------------------------- +export enum RunTimeType { + Bun = 'bun', + Deno = 'deno', + Unknown = 'common', +} + +/** + * The native functions for getting/setting terminal settings + */ +export interface IFFI { + /** + * Get the existing termios settings (for canonical mode) + */ + tcgetattr(fd: number, termiosPtr: unknown): number; + + /** + * Update the termios settings + */ + tcsetattr(fd: number, act: number, termiosPtr: unknown): number; + + /** + * Update the termios pointer with raw mode settings + */ + cfmakeraw(termiosPtr: unknown): void; + + /** + * Convert a TypedArray to an opaque pointer for ffi calls + */ + // deno-lint-ignore no-explicit-any + getPointer(ta: any): unknown; +} + +export interface ITerminalSize { + rows: number; + cols: number; +} + +/** + * Runtime-specific terminal functionality + */ +export interface ITerminal { + /** + * The arguments passed to the program on launch + */ + argv: string[]; + /** + * The generator function returning chunks of input from the stdin stream + */ + inputLoop(): AsyncGenerator; + + /** + * Get the size of the terminal + */ + getTerminalSize(): Promise; + + /** + * Pipe a string to stdout + */ + writeStdout(s: string): Promise; +} + +/** + * Runtime-specific file handling + */ +export interface IFIO { + openFile(path: string): Promise; + openFileSync(path: string): string; + appendFile(path: string, contents: string): Promise; +} + +/** + * The common interface for runtime adapters + */ +export interface IRuntime { + /** + * The name of the runtime + */ + name: RunTimeType; + + /** + * Runtime-specific FFI + */ + ffi: IFFI; + + /** + * Runtime-specific terminal functionality + */ + term: ITerminal; + + /** + * Runtime-specific file system io + */ + file: IFIO; + + /** + * Set up an event handler + * + * @param eventName - The event to listen for + * @param handler - The event handler + */ + onEvent: (eventName: string, handler: (e: Event) => void) => void; + + /** + * Set a beforeExit/beforeUnload event handler for the runtime + * @param cb - The event handler + */ + onExit(cb: () => void): void; + + /** + * Stop execution + * + * @param code + */ + exit(code?: number): void; +} + +// ---------------------------------------------------------------------------- +// Misc runtime functions +// ---------------------------------------------------------------------------- let scrollRuntime: IRuntime | null = null; diff --git a/src/common/types.ts b/src/common/types.ts index 0ec1f25..d5c67d1 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -7,109 +7,6 @@ export interface IPoint { y: number; } -// ---------------------------------------------------------------------------- -// Runtime adapter interfaces -// ---------------------------------------------------------------------------- -export enum RunTimeType { - Bun = 'bun', - Deno = 'deno', - Unknown = 'common', -} - -/** - * The native functions for terminal settings - */ -export interface IFFI { - /** - * Get the existing termios settings (for canonical mode) - */ - tcgetattr(fd: number, termiosPtr: unknown): number; - - /** - * Update the termios settings - */ - tcsetattr(fd: number, act: number, termiosPtr: unknown): number; - - /** - * Update the termios pointer with raw mode settings - */ - cfmakeraw(termiosPtr: unknown): void; - - /** - * Convert a TypedArray to an opaque pointer for ffi calls - */ - getPointer(ta: any): unknown; -} - -export interface ITerminalSize { - rows: number; - cols: number; -} - -/** - * Runtime-specific terminal functionality - */ -export interface ITerminal { - /** - * The arguments passed to the program on launch - */ - argv: string[]; - /** - * The generator function returning chunks of input from the stdin stream - */ - inputLoop(): AsyncGenerator; - - /** - * Get the size of the terminal - */ - getTerminalSize(): Promise; - - /** - * Pipe a string to stdout - */ - writeStdout(s: string): Promise; -} - -/** - * Runtime-specific file handling - */ -export interface IFIO { - openFile(path: string): Promise; - openFileSync(path: string): string; -} - -export interface IRuntime { - /** - * The name of the runtime - */ - name: RunTimeType; - /** - * Runtime-specific FFI - */ - ffi: IFFI; - /** - * Runtime-specific terminal functionality - */ - term: ITerminal; - /** - * Runtime-specific file system io - */ - file: IFIO; - - /** - * Set a beforeExit/beforeUnload event handler for the runtime - * @param cb - The event handler - */ - onExit(cb: () => void): void; - - /** - * Stop execution - * - * @param code - */ - exit(code?: number): void; -} - // ---------------------------------------------------------------------------- // Testing // ---------------------------------------------------------------------------- diff --git a/src/deno/ffi.ts b/src/deno/ffi.ts index 08644f4..f2c03ff 100644 --- a/src/deno/ffi.ts +++ b/src/deno/ffi.ts @@ -1,5 +1,5 @@ // Deno-specific ffi code -import { IFFI } from '../common/types.ts'; +import { IFFI } from '../common/runtime.ts'; let libSuffix = ''; switch (Deno.build.os) { diff --git a/src/deno/file_io.ts b/src/deno/file_io.ts index 0cadcb2..a1cb2d8 100644 --- a/src/deno/file_io.ts +++ b/src/deno/file_io.ts @@ -1,4 +1,4 @@ -import { IFIO } from '../common/types.ts'; +import { IFIO } from '../common/runtime.ts'; const DenoFileIO: IFIO = { openFile: async function (path: string): Promise { @@ -11,6 +11,18 @@ const DenoFileIO: IFIO = { const data = Deno.readFileSync(path); return decoder.decode(data); }, + appendFile: async function (path: string, contents: string): Promise { + const file = await Deno.open(path, { + write: true, + append: true, + create: true, + }); + const encoder = new TextEncoder(); + const writer = file.writable.getWriter(); + + await writer.write(encoder.encode(contents)); + file.close(); + }, }; export default DenoFileIO; diff --git a/src/deno/mod.ts b/src/deno/mod.ts index c8347f3..fc66d24 100644 --- a/src/deno/mod.ts +++ b/src/deno/mod.ts @@ -11,6 +11,8 @@ const DenoRuntime: IRuntime = { file: DenoFileIO, ffi: DenoFFI, term: DenoTerminalIO, + onEvent: (eventName: string, handler: (e: Event) => void) => + globalThis.addEventListener(eventName, handler), onExit: (cb: () => void): void => { globalThis.addEventListener('onbeforeunload', cb); globalThis.onbeforeunload = cb; diff --git a/src/deno/terminal_io.ts b/src/deno/terminal_io.ts index aae62ff..1c97ce3 100644 --- a/src/deno/terminal_io.ts +++ b/src/deno/terminal_io.ts @@ -1,4 +1,4 @@ -import { ITerminal, ITerminalSize } from '../common/types.ts'; +import { ITerminal, ITerminalSize } from '../common/runtime.ts'; const DenoTerminalIO: ITerminal = { argv: Deno.args,