Skip to content

Instantly share code, notes, and snippets.

@piksel
Last active February 1, 2024 17:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save piksel/19bd5ae774f8a2a78cb828cd539a14f1 to your computer and use it in GitHub Desktop.
Save piksel/19bd5ae774f8a2a78cb828cd539a14f1 to your computer and use it in GitHub Desktop.
conlib
import readline from 'node:readline/promises';
/**
* conlib v6
* @author nils måsén
* @copyright 2018-2024, nils måsén
*/
export default class ConLib {
constructor(stream) {
this.stream = stream;
this.usage = '';
}
static stderr() {
return new ConLib(process.stderr);
}
static stdout() {
return new ConLib(process.stdout);
}
log(s) {
this.stream.write(s);
return this;
}
logf = (...a) => this.log(colorFormat(...a))
sendEsc(action, n) {
this.stream.write(`\x1b[${n}${action}`);
return this;
}
up = n => this.sendEsc('A', n)
down = n => this.sendEsc('B', n)
right = n => this.sendEsc('C', n)
left = n => this.sendEsc('D', n)
startNext = n => this.sendEsc('E', n)
startPrev = n => this.sendEsc('F', n)
col = n => this.sendEsc('G', n);
color = n => this.sendEsc('m', n);
reset = () => this.color(0);
nl = () => this.log('\n');
clearToStart = () => this.sendEsc('K', 0);
clearToEnd = () => this.sendEsc('K', 1);
clearLine = () => this.sendEsc('K', 2);
fatal = (message) => {
this.log(message).nl().log(this.usage);
process.exit(1);
}
fatalf = (...a) => this.fatal(colorFormat(...a))
confirm = async (question) => {
const rl = readline.createInterface({ input: process.stdin, output: this.stream });
const answers = {y: true, n: false};
do {
const answer = answers[(await rl.question(question + ' [y/n] ')).trim().toLowerCase()];
if (typeof answer === 'boolean') {
rl.close();
return answer;
}
} while(true);
}
static colorEnabled = true;
static setColorEnabled = (enabled) => {
ConLib.colorEnabled = enabled;
}
static createColorVariant = val => {
const colorFormatted = () => ConLib.colorEnabled ? `\x1b[${val}m` : '';
const wrap = (v) => `${colorFormatted()}${v}\x1b[m`;
const tpl = (strings, ...values) => wrap(Array.isArray(strings)
? strings.map((s, i) => `${s ?? ''}${i < values.length ? values[i] : ''}`)
: strings);
const variant = (...a) => tpl(...a);
variant.tpl = tpl;
variant.wrap = wrap;
variant.colorFormatted = colorFormatted;
variant.toString = colorFormatted;
return variant;
}
static createColor = val => {
const loColor = ConLib.createColorVariant(30 + val);
const hiColor = ConLib.createColorVariant(90 + val);
loColor.hi = hiColor;
loColor.Hi = hiColor;
return loColor;
}
static black = ConLib.createColor(0);
static red = ConLib.createColor(1);
static green = ConLib.createColor(2);
static yellow = ConLib.createColor(3);
static blue = ConLib.createColor(4);
static purple = ConLib.createColor(5);
static cyan = ConLib.createColor(6);
static normal = ConLib.createColor(7);
static white = ConLib.normal.hi;
static grey = ConLib.black.hi;
static reset = ConLib.createColorVariant(0, 0);
static Colors = {
black: ConLib.black,
red: ConLib.red,
green: ConLib.green,
yellow: ConLib.yellow,
blue: ConLib.blue,
purple: ConLib.purple,
cyan: ConLib.cyan,
normal: ConLib.normal,
grey: ConLib.grey,
white: ConLib.white,
reset: ConLib.reset,
}
static coloredValue = (value, maxDepth = 2) => {
const { yellow, green, red, grey, reset, cyan, purple, blue, white } = ConLib.Colors;
const { coloredValue } = ConLib;
if (typeof value?.colorFormatted === 'function') {
return value.colorFormatted();
}
switch (typeof value) {
case 'string':
// Prevent double encoding:
if (value.startsWith('\x1b[')) return value;
return yellow.hi(value);
case 'boolean':
return value ? green.hi(value) : red.hi(value);
case 'object':
if (value instanceof Error) {
const msg = value.message.trim().split('\n')
.map(r => r.toLowerCase().startsWith('error: ') ? r.substring(7) : r)
.join('\n');
const name = value.name.toLowerCase() != 'error' ? red.wrap(value.name) + ': ' : '';
const stack = ((stack) => {
if (!stack) return '';
stack = stack.split('\n').filter((r, i) => i > 0 && !r.includes('conlib.mjs')).join('\n');
return '\n'+grey.wrap(stack);
})(value.stack);
return `${name}${white}${msg}${stack}${reset}`;
}
if (Array.isArray(value)) {
if (maxDepth > 0) {
return grey`[` + value.map(av => coloredValue(av, maxDepth - 1)).join(grey + `, `) + grey`]`;
} else {
return purple.hi.wrap(`Array[${value.length}]`)
}
}
if (maxDepth > 0) {
return `${grey}{${Object.entries(value).map(([ek, ev]) =>
`${cyan.hi(ek)}${grey}: ${coloredValue(ev, maxDepth - 1)}`
).join(grey`, `)}${grey}}${reset}`;
} else {
return purple.hi.wrap(Object.getPrototypeOf(value)?.constructor?.name ?? value)
}
case 'number':
case 'bigint':
return blue.hi(value);
case 'undefined':
return grey(value);
case 'symbol':
return cyan.hi(value.description ?? 'Symbol')
case 'function':
const strVal = ('' + value);
if (strVal.startsWith('function')) {
return `${cyan}function ${cyan.Hi}${value.name}${cyan}${strVal.substring(9 + value.name.length)}${reset}`
} else {
return `${cyan.Hi}${value.name}${cyan}${strVal}${reset}`;
}
default:
return white(value);
}
}
/**
* @param {string[]} strings
* @param {...any} values
* @returns {string}
*/
static colorFormat = (strings, ...values) =>
strings.flatMap((s, i) => [s, ...(i < values.length ? [ConLib.coloredValue(values[i])] : [ConLib.reset])]).join('');
}
export const black = ConLib.black;
export const red = ConLib.red;
export const green = ConLib.green;
export const yellow = ConLib.yellow;
export const blue = ConLib.blue;
export const purple = ConLib.purple;
export const cyan = ConLib.cyan;
export const normal = ConLib.normal;
export const grey = ConLib.grey;
export const white = ConLib.white;
export const reset = ConLib.reset;
export const Colors = ConLib.Colors;
export const colorFormat = ConLib.colorFormat;
export const coloredPath = (path, root, opts) => new ColoredPath(path, root, opts);
export class ColoredPath {
prefix = '';
constructor(path, root, opts = { stripRoot: false }) {
path = (path ?? '').replaceAll('\\', '/');
root = (root ?? '').replaceAll('\\', '/');
if (root && !root.endsWith('/')) {
root += '/'
}
if (path.startsWith(root)) {
path = path.slice(root.length);
this.prefix = root;
}
this.paths = path.split(/\/|\\/);
this.opts = opts;
}
formatPrefix() {
if (!this.opts.stripRoot) {
return `${ConLib.grey}${this.prefix}`;
}
return ''
}
colorFormatted() {
const purple = ConLib.purple;
const sep = purple + '/' + purple.Hi;
return `${this.formatPrefix()}${purple.hi(this.paths.join(sep))}`
}
toString() {
return this.colorFormatted();
}
}
import ConLib, { colorFormat, Colors, coloredPath } from "./conlib.mjs";
const { grey, reset, white, blue } = Colors;
let level = 0;
const con = ConLib.stdout();
const logPad = () => `${grey}${(level > 1 ? '' : '').padStart(level * 2, ' ')}${reset}`;
const log = m => con.log(logPad() + (m ?? '')).nl();
const logf = (s, ...v) => log(colorFormat(s, ...v));
const section = (title, fun) => {
const border = ''.padEnd((70 - title.length) - level, '-');
if (level < 1) {
log(`${grey}--| ${white}${title} ${grey}|${border}${reset}`);
} else {
log();
log(`${grey} -| ${white}${title}${reset}`);
}
level++;
log();
fun();
log();
level--;
}
log(white`\n ConLib Example\n`);
section('Strings', () => {
logf`${'hello?'}`;
});
section('Numbers', () => {
// eslint-disable-next-line no-undef
logf`${5} ${BigInt(5)}`;
});
section('Booleans', () => {
logf`${true} ${false}`;
});
section('Arrays', () => {
logf`${[1, 2, 3, 4, 8]}`;
logf`${['somtimesimalone', 'sometimesamnot']}`;
});
section('Objects', () => {
logf`${{ foo: 'bar', baz: 5, kez: [2, 5] }}`;
});
section('Symbols', () => {
logf`\${Symbol()} ${grey`=>`} ${Symbol()}`
logf`\${Symbol('NamedSymbol')} ${grey`=>`} ${Symbol('NamedSymbol')}`
});
section('Functions', () => {
section('Fat arrow', () => {
const add = (left, right) => left + right;
logf` Named: ${add}`
logf`Anonymous: ${(left, right) => left * right}`
})
section('Regular', () => {
function subtract(left, right) { return left - right }
logf` Named: ${subtract}`
const modulus = function (left, right) { return left - right }
logf` Variable: ${modulus}`
logf`Anonymous: ${function (left, right) { return left / right }}`
});
});
section('Paths', () => {
logf`${coloredPath('/usr/local/etc/service.config', '/usr/local')}`;
logf`${coloredPath('/usr/local/etc/service.config', '/usr/local', { stripRoot: true })}`;
logf`${coloredPath('/usr/local/etc/service.config', '/usr/local/', { stripRoot: true })}`;
});
section('Proxies', () => {
section('Stringable', () => {
log(`\${blue}blue ${grey`=>`} ${blue}blue${reset}`);
log(`\${blue.hi}blue ${grey`=>`} ${blue.hi}blue.hi${reset}`);
log();
log(`colorFormat\`\${blue}blue\` ${grey`=>`} ${colorFormat`${blue}blue`}`);
});
section('Tagged template', () => {
log(`blue\`blue\` ${grey`=>`} ${blue`blue`}`);
log(`blue.hi\`blue\` ${grey`=>`} ${blue.hi`blue.hi`}`);
});
});
section('Config', () => {
section('Color Enabled', () => {
ConLib.setColorEnabled(true);
logf`${{ str: 'bar', yes: true, no: false, num: 5, arr: [2, 5], path: coloredPath('/etc/passwd', '/etc') }}`;
})
section('Color Disabled', () => {
ConLib.setColorEnabled(false);
logf`${{ str: 'bar', yes: true, no: false, num: 5, arr: [2, 5], path: coloredPath('/etc/passwd', '/etc') }}`;
ConLib.setColorEnabled(true);
})
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment