From 820d383c3a4632eacab442ffc0c7bc953b033576 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 24 Nov 2023 08:31:51 -0500 Subject: [PATCH] Get parsed keypresses from input loop --- src/bun/terminal_io.ts | 5 +++-- src/common/all_test.ts | 23 ++++++++++++------- src/common/ansi.ts | 42 ---------------------------------- src/common/main.ts | 5 +---- src/common/runtime.ts | 50 ++++++++++++++++++++++++++++++++++++++--- src/deno/terminal_io.ts | 4 ++-- 6 files changed, 68 insertions(+), 61 deletions(-) diff --git a/src/bun/terminal_io.ts b/src/bun/terminal_io.ts index 9299b08..51c5951 100644 --- a/src/bun/terminal_io.ts +++ b/src/bun/terminal_io.ts @@ -1,12 +1,13 @@ /** * Wrap the runtime-specific hook into stdin */ +import Ansi from '../common/ansi.ts'; import { defaultTerminalSize, ITerminal, ITerminalSize, + readKey, } from '../common/mod.ts'; -import Ansi from '../common/ansi.ts'; const BunTerminalIO: ITerminal = { // Deno only returns arguments passed to the script, so @@ -15,7 +16,7 @@ const BunTerminalIO: ITerminal = { argv: (Bun.argv.length > 2) ? Bun.argv.slice(2) : [], inputLoop: async function* inputLoop() { for await (const chunk of Bun.stdin.stream()) { - yield chunk; + yield readKey(chunk); } }, getTerminalSize: async function getTerminalSize(): Promise { diff --git a/src/common/all_test.ts b/src/common/all_test.ts index 3be6288..1d08b27 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -1,9 +1,9 @@ -import { Ansi, KeyCommand, readKey } from './ansi.ts'; +import { Ansi, KeyCommand } from './ansi.ts'; import Buffer from './buffer.ts'; import Document from './document.ts'; import Editor from './editor.ts'; import Row from './row.ts'; -import { getTestRunner } from './runtime.ts'; +import { getTestRunner, readKey } from './runtime.ts'; import { defaultTerminalSize } from './termios.ts'; import { Position } from './types.ts'; import * as Util from './utils.ts'; @@ -18,9 +18,10 @@ const { testSuite, } = await getTestRunner(); +const encoder = new TextEncoder(); const testKeyMap = (codes: string[], expected: string) => { codes.forEach((code) => { - assertEquals(readKey(code), expected); + assertEquals(readKey(encoder.encode(code)), expected); }); }; @@ -39,15 +40,21 @@ testSuite({ 'ANSI::readKey()': { 'readKey passthrough': () => { // Ignore unhandled escape sequences - assertEquals(readKey('\x1b[]'), '\x1b[]'); + assertEquals(readKey(encoder.encode('\x1b[]')), '\x1b[]'); // Pass explicitly mapped values right through - assertEquals(readKey(KeyCommand.ArrowUp), KeyCommand.ArrowUp); - assertEquals(readKey(KeyCommand.Home), KeyCommand.Home); - assertEquals(readKey(KeyCommand.Delete), KeyCommand.Delete); + assertEquals( + readKey(encoder.encode(KeyCommand.ArrowUp)), + KeyCommand.ArrowUp, + ); + assertEquals(readKey(encoder.encode(KeyCommand.Home)), KeyCommand.Home); + assertEquals( + readKey(encoder.encode(KeyCommand.Delete)), + KeyCommand.Delete, + ); // And pass through whatever else - assertEquals(readKey('foobaz'), 'foobaz'); + assertEquals(readKey(encoder.encode('foobaz')), 'foobaz'); }, 'readKey Esc': () => testKeyMap(['\x1b', Util.ctrlKey('l')], KeyCommand.Escape), diff --git a/src/common/ansi.ts b/src/common/ansi.ts index d29d31b..cc17b44 100644 --- a/src/common/ansi.ts +++ b/src/common/ansi.ts @@ -1,7 +1,6 @@ /** * ANSI/VT terminal escape code handling */ -import { ctrlKey } from './utils.ts'; export const ANSI_PREFIX = '\x1b['; @@ -45,45 +44,4 @@ export const Ansi = { moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`, }; -/** - * Convert input from ANSI escape sequences into a form - * that can be more easily mapped to editor commands - * - * @param parsed - the decoded chunk of input - */ -export function readKey(parsed: string): string { - // Return the input if it's unambiguous - if (parsed in KeyCommand) { - return parsed; - } - - // Some keycodes have multiple potential inputs - switch (parsed) { - case '\x1b[1~': - case '\x1b[7~': - case '\x1bOH': - case '\x1b[H': - return KeyCommand.Home; - - case '\x1b[4~': - case '\x1b[8~': - case '\x1bOF': - case '\x1b[F': - return KeyCommand.End; - - case '\n': - case '\v': - return KeyCommand.Enter; - - case ctrlKey('l'): - return KeyCommand.Escape; - - case ctrlKey('h'): - return KeyCommand.Backspace; - - default: - return parsed; - } -} - export default Ansi; diff --git a/src/common/main.ts b/src/common/main.ts index 9afc4e2..01c9454 100644 --- a/src/common/main.ts +++ b/src/common/main.ts @@ -1,10 +1,8 @@ -import { readKey } from './ansi.ts'; import { getRuntime } from './runtime.ts'; import { getTermios } from './termios.ts'; import Editor from './editor.ts'; export async function main() { - const decoder = new TextDecoder(); const { term, file, onExit, onEvent } = await getRuntime(); // Setup raw mode, and tear down on error or normal exit @@ -38,9 +36,8 @@ export async function main() { // Clear the screen await editor.refreshScreen(); - for await (const chunk of term.inputLoop()) { + for await (const char of term.inputLoop()) { // Process input - const char = readKey(decoder.decode(chunk)); const shouldLoop = await editor.processKeyPress(char); if (!shouldLoop) { return 0; diff --git a/src/common/runtime.ts b/src/common/runtime.ts index b06dcc1..9e6fd41 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime.ts @@ -1,6 +1,7 @@ import { getTermios } from './termios.ts'; -import { noop } from './utils.ts'; +import { ctrlKey, noop } from './utils.ts'; import { ITestBase } from './types.ts'; +import { KeyCommand } from './ansi.ts'; export enum RunTimeType { Bun = 'bun', @@ -63,7 +64,7 @@ export interface ITerminal { /** * The generator function returning chunks of input from the stdin stream */ - inputLoop(): AsyncGenerator; + inputLoop(): AsyncGenerator; /** * Get the size of the terminal @@ -134,9 +135,52 @@ export interface IRuntime { // ---------------------------------------------------------------------------- // Misc runtime functions // ---------------------------------------------------------------------------- - +const decoder = new TextDecoder(); let scrollRuntime: IRuntime | null = null; +/** + * Convert input from ANSI escape sequences into a form + * that can be more easily mapped to editor commands + * + * @param raw - the raw chunk of input + */ +export function readKey(raw: Uint8Array): string { + const parsed = decoder.decode(raw); + + // Return the input if it's unambiguous + if (parsed in KeyCommand) { + return parsed; + } + + // Some keycodes have multiple potential inputs + switch (parsed) { + case '\x1b[1~': + case '\x1b[7~': + case '\x1bOH': + case '\x1b[H': + return KeyCommand.Home; + + case '\x1b[4~': + case '\x1b[8~': + case '\x1bOF': + case '\x1b[F': + return KeyCommand.End; + + case '\n': + case '\v': + return KeyCommand.Enter; + + case ctrlKey('l'): + return KeyCommand.Escape; + + case ctrlKey('h'): + return KeyCommand.Backspace; + + default: + return parsed; + } +} + export function logToFile(s: unknown) { importForRuntime('file_io').then((f) => { const raw = (typeof s === 'string') ? s : JSON.stringify(s, null, 2); diff --git a/src/deno/terminal_io.ts b/src/deno/terminal_io.ts index 1c97ce3..200e2a2 100644 --- a/src/deno/terminal_io.ts +++ b/src/deno/terminal_io.ts @@ -1,4 +1,4 @@ -import { ITerminal, ITerminalSize } from '../common/runtime.ts'; +import { ITerminal, ITerminalSize, readKey } from '../common/runtime.ts'; const DenoTerminalIO: ITerminal = { argv: Deno.args, @@ -7,7 +7,7 @@ const DenoTerminalIO: ITerminal = { */ inputLoop: async function* inputLoop() { for await (const chunk of Deno.stdin.readable) { - yield chunk; + yield readKey(chunk); } }, getTerminalSize: function getSize(): Promise {