106 lines
3.5 KiB
JavaScript
106 lines
3.5 KiB
JavaScript
'use strict';
|
|
|
|
const readline = require('readline');
|
|
const { visibleWidth, truncatePlain } = require('../utils/text_width');
|
|
|
|
function truncateNoEllipsis(text, maxCols) {
|
|
const out = truncatePlain(text, maxCols);
|
|
if (out.endsWith('...') && visibleWidth(text) > maxCols) {
|
|
return out.slice(0, -3);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function createStatusBar({ getTokens, maxTokens, getMaxTokens }) {
|
|
let mode = 'input';
|
|
let enabled = true;
|
|
let scrollApplied = false;
|
|
let lastRows = null;
|
|
|
|
const render = () => {
|
|
if (!enabled || !process.stdout.isTTY) return;
|
|
const cols = process.stdout.columns || 80;
|
|
const rows = process.stdout.rows || 24;
|
|
if (rows < 3) return;
|
|
const left = mode === 'running' ? '按下Esc停止' : '输入/查看所有指令';
|
|
const total = typeof getTokens === 'function' ? getTokens() : 0;
|
|
const formatCount = (value) => {
|
|
const num = Number(value) || 0;
|
|
if (num < 1000) return String(num);
|
|
const k = Math.round((num / 1000) * 10) / 10;
|
|
return `${k % 1 === 0 ? k.toFixed(0) : k.toFixed(1)}k`;
|
|
};
|
|
const maxValue = typeof getMaxTokens === 'function' ? getMaxTokens() : maxTokens;
|
|
const maxText = typeof maxValue === 'number' ? formatCount(maxValue) : (maxValue || '?');
|
|
const right = `当前上下文 ${formatCount(total)}/${maxText}`;
|
|
const rightTail = ` ${formatCount(total)}/${maxText}`;
|
|
const leftWidth = visibleWidth(left);
|
|
const rightWidth = visibleWidth(right);
|
|
const usableCols = Math.max(1, cols - 1);
|
|
let safeLine = '';
|
|
|
|
const availableLeft = usableCols - rightWidth - 1;
|
|
if (availableLeft >= 1) {
|
|
const displayLeft = truncateNoEllipsis(left, availableLeft);
|
|
const gap = usableCols - visibleWidth(displayLeft) - rightWidth;
|
|
safeLine = `${displayLeft}${' '.repeat(Math.max(0, gap))}${right}`;
|
|
} else {
|
|
safeLine = truncateNoEllipsis(right, usableCols);
|
|
}
|
|
|
|
process.stdout.write('\x1b7'); // save cursor
|
|
if (!scrollApplied || lastRows !== rows) {
|
|
process.stdout.write('\x1b[r'); // reset scroll region
|
|
process.stdout.write(`\x1b[1;${rows - 2}r`); // reserve last 2 lines
|
|
scrollApplied = true;
|
|
lastRows = rows;
|
|
}
|
|
// keep one blank spacer line above status line
|
|
readline.cursorTo(process.stdout, 0, rows - 2);
|
|
readline.clearLine(process.stdout, 0);
|
|
// render status line on last row
|
|
readline.cursorTo(process.stdout, 0, rows - 1);
|
|
readline.clearLine(process.stdout, 0);
|
|
process.stdout.write('\x1b[?7l'); // disable auto-wrap
|
|
process.stdout.write(safeLine);
|
|
process.stdout.write('\x1b[?7h'); // re-enable auto-wrap
|
|
process.stdout.write('\x1b8'); // restore cursor
|
|
};
|
|
|
|
const setMode = (nextMode) => {
|
|
if (mode === nextMode) return;
|
|
mode = nextMode;
|
|
render();
|
|
};
|
|
|
|
const setEnabled = (nextEnabled) => {
|
|
enabled = nextEnabled;
|
|
if (enabled) render();
|
|
};
|
|
|
|
const destroy = () => {
|
|
if (!process.stdout.isTTY) return;
|
|
if (scrollApplied) {
|
|
process.stdout.write('\x1b[r'); // reset scroll region
|
|
scrollApplied = false;
|
|
}
|
|
const rows = process.stdout.rows || 24;
|
|
if (rows >= 2) {
|
|
readline.cursorTo(process.stdout, 0, rows - 2);
|
|
readline.clearLine(process.stdout, 0);
|
|
}
|
|
if (rows >= 1) {
|
|
readline.cursorTo(process.stdout, 0, rows - 1);
|
|
readline.clearLine(process.stdout, 0);
|
|
}
|
|
};
|
|
|
|
if (process.stdout.isTTY) {
|
|
process.stdout.on('resize', () => render());
|
|
}
|
|
|
|
return { render, setMode, setEnabled, destroy };
|
|
}
|
|
|
|
module.exports = { createStatusBar };
|