Skip to content

Commit

Permalink
feat: implement checkbox component
Browse files Browse the repository at this point in the history
  • Loading branch information
LeadcodeDev committed Sep 27, 2024
1 parent 307ac6d commit 229609f
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 3 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.4.0

- Add `hidden` property on `input` component
- Implement `checkbox` component

## 1.3.0

- Add select max result into configurable option
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ A simple example of using Commander to create a progress component :

```dart
void main() async {
final progress = ProgressBar(max: 50);
final progress = Progress(max: 50);
for (int i = 0; i < 50; i++) {
progress.next(message: [Print('Downloading file ${i + 1}/50...')]);
Expand All @@ -136,3 +136,18 @@ void main() async {
]);
}
```

### Checkbox component
A simple example of using Commander to create a checkbox component :

```dart
Future<void> main() async {
final checkbox = Checkbox(
answer: 'What is your favorite pet ?',
options: ['cat', 'dog', 'bird'],
);
final value = await checkbox.handle();
print(value);
}
```
11 changes: 11 additions & 0 deletions example/checkbox.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:commander_ui/src/components/checkbox.dart';

Future<void> main() async {
final checkbox = Checkbox(
answer: 'What is your favorite pet ?',
options: ['cat', 'dog', 'bird'],
);

final value = await checkbox.handle();
print(value);
}
2 changes: 1 addition & 1 deletion example/progress_bar.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:async';

import 'package:commander_ui/src/components/progress_bar.dart';
import 'package:commander_ui/src/components/progress.dart';
import 'package:mansion/mansion.dart';

void main() async {
Expand Down
6 changes: 6 additions & 0 deletions lib/commander_ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ export '../src/application/stdin_buffer.dart';
export '../src/commons/ansi_character.dart';
export '../src/commons/cli.dart';
export '../src/commons/color.dart';

export '../src/component.dart';
export '../src/components/input.dart';
export '../src/components/select.dart';
export '../src/components/checkbox.dart';
export '../src/components/delayed.dart';
export '../src/components/progress.dart';
export '../src/components/switch.dart';

export '../src/key_down_event_listener.dart';
export '../src/result.dart';
1 change: 1 addition & 0 deletions lib/src/commons/ansi_character.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum AnsiCharacter {
rightArrow('\u001b[C'),
leftArrow('\u001b[D'),
del('\u007f'),
space('\x20'),
enter('\n');

final String value;
Expand Down
263 changes: 263 additions & 0 deletions lib/src/components/checkbox.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import 'dart:async';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:commander_ui/commander_ui.dart';
import 'package:mansion/mansion.dart';

/// A class that represents a checkbox component.
/// This component handles user selection from a list of options.
final class Checkbox<T> with Tools implements Component<T> {
int currentIndex = 0;
bool isRendering = false;

final List<T> options;
final List<int> _selectedIndexes = [];
final String answer;
final String? placeholder;
late final List<Sequence> noResultFoundMessage;
late final List<Sequence> exitMessage;

final String Function(T)? onDisplay;
late final List<Sequence> Function(String) selectedLineStyle;
late final List<Sequence> Function(String) unselectedLineStyle;
late final List<Sequence> Function(String) highlightedSelectedLineStyle;
late final List<Sequence> Function(String) highlightedUnselectedLineStyle;

final _completer = Completer<List<T>>();

/// Creates a new instance of [Checkbox].
///
/// * The [answer] parameter is the question that the user is asked.
/// * The [options] parameter is the list of options that the user can select from.
/// * The [onDisplay] parameter is a function that transforms an option into a string for display.
/// * The [placeholder] parameter is an optional placeholder for the input.
/// * The [noResultFoundMessage] parameter is an optional message that is displayed when no results are found.
/// * The [exitMessage] parameter is an optional message that is displayed when the user exits the input.
/// * The [selectedLineStyle] parameter is a function that styles the selected line.
/// * The [unselectedLineStyle] parameter is a function that styles the unselected line.
/// * The [highlightedSelectedLineStyle] parameter is a function that styles the highlighted selected line.
/// * The [highlightedUnselectedLineStyle] parameter is a function that styles the highlighted unselected line.
Checkbox({
required this.answer,
required this.options,
this.onDisplay,
this.placeholder,
List<Sequence>? noResultFoundMessage,
List<Sequence>? exitMessage,
List<Sequence> Function(String)? selectedLineStyle,
List<Sequence> Function(String)? unselectedLineStyle,
List<Sequence> Function(String)? highlightedSelectedLineStyle,
List<Sequence> Function(String)? highlightedUnselectedLineStyle,
}) {
StdinBuffer.initialize();

this.noResultFoundMessage = noResultFoundMessage ??
[
SetStyles(Style.foreground(Color.brightBlack)),
Print('No result found'),
SetStyles.reset,
];

this.exitMessage = exitMessage ??
[
SetStyles(Style.foreground(Color.brightRed)),
Print('✘'),
SetStyles.reset,
Print(' Operation canceled by user'),
AsciiControl.lineFeed,
];

this.selectedLineStyle = selectedLineStyle ??
(line) => [
SetStyles(Style.foreground(Color.brightGreen)),
Print('•'),
SetStyles(Style.foreground(Color.brightBlack)),
Print(' $line'),
SetStyles.reset,
];

this.unselectedLineStyle = unselectedLineStyle ??
(line) => [
SetStyles(Style.foreground(Color.brightBlack)),
Print('•'.padRight(2)),
Print(line),
SetStyles.reset,
];

this.highlightedSelectedLineStyle = highlightedSelectedLineStyle ??
(line) => [
SetStyles(Style.foreground(Color.brightGreen)),
Print('•'),
SetStyles.reset,
Print(' $line'),
];

this.highlightedUnselectedLineStyle = highlightedUnselectedLineStyle ??
(line) => [
SetStyles(Style.foreground(Color.brightBlack)),
Print('•'),
SetStyles.reset,
Print(' $line'),
];
}

/// Handles the select component and returns a [Future] that completes with the result of the selection.
Future<List<T>> handle() async {
saveCursorPosition();
hideCursor();
hideInput();

KeyDownEventListener()
..match(AnsiCharacter.downArrow, onKeyDown)
..match(AnsiCharacter.upArrow, onKeyUp)
..match(AnsiCharacter.enter, onSubmit)
..match(AnsiCharacter.space, onSpace)
..onExit(onExit);

render();

return _completer.future;
}

void onKeyDown(String key, void Function() dispose) {
saveCursorPosition();
if (currentIndex != 0) {
currentIndex = currentIndex - 1;
}
render();
}

void onKeyUp(String key, void Function() dispose) {
saveCursorPosition();
if (currentIndex < options.length - 1) {
currentIndex = currentIndex + 1;
}
render();
}

void onSubmit(String key, void Function() dispose) {
restoreCursorPosition();
clearFromCursorToEnd();
showInput();

dispose();

if (options.elementAtOrNull(currentIndex) == null) {
throw Exception('No result found');
}

final value = onDisplay?.call(options[currentIndex]) ?? options[currentIndex].toString();

stdout.writeAnsiAll([
SetStyles(Style.foreground(Color.green)),
Print('✔'),
SetStyles.reset,
Print(' $answer '),
SetStyles(Style.foreground(Color.brightBlack)),
Print(value),
SetStyles.reset
]);

stdout.writeln();

saveCursorPosition();
showCursor();

final selectedOptions =
options.whereIndexed((index, _) => _selectedIndexes.contains(index)).toList();
_completer.complete(selectedOptions);
}

void onExit(void Function() dispose) {
dispose();

restoreCursorPosition();
clearFromCursorToEnd();
showInput();

stdout.writeAnsiAll(exitMessage);
exit(1);
}

void onSpace(String key, void Function() dispose) {
saveCursorPosition();

if (_selectedIndexes.contains(currentIndex)) {
_selectedIndexes.remove(currentIndex);
} else {
_selectedIndexes.add(currentIndex);
}

render();
}

void render() async {
isRendering = true;

saveCursorPosition();

final buffer = StringBuffer();
final List<Sequence> copy = [];

buffer.writeAnsiAll([
SetStyles(Style.foreground(Color.yellow)),
Print('?'),
SetStyles.reset,
Print(' $answer : '),
SetStyles(Style.foreground(Color.brightBlack)),
Print(placeholder ?? ''),
SetStyles.reset,
]);

copy.add(AsciiControl.lineFeed);

for (int i = 0; i < options.length; i++) {
final value = onDisplay?.call(options[i]) ?? options[i].toString();

if (_selectedIndexes.contains(i)) {
if (currentIndex == i) {
copy.addAll([...highlightedSelectedLineStyle(value), AsciiControl.lineFeed]);
} else {
copy.addAll([...selectedLineStyle(value), AsciiControl.lineFeed]);
}
} else {
if (currentIndex == i) {
copy.addAll([...highlightedUnselectedLineStyle(value), AsciiControl.lineFeed]);
} else {
copy.addAll([...unselectedLineStyle(value), AsciiControl.lineFeed]);
}
}
}

while (copy.isNotEmpty) {
buffer.writeAnsi(copy.removeAt(0));
}

buffer.writeAnsiAll([
AsciiControl.lineFeed,
SetStyles(Style.foreground(Color.brightBlack)),
Print('(Type to filter, press ↑/↓ to navigate, enter to select)'),
SetStyles.reset,
]);

final availableLines = await getAvailableLinesBelowCursor();
final linesNeeded = buffer.toString().split('\n').length;

if (availableLines < linesNeeded) {
moveCursorUp(count: linesNeeded - availableLines);
saveCursorPosition();
clearFromCursorToEnd();
}

clearFromCursorToEnd();
restoreCursorPosition();
saveCursorPosition();

stdout.write(buffer.toString());

restoreCursorPosition();

isRendering = false;
}
}
File renamed without changes.
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: commander_ui
description: Commander is a Dart library for creating user interfaces within the terminal.
version: 1.3.0
version: 1.4.0
repository: https://github.com/LeadcodeDev/commander

topics:
Expand Down

0 comments on commit 229609f

Please sign in to comment.