diff --git a/CHANGELOG.md b/CHANGELOG.md index d6bf22e..99df34e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0 + +- Colorized help output + ## 0.3.2 - Improve I/O pass-through to commands diff --git a/lib/src/config.dart b/lib/src/config.dart index 1a21846..15e5c9a 100644 --- a/lib/src/config.dart +++ b/lib/src/config.dart @@ -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 _getPubspecConfig( - FileSystem fileSystem, String folderPath) async { + static Future _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? _getCustomConfig( - FileSystem fileSystem, String folderPath) async { + static Future? _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('', [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> _tryFindConfig( - FileSystem fs, String startDir) async { + static Future> _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 } - diff --git a/lib/src/runnable_script.dart b/lib/src/runnable_script.dart index c7f0414..d230af3 100644 --- a/lib/src/runnable_script.dart +++ b/lib/src/runnable_script.dart @@ -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 = {}; if (map['name'] == null && map.keys.length == 1) { @@ -74,15 +73,13 @@ class RunnableScript { out['cmd'] = map.values.first; } else { out.addAll(map.cast()); - 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(); } 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 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'); } } } - diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 2504c26..ff71460 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -42,26 +42,39 @@ List 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 arg) => arg; + /// Split string into chunks of [maxLen] characters. // @internal -List chunks(String str, int maxLen) { +List chunks( + String str, + int maxLen, { + bool stripColors = false, + String Function(String) wrapLine = noop, +}) { final words = str.split(' '); final chunks = []; 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(Iterable list) { } return null; } + +String colorize(String text, [Iterable colors = const []]) { + for (final color in colors) { + text = '\x1B[${color.index}m$text'; + } + return '$text\x1B[0m'; +} + +void printColor(String text, [Iterable 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); +} diff --git a/pubspec.yaml b/pubspec.yaml index 8635c64..3f6ea52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/config_test.dart b/test/config_test.dart index 8d58555..2a62c01 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -144,8 +144,7 @@ void main() { } Future _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 ??