feat: colorize help output

This commit is contained in:
2023-11-30 01:40:48 +02:00
parent 76ffe85b2d
commit 1452b80536
6 changed files with 120 additions and 53 deletions

View File

@@ -1,3 +1,7 @@
## 0.4.0
- Colorized help output
## 0.3.2
- Improve I/O pass-through to commands

View File

@@ -9,7 +9,7 @@ import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart' as yaml;
import 'runnable_script.dart';
import 'utils.dart' as utils;
import 'utils.dart';
/// The configuration for a script runner. See each field's documentation for more information.
class ScriptRunnerConfig {
@@ -76,8 +76,7 @@ class ScriptRunnerConfig {
final sourceMap = await _tryFindConfig(fs, startDir);
if (sourceMap.isEmpty) {
throw StateError(
'Must provide scripts in either pubspec.yaml or script_runner.yaml');
throw StateError('Must provide scripts in either pubspec.yaml or script_runner.yaml');
}
final source = sourceMap.values.first;
@@ -98,8 +97,7 @@ class ScriptRunnerConfig {
);
}
static Future<yaml.YamlMap?> _getPubspecConfig(
FileSystem fileSystem, String folderPath) async {
static Future<yaml.YamlMap?> _getPubspecConfig(FileSystem fileSystem, String folderPath) async {
final filePath = path.join(folderPath, 'pubspec.yaml');
final file = fileSystem.file(filePath);
if (!file.existsSync()) {
@@ -116,8 +114,7 @@ class ScriptRunnerConfig {
}
}
static Future<yaml.YamlMap?>? _getCustomConfig(
FileSystem fileSystem, String folderPath) async {
static Future<yaml.YamlMap?>? _getCustomConfig(FileSystem fileSystem, String folderPath) async {
final filePath = path.join(folderPath, 'script_runner.yaml');
final file = fileSystem.file(filePath);
if (!file.existsSync()) {
@@ -137,36 +134,66 @@ class ScriptRunnerConfig {
yaml.YamlList scriptsRaw, {
FileSystem? fileSystem,
}) {
final scripts = scriptsRaw
.map((script) =>
RunnableScript.fromYamlMap(script, fileSystem: fileSystem))
.toList();
final scripts = scriptsRaw.map((script) => RunnableScript.fromYamlMap(script, fileSystem: fileSystem)).toList();
return scripts.map((s) => s..preloadScripts = scripts).toList();
}
/// Prints usage help text for this config
void printUsage() {
print('Dart Script Runner');
print(' Usage: scr script_name ...args');
print('');
var maxLen = 0;
print(
[
colorize('Usage:', [TerminalColor.bold]),
colorize('scr', [TerminalColor.yellow]),
colorize('<script_name>', [TerminalColor.brightWhite]),
colorize('[...args]', [TerminalColor.gray]),
].join(' '),
);
print(
[
' ' * 'Usage:'.length,
colorize('scr', [TerminalColor.yellow]),
colorize('-h', [TerminalColor.brightWhite]),
].join(' '),
);
print('');
final titleStyle = [TerminalColor.bold, TerminalColor.brightWhite];
printColor('Built-in flags:', titleStyle);
print('');
var maxLen = '-h, --help'.length;
for (final scr in scripts) {
maxLen = math.max(maxLen, scr.name.length);
}
final padLen = maxLen + 6;
print(' ${'-h, --help'.padRight(padLen, ' ')} Print this help message\n');
print(' ${colorize('-h, --help'.padRight(padLen, ' '), [
TerminalColor.yellow
])} ${colorize('Print this help message', [TerminalColor.gray])}');
print('');
print(
'Available scripts'
'${configSource?.isNotEmpty == true ? ' on $configSource:' : ':'}',
[
colorize('Available scripts', [
TerminalColor.bold,
TerminalColor.brightWhite,
]),
(configSource?.isNotEmpty == true
? [
colorize(' on ', titleStyle),
colorize(configSource!, [...titleStyle, TerminalColor.underline]),
colorize(':', titleStyle)
].join('')
: ':'),
].join(''),
);
print('');
for (final scr in scripts) {
final lines = utils.chunks(
final lines = chunks(
scr.description ?? '\$ ${[scr.cmd, ...scr.args].join(' ')}',
80 - padLen,
lineLength - padLen,
stripColors: true,
wrapLine: (line) => colorize(line, [TerminalColor.gray]),
);
print(' ${scr.name.padRight(padLen, ' ')} ${lines.first}');
printColor(' ${scr.name.padRight(padLen, ' ')} ${lines.first}', [TerminalColor.yellow]);
for (final line in lines.sublist(1)) {
print(' ${''.padRight(padLen, ' ')} $line');
}
@@ -174,8 +201,7 @@ class ScriptRunnerConfig {
}
}
static Future<Map<String, yaml.YamlMap>> _tryFindConfig(
FileSystem fs, String startDir) async {
static Future<Map<String, yaml.YamlMap>> _tryFindConfig(FileSystem fs, String startDir) async {
var dir = fs.directory(startDir);
String sourceFile;
yaml.YamlMap? source;
@@ -291,7 +317,7 @@ class ScriptRunnerShellConfig {
case OS.linux:
case OS.macos:
try {
final envShell = utils.firstNonNull([
final envShell = firstNonNull([
Platform.environment['SHELL'],
Platform.environment['TERM'],
]);
@@ -309,4 +335,3 @@ enum OS {
linux,
// other
}

View File

@@ -65,8 +65,7 @@ class RunnableScript {
}) : _fileSystem = fileSystem ?? LocalFileSystem();
/// Generate a runnable script from a yaml loaded map as defined in the config.
factory RunnableScript.fromYamlMap(yaml.YamlMap map,
{FileSystem? fileSystem}) {
factory RunnableScript.fromYamlMap(yaml.YamlMap map, {FileSystem? fileSystem}) {
final out = <String, dynamic>{};
if (map['name'] == null && map.keys.length == 1) {
@@ -74,15 +73,13 @@ class RunnableScript {
out['cmd'] = map.values.first;
} else {
out.addAll(map.cast<String, dynamic>());
out['args'] =
(map['args'] as yaml.YamlList?)?.map((e) => e.toString()).toList();
out['args'] = (map['args'] as yaml.YamlList?)?.map((e) => e.toString()).toList();
out['env'] = (map['env'] as yaml.YamlMap?)?.cast<String, String>();
}
try {
return RunnableScript.fromMap(out, fileSystem: fileSystem);
} catch (e) {
throw StateError(
'Failed to parse script, arguments: $map, $fileSystem. Error: $e');
throw StateError('Failed to parse script, arguments: $map, $fileSystem. Error: $e');
}
}
@@ -112,8 +109,7 @@ class RunnableScript {
appendNewline: appendNewline,
);
} catch (e) {
throw StateError(
'Failed to parse script, arguments: $map, $fileSystem. Error: $e');
throw StateError('Failed to parse script, arguments: $map, $fileSystem. Error: $e');
}
}
@@ -132,7 +128,7 @@ class RunnableScript {
if (result.exitCode != 0) throw Exception(result.stderr);
}
final origCmd = [cmd, ...effectiveArgs.map(_utils.wrap)].join(' ');
final origCmd = [cmd, ...effectiveArgs.map(_utils.quoteWrap)].join(' ');
if (!suppressHeaderOutput) {
print('\$ $origCmd');
@@ -173,14 +169,13 @@ class RunnableScript {
return exitCode;
}
String _getScriptPath() => _fileSystem.path
.join(_fileSystem.systemTempDirectory.path, 'script_runner_$name.sh');
String _getScriptPath() => _fileSystem.path.join(_fileSystem.systemTempDirectory.path, 'script_runner_$name.sh');
String _getScriptContents(
ScriptRunnerConfig config, {
List<String> extraArgs = const [],
}) {
final script = "$cmd ${(args + extraArgs).map(_utils.wrap).join(' ')}";
final script = "$cmd ${(args + extraArgs).map(_utils.quoteWrap).join(' ')}";
switch (config.shell.os) {
case OS.windows:
return [
@@ -190,12 +185,8 @@ class RunnableScript {
].join('\n');
case OS.linux:
case OS.macos:
return [
...preloadScripts.map((e) =>
"[[ ! \$(which ${e.name}) ]] && alias ${e.name}='scr ${e.name}'"),
script
].join('\n');
return [...preloadScripts.map((e) => "[[ ! \$(which ${e.name}) ]] && alias ${e.name}='scr ${e.name}'"), script]
.join('\n');
}
}
}

View File

@@ -42,26 +42,39 @@ List<String> splitArgs(String string) {
return out.where((e) => e.isNotEmpty).toList();
}
String stripColor(String str) {
return str.replaceAll(RegExp(r'\x1B\[\d+m'), '');
}
T noop<T>(T arg) => arg;
/// Split string into chunks of [maxLen] characters.
// @internal
List<String> chunks(String str, int maxLen) {
List<String> chunks(
String str,
int maxLen, {
bool stripColors = false,
String Function(String) wrapLine = noop,
}) {
final words = str.split(' ');
final chunks = <String>[];
var chunk = '';
for (final word in words) {
if (chunk.length + word.length > maxLen) {
chunks.add(chunk);
final chunkLength = stripColors ? stripColor(chunk).length : chunk.length;
final wordLength = stripColors ? stripColor(word).length : word.length;
if (chunkLength + wordLength > maxLen) {
chunks.add(wrapLine(chunk));
chunk = '';
}
chunk += '$word ';
}
chunks.add(chunk);
chunks.add(wrapLine(chunk));
return chunks;
}
/// wrap args with quotes if necessary
// @internal
String wrap(String arg) {
String quoteWrap(String arg) {
if (arg.contains(' ')) {
return '"$arg"';
}
@@ -78,3 +91,39 @@ T? firstNonNull<T>(Iterable<T?> list) {
}
return null;
}
String colorize(String text, [Iterable<TerminalColor> colors = const []]) {
for (final color in colors) {
text = '\x1B[${color.index}m$text';
}
return '$text\x1B[0m';
}
void printColor(String text, [Iterable<TerminalColor> colors = const []]) {
print(colorize(text, colors));
}
class TerminalColor {
const TerminalColor._(this.index);
final int index;
static const TerminalColor none = TerminalColor._(-1);
static const TerminalColor red = TerminalColor._(31);
static const TerminalColor green = TerminalColor._(32);
static const TerminalColor yellow = TerminalColor._(33);
static const TerminalColor blue = TerminalColor._(34);
static const TerminalColor magenta = TerminalColor._(35);
static const TerminalColor cyan = TerminalColor._(36);
static const TerminalColor white = TerminalColor._(37);
static const TerminalColor gray = TerminalColor._(90);
static const TerminalColor brightRed = TerminalColor._(91);
static const TerminalColor brightGreen = TerminalColor._(92);
static const TerminalColor brightYellow = TerminalColor._(93);
static const TerminalColor brightBlue = TerminalColor._(94);
static const TerminalColor brightMagenta = TerminalColor._(95);
static const TerminalColor brightCyan = TerminalColor._(96);
static const TerminalColor brightWhite = TerminalColor._(97);
static const TerminalColor bold = TerminalColor._(1);
static const TerminalColor underline = TerminalColor._(4);
}

View File

@@ -1,6 +1,6 @@
name: script_runner
description: Run all your project-related scripts in a portable, simple config.
version: 0.3.2
version: 0.3.3
homepage: https://casraf.dev/
repository: https://github.com/chenasraf/dart_script_runner
license: MIT
@@ -17,13 +17,12 @@ dev_dependencies:
test:
btool:
script_runner:
# line_length: 100
scripts:
# Real
- auto-fix: dart fix --apply
- publish: dart pub publish --force
- publish: dart format .; dart pub publish; format
- publish:dry: dart pub publish --dry-run
- doc: dart doc
- name: version
@@ -32,7 +31,7 @@ script_runner:
- name: 'version:set'
cmd: dart run btool set packageVersion
suppress_header_output: true
- format: dart format .
- format: dart format --line-length 120 .
# Examples
- name: echo1

View File

@@ -144,8 +144,7 @@ void main() {
}
Future<void> _writeCustomConf(FileSystem fs, [String? contents]) async {
final pubFile =
fs.file(path.join(fs.currentDirectory.path, 'script_runner.yaml'));
final pubFile = fs.file(path.join(fs.currentDirectory.path, 'script_runner.yaml'));
pubFile.create(recursive: true);
await pubFile.writeAsString(
contents ??