diff --git a/src/bun/terminal_io.ts b/src/bun/terminal_io.ts index 4b5ac46..8fbb3d6 100644 --- a/src/bun/terminal_io.ts +++ b/src/bun/terminal_io.ts @@ -16,8 +16,10 @@ 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 readKey(chunk); + yield chunk; } + + return null; }, getTerminalSize: async function getTerminalSize(): Promise { const encoder = new TextEncoder(); @@ -33,28 +35,32 @@ const BunTerminalIO: ITerminal = { // Get the first chunk from stdin // The response is \x1b[(rows);(cols)R.. - for await (const chunk of Bun.stdin.stream()) { - const rawCode = (new TextDecoder()).decode(chunk); - const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1'); - const [srows, scols] = res.split(';'); - const rows = parseInt(srows, 10) ?? 24; - const cols = parseInt(scols, 10) ?? 80; - - // Clear the screen - await write(Ansi.ClearScreen + Ansi.ResetCursor); - - return { - rows, - cols, - }; + const chunk = await BunTerminalIO.readStdinRaw(); + if (chunk === null) { + return defaultTerminalSize; } - return defaultTerminalSize; + const rawCode = (new TextDecoder()).decode(chunk); + const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1'); + const [srows, scols] = res.split(';'); + const rows = parseInt(srows, 10) ?? 24; + const cols = parseInt(scols, 10) ?? 80; + + // Clear the screen + await write(Ansi.ClearScreen + Ansi.ResetCursor); + + return { + rows, + cols, + }; }, - readStdin: async function (): Promise { - const gen = BunTerminalIO.inputLoop(); - const chunk = await gen.next(); - return chunk.value!; + readStdin: async function (): Promise { + const raw = await BunTerminalIO.readStdinRaw(); + return readKey(raw ?? new Uint8Array(0)); + }, + readStdinRaw: async function (): Promise { + const chunk = await BunTerminalIO.inputLoop().next(); + return chunk.value ?? null; }, writeStdout: async function write(s: string): Promise { const buffer = new TextEncoder().encode(s); diff --git a/src/common/editor.ts b/src/common/editor.ts index 7d05c48..ca0d563 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -7,6 +7,7 @@ import { ITerminalSize, logToFile, Position, + readKey, SCROLL_QUIT_TIMES, SCROLL_VERSION, } from './mod.ts'; @@ -229,23 +230,35 @@ class Editor { let res = ''; - while (true) { + outer: while (true) { this.setStatusMessage(`${p}${res}`); await this.refreshScreen(); - const char = await term.readStdin(); - // End the prompt - if (char === KeyCommand.Enter) { - this.setStatusMessage(''); - if (res.length === 0) { - return null; + for await (const chunk of term.inputLoop()) { + const char = readKey(chunk); + if (char === null) { + continue; } - return res; - } + // End the prompt + if (char === KeyCommand.Enter) { + this.setStatusMessage(''); + if (res.length === 0) { + return null; + } - // Add to the prompt result - if (!isControl(char)) { - res += char; + return res; + } + + // Allow backspacing + if (char === KeyCommand.Backspace || char === KeyCommand.Delete) { + res = truncate(res, res.length - 1); + continue outer; + } + + // Add to the prompt result + if (!isControl(char!)) { + res += char; + } } } } diff --git a/src/common/main.ts b/src/common/main.ts index 7e0d2c7..91aa0fc 100644 --- a/src/common/main.ts +++ b/src/common/main.ts @@ -1,4 +1,4 @@ -import { getRuntime } from './runtime.ts'; +import { getRuntime, readKey } from './runtime.ts'; import { getTermios } from './termios.ts'; import Editor from './editor.ts'; @@ -37,16 +37,21 @@ export async function main() { // Clear the screen await editor.refreshScreen(); - for await (const char of term.inputLoop()) { - // Process input - const shouldLoop = await editor.processKeyPress(char); - if (!shouldLoop) { - return 0; + while (true) { + for await (const char of term.inputLoop()) { + const parsed = readKey(char); + if (char.length === 0 || parsed.length === 0) { + continue; + } + + // Process input + const shouldLoop = await editor.processKeyPress(parsed); + if (!shouldLoop) { + return; + } + + // Render output + await editor.refreshScreen(); } - - // Render output - await editor.refreshScreen(); } - - return -1; } diff --git a/src/common/runtime.ts b/src/common/runtime.ts index f953de1..a316ef0 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime.ts @@ -64,14 +64,22 @@ export interface ITerminal { /** * The generator function returning chunks of input from the stdin stream */ - inputLoop(): AsyncGenerator; + inputLoop(): AsyncGenerator; /** * Get the size of the terminal */ getTerminalSize(): Promise; - readStdin(): Promise; + /** + * Get the current chunk of input, if it exists + */ + readStdin(): Promise; + + /** + * Get the raw chunk of input + */ + readStdinRaw(): Promise; /** * Pipe a string to stdout @@ -147,6 +155,9 @@ let scrollRuntime: IRuntime | null = null; * @param raw - the raw chunk of input */ export function readKey(raw: Uint8Array): string { + if (raw.length === 0) { + return ''; + } const parsed = decoder.decode(raw); // Return the input if it's unambiguous diff --git a/src/deno/terminal_io.ts b/src/deno/terminal_io.ts index 5f62f83..5e73245 100644 --- a/src/deno/terminal_io.ts +++ b/src/deno/terminal_io.ts @@ -2,13 +2,10 @@ import { ITerminal, ITerminalSize, readKey } from '../common/runtime.ts'; const DenoTerminalIO: ITerminal = { argv: Deno.args, - /** - * Wrap the runtime-specific hook into stdin - */ - inputLoop: async function* inputLoop() { - for await (const chunk of Deno.stdin.readable) { - yield readKey(chunk); - } + inputLoop: async function* (): AsyncGenerator { + yield await DenoTerminalIO.readStdinRaw() ?? new Uint8Array(0); + + return null; }, getTerminalSize: function getSize(): Promise { const size: { rows: number; columns: number } = Deno.consoleSize(); @@ -18,10 +15,16 @@ const DenoTerminalIO: ITerminal = { cols: size.columns, }); }, - readStdin: async function (): Promise { - const gen = DenoTerminalIO.inputLoop(); - const chunk = await gen.next(); - return chunk.value!; + readStdin: async function (): Promise { + const raw = await DenoTerminalIO.readStdinRaw(); + return readKey(raw ?? new Uint8Array(0)); + }, + readStdinRaw: async function (): Promise { + const reader = Deno.stdin.readable.getReader(); + const chunk = await reader.read(); + const res = chunk?.value; + reader.releaseLock(); + return res ?? null; }, writeStdout: async function write(s: string): Promise { const buffer: Uint8Array = new TextEncoder().encode(s);