Move some things around to more logical places, attempt to set up an error log file
This commit is contained in:
parent
a4ef630c7b
commit
f966ebf4ac
@ -1,12 +1,13 @@
|
|||||||
# Scroll
|
# Scroll
|
||||||
|
|
||||||
Making a text editor in Typescript based on Kilo (Script + Kilo = Scroll). This
|
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`
|
- Bun: `just bun-run [filename]`
|
||||||
- Deno: `just deno-run`
|
- Deno: `just deno-run [filename]`
|
||||||
|
|
||||||
## Development Notes
|
## Development Notes
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* This is all the nasty ffi setup for the bun runtime
|
* This is all the nasty ffi setup for the bun runtime
|
||||||
*/
|
*/
|
||||||
import { dlopen, ptr, suffix } from 'bun:ffi';
|
import { dlopen, ptr, suffix } from 'bun:ffi';
|
||||||
import { IFFI } from '../common/types.ts';
|
import { IFFI } from '../common/runtime.ts';
|
||||||
|
|
||||||
const getLib = (name: string) => {
|
const getLib = (name: string) => {
|
||||||
return dlopen(
|
return dlopen(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { IFIO } from '../common/types.ts';
|
import { IFIO } from '../common/runtime.ts';
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { appendFile } from 'node:fs/promises';
|
||||||
|
|
||||||
const BunFileIO: IFIO = {
|
const BunFileIO: IFIO = {
|
||||||
openFile: async (path: string): Promise<string> => {
|
openFile: async (path: string): Promise<string> => {
|
||||||
@ -10,6 +11,9 @@ const BunFileIO: IFIO = {
|
|||||||
openFileSync: (path: string): string => {
|
openFileSync: (path: string): string => {
|
||||||
return readFileSync(path).toString();
|
return readFileSync(path).toString();
|
||||||
},
|
},
|
||||||
|
appendFile: async function (path: string, contents: string): Promise<void> {
|
||||||
|
await appendFile(path, contents);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BunFileIO;
|
export default BunFileIO;
|
||||||
|
@ -2,22 +2,18 @@
|
|||||||
* The main entrypoint when using Bun as the runtime
|
* 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 BunFFI from './ffi.ts';
|
||||||
import BunTerminalIO from './terminal_io.ts';
|
import BunTerminalIO from './terminal_io.ts';
|
||||||
import BunFileIO from './file_io.ts';
|
import BunFileIO from './file_io.ts';
|
||||||
|
|
||||||
process.on('error', async (e) => {
|
|
||||||
(await getTermios()).disableRawMode();
|
|
||||||
console.error(e);
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
const BunRuntime: IRuntime = {
|
const BunRuntime: IRuntime = {
|
||||||
name: RunTimeType.Bun,
|
name: RunTimeType.Bun,
|
||||||
file: BunFileIO,
|
file: BunFileIO,
|
||||||
ffi: BunFFI,
|
ffi: BunFFI,
|
||||||
term: BunTerminalIO,
|
term: BunTerminalIO,
|
||||||
|
onEvent: (eventName: string, handler: (e: Event) => void) =>
|
||||||
|
process.on(eventName, handler),
|
||||||
onExit: (cb: () => void): void => {
|
onExit: (cb: () => void): void => {
|
||||||
process.on('beforeExit', cb);
|
process.on('beforeExit', cb);
|
||||||
process.on('exit', cb);
|
process.on('exit', cb);
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* ANSI/VT terminal escape code handling
|
||||||
|
*/
|
||||||
|
|
||||||
export const ANSI_PREFIX = '\x1b[';
|
export const ANSI_PREFIX = '\x1b[';
|
||||||
|
|
||||||
function esc(pieces: TemplateStringsArray): string {
|
function esc(pieces: TemplateStringsArray): string {
|
||||||
@ -39,4 +43,33 @@ export const Ansi = {
|
|||||||
moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`,
|
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;
|
export default Ansi;
|
||||||
|
@ -20,7 +20,7 @@ export class Document {
|
|||||||
this.#rows = [];
|
this.#rows = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get numrows(): number {
|
get numRows(): number {
|
||||||
return this.#rows.length;
|
return this.#rows.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ export class Editor {
|
|||||||
|
|
||||||
private drawRows(): void {
|
private drawRows(): void {
|
||||||
for (let y = 0; y < this.#screen.rows; y++) {
|
for (let y = 0; y < this.#screen.rows; y++) {
|
||||||
if (this.#document.numrows < y) {
|
if (this.#document.numRows < y) {
|
||||||
this.drawPlaceholderRow(y);
|
this.drawPlaceholderRow(y);
|
||||||
} else {
|
} else {
|
||||||
this.drawFileRow(y);
|
this.drawFileRow(y);
|
||||||
|
@ -1,40 +1,11 @@
|
|||||||
import { KeyCommand } from './ansi.ts';
|
import { readKey } from './ansi.ts';
|
||||||
import { getRuntime } from './runtime.ts';
|
import { getRuntime } from './runtime.ts';
|
||||||
import { getTermios } from './termios.ts';
|
import { getTermios } from './termios.ts';
|
||||||
import { Editor } from './editor.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() {
|
export async function main() {
|
||||||
const runTime = await getRuntime();
|
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
|
// Setup raw mode, and tear down on error or normal exit
|
||||||
const t = await getTermios();
|
const t = await getTermios();
|
||||||
@ -43,6 +14,16 @@ export async function main() {
|
|||||||
t.disableRawMode();
|
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();
|
const terminalSize = await term.getTerminalSize();
|
||||||
|
|
||||||
// Create the editor itself
|
// Create the editor itself
|
||||||
@ -53,6 +34,8 @@ export async function main() {
|
|||||||
await editor.open(filename);
|
await editor.open(filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the screen
|
||||||
await editor.refreshScreen();
|
await editor.refreshScreen();
|
||||||
|
|
||||||
// The main event loop
|
// The main event loop
|
||||||
|
@ -2,7 +2,6 @@ export * from './editor.ts';
|
|||||||
export * from './runtime.ts';
|
export * from './runtime.ts';
|
||||||
export * from './termios.ts';
|
export * from './termios.ts';
|
||||||
export * from './utils.ts';
|
export * from './utils.ts';
|
||||||
export * from './types.ts';
|
|
||||||
export type * from './types.ts';
|
export type * from './types.ts';
|
||||||
|
|
||||||
export const VERSION = '0.0.1';
|
export const VERSION = '0.0.1';
|
||||||
|
@ -1,5 +1,127 @@
|
|||||||
import { getTermios } from './termios.ts';
|
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<Uint8Array, void, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the terminal
|
||||||
|
*/
|
||||||
|
getTerminalSize(): Promise<ITerminalSize>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipe a string to stdout
|
||||||
|
*/
|
||||||
|
writeStdout(s: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime-specific file handling
|
||||||
|
*/
|
||||||
|
export interface IFIO {
|
||||||
|
openFile(path: string): Promise<string>;
|
||||||
|
openFileSync(path: string): string;
|
||||||
|
appendFile(path: string, contents: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
let scrollRuntime: IRuntime | null = null;
|
||||||
|
|
||||||
|
@ -7,109 +7,6 @@ export interface IPoint {
|
|||||||
y: number;
|
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<Uint8Array, void, unknown>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the size of the terminal
|
|
||||||
*/
|
|
||||||
getTerminalSize(): Promise<ITerminalSize>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pipe a string to stdout
|
|
||||||
*/
|
|
||||||
writeStdout(s: string): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime-specific file handling
|
|
||||||
*/
|
|
||||||
export interface IFIO {
|
|
||||||
openFile(path: string): Promise<string>;
|
|
||||||
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
|
// Testing
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Deno-specific ffi code
|
// Deno-specific ffi code
|
||||||
import { IFFI } from '../common/types.ts';
|
import { IFFI } from '../common/runtime.ts';
|
||||||
|
|
||||||
let libSuffix = '';
|
let libSuffix = '';
|
||||||
switch (Deno.build.os) {
|
switch (Deno.build.os) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IFIO } from '../common/types.ts';
|
import { IFIO } from '../common/runtime.ts';
|
||||||
|
|
||||||
const DenoFileIO: IFIO = {
|
const DenoFileIO: IFIO = {
|
||||||
openFile: async function (path: string): Promise<string> {
|
openFile: async function (path: string): Promise<string> {
|
||||||
@ -11,6 +11,18 @@ const DenoFileIO: IFIO = {
|
|||||||
const data = Deno.readFileSync(path);
|
const data = Deno.readFileSync(path);
|
||||||
return decoder.decode(data);
|
return decoder.decode(data);
|
||||||
},
|
},
|
||||||
|
appendFile: async function (path: string, contents: string): Promise<void> {
|
||||||
|
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;
|
export default DenoFileIO;
|
||||||
|
@ -11,6 +11,8 @@ const DenoRuntime: IRuntime = {
|
|||||||
file: DenoFileIO,
|
file: DenoFileIO,
|
||||||
ffi: DenoFFI,
|
ffi: DenoFFI,
|
||||||
term: DenoTerminalIO,
|
term: DenoTerminalIO,
|
||||||
|
onEvent: (eventName: string, handler: (e: Event) => void) =>
|
||||||
|
globalThis.addEventListener(eventName, handler),
|
||||||
onExit: (cb: () => void): void => {
|
onExit: (cb: () => void): void => {
|
||||||
globalThis.addEventListener('onbeforeunload', cb);
|
globalThis.addEventListener('onbeforeunload', cb);
|
||||||
globalThis.onbeforeunload = cb;
|
globalThis.onbeforeunload = cb;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ITerminal, ITerminalSize } from '../common/types.ts';
|
import { ITerminal, ITerminalSize } from '../common/runtime.ts';
|
||||||
|
|
||||||
const DenoTerminalIO: ITerminal = {
|
const DenoTerminalIO: ITerminal = {
|
||||||
argv: Deno.args,
|
argv: Deno.args,
|
||||||
|
Loading…
Reference in New Issue
Block a user