Get raw mode working in Deno

This commit is contained in:
Timothy Warren 2023-11-01 15:05:31 -04:00
parent 52632ad9a9
commit b30c4d40d6
6 changed files with 171 additions and 48 deletions

View File

@ -1,7 +1,6 @@
{
"imports": {
"std": "https://deno.land/std@0.204.0/",
// "/": "./src/",
},
"lint": {
"include": ["src/"],

44
src/common/index.ts Normal file
View File

@ -0,0 +1,44 @@
export enum RunTime {
Bun = 'bun',
Deno = 'deno',
Unknown = 'common',
}
/**
* Determine which Typescript runtime we are operating under
*/
export const getRuntime = (): RunTime => {
let runtime = RunTime.Unknown;
if ('Deno' in globalThis) {
runtime = RunTime.Deno;
}
if ('Bun' in globalThis) {
runtime = RunTime.Bun;
}
return runtime;
};
/**
* Import a runtime-specific module
*
* eg. to load "src/bun/index.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 = getRuntime();
const suffix = (runtime === RunTime.Deno) ? '.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;
return await import(importPath);
}

41
src/deno/ffi.ts Normal file
View File

@ -0,0 +1,41 @@
// Deno-specific ffi code
// Determine library extension based on
// your OS.
// import { termiosStruct } from "../common/termios.ts";
let libSuffix = '';
switch (Deno.build.os) {
case 'windows':
libSuffix = 'dll';
break;
case 'darwin':
libSuffix = 'dylib';
break;
default:
libSuffix = 'so.6';
break;
}
const cSharedLib = `libc.${libSuffix}`;
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,
);
export const { tcgetattr, tcsetattr, cfmakeraw} = cStdLib.symbols;
export const getPointer = Deno.UnsafePointer.of;

View File

@ -1,12 +1,18 @@
/**
* The main entrypoint when using Deno as the runtime
*/
import { Termios } from './termios.ts'
export async function main(): Promise<number> {
const t = new Termios();
t.enableRawMode();
const decoder = new TextDecoder();
for await (const chunk of Deno.stdin.readable) {
const char = String(decoder.decode(chunk)).trim();
if (char === 'q') {
t.disableRawMode();
return 0;
}
}

View File

@ -1,42 +1,81 @@
// Deno-specific ffi code
import { STDIN_FILENO, TCSANOW, ITermios, TERMIOS_SIZE } from "../common/termios.ts";
import { cfmakeraw, tcgetattr, tcsetattr, getPointer } from "./ffi.ts";
// Determine library extension based on
// your OS.
// import { termiosStruct } from "../common/termios.ts";
/**
* Implementation to toggle raw mode with Deno runtime
*/
export class Termios implements ITermios {
/**
* Are we in raw mode?
* @private
*/
#inRawMode:boolean;
let libSuffix = '';
switch (Deno.build.os) {
case 'windows':
libSuffix = 'dll';
break;
case 'darwin':
libSuffix = 'dylib';
break;
default:
libSuffix = 'so.6';
break;
/**
* 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;
/**
* The pointer to the termios struct
* @private
*/
#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);
}
const cSharedLib = `libc.${libSuffix}`;
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,
);
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
tcgetattr(STDIN_FILENO, this.#ptr);
// 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 raw settings
cfmakeraw(this.#ptr);
// Actually set the new termios settings
tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr);
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);
tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr);
this.#inRawMode = false;
}
}
export default cStdLib.symbols;
export const tcgetattr = cStdLib.symbols.tcgetattr;
export const tcsetattr = cStdLib.symbols.tcsetattr;
export const cfmakeraw = cStdLib.symbols.cfmakeraw;

View File

@ -8,18 +8,12 @@ export enum RunTime {
Unknown = 'common',
}
import { importForRuntime } from "./common/index.ts";
/**
* Determine the runtime strategy, and go!
*/
(async () => {
let RUNTIME = RunTime.Unknown;
if ('Deno' in globalThis) {
RUNTIME = RunTime.Deno;
}
if ('Bun' in globalThis) {
RUNTIME = RunTime.Bun;
}
const { main } = await import(`./${RUNTIME}/index.ts`);
const { main } = await importForRuntime('./index.ts');
await main();
})();