refactor: add shared terminal rendering helpers to terminal.ts

Add hideCursor, showCursor, clearLine, moveUp, clearScreen,
visibleLength, padRight as centralized exports to eliminate
duplication across menu.ts, config.ts, and index.ts.

Also cache isColorEnabled result to avoid repeated env checks.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-18 01:54:37 +08:00
parent ff4e18ca8b
commit a1151d7f76
+53 -11
View File
@@ -1,29 +1,39 @@
// Terminal styling utilities.
// Terminal styling and rendering utilities.
// Respects NO_COLOR convention, --no-color flag, and TTY detection.
import { isStdoutTTY } from "./tty";
// ── Color support ─────────────────────────────────────────────────────
let _enabled: boolean | null = null;
export function setColorEnabled(enabled: boolean): void {
_enabled = enabled;
_enabled = enabled;
}
export function isColorEnabled(): boolean {
if (_enabled !== null) return _enabled;
if (_enabled !== null) return _enabled;
// Respect NO_COLOR: https://no-color.org/
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") {
return false;
}
if (!isStdoutTTY()) return false;
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
// Respect NO_COLOR: https://no-color.org/
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") {
_enabled = false;
return false;
}
if (!isStdoutTTY()) {
_enabled = false;
return false;
}
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") {
_enabled = true;
return true;
}
return true;
_enabled = true;
return true;
}
function s(code: string): string {
return isColorEnabled() ? code : "";
return isColorEnabled() ? code : "";
}
export const BOLD = () => s("\x1b[1m");
@@ -33,3 +43,35 @@ export const YELLOW = () => s("\x1b[33m");
export const CYAN = () => s("\x1b[36m");
export const RED = () => s("\x1b[31m");
export const RESET = () => s("\x1b[0m");
// ── Terminal rendering helpers ───────────────────────────────────────
export function hideCursor(): void {
process.stdout.write("\x1b[?25l");
}
export function showCursor(): void {
process.stdout.write("\x1b[?25h");
}
export function clearLine(): void {
process.stdout.write("\r\x1b[2K");
}
export function moveUp(lines: number): void {
if (lines > 0) process.stdout.write(`\x1b[${lines}A`);
}
export function clearScreen(): void {
process.stdout.write("\x1b[2J\x1b[H");
}
/** Calculate visible length of a string, stripping ANSI escape codes. */
export function visibleLength(value: string): number {
return value.replace(/\x1b\[[0-9;]*m/g, "").length;
}
/** Pad a string to the given visible width (accounting for ANSI codes). */
export function padRight(value: string, width: number): string {
return value + " ".repeat(Math.max(0, width - visibleLength(value)));
}