✨ Lesson 8: Polish Your CLI App
From Functional to Professional — Five CLI Polish Techniques
🧠 A working CLI is good. A polished CLI is memorable.
The official Dart tutorial covers five key areas for polishing command-line applications. We'll cover each one with clear explanations and interactive examples.
❌ Before Polish
search word Not found downloaded 45% Error: file missing help search - search download - download
😰 Hard to read, no visual cues, confusing output
✅ After Polish
🔍 Search Results: ✓ Found 3 matches for "word" ⬇️ Downloading... [████████████░░░░] 65% ✗ Error: File not found at path 📚 COMMAND HELP: search Search the database download Download files
😍 Clear, visually structured, user-friendly
StringBuffer — Efficient String Building
🧠 String concatenation with + creates a new string every time. StringBuffer avoids this waste.
When building large strings (like help text, tables, or reports), using + creates many intermediate string objects. StringBuffer collects all the pieces in a single buffer and creates the final string only once.
The Problem: String Concatenation Waste
❌ Inefficient: Creates 4 intermediate strings
var result = 'Hello';
result = result + ' '; // Creates new string #2
result = result + 'World'; // Creates new string #3
result = result + '!'; // Creates new string #4
// Total: 4 allocations, 3 discarded strings
✅ Efficient: Single buffer, one final string
final buffer = StringBuffer();
buffer.write('Hello');
buffer.write(' ');
buffer.write('World');
buffer.write('!');
final result = buffer.toString();
// Total: 1 allocation — much faster!
StringBuffer API Explained
final buffer = StringBuffer();
// write() — appends text without newline
buffer.write('Loading');
// writeln() — appends text WITH newline (\n)
buffer.writeln('...');
// writeAll() — appends each item from an Iterable
buffer.writeAll(['User: ', 'Alice', '\n']);
// isEmpty / isNotEmpty — check buffer state
if (buffer.isNotEmpty) {
print(buffer.toString()); // Convert to final string
}
// clear() — reset the buffer (reuse without creating new one)
buffer.clear();
• Building large reports or help text
• Assembling table output
• Constructing formatted messages in loops
• Any situation where you'd normally do
+= on a string multiple times
🎮 Build a StringBuffer
📤 Buffer Output
ANSI Escape Codes — Colors, Bold, Underline & More
🧠 Terminals understand special escape sequences that control text appearance
ANSI escape codes start with \x1B[ (ESC[) followed by a number code and end with m. For example, \x1B[31m turns text red, and \x1B[0m resets everything back to normal. You MUST reset after colored text, or everything after stays colored.
ANSI Code Reference Table
Using ANSI Codes in Dart
// Define color constants for readability
const red = '\x1B[31m';
const green = '\x1B[32m';
const bold = '\x1B[1m';
const reset = '\x1B[0m';
// Wrap text with color + reset
print('$red\$bold ERROR:$reset File not found');
// Extension method approach (cleaner)
extension Colorize on String {
String get red => '\x1B[31m\$this\x1B[0m';
String get green => '\x1B[32m\$this\x1B[0m';
String get bold => '\x1B[1m\$this\x1B[0m';
}
print('Success!'.green.bold); // Much cleaner!
📝 Key Points
\x1B[0m to reset\$this interpolates the original string.green.boldLive Color Preview
Click any swatch above to see the effect and the escape code.
Progress Bars — Visual Feedback for Long Operations
🧠 Users hate staring at a blank terminal wondering if your app is stuck
A progress bar shows completion percentage using filled/empty characters. Use \r (carriage return) to overwrite the same line instead of printing new lines. This creates the "animated" effect.
Building a Progress Bar — Step by Step
String buildProgress(int current, int total, {int width = 30}) {
final percent = (current / total * 100).round();
final filled = (current / total * width).round();
final empty = width - filled;
final bar = '█' * filled + '░' * empty;
return '[\$bar] \$percent%';
}
// Using \r to animate on the same line
for (var i = 0; i <= 100; i++) {
stdout.write('\r\${buildProgress(i, 100)}');
await Future.delayed(Duration(milliseconds: 50));
}
print(''); // Move to next line when done
📝 How It Works
'█' * filled repeats the block character\r returns cursor to start of linestdout.write() prints without newlineprint('') moves to next line\r moves the cursor back to the beginning of the current line. stdout.write() (not print()) outputs without a newline. Together, they overwrite the same line repeatedly, creating smooth animation.
🎮 Progress Bar Demo
Formatted Tables — Structured Data Display
🧠 Tables make structured data scannable and professional
Using box-drawing characters (╔═╗║╚═╝), you can create clean tables with proper alignment. The key is calculating column widths based on the longest value in each column, then padding each cell accordingly.
Table Building Logic
// Step 1: Calculate the width needed for each column
List<int> calculateWidths(List<String> headers, List<List<String>> rows) {
final widths = List<int>.filled(headers.length, 0);
for (var i = 0; i < headers.length; i++) {
widths[i] = headers[i].length;
for (var row in rows) {
if (row[i].length > widths[i]) widths[i] = row[i].length;
}
}
return widths;
}
// Step 2: Pad each cell to its column width
String padRight(String text, int width) => text.padRight(width);
// Step 3: Assemble rows with borders
// ╔═══╦═══╗
// ║ A ║ B ║
// ╚═══╩═══╝
🎮 Interactive Table Builder
Verbose Help System — User-Friendly Documentation
🧠 Good CLI apps provide layered help — from quick summaries to detailed examples
Users should be able to get basic command lists, detailed descriptions with parameters, and real usage examples. Use flags like --help, --verbose, and --examples to control the level of detail.
Designing Help Output Levels
void showHelp({bool verbose = false, bool examples = false}) {
final buffer = StringBuffer();
// Basic: Always show
buffer.writeln('USAGE: myapp <command> [options]');
buffer.writeln('');
buffer.writeln('Commands:');
buffer.writeln(' search Search the database');
buffer.writeln(' download Download files');
// Detailed: Show with --verbose flag
if (verbose) {
buffer.writeln('');
buffer.writeln('Options:');
buffer.writeln(' --limit <n> Max results (default: 10)');
buffer.writeln(' --output <file> Save to file');
}
// Examples: Show with --examples flag
if (examples) {
buffer.writeln('');
buffer.writeln('Examples:');
buffer.writeln(' \$ myapp search --query "dart"');
buffer.writeln(' \$ myapp download https://example.com/file.zip');
}
print(buffer.toString());
}
🎮 Help System Demo
• Always show basic usage on
--help• Use
--verbose for detailed parameter descriptions• Use
--examples for real copy-paste-ready examples• Use
StringBuffer to assemble help text efficiently• Add ANSI colors to make headers and commands stand out
• Show exit codes and common errors in verbose mode
🎯 CLI Polish Quiz
Score: 0/5 | Hints: 3
Loading question...
Practice Projects
Apply what you've learned by building these hands-on projects.