From 15496646d6856c14d74d97790d29f649822904c9 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 10 Jan 2024 15:44:19 -0500 Subject: [PATCH] Minor refactoring, build up the ansi escape codes to handle colors --- src/bun/terminal_io.ts | 10 +++- src/common/all_test.ts | 96 ++++++++++++++++---------------- src/common/ansi.ts | 122 +++++++++++++++++++++++++++++++++++------ src/common/fns.ts | 8 +-- src/common/row.ts | 12 ++-- src/common/termios.ts | 4 +- src/common/types.ts | 8 +-- 7 files changed, 176 insertions(+), 84 deletions(-) diff --git a/src/bun/terminal_io.ts b/src/bun/terminal_io.ts index 2152fae..e4027eb 100644 --- a/src/bun/terminal_io.ts +++ b/src/bun/terminal_io.ts @@ -18,13 +18,17 @@ const BunTerminalIO: ITerminal = { return null; }, + /** + * Get the size of the terminal window via ANSI codes + * @see https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html#window-size-the-hard-way + */ getTerminalSize: async function getTerminalSize(): Promise { - const encoder = new TextEncoder(); - // Tell the cursor to move to Row 999 and Column 999 // Since this command specifically doesn't go off the screen // When we ask where the cursor is, we should get the size of the screen - await BunTerminalIO.writeStdout(Ansi.moveCursorForward(999) + Ansi.moveCursorDown(999)); + await BunTerminalIO.writeStdout( + Ansi.moveCursorForward(999) + Ansi.moveCursorDown(999), + ); // Ask where the cursor is await BunTerminalIO.writeStdout(Ansi.GetCursorLocation); diff --git a/src/common/all_test.ts b/src/common/all_test.ts index e797dcb..541302b 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -6,7 +6,7 @@ import { Ansi, KeyCommand } from './ansi.ts'; import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts'; import { getTestRunner } from './runtime.ts'; import { Position } from './types.ts'; -import * as Util from './fns.ts'; +import * as Fn from './fns.ts'; const { assertEquals, @@ -22,7 +22,7 @@ const { const encoder = new TextEncoder(); const testKeyMap = (codes: string[], expected: string) => { codes.forEach((code) => { - assertEquals(Util.readKey(encoder.encode(code)), expected); + assertEquals(Fn.readKey(encoder.encode(code)), expected); }); }; @@ -192,7 +192,7 @@ testSuite({ assertEquals(row.byteIndexToCharIndex(0), 0); // Return count on nonsense index - assertEquals(Util.strlen(row.toString()), 10); + assertEquals(Fn.strlen(row.toString()), 10); assertEquals(row.byteIndexToCharIndex(72), 10); const row2 = Row.from('foobar'); @@ -224,121 +224,121 @@ testSuite({ 'fns': { 'arrayInsert() strings': () => { const a = ['😺', '😸', '😹']; - const b = Util.arrayInsert(a, 1, 'x'); + const b = Fn.arrayInsert(a, 1, 'x'); const c = ['😺', 'x', '😸', '😹']; assertEquals(b, c); - const d = Util.arrayInsert(c, 17, 'y'); + const d = Fn.arrayInsert(c, 17, 'y'); const e = ['😺', 'x', '😸', '😹', 'y']; assertEquals(d, e); - assertEquals(Util.arrayInsert([], 0, 'foo'), ['foo']); + assertEquals(Fn.arrayInsert([], 0, 'foo'), ['foo']); }, 'arrayInsert() numbers': () => { const a = [1, 3, 5]; const b = [1, 3, 4, 5]; - assertEquals(Util.arrayInsert(a, 2, 4), b); + assertEquals(Fn.arrayInsert(a, 2, 4), b); const c = [1, 2, 3, 4, 5]; - assertEquals(Util.arrayInsert(b, 1, 2), c); + assertEquals(Fn.arrayInsert(b, 1, 2), c); }, 'noop fn': () => { - assertExists(Util.noop); - assertEquals(Util.noop(), undefined); + assertExists(Fn.noop); + assertEquals(Fn.noop(), undefined); }, 'posSub()': () => { - assertEquals(Util.posSub(14, 15), 0); - assertEquals(Util.posSub(15, 1), 14); + assertEquals(Fn.posSub(14, 15), 0); + assertEquals(Fn.posSub(15, 1), 14); }, 'minSub()': () => { - assertEquals(Util.minSub(13, 25, -1), -1); - assertEquals(Util.minSub(25, 13, 0), 12); + assertEquals(Fn.minSub(13, 25, -1), -1); + assertEquals(Fn.minSub(25, 13, 0), 12); }, 'maxAdd()': () => { - assertEquals(Util.maxAdd(99, 99, 75), 75); - assertEquals(Util.maxAdd(25, 74, 101), 99); + assertEquals(Fn.maxAdd(99, 99, 75), 75); + assertEquals(Fn.maxAdd(25, 74, 101), 99); }, 'ord()': () => { // Invalid output - assertEquals(Util.ord(''), 256); + assertEquals(Fn.ord(''), 256); // Valid output - assertEquals(Util.ord('a'), 97); + assertEquals(Fn.ord('a'), 97); }, - 'chars() properly splits strings into unicode characters': () => { - assertEquals(Util.chars('😺😸😹'), ['😺', '😸', '😹']); + 'strChars() properly splits strings into unicode characters': () => { + assertEquals(Fn.strChars('😺😸😹'), ['😺', '😸', '😹']); }, 'ctrlKey()': () => { - const ctrl_a = Util.ctrlKey('a'); - assertTrue(Util.isControl(ctrl_a)); + const ctrl_a = Fn.ctrlKey('a'); + assertTrue(Fn.isControl(ctrl_a)); assertEquals(ctrl_a, String.fromCodePoint(0x01)); - const invalid = Util.ctrlKey('😺'); - assertFalse(Util.isControl(invalid)); + const invalid = Fn.ctrlKey('😺'); + assertFalse(Fn.isControl(invalid)); assertEquals(invalid, '😺'); }, 'isAscii()': () => { - assertTrue(Util.isAscii('asjyverkjhsdf1928374')); - assertFalse(Util.isAscii('😺acalskjsdf')); - assertFalse(Util.isAscii('ab😺ac')); + assertTrue(Fn.isAscii('asjyverkjhsdf1928374')); + assertFalse(Fn.isAscii('😺acalskjsdf')); + assertFalse(Fn.isAscii('ab😺ac')); }, 'isControl()': () => { - assertFalse(Util.isControl('abc')); - assertTrue(Util.isControl(String.fromCodePoint(0x01))); - assertFalse(Util.isControl('😺')); + assertFalse(Fn.isControl('abc')); + assertTrue(Fn.isControl(String.fromCodePoint(0x01))); + assertFalse(Fn.isControl('😺')); }, 'strlen()': () => { // Ascii length - assertEquals(Util.strlen('abc'), 'abc'.length); + assertEquals(Fn.strlen('abc'), 'abc'.length); // Get number of visible unicode characters - assertEquals(Util.strlen('😺😸😹'), 3); - assertNotEquals('😺😸😹'.length, Util.strlen('😺😸😹')); + assertEquals(Fn.strlen('😺😸😹'), 3); + assertNotEquals('😺😸😹'.length, Fn.strlen('😺😸😹')); // Skin tone modifier + base character - assertEquals(Util.strlen('🀰🏼'), 2); - assertNotEquals('🀰🏼'.length, Util.strlen('🀰🏼')); + assertEquals(Fn.strlen('🀰🏼'), 2); + assertNotEquals('🀰🏼'.length, Fn.strlen('🀰🏼')); // This has 4 sub-characters, and 3 zero-width-joiners - assertEquals(Util.strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'), 7); - assertNotEquals('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'.length, Util.strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')); + assertEquals(Fn.strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'), 7); + assertNotEquals('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'.length, Fn.strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')); }, 'truncate()': () => { - assertEquals(Util.truncate('😺😸😹', 1), '😺'); - assertEquals(Util.truncate('😺😸😹', 5), '😺😸😹'); - assertEquals(Util.truncate('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', 5), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); + assertEquals(Fn.truncate('😺😸😹', 1), '😺'); + assertEquals(Fn.truncate('😺😸😹', 5), '😺😸😹'); + assertEquals(Fn.truncate('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', 5), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); }, }, 'readKey()': { 'empty input': () => { - assertEquals(Util.readKey(new Uint8Array(0)), ''); + assertEquals(Fn.readKey(new Uint8Array(0)), ''); }, 'passthrough': () => { // Ignore unhandled escape sequences - assertEquals(Util.readKey(encoder.encode('\x1b[]')), '\x1b[]'); + assertEquals(Fn.readKey(encoder.encode('\x1b[]')), '\x1b[]'); // Pass explicitly mapped values right through assertEquals( - Util.readKey(encoder.encode(KeyCommand.ArrowUp)), + Fn.readKey(encoder.encode(KeyCommand.ArrowUp)), KeyCommand.ArrowUp, ); assertEquals( - Util.readKey(encoder.encode(KeyCommand.Home)), + Fn.readKey(encoder.encode(KeyCommand.Home)), KeyCommand.Home, ); assertEquals( - Util.readKey(encoder.encode(KeyCommand.Delete)), + Fn.readKey(encoder.encode(KeyCommand.Delete)), KeyCommand.Delete, ); // And pass through whatever else - assertEquals(Util.readKey(encoder.encode('foobaz')), 'foobaz'); + assertEquals(Fn.readKey(encoder.encode('foobaz')), 'foobaz'); }, - 'Esc': () => testKeyMap(['\x1b', Util.ctrlKey('l')], KeyCommand.Escape), + 'Esc': () => testKeyMap(['\x1b', Fn.ctrlKey('l')], KeyCommand.Escape), 'Backspace': () => testKeyMap( - [Util.ctrlKey('h'), '\x7f'], + [Fn.ctrlKey('h'), '\x7f'], KeyCommand.Backspace, ), 'Home': () => diff --git a/src/common/ansi.ts b/src/common/ansi.ts index cd1094b..05386f0 100644 --- a/src/common/ansi.ts +++ b/src/common/ansi.ts @@ -24,24 +24,112 @@ export enum KeyCommand { End = 'LineEnd', } -export const Ansi = { - ClearLine: ANSI_PREFIX + 'K', - ClearScreen: ANSI_PREFIX + '2J', - ResetCursor: ANSI_PREFIX + 'H', - HideCursor: ANSI_PREFIX + '?25l', - ShowCursor: ANSI_PREFIX + '?25h', - GetCursorLocation: ANSI_PREFIX + '6n', - InvertColor: ANSI_PREFIX + '7m', - ResetFormatting: ANSI_PREFIX + 'm', - moveCursor: (row: number, col: number): string => { - // Convert to 1-based counting - row++; - col++; +/** + * Values for Basic ANSI colors and formatting + */ +export enum AnsiColor { + TypeRGB = 2, + Type256 = 5, - return ANSI_PREFIX + `${row};${col}H`; - }, - moveCursorForward: (col: number): string => ANSI_PREFIX + `${col}C`, - moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`, + Invert = 7, + + // Foreground Colors + FgBlack = 30, + FgRed, + FgGreen, + FgYellow, + FgBlue, + FgMagenta, + FgCyan, + FgWhite, + FgDefault, + + // Background Colors + BgBlack = 40, + BgRed, + BgGreen, + BgYellow, + BgBlue, + BgMagenta, + BgCyan, + BgWhite, + BgDefault, + + // Bright Foreground Colors + FgBrightBlack = 90, + FgBrightRed, + FgBrightGreen, + FgBrightYellow, + FgBrightBlue, + FgBrightMagenta, + FgBrightCyan, + FgBrightWhite, + + // Bright Background Colors + BgBrightBlack = 100, + BgBrightRed, + BgBrightGreen, + BgBrightYellow, + BgBrightBlue, + BgBrightMagenta, + BgBrightCyan, + BgBrightWhite, +} + +export enum Ground { + Fore = AnsiColor.FgDefault, + Back = AnsiColor.BgDefault, +} + +// ---------------------------------------------------------------------------- +// ANSI escape code generation fns +// ---------------------------------------------------------------------------- +const code = ( + param: string | number | string[] | number[], + suffix: string = '', +): string => { + if (Array.isArray(param)) { + param = param.join(';'); + } + + return [ANSI_PREFIX, param, suffix].join(''); +}; + +const moveCursor = (row: number, col: number): string => { + // Convert to 1-based counting + row++; + col++; + + return code([row, col], 'H'); +}; +const moveCursorForward = (col: number): string => code(col, 'C'); +const moveCursorDown = (row: number): string => code(row, 'B'); +const textFormat = (param: string | number | string[] | number[]): string => + code(param, 'm'); +const color256 = (value: number, ground: Ground = Ground.Fore): string => + textFormat([ground, AnsiColor.Type256, value]); +const rgb = ( + r: number, + g: number, + b: number, + ground: Ground = Ground.Fore, +): string => textFormat([ground, AnsiColor.TypeRGB, r, g, b]); + +export const Ansi = { + ClearLine: code('K'), + ClearScreen: code('2J'), + ResetCursor: code('H'), + HideCursor: code('?25l'), + ShowCursor: code('?25h'), + GetCursorLocation: code('6n'), + InvertColor: textFormat(AnsiColor.Invert), + ResetFormatting: textFormat(''), + moveCursor, + moveCursorForward, + moveCursorDown, + textFormat, + color256, + rgb, }; export default Ansi; diff --git a/src/common/fns.ts b/src/common/fns.ts index b49b412..75153d9 100644 --- a/src/common/fns.ts +++ b/src/common/fns.ts @@ -137,7 +137,7 @@ export function ord(s: string): number { * * @param s - the string to split into 'characters' */ -export function chars(s: string): string[] { +export function strChars(s: string): string[] { return s.split(/(?:)/u); } @@ -147,7 +147,7 @@ export function chars(s: string): string[] { * @param s - the string to check */ export function strlen(s: string): number { - return chars(s).length; + return strChars(s).length; } /** @@ -156,7 +156,7 @@ export function strlen(s: string): number { * @param char - string to check */ export function isAscii(char: string): boolean { - return chars(char).every((char) => ord(char) < 0x80); + return strChars(char).every((char) => ord(char) < 0x80); } /** @@ -191,7 +191,7 @@ export function ctrlKey(char: string): string { * @param maxLen */ export function truncate(s: string, maxLen: number): string { - const chin = chars(s); + const chin = strChars(s); if (maxLen >= chin.length) { return s; } diff --git a/src/common/row.ts b/src/common/row.ts index d2934cb..ab86b30 100644 --- a/src/common/row.ts +++ b/src/common/row.ts @@ -17,7 +17,7 @@ export class Row { render: string[] = []; private constructor(s: string | string[] = '') { - this.chars = Array.isArray(s) ? s : Util.chars(s); + this.chars = Array.isArray(s) ? s : Util.strChars(s); this.render = []; } @@ -46,12 +46,12 @@ export class Row { } public append(s: string): void { - this.chars = this.chars.concat(Util.chars(s)); + this.chars = this.chars.concat(Util.strChars(s)); this.updateRender(); } public insertChar(at: number, c: string): void { - const newSlice = Util.chars(c); + const newSlice = Util.strChars(c); if (at >= this.size) { this.chars = this.chars.concat(newSlice); } else { @@ -96,8 +96,8 @@ export class Row { public cxToRx(cx: number): number { let rx = 0; - let j = 0; - for (; j < cx; j++) { + let j; + for (j = 0; j < cx; j++) { if (this.chars[j] === '\t') { rx += (SCROLL_TAB_SIZE - 1) - (rx % SCROLL_TAB_SIZE); } @@ -162,7 +162,7 @@ export class Row { ' '.repeat(SCROLL_TAB_SIZE), ); - this.render = Util.chars(newString); + this.render = Util.strChars(newString); } } diff --git a/src/common/termios.ts b/src/common/termios.ts index b8788cc..e49cd0e 100644 --- a/src/common/termios.ts +++ b/src/common/termios.ts @@ -90,7 +90,7 @@ class Termios { // Actually set the new termios settings res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr); if (res === -1) { - die('Failed to update terminal settings. Can\'t enter raw mode'); + die("Failed to update terminal settings. Can't enter raw mode"); } this.#inRawMode = true; @@ -101,7 +101,7 @@ class Termios { // and aren't in raw mode. It just doesn't really matter. if (!this.#inRawMode) { log( - 'Attampting to disable raw mode when not in raw mode', + 'Attempting to disable raw mode when not in raw mode', LogLevel.Warning, ); return; diff --git a/src/common/types.ts b/src/common/types.ts index ff538d7..d7953a6 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,9 +1,5 @@ import { RunTimeType } from './runtime.ts'; -// ---------------------------------------------------------------------------- -// Runtime adapter interfaces -// ---------------------------------------------------------------------------- - /** * The size of terminal in rows and columns */ @@ -43,6 +39,10 @@ export interface IFFI { close(): void; } +// ---------------------------------------------------------------------------- +// Runtime adapter interfaces +// ---------------------------------------------------------------------------- + /** * The common interface for runtime adapters */