Get parsed keypresses from input loop
This commit is contained in:
parent
c466788b9e
commit
820d383c3a
@ -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> {
|
||||||
|
@ -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),
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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> {
|
||||||
|
Loading…
Reference in New Issue
Block a user