2023-11-29 16:09:58 -05:00
|
|
|
import { KeyCommand } from './ansi.ts';
|
|
|
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
2023-11-16 16:00:03 -05:00
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Misc
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
|
2023-11-29 16:09:58 -05:00
|
|
|
/**
|
|
|
|
* An empty function
|
|
|
|
*/
|
|
|
|
export const noop = () => {};
|
|
|
|
|
2024-06-26 13:40:42 -04:00
|
|
|
/**
|
|
|
|
* Does a value exist? (not null or undefined)
|
|
|
|
*/
|
|
|
|
export function defined(v: unknown): boolean {
|
|
|
|
return v !== null && typeof v !== 'undefined';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Is the value null or undefined?
|
|
|
|
*/
|
|
|
|
export function nullish(v: unknown): boolean {
|
|
|
|
return v === null || typeof v === 'undefined';
|
|
|
|
}
|
|
|
|
|
2023-11-29 16:09:58 -05:00
|
|
|
/**
|
|
|
|
* 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 {
|
|
|
|
if (raw.length === 0) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Array manipulation
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
|
2023-11-22 17:09:41 -05:00
|
|
|
/**
|
|
|
|
* Insert a value into an array at the specified index
|
|
|
|
* @param arr - the array
|
|
|
|
* @param at - the index to insert at
|
|
|
|
* @param value - what to add into the array
|
|
|
|
*/
|
|
|
|
export function arrayInsert<T>(
|
|
|
|
arr: Array<T>,
|
|
|
|
at: number,
|
|
|
|
value: T | Array<T>,
|
|
|
|
): Array<T> {
|
|
|
|
const insert = Array.isArray(value) ? value : [value];
|
|
|
|
if (at >= arr.length) {
|
|
|
|
arr.push(...insert);
|
|
|
|
return arr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return [...arr.slice(0, at), ...insert, ...arr.slice(at)];
|
|
|
|
}
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Math
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
|
2023-11-20 14:21:42 -05:00
|
|
|
/**
|
|
|
|
* Subtract two numbers, returning a zero if the result is negative
|
|
|
|
* @param l
|
|
|
|
* @param s
|
|
|
|
*/
|
|
|
|
export function posSub(l: number, s: number): number {
|
|
|
|
return minSub(l, s, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Subtract two numbers, returning at least the minimum specified
|
|
|
|
* @param l
|
|
|
|
* @param s
|
|
|
|
* @param min
|
|
|
|
*/
|
|
|
|
export function minSub(l: number, s: number, min: number): number {
|
|
|
|
return Math.max(l - s, min);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add two numbers, up to a max value
|
|
|
|
* @param n1
|
|
|
|
* @param n2
|
|
|
|
* @param max
|
|
|
|
*/
|
|
|
|
export function maxAdd(n1: number, n2: number, max: number): number {
|
|
|
|
return Math.min(n1 + n2, max);
|
|
|
|
}
|
|
|
|
|
2023-11-10 08:36:18 -05:00
|
|
|
// ----------------------------------------------------------------------------
|
2023-11-09 12:32:41 -05:00
|
|
|
// Strings
|
2023-11-10 08:36:18 -05:00
|
|
|
// ----------------------------------------------------------------------------
|
2023-11-09 12:32:41 -05:00
|
|
|
|
2023-11-16 16:00:03 -05:00
|
|
|
/**
|
|
|
|
* Get the codepoint of the first byte of a string. If the string
|
|
|
|
* is empty, this will return 256
|
|
|
|
*
|
|
|
|
* @param s - the string
|
|
|
|
*/
|
|
|
|
export function ord(s: string): number {
|
|
|
|
if (s.length > 0) {
|
|
|
|
return s.codePointAt(0)!;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 256;
|
|
|
|
}
|
|
|
|
|
2023-11-02 13:06:48 -04:00
|
|
|
/**
|
|
|
|
* Split a string by graphemes, not just bytes
|
2023-11-16 16:00:03 -05:00
|
|
|
*
|
2023-11-02 13:06:48 -04:00
|
|
|
* @param s - the string to split into 'characters'
|
|
|
|
*/
|
2024-01-10 15:44:19 -05:00
|
|
|
export function strChars(s: string): string[] {
|
2023-11-02 13:06:48 -04:00
|
|
|
return s.split(/(?:)/u);
|
|
|
|
}
|
|
|
|
|
2023-11-09 12:05:30 -05:00
|
|
|
/**
|
|
|
|
* Get the 'character length' of a string, not its UTF16 byte count
|
2023-11-16 16:00:03 -05:00
|
|
|
*
|
2023-11-09 12:05:30 -05:00
|
|
|
* @param s - the string to check
|
|
|
|
*/
|
|
|
|
export function strlen(s: string): number {
|
2024-01-10 15:44:19 -05:00
|
|
|
return strChars(s).length;
|
2023-11-09 12:05:30 -05:00
|
|
|
}
|
|
|
|
|
2023-11-02 13:06:48 -04:00
|
|
|
/**
|
2023-11-16 16:00:03 -05:00
|
|
|
* Are all the characters in the string in ASCII range?
|
2023-11-02 13:06:48 -04:00
|
|
|
*
|
2023-11-16 16:00:03 -05:00
|
|
|
* @param char - string to check
|
2023-11-02 13:06:48 -04:00
|
|
|
*/
|
2023-11-16 21:22:24 -05:00
|
|
|
export function isAscii(char: string): boolean {
|
2024-01-10 15:44:19 -05:00
|
|
|
return strChars(char).every((char) => ord(char) < 0x80);
|
2023-11-02 13:06:48 -04:00
|
|
|
}
|
|
|
|
|
2024-02-29 14:24:22 -05:00
|
|
|
/**
|
|
|
|
* Are all the characters numerals?
|
|
|
|
*
|
|
|
|
* @param char - string to check
|
|
|
|
*/
|
|
|
|
export function isAsciiDigit(char: string): boolean {
|
|
|
|
return isAscii(char) &&
|
|
|
|
strChars(char).every((char) => ord(char) >= 0x30 && ord(char) < 0x3a);
|
|
|
|
}
|
|
|
|
|
2023-11-02 13:06:48 -04:00
|
|
|
/**
|
|
|
|
* Is the one char in the string an ascii control character?
|
|
|
|
*
|
|
|
|
* @param char - a one character string to check
|
|
|
|
*/
|
2023-11-16 21:22:24 -05:00
|
|
|
export function isControl(char: string): boolean {
|
2023-11-16 16:00:03 -05:00
|
|
|
const code = ord(char);
|
2023-11-16 21:22:24 -05:00
|
|
|
return isAscii(char) && (code === 0x7f || code < 0x20);
|
2023-11-02 13:06:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the key code for a ctrl chord
|
2023-11-16 16:00:03 -05:00
|
|
|
*
|
2023-11-02 13:06:48 -04:00
|
|
|
* @param char - a one character string
|
|
|
|
*/
|
2023-11-16 21:22:24 -05:00
|
|
|
export function ctrlKey(char: string): string {
|
2023-11-02 13:06:48 -04:00
|
|
|
// This is the normal use case, of course
|
2023-11-16 21:22:24 -05:00
|
|
|
if (isAscii(char)) {
|
2023-11-16 16:00:03 -05:00
|
|
|
const point = ord(char);
|
2023-11-02 13:06:48 -04:00
|
|
|
return String.fromCodePoint(point & 0x1f);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If it's not ascii, just return the input key code
|
|
|
|
return char;
|
|
|
|
}
|
2023-11-09 12:05:30 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Trim a string to a max number of characters
|
|
|
|
* @param s
|
|
|
|
* @param maxLen
|
|
|
|
*/
|
|
|
|
export function truncate(s: string, maxLen: number): string {
|
2024-01-10 15:44:19 -05:00
|
|
|
const chin = strChars(s);
|
2023-11-09 12:05:30 -05:00
|
|
|
if (maxLen >= chin.length) {
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
|
|
|
return chin.slice(0, maxLen).join('');
|
|
|
|
}
|