From b30c4d40d657a45be37c1de05850efc25ff9df2d Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 1 Nov 2023 15:05:31 -0400 Subject: [PATCH] Get raw mode working in Deno --- deno.jsonc | 1 - src/common/index.ts | 44 +++++++++++++++++ src/deno/ffi.ts | 41 ++++++++++++++++ src/deno/index.ts | 6 +++ src/deno/termios.ts | 115 +++++++++++++++++++++++++++++--------------- src/scroll.ts | 12 ++--- 6 files changed, 171 insertions(+), 48 deletions(-) create mode 100644 src/common/index.ts create mode 100644 src/deno/ffi.ts diff --git a/deno.jsonc b/deno.jsonc index 19a82d2..a550008 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,7 +1,6 @@ { "imports": { "std": "https://deno.land/std@0.204.0/", - // "/": "./src/", }, "lint": { "include": ["src/"], diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 0000000..ae680d9 --- /dev/null +++ b/src/common/index.ts @@ -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); +} \ No newline at end of file diff --git a/src/deno/ffi.ts b/src/deno/ffi.ts new file mode 100644 index 0000000..703868a --- /dev/null +++ b/src/deno/ffi.ts @@ -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; diff --git a/src/deno/index.ts b/src/deno/index.ts index f29d9bc..e176f53 100644 --- a/src/deno/index.ts +++ b/src/deno/index.ts @@ -1,12 +1,18 @@ /** * The main entrypoint when using Deno as the runtime */ +import { Termios } from './termios.ts' + export async function main(): Promise { + 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; } } diff --git a/src/deno/termios.ts b/src/deno/termios.ts index 9a28695..f8c7b59 100644 --- a/src/deno/termios.ts +++ b/src/deno/termios.ts @@ -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); + } + + 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; + } } -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 default cStdLib.symbols; -export const tcgetattr = cStdLib.symbols.tcgetattr; -export const tcsetattr = cStdLib.symbols.tcsetattr; -export const cfmakeraw = cStdLib.symbols.cfmakeraw; diff --git a/src/scroll.ts b/src/scroll.ts index 2973721..8fd5339 100644 --- a/src/scroll.ts +++ b/src/scroll.ts @@ -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(); })();