dev_build console menu
package:dev_build/menu/menu_io.dart turns a Dart script into a numbered
console menu: you declare menus and items with plain functions, the console
prints the choices, reads a line on stdin and runs the item (sync or async).
Nested menus, enter/leave hooks and prompts are supported. dev_test uses
the same declarations to run tests as a menu.
// tool/menu.dart — run with: dart run tool/menu.dart
import 'package:dev_build/menu/menu_io.dart';
void main(List<String> arguments) {
mainMenuConsole(arguments, () {
menu('main', () {
item('say hi', () => write('hi'));
item('slow', () async {
await Future<void>.delayed(const Duration(seconds: 1));
write('done');
});
});
});
}
Guidelines
Structure
- Import
package:dev_build/menu/menu_io.dart in a script; it re-exports
package:dev_build/menu/menu.dart (the declaration API) and adds
initMenuConsole(arguments) and mainMenuConsole(arguments, declare).
mainMenuConsole initializes the console then calls declare.
- Declare with
menu(name, body) (a sub menu, body must be synchronous)
and item(name, body) (body may return a Future; it is awaited).
Declarations are collected and the menu is shown after the declaring
microtask completes, so declare everything up front, not from inside an
item.
- Items are listed as
0 name, 1 name... Pass cmd: 'x' to item or
menu to use a word instead of the index. . pops the current menu and
exits when typed at the top level, ? prints the menu again.
- Items declared outside any
menu() form the top level. A menu() is
listed as N menu name and has to be entered first (N), so a script
with a single menu('main', ...) starts with 0 to reach its items.
- Extra command line arguments are executed as if typed:
dart run tool/menu.dart 0 2 . runs item 0, then item 2, then ..
-h prints the console help, -v echoes each command.
write(message) / writeln(message) print through the active presenter;
use them instead of print inside menu bodies. await prompt('Name')
reads one line from the user and returns it.
enter(body) / leave(body) run once when a menu is entered / left.
enterItem(body) / leaveItem(body) run before / after every item of
that menu (leave hooks also run when the item throws). Errors thrown by
an item are printed (ERROR CAUGHT) and the menu stays open.
command((line) {...}) on a menu receives any typed line that is not an
item index, a cmd or ./?.
showMenu(() { item(...); }) pushes a menu declared on the fly and
completes when it is popped; popMenu() leaves the current menu
programmatically (returns false at the top level).
solo_item(...) / solo_menu(...) (also item(..., solo: true)) run
only that item/menu when the script starts, for a quick debug loop. They
are @doNotSubmit: the analyzer flags them so they are not committed.
Same for devWrite.
menuRun() runs the declared menu without mainMenuConsole (e.g. in a
test, the output goes to print) and resets the declaration.
Ready-made CI menu
package:dev_build/menu/menu_run_ci.dart exports runCiMenu(path)
which declares items for info, pub get, pub upgrade,
pub downgrade, dump dependencies, run_ci, analyze, format and
a cd (prompt) for the package at path. Wrap it in mainMenuConsole
or nest it in your own menu.
Platform
menu.dart alone has no dart:io dependency and can be imported from
Flutter or web code. menu_io.dart reads stdin; on non-io platforms
initMenuConsole is a no-op. Interactive use is meant for
dart run scripts.
Examples
Nested menus with enter/leave hooks
import 'package:dev_build/menu/menu_io.dart';
void main(List<String> arguments) {
mainMenuConsole(arguments, () {
menu('db', () {
enter(() async {
write('opening database');
});
leave(() async {
write('closing database');
});
enterItem(() => write('--'));
item('count', () async {
write('count: 42');
});
item('clear', cmd: 'c', () async {
write('cleared');
});
item('back', () => popMenu());
});
item('print args', () => write(arguments));
});
}
Prompting the user
import 'package:dev_build/menu/menu_io.dart';
void main(List<String> arguments) {
mainMenuConsole(arguments, () {
item('greet', () async {
var name = await prompt('Your name');
write('Hello $name');
});
});
}
Menu built from data with showMenu
import 'package:dev_build/menu/menu_io.dart';
void main(List<String> arguments) {
var files = ['a.txt', 'b.txt'];
mainMenuConsole(arguments, () {
item('pick a file', () async {
await showMenu(() {
for (var file in files) {
item(file, () async {
write('picked $file');
await popMenu();
});
}
});
});
});
}
Catch-all command handler
import 'package:dev_build/menu/menu_io.dart';
void main(List<String> arguments) {
mainMenuConsole(arguments, () {
menu('shell', () {
command((line) async {
write('unknown command: $line');
});
item('help', () => write('type anything'));
});
});
}
CI menu for the current package
import 'package:dev_build/menu/menu_io.dart';
import 'package:dev_build/menu/menu_run_ci.dart';
Future<void> main(List<String> args) async {
mainMenuConsole(args, () {
runCiMenu('.');
});
}
Debugging one item with solo_item
import 'package:dev_build/menu/menu_io.dart';
void main(List<String> arguments) {
mainMenuConsole(arguments, () {
item('one', () => write('one'));
// Only this item runs at startup; remove before committing.
// ignore: invalid_use_of_do_not_submit_member
solo_item('two', () => write('two'));
});
}
Testing a menu script
import 'package:dev_build/menu/menu.dart';
import 'package:test/test.dart';
void main() {
test('enterItem/leaveItem wrap the item', () async {
var log = <String>[];
menu('main', () {
enterItem(() => log.add('enter'));
leaveItem(() => log.add('leave'));
// ignore: invalid_use_of_do_not_submit_member
solo_item('work', () => log.add('work'));
});
await menuRun();
expect(log, ['enter', 'work', 'leave']);
});
}
Common mistakes
- Making the body of
menu() async or calling item() from inside a
running item: declarations must happen synchronously at declaration time
(use showMenu for dynamic menus).
- Using
print instead of write inside items: it bypasses the presenter
(works in a console, lost elsewhere).
- Forgetting
. in scripted invocations (dart run tool/menu.dart 0): the
process keeps waiting for input after running item 0.
- Committing
solo_item/solo_menu/devWrite: they are meant for a
temporary debug session.
1---2name: dev-build-menu3description: Use when writing an interactive console script, a developer tool menu, or a demo/debug harness with package:dev_build/menu (menu.dart, menu_io.dart, menu_run_ci.dart): mainMenuConsole, initMenuConsole, menu, item, enter, leave, enterItem, leaveItem, command, write, writeln, prompt, showMenu, popMenu, solo_item, solo_menu, menuRun, runCiMenu, numbered items, cmd shortcuts, initial commands from the command line.4---56# dev_build console menu78`package:dev_build/menu/menu_io.dart` turns a Dart script into a numbered9console menu: you declare menus and items with plain functions, the console10prints the choices, reads a line on stdin and runs the item (sync or async).11Nested menus, enter/leave hooks and prompts are supported. `dev_test` uses12the same declarations to run tests as a menu.1314```dart15// tool/menu.dart — run with: dart run tool/menu.dart16import 'package:dev_build/menu/menu_io.dart';1718void main(List<String> arguments) {19 mainMenuConsole(arguments, () {20 menu('main', () {21 item('say hi', () => write('hi'));22 item('slow', () async {23 await Future<void>.delayed(const Duration(seconds: 1));24 write('done');25 });26 });27 });28}29```3031## Guidelines3233### Structure3435* Import `package:dev_build/menu/menu_io.dart` in a script; it re-exports36 `package:dev_build/menu/menu.dart` (the declaration API) and adds37 `initMenuConsole(arguments)` and `mainMenuConsole(arguments, declare)`.38 `mainMenuConsole` initializes the console then calls `declare`.39* Declare with `menu(name, body)` (a sub menu, `body` must be synchronous)40 and `item(name, body)` (`body` may return a `Future`; it is awaited).41 Declarations are collected and the menu is shown after the declaring42 microtask completes, so declare everything up front, not from inside an43 item.44* Items are listed as `0 name`, `1 name`... Pass `cmd: 'x'` to `item` or45 `menu` to use a word instead of the index. `.` pops the current menu and46 exits when typed at the top level, `?` prints the menu again.47* Items declared outside any `menu()` form the top level. A `menu()` is48 listed as `N menu name` and has to be entered first (`N`), so a script49 with a single `menu('main', ...)` starts with `0` to reach its items.50* Extra command line arguments are executed as if typed:51 `dart run tool/menu.dart 0 2 .` runs item 0, then item 2, then `.`.52 `-h` prints the console help, `-v` echoes each command.53* `write(message)` / `writeln(message)` print through the active presenter;54 use them instead of `print` inside menu bodies. `await prompt('Name')`55 reads one line from the user and returns it.56* `enter(body)` / `leave(body)` run once when a menu is entered / left.57 `enterItem(body)` / `leaveItem(body)` run before / after every item of58 that menu (leave hooks also run when the item throws). Errors thrown by59 an item are printed (`ERROR CAUGHT`) and the menu stays open.60* `command((line) {...})` on a menu receives any typed line that is not an61 item index, a `cmd` or `.`/`?`.62* `showMenu(() { item(...); })` pushes a menu declared on the fly and63 completes when it is popped; `popMenu()` leaves the current menu64 programmatically (returns false at the top level).65* `solo_item(...)` / `solo_menu(...)` (also `item(..., solo: true)`) run66 only that item/menu when the script starts, for a quick debug loop. They67 are `@doNotSubmit`: the analyzer flags them so they are not committed.68 Same for `devWrite`.69* `menuRun()` runs the declared menu without `mainMenuConsole` (e.g. in a70 test, the output goes to `print`) and resets the declaration.7172### Ready-made CI menu7374* `package:dev_build/menu/menu_run_ci.dart` exports `runCiMenu(path)`75 which declares items for `info`, `pub get`, `pub upgrade`,76 `pub downgrade`, `dump dependencies`, `run_ci`, `analyze`, `format` and77 a `cd (prompt)` for the package at `path`. Wrap it in `mainMenuConsole`78 or nest it in your own menu.7980### Platform8182* `menu.dart` alone has no `dart:io` dependency and can be imported from83 Flutter or web code. `menu_io.dart` reads stdin; on non-io platforms84 `initMenuConsole` is a no-op. Interactive use is meant for85 `dart run` scripts.8687## Examples8889### Nested menus with enter/leave hooks9091```dart92import 'package:dev_build/menu/menu_io.dart';9394void main(List<String> arguments) {95 mainMenuConsole(arguments, () {96 menu('db', () {97 enter(() async {98 write('opening database');99 });100 leave(() async {101 write('closing database');102 });103 enterItem(() => write('--'));104 item('count', () async {105 write('count: 42');106 });107 item('clear', cmd: 'c', () async {108 write('cleared');109 });110 item('back', () => popMenu());111 });112 item('print args', () => write(arguments));113 });114}115```116117### Prompting the user118119```dart120import 'package:dev_build/menu/menu_io.dart';121122void main(List<String> arguments) {123 mainMenuConsole(arguments, () {124 item('greet', () async {125 var name = await prompt('Your name');126 write('Hello $name');127 });128 });129}130```131132### Menu built from data with showMenu133134```dart135import 'package:dev_build/menu/menu_io.dart';136137void main(List<String> arguments) {138 var files = ['a.txt', 'b.txt'];139 mainMenuConsole(arguments, () {140 item('pick a file', () async {141 await showMenu(() {142 for (var file in files) {143 item(file, () async {144 write('picked $file');145 await popMenu();146 });147 }148 });149 });150 });151}152```153154### Catch-all command handler155156```dart157import 'package:dev_build/menu/menu_io.dart';158159void main(List<String> arguments) {160 mainMenuConsole(arguments, () {161 menu('shell', () {162 command((line) async {163 write('unknown command: $line');164 });165 item('help', () => write('type anything'));166 });167 });168}169```170171### CI menu for the current package172173```dart174import 'package:dev_build/menu/menu_io.dart';175import 'package:dev_build/menu/menu_run_ci.dart';176177Future<void> main(List<String> args) async {178 mainMenuConsole(args, () {179 runCiMenu('.');180 });181}182```183184### Debugging one item with solo_item185186```dart187import 'package:dev_build/menu/menu_io.dart';188189void main(List<String> arguments) {190 mainMenuConsole(arguments, () {191 item('one', () => write('one'));192 // Only this item runs at startup; remove before committing.193 // ignore: invalid_use_of_do_not_submit_member194 solo_item('two', () => write('two'));195 });196}197```198199### Testing a menu script200201```dart202import 'package:dev_build/menu/menu.dart';203import 'package:test/test.dart';204205void main() {206 test('enterItem/leaveItem wrap the item', () async {207 var log = <String>[];208 menu('main', () {209 enterItem(() => log.add('enter'));210 leaveItem(() => log.add('leave'));211 // ignore: invalid_use_of_do_not_submit_member212 solo_item('work', () => log.add('work'));213 });214 await menuRun();215 expect(log, ['enter', 'work', 'leave']);216 });217}218```219220## Common mistakes221222* Making the body of `menu()` `async` or calling `item()` from inside a223 running item: declarations must happen synchronously at declaration time224 (use `showMenu` for dynamic menus).225* Using `print` instead of `write` inside items: it bypasses the presenter226 (works in a console, lost elsewhere).227* Forgetting `.` in scripted invocations (`dart run tool/menu.dart 0`): the228 process keeps waiting for input after running item 0.229* Committing `solo_item`/`solo_menu`/`devWrite`: they are meant for a230 temporary debug session.