Extract common interfaces out of runtime-specific adapters
This commit is contained in:
parent
8155f4dc73
commit
9cca55b101
@ -1,5 +1,4 @@
|
||||
{
|
||||
"exclude": ["src/bun/**/*.ts"],
|
||||
"lint": {
|
||||
"include": ["src/"],
|
||||
"rules": {
|
||||
|
16
justfile
16
justfile
@ -3,12 +3,7 @@ default:
|
||||
@just --list
|
||||
|
||||
# Typescript checking
|
||||
check: lint
|
||||
deno check --unstable --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts
|
||||
|
||||
# Code linting
|
||||
lint:
|
||||
deno lint
|
||||
check: deno-check bun-check
|
||||
|
||||
# Reformat the code
|
||||
fmt:
|
||||
@ -27,6 +22,10 @@ clean:
|
||||
# Bun-specific commands
|
||||
########################################################################################################################
|
||||
|
||||
# Check code with actual Typescript compiler
|
||||
bun-check:
|
||||
bunx tsc
|
||||
|
||||
# Test with bun
|
||||
bun-test:
|
||||
bun test --coverage
|
||||
@ -39,6 +38,11 @@ bun-run:
|
||||
# Deno-specific commands
|
||||
########################################################################################################################
|
||||
|
||||
# Lint code and check types
|
||||
deno-check:
|
||||
deno lint
|
||||
deno check --unstable --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts
|
||||
|
||||
# Test with deno
|
||||
deno-test:
|
||||
deno test --allow-all
|
||||
|
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.0.11"
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
* This is all the nasty ffi setup for the bun runtime
|
||||
*/
|
||||
import { dlopen, ptr, suffix } from 'bun:ffi';
|
||||
import { IFFI } from '../common/types.ts';
|
||||
|
||||
const getLib = (name: string) => {
|
||||
return dlopen(
|
||||
@ -23,7 +24,7 @@ const getLib = (name: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
let cStdLib = { symbols: {} };
|
||||
let cStdLib: any = { symbols: {} };
|
||||
|
||||
try {
|
||||
cStdLib = getLib(`libc.${suffix}`);
|
||||
@ -35,5 +36,12 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
export const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
|
||||
export const getPointer = ptr;
|
||||
const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
|
||||
const BunFFI: IFFI = {
|
||||
tcgetattr,
|
||||
tcsetattr,
|
||||
cfmakeraw,
|
||||
getPointer: ptr,
|
||||
};
|
||||
|
||||
export default BunFFI;
|
||||
|
@ -2,16 +2,8 @@
|
||||
* The main entrypoint when using Bun as the runtime
|
||||
*/
|
||||
|
||||
import { getTermios } from "../common/mod.ts";
|
||||
export * from './terminal_io.ts';
|
||||
|
||||
export async function init() {
|
||||
const t = await getTermios();
|
||||
|
||||
t.enableRawMode();
|
||||
|
||||
process.on('exit', () => {
|
||||
console.info('Disabling raw mode');
|
||||
t.disableRawMode();
|
||||
})
|
||||
}
|
||||
export const onExit = (cb: () => void): void => {
|
||||
process.on('beforeExit', cb);
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Wrap the runtime-specific hook into stdin
|
||||
*/
|
||||
import { ITerminalIO } from '../common/mod.ts';
|
||||
|
||||
export async function* inputLoop() {
|
||||
for await (const chunk of Bun.stdin.stream()) {
|
||||
yield chunk;
|
||||
@ -12,3 +14,10 @@ export async function write(s: string): Promise<void> {
|
||||
|
||||
await Bun.write(Bun.stdout, buffer);
|
||||
}
|
||||
|
||||
const BunTerminalIO: ITerminalIO = {
|
||||
inputLoop,
|
||||
write,
|
||||
};
|
||||
|
||||
export default BunTerminalIO;
|
||||
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Adapt the bun test interface to the shared testing interface
|
||||
*/
|
||||
import {test as btest, expect } from 'bun:test';
|
||||
import {ITestBase} from "../common/mod";
|
||||
import { expect, test as btest } from 'bun:test';
|
||||
import { ITestBase } from '../common/mod';
|
||||
|
||||
class TestBase implements ITestBase {
|
||||
test(name: string, fn: () => void) {
|
||||
@ -32,7 +32,11 @@ class TestBase implements ITestBase {
|
||||
assertTrue(actual: boolean): void {
|
||||
return expect(actual).toBe(true);
|
||||
}
|
||||
|
||||
assertStrictEquals(actual: unknown, expected: unknown): void {
|
||||
return expect(actual).toBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
const testBase = new TestBase();
|
||||
export default testBase;
|
||||
export default testBase;
|
||||
|
@ -1,18 +1,23 @@
|
||||
import { importForRuntime } from './runtime.ts';
|
||||
import { Editor } from './editor.ts';
|
||||
import { getTermios } from './termios.ts';
|
||||
|
||||
export * from './runtime.ts';
|
||||
export * from './strings.ts';
|
||||
export * from './termios.ts';
|
||||
export type { ITestBase } from './test_base.ts';
|
||||
export type * from './types.ts';
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export async function main() {
|
||||
const { inputLoop, init } = await importForRuntime('mod.ts');
|
||||
const { inputLoop, onExit } = await importForRuntime('mod.ts');
|
||||
|
||||
// Set up handlers to enable/disable raw mode for each runtime
|
||||
await init();
|
||||
// Setup raw mode, and tear down on error or normal exit
|
||||
const t = await getTermios();
|
||||
t.enableRawMode();
|
||||
onExit(() => {
|
||||
console.info('Exit handler called, disabling raw mode');
|
||||
t.disableRawMode();
|
||||
});
|
||||
|
||||
// Create the editor itself
|
||||
const editor = new Editor();
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { chars, importDefaultForRuntime, is_ascii, ITestBase } from './mod.ts';
|
||||
import { importDefaultForRuntime, ITestBase } from './mod.ts';
|
||||
import { chars, is_ascii } from './strings.ts';
|
||||
|
||||
const t: ITestBase = await importDefaultForRuntime('test_base');
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { die, importForRuntime } from './mod.ts';
|
||||
import { die, IFFI, importDefaultForRuntime } from './mod.ts';
|
||||
|
||||
export const STDIN_FILENO = 0;
|
||||
export const STOUT_FILENO = 1;
|
||||
@ -8,26 +8,104 @@ export const TCSAFLUSH = 2;
|
||||
export const TERMIOS_SIZE = 60;
|
||||
|
||||
/**
|
||||
* Common interface for setting Termios properties
|
||||
* Implementation to toggle raw mode
|
||||
*/
|
||||
export interface ITermios {
|
||||
class Termios {
|
||||
/**
|
||||
* Are we currently in raw mode?
|
||||
* The ffi implementation for the current runtime
|
||||
* @private
|
||||
*/
|
||||
inRawMode: boolean;
|
||||
#ffi: IFFI;
|
||||
|
||||
/**
|
||||
* Toggles on raw mode
|
||||
* Are we in raw mode?
|
||||
* @private
|
||||
*/
|
||||
enableRawMode(): void;
|
||||
#inRawMode: boolean;
|
||||
|
||||
/**
|
||||
* Restores canonical mode
|
||||
* The saved version of the termios struct for cooked/canonical mode
|
||||
* @private
|
||||
*/
|
||||
disableRawMode(): void;
|
||||
#cookedTermios: Uint8Array;
|
||||
|
||||
/**
|
||||
* The data for the termios struct we are manipulating
|
||||
* @private
|
||||
*/
|
||||
readonly #termios: Uint8Array;
|
||||
|
||||
/**
|
||||
* The pointer to the termios struct
|
||||
* @private
|
||||
*/
|
||||
readonly #ptr: any;
|
||||
|
||||
constructor(ffi: IFFI) {
|
||||
this.#ffi = ffi;
|
||||
this.#inRawMode = false;
|
||||
|
||||
// These are the TypedArrays linked to the raw pointer data
|
||||
this.#cookedTermios = new Uint8Array(TERMIOS_SIZE);
|
||||
this.#termios = new Uint8Array(TERMIOS_SIZE);
|
||||
|
||||
// The current pointer for C
|
||||
this.#ptr = ffi.getPointer(this.#termios);
|
||||
}
|
||||
|
||||
get inRawMode() {
|
||||
return this.#inRawMode;
|
||||
}
|
||||
|
||||
enableRawMode() {
|
||||
if (this.#inRawMode) {
|
||||
throw new Error('Can not enable raw mode when in raw mode');
|
||||
}
|
||||
|
||||
// Get the current termios settings
|
||||
let res = this.#ffi.tcgetattr(STDIN_FILENO, this.#ptr);
|
||||
if (res === -1) {
|
||||
die('Failed to get terminal settings');
|
||||
}
|
||||
|
||||
// The #ptr property is pointing to the #termios TypedArray. As the pointer
|
||||
// is manipulated, the TypedArray is as well. We will use this to save
|
||||
// the original canonical/cooked terminal settings for disabling raw mode later.
|
||||
// @ts-ignore: bad type definition
|
||||
this.#cookedTermios = new Uint8Array(this.#termios, 0, 60);
|
||||
|
||||
// Update termios struct with (most of the) raw settings
|
||||
this.#ffi.cfmakeraw(this.#ptr);
|
||||
|
||||
// @TODO: Tweak a few more terminal settings
|
||||
|
||||
// Actually set the new termios settings
|
||||
res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr);
|
||||
if (res === -1) {
|
||||
die('Failed to update terminal settings. Can\'t enter raw mode');
|
||||
}
|
||||
|
||||
this.#inRawMode = true;
|
||||
}
|
||||
|
||||
disableRawMode() {
|
||||
// Don't even bother throwing an error if we try to disable raw mode
|
||||
// and aren't in raw mode. It just doesn't really matter.
|
||||
if (!this.#inRawMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldTermiosPtr = this.#ffi.getPointer(this.#cookedTermios);
|
||||
const res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr);
|
||||
if (res === -1) {
|
||||
die('Failed to restore canonical mode.');
|
||||
}
|
||||
|
||||
this.#inRawMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
let termiosSingleton: ITermios | null = null;
|
||||
let termiosSingleton: Termios | null = null;
|
||||
|
||||
export const getTermios = async () => {
|
||||
if (termiosSingleton !== null) {
|
||||
@ -35,103 +113,8 @@ export const getTermios = async () => {
|
||||
}
|
||||
|
||||
// Get the runtime-specific ffi wrappers
|
||||
const { tcgetattr, tcsetattr, cfmakeraw, getPointer } =
|
||||
await importForRuntime('ffi');
|
||||
|
||||
/**
|
||||
* Implementation to toggle raw mode
|
||||
*/
|
||||
class Termios implements ITermios {
|
||||
/**
|
||||
* Are we in raw mode?
|
||||
* @private
|
||||
*/
|
||||
#inRawMode: boolean;
|
||||
|
||||
/**
|
||||
* The saved version of the termios struct for cooked/canonical mode
|
||||
* @private
|
||||
*/
|
||||
#cookedTermios: Uint8Array;
|
||||
|
||||
/**
|
||||
* The data for the termios struct we are manipulating
|
||||
* @private
|
||||
*/
|
||||
readonly #termios: Uint8Array;
|
||||
|
||||
/**
|
||||
* The pointer to the termios struct
|
||||
* @private
|
||||
*/
|
||||
readonly #ptr: any;
|
||||
|
||||
constructor() {
|
||||
this.#inRawMode = false;
|
||||
|
||||
// These are the TypedArrays linked to the raw pointer data
|
||||
this.#cookedTermios = new Uint8Array(TERMIOS_SIZE);
|
||||
this.#termios = new Uint8Array(TERMIOS_SIZE);
|
||||
|
||||
// The current pointer for C
|
||||
this.#ptr = getPointer(this.#termios);
|
||||
}
|
||||
|
||||
get inRawMode() {
|
||||
return this.#inRawMode;
|
||||
}
|
||||
|
||||
enableRawMode() {
|
||||
if (this.#inRawMode) {
|
||||
throw new Error('Can not enable raw mode when in raw mode');
|
||||
}
|
||||
|
||||
// Get the current termios settings
|
||||
let res = tcgetattr(STDIN_FILENO, this.#ptr);
|
||||
if (res === -1) {
|
||||
die('Failed to get terminal settings');
|
||||
}
|
||||
|
||||
// The #ptr property is pointing to the #termios TypedArray. As the pointer
|
||||
// is manipulated, the TypedArray is as well. We will use this to save
|
||||
// the original canonical/cooked terminal settings for disabling raw mode later.
|
||||
this.#cookedTermios = new Uint8Array(this.#termios, 0, 60);
|
||||
|
||||
// Update termios struct with (most of the) raw settings
|
||||
res = cfmakeraw(this.#ptr);
|
||||
if (res === -1) {
|
||||
die('Failed to call cfmakeraw');
|
||||
}
|
||||
|
||||
// @TODO: Tweak a few more terminal settings
|
||||
|
||||
// Actually set the new termios settings
|
||||
res = tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr);
|
||||
if (res === -1) {
|
||||
die('Failed to update terminal settings. Can\'t enter raw mode');
|
||||
}
|
||||
|
||||
this.#inRawMode = true;
|
||||
}
|
||||
|
||||
disableRawMode() {
|
||||
// Don't even bother throwing an error if we try to disable raw mode
|
||||
// and aren't in raw mode. It just doesn't really matter.
|
||||
if (!this.#inRawMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldTermiosPtr = getPointer(this.#cookedTermios);
|
||||
const res = tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr);
|
||||
if (res === -1) {
|
||||
die('Failed to restore canonical mode.');
|
||||
}
|
||||
|
||||
this.#inRawMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
termiosSingleton = new Termios();
|
||||
const ffi: IFFI = await importDefaultForRuntime('ffi');
|
||||
termiosSingleton = new Termios(ffi);
|
||||
|
||||
return termiosSingleton;
|
||||
};
|
||||
|
@ -1,13 +0,0 @@
|
||||
/**
|
||||
* The shared test interface, so tests can be run by both runtimes
|
||||
*/
|
||||
export interface ITestBase {
|
||||
test(name: string, fn: () => void): void;
|
||||
assertEquals(actual: unknown, expected: unknown): void;
|
||||
assertNotEquals(actual: unknown, expected: unknown): void;
|
||||
assertStrictEquals(actual: unknown, expected: unknown): void;
|
||||
assertExists(actual: unknown): void;
|
||||
assertInstanceOf(actual: unknown, expectedType: any): void;
|
||||
assertTrue(actual: boolean): void;
|
||||
assertFalse(actual: boolean): void;
|
||||
}
|
61
src/common/types.ts
Normal file
61
src/common/types.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* The shared test interface, so tests can be run by both runtimes
|
||||
*/
|
||||
export interface ITestBase {
|
||||
test(name: string, fn: () => void): void;
|
||||
assertEquals(actual: unknown, expected: unknown): void;
|
||||
assertNotEquals(actual: unknown, expected: unknown): void;
|
||||
assertStrictEquals(actual: unknown, expected: unknown): void;
|
||||
assertExists(actual: unknown): void;
|
||||
assertInstanceOf(actual: unknown, expectedType: any): void;
|
||||
assertTrue(actual: boolean): void;
|
||||
assertFalse(actual: boolean): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The native functions for 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
|
||||
*/
|
||||
getPointer(ta: any): unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime-specific IO streams
|
||||
*/
|
||||
export interface ITerminalIO {
|
||||
/**
|
||||
* The generator function returning chunks of input from the stdin stream
|
||||
*/
|
||||
inputLoop(): AsyncGenerator<Uint8Array, void, unknown>;
|
||||
|
||||
/**
|
||||
* Pipe a string to stdout
|
||||
*/
|
||||
write(s: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IRuntime {
|
||||
/**
|
||||
* Set a beforeExit/beforeUnload event handler for the runtime
|
||||
* @param cb - The event handler
|
||||
*/
|
||||
onExit(cb: () => void): void;
|
||||
}
|
@ -1,8 +1,5 @@
|
||||
// Deno-specific ffi code
|
||||
|
||||
// Determine library extension based on
|
||||
// your OS.
|
||||
// import { termiosStruct } from "../common/termios.ts";
|
||||
import { IFFI } from '../common/types.ts';
|
||||
|
||||
let libSuffix = '';
|
||||
switch (Deno.build.os) {
|
||||
@ -36,6 +33,12 @@ const cStdLib = Deno.dlopen(
|
||||
} as const,
|
||||
);
|
||||
|
||||
export const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
|
||||
const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
|
||||
const DenoFFI: IFFI = {
|
||||
tcgetattr,
|
||||
tcsetattr,
|
||||
cfmakeraw,
|
||||
getPointer: Deno.UnsafePointer.of,
|
||||
};
|
||||
|
||||
export const getPointer = Deno.UnsafePointer.of;
|
||||
export default DenoFFI;
|
||||
|
@ -1,17 +1,16 @@
|
||||
/**
|
||||
* The main entrypoint when using Deno as the runtime
|
||||
*/
|
||||
import { getTermios } from '../common/mod.ts';
|
||||
import { IRuntime } from '../common/types.ts';
|
||||
|
||||
export * from './terminal_io.ts';
|
||||
|
||||
export async function init() {
|
||||
const t = await getTermios();
|
||||
export const onExit = (cb: () => void): void => {
|
||||
globalThis.addEventListener('onbeforeunload', cb);
|
||||
};
|
||||
|
||||
t.enableRawMode();
|
||||
const DenoRuntime: IRuntime = {
|
||||
onExit,
|
||||
};
|
||||
|
||||
globalThis.onbeforeunload = (): void => {
|
||||
console.info('Disabling raw mode');
|
||||
t.disableRawMode();
|
||||
};
|
||||
}
|
||||
export default DenoRuntime;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ITerminalIO } from '../common/types.ts';
|
||||
|
||||
/**
|
||||
* Wrap the runtime-specific hook into stdin
|
||||
*/
|
||||
@ -14,3 +16,10 @@ export async function write(s: string): Promise<void> {
|
||||
await stdout.write(buffer);
|
||||
stdout.releaseLock();
|
||||
}
|
||||
|
||||
const DenoTerminalIO: ITerminalIO = {
|
||||
inputLoop,
|
||||
write,
|
||||
};
|
||||
|
||||
export default DenoTerminalIO;
|
||||
|
@ -47,5 +47,5 @@ class TestBase implements ITestBase {
|
||||
}
|
||||
}
|
||||
|
||||
export const testBase = new TestBase();
|
||||
const testBase = new TestBase();
|
||||
export default testBase;
|
||||
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"target": "esnext",
|
||||
"types": ["bun-types"],
|
||||
"lib": ["ESNext"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"composite": true,
|
||||
"downlevelIteration": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": ["src/deno"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user