Skip to main content

Print and prompt from a CLI

Console handles the everyday work of talking to someone in a terminal. Use it for status messages, questions, tables, progress, and errors without managing ANSI sequences or terminal capabilities yourself.

Quick start

import 'package:artisanal/artisanal.dart';

void main() {
final console = Console();

console.title('My Application');
console.section('Welcome');

console.info('This is an information message');
console.success('Operation completed successfully');
console.warn('This is a warning');
console.error('An error occurred');

final name = console.ask('What is your name?');
console.text('Hello, $name!');

if (console.confirm('Do you want to continue?')) {
console.task('Processing', run: () async {
// Do some work
await Future.delayed(Duration(seconds: 2));
return TaskResult.success;
});
}
}

Creating a Console

Default Console

final console = Console();

Custom Configuration

final console = Console(
verbosity: Verbosity.verbose,
interactive: false,
terminalWidth: 120,
);

Testing Configuration

final output = <String>[];
final console = Console(
out: (line) => output.add(line),
err: (line) => output.add(line),
readLine: () => 'test input',
renderer: StringRenderer(colorProfile: ColorProfile.ascii),
);

Text Output

Basic Output

console.writeln('Hello, World!');
console.write('Loading...');
console.newLine(2);

console.writelnErr('Error message');
console.writeErr('Error details');

Formatted Output

console.title('Main Title');
console.section('Section Header');
console.text('This is some indented text');

console.listing([
'First item',
'Second item',
'Third item',
]);

Message Blocks

console.line('This is a plain line');
console.info('This is an information message');
console.success('Operation completed successfully');
console.comment('This is a comment');
console.question('This is a question');
console.warn('This is a warning');
console.error('An error occurred');
console.note('This is a note');
console.caution('This is a caution');
console.verbose('Verbose message (only in verbose mode)');
console.debug('Debug message (only in debug mode)');

These methods print directly in color without label prefixes, matching Laravel Artisan.

Alert Boxes

console.alert('This is an important alert!');

console.alert('''
This is a multi-line alert
with multiple paragraphs.

- Point 1
- Point 2
- Point 3
''');

Text Styling

Console Tags

Use Symfony/Laravel-style tags for inline styling:

console.writeln('<info>Information message</info>');
console.writeln('<comment>Comment</comment>');
console.writeln('<question>Question?</question>');
console.writeln('<error>Error message</error>');
console.writeln('<success>Success!</success>');
console.writeln('<warning>Warning</warning>');
console.writeln('<alert>Alert!</alert>');
console.writeln('<muted>Muted text</muted>');

Tags automatically map to the active OutputTheme semantic colors.

Custom Styles

console.registerStyle('brand', Style().foreground(BasicColor('#ff5500')).bold());
console.registerStyle('highlight', Style().foreground(Colors.yellow).background(Colors.black));

console.writeln('<brand>My Brand</brand>');
console.writeln('<highlight>Important content</highlight>');

Direct Styling

final style = Style()
.bold()
.foreground(Colors.blue)
.background(Colors.white);

console.writeln(style.render('Styled text'));

Interactive Prompts

Text Input

final name = console.ask('What is your name?');
final email = console.ask('What is your email?', defaultValue: 'user@example.com');

Confirm Dialogs

final confirmed = console.confirm('Are you sure?');
final proceed = console.confirm('Do you want to continue?', defaultValue: true);

Selection Prompts

final choice = console.choice(
'Choose an option:',
['Option 1', 'Option 2', 'Option 3'],
);

final selected = console.choice(
'Pick a color:',
['Red', 'Green', 'Blue'],
defaultValue: 'Green',
);

Password Input

final password = console.secret('Enter your password:');

Prompt Validators

import 'package:artisanal/artisanal.dart';
import 'package:acanthis/acanthis.dart';

void main() {
final console = Console();
final email = console.ask('Email', validator: Validators.email());
final port = console.ask('Port', validator: Validators.integer(min: 1, max: 65535));

final schema = string().min(3).max(20);
final name = console.ask('Name', validator: schema.toValidator());

console.info('Email: $email');
console.info('Port: $port');
console.info('Name: $name');
}

Tasks and Progress

Simple Tasks

console.task('Processing files', run: () async {
await Future.delayed(Duration(seconds: 1));
return TaskResult.success;
});

Complex Tasks

final result = await console.task('Deploying application', run: () async {
console.text('Step 1: Preparing');
await Future.delayed(Duration(milliseconds: 500));

console.text('Step 2: Deploying');
await Future.delayed(Duration(milliseconds: 1000));

console.text('Step 3: Verifying');
await Future.delayed(Duration(milliseconds: 500));

return TaskResult.success;
});

Task Group

final tasks = await console.taskGroup('Setup', [
('Install dependencies', () async {
await Future.delayed(Duration(seconds: 1));
return TaskResult.success;
}),
('Configure environment', () async {
await Future.delayed(Duration(seconds: 0.5));
return TaskResult.success;
}),
('Start server', () async {
await Future.delayed(Duration(seconds: 1));
return TaskResult.success;
}),
]);

if (tasks.success) {
console.success('All tasks completed successfully');
} else {
console.error('${tasks.failed.length} tasks failed');
}

Components

Tables

console.table([
['Name', 'Email', 'Role'],
['John Doe', 'john@example.com', 'Admin'],
['Jane Smith', 'jane@example.com', 'User'],
['Bob Johnson', 'bob@example.com', 'Guest'],
]);

Trees

console.tree({
'Folder 1': {
'File 1.txt': null,
'File 2.txt': null,
'Subfolder': {
'File 3.txt': null,
},
},
'Folder 2': {
'Image.png': null,
},
});

Progress Bars

final progress = console.progressBar(total: 100);
for (var i = 0; i < 100; i++) {
await Future.delayed(Duration(milliseconds: 50));
progress.update(i + 1);
}
progress.complete();

Listings

console.listing([
'Item 1',
'Item 2',
'Item 3',
]);

console.listing([
'First level',
{
'Second level': [
'Third level 1',
'Third level 2',
],
},
]);

Components Helper

final console = Console();

console.components.alert('Heads up!');
console.components.twoColumnDetail('Version', '1.2.3');
console.components.definitionList({
'Host': 'localhost',
'Port': 5432,
});
console.components.horizontalTable({'Env': 'prod', 'Region': 'us-east'});

Advanced Features

Verbosity Control

console.verbose('This is only shown in verbose mode');
console.debug('This is only shown in debug mode');

// Output only when the current verbosity is very verbose or higher
console.info('Detailed info', verbosity: Verbosity.veryVerbose);

// Check verbosity level
if (console.verbosity >= Verbosity.verbose) {
console.writeln('Verbose output');
}

// Create console with specific verbosity
final verboseConsole = Console(verbosity: Verbosity.veryVerbose);

Interactive Mode

final console = Console(interactive: false);

// Check if interactive
if (console.interactive) {
final response = console.ask('Enter input:');
} else {
console.info('Using default settings');
}

Terminal Detection

final console = Console();

// Check terminal capabilities
print(console.renderer.colorProfile); // ColorProfile.trueColor
print(console.renderer.hasDarkBackground); // true/false
print(console.terminalWidth); // 80/120/etc.

Inline Animations

import 'package:artisanal/artisanal.dart';

Future<void> main() async {
final console = Console();
final animation = InlineAnimation(terminal: console.promptTerminal);

await animation.spin(
message: 'Connecting',
task: () async => Future.delayed(const Duration(milliseconds: 200)),
clearOnDone: true,
);

await animation.progress(
message: 'Downloading',
task: (setProgress) async {
for (var i = 0; i <= 100; i++) {
await Future.delayed(const Duration(milliseconds: 10));
setProgress(i / 100);
}
},
);
}

Diff Comments

The Console provides diff rendering with anchor links and selection exposure for interactive diff browsing. These features are useful for code review tools, CLI git clients, and any application that needs to display or interact with unified diffs.

Diff Comment Anchors

Diff comment anchors are hyperlinks embedded within diff output that point to specific lines or hunks. They enable direct navigation to exact changes, making them ideal for code review systems where users need to reference or comment on specific modifications.

The console.diff() method renders a unified diff with line number anchors. In terminals that support hyperlinks (e.g., iTerm2, Kitty, GNOME Terminal), the line numbers become clickable links that can be used to reference specific locations.

Example: Rendering a unified diff with line number anchors

final console = Console();

final unifiedDiff = '''
diff --git a/lib/example.dart b/lib/example.dart
--- a/lib/example.dart
+++ b/lib/example.dart
@@ -10,6 +10,7 @@ void main() {
print('Hello');
print('World');
print('Foo');
+ print('Bar');
print('Baz');
}
''';

console.diff(unifiedDiff);

The output includes anchored line numbers for changed lines. The anchors encode file path, side (left/right), and line number, enabling other tools to parse and link back to those exact locations.

Programmatic anchor access

If you need to retrieve or work with the anchors programmatically (e.g., to build custom UI or generate external links), you can access them via the underlying model:

final model = console.diffModel; // Access the GitDiffModel
final anchors = model.commentAnchors;

for (final anchor in anchors) {
print('${anchor.path}:${anchor.side.name} line ${anchor.line}');
// anchor.key provides a stable DiffCommentLineKey for identification
}

Diff Comment Selection

The Console exposes selected diff hunks or lines for further processing. This allows building interactive diff viewers where users can select a region and perform actions like adding a review comment, copying the selected text, or applying a patch.

Use console.diffSelection() to retrieve the currently selected diff hunk or line content. This returns a DiffSelection? object containing the selected file path, line range, and text.

Example: Selecting a diff hunk and getting its content

final console = Console(interactive: true);

// Render an interactive diff (user can navigate with arrow keys)
console.diff(unifiedDiff, interactive: true);

// Later, retrieve the user's selection
final selected = console.diffSelection();
if (selected != null) {
console.info('Selected hunk in ${selected.path}:');
console.text(selected.content);

// Use selection to add a comment, copy to clipboard, etc.
await addReviewComment(selected.path, selected.line, selected.side);
}

The selection updates as the user moves the cursor in interactive mode. You can also set the selection programmatically for custom navigation controls:

// Jump to a specific line
console.setDiffSelection(
file: 'lib/example.dart',
line: 12,
side: DiffCommentSide.right,
);

Selection state access

For more control, the Console exposes the selection state through properties:

// Check if a selection exists
if (console.hasDiffSelection) {
final anchor = console.selectedDiffAnchor;
final range = console.selectedDiffRange;
// Process selection...
}

// Clear current selection
console.clearDiffSelection();

Interactive Diff Navigation

When interactive: true, the diff viewer supports full keyboard navigation:

KeyAction
/ Move between commentable lines
/ Jump between hunk boundaries
EnterSelect the current line/hunk
SpaceToggle multi-line range selection
EscCancel selection / exit
?Show key binding help
vCycle view mode (unified / side-by-side / pretty)

Navigation state is maintained in the GitDiffModel's viewport and can be queried or controlled via the model's properties and methods.

Diff Display Modes

The diff viewer supports multiple display modes, configurable via console.diffViewMode or per-call:

  • Unified — Traditional git diff format with +/- markers in a single column.
  • Side-by-side — Old version on left, new version on right, separated by .
  • Pretty — Clean layout with single line-number column and minimal noise.
// Set globally
console.diffViewMode = DiffViewMode.sideBySide;

// Or per call
console.diff(diffText, viewMode: DiffViewMode.pretty);

Customizing Diff Appearance

Diff styling can be customized via DiffStyles. The Console provides theme-aware defaults, but you can override specific elements:

console.diffStyles = DiffStyles(
addedLine: Style().foreground(Colors.green).bold(),
removedLine: Style().foreground(Colors.red),
selectedCommentLine: Style().background(Colors.blue).foreground(Colors.white),
);

The DiffStyles.fromColors() factory maps semantic colors from your theme:

console.diffStyles = DiffStyles.fromColors(
success: Colors.green, // added (+) lines
error: Colors.red, // removed (-) lines
muted: Colors.gray, // line numbers, gutters
surface: Colors.gray850, // panel background
onSurface: Colors.white, // file headers
onBackground: Colors.gray200, // context lines
border: Colors.gray600, // separators
);

Diff Model API

When working with diffs programmatically, you can access the underlying GitDiffModel directly:

final model = console.diffModel;

// Parse diff without rendering
final files = model.files;
for (final file in files) {
print('File: ${file.oldPath}${file.newPath}');
print(' +${file.additions} -${file.deletions}');
}

// Get rendered output (useful for testing or custom rendering)
final output = model.view();

// Programmatic navigation
model.viewport.scrollTo(10);
final anchor = model.nearestCommentAnchor(renderLine: 5);

Non-Interactive Mode

In non-interactive environments (CI/CD, logging), diffs are rendered without anchors or interactive prompts:

final console = Console(interactive: false);
console.diff(diffText); // Plain text diff, no hyperlinks or key handling

The rendering remains fully styled with ANSI colors but omits terminal-specific features like hyperlink OSC sequences.


Configuration

Renderer Options

final console = Console(
renderer: StringRenderer(colorProfile: ColorProfile.ansi256),
);

// or

final console = Console(
renderer: TerminalRenderer(forceProfile: ColorProfile.ascii),
);

Custom I/O Callbacks

final output = <String>[];
final console = Console(
out: (line) => output.add(line),
err: (line) => output.add(line),
readLine: () => 'user input',
);

Integration with Args

Commands automatically get a Console instance:

import 'package:artisanal/args.dart';

class MyCommand extends Command<void> {

String get name => 'my-command';


String get description => 'My command description';


void run() {
io.title('My Command');
io.text('This command uses the console');

final name = io.ask('What is your name?');
io.success('Hello, $name!');
}
}