Minor refactoring, build up the ansi escape codes to handle colors

This commit is contained in:
Timothy Warren 2024-01-10 15:44:19 -05:00
parent 30230520a0
commit 15496646d6
7 changed files with 176 additions and 84 deletions

View File

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

View File

@ -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': () =>

View File

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

View File

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

View File

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

View File

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

View File

@ -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
*/