Refactor runtimes
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-07-12 10:58:23 -04:00
parent 21ff71cc94
commit 1951425508
20 changed files with 306 additions and 371 deletions

View File

@ -1,26 +1,14 @@
/** /**
* The main entrypoint when using Bun as the runtime * The main entrypoint when using Bun as the runtime
*/ */
import { CommonRuntime, IRuntime, RunTimeType } from '../common/runtime/mod.ts';
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';
/** /**
* The Bun Runtime implementation * The Bun Runtime implementation
*/ */
const BunRuntime: IRuntime = { const BunRuntime: IRuntime = {
...CommonRuntime,
name: RunTimeType.Bun, 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; export default BunRuntime;

View File

@ -9,7 +9,7 @@ import Row from './row.ts';
import * as Fn from './fns.ts'; import * as Fn from './fns.ts';
import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.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'; import { SearchDirection } from './types.ts';
const { const {
@ -20,6 +20,8 @@ const {
assertNotEquals, assertNotEquals,
assertFalse, assertFalse,
assertTrue, assertTrue,
assertSome,
assertNone,
testSuite, testSuite,
} = await getTestRunner(); } = await getTestRunner();
@ -433,10 +435,15 @@ const EditorTest = {
const OptionTest = { const OptionTest = {
'Option.from()': () => { 'Option.from()': () => {
assertTrue(Option.from(null).isNone()); assertNone(Option.from(null));
assertTrue(Option.from().isNone()); assertNone(Option.from());
assertEquivalent(Option.from(undefined), None); 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(Option.from(Some('foo')), Some('foo'));
assertEquivalent(Some(Some('bar')), Some('bar')); assertEquivalent(Some(Some('bar')), Some('bar'));
}, },

View File

@ -1,5 +1,5 @@
import { strlen, truncate } from './fns.ts'; import { strlen, truncate } from './fns.ts';
import { getRuntime } from './runtime.ts'; import { getRuntime } from './runtime/mod.ts';
class Buffer { class Buffer {
#b = ''; #b = '';

View File

@ -1,7 +1,7 @@
import Row from './row.ts'; import Row from './row.ts';
import { arrayInsert, maxAdd, minSub } from './fns.ts'; import { arrayInsert, maxAdd, minSub } from './fns.ts';
import Option, { None, Some } from './option.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'; import { Position, SearchDirection } from './types.ts';
export class Document { export class Document {

View File

@ -13,7 +13,7 @@ import {
truncate, truncate,
} from './fns.ts'; } from './fns.ts';
import Option, { None, Some } from './option.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'; import { ITerminalSize, Position, SearchDirection } from './types.ts';
export default class Editor { export default class Editor {

View File

@ -1,6 +1,5 @@
import { process } from './runtime/node.ts';
import { readKey } from './fns.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'; import Editor from './editor.ts';
export async function main() { export async function main() {

View File

@ -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<ITerminalSize> {
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<IRuntime> {
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<ITestBase> {
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;
};

View File

@ -1,9 +1,9 @@
import { appendFile, readFile, writeFile } from 'node:fs/promises'; import { appendFile, readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path'; 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<string> { openFile: async function (path: string): Promise<string> {
const filePath = resolve(path); const filePath = resolve(path);
const contents = await readFile(filePath, { encoding: 'utf8' }); const contents = await readFile(filePath, { encoding: 'utf8' });

View File

@ -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<IRuntime> {
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<ITestBase> {
const runtime = runtimeType();
const path = `../../${runtime}/test_base.ts`;
const pkg = await import(path);
if ('default' in pkg) {
return pkg.default;
}
return pkg;
}

45
src/common/runtime/log.ts Normal file
View File

@ -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);

View File

@ -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';

View File

@ -1,3 +1,7 @@
/**
* Re-export of node apis shared by runtimes
*/
import * as assert from 'node:assert';
import * as process from 'node:process'; import * as process from 'node:process';
export { process }; export { assert, process };

View File

@ -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<Uint8Array, null>;
/**
* Get the size of the terminal
*/
getTerminalSize(): Promise<ITerminalSize>;
/**
* Get the current chunk of input, if it exists
*/
readStdin(): Promise<string | null>;
/**
* Get the raw chunk of input
*/
readStdinRaw(): Promise<Uint8Array | null>;
/**
* Pipe a string to stdout
*/
writeStdout(s: string): Promise<void>;
}
/**
* Runtime-specific file system io
*/
export interface IFileIO {
/**
* Open an entire file
*
* @param path
*/
openFile(path: string): Promise<string>;
/**
* Append to a file, or create it if it doesn't exist
*
* @param path
* @param contents
*/
appendFile(path: string, contents: string): Promise<void>;
/**
* Save a string into a file
*
* @param path
* @param contents
*/
saveFile(path: string, contents: string): Promise<void>;
}
/**
* 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),
};

View File

@ -2,7 +2,7 @@ import { process } from './node.ts';
import { readKey } from '../fns.ts'; import { readKey } from '../fns.ts';
import { ITerminal, ITerminalSize } from '../types.ts'; import { ITerminal, ITerminalSize } from '../types.ts';
const CommonTerminalIO: ITerminal = { export const CommonTerminalIO: ITerminal = {
argv: (process.argv.length > 2) ? process.argv.slice(2) : [], argv: (process.argv.length > 2) ? process.argv.slice(2) : [],
inputLoop: async function* (): AsyncGenerator<Uint8Array, null> { inputLoop: async function* (): AsyncGenerator<Uint8Array, null> {
yield (await CommonTerminalIO.readStdinRaw()) ?? new Uint8Array(0); yield (await CommonTerminalIO.readStdinRaw()) ?? new Uint8Array(0);

View File

@ -20,7 +20,7 @@ export interface ITestBase {
testSuite(testObj: any): void; testSuite(testObj: any): void;
} }
abstract class AbstractTestBase implements Partial<ITestBase> { export abstract class AbstractTestBase implements Partial<ITestBase> {
/** /**
* The values (often objects) have all the same property values * The values (often objects) have all the same property values
*/ */

View File

@ -1,15 +1,12 @@
import { RunTimeType } from './runtime.ts';
export { Position } from './position.ts'; export { Position } from './position.ts';
export type { ITestBase } from './runtime/test_base.ts'; export type { ITestBase } from './runtime/test_base.ts';
export type {
/** IFileIO,
* The size of terminal in rows and columns IRuntime,
*/ ITerminal,
export interface ITerminalSize { ITerminalSize,
rows: number; RunTimeType,
cols: number; } from './runtime/mod.ts';
}
/** /**
* Which direction to search in the current document * Which direction to search in the current document
@ -18,114 +15,3 @@ export enum SearchDirection {
Forward = 1, Forward = 1,
Backward = -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<Uint8Array, null>;
/**
* Get the size of the terminal
*/
getTerminalSize(): Promise<ITerminalSize>;
/**
* Get the current chunk of input, if it exists
*/
readStdin(): Promise<string | null>;
/**
* Get the raw chunk of input
*/
readStdinRaw(): Promise<Uint8Array | null>;
/**
* Pipe a string to stdout
*/
writeStdout(s: string): Promise<void>;
}
/**
* Runtime-specific file system io
*/
export interface IFileIO {
/**
* Open an entire file
*
* @param path
*/
openFile(path: string): Promise<string>;
/**
* Append to a file, or create it if it doesn't exist
*
* @param path
* @param contents
*/
appendFile(path: string, contents: string): Promise<void>;
/**
* Save a string into a file
*
* @param path
* @param contents
*/
saveFile(path: string, contents: string): Promise<void>;
}
/**
* 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;
}

View File

@ -1 +0,0 @@
export * as stdAssert from 'https://deno.land/std@0.208.0/assert/mod.ts';

View File

@ -1,4 +1,4 @@
import { IFileIO } from '../common/runtime.ts'; import { IFileIO } from '../common/runtime/mod.ts';
const DenoFileIO: IFileIO = { const DenoFileIO: IFileIO = {
openFile: async function (path: string): Promise<string> { openFile: async function (path: string): Promise<string> {

View File

@ -1,16 +1,15 @@
/** /**
* The main entrypoint when using Deno as the runtime * 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 DenoTerminalIO from './terminal_io.ts';
import DenoFileIO from './file_io.ts'; import DenoFileIO from './file_io.ts';
import * as node_process from 'node:process';
/** /**
* The Deno Runtime implementation * The Deno Runtime implementation
*/ */
const DenoRuntime: IRuntime = { const DenoRuntime: IRuntime = {
...CommonRuntime,
name: RunTimeType.Deno, name: RunTimeType.Deno,
file: DenoFileIO, file: DenoFileIO,
term: DenoTerminalIO, term: DenoTerminalIO,
@ -20,7 +19,6 @@ const DenoRuntime: IRuntime = {
globalThis.addEventListener('onbeforeunload', cb); globalThis.addEventListener('onbeforeunload', cb);
globalThis.onbeforeunload = cb; globalThis.onbeforeunload = cb;
}, },
exit: (code?: number) => node_process.exit(code),
}; };
export default DenoRuntime; export default DenoRuntime;

View File

@ -1,25 +1,14 @@
/** /**
* The main entrypoint when using Tsx as the runtime * The main entrypoint when using Tsx as the runtime
*/ */
import { IRuntime, RunTimeType } from '../common/runtime.ts'; import { CommonRuntime, IRuntime, RunTimeType } from '../common/runtime/mod.ts';
import { process } from '../common/runtime/node.ts';
import TsxTerminalIO from '../common/runtime/terminal_io.ts';
import FileIO from '../common/runtime/file_io.ts';
/** /**
* The Tsx Runtime implementation * The Tsx Runtime implementation
*/ */
const TsxRuntime: IRuntime = { const TsxRuntime: IRuntime = {
...CommonRuntime,
name: RunTimeType.Tsx, 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; export default TsxRuntime;