Parse command-line arguments
A Noir entry point is an ordinary Dart console program. Receive the startup
arguments in main(List<String> arguments), parse them before you mount the
widget tree, and pass typed values into the root widget.
Noir’s development runner forwards everything after the entry-point path without interpreting it, so both of these deliver the same arguments:
dart run bin/logview.dart build.log --follow
dart run noir:run bin/logview.dart build.log --followAdd Dart’s argument parser:
dart pub add argsParse flags for one application
Use ArgParser when one root widget takes its initial configuration from flags
or positional values. Parse once in main, convert the result into a type your
application owns, and pass that value to the root widget:
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:noir/noir.dart';
final class LogOptions {
const LogOptions({required this.path, required this.follow});
final String path;
final bool follow;
}
void main(List<String> arguments) {
final parser = ArgParser()
..addFlag('follow', abbr: 'f', negatable: false, help: 'Stay at the end.')
..addFlag('help', abbr: 'h', negatable: false, help: 'Show this usage.');
final LogOptions options;
try {
final results = parser.parse(arguments);
if (results.flag('help')) {
io.stdout.writeln('Usage: logview <file> [options]\n${parser.usage}');
return;
}
if (results.rest.length != 1) {
throw const FormatException('Expected exactly one log file.');
}
options = LogOptions(
path: results.rest.single,
follow: results.flag('follow'),
);
} on FormatException catch (error) {
io.stderr.writeln('${error.message}\n\n${parser.usage}');
io.exitCode = 64;
return;
}
runTuiApp(LogViewApp(options: options));
}
class LogViewApp extends StatelessWidget {
const LogViewApp({required this.options, super.key});
final LogOptions options;
@override
Widget build(BuildContext context) => Panel(
title: options.path,
child: Text(options.follow ? 'Following new lines' : 'Showing the file'),
);
}Three boundaries keep this readable:
mainis the process boundary. It parses, reports usage errors, and callsrunTuiAppexactly once.LogOptionsis the application type.LogViewAppnever readsArgResultsor a string-keyed option map.- Invalid arguments fail before
runTuiApp, so the program never enters terminal mode only to print usage.
ArgParserException extends FormatException, so one catch covers both an
unknown flag and your own validation.
Print usage on --help and exit 0. Report an invalid argument on stderr and
exit 64.
Optional: dispatch several subcommands
Use CommandRunner only when a command name selects a separate shell
operation, such as patcher review against patcher search. A subcommand is
not in-app navigation: after Noir mounts, ordinary widget state owns tabs,
pages, dialogs, and command palettes.
import 'dart:io' as io;
import 'package:args/command_runner.dart';
import 'package:noir/noir.dart';
enum ReviewMode { unified, split }
final class ReviewOptions {
const ReviewOptions({
required this.initialPath,
required this.mode,
required this.staged,
});
final String? initialPath;
final ReviewMode mode;
final bool staged;
}
Future<void> main(List<String> arguments) async {
final runner = CommandRunner<ReviewOptions>(
'patcher',
'Review patches in a terminal UI.',
)..addCommand(ReviewCommand());
final ReviewOptions? options;
try {
options = await runner.run(arguments);
} on UsageException catch (error) {
io.stderr.writeln(error);
io.exitCode = 64;
return;
}
if (options == null) return;
runTuiApp(ReviewApp(options: options));
}
final class ReviewCommand extends Command<ReviewOptions> {
ReviewCommand() {
argParser
..addOption(
'mode',
allowed: [for (final mode in ReviewMode.values) mode.name],
defaultsTo: ReviewMode.unified.name,
)
..addFlag('staged', negatable: false);
}
@override
String get name => 'review';
@override
String get description => 'Review a patch interactively.';
@override
ReviewOptions run() {
final paths = argResults!.rest;
if (paths.length > 1) {
usageException('Expected zero or one patch path.');
}
return ReviewOptions(
initialPath: paths.isEmpty ? null : paths.single,
mode: ReviewMode.values.byName(argResults!.option('mode')!),
staged: argResults!.flag('staged'),
);
}
}
class ReviewApp extends StatelessWidget {
const ReviewApp({required this.options, super.key});
final ReviewOptions options;
@override
Widget build(BuildContext context) => Panel(
title: 'Review',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 1,
children: [
Text(options.initialPath ?? 'Choose a patch in the TUI'),
Text('Mode: ${options.mode.name}'),
Text(options.staged ? 'Source: staged changes' : 'Source: working tree'),
],
),
);
}The leaf command is the boundary between CLI strings and application types. It
returns ReviewOptions without mounting a terminal. CommandRunner.run
returns null after it prints help or top-level usage, so that branch returns
without mounting anything.
If several subcommands select different workflows, give CommandRunner a
sealed result type your application owns. Each leaf returns one subtype, and
main passes the selected value to one root widget. That root may own shared
themes, services, overlays, and in-app navigation. It should not receive
arguments, ArgResults, or the CommandRunner: startup parsing is already
complete.
Decide how a missing value behaves
The review command above allows the patch path to be omitted, so ReviewApp
receives a null initialPath and can collect the path with a TextInput, an
Autocomplete, or a file-picker flow. That is an application decision, not
parser behavior. For a command that must stay non-interactive, reject the
missing value in run() with usageException instead.
Startup parsing happens once
runTuiApp returns as soon as the tree is mounted. Dart’s input and signal
subscriptions keep the process alive after main finishes. Quit from the
widget tree with TuiApp.exit(context) so Noir restores the terminal resources
it owns; calling dart:io’s exit from a running TUI can skip that cleanup.
Hot reload rebuilds the retained widget tree but does not rerun main().
Restart the process to change a command, a flag, or a startup value.
This startup boundary is separate from Noir’s Shortcuts, Intent, and
Actions APIs. Those route semantic input inside a mounted widget tree; see
Handle input and focus. For nested commands, aliases,
multi-value options, and the complete parser contract, continue with the
package:args documentation.