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
*/
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<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 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),

View File

@ -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;

View File

@ -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;

View File

@ -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<Uint8Array, void>;
inputLoop(): AsyncGenerator<string, void>;
/**
* 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);

View File

@ -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<ITerminalSize> {