Skip to content

Commit 229609f

Browse files
committed
feat: implement checkbox component
1 parent 307ac6d commit 229609f

File tree

9 files changed

+304
-3
lines changed

9 files changed

+304
-3
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.4.0
2+
3+
- Add `hidden` property on `input` component
4+
- Implement `checkbox` component
5+
16
## 1.3.0
27

38
- Add select max result into configurable option

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ A simple example of using Commander to create a progress component :
121121

122122
```dart
123123
void main() async {
124-
final progress = ProgressBar(max: 50);
124+
final progress = Progress(max: 50);
125125
126126
for (int i = 0; i < 50; i++) {
127127
progress.next(message: [Print('Downloading file ${i + 1}/50...')]);
@@ -136,3 +136,18 @@ void main() async {
136136
]);
137137
}
138138
```
139+
140+
### Checkbox component
141+
A simple example of using Commander to create a checkbox component :
142+
143+
```dart
144+
Future<void> main() async {
145+
final checkbox = Checkbox(
146+
answer: 'What is your favorite pet ?',
147+
options: ['cat', 'dog', 'bird'],
148+
);
149+
150+
final value = await checkbox.handle();
151+
print(value);
152+
}
153+
```

example/checkbox.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'package:commander_ui/src/components/checkbox.dart';
2+
3+
Future<void> main() async {
4+
final checkbox = Checkbox(
5+
answer: 'What is your favorite pet ?',
6+
options: ['cat', 'dog', 'bird'],
7+
);
8+
9+
final value = await checkbox.handle();
10+
print(value);
11+
}

example/progress_bar.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'dart:async';
22

3-
import 'package:commander_ui/src/components/progress_bar.dart';
3+
import 'package:commander_ui/src/components/progress.dart';
44
import 'package:mansion/mansion.dart';
55

66
void main() async {

lib/commander_ui.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ export '../src/application/stdin_buffer.dart';
22
export '../src/commons/ansi_character.dart';
33
export '../src/commons/cli.dart';
44
export '../src/commons/color.dart';
5+
56
export '../src/component.dart';
67
export '../src/components/input.dart';
78
export '../src/components/select.dart';
9+
export '../src/components/checkbox.dart';
10+
export '../src/components/delayed.dart';
11+
export '../src/components/progress.dart';
12+
export '../src/components/switch.dart';
13+
814
export '../src/key_down_event_listener.dart';
915
export '../src/result.dart';

lib/src/commons/ansi_character.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ enum AnsiCharacter {
55
rightArrow('\u001b[C'),
66
leftArrow('\u001b[D'),
77
del('\u007f'),
8+
space('\x20'),
89
enter('\n');
910

1011
final String value;

lib/src/components/checkbox.dart

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import 'dart:async';
2+
import 'dart:io';
3+
4+
import 'package:collection/collection.dart';
5+
import 'package:commander_ui/commander_ui.dart';
6+
import 'package:mansion/mansion.dart';
7+
8+
/// A class that represents a checkbox component.
9+
/// This component handles user selection from a list of options.
10+
final class Checkbox<T> with Tools implements Component<T> {
11+
int currentIndex = 0;
12+
bool isRendering = false;
13+
14+
final List<T> options;
15+
final List<int> _selectedIndexes = [];
16+
final String answer;
17+
final String? placeholder;
18+
late final List<Sequence> noResultFoundMessage;
19+
late final List<Sequence> exitMessage;
20+
21+
final String Function(T)? onDisplay;
22+
late final List<Sequence> Function(String) selectedLineStyle;
23+
late final List<Sequence> Function(String) unselectedLineStyle;
24+
late final List<Sequence> Function(String) highlightedSelectedLineStyle;
25+
late final List<Sequence> Function(String) highlightedUnselectedLineStyle;
26+
27+
final _completer = Completer<List<T>>();
28+
29+
/// Creates a new instance of [Checkbox].
30+
///
31+
/// * The [answer] parameter is the question that the user is asked.
32+
/// * The [options] parameter is the list of options that the user can select from.
33+
/// * The [onDisplay] parameter is a function that transforms an option into a string for display.
34+
/// * The [placeholder] parameter is an optional placeholder for the input.
35+
/// * The [noResultFoundMessage] parameter is an optional message that is displayed when no results are found.
36+
/// * The [exitMessage] parameter is an optional message that is displayed when the user exits the input.
37+
/// * The [selectedLineStyle] parameter is a function that styles the selected line.
38+
/// * The [unselectedLineStyle] parameter is a function that styles the unselected line.
39+
/// * The [highlightedSelectedLineStyle] parameter is a function that styles the highlighted selected line.
40+
/// * The [highlightedUnselectedLineStyle] parameter is a function that styles the highlighted unselected line.
41+
Checkbox({
42+
required this.answer,
43+
required this.options,
44+
this.onDisplay,
45+
this.placeholder,
46+
List<Sequence>? noResultFoundMessage,
47+
List<Sequence>? exitMessage,
48+
List<Sequence> Function(String)? selectedLineStyle,
49+
List<Sequence> Function(String)? unselectedLineStyle,
50+
List<Sequence> Function(String)? highlightedSelectedLineStyle,
51+
List<Sequence> Function(String)? highlightedUnselectedLineStyle,
52+
}) {
53+
StdinBuffer.initialize();
54+
55+
this.noResultFoundMessage = noResultFoundMessage ??
56+
[
57+
SetStyles(Style.foreground(Color.brightBlack)),
58+
Print('No result found'),
59+
SetStyles.reset,
60+
];
61+
62+
this.exitMessage = exitMessage ??
63+
[
64+
SetStyles(Style.foreground(Color.brightRed)),
65+
Print('✘'),
66+
SetStyles.reset,
67+
Print(' Operation canceled by user'),
68+
AsciiControl.lineFeed,
69+
];
70+
71+
this.selectedLineStyle = selectedLineStyle ??
72+
(line) => [
73+
SetStyles(Style.foreground(Color.brightGreen)),
74+
Print('•'),
75+
SetStyles(Style.foreground(Color.brightBlack)),
76+
Print(' $line'),
77+
SetStyles.reset,
78+
];
79+
80+
this.unselectedLineStyle = unselectedLineStyle ??
81+
(line) => [
82+
SetStyles(Style.foreground(Color.brightBlack)),
83+
Print('•'.padRight(2)),
84+
Print(line),
85+
SetStyles.reset,
86+
];
87+
88+
this.highlightedSelectedLineStyle = highlightedSelectedLineStyle ??
89+
(line) => [
90+
SetStyles(Style.foreground(Color.brightGreen)),
91+
Print('•'),
92+
SetStyles.reset,
93+
Print(' $line'),
94+
];
95+
96+
this.highlightedUnselectedLineStyle = highlightedUnselectedLineStyle ??
97+
(line) => [
98+
SetStyles(Style.foreground(Color.brightBlack)),
99+
Print('•'),
100+
SetStyles.reset,
101+
Print(' $line'),
102+
];
103+
}
104+
105+
/// Handles the select component and returns a [Future] that completes with the result of the selection.
106+
Future<List<T>> handle() async {
107+
saveCursorPosition();
108+
hideCursor();
109+
hideInput();
110+
111+
KeyDownEventListener()
112+
..match(AnsiCharacter.downArrow, onKeyDown)
113+
..match(AnsiCharacter.upArrow, onKeyUp)
114+
..match(AnsiCharacter.enter, onSubmit)
115+
..match(AnsiCharacter.space, onSpace)
116+
..onExit(onExit);
117+
118+
render();
119+
120+
return _completer.future;
121+
}
122+
123+
void onKeyDown(String key, void Function() dispose) {
124+
saveCursorPosition();
125+
if (currentIndex != 0) {
126+
currentIndex = currentIndex - 1;
127+
}
128+
render();
129+
}
130+
131+
void onKeyUp(String key, void Function() dispose) {
132+
saveCursorPosition();
133+
if (currentIndex < options.length - 1) {
134+
currentIndex = currentIndex + 1;
135+
}
136+
render();
137+
}
138+
139+
void onSubmit(String key, void Function() dispose) {
140+
restoreCursorPosition();
141+
clearFromCursorToEnd();
142+
showInput();
143+
144+
dispose();
145+
146+
if (options.elementAtOrNull(currentIndex) == null) {
147+
throw Exception('No result found');
148+
}
149+
150+
final value = onDisplay?.call(options[currentIndex]) ?? options[currentIndex].toString();
151+
152+
stdout.writeAnsiAll([
153+
SetStyles(Style.foreground(Color.green)),
154+
Print('✔'),
155+
SetStyles.reset,
156+
Print(' $answer '),
157+
SetStyles(Style.foreground(Color.brightBlack)),
158+
Print(value),
159+
SetStyles.reset
160+
]);
161+
162+
stdout.writeln();
163+
164+
saveCursorPosition();
165+
showCursor();
166+
167+
final selectedOptions =
168+
options.whereIndexed((index, _) => _selectedIndexes.contains(index)).toList();
169+
_completer.complete(selectedOptions);
170+
}
171+
172+
void onExit(void Function() dispose) {
173+
dispose();
174+
175+
restoreCursorPosition();
176+
clearFromCursorToEnd();
177+
showInput();
178+
179+
stdout.writeAnsiAll(exitMessage);
180+
exit(1);
181+
}
182+
183+
void onSpace(String key, void Function() dispose) {
184+
saveCursorPosition();
185+
186+
if (_selectedIndexes.contains(currentIndex)) {
187+
_selectedIndexes.remove(currentIndex);
188+
} else {
189+
_selectedIndexes.add(currentIndex);
190+
}
191+
192+
render();
193+
}
194+
195+
void render() async {
196+
isRendering = true;
197+
198+
saveCursorPosition();
199+
200+
final buffer = StringBuffer();
201+
final List<Sequence> copy = [];
202+
203+
buffer.writeAnsiAll([
204+
SetStyles(Style.foreground(Color.yellow)),
205+
Print('?'),
206+
SetStyles.reset,
207+
Print(' $answer : '),
208+
SetStyles(Style.foreground(Color.brightBlack)),
209+
Print(placeholder ?? ''),
210+
SetStyles.reset,
211+
]);
212+
213+
copy.add(AsciiControl.lineFeed);
214+
215+
for (int i = 0; i < options.length; i++) {
216+
final value = onDisplay?.call(options[i]) ?? options[i].toString();
217+
218+
if (_selectedIndexes.contains(i)) {
219+
if (currentIndex == i) {
220+
copy.addAll([...highlightedSelectedLineStyle(value), AsciiControl.lineFeed]);
221+
} else {
222+
copy.addAll([...selectedLineStyle(value), AsciiControl.lineFeed]);
223+
}
224+
} else {
225+
if (currentIndex == i) {
226+
copy.addAll([...highlightedUnselectedLineStyle(value), AsciiControl.lineFeed]);
227+
} else {
228+
copy.addAll([...unselectedLineStyle(value), AsciiControl.lineFeed]);
229+
}
230+
}
231+
}
232+
233+
while (copy.isNotEmpty) {
234+
buffer.writeAnsi(copy.removeAt(0));
235+
}
236+
237+
buffer.writeAnsiAll([
238+
AsciiControl.lineFeed,
239+
SetStyles(Style.foreground(Color.brightBlack)),
240+
Print('(Type to filter, press ↑/↓ to navigate, enter to select)'),
241+
SetStyles.reset,
242+
]);
243+
244+
final availableLines = await getAvailableLinesBelowCursor();
245+
final linesNeeded = buffer.toString().split('\n').length;
246+
247+
if (availableLines < linesNeeded) {
248+
moveCursorUp(count: linesNeeded - availableLines);
249+
saveCursorPosition();
250+
clearFromCursorToEnd();
251+
}
252+
253+
clearFromCursorToEnd();
254+
restoreCursorPosition();
255+
saveCursorPosition();
256+
257+
stdout.write(buffer.toString());
258+
259+
restoreCursorPosition();
260+
261+
isRendering = false;
262+
}
263+
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: commander_ui
22
description: Commander is a Dart library for creating user interfaces within the terminal.
3-
version: 1.3.0
3+
version: 1.4.0
44
repository: https://github.com/LeadcodeDev/commander
55

66
topics:

0 commit comments

Comments
 (0)