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';
|
2023-11-16 11:10:33 -05:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-11-16 11:10:33 -05:00
|
|
|
/**
|
|
|
|
* 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;
|
2023-11-16 11:10:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
2023-11-16 11:10:33 -05:00
|
|
|
/**
|
|
|
|
* The generator function returning chunks of input from the stdin stream
|
|
|
|
*/
|
2023-11-27 15:05:48 -05:00
|
|
|
inputLoop(): AsyncGenerator<Uint8Array, null>;
|
2023-11-16 11:10:33 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the size of the terminal
|
|
|
|
*/
|
|
|
|
getTerminalSize(): Promise<ITerminalSize>;
|
|
|
|
|
2023-11-27 15:05:48 -05:00
|
|
|
/**
|
|
|
|
* Get the current chunk of input, if it exists
|
|
|
|
*/
|
|
|
|
readStdin(): Promise<string | null>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the raw chunk of input
|
|
|
|
*/
|
|
|
|
readStdinRaw(): Promise<Uint8Array | null>;
|
2023-11-27 11:07:26 -05:00
|
|
|
|
2023-11-16 11:10:33 -05:00
|
|
|
/**
|
|
|
|
* 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 {
|
2023-11-16 11:10:33 -05:00
|
|
|
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>;
|
2023-11-16 11:10:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
2023-11-16 11:10:33 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
2023-11-16 11:10:33 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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();
|
2023-11-10 18:22:09 -05:00
|
|
|
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 {
|
2023-11-27 15:05:48 -05:00
|
|
|
if (raw.length === 0) {
|
|
|
|
return '';
|
|
|
|
}
|
2023-11-24 08:31:51 -05:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-10 18:22:09 -05:00
|
|
|
/**
|
|
|
|
* 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 {
|
2023-11-10 18:22:09 -05:00
|
|
|
let runtime = RunTimeType.Unknown;
|
2023-11-01 15:05:31 -04:00
|
|
|
|
|
|
|
if ('Deno' in globalThis) {
|
2023-11-10 18:22:09 -05:00
|
|
|
runtime = RunTimeType.Deno;
|
2023-11-01 15:05:31 -04:00
|
|
|
}
|
|
|
|
if ('Bun' in globalThis) {
|
2023-11-10 18:22:09 -05:00
|
|
|
runtime = RunTimeType.Bun;
|
2023-11-01 15:05:31 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return runtime;
|
2023-11-10 18:22:09 -05:00
|
|
|
}
|
|
|
|
|
2023-11-16 13:00:02 -05:00
|
|
|
/**
|
|
|
|
* Get the adapter object for the current Runtime
|
|
|
|
*/
|
2023-11-10 18:22:09 -05:00
|
|
|
export async function getRuntime(): Promise<IRuntime> {
|
|
|
|
if (scrollRuntime === null) {
|
2023-11-16 16:00:03 -05:00
|
|
|
const runtime = runtimeType();
|
2023-11-10 18:22:09 -05:00
|
|
|
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
|
|
|
};
|