Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262import type { Writable } from 'node:stream';import { wrapAnsi } from 'fast-wrap-ansi';import { cursor, erase } from 'sisteransi';import { getColumns } from './utils/index.js';
function renderAsString(template: Template, host?: Component): string { if (typeof template === 'string') { return template; }
let result = '';
for (const part of template) { if (typeof part === 'string') { result += part; } else if (part instanceof Component) { if (host) { host.children.add(part); } result += renderAsString(part.render(), part); } }
return result;}
function renderDiff(from: string | undefined, to: string, output: Writable): void { const fromLines = from ? from.split('\n') : []; const toLines = to.split('\n'); const fromLineCount = fromLines.length; const toLineCount = toLines.length;
output.write(cursor.hide); output.write(cursor.left);
if (fromLineCount > 0) { output.write(cursor.up(fromLineCount)); }
for (let i = 0; i < toLineCount; i++) { const fromLine = fromLines[i]; const toLine = toLines[i];
if (fromLine === toLine) { output.write(cursor.down(1)); } else { output.write(erase.line); if (toLine !== undefined) { output.write(`${toLine}\n`); } else { output.write(cursor.down(1)); } } }
if (fromLineCount > toLineCount) { output.write(erase.down()); }
output.write(cursor.show);}
function renderToOutput( template: Template, output: Writable, lastFrame: string | undefined, host?: Component): string { const frame = renderAsString(template, host); const wrapped = wrapAnsi(frame, getColumns(output), { hard: true, trim: false, });
if (wrapped === lastFrame) { return lastFrame; }
renderDiff(lastFrame, wrapped, output);
return wrapped;}
export abstract class Component { public children: Set<Component> = new Set(); public host?: Component;
constructor(host: Component | undefined) { this.host = host; }
public abstract render(): Template;
public onMount(): void { for (const child of this.children) { child.onMount(); } }
public onUnmount(): void { for (const child of this.children) { child.onUnmount(); } }
public requestUpdate(): void { if (this.host) { this.host.requestUpdate(); } }}
export class RenderHost extends Component { #lastFrame: string | undefined; #output: Writable; #root?: Component; #mounted: boolean = false;
constructor(output: Writable = process.stdout) { super(undefined); this.#output = output; }
render(): Template { const result: TemplateArray = []; for (const child of this.children) { const childResult = child.render(); if (typeof childResult === 'string') { result.push(childResult); } else { for (const part of childResult) { result.push(part); } } } return result; }
requestUpdate() { this.#lastFrame = renderToOutput(this.render(), this.#output, this.#lastFrame, this.#root); if (!this.#mounted) { this.#mounted = true; this.onMount(); } }
onUnmount(): void { super.onUnmount(); this.#lastFrame = undefined; this.#mounted = false; }
attach(component: Component): void { component.host = this; this.#root = component; this.children.add(component); }}
export function render(component: Component, output: Writable = process.stdout): () => void { const host = new RenderHost(output);
host.attach(component); host.requestUpdate();
return () => { host.onUnmount(); };}
export const term = (strings: TemplateStringsArray, ...values: unknown[]): Template => { const result: TemplateArray = [];
for (let i = 0; i < strings.length; i++) { result.push(strings[i]); const value = values[i]; if (value !== undefined) { if (value instanceof Component) { result.push(value); } else { result.push(String(value)); } } }
return result;};
type TemplateArray = Array<string | Component>;
type Template = string | TemplateArray;
export class SmileComponent extends Component { #position: number = 0; #direction: -1 | 1 = 1; #timer?: ReturnType<typeof setInterval>;
override onMount() { super.onMount(); this.#timer = setInterval(() => { this.#updatePosition(); this.requestUpdate(); }, 100); }
override onUnmount() { super.onUnmount(); if (this.#timer) { clearInterval(this.#timer); this.#timer = undefined; } }
#updatePosition() { if (this.#direction === 1) { this.#position++; if (this.#position === 10) { this.#direction = -1; } } else { this.#position--; if (this.#position === 0) { this.#direction = 1; } } }
render(): Template { return term`${' '.repeat(this.#position)}💣 𝐂𝐋𝐀𝐂𝐊`; }}
export class SecondsComponent extends Component { #seconds = 0; #timer?: ReturnType<typeof setInterval>; #smileComponent = new SmileComponent(this);
override onMount() { super.onMount(); this.#timer = setInterval(() => { this.#seconds++; this.requestUpdate(); }, 1000); }
override onUnmount() { super.onUnmount(); if (this.#timer) { clearInterval(this.#timer); this.#timer = undefined; } }
render(): Template { return term`| Seconds elapsed:| ${this.#seconds}| Press Ctrl+C to exit| ${this.#smileComponent}`; }}