Get parsed keypresses from input loop

This commit is contained in:
Timothy Warren 2023-11-24 08:31:51 -05:00
parent c466788b9e
commit 820d383c3a
6 changed files with 68 additions and 61 deletions

View File

@ -1,12 +1,13 @@
/** /**
* Wrap the runtime-specific hook into stdin * Wrap the runtime-specific hook into stdin
*/ */
import Ansi from '../common/ansi.ts';
import { import {
defaultTerminalSize, defaultTerminalSize,
ITerminal, ITerminal,
ITerminalSize, ITerminalSize,
readKey,
} from '../common/mod.ts'; } from '../common/mod.ts';
import Ansi from '../common/ansi.ts';
const BunTerminalIO: ITerminal = { const BunTerminalIO: ITerminal = {
// Deno only returns arguments passed to the script, so // 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) : [], argv: (Bun.argv.length > 2) ? Bun.argv.slice(2) : [],
inputLoop: async function* inputLoop() { inputLoop: async function* inputLoop() {
for await (const chunk of Bun.stdin.stream()) { for await (const chunk of Bun.stdin.stream()) {
yield chunk; yield readKey(chunk);
} }
}, },
getTerminalSize: async function getTerminalSize(): Promise<ITerminalSize> { getTerminalSize: async function getTerminalSize(): Promise<ITerminalSize> {

View File

@ -1,9 +1,9 @@
import { Ansi, KeyCommand, readKey } from './ansi.ts'; import { Ansi, KeyCommand } from './ansi.ts';
import Buffer from './buffer.ts'; import Buffer from './buffer.ts';
import Document from './document.ts'; import Document from './document.ts';
import Editor from './editor.ts'; import Editor from './editor.ts';
import Row from './row.ts'; import Row from './row.ts';
import { getTestRunner } from './runtime.ts'; import { getTestRunner, readKey } from './runtime.ts';
import { defaultTerminalSize } from './termios.ts'; import { defaultTerminalSize } from './termios.ts';
import { Position } from './types.ts'; import { Position } from './types.ts';
import * as Util from './utils.ts'; import * as Util from './utils.ts';
@ -18,9 +18,10 @@ const {
testSuite, testSuite,
} = await getTestRunner(); } = await getTestRunner();
const encoder = new TextEncoder();
const testKeyMap = (codes: string[], expected: string) => { const testKeyMap = (codes: string[], expected: string) => {
codes.forEach((code) => { codes.forEach((code) => {
assertEquals(readKey(code), expected); assertEquals(readKey(encoder.encode(code)), expected);
}); });
}; };
@ -39,15 +40,21 @@ testSuite({
'ANSI::readKey()': { 'ANSI::readKey()': {
'readKey passthrough': () => { 'readKey passthrough': () => {
// Ignore unhandled escape sequences // Ignore unhandled escape sequences
assertEquals(readKey('\x1b[]'), '\x1b[]'); assertEquals(readKey(encoder.encode('\x1b[]')), '\x1b[]');
// Pass explicitly mapped values right through // Pass explicitly mapped values right through
assertEquals(readKey(KeyCommand.ArrowUp), KeyCommand.ArrowUp); assertEquals(
assertEquals(readKey(KeyCommand.Home), KeyCommand.Home); readKey(encoder.encode(KeyCommand.ArrowUp)),
assertEquals(readKey(KeyCommand.Delete), KeyCommand.Delete); KeyCommand.ArrowUp,
);
assertEquals(readKey(encoder.encode(KeyCommand.Home)), KeyCommand.Home);
assertEquals(
readKey(encoder.encode(KeyCommand.Delete)),
KeyCommand.Delete,
);
// And pass through whatever else // And pass through whatever else
assertEquals(readKey('foobaz'), 'foobaz'); assertEquals(readKey(encoder.encode('foobaz')), 'foobaz');
}, },
'readKey Esc': () => 'readKey Esc': () =>
testKeyMap(['\x1b', Util.ctrlKey('l')], KeyCommand.Escape), testKeyMap(['\x1b', Util.ctrlKey('l')], KeyCommand.Escape),

View File

@ -1,7 +1,6 @@
/** /**
* ANSI/VT terminal escape code handling * ANSI/VT terminal escape code handling
*/ */
import { ctrlKey } from './utils.ts';
export const ANSI_PREFIX = '\x1b['; export const ANSI_PREFIX = '\x1b[';
@ -45,45 +44,4 @@ export const Ansi = {
moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`, 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; export default Ansi;

View File

@ -1,10 +1,8 @@
import { readKey } from './ansi.ts';
import { getRuntime } from './runtime.ts'; import { getRuntime } from './runtime.ts';
import { getTermios } from './termios.ts'; import { getTermios } from './termios.ts';
import Editor from './editor.ts'; import Editor from './editor.ts';
export async function main() { export async function main() {
const decoder = new TextDecoder();
const { term, file, onExit, onEvent } = await getRuntime(); const { term, file, onExit, onEvent } = await getRuntime();
// Setup raw mode, and tear down on error or normal exit // Setup raw mode, and tear down on error or normal exit
@ -38,9 +36,8 @@ export async function main() {
// Clear the screen // Clear the screen
await editor.refreshScreen(); await editor.refreshScreen();
for await (const chunk of term.inputLoop()) { for await (const char of term.inputLoop()) {
// Process input // Process input
const char = readKey(decoder.decode(chunk));
const shouldLoop = await editor.processKeyPress(char); const shouldLoop = await editor.processKeyPress(char);
if (!shouldLoop) { if (!shouldLoop) {
return 0; return 0;

View File

@ -1,6 +1,7 @@
import { getTermios } from './termios.ts'; import { getTermios } from './termios.ts';
import { noop } from './utils.ts'; import { ctrlKey, noop } from './utils.ts';
import { ITestBase } from './types.ts'; import { ITestBase } from './types.ts';
import { KeyCommand } from './ansi.ts';
export enum RunTimeType { export enum RunTimeType {
Bun = 'bun', Bun = 'bun',
@ -63,7 +64,7 @@ export interface ITerminal {
/** /**
* The generator function returning chunks of input from the stdin stream * The generator function returning chunks of input from the stdin stream
*/ */
inputLoop(): AsyncGenerator<Uint8Array, void>; inputLoop(): AsyncGenerator<string, void>;
/** /**
* Get the size of the terminal * Get the size of the terminal
@ -134,9 +135,52 @@ export interface IRuntime {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Misc runtime functions // Misc runtime functions
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
const decoder = new TextDecoder();
let scrollRuntime: IRuntime | null = null; 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) { export function logToFile(s: unknown) {
importForRuntime('file_io').then((f) => { importForRuntime('file_io').then((f) => {
const raw = (typeof s === 'string') ? s : JSON.stringify(s, null, 2); const raw = (typeof s === 'string') ? s : JSON.stringify(s, null, 2);

View File

@ -1,4 +1,4 @@
import { ITerminal, ITerminalSize } from '../common/runtime.ts'; import { ITerminal, ITerminalSize, readKey } from '../common/runtime.ts';
const DenoTerminalIO: ITerminal = { const DenoTerminalIO: ITerminal = {
argv: Deno.args, argv: Deno.args,
@ -7,7 +7,7 @@ const DenoTerminalIO: ITerminal = {
*/ */
inputLoop: async function* inputLoop() { inputLoop: async function* inputLoop() {
for await (const chunk of Deno.stdin.readable) { for await (const chunk of Deno.stdin.readable) {
yield chunk; yield readKey(chunk);
} }
}, },
getTerminalSize: function getSize(): Promise<ITerminalSize> { getTerminalSize: function getSize(): Promise<ITerminalSize> {