152 lines
5.3 KiB
JavaScript
152 lines
5.3 KiB
JavaScript
'use strict';
|
|
|
|
const readline = require('readline');
|
|
const { gray } = require('../utils/colors');
|
|
|
|
const FRAMES = ['⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
|
|
class Spinner {
|
|
constructor(indent = '', leadingLines = 0) {
|
|
this.timer = null;
|
|
this.index = 0;
|
|
this.textProvider = () => '';
|
|
this.active = false;
|
|
this.indent = indent;
|
|
this.leadingLines = Math.max(0, Number(leadingLines) || 0);
|
|
this.leadingWritten = false;
|
|
this.thinkingLineReady = false;
|
|
}
|
|
|
|
setIndent(indent) {
|
|
this.indent = indent || '';
|
|
}
|
|
|
|
start(textProvider) {
|
|
this.textProvider = textProvider || (() => '');
|
|
this.active = true;
|
|
this.thinkingLineReady = false;
|
|
if (this.leadingLines > 0 && !this.leadingWritten) {
|
|
process.stdout.write('\n'.repeat(this.leadingLines));
|
|
this.leadingWritten = true;
|
|
}
|
|
this.render();
|
|
this.timer = setInterval(() => this.render(), 120);
|
|
}
|
|
|
|
ensureThinkingLine() {
|
|
if (this.thinkingLineReady) return;
|
|
process.stdout.write('\n');
|
|
this.thinkingLineReady = true;
|
|
readline.moveCursor(process.stdout, 0, -1);
|
|
readline.cursorTo(process.stdout, 0);
|
|
}
|
|
|
|
render() {
|
|
readline.clearLine(process.stdout, 0);
|
|
readline.cursorTo(process.stdout, 0);
|
|
const frame = FRAMES[this.index % FRAMES.length];
|
|
this.index += 1;
|
|
const provided = this.textProvider(frame);
|
|
let suffix = '';
|
|
let thinking = '';
|
|
let colorThinking = false;
|
|
let hasThinkingLine = false;
|
|
if (typeof provided === 'string') {
|
|
suffix = provided;
|
|
} else if (provided && typeof provided === 'object') {
|
|
suffix = provided.label || '';
|
|
thinking = provided.thinking || '';
|
|
colorThinking = !!provided.colorThinking;
|
|
hasThinkingLine = Object.prototype.hasOwnProperty.call(provided, 'thinking');
|
|
}
|
|
const linePrefix = `${this.indent}${frame}${suffix}`;
|
|
process.stdout.write(linePrefix);
|
|
if (hasThinkingLine) {
|
|
this.ensureThinkingLine();
|
|
const width = process.stdout.columns || 80;
|
|
let visibleThinking = thinking;
|
|
const maxThinking = Math.max(0, width - this.indent.length - 1);
|
|
if (visibleThinking && maxThinking > 0 && visibleThinking.length > maxThinking) {
|
|
visibleThinking = visibleThinking.slice(0, maxThinking);
|
|
}
|
|
if (visibleThinking && maxThinking === 0) visibleThinking = '';
|
|
const line = visibleThinking ? (colorThinking ? gray(visibleThinking) : visibleThinking) : '';
|
|
const leadSpaces = (suffix.match(/^\s*/) || [''])[0].length;
|
|
const alignCol = this.indent.length + frame.length + leadSpaces;
|
|
readline.moveCursor(process.stdout, 0, 1);
|
|
readline.cursorTo(process.stdout, alignCol);
|
|
readline.clearLine(process.stdout, 0);
|
|
process.stdout.write(line);
|
|
readline.moveCursor(process.stdout, 0, -1);
|
|
readline.cursorTo(process.stdout, 0);
|
|
}
|
|
}
|
|
|
|
stop(finalText) {
|
|
if (this.timer) clearInterval(this.timer);
|
|
this.timer = null;
|
|
this.active = false;
|
|
readline.clearLine(process.stdout, 0);
|
|
readline.cursorTo(process.stdout, 0);
|
|
const provided = this.textProvider('');
|
|
let thinking = '';
|
|
let colorThinking = false;
|
|
let hasThinkingLine = false;
|
|
if (provided && typeof provided === 'object' && Object.prototype.hasOwnProperty.call(provided, 'thinking')) {
|
|
thinking = provided.thinking || '';
|
|
colorThinking = !!provided.colorThinking;
|
|
hasThinkingLine = true;
|
|
}
|
|
const linePrefix = `${this.indent}${finalText}`;
|
|
process.stdout.write(linePrefix);
|
|
if (hasThinkingLine) {
|
|
this.ensureThinkingLine();
|
|
const width = process.stdout.columns || 80;
|
|
let visibleThinking = thinking;
|
|
const maxThinking = Math.max(0, width - this.indent.length - 1);
|
|
if (visibleThinking && maxThinking > 0 && visibleThinking.length > maxThinking) {
|
|
visibleThinking = visibleThinking.slice(0, maxThinking);
|
|
}
|
|
if (visibleThinking && maxThinking === 0) visibleThinking = '';
|
|
const line = visibleThinking ? (colorThinking ? gray(visibleThinking) : visibleThinking) : '';
|
|
let alignCol = this.indent.length;
|
|
const firstNonSpace = finalText.search(/\S/);
|
|
if (firstNonSpace >= 0) {
|
|
const afterFirst = finalText.slice(firstNonSpace + 1);
|
|
const spaceAfter = (afterFirst.match(/^\s*/) || [''])[0].length;
|
|
alignCol = this.indent.length + firstNonSpace + 1 + spaceAfter;
|
|
}
|
|
readline.moveCursor(process.stdout, 0, 1);
|
|
readline.cursorTo(process.stdout, alignCol);
|
|
readline.clearLine(process.stdout, 0);
|
|
process.stdout.write(line);
|
|
process.stdout.write('\n');
|
|
} else {
|
|
process.stdout.write('\n');
|
|
}
|
|
this.thinkingLineReady = false;
|
|
}
|
|
|
|
stopSilent() {
|
|
if (this.timer) clearInterval(this.timer);
|
|
this.timer = null;
|
|
this.active = false;
|
|
this.thinkingLineReady = false;
|
|
readline.clearLine(process.stdout, 0);
|
|
readline.cursorTo(process.stdout, 0);
|
|
}
|
|
}
|
|
|
|
function truncateThinking(text, max = 50) {
|
|
const sanitized = String(text || '').replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
if (sanitized.length <= max) return sanitized;
|
|
return sanitized.slice(0, max) + '...';
|
|
}
|
|
|
|
function formatThinkingLine(text) {
|
|
if (!text) return '';
|
|
return gray(truncateThinking(text));
|
|
}
|
|
|
|
module.exports = { Spinner, formatThinkingLine, truncateThinking };
|