dragon-ui-claude
Version:
🐲 Ultra-fast, cross-platform Claude Code Max usage dashboard with dragon-inspired design, advanced background services, and multi-currency support
360 lines (309 loc) • 8.49 kB
JavaScript
/**
* CLI Screen Management (CommonJS)
* Handles screen size, updates, and layout
*/
const { colors } = require('../components/colors.cjs');
/**
* Get terminal size
*/
function getTerminalSize() {
return {
width: process.stdout.columns || 80,
height: process.stdout.rows || 24
};
}
/**
* Clear screen completely
*/
function clearScreen() {
// Windows terminals are problematic with ANSI codes, use console.clear()
if (process.platform === 'win32') {
console.clear();
// Also try to reset cursor position after console.clear()
process.stdout.write('\x1b[H');
} else {
process.stdout.write('\x1b[3J\x1b[2J\x1b[1;1H');
}
}
/**
* Clear current line
*/
function clearLine() {
process.stdout.write('\x1b[2K\r');
}
/**
* Move cursor to position
*/
function moveCursor(x, y) {
process.stdout.write(`\x1b[${y};${x}H`);
}
/**
* Hide cursor
*/
function hideCursor() {
process.stdout.write('\x1b[?25l');
}
/**
* Show cursor
*/
function showCursor() {
process.stdout.write('\x1b[?25h');
}
/**
* Create a centered line
*/
function centerText(text, width = null) {
if (!width) {
width = getTerminalSize().width;
}
const padding = Math.max(0, Math.floor((width - text.length) / 2));
return ' '.repeat(padding) + text;
}
/**
* Create a box around text
*/
function createBox(content, options = {}) {
const {
width = null,
padding = 1,
style = 'single',
title = null,
color = 'primary'
} = options;
const terminalWidth = getTerminalSize().width;
const boxWidth = width || Math.min(terminalWidth - 4, 80);
// Box drawing characters
const chars = {
single: {
topLeft: '┌',
topRight: '┐',
bottomLeft: '└',
bottomRight: '┘',
horizontal: '─',
vertical: '│',
topMid: '┬',
bottomMid: '┴'
},
double: {
topLeft: '╔',
topRight: '╗',
bottomLeft: '╚',
bottomRight: '╝',
horizontal: '═',
vertical: '║',
topMid: '╦',
bottomMid: '╩'
},
rounded: {
topLeft: '╭',
topRight: '╮',
bottomLeft: '╰',
bottomRight: '╯',
horizontal: '─',
vertical: '│',
topMid: '┬',
bottomMid: '┴'
}
};
const boxChars = chars[style] || chars.single;
const colorFn = colors[color] || colors.primary;
// Split content into lines
const lines = content.split('\n');
const contentWidth = boxWidth - 2 - (padding * 2);
// Wrap long lines
const wrappedLines = [];
lines.forEach(line => {
if (line.length <= contentWidth) {
wrappedLines.push(line);
} else {
// Simple word wrapping
const words = line.split(' ');
let currentLine = '';
words.forEach(word => {
if ((currentLine + word).length <= contentWidth) {
currentLine += (currentLine ? ' ' : '') + word;
} else {
if (currentLine) {
wrappedLines.push(currentLine);
currentLine = word;
} else {
// Word is too long, truncate it
wrappedLines.push(word.substring(0, contentWidth));
currentLine = word.substring(contentWidth);
}
}
});
if (currentLine) {
wrappedLines.push(currentLine);
}
}
});
// Build box
const result = [];
// Top border
if (title) {
const titleLength = title.length;
const leftPadding = Math.max(0, Math.floor((boxWidth - titleLength - 4) / 2));
const rightPadding = Math.max(0, boxWidth - titleLength - 4 - leftPadding);
result.push(colorFn(
boxChars.topLeft +
boxChars.horizontal.repeat(leftPadding) +
'[ ' + title + ' ]' +
boxChars.horizontal.repeat(rightPadding) +
boxChars.topRight
));
} else {
result.push(colorFn(
boxChars.topLeft +
boxChars.horizontal.repeat(boxWidth - 2) +
boxChars.topRight
));
}
// Content lines
wrappedLines.forEach(line => {
const paddedLine = ' '.repeat(padding) + line + ' '.repeat(contentWidth - line.length + padding);
result.push(colorFn(boxChars.vertical) + paddedLine + colorFn(boxChars.vertical));
});
// Bottom border
result.push(colorFn(
boxChars.bottomLeft +
boxChars.horizontal.repeat(boxWidth - 2) +
boxChars.bottomRight
));
return result.join('\n');
}
/**
* Create a progress indicator
*/
function createProgressIndicator(current, total, width = 40) {
const percentage = Math.min(100, Math.max(0, (current / total) * 100));
const filled = Math.round((percentage / 100) * width);
const empty = width - filled;
return colors.success('█'.repeat(filled)) + colors.inactive('░'.repeat(empty));
}
/**
* Create a status line
*/
function createStatusLine(items, width = null) {
if (!width) {
width = getTerminalSize().width;
}
const separator = ' | ';
const content = items.join(separator);
if (content.length <= width) {
return content;
}
// Truncate if too long
return content.substring(0, width - 3) + '...';
}
/**
* Create a two-column layout
*/
function createTwoColumnLayout(left, right, width = null) {
if (!width) {
width = getTerminalSize().width;
}
const leftWidth = Math.floor(width / 2) - 2;
const rightWidth = width - leftWidth - 4;
const leftLines = left.split('\n');
const rightLines = right.split('\n');
const maxLines = Math.max(leftLines.length, rightLines.length);
const result = [];
for (let i = 0; i < maxLines; i++) {
const leftLine = (leftLines[i] || '').padEnd(leftWidth);
const rightLine = (rightLines[i] || '').padEnd(rightWidth);
result.push(leftLine + ' | ' + rightLine);
}
return result.join('\n');
}
/**
* Create a header with timestamp
*/
function createTimestampHeader(title, width = null) {
if (!width) {
width = getTerminalSize().width;
}
const timestamp = new Date().toLocaleString();
const titleLen = title.length;
const timestampLen = timestamp.length;
if (titleLen + timestampLen + 3 <= width) {
const padding = width - titleLen - timestampLen;
return colors.header(title) + ' '.repeat(padding) + colors.date(timestamp);
} else {
return colors.header(title);
}
}
/**
* Create a footer with navigation hints
*/
function createFooter(hints = [], width = null) {
if (!width) {
width = getTerminalSize().width;
}
const defaultHints = [
'[0] Menu',
'[q] Quit',
'[r] Refresh',
'[h] Help'
];
const allHints = [...hints, ...defaultHints];
const content = allHints.join(' ');
if (content.length <= width) {
return colors.subtitle(content);
}
// Show only essential hints if too long
const essential = ['[0] Menu', '[q] Quit'];
return colors.subtitle(essential.join(' '));
}
/**
* Handle terminal resize
*/
function onResize(callback) {
process.stdout.on('resize', callback);
}
/**
* Get safe display width (accounting for potential color codes)
*/
function getSafeDisplayWidth(text) {
// Remove ANSI escape codes to get actual text length
return text.replace(/\x1b\[[0-9;]*m/g, '').length;
}
/**
* Pad text to fit terminal width
*/
function padToTerminalWidth(text, align = 'left') {
const terminalWidth = getTerminalSize().width;
const textLength = getSafeDisplayWidth(text);
if (textLength >= terminalWidth) {
return text;
}
const padding = terminalWidth - textLength;
switch (align) {
case 'right':
return ' '.repeat(padding) + text;
case 'center':
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
default:
return text + ' '.repeat(padding);
}
}
module.exports = {
getTerminalSize,
clearScreen,
clearLine,
moveCursor,
hideCursor,
showCursor,
centerText,
createBox,
createProgressIndicator,
createStatusLine,
createTwoColumnLayout,
createTimestampHeader,
createFooter,
onResize,
getSafeDisplayWidth,
padToTerminalWidth
};