Skip to content

Commit 1452b80

Browse files
committed
feat: colorize help output
1 parent 76ffe85 commit 1452b80

File tree

6 files changed

+120
-53
lines changed

6 files changed

+120
-53
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.4.0
2+
3+
- Colorized help output
4+
15
## 0.3.2
26

37
- Improve I/O pass-through to commands

lib/src/config.dart

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'package:path/path.dart' as path;
99
import 'package:yaml/yaml.dart' as yaml;
1010

1111
import 'runnable_script.dart';
12-
import 'utils.dart' as utils;
12+
import 'utils.dart';
1313

1414
/// The configuration for a script runner. See each field's documentation for more information.
1515
class ScriptRunnerConfig {
@@ -76,8 +76,7 @@ class ScriptRunnerConfig {
7676
final sourceMap = await _tryFindConfig(fs, startDir);
7777

7878
if (sourceMap.isEmpty) {
79-
throw StateError(
80-
'Must provide scripts in either pubspec.yaml or script_runner.yaml');
79+
throw StateError('Must provide scripts in either pubspec.yaml or script_runner.yaml');
8180
}
8281

8382
final source = sourceMap.values.first;
@@ -98,8 +97,7 @@ class ScriptRunnerConfig {
9897
);
9998
}
10099

101-
static Future<yaml.YamlMap?> _getPubspecConfig(
102-
FileSystem fileSystem, String folderPath) async {
100+
static Future<yaml.YamlMap?> _getPubspecConfig(FileSystem fileSystem, String folderPath) async {
103101
final filePath = path.join(folderPath, 'pubspec.yaml');
104102
final file = fileSystem.file(filePath);
105103
if (!file.existsSync()) {
@@ -116,8 +114,7 @@ class ScriptRunnerConfig {
116114
}
117115
}
118116

119-
static Future<yaml.YamlMap?>? _getCustomConfig(
120-
FileSystem fileSystem, String folderPath) async {
117+
static Future<yaml.YamlMap?>? _getCustomConfig(FileSystem fileSystem, String folderPath) async {
121118
final filePath = path.join(folderPath, 'script_runner.yaml');
122119
final file = fileSystem.file(filePath);
123120
if (!file.existsSync()) {
@@ -137,45 +134,74 @@ class ScriptRunnerConfig {
137134
yaml.YamlList scriptsRaw, {
138135
FileSystem? fileSystem,
139136
}) {
140-
final scripts = scriptsRaw
141-
.map((script) =>
142-
RunnableScript.fromYamlMap(script, fileSystem: fileSystem))
143-
.toList();
137+
final scripts = scriptsRaw.map((script) => RunnableScript.fromYamlMap(script, fileSystem: fileSystem)).toList();
144138
return scripts.map((s) => s..preloadScripts = scripts).toList();
145139
}
146140

147141
/// Prints usage help text for this config
148142
void printUsage() {
149-
print('Dart Script Runner');
150-
print(' Usage: scr script_name ...args');
151143
print('');
152-
var maxLen = 0;
144+
print(
145+
[
146+
colorize('Usage:', [TerminalColor.bold]),
147+
colorize('scr', [TerminalColor.yellow]),
148+
colorize('<script_name>', [TerminalColor.brightWhite]),
149+
colorize('[...args]', [TerminalColor.gray]),
150+
].join(' '),
151+
);
152+
print(
153+
[
154+
' ' * 'Usage:'.length,
155+
colorize('scr', [TerminalColor.yellow]),
156+
colorize('-h', [TerminalColor.brightWhite]),
157+
].join(' '),
158+
);
159+
print('');
160+
final titleStyle = [TerminalColor.bold, TerminalColor.brightWhite];
161+
printColor('Built-in flags:', titleStyle);
162+
print('');
163+
var maxLen = '-h, --help'.length;
153164
for (final scr in scripts) {
154165
maxLen = math.max(maxLen, scr.name.length);
155166
}
156167
final padLen = maxLen + 6;
157-
print(' ${'-h, --help'.padRight(padLen, ' ')} Print this help message\n');
168+
print(' ${colorize('-h, --help'.padRight(padLen, ' '), [
169+
TerminalColor.yellow
170+
])} ${colorize('Print this help message', [TerminalColor.gray])}');
158171
print('');
172+
159173
print(
160-
'Available scripts'
161-
'${configSource?.isNotEmpty == true ? ' on $configSource:' : ':'}',
174+
[
175+
colorize('Available scripts', [
176+
TerminalColor.bold,
177+
TerminalColor.brightWhite,
178+
]),
179+
(configSource?.isNotEmpty == true
180+
? [
181+
colorize(' on ', titleStyle),
182+
colorize(configSource!, [...titleStyle, TerminalColor.underline]),
183+
colorize(':', titleStyle)
184+
].join('')
185+
: ':'),
186+
].join(''),
162187
);
163188
print('');
164189
for (final scr in scripts) {
165-
final lines = utils.chunks(
190+
final lines = chunks(
166191
scr.description ?? '\$ ${[scr.cmd, ...scr.args].join(' ')}',
167-
80 - padLen,
192+
lineLength - padLen,
193+
stripColors: true,
194+
wrapLine: (line) => colorize(line, [TerminalColor.gray]),
168195
);
169-
print(' ${scr.name.padRight(padLen, ' ')} ${lines.first}');
196+
printColor(' ${scr.name.padRight(padLen, ' ')} ${lines.first}', [TerminalColor.yellow]);
170197
for (final line in lines.sublist(1)) {
171198
print(' ${''.padRight(padLen, ' ')} $line');
172199
}
173200
print('');
174201
}
175202
}
176203

177-
static Future<Map<String, yaml.YamlMap>> _tryFindConfig(
178-
FileSystem fs, String startDir) async {
204+
static Future<Map<String, yaml.YamlMap>> _tryFindConfig(FileSystem fs, String startDir) async {
179205
var dir = fs.directory(startDir);
180206
String sourceFile;
181207
yaml.YamlMap? source;
@@ -291,7 +317,7 @@ class ScriptRunnerShellConfig {
291317
case OS.linux:
292318
case OS.macos:
293319
try {
294-
final envShell = utils.firstNonNull([
320+
final envShell = firstNonNull([
295321
Platform.environment['SHELL'],
296322
Platform.environment['TERM'],
297323
]);
@@ -309,4 +335,3 @@ enum OS {
309335
linux,
310336
// other
311337
}
312-

lib/src/runnable_script.dart

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -65,24 +65,21 @@ class RunnableScript {
6565
}) : _fileSystem = fileSystem ?? LocalFileSystem();
6666

6767
/// Generate a runnable script from a yaml loaded map as defined in the config.
68-
factory RunnableScript.fromYamlMap(yaml.YamlMap map,
69-
{FileSystem? fileSystem}) {
68+
factory RunnableScript.fromYamlMap(yaml.YamlMap map, {FileSystem? fileSystem}) {
7069
final out = <String, dynamic>{};
7170

7271
if (map['name'] == null && map.keys.length == 1) {
7372
out['name'] = map.keys.first;
7473
out['cmd'] = map.values.first;
7574
} else {
7675
out.addAll(map.cast<String, dynamic>());
77-
out['args'] =
78-
(map['args'] as yaml.YamlList?)?.map((e) => e.toString()).toList();
76+
out['args'] = (map['args'] as yaml.YamlList?)?.map((e) => e.toString()).toList();
7977
out['env'] = (map['env'] as yaml.YamlMap?)?.cast<String, String>();
8078
}
8179
try {
8280
return RunnableScript.fromMap(out, fileSystem: fileSystem);
8381
} catch (e) {
84-
throw StateError(
85-
'Failed to parse script, arguments: $map, $fileSystem. Error: $e');
82+
throw StateError('Failed to parse script, arguments: $map, $fileSystem. Error: $e');
8683
}
8784
}
8885

@@ -112,8 +109,7 @@ class RunnableScript {
112109
appendNewline: appendNewline,
113110
);
114111
} catch (e) {
115-
throw StateError(
116-
'Failed to parse script, arguments: $map, $fileSystem. Error: $e');
112+
throw StateError('Failed to parse script, arguments: $map, $fileSystem. Error: $e');
117113
}
118114
}
119115

@@ -132,7 +128,7 @@ class RunnableScript {
132128
if (result.exitCode != 0) throw Exception(result.stderr);
133129
}
134130

135-
final origCmd = [cmd, ...effectiveArgs.map(_utils.wrap)].join(' ');
131+
final origCmd = [cmd, ...effectiveArgs.map(_utils.quoteWrap)].join(' ');
136132

137133
if (!suppressHeaderOutput) {
138134
print('\$ $origCmd');
@@ -173,14 +169,13 @@ class RunnableScript {
173169
return exitCode;
174170
}
175171

176-
String _getScriptPath() => _fileSystem.path
177-
.join(_fileSystem.systemTempDirectory.path, 'script_runner_$name.sh');
172+
String _getScriptPath() => _fileSystem.path.join(_fileSystem.systemTempDirectory.path, 'script_runner_$name.sh');
178173

179174
String _getScriptContents(
180175
ScriptRunnerConfig config, {
181176
List<String> extraArgs = const [],
182177
}) {
183-
final script = "$cmd ${(args + extraArgs).map(_utils.wrap).join(' ')}";
178+
final script = "$cmd ${(args + extraArgs).map(_utils.quoteWrap).join(' ')}";
184179
switch (config.shell.os) {
185180
case OS.windows:
186181
return [
@@ -190,12 +185,8 @@ class RunnableScript {
190185
].join('\n');
191186
case OS.linux:
192187
case OS.macos:
193-
return [
194-
...preloadScripts.map((e) =>
195-
"[[ ! \$(which ${e.name}) ]] && alias ${e.name}='scr ${e.name}'"),
196-
script
197-
].join('\n');
188+
return [...preloadScripts.map((e) => "[[ ! \$(which ${e.name}) ]] && alias ${e.name}='scr ${e.name}'"), script]
189+
.join('\n');
198190
}
199191
}
200192
}
201-

lib/src/utils.dart

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,39 @@ List<String> splitArgs(String string) {
4242
return out.where((e) => e.isNotEmpty).toList();
4343
}
4444

45+
String stripColor(String str) {
46+
return str.replaceAll(RegExp(r'\x1B\[\d+m'), '');
47+
}
48+
49+
T noop<T>(T arg) => arg;
50+
4551
/// Split string into chunks of [maxLen] characters.
4652
// @internal
47-
List<String> chunks(String str, int maxLen) {
53+
List<String> chunks(
54+
String str,
55+
int maxLen, {
56+
bool stripColors = false,
57+
String Function(String) wrapLine = noop,
58+
}) {
4859
final words = str.split(' ');
4960
final chunks = <String>[];
5061
var chunk = '';
5162
for (final word in words) {
52-
if (chunk.length + word.length > maxLen) {
53-
chunks.add(chunk);
63+
final chunkLength = stripColors ? stripColor(chunk).length : chunk.length;
64+
final wordLength = stripColors ? stripColor(word).length : word.length;
65+
if (chunkLength + wordLength > maxLen) {
66+
chunks.add(wrapLine(chunk));
5467
chunk = '';
5568
}
5669
chunk += '$word ';
5770
}
58-
chunks.add(chunk);
71+
chunks.add(wrapLine(chunk));
5972
return chunks;
6073
}
6174

6275
/// wrap args with quotes if necessary
6376
// @internal
64-
String wrap(String arg) {
77+
String quoteWrap(String arg) {
6578
if (arg.contains(' ')) {
6679
return '"$arg"';
6780
}
@@ -78,3 +91,39 @@ T? firstNonNull<T>(Iterable<T?> list) {
7891
}
7992
return null;
8093
}
94+
95+
String colorize(String text, [Iterable<TerminalColor> colors = const []]) {
96+
for (final color in colors) {
97+
text = '\x1B[${color.index}m$text';
98+
}
99+
return '$text\x1B[0m';
100+
}
101+
102+
void printColor(String text, [Iterable<TerminalColor> colors = const []]) {
103+
print(colorize(text, colors));
104+
}
105+
106+
class TerminalColor {
107+
const TerminalColor._(this.index);
108+
final int index;
109+
110+
static const TerminalColor none = TerminalColor._(-1);
111+
static const TerminalColor red = TerminalColor._(31);
112+
static const TerminalColor green = TerminalColor._(32);
113+
static const TerminalColor yellow = TerminalColor._(33);
114+
static const TerminalColor blue = TerminalColor._(34);
115+
static const TerminalColor magenta = TerminalColor._(35);
116+
static const TerminalColor cyan = TerminalColor._(36);
117+
static const TerminalColor white = TerminalColor._(37);
118+
static const TerminalColor gray = TerminalColor._(90);
119+
static const TerminalColor brightRed = TerminalColor._(91);
120+
static const TerminalColor brightGreen = TerminalColor._(92);
121+
static const TerminalColor brightYellow = TerminalColor._(93);
122+
static const TerminalColor brightBlue = TerminalColor._(94);
123+
static const TerminalColor brightMagenta = TerminalColor._(95);
124+
static const TerminalColor brightCyan = TerminalColor._(96);
125+
static const TerminalColor brightWhite = TerminalColor._(97);
126+
127+
static const TerminalColor bold = TerminalColor._(1);
128+
static const TerminalColor underline = TerminalColor._(4);
129+
}

pubspec.yaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: script_runner
22
description: Run all your project-related scripts in a portable, simple config.
3-
version: 0.3.2
3+
version: 0.3.3
44
homepage: https://casraf.dev/
55
repository: https://github.com/chenasraf/dart_script_runner
66
license: MIT
@@ -17,13 +17,12 @@ dev_dependencies:
1717
test:
1818
btool:
1919

20-
2120
script_runner:
2221
# line_length: 100
2322
scripts:
2423
# Real
2524
- auto-fix: dart fix --apply
26-
- publish: dart pub publish --force
25+
- publish: dart format .; dart pub publish; format
2726
- publish:dry: dart pub publish --dry-run
2827
- doc: dart doc
2928
- name: version
@@ -32,7 +31,7 @@ script_runner:
3231
- name: 'version:set'
3332
cmd: dart run btool set packageVersion
3433
suppress_header_output: true
35-
- format: dart format .
34+
- format: dart format --line-length 120 .
3635

3736
# Examples
3837
- name: echo1

test/config_test.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,7 @@ void main() {
144144
}
145145

146146
Future<void> _writeCustomConf(FileSystem fs, [String? contents]) async {
147-
final pubFile =
148-
fs.file(path.join(fs.currentDirectory.path, 'script_runner.yaml'));
147+
final pubFile = fs.file(path.join(fs.currentDirectory.path, 'script_runner.yaml'));
149148
pubFile.create(recursive: true);
150149
await pubFile.writeAsString(
151150
contents ??

0 commit comments

Comments
 (0)