š Project: Progress Bar Builder
š Lesson 8: Polish Your CLI
ā± 25-30 min
š Intermediate
šÆ Objective
Build a customizable progress bar system using StringBuffer for efficient string building. Create different styles of progress bars with animations and percentage displays.
Requirements
- Create a
ProgressBarclass with configurable width, style, and colors - Support multiple styles: classic, blocks, dots, spinner
- Use
StringBufferfor efficient string building - Implement percentage, fraction, and bar visualization
- Create an animated progress simulation
import 'dart:async';
import 'dart:io';
// Step 1: Progress bar style enum
enum ProgressStyle {
classic,
blocks,
dots,
arrow,
spinner,
}
// Step 2: Progress bar class
class ProgressBar {
final int width;
final ProgressStyle style;
final String? prefix;
final String? suffix;
final bool showPercentage;
final bool showFraction;
double _progress = 0.0;
int _spinnerIndex = 0;
static const _spinnerFrames = ['ā ', 'ā ', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ', 'ā '];
ProgressBar({
this.width = 30,
this.style = ProgressStyle.classic,
this.prefix,
this.suffix,
this.showPercentage = true,
this.showFraction = false,
});
double get progress => _progress;
void update(double value) {
_progress = value.clamp(0.0, 1.0);
}
String render() {
final buffer = StringBuffer();
// Add prefix
if (prefix != null) {
buffer.write('$prefix ');
}
// Add spinner
if (style == ProgressStyle.spinner) {
buffer.write(_spinnerFrames[_spinnerIndex]);
_spinnerIndex = (_spinnerIndex + 1) % _spinnerFrames.length;
buffer.write(' ');
}
// Build the bar
buffer.write('[');
int filledWidth = (_progress * width).round();
switch (style) {
case ProgressStyle.classic:
buffer.write('=' * filledWidth);
buffer.write(' ' * (width - filledWidth));
break;
case ProgressStyle.blocks:
buffer.write('ā' * filledWidth);
buffer.write('ā' * (width - filledWidth));
break;
case ProgressStyle.dots:
buffer.write('ā' * filledWidth);
buffer.write('ā' * (width - filledWidth));
break;
case ProgressStyle.arrow:
if (filledWidth > 0) {
buffer.write('=' * (filledWidth - 1));
buffer.write('>');
}
buffer.write(' ' * (width - filledWidth));
break;
case ProgressStyle.spinner:
buffer.write('=' * filledWidth);
buffer.write(' ' * (width - filledWidth));
break;
}
buffer.write(']');
// Add percentage
if (showPercentage) {
buffer.write(' ${(_progress * 100).toStringAsFixed(1)}%');
}
// Add fraction
if (showFraction) {
buffer.write(' ($filledWidth/$width)');
}
// Add suffix
if (suffix != null) {
buffer.write(' $suffix');
}
return buffer.toString();
}
void renderToConsole() {
stdout.write('\r${render()}');
}
}
// Step 3: Multi-progress bar
class MultiProgressBar {
final List _bars = [];
final String title;
MultiProgressBar(this.title);
void addBar(ProgressBar bar) {
_bars.add(bar);
}
void updateBar(int index, double value) {
if (index >= 0 && index < _bars.length) {
_bars[index].update(value);
}
}
String render() {
final buffer = StringBuffer();
buffer.writeln(title);
buffer.writeln('ā' * 50);
for (var bar in _bars) {
buffer.writeln(bar.render());
}
return buffer.toString();
}
}
// Step 4: Animated progress demo
Future simulateProgress(ProgressBar bar, double target, String label) async {
print('$label:');
for (double i = 0; i <= target; i += 0.05) {
bar.update(i);
bar.renderToConsole();
await Future.delayed(Duration(milliseconds: 50));
}
bar.update(target);
bar.renderToConsole();
print(''); // New line
}
// Step 5: Main program
Future main() async {
print('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
print('ā PROGRESS BAR BUILDER ā');
print('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
// Demo different styles
print('š Progress Bar Styles:\n');
// Classic style
final classicBar = ProgressBar(
width: 25,
style: ProgressStyle.classic,
prefix: 'Classic',
);
await simulateProgress(classicBar, 0.75, 'Downloading');
// Blocks style
final blocksBar = ProgressBar(
width: 25,
style: ProgressStyle.blocks,
prefix: 'Blocks ',
);
await simulateProgress(blocksBar, 1.0, 'Installing');
// Dots style
final dotsBar = ProgressBar(
width: 25,
style: ProgressStyle.dots,
prefix: 'Dots ',
);
await simulateProgress(dotsBar, 0.5, 'Processing');
// Arrow style
final arrowBar = ProgressBar(
width: 30,
style: ProgressStyle.arrow,
prefix: 'Arrow ',
);
await simulateProgress(arrowBar, 0.9, 'Compiling');
// Multi-progress demo
print('\nš Multiple Progress Bars:\n');
final multi = MultiProgressBar('š Task Progress');
final task1 = ProgressBar(width: 20, style: ProgressStyle.blocks, prefix: 'Task 1');
final task2 = ProgressBar(width: 20, style: ProgressStyle.blocks, prefix: 'Task 2');
final task3 = ProgressBar(width: 20, style: ProgressStyle.blocks, prefix: 'Task 3');
multi.addBar(task1);
multi.addBar(task2);
multi.addBar(task3);
for (int i = 0; i <= 20; i++) {
double progress = i / 20;
task1.update((progress * 0.8).clamp(0, 1));
task2.update((progress * 0.5).clamp(0, 1));
task3.update((progress * 1.0).clamp(0, 1));
// Clear screen and re-render
stdout.write('\x1B[2J\x1B[H'); // ANSI clear screen
print(multi.render());
await Future.delayed(Duration(milliseconds: 100));
}
print('\nā
All tasks complete!');
}