From 7dcd42da139375eaa0926991aa6548edf5eb9ba6 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 2 Nov 2023 13:06:48 -0400 Subject: [PATCH] Set up basic ascii parsing and display --- deno.jsonc | 12 ++++---- justfile | 16 +++++++--- src/bun/index.ts | 14 +++++++-- src/common/index.ts | 7 +++++ src/common/strings.ts | 60 ++++++++++++++++++++++++++++++++++++++ src/common/strings_test.ts | 8 +++++ src/common/termios.ts | 37 ++++++++++++++++++----- src/deno/index.ts | 11 +++++-- src/deno/test_base.ts | 7 +++++ 9 files changed, 149 insertions(+), 23 deletions(-) create mode 100644 src/common/strings.ts create mode 100644 src/common/strings_test.ts create mode 100644 src/deno/test_base.ts diff --git a/deno.jsonc b/deno.jsonc index a550008..c82e5a1 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,11 +1,9 @@ { - "imports": { - "std": "https://deno.land/std@0.204.0/", - }, + "exclude": ["src/bun/**/*.ts"], "lint": { "include": ["src/"], "rules": { - "tags": ["recommended"], + "tags": ["recommended"] } }, "fmt": { @@ -13,7 +11,7 @@ "lineWidth": 80, "indentWidth": 2, "semiColons": true, - "singleQuote": true, + "singleQuote": true }, - "nodeModulesDir": true, -} \ No newline at end of file + "nodeModulesDir": true +} diff --git a/justfile b/justfile index 1874b09..e5ae9cd 100644 --- a/justfile +++ b/justfile @@ -2,12 +2,17 @@ default: @just --list -# Code linting with deno +# Typescript checking +check: + deno check --unstable --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts + +# Code linting deno-lint: deno lint -# Code linting with bun -bun-lint: +# Reformat the code +fmt: + deno fmt # Run with bun bun-run: @@ -15,4 +20,7 @@ bun-run: # Run with deno deno-run: - deno run --allow-all --allow-ffi --deny-net --deny-hrtime --unstable ./src/scroll.ts \ No newline at end of file + deno run --allow-all --allow-ffi --deny-net --deny-hrtime --unstable ./src/scroll.ts + +deno-test: + deno test --allow-all \ No newline at end of file diff --git a/src/bun/index.ts b/src/bun/index.ts index b8a8627..74db1ab 100644 --- a/src/bun/index.ts +++ b/src/bun/index.ts @@ -3,6 +3,7 @@ */ import { getTermios } from '../common/termios'; +import {ctrl_key, is_control} from "../common/strings"; export async function main(): Promise { const t = await getTermios(); @@ -10,12 +11,19 @@ export async function main(): Promise { const decoder = new TextDecoder(); for await (const chunk of Bun.stdin.stream()) { - const char = String(decoder.decode(chunk)).trim(); + const char = String(decoder.decode(chunk)); - if (char === 'q') { + if (char === ctrl_key('q')) { t.disableRawMode(); return 0; } + + if (is_control(char)) { + console.log(char.codePointAt(0) + '\r'); + } else { + console.log(`${char} ('${char.codePointAt(0)}')\r`); + } + } process.on('exit', (code) => { @@ -24,4 +32,4 @@ export async function main(): Promise { }); return -1; -} +} \ No newline at end of file diff --git a/src/common/index.ts b/src/common/index.ts index bcd538c..889b459 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,9 +1,16 @@ +import { getTermios } from './termios.ts'; + export enum RunTime { Bun = 'bun', Deno = 'deno', Unknown = 'common', } +export function die(s: string): void { + getTermios().then((t) => t.disableRawMode()); + console.error(s); +} + /** * Determine which Typescript runtime we are operating under */ diff --git a/src/common/strings.ts b/src/common/strings.ts new file mode 100644 index 0000000..3bbc44f --- /dev/null +++ b/src/common/strings.ts @@ -0,0 +1,60 @@ +/** + * Split a string by graphemes, not just bytes + * @param s - the string to split into 'characters' + */ +export function chars(s: string): string[] { + return s.split(/(?:)/u); +} + +/** + * Is the character part of ascii? + * + * @param char - a one character string to check + */ +export function is_ascii(char: string): boolean { + if (typeof char !== 'string') { + return false; + } + + return chars(char).every((char) => { + const point = char.codePointAt(0); + if (typeof point === 'undefined') { + return false; + } + + return point < 0x80; + }); +} + +/** + * Is the one char in the string an ascii control character? + * + * @param char - a one character string to check + */ +export function is_control(char: string): boolean { + const code = char.codePointAt(0); + if (code === undefined) { + return false; + } + + return is_ascii(char) && (code === 0x7f || code < 0x20); +} + +/** + * Get the key code for a ctrl chord + * @param char - a one character string + */ +export function ctrl_key(char: string): string { + // This is the normal use case, of course + if (is_ascii(char)) { + const point = char.codePointAt(0); + if (point === undefined) { + return char; + } + + return String.fromCodePoint(point & 0x1f); + } + + // If it's not ascii, just return the input key code + return char; +} diff --git a/src/common/strings_test.ts b/src/common/strings_test.ts new file mode 100644 index 0000000..df92376 --- /dev/null +++ b/src/common/strings_test.ts @@ -0,0 +1,8 @@ +import { importForRuntime } from './index.ts'; +import { chars } from './strings.ts'; + +const { test, assertEquals } = await importForRuntime('test_base'); + +test('chars fn properly splits strings into unicode characters', () => { + assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']); +}); diff --git a/src/common/termios.ts b/src/common/termios.ts index a64c48f..66ab955 100644 --- a/src/common/termios.ts +++ b/src/common/termios.ts @@ -1,4 +1,4 @@ -import { importForRuntime } from './index.ts'; +import { die, importForRuntime } from './index.ts'; export const STDIN_FILENO = 0; export const STOUT_FILENO = 1; @@ -27,7 +27,13 @@ export interface ITermios { disableRawMode(): void; } +let termiosSingleton: ITermios | null = null; + export const getTermios = async () => { + if (termiosSingleton !== null) { + return termiosSingleton; + } + // Get the runtime-specific ffi wrappers const { tcgetattr, tcsetattr, cfmakeraw, getPointer } = await importForRuntime('ffi'); @@ -81,18 +87,30 @@ export const getTermios = async () => { } // Get the current termios settings - tcgetattr(STDIN_FILENO, this.#ptr); + 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 raw settings - cfmakeraw(this.#ptr); + // 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 - tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr); + res = tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr); + if (res === -1) { + die('Failed to update terminal settings. Can\'t enter raw mode'); + } + this.#inRawMode = true; } @@ -104,11 +122,16 @@ export const getTermios = async () => { } const oldTermiosPtr = getPointer(this.#cookedTermios); - tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr); + let res = tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr); + if (res === -1) { + die('Failed to restore canonical mode.'); + } this.#inRawMode = false; } } - return new Termios(); + termiosSingleton = new Termios(); + + return termiosSingleton; }; diff --git a/src/deno/index.ts b/src/deno/index.ts index 6e844d5..276e0fc 100644 --- a/src/deno/index.ts +++ b/src/deno/index.ts @@ -2,6 +2,7 @@ * The main entrypoint when using Deno as the runtime */ import { getTermios } from '../common/termios.ts'; +import { ctrl_key, is_control } from '../common/strings.ts'; export async function main(): Promise { const t = await getTermios(); @@ -9,12 +10,18 @@ export async function main(): Promise { const decoder = new TextDecoder(); for await (const chunk of Deno.stdin.readable) { - const char = String(decoder.decode(chunk)).trim(); + const char = String(decoder.decode(chunk)); - if (char === 'q') { + if (char === ctrl_key('q')) { t.disableRawMode(); return 0; } + + if (is_control(char)) { + console.log(char.codePointAt(0) + '\r'); + } else { + console.log(`${char} ('${char.codePointAt(0)}')\r`); + } } return -1; diff --git a/src/deno/test_base.ts b/src/deno/test_base.ts new file mode 100644 index 0000000..c55b2aa --- /dev/null +++ b/src/deno/test_base.ts @@ -0,0 +1,7 @@ +export const test = Deno.test; +export { + assert, + assertEquals, + assertNotEquals, + assertStrictEquals, +} from 'https://deno.land/std/assert/mod.ts';