Skip to content

Commit be35afe

Browse files
authored
feat/implement input number
* feat: implement number component * feat: remove log
1 parent 679c4c8 commit be35afe

File tree

11 files changed

+405
-47
lines changed

11 files changed

+405
-47
lines changed

CHANGELOG.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
# 2.3.0
1+
## 2.4.0
2+
- Implement `number` component
3+
4+
## 2.3.0
25
- Implement theming components
36
- Add `AskTheme` property in `ask` component
47
- Add `SelectTheme` property in `select` component
@@ -17,26 +20,26 @@
1720
- `maxLength` : Check if the value length is lower than a value
1821
- `equals` : Check if the value is equals to a value
1922

20-
# 2.2.4
23+
## 2.2.4
2124
- Make `task` component as windows compatible
2225
- Change default placeholder for `swap` component in example
2326
- Remove "Tape to search" in `checkbox` component
2427
- Reset cursor position in enter `screen` component
2528

26-
# 2.2.3
29+
## 2.2.3
2730
- Add missing properties `select` in select commander entry
2831
- Fix multiple behaviour instead of single behaviour in `checkbox` component
2932
- Enhance `info` logger method
3033

31-
# 2.2.2
34+
## 2.2.2
3235
- Remove `createSpace` method in `ask` component
3336

34-
# 2.2.1
37+
## 2.2.1
3538
- The ask component disappeared after the validation stage
3639
- Calling `createSpace` method in all rendering cases for `ask` component
3740
- Remove missing `print` statement in `readKey` function
3841

39-
# 2.2.0
42+
## 2.2.0
4043
- Add `select` display handler
4144
- Fix bad FutureOr execution
4245

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,30 @@ Future<void> main() async {
4545
}
4646
```
4747

48+
### Number component
49+
50+
A simple example of using Commander to create a number component :
51+
52+
- ✅ Integrated or custom validators
53+
- ✅ Default value
54+
- ✅ Custom rendering
55+
- ✅ `double` or `int` (`num` by default)
56+
57+
```dart
58+
Future<void> main() async {
59+
final commander = Commander(level: Level.verbose);
60+
61+
final value = await commander.number('What is your age ?',
62+
interval: 1,
63+
onDisplay: (value) => value?.toStringAsFixed(2),
64+
validate: (validator) => validator
65+
..greaterThan(18, message: 'You must be at least 18 years old')
66+
..lowerThan(99, message: 'You must be at most 99 years old'));
67+
68+
print(value);
69+
}
70+
```
71+
4872
### Select component
4973
A simple example of using Commander to create an option selection component :
5074

example/number.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:commander_ui/commander_ui.dart';
2+
3+
Future<void> main() async {
4+
final commander = Commander(level: Level.verbose);
5+
6+
final value = await commander.number('What is your age ?',
7+
interval: 1,
8+
onDisplay: (value) => value?.toStringAsFixed(2),
9+
validate: (validator) => validator
10+
..lowerThan(18, message: 'You must be at least 18 years old')
11+
..greaterThan(99, message: 'You must be at most 99 years old'));
12+
13+
print(value);
14+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import 'dart:async';
2+
import 'dart:io';
3+
4+
import 'package:commander_ui/src/application/terminals/terminal.dart';
5+
import 'package:commander_ui/src/application/themes/default_number_theme.dart';
6+
import 'package:commander_ui/src/application/utils/terminal_tools.dart';
7+
import 'package:commander_ui/src/application/validators/chain_validator.dart';
8+
import 'package:commander_ui/src/domains/models/chain_validator.dart';
9+
import 'package:commander_ui/src/domains/models/component.dart';
10+
import 'package:commander_ui/src/domains/themes/number_theme.dart';
11+
import 'package:commander_ui/src/io.dart';
12+
import 'package:mansion/mansion.dart';
13+
14+
/// A component that asks the user for input.
15+
final class Number<T extends num>
16+
with TerminalTools
17+
implements Component<Future<T>> {
18+
final _completer = Completer<T>();
19+
20+
final Terminal _terminal;
21+
final NumberTheme _theme;
22+
23+
final String _message;
24+
double _value = 0.0;
25+
final double _interval;
26+
final Function(NumberChainValidator)? _validate;
27+
final String? Function(T?) _onDisplay;
28+
29+
String? normalizeValue(double value) {
30+
return _onDisplay(switch (T) {
31+
int => value.toInt() as T,
32+
_ => value as T,
33+
});
34+
}
35+
36+
Number(this._terminal,
37+
{required String message,
38+
T? defaultValue,
39+
T? interval,
40+
Function(NumberChainValidator)? validate,
41+
String? Function(T?)? onDisplay,
42+
NumberTheme? theme})
43+
: _message = message,
44+
_value = double.parse(defaultValue != null ? '$defaultValue' : '0.0'),
45+
_interval = double.parse(interval != null ? '$interval' : '1.0'),
46+
_validate = validate,
47+
_onDisplay = (onDisplay ?? (value) => value.toString()),
48+
_theme = theme ?? DefaultNumberTheme();
49+
50+
@override
51+
Future<T> handle() {
52+
createSpace(_terminal, 1);
53+
stdout.writeAnsiAll([CursorPosition.save, CursorVisibility.hide]);
54+
55+
_render();
56+
_waitResponse();
57+
58+
return _completer.future;
59+
}
60+
61+
void _waitResponse() {
62+
final key = readKey(_terminal);
63+
64+
if (key.controlChar == ControlCharacter.arrowUp || key.char == 'k') {
65+
_value += _interval;
66+
} else if (key.controlChar == ControlCharacter.arrowDown ||
67+
key.char == 'j') {
68+
_value -= _interval;
69+
} else if ([ControlCharacter.ctrlJ, ControlCharacter.ctrlM]
70+
.contains(key.controlChar)) {
71+
if (_validate != null) {
72+
final validator = ValidatorChain();
73+
_validate!(validator);
74+
75+
final result = validator.execute(_value);
76+
if (result case String error) {
77+
_onError(error);
78+
79+
return;
80+
}
81+
}
82+
83+
return _onSuccess();
84+
}
85+
86+
_render();
87+
_waitResponse();
88+
}
89+
90+
void _render() {
91+
final buffer = StringBuffer();
92+
93+
List<Sequence> askSequence = [
94+
..._theme.askPrefixColor,
95+
Print('${_theme.askPrefix} '),
96+
SetStyles.reset,
97+
];
98+
99+
buffer.writeAnsiAll([
100+
CursorPosition.restore,
101+
Clear.afterCursor,
102+
...askSequence,
103+
Print(_message),
104+
const CursorPosition.moveRight(1),
105+
..._theme.inputColor,
106+
Print('${normalizeValue(_value)}'),
107+
]);
108+
109+
stdout.write(buffer.toString());
110+
}
111+
112+
void _onError(String error) {
113+
final buffer = StringBuffer();
114+
115+
List<Sequence> errorSequence = [
116+
..._theme.errorPrefixColor,
117+
Print('${_theme.errorSuffix} '),
118+
SetStyles.reset,
119+
];
120+
121+
buffer.writeAnsiAll([
122+
CursorPosition.restore,
123+
Clear.afterCursor,
124+
...errorSequence,
125+
Print(_message),
126+
const CursorPosition.moveRight(1),
127+
]);
128+
129+
buffer.writeAnsiAll([
130+
AsciiControl.lineFeed,
131+
..._theme.validatorColorMessage,
132+
Print(error),
133+
SetStyles.reset,
134+
]);
135+
136+
stdout.write(buffer.toString());
137+
138+
stdout.writeAnsiAll([
139+
const CursorPosition.moveUp(1),
140+
CursorPosition.moveToColumn(_message.length + 2),
141+
const CursorPosition.moveRight(2),
142+
..._theme.inputColor,
143+
Print('${normalizeValue(_value)}'),
144+
]);
145+
146+
_waitResponse();
147+
}
148+
149+
void _onSuccess() {
150+
final buffer = StringBuffer();
151+
152+
List<Sequence> successSequence = [
153+
..._theme.successPrefixColor,
154+
Print('${_theme.successSuffix} '),
155+
SetStyles.reset,
156+
];
157+
158+
buffer.writeAnsiAll([
159+
CursorPosition.restore,
160+
Clear.untilEndOfLine,
161+
Clear.afterCursor,
162+
...successSequence,
163+
Print(_message),
164+
Print(' '),
165+
..._theme.inputColor,
166+
Print('${normalizeValue(_value)}'),
167+
SetStyles.reset,
168+
AsciiControl.lineFeed,
169+
CursorVisibility.show,
170+
]);
171+
172+
stdout.write(buffer.toString());
173+
174+
return switch (T) {
175+
int => _completer.complete(_value.toInt() as T),
176+
_ => _completer.complete(_value as T),
177+
};
178+
}
179+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import 'package:commander_ui/src/domains/themes/ask_theme.dart';
2+
import 'package:commander_ui/src/domains/themes/number_theme.dart';
3+
import 'package:mansion/mansion.dart';
4+
5+
final class DefaultNumberTheme implements NumberTheme {
6+
@override
7+
String askPrefix = '?';
8+
9+
@override
10+
String errorSuffix = '✘';
11+
12+
@override
13+
String successSuffix = '✔';
14+
15+
@override
16+
String Function(String? value) defaultValueFormatter =
17+
(String? value) => switch (value) {
18+
String value => ' ($value)',
19+
_ => '',
20+
};
21+
22+
@override
23+
String? Function(String? value) inputFormatter = (String? value) => value;
24+
25+
@override
26+
List<Sequence> successPrefixColor = [
27+
SetStyles.reset,
28+
SetStyles(Style.foreground(Color.green))
29+
];
30+
31+
@override
32+
List<Sequence> errorPrefixColor = [
33+
SetStyles(Style.foreground(Color.brightRed))
34+
];
35+
36+
@override
37+
List<Sequence> askPrefixColor = [SetStyles(Style.foreground(Color.yellow))];
38+
39+
@override
40+
List<Sequence> validatorColorMessage = [
41+
SetStyles(Style.foreground(Color.brightRed))
42+
];
43+
44+
@override
45+
List<Sequence> defaultValueColorMessage = [
46+
SetStyles(Style.foreground(Color.brightBlack))
47+
];
48+
49+
@override
50+
List<Sequence> inputColor = [SetStyles(Style.foreground(Color.brightBlack))];
51+
52+
DefaultNumberTheme();
53+
54+
/// Creates a new [AskTheme] with the provided values based on [DefaultAskTheme].
55+
factory DefaultNumberTheme.copyWith(
56+
{String? askPrefix,
57+
String? errorSuffix,
58+
String? successSuffix,
59+
String Function(String? value)? defaultValueFormatter,
60+
String? Function(String? value)? inputFormatter,
61+
List<Sequence>? successPrefixColor,
62+
List<Sequence>? errorPrefixColor,
63+
List<Sequence>? validatorColorMessage,
64+
List<Sequence>? askPrefixColor,
65+
List<Sequence>? defaultValueColorMessage,
66+
List<Sequence>? inputColor}) {
67+
final theme = DefaultNumberTheme();
68+
69+
theme.askPrefix = askPrefix ?? theme.askPrefix;
70+
theme.errorSuffix = errorSuffix ?? theme.errorSuffix;
71+
theme.successSuffix = successSuffix ?? theme.successSuffix;
72+
theme.defaultValueFormatter =
73+
defaultValueFormatter ?? theme.defaultValueFormatter;
74+
theme.inputFormatter = inputFormatter ?? theme.inputFormatter;
75+
theme.successPrefixColor = successPrefixColor ?? theme.successPrefixColor;
76+
theme.errorPrefixColor = errorPrefixColor ?? theme.errorPrefixColor;
77+
theme.validatorColorMessage =
78+
validatorColorMessage ?? theme.validatorColorMessage;
79+
theme.askPrefixColor = askPrefixColor ?? theme.askPrefixColor;
80+
theme.defaultValueColorMessage =
81+
defaultValueColorMessage ?? theme.defaultValueColorMessage;
82+
theme.inputColor = inputColor ?? theme.inputColor;
83+
84+
return theme;
85+
}
86+
}

0 commit comments

Comments
 (0)