diff --git a/src/bun/mod.ts b/src/bun/mod.ts index 65197ad..e83c484 100644 --- a/src/bun/mod.ts +++ b/src/bun/mod.ts @@ -1,26 +1,14 @@ /** * The main entrypoint when using Bun as the runtime */ - -import { IRuntime, RunTimeType } from '../common/runtime.ts'; -import { process } from '../common/runtime/node.ts'; -import TerminalIO from '../common/runtime/terminal_io.ts'; -import FileIO from '../common/runtime/file_io.ts'; +import { CommonRuntime, IRuntime, RunTimeType } from '../common/runtime/mod.ts'; /** * The Bun Runtime implementation */ const BunRuntime: IRuntime = { + ...CommonRuntime, name: RunTimeType.Bun, - file: FileIO, - term: TerminalIO, - onEvent: (eventName: string, handler) => process.on(eventName, handler), - onExit: (cb: () => void): void => { - process.on('beforeExit', cb); - process.on('exit', cb); - process.on('SIGINT', cb); - }, - exit: (code?: number) => process.exit(code), }; export default BunRuntime; diff --git a/src/common/all_test.ts b/src/common/all_test.ts index 7841d84..d747353 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -9,7 +9,7 @@ import Row from './row.ts'; import * as Fn from './fns.ts'; import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts'; -import { getTestRunner } from './runtime.ts'; +import { getTestRunner } from './runtime/mod.ts'; import { SearchDirection } from './types.ts'; const { @@ -20,6 +20,8 @@ const { assertNotEquals, assertFalse, assertTrue, + assertSome, + assertNone, testSuite, } = await getTestRunner(); @@ -433,10 +435,15 @@ const EditorTest = { const OptionTest = { 'Option.from()': () => { - assertTrue(Option.from(null).isNone()); - assertTrue(Option.from().isNone()); + assertNone(Option.from(null)); + assertNone(Option.from()); assertEquivalent(Option.from(undefined), None); + assertSome(Option.from('foo')); + assertSome(Option.from(234)); + assertSome(Option.from({})); + assertSome(Some([1, 2, 3])); + assertEquivalent(Option.from(Some('foo')), Some('foo')); assertEquivalent(Some(Some('bar')), Some('bar')); }, diff --git a/src/common/buffer.ts b/src/common/buffer.ts index ec15810..f2eeb85 100644 --- a/src/common/buffer.ts +++ b/src/common/buffer.ts @@ -1,5 +1,5 @@ import { strlen, truncate } from './fns.ts'; -import { getRuntime } from './runtime.ts'; +import { getRuntime } from './runtime/mod.ts'; class Buffer { #b = ''; diff --git a/src/common/document.ts b/src/common/document.ts index 6127b00..db7d5bd 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -1,7 +1,7 @@ import Row from './row.ts'; import { arrayInsert, maxAdd, minSub } from './fns.ts'; import Option, { None, Some } from './option.ts'; -import { getRuntime, logDebug, logWarning } from './runtime.ts'; +import { getRuntime, logDebug, logWarning } from './runtime/mod.ts'; import { Position, SearchDirection } from './types.ts'; export class Document { diff --git a/src/common/editor.ts b/src/common/editor.ts index db40ae3..29bba4d 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -13,7 +13,7 @@ import { truncate, } from './fns.ts'; import Option, { None, Some } from './option.ts'; -import { getRuntime, logDebug, logWarning } from './runtime.ts'; +import { getRuntime, logDebug, logWarning } from './runtime/mod.ts'; import { ITerminalSize, Position, SearchDirection } from './types.ts'; export default class Editor { diff --git a/src/common/main.ts b/src/common/main.ts index dbc2d62..147c6db 100644 --- a/src/common/main.ts +++ b/src/common/main.ts @@ -1,6 +1,5 @@ -import { process } from './runtime/node.ts'; import { readKey } from './fns.ts'; -import { getRuntime, logError, logWarning } from './runtime.ts'; +import { getRuntime, logError, logWarning, process } from './runtime/mod.ts'; import Editor from './editor.ts'; export async function main() { diff --git a/src/common/runtime.ts b/src/common/runtime.ts deleted file mode 100644 index 3597cc6..0000000 --- a/src/common/runtime.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Functions/Methods that depend on the current runtime to function - */ -import { process } from './runtime/node.ts'; -import Ansi from './ansi.ts'; -import { IRuntime, ITerminalSize, ITestBase } from './types.ts'; -import { noop } from './fns.ts'; -import { - defaultTerminalSize, - SCROLL_LOG_FILE_PREFIX, - SCROLL_LOG_FILE_SUFFIX, -} from './config.ts'; - -export type { IFileIO, IRuntime, ITerminal } from './types.ts'; - -/** - * Which Typescript runtime is currently being used - */ -export enum RunTimeType { - Bun = 'bun', - Deno = 'deno', - Tsx = 'tsx', - Unknown = 'common', -} - -/** - * The label for type/severity of the log entry - */ -export enum LogLevel { - Debug = 'Debug', - Info = 'Info', - Notice = 'Notice', - Warning = 'Warning', - Error = 'Error', -} - -let scrollRuntime: IRuntime | null = null; - -// ---------------------------------------------------------------------------- -// Misc runtime functions -// ---------------------------------------------------------------------------- - -/** - * Get the size of the terminal window via ANSI codes - * @see https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html#window-size-the-hard-way - */ -async function _getTerminalSizeFromAnsi(): Promise { - const { term } = await getRuntime(); - - // Tell the cursor to move to Row 999 and Column 999 - // Since this command specifically doesn't go off the screen - // When we ask where the cursor is, we should get the size of the screen - await term.writeStdout( - Ansi.moveCursorForward(999) + Ansi.moveCursorDown(999), - ); - - // Ask where the cursor is - await term.writeStdout(Ansi.GetCursorLocation); - - // Get the first chunk from stdin - // The response is \x1b[(rows);(cols)R.. - const chunk = await term.readStdinRaw(); - if (chunk === null) { - return defaultTerminalSize; - } - - const rawCode = (new TextDecoder()).decode(chunk); - const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1'); - const [srows, scols] = res.split(';'); - const rows = parseInt(srows, 10) ?? 24; - const cols = parseInt(scols, 10) ?? 80; - - // Clear the screen - await term.writeStdout(Ansi.ClearScreen + Ansi.ResetCursor); - - return { - rows, - cols, - }; -} - -/** - * Basic logging - - */ -export function log( - s: unknown, - level: LogLevel = LogLevel.Notice, - data?: any, -): void { - getRuntime().then(({ file }) => { - const rawS = JSON.stringify(s, null, 2); - const rawData = JSON.stringify(data, null, 2); - const output = (typeof data !== 'undefined') - ? `${rawS}\n${rawData}\n\n` - : `${rawS}\n`; - - const outputFile = - `${SCROLL_LOG_FILE_PREFIX}-${level.toLowerCase()}${SCROLL_LOG_FILE_SUFFIX}`; - file.appendFile(outputFile, output).then(noop); - }); -} - -export const logDebug = (s: unknown, data?: any) => - log(s, LogLevel.Debug, data); -export const logInfo = (s: unknown, data?: any) => log(s, LogLevel.Info, data); -export const logNotice = (s: unknown, data?: any) => - log(s, LogLevel.Notice, data); -export const logWarning = (s: unknown, data?: any) => - log(s, LogLevel.Warning, data); -export const logError = (s: unknown, data?: any) => - log(s, LogLevel.Warning, data); - -/** - * Kill program, displaying an error message - * @param s - */ -export function die(s: string | Error): void { - logError(s); - process.stdin.setRawMode(false); - console.error(s); - getRuntime().then((r) => r.exit()); -} - -/** - * Determine which Typescript runtime we are operating under - */ -export function runtimeType(): RunTimeType { - let runtime = RunTimeType.Tsx; - - if ('Deno' in globalThis) { - runtime = RunTimeType.Deno; - } - if ('Bun' in globalThis) { - runtime = RunTimeType.Bun; - } - - return runtime; -} - -/** - * Get the adapter object for the current Runtime - */ -export async function getRuntime(): Promise { - if (scrollRuntime === null) { - const runtime = runtimeType(); - const path = `../${runtime}/mod.ts`; - - const pkg = await import(path); - if ('default' in pkg) { - scrollRuntime = pkg.default; - } - - if (scrollRuntime !== null) { - return Promise.resolve(scrollRuntime); - } - - return Promise.reject('Missing default import'); - } - - return Promise.resolve(scrollRuntime); -} - -/** - * Get the common test interface object - */ -export async function getTestRunner(): Promise { - const runtime = runtimeType(); - const path = `../${runtime}/test_base.ts`; - const pkg = await import(path); - if ('default' in pkg) { - return pkg.default; - } - - return pkg; -} - -/** - * Import a runtime-specific module - * - * e.g. to load "src/bun/mod.ts", if the runtime is bun, - * you can use like so `await importForRuntime('index')`; - * - * @param path - the path within the runtime module - */ -export const importForRuntime = async (path: string) => { - const runtime = runtimeType(); - const suffix = '.ts'; - const base = `../${runtime}/`; - - const pathParts = path - .split('/') - .filter((part) => part !== '' && part !== '.' && part !== suffix) - .map((part) => part.replace(suffix, '')); - - const cleanedPath = pathParts.join('/'); - const importPath = base + cleanedPath + suffix; - - const pkg = await import(importPath); - if ('default' in pkg) { - return pkg.default; - } - - return pkg; -}; diff --git a/src/common/runtime/file_io.ts b/src/common/runtime/file_io.ts index e810782..9f13d08 100644 --- a/src/common/runtime/file_io.ts +++ b/src/common/runtime/file_io.ts @@ -1,9 +1,9 @@ import { appendFile, readFile, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; -import { IFileIO } from '../runtime.ts'; +import { IFileIO } from './mod.ts'; -const CommonFileIO: IFileIO = { +export const CommonFileIO: IFileIO = { openFile: async function (path: string): Promise { const filePath = resolve(path); const contents = await readFile(filePath, { encoding: 'utf8' }); diff --git a/src/common/runtime/helpers.ts b/src/common/runtime/helpers.ts new file mode 100644 index 0000000..6e12845 --- /dev/null +++ b/src/common/runtime/helpers.ts @@ -0,0 +1,72 @@ +/** + * Functions/Methods that depend on the current runtime to function + */ +import { logError } from './log.ts'; +import { process } from './node.ts'; +import { RunTimeType } from './runtime.ts'; +import { IRuntime, ITestBase } from '../types.ts'; + +let scrollRuntime: IRuntime | null = null; + +/** + * Kill program, displaying an error message + * @param s + */ +export function die(s: string | Error): void { + logError(s); + process.stdin.setRawMode(false); + console.error(s); + getRuntime().then((r) => r.exit()); +} + +/** + * Determine which Typescript runtime we are operating under + */ +export function runtimeType(): RunTimeType { + let runtime = RunTimeType.Tsx; + + if ('Deno' in globalThis) { + runtime = RunTimeType.Deno; + } else if ('Bun' in globalThis) { + runtime = RunTimeType.Bun; + } + + return runtime; +} + +/** + * Get the adapter object for the current Runtime + */ +export async function getRuntime(): Promise { + if (scrollRuntime === null) { + const runtime = runtimeType(); + const path = `../../${runtime}/mod.ts`; + + const pkg = await import(path); + if ('default' in pkg) { + scrollRuntime = pkg.default; + } + + if (scrollRuntime !== null) { + return Promise.resolve(scrollRuntime); + } + + return Promise.reject('Missing default import'); + } + + return Promise.resolve(scrollRuntime); +} + +/** + * Get the common test interface object + */ +export async function getTestRunner(): Promise { + const runtime = runtimeType(); + const path = `../../${runtime}/test_base.ts`; + const pkg = await import(path); + if ('default' in pkg) { + return pkg.default; + } + + return pkg; +} diff --git a/src/common/runtime/log.ts b/src/common/runtime/log.ts new file mode 100644 index 0000000..308949e --- /dev/null +++ b/src/common/runtime/log.ts @@ -0,0 +1,45 @@ +import { noop } from '../fns.ts'; +import { SCROLL_LOG_FILE_PREFIX, SCROLL_LOG_FILE_SUFFIX } from '../config.ts'; +import { getRuntime } from './mod.ts'; + +/** + * The label for type/severity of the log entry + */ +export enum LogLevel { + Debug = 'Debug', + Info = 'Info', + Notice = 'Notice', + Warning = 'Warning', + Error = 'Error', +} + +/** + * Basic logging - + */ +export function log( + s: unknown, + level: LogLevel = LogLevel.Notice, + data?: any, +): void { + getRuntime().then(({ file }) => { + const rawS = JSON.stringify(s, null, 2); + const rawData = JSON.stringify(data, null, 2); + const output = (typeof data !== 'undefined') + ? `${rawS}\n${rawData}\n\n` + : `${rawS}\n`; + + const outputFile = + `${SCROLL_LOG_FILE_PREFIX}-${level.toLowerCase()}${SCROLL_LOG_FILE_SUFFIX}`; + file.appendFile(outputFile, output).then(noop); + }); +} + +export const logDebug = (s: unknown, data?: any) => + log(s, LogLevel.Debug, data); +export const logInfo = (s: unknown, data?: any) => log(s, LogLevel.Info, data); +export const logNotice = (s: unknown, data?: any) => + log(s, LogLevel.Notice, data); +export const logWarning = (s: unknown, data?: any) => + log(s, LogLevel.Warning, data); +export const logError = (s: unknown, data?: any) => + log(s, LogLevel.Warning, data); diff --git a/src/common/runtime/mod.ts b/src/common/runtime/mod.ts new file mode 100644 index 0000000..55e89bf --- /dev/null +++ b/src/common/runtime/mod.ts @@ -0,0 +1,7 @@ +export * from './file_io.ts'; +export * from './helpers.ts'; +export * from './log.ts'; +export * from './node.ts'; +export * from './runtime.ts'; +export * from './terminal_io.ts'; +export * from './test_base.ts'; diff --git a/src/common/runtime/node.ts b/src/common/runtime/node.ts index 095eaf1..0d10331 100644 --- a/src/common/runtime/node.ts +++ b/src/common/runtime/node.ts @@ -1,3 +1,7 @@ +/** + * Re-export of node apis shared by runtimes + */ +import * as assert from 'node:assert'; import * as process from 'node:process'; -export { process }; +export { assert, process }; diff --git a/src/common/runtime/runtime.ts b/src/common/runtime/runtime.ts new file mode 100644 index 0000000..060e714 --- /dev/null +++ b/src/common/runtime/runtime.ts @@ -0,0 +1,145 @@ +import { process } from './node.ts'; +import CommonFileIO from './file_io.ts'; +import CommonTerminalIO from './terminal_io.ts'; + +// ---------------------------------------------------------------------------- +// Runtime adapter interfaces +// ---------------------------------------------------------------------------- + +/** + * The size of terminal in rows and columns + */ +export interface ITerminalSize { + rows: number; + cols: number; +} + +/** + * Which Typescript runtime is currently being used + */ +export enum RunTimeType { + Bun = 'bun', + Deno = 'deno', + Tsx = 'tsx', + Unknown = 'common', +} + +/** + * 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; + + /** + * Get the current chunk of input, if it exists + */ + readStdin(): Promise; + + /** + * Get the raw chunk of input + */ + readStdinRaw(): Promise; + + /** + * Pipe a string to stdout + */ + writeStdout(s: string): Promise; +} + +/** + * Runtime-specific file system io + */ +export interface IFileIO { + /** + * Open an entire file + * + * @param path + */ + openFile(path: string): Promise; + + /** + * Append to a file, or create it if it doesn't exist + * + * @param path + * @param contents + */ + appendFile(path: string, contents: string): Promise; + + /** + * Save a string into a file + * + * @param path + * @param contents + */ + saveFile(path: string, contents: string): Promise; +} + +/** + * The common interface for runtime adapters + */ +export interface IRuntime { + /** + * The name of the runtime + */ + name: RunTimeType; + + /** + * Runtime-specific terminal functionality + */ + term: ITerminal; + + /** + * Runtime-specific file system io + */ + file: IFileIO; + + /** + * Set up an event handler + * + * @param eventName - The event to listen for + * @param handler - The event handler + */ + onEvent: ( + eventName: string, + handler: (e: Event | ErrorEvent) => 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; +} + +export const CommonRuntime: IRuntime = { + name: RunTimeType.Unknown, + term: CommonTerminalIO, + file: CommonFileIO, + onEvent: (eventName: string, handler) => process.on(eventName, handler), + onExit: (cb: () => void): void => { + process.on('beforeExit', cb); + process.on('exit', cb); + process.on('SIGINT', cb); + }, + exit: (code?: number) => process.exit(code), +}; diff --git a/src/common/runtime/terminal_io.ts b/src/common/runtime/terminal_io.ts index b270ddd..3804cfe 100644 --- a/src/common/runtime/terminal_io.ts +++ b/src/common/runtime/terminal_io.ts @@ -2,7 +2,7 @@ import { process } from './node.ts'; import { readKey } from '../fns.ts'; import { ITerminal, ITerminalSize } from '../types.ts'; -const CommonTerminalIO: ITerminal = { +export const CommonTerminalIO: ITerminal = { argv: (process.argv.length > 2) ? process.argv.slice(2) : [], inputLoop: async function* (): AsyncGenerator { yield (await CommonTerminalIO.readStdinRaw()) ?? new Uint8Array(0); diff --git a/src/common/runtime/test_base.ts b/src/common/runtime/test_base.ts index 9424be0..c143d31 100644 --- a/src/common/runtime/test_base.ts +++ b/src/common/runtime/test_base.ts @@ -20,7 +20,7 @@ export interface ITestBase { testSuite(testObj: any): void; } -abstract class AbstractTestBase implements Partial { +export abstract class AbstractTestBase implements Partial { /** * The values (often objects) have all the same property values */ diff --git a/src/common/types.ts b/src/common/types.ts index 9f62d23..93aed89 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,15 +1,12 @@ -import { RunTimeType } from './runtime.ts'; - export { Position } from './position.ts'; export type { ITestBase } from './runtime/test_base.ts'; - -/** - * The size of terminal in rows and columns - */ -export interface ITerminalSize { - rows: number; - cols: number; -} +export type { + IFileIO, + IRuntime, + ITerminal, + ITerminalSize, + RunTimeType, +} from './runtime/mod.ts'; /** * Which direction to search in the current document @@ -18,114 +15,3 @@ export enum SearchDirection { Forward = 1, Backward = -1, } - -// ---------------------------------------------------------------------------- -// Runtime adapter interfaces -// ---------------------------------------------------------------------------- - -/** - * 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; - - /** - * Get the current chunk of input, if it exists - */ - readStdin(): Promise; - - /** - * Get the raw chunk of input - */ - readStdinRaw(): Promise; - - /** - * Pipe a string to stdout - */ - writeStdout(s: string): Promise; -} - -/** - * Runtime-specific file system io - */ -export interface IFileIO { - /** - * Open an entire file - * - * @param path - */ - openFile(path: string): Promise; - - /** - * Append to a file, or create it if it doesn't exist - * - * @param path - * @param contents - */ - appendFile(path: string, contents: string): Promise; - - /** - * Save a string into a file - * - * @param path - * @param contents - */ - saveFile(path: string, contents: string): Promise; -} - -/** - * The common interface for runtime adapters - */ -export interface IRuntime { - /** - * The name of the runtime - */ - name: RunTimeType; - - /** - * Runtime-specific terminal functionality - */ - term: ITerminal; - - /** - * Runtime-specific file system io - */ - file: IFileIO; - - /** - * Set up an event handler - * - * @param eventName - The event to listen for - * @param handler - The event handler - */ - onEvent: ( - eventName: string, - handler: (e: Event | ErrorEvent) => 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; -} diff --git a/src/deno/deps.ts b/src/deno/deps.ts deleted file mode 100644 index 40ae756..0000000 --- a/src/deno/deps.ts +++ /dev/null @@ -1 +0,0 @@ -export * as stdAssert from 'https://deno.land/std@0.208.0/assert/mod.ts'; diff --git a/src/deno/file_io.ts b/src/deno/file_io.ts index c81194d..83f6bf7 100644 --- a/src/deno/file_io.ts +++ b/src/deno/file_io.ts @@ -1,4 +1,4 @@ -import { IFileIO } from '../common/runtime.ts'; +import { IFileIO } from '../common/runtime/mod.ts'; const DenoFileIO: IFileIO = { openFile: async function (path: string): Promise { diff --git a/src/deno/mod.ts b/src/deno/mod.ts index 4fefcac..6d528e9 100644 --- a/src/deno/mod.ts +++ b/src/deno/mod.ts @@ -1,16 +1,15 @@ /** * The main entrypoint when using Deno as the runtime */ -import { IRuntime, RunTimeType } from '../common/runtime.ts'; +import { CommonRuntime, IRuntime, RunTimeType } from '../common/runtime/mod.ts'; import DenoTerminalIO from './terminal_io.ts'; import DenoFileIO from './file_io.ts'; -import * as node_process from 'node:process'; - /** * The Deno Runtime implementation */ const DenoRuntime: IRuntime = { + ...CommonRuntime, name: RunTimeType.Deno, file: DenoFileIO, term: DenoTerminalIO, @@ -20,7 +19,6 @@ const DenoRuntime: IRuntime = { globalThis.addEventListener('onbeforeunload', cb); globalThis.onbeforeunload = cb; }, - exit: (code?: number) => node_process.exit(code), }; export default DenoRuntime; diff --git a/src/tsx/mod.ts b/src/tsx/mod.ts index fe7904e..bd203c7 100644 --- a/src/tsx/mod.ts +++ b/src/tsx/mod.ts @@ -1,25 +1,14 @@ /** * The main entrypoint when using Tsx as the runtime */ -import { IRuntime, RunTimeType } from '../common/runtime.ts'; -import { process } from '../common/runtime/node.ts'; -import TsxTerminalIO from '../common/runtime/terminal_io.ts'; -import FileIO from '../common/runtime/file_io.ts'; +import { CommonRuntime, IRuntime, RunTimeType } from '../common/runtime/mod.ts'; /** * The Tsx Runtime implementation */ const TsxRuntime: IRuntime = { + ...CommonRuntime, name: RunTimeType.Tsx, - file: FileIO, - term: TsxTerminalIO, - onEvent: (eventName: string, handler) => process.on(eventName, handler), - onExit: (cb: () => void): void => { - process.on('beforeExit', cb); - process.on('exit', cb); - process.on('SIGINT', cb); - }, - exit: (code?: number) => process.exit(code), }; export default TsxRuntime;