|
| 1 | +// Terminal configuration |
| 2 | +const TERMINAL_WIDTH = 80 |
| 3 | +const DEFAULT_INDENT = 28 |
| 4 | +const ANSI_REGEX = /\x1b\[[0-9;]*m/g |
| 5 | + |
| 6 | +// Check if colors should be disabled |
| 7 | +const SUPPORTS_COLOR = |
| 8 | + process.env.NO_COLOR === undefined && |
| 9 | + process.stdout.isTTY && |
| 10 | + process.env.TERM !== 'dumb' |
| 11 | + |
| 12 | +// ANSI color codes |
1 | 13 | const GREEN = '\x1b[32m' // For short options (-h) |
2 | 14 | const CYAN = '\x1b[36m' // For long options (--help) |
3 | 15 | const YELLOW = '\x1b[33m' // Could be used for parameter values |
4 | 16 | const RESET = '\x1b[0m' // Reset to default color |
5 | 17 |
|
| 18 | +/** |
| 19 | + * Helper function to wrap text to a specified width |
| 20 | + * @param text - The text to wrap |
| 21 | + * @param maxLength - Maximum line length |
| 22 | + * @returns Array of wrapped lines |
| 23 | + */ |
| 24 | +function wrapText(text: string, maxLength: number): string[] { |
| 25 | + const words = text.split(' ') |
| 26 | + const lines: string[] = [] |
| 27 | + let currentLine = '' |
| 28 | + |
| 29 | + for (const word of words) { |
| 30 | + const testLine = currentLine + word + ' ' |
| 31 | + if (testLine.length > maxLength && currentLine.length > 0) { |
| 32 | + lines.push(currentLine.trim()) |
| 33 | + currentLine = word + ' ' |
| 34 | + } else { |
| 35 | + currentLine = testLine |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + if (currentLine.trim().length > 0) { |
| 40 | + lines.push(currentLine.trim()) |
| 41 | + } |
| 42 | + |
| 43 | + return lines |
| 44 | +} |
| 45 | + |
| 46 | +/** |
| 47 | + * Helper function to strip ANSI codes from a string |
| 48 | + * @param text - Text with ANSI codes |
| 49 | + * @returns Text without ANSI codes |
| 50 | + */ |
| 51 | +function stripAnsi(text: string): string { |
| 52 | + return text.replace(ANSI_REGEX, '') |
| 53 | +} |
| 54 | + |
| 55 | +/** |
| 56 | + * Helper function to apply color only if terminal supports it |
| 57 | + * @param color - ANSI color code |
| 58 | + * @param text - Text to colorize |
| 59 | + * @returns Colorized text or plain text |
| 60 | + */ |
| 61 | +function colorize(color: string, text: string): string { |
| 62 | + return SUPPORTS_COLOR ? `${color}${text}${RESET}` : text |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Colorize text in green (for short options like -h) |
| 67 | + * @param text - The text to colorize |
| 68 | + * @returns Colorized text |
| 69 | + */ |
6 | 70 | export function short(text: string): string { |
7 | | - return `${GREEN}${text}${RESET}` |
| 71 | + return colorize(GREEN, text) |
8 | 72 | } |
9 | 73 |
|
| 74 | +/** |
| 75 | + * Colorize text in cyan (for long options like --help) |
| 76 | + * @param text - The text to colorize |
| 77 | + * @returns Colorized text |
| 78 | + */ |
10 | 79 | export function long(text: string): string { |
11 | | - return `${CYAN}${text}${RESET}` |
| 80 | + return colorize(CYAN, text) |
12 | 81 | } |
13 | 82 |
|
| 83 | +/** |
| 84 | + * Format text as a heading (yellow, bold, underlined) |
| 85 | + * @param text - The text to format |
| 86 | + * @returns Formatted heading text |
| 87 | + */ |
14 | 88 | export function heading(text: string): string { |
15 | | - return underline(bold(`${YELLOW}${text}${RESET}`)) |
| 89 | + const colored = colorize(YELLOW, text) |
| 90 | + return underline(bold(colored)) |
16 | 91 | } |
17 | 92 |
|
| 93 | +/** |
| 94 | + * Make text bold |
| 95 | + * @param text - The text to make bold |
| 96 | + * @returns Bold text |
| 97 | + */ |
18 | 98 | export function bold(text: string): string { |
19 | | - return `\x1b[1m${text}${RESET}` |
| 99 | + return SUPPORTS_COLOR ? `\x1b[1m${text}${RESET}` : text |
20 | 100 | } |
21 | 101 |
|
| 102 | +/** |
| 103 | + * Make text italic |
| 104 | + * @param text - The text to make italic |
| 105 | + * @returns Italic text |
| 106 | + */ |
22 | 107 | export function italic(text: string): string { |
23 | | - return `\x1b[3m${text}${RESET}` |
| 108 | + return SUPPORTS_COLOR ? `\x1b[3m${text}${RESET}` : text |
24 | 109 | } |
25 | 110 |
|
| 111 | +/** |
| 112 | + * Underline text |
| 113 | + * @param text - The text to underline |
| 114 | + * @returns Underlined text |
| 115 | + */ |
26 | 116 | export function underline(text: string): string { |
27 | | - return `\x1b[4m${text}${RESET}` |
| 117 | + return SUPPORTS_COLOR ? `\x1b[4m${text}${RESET}` : text |
28 | 118 | } |
29 | 119 |
|
| 120 | +/** |
| 121 | + * Print a formatted command with description |
| 122 | + * @param short_command - Short command option (e.g., '-h') or null |
| 123 | + * @param long_command - Long command option (e.g., '--help') |
| 124 | + * @param description - Description text |
| 125 | + * @param indentation - Column width for command alignment (default: 28) |
| 126 | + */ |
30 | 127 | export function command( |
31 | 128 | short_command: string | null, |
32 | 129 | long_command: string, |
33 | 130 | description: string, |
34 | | - indentation: number = 28 |
35 | | -): undefined { |
| 131 | + indentation: number = DEFAULT_INDENT |
| 132 | +): void { |
36 | 133 | // Format the command portion |
37 | 134 | let commandPart = '' |
38 | 135 |
|
39 | 136 | if (short_command) { |
40 | 137 | // Handle newline character if present in short_command |
41 | | - if (short_command.includes('\n')) { |
42 | | - commandPart = |
43 | | - short(short_command.replace('\n', '')) + ' ' + bold(long(long_command)) |
44 | | - } else { |
45 | | - commandPart = short(short_command) + ' ' + bold(long(long_command)) |
46 | | - } |
| 138 | + const cleanShort = short_command.replace('\n', '') |
| 139 | + commandPart = short(cleanShort) + ' ' + bold(long(long_command)) |
47 | 140 | } else { |
48 | 141 | commandPart = bold(long(long_command)) |
49 | 142 | } |
50 | 143 |
|
51 | 144 | // Get the visible length of the command part (without ANSI codes) |
52 | | - const commandPartLength = commandPart.replace(/\x1b\[\d+m/g, '').length |
| 145 | + const commandPartLength = stripAnsi(commandPart).length |
53 | 146 |
|
54 | 147 | // Calculate indentation padding |
55 | 148 | const padding = Math.max(0, indentation - commandPartLength) |
56 | 149 |
|
57 | 150 | // Calculate available space for description after indentation |
58 | | - const maxLineLength = 80 // Standard terminal width |
59 | | - const wrapLength = maxLineLength - indentation |
| 151 | + const wrapLength = TERMINAL_WIDTH - indentation |
60 | 152 |
|
61 | 153 | // Word wrap the description |
62 | | - const words = description.split(' ') |
63 | | - let lines = [] |
64 | | - let currentLine = '' |
65 | | - |
66 | | - for (const word of words) { |
67 | | - if ((currentLine + word).length > wrapLength && currentLine.length > 0) { |
68 | | - lines.push(currentLine.trim()) |
69 | | - currentLine = word + ' ' |
70 | | - } else { |
71 | | - currentLine += word + ' ' |
72 | | - } |
73 | | - } |
74 | | - |
75 | | - if (currentLine.trim().length > 0) { |
76 | | - lines.push(currentLine.trim()) |
77 | | - } |
| 154 | + const lines = wrapText(description, wrapLength) |
78 | 155 |
|
79 | 156 | // Output first line with proper indentation |
80 | | - console.log(commandPart + ' '.repeat(padding) + lines[0]) |
| 157 | + console.log(commandPart + ' '.repeat(padding) + (lines[0] || '')) |
81 | 158 |
|
82 | 159 | // Output additional lines with consistent indentation |
83 | 160 | for (let i = 1; i < lines.length; i++) { |
84 | 161 | console.log(' '.repeat(indentation) + lines[i]) |
85 | 162 | } |
86 | 163 | } |
87 | 164 |
|
| 165 | +/** |
| 166 | + * Print formatted info text with optional indentation |
| 167 | + * @param text - The text to display |
| 168 | + * @param indentation - Number of spaces to indent (default: 0) |
| 169 | + */ |
88 | 170 | export function info(text: string, indentation: number = 0): void { |
89 | | - // Calculate maximum line length |
90 | | - const maxLineLength = 80 |
91 | | - const wrapLength = maxLineLength - indentation |
| 171 | + // Calculate available space for text after indentation |
| 172 | + const wrapLength = TERMINAL_WIDTH - indentation |
92 | 173 |
|
93 | 174 | // Word wrap the text |
94 | | - const words = text.split(' ') |
95 | | - let lines = [] |
96 | | - let currentLine = '' |
97 | | - |
98 | | - for (const word of words) { |
99 | | - if ((currentLine + word).length > wrapLength && currentLine.length > 0) { |
100 | | - lines.push(currentLine.trim()) |
101 | | - currentLine = word + ' ' |
102 | | - } else { |
103 | | - currentLine += word + ' ' |
104 | | - } |
105 | | - } |
106 | | - |
107 | | - if (currentLine.trim().length > 0) { |
108 | | - lines.push(currentLine.trim()) |
109 | | - } |
| 175 | + const lines = wrapText(text, wrapLength) |
110 | 176 |
|
111 | 177 | // Print each line with indentation and styling |
112 | | - for (let i = 0; i < lines.length; i++) { |
113 | | - console.log(' '.repeat(indentation) + italic(lines[i])) |
| 178 | + for (const line of lines) { |
| 179 | + console.log(' '.repeat(indentation) + italic(line)) |
114 | 180 | } |
115 | 181 | } |
0 commit comments