Minor refactoring, build up the ansi escape codes to handle colors
This commit is contained in:
parent
30230520a0
commit
15496646d6
@ -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);
|
||||
|
@ -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': () =>
|
||||
|
@ -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 => {
|
||||
/**
|
||||
* Values for Basic ANSI colors and formatting
|
||||
*/
|
||||
export enum AnsiColor {
|
||||
TypeRGB = 2,
|
||||
Type256 = 5,
|
||||
|
||||
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 ANSI_PREFIX + `${row};${col}H`;
|
||||
},
|
||||
moveCursorForward: (col: number): string => ANSI_PREFIX + `${col}C`,
|
||||
moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`,
|
||||
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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user