import { getTermios } from './termios.ts'; import { noop } from './utils.ts'; import { ITestBase } from './types.ts'; export enum RunTimeType { Bun = 'bun', Deno = 'deno', Unknown = 'common', } // ---------------------------------------------------------------------------- // 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; /** * Closes the FFI handle */ close(): void; } /** * 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 IFileIO { openFile(path: string): Promise; openFileSync(path: string): string; appendFile(path: string, contents: string): Promise; appendFileSync(path: string, contents: string): 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; } // ---------------------------------------------------------------------------- // Misc runtime functions // ---------------------------------------------------------------------------- let scrollRuntime: IRuntime | null = null; 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 */ export function die(s: string | Error): void { getTermios().then((t) => { t.disableRawMode(); console.error(s); getRuntime().then((r) => r.exit()); }); } /** * Determine which Typescript runtime we are operating under */ export function runtimeType(): RunTimeType { let runtime = RunTimeType.Unknown; 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; } } 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; };