Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pkgs/cli_util/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
29 changes: 24 additions & 5 deletions pkgs/cli_util/lib/src/components/select_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -142,12 +144,13 @@ Future<Set<int>?> _runDialog(
final cleanupTasks = <FutureOr<void> 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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Comment thread
jakemac53 marked this conversation as resolved.
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
Expand Down
2 changes: 1 addition & 1 deletion pkgs/cli_util/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkgs/cli_util/test/fake_terminal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
52 changes: 30 additions & 22 deletions pkgs/cli_util/test/select_dialog_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -262,23 +266,23 @@ void main() {
>$uBox apple
$uBox banana
$uBox cherry
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.down);
await pumpEventQueue();
expect(mockStdout.terminal.content, '''
$uBox apple
>$uBox banana
$uBox cherry
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.space);
await pumpEventQueue();
expect(mockStdout.terminal.content, '''
$uBox apple
>$sBox banana
$uBox cherry
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.enter);
expect(await future, multiSelect ? {1} : 1);
Expand All @@ -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();
Expand All @@ -309,7 +313,7 @@ void main() {
>$sBox c █
$uBox d █
$uBox e │
''');
${maybeLegend()}''');

inputController.addKeys(List.filled(4, KeyVariants.down));
await pumpEventQueue();
Expand All @@ -320,7 +324,7 @@ void main() {
$uBox e █
$uBox f █
>$uBox g █
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.enter);
expect(await future, multiSelect ? {2} : 6);
Expand All @@ -341,7 +345,7 @@ void main() {
$uBox 2 │
$uBox 3 │
$uBox 4 │
''');
${maybeLegend()}''');

inputController.addKeys(List.filled(2, KeyVariants.down));
await pumpEventQueue();
Expand All @@ -351,7 +355,7 @@ void main() {
>$uBox 2 │
$uBox 3 │
$uBox 4 │
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.down);
await pumpEventQueue();
Expand All @@ -361,7 +365,7 @@ void main() {
>$uBox 3 │
$uBox 4 │
$uBox 5 │
''');
${maybeLegend()}''');

inputController.addKeys(List.filled(6, KeyVariants.down));
await pumpEventQueue();
Expand All @@ -371,7 +375,7 @@ void main() {
>$uBox 9 │
$uBox 10 │
$uBox 11 │
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.down);
await pumpEventQueue();
Expand All @@ -381,7 +385,7 @@ void main() {
>$uBox 10 █
$uBox 11 │
$uBox 12 │
''');
${maybeLegend()}''');

inputController.addKeys(List.filled(5, KeyVariants.down));
await pumpEventQueue();
Expand All @@ -391,7 +395,7 @@ void main() {
>$uBox 15 █
$uBox 16 │
$uBox 17 │
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.down);
await pumpEventQueue();
Expand All @@ -401,7 +405,7 @@ void main() {
>$uBox 16 │
$uBox 17 █
$uBox 18 │
''');
${maybeLegend()}''');

inputController.addKeys(List.filled(5, KeyVariants.down));
await pumpEventQueue();
Expand All @@ -411,7 +415,7 @@ void main() {
>$uBox 21 │
$uBox 22 █
$uBox 23 │
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.down);
await pumpEventQueue();
Expand All @@ -421,7 +425,7 @@ void main() {
>$uBox 22 │
$uBox 23 │
$uBox 24 █
''');
${maybeLegend()}''');

inputController.addKeys(List.filled(2, KeyVariants.down));
await pumpEventQueue();
Expand All @@ -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);
Expand All @@ -452,7 +456,7 @@ void main() {
$uBox 2 █
$uBox 3 █
$uBox 4 │
''');
${maybeLegend()}''');

inputController.addKeys(List.filled(4, KeyVariants.down));
await pumpEventQueue();
Expand All @@ -463,7 +467,7 @@ void main() {
$uBox 3 █
>$uBox 4 █
$uBox 5 █
''');
${maybeLegend()}''');

inputController.addKey(KeyVariants.enter);
expect(await future, multiSelect ? <int>{} : 4);
Expand All @@ -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 ? <int>{0} : 0);
Expand All @@ -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 ? <int>{0} : 0);
Expand All @@ -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);
Expand All @@ -538,7 +542,7 @@ void main() {
$uBox c █
$uBox d █
$uBox e │
''');
${maybeLegend()}''');

if (multiSelect) {
inputController.addKeys([KeyVariants.space, KeyVariants.enter]);
Expand All @@ -561,6 +565,7 @@ void main() {
> [ ] apple
[x] banana
[x] cherry
$multiSelectLegend
''');
inputController.addKey(KeyVariants.enter);
expect(await future, {1, 2});
Expand All @@ -577,6 +582,7 @@ void main() {
> [ ] apple
[ ] banana
[ ] cherry
$multiSelectLegend
''');

inputController.addKey(KeyVariants.selectAll);
Expand All @@ -585,6 +591,7 @@ void main() {
> [x] apple
[x] banana
[x] cherry
$multiSelectLegend
''');

inputController.addKey(KeyVariants.selectAll);
Expand All @@ -593,6 +600,7 @@ void main() {
> [ ] apple
[ ] banana
[ ] cherry
$multiSelectLegend
''');

inputController.addKey(KeyVariants.enter);
Expand Down
Loading