diff --git a/pkgs/cli_util/CHANGELOG.md b/pkgs/cli_util/CHANGELOG.md index 4355f64fd9..22d7619946 100644 --- a/pkgs/cli_util/CHANGELOG.md +++ b/pkgs/cli_util/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.2 + +- Add a legend to multiselect dialogs to make the usage more obvious. + ## 0.5.1 - Add `showMultiSelectDialog` and `showSingleSelectDialog` to new diff --git a/pkgs/cli_util/lib/src/components/select_dialog.dart b/pkgs/cli_util/lib/src/components/select_dialog.dart index b7c14206de..95d60e9d22 100644 --- a/pkgs/cli_util/lib/src/components/select_dialog.dart +++ b/pkgs/cli_util/lib/src/components/select_dialog.dart @@ -9,6 +9,8 @@ import 'dart:async'; import 'dart:io'; import 'dart:math' as math; +import 'package:meta/meta.dart'; + import 'keys.dart'; /// Shows a scrollable terminal selection dialog and returns the set of @@ -142,12 +144,13 @@ Future?> _runDialog( final cleanupTasks = Function()>[ () { // Try to clear the dialog from the terminal - final visibleCount = math.min(options.length, maxVisibleItems); - stdout.write('\x1b[${visibleCount}A'); // Move cursor to top - for (var i = 0; i < visibleCount; i++) { + var linesToClear = math.min(options.length, maxVisibleItems); + if (multiSelect) linesToClear++; + stdout.write('\x1b[${linesToClear}A'); // Move cursor to top + for (var i = 0; i < linesToClear; i++) { stdout.write('\x1b[2K\n'); // Clear each line } - stdout.write('\x1b[${visibleCount}A'); // Move back + stdout.write('\x1b[${linesToClear}A'); // Move back }, ]; try { @@ -317,7 +320,8 @@ void _render({ // Move the cursor to the top of the dialog if we're not on the first render. if (!isFirstRender) { - stdout.write('\x1b[${visibleCount}A'); + final linesToMoveUp = multiSelect ? visibleCount + 1 : visibleCount; + stdout.write('\x1b[${linesToMoveUp}A'); } var thumbHeight = 0; @@ -383,6 +387,21 @@ void _render({ stdout.write('$line\n'); } } + + if (multiSelect) { + stdout.write('\x1b[2m$multiSelectLegend\x1b[0m\n'); + } +} + +/// The legend text displayed at the bottom of multi-select dialogs, trimmed +/// to the terminal width. +@visibleForTesting +String get multiSelectLegend { + const fullText = + 'Toggle: Space | Toggle All: Ctrl+A | Submit: Enter | Abort: Esc'; + final width = _terminalWidth; + final limit = math.max(0, width - 1); + return fullText.length > limit ? fullText.substring(0, limit) : fullText; } /// Returns the minimum width required to display a dialog with the given diff --git a/pkgs/cli_util/pubspec.yaml b/pkgs/cli_util/pubspec.yaml index 6367dafd5e..f3979b6365 100644 --- a/pkgs/cli_util/pubspec.yaml +++ b/pkgs/cli_util/pubspec.yaml @@ -1,5 +1,5 @@ name: cli_util -version: 0.5.1 +version: 0.5.2 description: A library to help in building Dart command-line apps. repository: https://github.com/dart-lang/tools/tree/main/pkgs/cli_util issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Acli_util diff --git a/pkgs/cli_util/test/fake_terminal.dart b/pkgs/cli_util/test/fake_terminal.dart index 6463e66251..002d53c1f4 100644 --- a/pkgs/cli_util/test/fake_terminal.dart +++ b/pkgs/cli_util/test/fake_terminal.dart @@ -25,7 +25,7 @@ class FakeTerminal { void write(String text) { // Regex for the specific escape sequences used in select_dialog.dart final seqRegex = RegExp( - r'(\x1b\[\d*A|\x1b\[2K|\x1b\[1m|\x1b\[0m|\x1b\[\?25[lh])', + r'(\x1b\[\d*A|\x1b\[2K|\x1b\[1m|\x1b\[2m|\x1b\[0m|\x1b\[\?25[lh])', ); var lastEnd = 0; diff --git a/pkgs/cli_util/test/select_dialog_test.dart b/pkgs/cli_util/test/select_dialog_test.dart index a21a85c369..cb3fca6341 100644 --- a/pkgs/cli_util/test/select_dialog_test.dart +++ b/pkgs/cli_util/test/select_dialog_test.dart @@ -251,6 +251,10 @@ void main() { final renderer = multiSelect ? showMultiSelectDialog : showSingleSelectDialog; + // Always compute this on demand to account for the current terminal + // width. + String maybeLegend() => multiSelect ? '$multiSelectLegend\n' : ''; + test('renders UI state correctly', () async { final future = renderer([ 'apple', @@ -262,7 +266,7 @@ void main() { >$uBox apple $uBox banana $uBox cherry -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.down); await pumpEventQueue(); @@ -270,7 +274,7 @@ void main() { $uBox apple >$uBox banana $uBox cherry -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.space); await pumpEventQueue(); @@ -278,7 +282,7 @@ void main() { $uBox apple >$sBox banana $uBox cherry -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.enter); expect(await future, multiSelect ? {1} : 1); @@ -298,7 +302,7 @@ void main() { $uBox c █ $uBox d █ $uBox e │ -'''); +${maybeLegend()}'''); inputController.addKeys(List.filled(2, KeyVariants.down)); inputController.addKey(KeyVariants.space); await pumpEventQueue(); @@ -309,7 +313,7 @@ void main() { >$sBox c █ $uBox d █ $uBox e │ -'''); +${maybeLegend()}'''); inputController.addKeys(List.filled(4, KeyVariants.down)); await pumpEventQueue(); @@ -320,7 +324,7 @@ void main() { $uBox e █ $uBox f █ >$uBox g █ -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.enter); expect(await future, multiSelect ? {2} : 6); @@ -341,7 +345,7 @@ void main() { $uBox 2 │ $uBox 3 │ $uBox 4 │ -'''); +${maybeLegend()}'''); inputController.addKeys(List.filled(2, KeyVariants.down)); await pumpEventQueue(); @@ -351,7 +355,7 @@ void main() { >$uBox 2 │ $uBox 3 │ $uBox 4 │ -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.down); await pumpEventQueue(); @@ -361,7 +365,7 @@ void main() { >$uBox 3 │ $uBox 4 │ $uBox 5 │ -'''); +${maybeLegend()}'''); inputController.addKeys(List.filled(6, KeyVariants.down)); await pumpEventQueue(); @@ -371,7 +375,7 @@ void main() { >$uBox 9 │ $uBox 10 │ $uBox 11 │ -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.down); await pumpEventQueue(); @@ -381,7 +385,7 @@ void main() { >$uBox 10 █ $uBox 11 │ $uBox 12 │ -'''); +${maybeLegend()}'''); inputController.addKeys(List.filled(5, KeyVariants.down)); await pumpEventQueue(); @@ -391,7 +395,7 @@ void main() { >$uBox 15 █ $uBox 16 │ $uBox 17 │ -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.down); await pumpEventQueue(); @@ -401,7 +405,7 @@ void main() { >$uBox 16 │ $uBox 17 █ $uBox 18 │ -'''); +${maybeLegend()}'''); inputController.addKeys(List.filled(5, KeyVariants.down)); await pumpEventQueue(); @@ -411,7 +415,7 @@ void main() { >$uBox 21 │ $uBox 22 █ $uBox 23 │ -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.down); await pumpEventQueue(); @@ -421,7 +425,7 @@ void main() { >$uBox 22 │ $uBox 23 │ $uBox 24 █ -'''); +${maybeLegend()}'''); inputController.addKeys(List.filled(2, KeyVariants.down)); await pumpEventQueue(); @@ -431,7 +435,7 @@ void main() { $uBox 22 │ $uBox 23 │ >$uBox 24 █ -'''); +${maybeLegend()}'''); inputController.addKeys([KeyVariants.space, KeyVariants.enter]); expect(await future, multiSelect ? {24} : 24); @@ -452,7 +456,7 @@ void main() { $uBox 2 █ $uBox 3 █ $uBox 4 │ -'''); +${maybeLegend()}'''); inputController.addKeys(List.filled(4, KeyVariants.down)); await pumpEventQueue(); @@ -463,7 +467,7 @@ void main() { $uBox 3 █ >$uBox 4 █ $uBox 5 █ -'''); +${maybeLegend()}'''); inputController.addKey(KeyVariants.enter); expect(await future, multiSelect ? {} : 4); @@ -480,7 +484,7 @@ void main() { expect(mockStdout.terminal.content, ''' >$uBox abcdefg $uBox hijklmn -'''); +${maybeLegend()}'''); inputController.addKeys([KeyVariants.space, KeyVariants.enter]); expect(await future, multiSelect ? {0} : 0); @@ -497,7 +501,7 @@ void main() { expect(mockStdout.terminal.content, ''' >$uBox abcdefg $uBox hijk... -'''); +${maybeLegend()}'''); inputController.addKeys([KeyVariants.space, KeyVariants.enter]); expect(await future, multiSelect ? {0} : 0); @@ -514,7 +518,7 @@ void main() { expect(mockStdout.terminal.content, ''' >$uBox a very long opt... $uBox short -'''); +${maybeLegend()}'''); inputController.addKeys([KeyVariants.space, KeyVariants.enter]); expect(await future, multiSelect ? {0} : 0); @@ -538,7 +542,7 @@ void main() { $uBox c █ $uBox d █ $uBox e │ -'''); +${maybeLegend()}'''); if (multiSelect) { inputController.addKeys([KeyVariants.space, KeyVariants.enter]); @@ -561,6 +565,7 @@ void main() { > [ ] apple [x] banana [x] cherry +$multiSelectLegend '''); inputController.addKey(KeyVariants.enter); expect(await future, {1, 2}); @@ -577,6 +582,7 @@ void main() { > [ ] apple [ ] banana [ ] cherry +$multiSelectLegend '''); inputController.addKey(KeyVariants.selectAll); @@ -585,6 +591,7 @@ void main() { > [x] apple [x] banana [x] cherry +$multiSelectLegend '''); inputController.addKey(KeyVariants.selectAll); @@ -593,6 +600,7 @@ void main() { > [ ] apple [ ] banana [ ] cherry +$multiSelectLegend '''); inputController.addKey(KeyVariants.enter);