@ideascol/cli-maker
Version:
A simple library to help create CLIs
201 lines (200 loc) • 8.79 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.drawBox = drawBox;
exports.drawTwoColumnBox = drawTwoColumnBox;
exports.infoBar = infoBar;
exports.separator = separator;
exports.toolCallHeader = toolCallHeader;
exports.toolCallArgs = toolCallArgs;
exports.toolResultBox = toolResultBox;
exports.sectionHeader = sectionHeader;
exports.alignedList = alignedList;
const colors_1 = require("../colors");
const common_1 = require("../common");
const BORDERS = {
single: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
dashed: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '╌', v: '╎' },
rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' },
};
/**
* Draw a box around content lines with optional title.
*/
function drawBox(contentLines, opts = {}) {
const border = BORDERS[opts.borderStyle ?? 'rounded'];
const bColor = opts.borderColor ?? colors_1.Colors.FgGray;
const pad = opts.padding ?? 1;
const termWidth = process.stdout.columns || 80;
const maxW = opts.maxWidth ?? termWidth - 4;
const contentWidths = contentLines.map(l => (0, common_1.stripAnsiCodes)(l).length);
let innerWidth = Math.max(...contentWidths, 10);
if (opts.title) {
innerWidth = Math.max(innerWidth, (0, common_1.stripAnsiCodes)(opts.title).length + 4);
}
innerWidth = Math.min(innerWidth, maxW - pad * 2 - 2);
const totalInner = innerWidth + pad * 2;
const padStr = ' '.repeat(pad);
const output = [];
// Top border
if (opts.title) {
const tColor = opts.titleColor ?? colors_1.Colors.Bright;
const titleClean = (0, common_1.stripAnsiCodes)(opts.title);
const remaining = Math.max(0, totalInner - titleClean.length - 3);
output.push(`${bColor}${border.tl}${border.h} ${colors_1.Colors.Reset}${tColor}${opts.title}${colors_1.Colors.Reset}${bColor} ${border.h.repeat(remaining)}${border.tr}${colors_1.Colors.Reset}`);
}
else {
output.push(`${bColor}${border.tl}${border.h.repeat(totalInner)}${border.tr}${colors_1.Colors.Reset}`);
}
// Content lines
for (const line of contentLines) {
const cleanLen = (0, common_1.stripAnsiCodes)(line).length;
const rightPad = Math.max(0, innerWidth - cleanLen);
output.push(`${bColor}${border.v}${colors_1.Colors.Reset}${padStr}${line}${' '.repeat(rightPad)}${padStr}${bColor}${border.v}${colors_1.Colors.Reset}`);
}
// Bottom border
output.push(`${bColor}${border.bl}${border.h.repeat(totalInner)}${border.br}${colors_1.Colors.Reset}`);
return output.join('\n');
}
/**
* Draw a two-column box panel (like Claude Code's welcome screen).
* Spans the full terminal width with a ~45%/55% column split.
*/
function drawTwoColumnBox(leftLines, rightLines, opts = {}) {
const border = BORDERS[opts.borderStyle ?? 'rounded'];
const bColor = opts.borderColor ?? colors_1.Colors.FgGray;
const pad = opts.padding ?? 1;
const termWidth = process.stdout.columns || 80;
const maxW = opts.maxWidth ?? termWidth;
// Total inner width = maxW - 2 (for left + right border chars)
const totalInner = maxW - 2;
// Column split: left gets ~45%, right gets ~55% (minus divider and padding)
const ratio = opts.leftRatio ?? 0.45;
const divider = opts.dividerChar ?? border.v;
// Layout: pad + leftW + pad + divider + pad + rightW + pad
const overhead = pad * 4 + 1; // 4 pads + 1 divider
const usable = totalInner - overhead;
const leftW = Math.max(10, Math.floor(usable * ratio));
const rightW = Math.max(10, usable - leftW);
const maxRows = Math.max(leftLines.length, rightLines.length);
const padStr = ' '.repeat(pad);
// Truncate a string (with ANSI) to fit a given visible width
const truncate = (s, maxLen) => {
const clean = (0, common_1.stripAnsiCodes)(s);
if (clean.length <= maxLen)
return s;
// Simple truncation: walk the string and count visible chars
let visible = 0;
let i = 0;
let inEsc = false;
let result = '';
while (i < s.length && visible < maxLen - 1) {
if (s[i] === '\x1b') {
inEsc = true;
result += s[i];
}
else if (inEsc) {
result += s[i];
if (/[a-zA-Z]/.test(s[i]))
inEsc = false;
}
else {
result += s[i];
visible++;
}
i++;
}
return result + '\u2026' + colors_1.Colors.Reset;
};
const output = [];
// Top border with optional title
if (opts.title) {
const tColor = opts.titleColor ?? colors_1.Colors.Bright;
const titleClean = (0, common_1.stripAnsiCodes)(opts.title);
const remaining = Math.max(0, totalInner - titleClean.length - 4);
output.push(`${bColor}${border.tl}${border.h}${border.h}${border.h} ${colors_1.Colors.Reset}${tColor}${opts.title}${colors_1.Colors.Reset}${bColor} ${border.h.repeat(remaining)}${border.tr}${colors_1.Colors.Reset}`);
}
else {
output.push(`${bColor}${border.tl}${border.h.repeat(totalInner)}${border.tr}${colors_1.Colors.Reset}`);
}
// Rows
for (let i = 0; i < maxRows; i++) {
const rawLeft = leftLines[i] ?? '';
const rawRight = rightLines[i] ?? '';
const left = truncate(rawLeft, leftW);
const right = truncate(rawRight, rightW);
const leftClean = (0, common_1.stripAnsiCodes)(left).length;
const rightClean = (0, common_1.stripAnsiCodes)(right).length;
const leftPadR = Math.max(0, leftW - leftClean);
const rightPadR = Math.max(0, rightW - rightClean);
output.push(`${bColor}${border.v}${colors_1.Colors.Reset}${padStr}${left}${' '.repeat(leftPadR)}${padStr}${bColor}${divider}${colors_1.Colors.Reset}${padStr}${right}${' '.repeat(rightPadR)}${padStr}${bColor}${border.v}${colors_1.Colors.Reset}`);
}
// Bottom border
output.push(`${bColor}${border.bl}${border.h.repeat(totalInner)}${border.br}${colors_1.Colors.Reset}`);
return output.join('\n');
}
/**
* Render an info bar (single line with dim background).
*/
function infoBar(text) {
return `${colors_1.Colors.FgGray}${text}${colors_1.Colors.Reset}`;
}
/**
* Render a separator line.
*/
function separator(char = '─', width) {
const w = width ?? (process.stdout.columns || 80) - 2;
return `${colors_1.Colors.FgGray}${char.repeat(w)}${colors_1.Colors.Reset}`;
}
/**
* Render a tool call header (Claude-style ⏺ indicator with left accent).
*/
function toolCallHeader(toolName, description) {
return ` ${colors_1.Colors.FgYellow}⏺${colors_1.Colors.Reset} ${colors_1.Colors.Bright}${toolName}${colors_1.Colors.Reset}`;
}
/**
* Render a tool call argument list.
*/
function toolCallArgs(args) {
const lines = [];
for (const [key, val] of Object.entries(args)) {
const display = typeof val === 'string' ? val : JSON.stringify(val);
lines.push(` ${colors_1.Colors.FgGray}${key}:${colors_1.Colors.Reset} ${display}`);
}
return lines.join('\n');
}
/**
* Render a tool result box (indented, with subtle border).
*/
function toolResultBox(result) {
const lines = result.split('\n');
const output = [];
for (const line of lines) {
output.push(` ${colors_1.Colors.FgGray}│${colors_1.Colors.Reset} ${line}`);
}
return output.join('\n');
}
/**
* Render a section header with aligned items (for /help, /tools).
*/
function sectionHeader(title) {
return `\n ${colors_1.Colors.Bright}${colors_1.Colors.FgCyan}${title}${colors_1.Colors.Reset}`;
}
/**
* Render aligned key-value rows (for /help listing).
* Automatically calculates the widest key for alignment.
*/
function alignedList(items, indent = 4) {
const indentStr = ' '.repeat(indent);
const maxKeyLen = Math.max(...items.map(i => (0, common_1.stripAnsiCodes)(i.prefix ?? '').length + (0, common_1.stripAnsiCodes)(i.key).length));
const gap = 3;
const lines = [];
for (const item of items) {
const prefix = item.prefix ?? '';
const prefixClean = (0, common_1.stripAnsiCodes)(prefix).length;
const keyClean = (0, common_1.stripAnsiCodes)(item.key).length;
const keyColor = item.keyColor ?? colors_1.Colors.FgGreen;
const padding = ' '.repeat(Math.max(1, maxKeyLen - prefixClean - keyClean + gap));
lines.push(`${indentStr}${prefix}${keyColor}${item.key}${colors_1.Colors.Reset}${padding}${colors_1.Colors.FgGray}${item.value}${colors_1.Colors.Reset}`);
}
return lines.join('\n');
}