scroll/src/common/runtime.ts

280 lines
5.7 KiB
JavaScript
Raw Normal View History

2023-11-02 13:06:48 -04:00
import { getTermios } from './termios.ts';
2023-11-24 08:31:51 -05:00
import { ctrlKey, noop } from './utils.ts';
2023-11-16 16:00:03 -05:00
import { ITestBase } from './types.ts';
2023-11-24 08:31:51 -05:00
import { KeyCommand } from './ansi.ts';
export enum RunTimeType {
Bun = 'bun',
Deno = 'deno',
Unknown = 'common',
}
2023-11-16 16:00:03 -05:00
// ----------------------------------------------------------------------------
// Runtime adapter interfaces
// ----------------------------------------------------------------------------
/**
* The size of terminal in rows and columns
*/
export interface ITerminalSize {
rows: number;
cols: number;
}
/**
* 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;
2023-11-16 16:00:03 -05:00
/**
* Closes the FFI handle
*/
close(): void;
}
/**
* Runtime-specific terminal functionality
*/
export interface ITerminal {
/**
* The arguments passed to the program on launch
*/
argv: string[];
2023-11-16 16:00:03 -05:00
/**
* The generator function returning chunks of input from the stdin stream
*/
2023-11-24 08:31:51 -05:00
inputLoop(): AsyncGenerator<string, void>;
/**
* Get the size of the terminal
*/
getTerminalSize(): Promise<ITerminalSize>;
/**
* Pipe a string to stdout
*/
writeStdout(s: string): Promise<void>;
}
/**
* Runtime-specific file handling
*/
2023-11-16 16:00:03 -05:00
export interface IFileIO {
openFile(path: string): Promise<string>;
openFileSync(path: string): string;
appendFile(path: string, contents: string): Promise<void>;
2023-11-16 13:00:02 -05:00
appendFileSync(path: string, contents: string): void;
2023-11-21 15:14:08 -05:00
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
*/
2023-11-16 16:00:03 -05:00
file: IFileIO;
/**
* Set up an event handler
*
* @param eventName - The event to listen for
* @param handler - The event handler
*/
2023-11-16 13:00:02 -05:00
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;
}
// ----------------------------------------------------------------------------
// Misc runtime functions
// ----------------------------------------------------------------------------
2023-11-24 08:31:51 -05:00
const decoder = new TextDecoder();
let scrollRuntime: IRuntime | null = null;
2023-11-01 15:05:31 -04:00
2023-11-24 08:31:51 -05:00
/**
* Convert input from ANSI escape sequences into a form
* that can be more easily mapped to editor commands
*
* @param raw - the raw chunk of input
*/
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 '\x1b[1~':
case '\x1b[7~':
case '\x1bOH':
case '\x1b[H':
return KeyCommand.Home;
case '\x1b[4~':
case '\x1b[8~':
case '\x1bOF':
case '\x1b[F':
return KeyCommand.End;
case '\n':
case '\v':
return KeyCommand.Enter;
case ctrlKey('l'):
return KeyCommand.Escape;
case ctrlKey('h'):
return KeyCommand.Backspace;
default:
return parsed;
}
}
2023-11-16 16:00:03 -05:00
export function logToFile(s: unknown) {
importForRuntime('file_io').then((f) => {
const raw = (typeof s === 'string') ? s : JSON.stringify(s, null, 2);
const output = raw + '\n';
f.appendFile('./scroll.log', output).then(noop);
});
}
/**
* Kill program, displaying an error message
* @param s
*/
2023-11-16 13:00:02 -05:00
export function die(s: string | Error): void {
getTermios().then((t) => {
t.disableRawMode();
console.error(s);
getRuntime().then((r) => r.exit());
});
2023-11-02 13:06:48 -04:00
}
2023-11-01 15:05:31 -04:00
/**
* Determine which Typescript runtime we are operating under
*/
2023-11-16 16:00:03 -05:00
export function runtimeType(): RunTimeType {
let runtime = RunTimeType.Unknown;
2023-11-01 15:05:31 -04:00
if ('Deno' in globalThis) {
runtime = RunTimeType.Deno;
2023-11-01 15:05:31 -04:00
}
if ('Bun' in globalThis) {
runtime = RunTimeType.Bun;
2023-11-01 15:05:31 -04:00
}
return runtime;
}
2023-11-16 13:00:02 -05:00
/**
* Get the adapter object for the current Runtime
*/
export async function getRuntime(): Promise<IRuntime> {
if (scrollRuntime === null) {
2023-11-16 16:00:03 -05:00
const runtime = runtimeType();
const path = `../${runtime}/mod.ts`;
const pkg = await import(path);
if ('default' in pkg) {
scrollRuntime = pkg.default;
}
}
return Promise.resolve(scrollRuntime!);
}
2023-11-01 15:05:31 -04:00
2023-11-16 16:00:03 -05:00
/**
* 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;
}
2023-11-01 15:05:31 -04:00
/**
* Import a runtime-specific module
*
2023-11-16 16:00:03 -05:00
* e.g. to load "src/bun/mod.ts", if the runtime is bun,
2023-11-01 15:05:31 -04:00
* you can use like so `await importForRuntime('index')`;
*
* @param path - the path within the runtime module
*/
export const importForRuntime = async (path: string) => {
2023-11-16 16:00:03 -05:00
const runtime = runtimeType();
2023-11-01 15:25:52 -04:00
const suffix = '.ts';
2023-11-01 15:05:31 -04:00
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;
2023-11-16 16:00:03 -05:00
const pkg = await import(importPath);
2023-11-03 11:59:58 -04:00
if ('default' in pkg) {
return pkg.default;
}
2023-11-16 16:00:03 -05:00
return pkg;
2023-11-03 11:59:58 -04:00
};