UNPKG

@syncify/ansi

Version:

ANSI Colors, Symbols and TUI related terminal enchancements for Syncify.

330 lines (277 loc) 6.04 kB
import type { Ansis } from 'ansis'; import type { Merge } from 'type-fest'; import update from 'log-update'; import { bold, neonGreen, pink } from './colors'; import { Header, Prefix } from './write'; type SpinnerStyles = 'brielle' | 'arrows' | 'spinning' export interface SpinnerOptions { /** * Whether or not a line prefix is applied * * @default true * @example │ ⠋ */ line?: boolean; /** * An actionable spinner renders differently and will impose * an arrows spinner style. * * **Examples** * * ```bash * # when input param is passed * │ input → before ▹▹▹▹▹ after * * # when input param is omitted * │ before ▹▹▹▹▹ after * * ``` * * @default null */ action?: { /** * The arrows loading color, if `color` is passed it will inherit, * otherwise when set to defaults, it uses `neonGreen` * * @default 'neonGreen' */ color?: Ansis; /** * The before label * * ```bash * before ▹▹▹▹▹ * ``` */ before: string; /** * The after label * * ```bash * ▹▹▹▹▹ after * ``` */ after: string; }; /** * The spinner color - If `action` is provided, this will have no effect. * * @default 'pink' */ color?: Ansis; /** * The spinner color - If `action` is provided, this will be set to `arrows` * * ```bash * ◓ # spinning * ⠋ # brielle * ▹ # arrows (only used for actionable spinner) * ``` * * @default 'spinning' */ style?: SpinnerStyles; } export interface CLISpinner { /** * Render Spinner * * Loads the spinner. Optionally pass in text to append. * * **Passing no parametes** * * ``` * │ ⠋ * ``` * * **Passing text parameter** * * ``` * │ ⠋ input * ``` * * **Passing spinning style** * * ``` * │ ◓ input * ``` * * **Passing action options with input** * * ``` * │ input → before ▹▹▹▹▹ after * ``` * * **Passing action options without input** * * ``` * │ before ▹▹▹▹▹ after * ``` * * --- * * @param text * The text to append on the right side of the spinner * * @param options * Spinner options * */ (input?: SpinnerOptions | string, options?: SpinnerOptions): void; /** * Updates the text of the loader * * @param message * The new message to apply */ update: (message: string) => void; /** * Clears the interval and stops the spinner. Optionally * provide preserve text, if none passed, line is cleared. * * @param message * Optional text to append */ stop: (message?: string) => void; /** * Whether or not the spinner is running */ readonly active?: boolean; } export function Spinner () { /** * The interval instance */ let interval: NodeJS.Timeout; /** * Whether or not the spinner is running */ let active: boolean = false; /** * The log message */ let message: string = ''; /** * Whether or not a tree line should apply */ let tline: boolean = true; const { loaders } = Spinner; const defaults: Merge<SpinnerOptions, { label: string }> = { label: '', line: true, color: null, style: 'spinning', action: null }; /** * TUI Spinner * * Generates a log spinner. */ const spin: CLISpinner = function spin (input, settings) { let options: Merge<SpinnerOptions, { label: string }> = { ...defaults }; if (typeof input === 'object') { options = Object.assign(options, input); } else if (typeof input === 'string') { options.label = input; if (typeof settings === 'object') { options = Object.assign(options, settings); } } active = true; tline = options.line; let color: Ansis; let frame: number = 0; let frames: string[]; let size: number = 0; if (options.action !== null) { options.style = 'arrows'; color = 'color' in options.action ? options.action.color : neonGreen; frames = loaders.arrows.frames; size = frames.length; } else { color = typeof options.color === 'function' ? options.color : pink; message = options.label; frames = loaders[options.style].frames; size = frames.length; } update.done(); interval = setInterval(() => { if (!active) return; let label: string; if (options.action !== null) { const string = bold(options.action.before) + ' ' + frames[frame = ++frame % size] + ' ' + options.action.after; label = color(message !== '' ? Prefix(message, string) : string); } else { label = color(frames[frame = ++frame % size] + ' ' + message); } update(options.line ? Header(label) : label); }, loaders[options.style].interval); }; spin.update = function (input: string) { message = input; }; spin.stop = function (input?: string) { if (active === false) return; active = false; if (input) { update(tline ? Header(input) : input); update.done(); } else { update.clear(); } clearInterval(interval); interval = undefined; message = ''; }; Object.defineProperty(spin, 'active', { get () { return active; } }); return spin; } Spinner.loaders = { dots: { interval: 100, frames: [ '.', '..', '...', '....' ] }, arrows: { interval: 120, frames: [ '▹▹▹▹', '▸▹▹▹', '▹▸▹▹', '▹▹▸▹', '▹▹▹▸' ] }, brielle: { interval: 80, frames: [ '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏' ] }, spinning: { interval: 80, frames: [ '◐', '◓', '◑', '◒' ] } };