Remove buggy FFI implementation in favor of Node API (implemented by Bun and Deno)
timw4mail/scroll/pipeline/head This commit looks good Details

This commit is contained in:
Timothy Warren 2024-03-01 11:06:47 -05:00
parent 9458794fa3
commit ab42873182
7 changed files with 47 additions and 314 deletions

View File

@ -1,56 +0,0 @@
/**
* This is all the nasty ffi setup for the bun runtime
*/
import { dlopen, ptr, suffix } from 'bun:ffi';
import { IFFI } from '../common/runtime.ts';
const getLib = (name: string) => {
return dlopen(
name,
{
tcgetattr: {
args: ['i32', 'pointer'],
returns: 'i32',
},
tcsetattr: {
args: ['i32', 'i32', 'pointer'],
returns: 'i32',
},
cfmakeraw: {
args: ['pointer'],
returns: 'void',
},
},
);
};
let cStdLib: any = { symbols: {} };
try {
cStdLib = getLib(`libc.${suffix}`);
} catch {
try {
cStdLib = getLib(`libc.${suffix}.6`);
} catch {
throw new Error('Could not find c standard library');
}
}
const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
let closed = false;
const BunFFI: IFFI = {
tcgetattr,
tcsetattr,
cfmakeraw,
getPointer: ptr,
close: () => {
if (!closed) {
cStdLib.close();
closed = true;
}
// Do nothing if FFI library was already closed
},
};
export default BunFFI;

View File

@ -1,11 +1,45 @@
/**
* Wrap the runtime-specific hook into stdin
*/
import process from 'node:process';
import Ansi from '../common/ansi.ts';
import { defaultTerminalSize } from '../common/config.ts';
import { readKey } from '../common/fns.ts';
import { ITerminal, ITerminalSize } from '../common/types.ts';
async function _getTerminalSizeFromAnsi(): Promise<ITerminalSize> {
// 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 BunTerminalIO.writeStdout(
Ansi.moveCursorForward(999) + Ansi.moveCursorDown(999),
);
// Ask where the cursor is
await BunTerminalIO.writeStdout(Ansi.GetCursorLocation);
// Get the first chunk from stdin
// The response is \x1b[(rows);(cols)R..
const chunk = await BunTerminalIO.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 BunTerminalIO.writeStdout(Ansi.ClearScreen + Ansi.ResetCursor);
return {
rows,
cols,
};
}
const BunTerminalIO: ITerminal = {
// Deno only returns arguments passed to the script, so
// remove the bun runtime executable, and entry script arguments
@ -22,37 +56,13 @@ const BunTerminalIO: ITerminal = {
* Get the size of the terminal window via ANSI codes
* @see https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html#window-size-the-hard-way
*/
getTerminalSize: async function getTerminalSize(): Promise<ITerminalSize> {
// 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 BunTerminalIO.writeStdout(
Ansi.moveCursorForward(999) + Ansi.moveCursorDown(999),
);
getTerminalSize: function getTerminalSize(): Promise<ITerminalSize> {
const [cols, rows] = process.stdout.getWindowSize();
// Ask where the cursor is
await BunTerminalIO.writeStdout(Ansi.GetCursorLocation);
// Get the first chunk from stdin
// The response is \x1b[(rows);(cols)R..
const chunk = await BunTerminalIO.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 BunTerminalIO.writeStdout(Ansi.ClearScreen + Ansi.ResetCursor);
return {
return Promise.resolve({
rows,
cols,
};
});
},
readStdin: async function (): Promise<string | null> {
const raw = await BunTerminalIO.readStdinRaw();

View File

@ -1,6 +1,6 @@
import process from 'node:process';
import { readKey } from './fns.ts';
import { getRuntime, logError } from './runtime.ts';
import { getTermios } from './termios.ts';
import Editor from './editor.ts';
export async function main() {
@ -8,16 +8,14 @@ export async function main() {
const { term } = rt;
// Setup raw mode, and tear down on error or normal exit
const t = await getTermios();
t.enableRawMode();
process.stdin.setRawMode(true);
rt.onExit(() => {
t.disableRawMode();
t.cleanup();
process.stdin.setRawMode(false);
});
// Setup error handler to log to file
rt.onEvent('error', (error) => {
t.disableRawMode();
process.stdin.setRawMode(false);
logError(JSON.stringify(error, null, 2));
});

View File

@ -1,9 +1,9 @@
import process from 'node:process';
import { IRuntime, ITestBase } from './types.ts';
import { getTermios } from './termios.ts';
import { noop } from './fns.ts';
import { SCROLL_ERR_FILE, SCROLL_LOG_FILE } from './config.ts';
export type { IFFI, IFileIO, IRuntime, ITerminal } from './types.ts';
export type { IFileIO, IRuntime, ITerminal } from './types.ts';
/**
* Which Typescript runtime is currently being used
@ -53,12 +53,9 @@ export function logError(s: unknown): void {
*/
export function die(s: string | Error): void {
logError(s);
getTermios().then((t) => {
t.disableRawMode();
t.cleanup();
console.error(s);
getRuntime().then((r) => r.exit());
});
process.stdin.setRawMode(false);
console.error(s);
getRuntime().then((r) => r.exit());
}
/**

View File

@ -1,132 +0,0 @@
import { die, IFFI, importForRuntime, log, LogLevel } from './runtime.ts';
export const STDIN_FILENO = 0;
export const TCSANOW = 0;
export const TERMIOS_SIZE = 60;
/**
* Implementation to toggle raw mode
*/
class Termios {
/**
* The ffi implementation for the current runtime
* @private
*/
#ffi: IFFI;
/**
* 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
*/
#termios: Uint8Array;
/**
* Has the nasty ffi stuff been cleaned up?
* @private
*/
#cleaned: boolean = false;
/**
* The pointer to the termios struct
* @private
*/
readonly #ptr: unknown;
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);
}
cleanup() {
if (!this.#cleaned) {
this.#ffi.close();
this.#cleaned = true;
}
log('Attempting to cleanup Termios class again', LogLevel.Warning);
}
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 the raw settings
this.#ffi.cfmakeraw(this.#ptr);
// 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) {
log(
'Attempting to disable raw mode when not in raw mode',
LogLevel.Warning,
);
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: Termios | null = null;
export const getTermios = async () => {
if (termiosSingleton !== null) {
return termiosSingleton;
}
// Get the runtime-specific ffi wrappers
const ffi = await importForRuntime('ffi');
termiosSingleton = new Termios(ffi);
return termiosSingleton;
};

View File

@ -8,37 +8,6 @@ export interface ITerminalSize {
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 adapter interfaces
// ----------------------------------------------------------------------------

View File

@ -1,53 +0,0 @@
// Deno-specific ffi code
import { IFFI } from '../common/runtime.ts';
let suffix = '';
switch (Deno.build.os) {
case 'windows':
suffix = 'dll';
break;
case 'darwin':
suffix = 'dylib';
break;
default:
suffix = 'so.6';
break;
}
const cSharedLib = `libc.${suffix}`;
const cStdLib = Deno.dlopen(
cSharedLib,
{
tcgetattr: {
parameters: ['i32', 'pointer'],
result: 'i32',
},
tcsetattr: {
parameters: ['i32', 'i32', 'pointer'],
result: 'i32',
},
cfmakeraw: {
parameters: ['pointer'],
result: 'void',
},
} as const,
);
const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
let closed = false;
const DenoFFI: IFFI = {
tcgetattr,
tcsetattr,
cfmakeraw,
getPointer: Deno.UnsafePointer.of,
close: () => {
if (!closed) {
cStdLib.close();
closed = true;
}
// Do nothing if FFI library was already closed
},
};
export default DenoFFI;