@travetto/terminal
Version:
General terminal support
113 lines (98 loc) • 3.92 kB
text/typescript
import chalk from 'chalk';
import { Env, TypedObject } from '@travetto/runtime';
type TemplatePrim = number | string | bigint | boolean | RegExp;
type Color = `#${string}`;
export type TermStyleInput = Color | { text: Color, background?: Color, inverse?: boolean, bold?: boolean, italic?: boolean, underline?: boolean };
type TermStylePairInput = TermStyleInput | [dark: TermStyleInput, light: TermStyleInput];
export type TermStyleFn = (input: TemplatePrim) => string;
type TermStyledTemplate<T extends string> = (values: TemplateStringsArray, ...keys: (Partial<Record<T, TemplatePrim>> | string)[]) => string;
export type ColorLevel = 0 | 1 | 2 | 3;
export class StyleUtil {
static #darkAnsi256 = new Set([
0, 1, 2, 3, 4, 5, 6, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26, 28, 29, 30, 31, 32, 34, 35, 36, 37, 38, 40, 41, 42, 43, 44, 52, 53, 54,
55, 56, 58, 59, 60, 64, 65, 66, 70, 76, 88, 89, 90, 91, 92, 94, 95, 96, 100, 101, 106, 112, 124, 125, 126, 127, 128, 130, 136, 142,
148, 160, 161, 162, 163, 164, 166, 172, 178, 184, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243
]);
static #scheme: { key: string, dark: boolean } = { key: '', dark: true };
/**
* Create text render function from style input using current color levels
*/
static getStyle(input: TermStyleInput): TermStyleFn {
if (typeof input === 'string') {
return chalk.hex(input);
} else {
let style: chalk.Chalk = chalk;
for (const key of TypedObject.keys(input)) {
switch (key) {
case 'text': style = style.hex(input[key]!); break;
case 'background': style = style.bgHex(input[key]!); break;
default: style = (input[key] ? style[key] : style); break;
}
}
return style;
}
}
/**
* Read foreground/background color if env var is set
*/
static isBackgroundDark(): boolean {
const key = Env.COLORFGBG.val ?? '';
if (this.#scheme.key === key) {
return this.#scheme.dark;
}
const [, bg = '0'] = key.split(';');
const dark = this.#darkAnsi256.has(+bg);
Object.assign(this.#scheme, { key, dark });
return dark;
}
/**
* Create renderer from input source
*/
static getThemedStyle(input: TermStylePairInput): TermStyleFn {
const [dark, light] = (Array.isArray(input) ? input : [input]);
const isDark = this.isBackgroundDark();
return isDark ? this.getStyle(dark) : this.getStyle(light ?? dark);
}
/**
* Is styling currently enabled
*/
static get enabled(): boolean {
return chalk.level > 0;
}
/**
* Build style palette, with support for background theme awareness
*/
static getPalette<K extends string>(inp: Record<K, TermStylePairInput>): Record<K, TermStyleFn> {
return TypedObject.fromEntries(
TypedObject.entries(inp).map(([k, v]) => [k, this.getThemedStyle(v)]));
}
/**
* Make a template function based on the input set
*/
static getTemplate<K extends string>(input: Record<K, TermStylePairInput>): TermStyledTemplate<K> {
const palette = this.getPalette(input);
return (values: TemplateStringsArray, ...keys: (Partial<Record<K, TemplatePrim>> | string)[]) => {
if (keys.length === 0) {
return values[0];
} else {
const out = keys.map((el, i) => {
let final = el;
if (typeof el !== 'string') {
const subKeys = TypedObject.keys(el);
if (subKeys.length !== 1) {
throw new Error('Invalid template variable, one and only one key should be specified');
}
const [k] = subKeys;
const v = el[k]!;
final = v === undefined ? '' : palette[k](v)!;
}
return `${values[i] ?? ''}${final ?? ''}`;
});
if (values.length > keys.length) {
out.push(values[values.length - 1]);
}
return out.join('');
}
};
}
}