@syncify/ansi
Version:
ANSI Colors, Symbols and TUI related terminal enchancements for Syncify.
330 lines (277 loc) • 6.04 kB
text/typescript
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: [
'◐',
'◓',
'◑',
'◒'
]
}
};