Skip to content

Commit

Permalink
feat/implement input number
Browse files Browse the repository at this point in the history
* feat: implement number component

* feat: remove log
  • Loading branch information
LeadcodeDev authored Nov 7, 2024
1 parent 679c4c8 commit be35afe
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 47 deletions.
15 changes: 9 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# 2.3.0
## 2.4.0
- Implement `number` component

## 2.3.0
- Implement theming components
- Add `AskTheme` property in `ask` component
- Add `SelectTheme` property in `select` component
Expand All @@ -17,26 +20,26 @@
- `maxLength` : Check if the value length is lower than a value
- `equals` : Check if the value is equals to a value

# 2.2.4
## 2.2.4
- Make `task` component as windows compatible
- Change default placeholder for `swap` component in example
- Remove "Tape to search" in `checkbox` component
- Reset cursor position in enter `screen` component

# 2.2.3
## 2.2.3
- Add missing properties `select` in select commander entry
- Fix multiple behaviour instead of single behaviour in `checkbox` component
- Enhance `info` logger method

# 2.2.2
## 2.2.2
- Remove `createSpace` method in `ask` component

# 2.2.1
## 2.2.1
- The ask component disappeared after the validation stage
- Calling `createSpace` method in all rendering cases for `ask` component
- Remove missing `print` statement in `readKey` function

# 2.2.0
## 2.2.0
- Add `select` display handler
- Fix bad FutureOr execution

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,30 @@ Future<void> main() async {
}
```

### Number component

A simple example of using Commander to create a number component :

- ✅ Integrated or custom validators
- ✅ Default value
- ✅ Custom rendering
- ✅ `double` or `int` (`num` by default)

```dart
Future<void> main() async {
final commander = Commander(level: Level.verbose);
final value = await commander.number('What is your age ?',
interval: 1,
onDisplay: (value) => value?.toStringAsFixed(2),
validate: (validator) => validator
..greaterThan(18, message: 'You must be at least 18 years old')
..lowerThan(99, message: 'You must be at most 99 years old'));
print(value);
}
```

### Select component
A simple example of using Commander to create an option selection component :

Expand Down
14 changes: 14 additions & 0 deletions example/number.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:commander_ui/commander_ui.dart';

Future<void> main() async {
final commander = Commander(level: Level.verbose);

final value = await commander.number('What is your age ?',
interval: 1,
onDisplay: (value) => value?.toStringAsFixed(2),
validate: (validator) => validator
..lowerThan(18, message: 'You must be at least 18 years old')
..greaterThan(99, message: 'You must be at most 99 years old'));

print(value);
}
179 changes: 179 additions & 0 deletions lib/src/application/components/number.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import 'dart:async';
import 'dart:io';

import 'package:commander_ui/src/application/terminals/terminal.dart';
import 'package:commander_ui/src/application/themes/default_number_theme.dart';
import 'package:commander_ui/src/application/utils/terminal_tools.dart';
import 'package:commander_ui/src/application/validators/chain_validator.dart';
import 'package:commander_ui/src/domains/models/chain_validator.dart';
import 'package:commander_ui/src/domains/models/component.dart';
import 'package:commander_ui/src/domains/themes/number_theme.dart';
import 'package:commander_ui/src/io.dart';
import 'package:mansion/mansion.dart';

/// A component that asks the user for input.
final class Number<T extends num>
with TerminalTools
implements Component<Future<T>> {
final _completer = Completer<T>();

final Terminal _terminal;
final NumberTheme _theme;

final String _message;
double _value = 0.0;
final double _interval;
final Function(NumberChainValidator)? _validate;
final String? Function(T?) _onDisplay;

String? normalizeValue(double value) {
return _onDisplay(switch (T) {
int => value.toInt() as T,
_ => value as T,
});
}

Number(this._terminal,
{required String message,
T? defaultValue,
T? interval,
Function(NumberChainValidator)? validate,
String? Function(T?)? onDisplay,
NumberTheme? theme})
: _message = message,
_value = double.parse(defaultValue != null ? '$defaultValue' : '0.0'),
_interval = double.parse(interval != null ? '$interval' : '1.0'),
_validate = validate,
_onDisplay = (onDisplay ?? (value) => value.toString()),
_theme = theme ?? DefaultNumberTheme();

@override
Future<T> handle() {
createSpace(_terminal, 1);
stdout.writeAnsiAll([CursorPosition.save, CursorVisibility.hide]);

_render();
_waitResponse();

return _completer.future;
}

void _waitResponse() {
final key = readKey(_terminal);

if (key.controlChar == ControlCharacter.arrowUp || key.char == 'k') {
_value += _interval;
} else if (key.controlChar == ControlCharacter.arrowDown ||
key.char == 'j') {
_value -= _interval;
} else if ([ControlCharacter.ctrlJ, ControlCharacter.ctrlM]
.contains(key.controlChar)) {
if (_validate != null) {
final validator = ValidatorChain();
_validate!(validator);

final result = validator.execute(_value);
if (result case String error) {
_onError(error);

return;
}
}

return _onSuccess();
}

_render();
_waitResponse();
}

void _render() {
final buffer = StringBuffer();

List<Sequence> askSequence = [
..._theme.askPrefixColor,
Print('${_theme.askPrefix} '),
SetStyles.reset,
];

buffer.writeAnsiAll([
CursorPosition.restore,
Clear.afterCursor,
...askSequence,
Print(_message),
const CursorPosition.moveRight(1),
..._theme.inputColor,
Print('${normalizeValue(_value)}'),
]);

stdout.write(buffer.toString());
}

void _onError(String error) {
final buffer = StringBuffer();

List<Sequence> errorSequence = [
..._theme.errorPrefixColor,
Print('${_theme.errorSuffix} '),
SetStyles.reset,
];

buffer.writeAnsiAll([
CursorPosition.restore,
Clear.afterCursor,
...errorSequence,
Print(_message),
const CursorPosition.moveRight(1),
]);

buffer.writeAnsiAll([
AsciiControl.lineFeed,
..._theme.validatorColorMessage,
Print(error),
SetStyles.reset,
]);

stdout.write(buffer.toString());

stdout.writeAnsiAll([
const CursorPosition.moveUp(1),
CursorPosition.moveToColumn(_message.length + 2),
const CursorPosition.moveRight(2),
..._theme.inputColor,
Print('${normalizeValue(_value)}'),
]);

_waitResponse();
}

void _onSuccess() {
final buffer = StringBuffer();

List<Sequence> successSequence = [
..._theme.successPrefixColor,
Print('${_theme.successSuffix} '),
SetStyles.reset,
];

buffer.writeAnsiAll([
CursorPosition.restore,
Clear.untilEndOfLine,
Clear.afterCursor,
...successSequence,
Print(_message),
Print(' '),
..._theme.inputColor,
Print('${normalizeValue(_value)}'),
SetStyles.reset,
AsciiControl.lineFeed,
CursorVisibility.show,
]);

stdout.write(buffer.toString());

return switch (T) {
int => _completer.complete(_value.toInt() as T),
_ => _completer.complete(_value as T),
};
}
}
86 changes: 86 additions & 0 deletions lib/src/application/themes/default_number_theme.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import 'package:commander_ui/src/domains/themes/ask_theme.dart';
import 'package:commander_ui/src/domains/themes/number_theme.dart';
import 'package:mansion/mansion.dart';

final class DefaultNumberTheme implements NumberTheme {
@override
String askPrefix = '?';

@override
String errorSuffix = '✘';

@override
String successSuffix = '✔';

@override
String Function(String? value) defaultValueFormatter =
(String? value) => switch (value) {
String value => ' ($value)',
_ => '',
};

@override
String? Function(String? value) inputFormatter = (String? value) => value;

@override
List<Sequence> successPrefixColor = [
SetStyles.reset,
SetStyles(Style.foreground(Color.green))
];

@override
List<Sequence> errorPrefixColor = [
SetStyles(Style.foreground(Color.brightRed))
];

@override
List<Sequence> askPrefixColor = [SetStyles(Style.foreground(Color.yellow))];

@override
List<Sequence> validatorColorMessage = [
SetStyles(Style.foreground(Color.brightRed))
];

@override
List<Sequence> defaultValueColorMessage = [
SetStyles(Style.foreground(Color.brightBlack))
];

@override
List<Sequence> inputColor = [SetStyles(Style.foreground(Color.brightBlack))];

DefaultNumberTheme();

/// Creates a new [AskTheme] with the provided values based on [DefaultAskTheme].
factory DefaultNumberTheme.copyWith(
{String? askPrefix,
String? errorSuffix,
String? successSuffix,
String Function(String? value)? defaultValueFormatter,
String? Function(String? value)? inputFormatter,
List<Sequence>? successPrefixColor,
List<Sequence>? errorPrefixColor,
List<Sequence>? validatorColorMessage,
List<Sequence>? askPrefixColor,
List<Sequence>? defaultValueColorMessage,
List<Sequence>? inputColor}) {
final theme = DefaultNumberTheme();

theme.askPrefix = askPrefix ?? theme.askPrefix;
theme.errorSuffix = errorSuffix ?? theme.errorSuffix;
theme.successSuffix = successSuffix ?? theme.successSuffix;
theme.defaultValueFormatter =
defaultValueFormatter ?? theme.defaultValueFormatter;
theme.inputFormatter = inputFormatter ?? theme.inputFormatter;
theme.successPrefixColor = successPrefixColor ?? theme.successPrefixColor;
theme.errorPrefixColor = errorPrefixColor ?? theme.errorPrefixColor;
theme.validatorColorMessage =
validatorColorMessage ?? theme.validatorColorMessage;
theme.askPrefixColor = askPrefixColor ?? theme.askPrefixColor;
theme.defaultValueColorMessage =
defaultValueColorMessage ?? theme.defaultValueColorMessage;
theme.inputColor = inputColor ?? theme.inputColor;

return theme;
}
}
Loading

0 comments on commit be35afe

Please sign in to comment.