✨ Lesson 8: Polish Your CLI App

⏱ 25-35 min 📊 Intermediate 📖 In-depth tutorial 🔗 Official Source
0

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

1

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();
📌 When to use StringBuffer:
• 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

Buffer is empty. Add lines above.
2

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

Foreground: 30 31 32 33 34 35 36 37
Background: 40 41 42 43 44 45 46 47
Styles: B I U S R

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

2-5 Store codes as constants — more readable
8 Always end with \x1B[0m to reset
11-15 Extension methods make it clean
13 \$this interpolates the original string
18 Chainable: .green.bold

Live Color Preview

Colored Text Preview
\x1B[32mColored Text Preview\x1B[0m

Click any swatch above to see the effect and the escape code.

3

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

1 Function takes current, total, width
2 Calculate percentage (0-100)
3-4 How many blocks to fill vs leave empty
6 '█' * filled repeats the block character
11 \r returns cursor to start of line
11 stdout.write() prints without newline
14 Final print('') moves to next line
🔑 The Carriage Return Trick: \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

45%
[█████████████░░░░░░░░░░░░░░░░░] 45%
4

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

╔═════════╦═════╦═══════╦══════════╗ ║ Name ║ Age ║ Role ║ Status ║ ╠═════════╬═════╬═══════╬══════════╣ ║ Alice ║ 30 ║ Admin ║ Active ║ ║ Bob ║ 25 ║ User ║ Active ║ ║ Charlie ║ 35 ║ Mod ║ Inactive ║ ╚═════════╩═════╩═══════╩══════════╝
5

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

USAGE: myapp <command> [options] Commands: search Search the database download Download files config Configure settings help Show this help message Use 'help <command>' for more details.
📌 Help System Best Practices:
• 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.

📦 Project 1 📦 Project 2