dry-cli-help 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +248 -0
- data/Rakefile +38 -0
- data/SPECIFICATION.md +200 -0
- data/lib/dry/cli/help/colors.rb +71 -0
- data/lib/dry/cli/help/configuration.rb +241 -0
- data/lib/dry/cli/help/formatter.rb +107 -0
- data/lib/dry/cli/help/integration.rb +84 -0
- data/lib/dry/cli/help/screens/base.rb +87 -0
- data/lib/dry/cli/help/screens/command.rb +126 -0
- data/lib/dry/cli/help/screens/listing.rb +100 -0
- data/lib/dry/cli/help/terminal.rb +39 -0
- data/lib/dry/cli/help/text.rb +66 -0
- data/lib/dry/cli/help/version.rb +15 -0
- data/lib/dry/cli/help.rb +60 -0
- data/lib/dry-cli-help.rb +3 -0
- data/sig/dry/cli/help.rbs +88 -0
- metadata +99 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8756ab593e09c6cdc0e0ab32fe6f3909d05625734a07e7d7c334e3d832ba70ac
|
|
4
|
+
data.tar.gz: 4477df389d4c4c0d984fe4024f768adf3e254772464694f7acd81609936060a4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6a61174289b02e6a523ba5ccdae1a79cd72241ea7c5bb16de5d543e3aad488a3de9d973644a0297c2c94e0516475241c490470f5d52208f450d7efc243dfd575
|
|
7
|
+
data.tar.gz: 041553f6adda56086400f208d6bef2ce77613e635581e871ffc58488a3ead1cca4038c3ce4d89f39e79049f529ffaa7cb013dea09bea8701e9c7b5c78ea1d3eb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-09-12
|
|
4
|
+
|
|
5
|
+
- Initial release as `dry-cli-help`, converted from `dry-cli-autocomplete`. The shell completion generator is removed; that gem remains the place for it.
|
|
6
|
+
- Help screens with a title, description and epilogue, uppercase headings, aligned and wrapped descriptions, and ANSI colors through `pastel`.
|
|
7
|
+
- Settings through `Dry::CLI::Help.configure` for the whole process and a `help` block on any registry: `title`, `description`, `epilogue`, `color`, `wrap`, `width`, `margin`, `exit_code_without_arguments`, `banner_on_subcommands`, `heading_case`, `command_order`, `heading`, `style`, `group`, `sections` and `hide`.
|
|
8
|
+
- `-h` and `--help` at a registry level exit 0. Running with no command keeps dry-cli's exit status 1 unless configured otherwise.
|
|
9
|
+
- `Dry::CLI::Help::Colors`, a module exposing every foreground, background and text style as a method.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Konstantin Gredeskoul
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# dry-cli-help
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kigster/dry-cli-help/actions/workflows/main.yml) 
|
|
4
|
+
|
|
5
|
+
Configurable, wrapped, colored help screens for [dry-cli](https://github.com/dry-rb/dry-cli) applications.
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> The design, the settings and every decision behind them are in [SPECIFICATION.md](SPECIFICATION.md).
|
|
9
|
+
|
|
10
|
+
dry-cli prints help as it finds it: no title, no description of the program, no color, one line per description however long, and commands sorted alphabetically. This gem keeps the command structure you already declared and changes only what the user reads before a command runs. Progress bars, spinners and error panels belong in `dry-cli-ui`.
|
|
11
|
+
|
|
12
|
+
## Before and after
|
|
13
|
+
|
|
14
|
+
`taxlibris compile -h` with dry-cli alone:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
Command:
|
|
18
|
+
taxlibris compile
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
taxlibris compile RULES [OUTPUT]
|
|
22
|
+
|
|
23
|
+
Description:
|
|
24
|
+
Compile tax rules
|
|
25
|
+
|
|
26
|
+
Arguments:
|
|
27
|
+
RULES # REQUIRED Rule file to compile
|
|
28
|
+
OUTPUT # Where to write the compiled rules
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--format=VALUE, -f VALUE # Output format: (json/yaml), default: "json"
|
|
32
|
+
--[no-]strict # Treat warnings as errors
|
|
33
|
+
--help, -h # Print this help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
With `require "dry/cli/help"`:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
USAGE
|
|
40
|
+
taxlibris compile RULES [OUTPUT] [OPTIONS]
|
|
41
|
+
|
|
42
|
+
DESCRIPTION
|
|
43
|
+
Compile tax rules
|
|
44
|
+
|
|
45
|
+
ARGUMENTS
|
|
46
|
+
RULES Rule file to compile (required)
|
|
47
|
+
OUTPUT Where to write the compiled rules
|
|
48
|
+
|
|
49
|
+
OPTIONS
|
|
50
|
+
-f, --format=VALUE Output format (one of: json, yaml; default: "json")
|
|
51
|
+
--[no-]strict Treat warnings as errors
|
|
52
|
+
-h, --help Show help
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Headings are bold and yellow, commands green, options and arguments cyan, and every description wraps to the terminal with a hanging indent.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
gem install dry-cli-help
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or add `gem "dry-cli-help"` to your `Gemfile`.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
Require it after dry-cli. That alone changes every help screen in the process.
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
require "dry/cli"
|
|
71
|
+
require "dry/cli/help"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Describe the program in the registry:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
module Taxlibris
|
|
78
|
+
module CLI
|
|
79
|
+
extend Dry::CLI::Registry
|
|
80
|
+
|
|
81
|
+
help do
|
|
82
|
+
title "Taxlibris"
|
|
83
|
+
|
|
84
|
+
description <<~TEXT
|
|
85
|
+
Compile, validate, and evaluate tax rules.
|
|
86
|
+
TEXT
|
|
87
|
+
|
|
88
|
+
epilogue "Documentation: https://example.com/taxlibris"
|
|
89
|
+
|
|
90
|
+
color :auto
|
|
91
|
+
width :terminal
|
|
92
|
+
wrap true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
register "compile", Compile
|
|
96
|
+
register "validate", Validate
|
|
97
|
+
register "evaluate", Evaluate
|
|
98
|
+
register "version", Version, aliases: ["--version", "-v"]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`taxlibris -h` then prints:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
Taxlibris
|
|
107
|
+
|
|
108
|
+
Compile, validate, and evaluate tax rules.
|
|
109
|
+
|
|
110
|
+
USAGE
|
|
111
|
+
taxlibris COMMAND [OPTIONS]
|
|
112
|
+
|
|
113
|
+
COMMANDS
|
|
114
|
+
compile Compile tax rules
|
|
115
|
+
validate Validate the rule corpus
|
|
116
|
+
evaluate Evaluate a tax return
|
|
117
|
+
version Show version
|
|
118
|
+
|
|
119
|
+
OPTIONS
|
|
120
|
+
-h, --help Show help
|
|
121
|
+
-v, --version Show version
|
|
122
|
+
|
|
123
|
+
Documentation: https://example.com/taxlibris
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
A command reachable as `--version` lists under Options by its dashed names.
|
|
127
|
+
|
|
128
|
+
Settings for the whole process go through `configure`. A registry's `help` block overrides them:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
Dry::CLI::Help.configure do |config|
|
|
132
|
+
config.width = 100
|
|
133
|
+
config.color = false
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A `help` block that takes an argument receives the configuration instead, so `help { |h| h.title = "Taxlibris" }` works too.
|
|
138
|
+
|
|
139
|
+
## Settings
|
|
140
|
+
|
|
141
|
+
| Setting | Values | Default |
|
|
142
|
+
| ----------------------------- | --------------------------------- | --------------- |
|
|
143
|
+
| `title` | String | none |
|
|
144
|
+
| `description` | String | none |
|
|
145
|
+
| `epilogue` | String | none |
|
|
146
|
+
| `color` | `true`, `false`, `:auto` | `:auto` |
|
|
147
|
+
| `wrap` | `true`, `false` | `true` |
|
|
148
|
+
| `width` | `:terminal`, Integer | `:terminal` |
|
|
149
|
+
| `margin` | Integer | `0` |
|
|
150
|
+
| `exit_code_without_arguments` | 0 to 255 | `1` |
|
|
151
|
+
| `banner_on_subcommands` | `true`, `false` | `false` |
|
|
152
|
+
| `heading_case` | `:upcase`, `:capitalize`, `:none` | `:upcase` |
|
|
153
|
+
| `command_order` | `:registration`, `:alphabetical` | `:registration` |
|
|
154
|
+
|
|
155
|
+
`color :auto` colors a terminal and honors [`NO_COLOR`](https://no-color.org). `width :terminal` reads `COLUMNS`, then the console, then falls back to 80, and `margin` keeps columns free at the right edge.
|
|
156
|
+
|
|
157
|
+
Running the program with no command prints the top-level help and exits 1, as dry-cli does. `exit_code_without_arguments 0` prints it to stdout and exits 0 instead. `-h` and `--help` always exit 0.
|
|
158
|
+
|
|
159
|
+
### Headings, sections and groups
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
help do
|
|
163
|
+
heading :commands, "Available commands"
|
|
164
|
+
heading_case :capitalize
|
|
165
|
+
|
|
166
|
+
group "Rules", "compile", "validate"
|
|
167
|
+
group "Returns", "evaluate"
|
|
168
|
+
|
|
169
|
+
hide :examples
|
|
170
|
+
sections :banner, :usage, :commands, :options, :epilogue
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `heading` replaces one section's heading text.
|
|
175
|
+
- `group` lists commands under a heading of their own, in the order given. Ungrouped commands stay under Commands. A group inside a group names the full path, such as `"db migrate"`.
|
|
176
|
+
- `sections` sets the order; a section left out is hidden. `hide` hides sections without restating the order.
|
|
177
|
+
|
|
178
|
+
The sections are `banner`, `usage`, `description`, `commands`, `subcommands`, `arguments`, `options`, `examples` and `epilogue`. Each screen prints the ones that apply to it.
|
|
179
|
+
|
|
180
|
+
### Styles
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
help do
|
|
184
|
+
style :heading, :bold, :bright_blue
|
|
185
|
+
style :comment # no styles: print it plain
|
|
186
|
+
end
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The styled elements are `title`, `heading`, `command`, `argument`, `option` and `comment`, the last being the part of an example after the first `#` surrounded by spaces, as in `"rules.form # compile one file"`.
|
|
190
|
+
|
|
191
|
+
### The Colors module
|
|
192
|
+
|
|
193
|
+
Every style is also available to your own code:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class Deploy < Dry::CLI::Command
|
|
197
|
+
include Dry::CLI::Help::Colors
|
|
198
|
+
|
|
199
|
+
def call(**)
|
|
200
|
+
puts green("Deployed.")
|
|
201
|
+
puts red.bold("Rolled back.")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The methods are the eight colors `black red green yellow blue magenta cyan white`, their `bright_` forms, the `on_` and `on_bright_` backgrounds, and `clear bold dim italic underline inverse hidden strikethrough`. `Dry::CLI::Help::Colors.enabled = false` turns them all off.
|
|
207
|
+
|
|
208
|
+
## How it works
|
|
209
|
+
|
|
210
|
+
The gem prepends one module to `Dry::CLI`, overriding the two private methods dry-cli prints help from. It does not replace `Dry::CLI::Banner` or `Dry::CLI::Usage`.
|
|
211
|
+
|
|
212
|
+
```mermaid
|
|
213
|
+
flowchart LR
|
|
214
|
+
argv[ARGV] --> call["Dry::CLI#call"]
|
|
215
|
+
call -->|"command found, -h given"| help["#help"]
|
|
216
|
+
call -->|"no command, a group, -h at a level, a typo"| spell["#spell_checker"]
|
|
217
|
+
help --> command[Screens::Command]
|
|
218
|
+
spell --> listing[Screens::Listing]
|
|
219
|
+
command --> formatter[Formatter]
|
|
220
|
+
listing --> formatter
|
|
221
|
+
config["Help.configure + registry help block"] --> formatter
|
|
222
|
+
formatter --> out[stdout or stderr]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Both methods are `@api private` in dry-cli. `spec/dry/cli/help/dry_cli_contract_spec.rb` asserts every internal the gem reads, so a dry-cli release that moves one fails this suite, naming what moved.
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
just install # bundle install
|
|
231
|
+
just test # the suite; a full run enforces 100% line and branch coverage
|
|
232
|
+
just lint # rubocop
|
|
233
|
+
just ci # both
|
|
234
|
+
just lefthook # every pre-commit hook against every file
|
|
235
|
+
just format # rubocop -a, then mdformat --wrap no on every Markdown file
|
|
236
|
+
bin/console # IRB with the gem loaded
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Contributing
|
|
240
|
+
|
|
241
|
+
Bug reports and pull requests are welcome at <https://github.com/kigster/dry-cli-help>.
|
|
242
|
+
|
|
243
|
+
> [!WARNING]
|
|
244
|
+
> The `dry-` prefix and the `Dry::CLI::Help` namespace do not imply endorsement by dry-rb. This is an independent gem that extends theirs.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "yard"
|
|
6
|
+
|
|
7
|
+
def shell(*args)
|
|
8
|
+
puts "running: #{args.join(' ')}"
|
|
9
|
+
system(args.join(" "))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
task :clean do
|
|
13
|
+
shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
task gem: [:build] do
|
|
17
|
+
shell("gem install pkg/*")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
task permissions: [:clean] do
|
|
21
|
+
# One traversal replaces a six-level glob chain that printed "No such file
|
|
22
|
+
# or directory" for every level this project does not have, skipped dotfiles
|
|
23
|
+
# entirely, and silently stopped at depth six. .git is pruned: its objects
|
|
24
|
+
# have no business being group-readable.
|
|
25
|
+
shell("find . -path ./.git -prune -o -type d -exec chmod o+rx,g+rx {} + -o -type f -exec chmod o+r,g+r {} +")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
task build: :permissions
|
|
29
|
+
|
|
30
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
|
31
|
+
t.files = %w[lib/**/*.rb - README.md LICENSE.txt CHANGELOG.md SPECIFICATION.md]
|
|
32
|
+
t.options.unshift("--title", '"dry-cli-help: configurable help screens for dry-cli"')
|
|
33
|
+
t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
37
|
+
|
|
38
|
+
task default: :spec
|
data/SPECIFICATION.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# dry-cli-help
|
|
2
|
+
|
|
3
|
+
Enhanced help presentation for [`dry-cli`](https://github.com/dry-rb/dry-cli).
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`dry-cli-help` improves the **static, human-facing documentation** generated by `dry-cli`.
|
|
8
|
+
|
|
9
|
+
It does not replace `dry-cli` or introduce another command framework. It takes the command structure already defined through `dry-cli` and provides richer, more configurable help output.
|
|
10
|
+
|
|
11
|
+
## Responsibilities
|
|
12
|
+
|
|
13
|
+
- Top-level application title and description
|
|
14
|
+
- Command descriptions
|
|
15
|
+
- Automatic line wrapping based on terminal width
|
|
16
|
+
- ANSI color and styling
|
|
17
|
+
- Configurable headings
|
|
18
|
+
- Improved spacing and indentation
|
|
19
|
+
- Usage formatting
|
|
20
|
+
- Arguments and options formatting
|
|
21
|
+
- Examples
|
|
22
|
+
- Epilogues
|
|
23
|
+
- Command grouping
|
|
24
|
+
- Section ordering
|
|
25
|
+
- Hiding or customizing sections
|
|
26
|
+
- Consistent formatting across commands
|
|
27
|
+
|
|
28
|
+
## Example
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "dry/cli"
|
|
32
|
+
require "dry/cli/help"
|
|
33
|
+
|
|
34
|
+
Dry::CLI::Help.configure do |config|
|
|
35
|
+
config.width = :terminal
|
|
36
|
+
config.wrap = true
|
|
37
|
+
config.color = true
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
An application could provide richer top-level help:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class CLI
|
|
45
|
+
extend Dry::CLI::Registry
|
|
46
|
+
|
|
47
|
+
help do
|
|
48
|
+
title "Taxlibris"
|
|
49
|
+
|
|
50
|
+
description <<~TEXT
|
|
51
|
+
Compile, validate, and evaluate tax rules.
|
|
52
|
+
TEXT
|
|
53
|
+
|
|
54
|
+
color true
|
|
55
|
+
width :terminal
|
|
56
|
+
wrap true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Result:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Taxlibris
|
|
65
|
+
|
|
66
|
+
Compile, validate, and evaluate tax rules.
|
|
67
|
+
|
|
68
|
+
USAGE
|
|
69
|
+
taxlibris COMMAND [OPTIONS]
|
|
70
|
+
|
|
71
|
+
COMMANDS
|
|
72
|
+
compile Compile tax rules
|
|
73
|
+
validate Validate the rule corpus
|
|
74
|
+
evaluate Evaluate a tax return
|
|
75
|
+
|
|
76
|
+
OPTIONS
|
|
77
|
+
--help Show help
|
|
78
|
+
--version Show version
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Design Principle
|
|
82
|
+
|
|
83
|
+
`dry-cli-help` owns what the user sees **before a command runs**.
|
|
84
|
+
|
|
85
|
+
It should remain focused on documentation and presentation rather than runtime command UI.
|
|
86
|
+
|
|
87
|
+
Runtime features such as progress bars, spinners, status displays, and error panels belong in `dry-cli-ui`.
|
|
88
|
+
|
|
89
|
+
## Relationship
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
dry-cli
|
|
93
|
+
│
|
|
94
|
+
└── dry-cli-help
|
|
95
|
+
│
|
|
96
|
+
├── descriptions
|
|
97
|
+
├── usage
|
|
98
|
+
├── wrapping
|
|
99
|
+
├── headings
|
|
100
|
+
├── colors
|
|
101
|
+
├── arguments/options
|
|
102
|
+
└── examples
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Requirements carried from the first draft
|
|
106
|
+
|
|
107
|
+
1. **Exit status without arguments.** dry-cli prints the command list and exits 1 when run with no command. The status is configurable. Asking for help with `-h` or `--help` always exits 0.
|
|
108
|
+
1. **Banner.** The title and description print above the command list for `mycli`, `mycli -h` and `mycli --help`. A separate setting decides whether they also print for `mycli subcommand -h`.
|
|
109
|
+
1. **Wrapping.** Three modes: no wrapping (dry-cli's behavior), wrapping at a fixed column such as 80 or 100, and wrapping at the terminal's width minus an optional margin.
|
|
110
|
+
1. **Colors.** Built on `pastel`. A `Colors` module, when included, makes each style below available as a method: `red("text")` returns decorated text, and `red.bold("text")` chains.
|
|
111
|
+
- Foreground: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, and the `bright_` form of each.
|
|
112
|
+
- Background: `on_black`, `on_red`, `on_green`, `on_yellow`, `on_blue`, `on_magenta`, `on_cyan`, `on_white`, and the `on_bright_` form of each.
|
|
113
|
+
- Styles: `clear`, `bold`, `dim`, `italic`, `underline`, `inverse`, `hidden`, `strikethrough`.
|
|
114
|
+
|
|
115
|
+
## Decisions
|
|
116
|
+
|
|
117
|
+
### Integration
|
|
118
|
+
|
|
119
|
+
Requiring `dry/cli/help` changes help output for every `Dry::CLI` in the process. It does not replace `Dry::CLI::Banner` or `Dry::CLI::Usage`. It prepends one module to `Dry::CLI` that overrides two private methods, the only two places dry-cli prints help:
|
|
120
|
+
|
|
121
|
+
| dry-cli method | When dry-cli calls it |
|
|
122
|
+
| -------------------------------------- | ---------------------------------------------------------- |
|
|
123
|
+
| `Dry::CLI#help(command, prog_name)` | `mycli deploy -h`, for any command with a class |
|
|
124
|
+
| `Dry::CLI#spell_checker(result, argv)` | `mycli`, `mycli -h`, `mycli db` for a group, `mycli bogus` |
|
|
125
|
+
|
|
126
|
+
Both are `@api private` in dry-cli 1.4.1. A spec asserts they exist, so a dry-cli release that renames them fails this gem's suite rather than a host's help screen. `Dry::CLI::Help::Integration` holds the override.
|
|
127
|
+
|
|
128
|
+
`help` is added to `Dry::CLI::Registry`, so any module or class that extends a registry can call it.
|
|
129
|
+
|
|
130
|
+
### Configuration
|
|
131
|
+
|
|
132
|
+
Two levels, one vocabulary. `Dry::CLI::Help.configure` sets process-wide values. A registry's `help` block overrides them for that registry. A setting neither level sets takes the default below.
|
|
133
|
+
|
|
134
|
+
Every setting reads and writes both ways: `title "Taxlibris"` inside a `help` block, and `config.title = "Taxlibris"` on the yielded object. A `help` block taking one argument receives the configuration instead of being evaluated against it.
|
|
135
|
+
|
|
136
|
+
| Setting | Values | Default | Effect |
|
|
137
|
+
| ----------------------------- | ------------------------------------ | --------------- | ---------------------------------------------------------------------- |
|
|
138
|
+
| `title` | String | none | First line of the banner |
|
|
139
|
+
| `description` | String | none | Paragraphs under the title, wrapped |
|
|
140
|
+
| `epilogue` | String | none | Paragraphs at the end of the top-level help |
|
|
141
|
+
| `color` | `true`, `false`, `:auto` | `:auto` | `:auto` colors a terminal and honors `NO_COLOR` |
|
|
142
|
+
| `wrap` | `true`, `false` | `true` | `false` prints descriptions as written |
|
|
143
|
+
| `width` | `:terminal`, Integer | `:terminal` | The column text wraps at |
|
|
144
|
+
| `margin` | Integer | `0` | Columns kept free at the right edge when `width` is `:terminal` |
|
|
145
|
+
| `exit_code_without_arguments` | Integer, 0 to 255 | `1` | Status for `mycli` or `mycli group` with no command |
|
|
146
|
+
| `banner_on_subcommands` | `true`, `false` | `false` | Print the title and description above `mycli command -h` |
|
|
147
|
+
| `heading_case` | `:upcase`, `:capitalize`, `:none` | `:upcase` | How every heading is cased; `:capitalize` raises only the first letter |
|
|
148
|
+
| `command_order` | `:registration`, `:alphabetical` | `:registration` | Order commands list in; dry-cli sorts alphabetically |
|
|
149
|
+
| `heading(section, text)` | Section name, String | see below | Replaces one heading's text |
|
|
150
|
+
| `style(element, *styles)` | Element name, `Colors::STYLES` names | see below | Replaces one element's styles |
|
|
151
|
+
| `group(name, *commands)` | String, command paths | none | Lists those commands under their own heading, in declaration order |
|
|
152
|
+
| `sections(*names)` | Section names | all | Order of sections; a section left out is hidden |
|
|
153
|
+
| `hide(*names)` | Section names | none | Hides sections without restating the order |
|
|
154
|
+
|
|
155
|
+
Terminal width comes from `COLUMNS`, then the console, then 80. A resolved wrap width never falls below 20 columns.
|
|
156
|
+
|
|
157
|
+
### Sections
|
|
158
|
+
|
|
159
|
+
In default order, with default headings:
|
|
160
|
+
|
|
161
|
+
| Section | Heading | Top-level help | Command help |
|
|
162
|
+
| ------------- | ----------- | -------------- | ----------------------------------- |
|
|
163
|
+
| `banner` | none | yes | when `banner_on_subcommands` is set |
|
|
164
|
+
| `usage` | Usage | yes | yes |
|
|
165
|
+
| `description` | Description | no | yes |
|
|
166
|
+
| `commands` | Commands | yes | no |
|
|
167
|
+
| `subcommands` | Subcommands | no | when the command has children |
|
|
168
|
+
| `arguments` | Arguments | no | yes |
|
|
169
|
+
| `options` | Options | yes | yes |
|
|
170
|
+
| `examples` | Examples | no | yes |
|
|
171
|
+
| `epilogue` | none | yes | only for a single command |
|
|
172
|
+
|
|
173
|
+
A single command passed to `Dry::CLI.new(SomeCommand)` is the whole program, so its command help prints the banner and the epilogue too. It has no registry, so it renders with the process-wide settings alone.
|
|
174
|
+
|
|
175
|
+
A group listing, `mycli db` where `db` has no command of its own, renders as top-level help scoped to the group, with the banner only when `banner_on_subcommands` is set, and no epilogue.
|
|
176
|
+
|
|
177
|
+
Every Options section starts with `-h, --help Show help`, because both spellings work at every level. The example above shows `--help` alone; `spec/dry/cli/help/integration_spec.rb` holds the exact output.
|
|
178
|
+
|
|
179
|
+
At any level, a command registered under a name or alias starting with `-`, such as `register "version", Version, aliases: ["--version"]`, lists under Options rather than Commands.
|
|
180
|
+
|
|
181
|
+
### Styled elements
|
|
182
|
+
|
|
183
|
+
| Element | Default styles | Applies to |
|
|
184
|
+
| ---------- | ---------------- | -------------------------------------- |
|
|
185
|
+
| `title` | `bold` | The banner title |
|
|
186
|
+
| `heading` | `bold`, `yellow` | Every section heading |
|
|
187
|
+
| `command` | `green` | Command names and the program in usage |
|
|
188
|
+
| `argument` | `cyan` | Argument names |
|
|
189
|
+
| `option` | `cyan` | Option names |
|
|
190
|
+
| `comment` | `bright_black` | The comment half of an example |
|
|
191
|
+
|
|
192
|
+
### Layout
|
|
193
|
+
|
|
194
|
+
- Every section after the first starts after one blank line. Section bodies indent two columns.
|
|
195
|
+
- A definition list (commands, arguments, options) aligns its descriptions in one column across the whole screen, two columns past the longest term that has a description. The column never passes half the wrap width, and a term longer than the column puts its description on the next line.
|
|
196
|
+
- Examples align among themselves. A full command line is longer than any option, and sharing its column would push every other description to the right.
|
|
197
|
+
- A group registered without a command has no description of its own, so it lists as `Subcommands: a, b`.
|
|
198
|
+
- An argument or option description ends with what a reader needs to supply it: `(required; one of: json, yaml; default: "json")`. An array argument reads `NAME...`.
|
|
199
|
+
- Descriptions wrap with a hanging indent under the description column. Paragraphs split on blank lines and reflow; a line starting with whitespace prints verbatim.
|
|
200
|
+
- An example written `"prod # ship to production"` renders the part after the first `#` surrounded by spaces as its description.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dry
|
|
4
|
+
class CLI
|
|
5
|
+
module Help
|
|
6
|
+
# Every pastel style as a method. Include it, or extend a module with it:
|
|
7
|
+
#
|
|
8
|
+
# include Dry::CLI::Help::Colors
|
|
9
|
+
#
|
|
10
|
+
# red("text") # => decorated text
|
|
11
|
+
# red.bold("text") # => chained styles
|
|
12
|
+
#
|
|
13
|
+
# Whether the methods decorate is one process-wide switch, {Colors.enabled=}.
|
|
14
|
+
# Help screens do not read it: they follow the `color` setting instead.
|
|
15
|
+
module Colors
|
|
16
|
+
HUES = %i[black red green yellow blue magenta cyan white].freeze
|
|
17
|
+
FOREGROUNDS = (HUES + HUES.map { :"bright_#{it}" }).freeze
|
|
18
|
+
BACKGROUNDS = FOREGROUNDS.map { :"on_#{it}" }.freeze
|
|
19
|
+
MODIFIERS = %i[clear bold dim italic underline inverse hidden strikethrough].freeze
|
|
20
|
+
STYLES = (FOREGROUNDS + BACKGROUNDS + MODIFIERS).freeze
|
|
21
|
+
|
|
22
|
+
# Values the color switch and the `color` setting accept.
|
|
23
|
+
SETTINGS = [true, false, :auto].freeze
|
|
24
|
+
|
|
25
|
+
@enabled = :auto
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# @return [Boolean, Symbol] true, false or :auto
|
|
29
|
+
attr_reader :enabled
|
|
30
|
+
|
|
31
|
+
# @param value [Boolean, Symbol] true, false or :auto
|
|
32
|
+
def enabled=(value)
|
|
33
|
+
raise ArgumentError, "color must be one of #{SETTINGS.inspect}, got #{value.inspect}" \
|
|
34
|
+
unless SETTINGS.include?(value)
|
|
35
|
+
|
|
36
|
+
@enabled = value
|
|
37
|
+
@pastel = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [Pastel] shared by every includer, rebuilt when the switch changes
|
|
41
|
+
def pastel
|
|
42
|
+
@pastel ||= ::Pastel.new(enabled: enabled_for?(enabled, $stdout))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# `:auto` colors a terminal unless NO_COLOR is set to anything but
|
|
46
|
+
# the empty string. https://no-color.org
|
|
47
|
+
#
|
|
48
|
+
# @param setting [Boolean, Symbol] true, false or :auto
|
|
49
|
+
# @param io [IO, #tty?] where the text is going
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def enabled_for?(setting, io)
|
|
52
|
+
return setting unless setting == :auto
|
|
53
|
+
|
|
54
|
+
io.respond_to?(:tty?) && io.tty? && ENV.fetch("NO_COLOR", "").empty?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Pastel]
|
|
59
|
+
def pastel
|
|
60
|
+
Colors.pastel
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
STYLES.each do |style|
|
|
64
|
+
define_method(style) do |*text|
|
|
65
|
+
text.empty? ? pastel.public_send(style) : pastel.public_send(style, *text)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|