dry-cli-help 0.1.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8756ab593e09c6cdc0e0ab32fe6f3909d05625734a07e7d7c334e3d832ba70ac
4
- data.tar.gz: 4477df389d4c4c0d984fe4024f768adf3e254772464694f7acd81609936060a4
3
+ metadata.gz: 10fb8e86e571ad96c6f523f4807d040b0421ebe1fa65e334738fa785e0212196
4
+ data.tar.gz: 408f6dd175e32d407f38306c9c61211a9b8e9d7e82f8870cde02ce58cfe64a23
5
5
  SHA512:
6
- metadata.gz: 6a61174289b02e6a523ba5ccdae1a79cd72241ea7c5bb16de5d543e3aad488a3de9d973644a0297c2c94e0516475241c490470f5d52208f450d7efc243dfd575
7
- data.tar.gz: 041553f6adda56086400f208d6bef2ce77613e635581e871ffc58488a3ead1cca4038c3ce4d89f39e79049f529ffaa7cb013dea09bea8701e9c7b5c78ea1d3eb
6
+ metadata.gz: f42c06e5cb90eae00d6fc11f1fa4cbfed1f789005b4aa6dc0ea27cd60522de63019804c154e818efc4eb4bc52e673765ad638f881f94faa445b0b1a6ce637060
7
+ data.tar.gz: 9e711ac3f6b30580e1e979f391019de8f1c5d155b9ded6b1c72881e698d79347e337299aca96316b9c9806507d9b1d7022c0f30d9bf715083e60a7a651775c82
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - Unreleased
4
+
5
+ - The `dry-cli` dependency no longer pins a version range; any release satisfies it.
6
+ - `SPECIFICATION.md` moves to `docs/SPECIFICATION.md`, marked as the specification for 0.1.0, and stays out of the YARD documentation.
7
+ - The README documents which sections each screen prints, how groups and nested commands list, the description column and minimum wrap width, `Help.config`, `Help.reset!`, and the `ArgumentError` a bad setting raises.
8
+
9
+ ## [0.2.1] - 2026-09-15
10
+
11
+ - Every setting is made once, in `Dry::CLI::Help.configure`, which now also accepts a block without an argument and runs it against the configuration. The `help` block on registries is removed, along with `Help.config_for` and `Configuration#merge`: the gem adds nothing to dry-cli's registry or command DSL.
12
+ - A `styles` block declares every element's look in one place, replacing `style(element, *names)`. New elements `usage`, `example` and `example_comment`; `comment` is renamed `example_comment` and defaults to bold black.
13
+ - Heading case moves into the heading style: `styles { heading :bold, case: :Capitalize }`, one of `:UPPERCASE`, `:Capitalize`, `:lowercase` or `:as_is`. The `heading_case` setting is removed.
14
+ - `examples/` holds three single-file dry-cli tools that switch this gem on with `--with-dry-cli-help` or `-w`.
15
+
16
+ ## [0.2.0] - 2026-09-14
17
+
18
+ - Documentation, justfile, lefthook and packaging fixes after the conversion. No change to behavior.
19
+
3
20
  ## [0.1.0] - 2026-09-12
4
21
 
5
22
  - Initial release as `dry-cli-help`, converted from `dry-cli-autocomplete`. The shell completion generator is removed; that gem remains the place for it.
data/README.md CHANGED
@@ -5,20 +5,20 @@
5
5
  Configurable, wrapped, colored help screens for [dry-cli](https://github.com/dry-rb/dry-cli) applications.
6
6
 
7
7
  > [!NOTE]
8
- > The design, the settings and every decision behind them are in [SPECIFICATION.md](SPECIFICATION.md).
8
+ > The original design, the settings and the decisions behind them are in [docs/SPECIFICATION.md](docs/SPECIFICATION.md), written for version 0.1.0.
9
9
 
10
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
11
 
12
12
  ## Before and after
13
13
 
14
- `taxlibris compile -h` with dry-cli alone:
14
+ `my-cli compile -h` with dry-cli alone:
15
15
 
16
16
  ```text
17
17
  Command:
18
- taxlibris compile
18
+ my-cli compile
19
19
 
20
20
  Usage:
21
- taxlibris compile RULES [OUTPUT]
21
+ my-cli compile RULES [OUTPUT]
22
22
 
23
23
  Description:
24
24
  Compile tax rules
@@ -37,7 +37,7 @@ With `require "dry/cli/help"`:
37
37
 
38
38
  ```text
39
39
  USAGE
40
- taxlibris compile RULES [OUTPUT] [OPTIONS]
40
+ my-cli compile RULES [OUTPUT] [OPTIONS]
41
41
 
42
42
  DESCRIPTION
43
43
  Compile tax rules
@@ -54,6 +54,13 @@ OPTIONS
54
54
 
55
55
  Headings are bold and yellow, commands green, options and arguments cyan, and every description wraps to the terminal with a hanging indent.
56
56
 
57
+ The same comparison as screenshots, taken from `examples/rbcheck`:
58
+
59
+ | Standard dry-cli Help Screen | Require `dry/cli/help` |
60
+ | :---------------------------------------------- | :--------------------------------------- |
61
+ | ![original](docs/img/without-dry-cli-help.avif) | ![with](docs/img/with-dry-cli-help.avif) |
62
+ | | |
63
+
57
64
  ## Installation
58
65
 
59
66
  ```bash
@@ -62,6 +69,8 @@ gem install dry-cli-help
62
69
 
63
70
  Or add `gem "dry-cli-help"` to your `Gemfile`.
64
71
 
72
+ It requires Ruby 4.0 or newer, and depends on `dry-cli` and `pastel`.
73
+
65
74
  ## Usage
66
75
 
67
76
  Require it after dry-cli. That alone changes every help screen in the process.
@@ -71,44 +80,44 @@ require "dry/cli"
71
80
  require "dry/cli/help"
72
81
  ```
73
82
 
74
- Describe the program in the registry:
83
+ Then make every setting once, in one block, before the CLI runs. Your registry, commands and options stay plain dry-cli: the gem adds nothing to them, so you can add or remove it without touching a command.
75
84
 
76
85
  ```ruby
77
- module Taxlibris
78
- module CLI
79
- extend Dry::CLI::Registry
86
+ Dry::CLI::Help.configure do
87
+ title "MyCLI"
80
88
 
81
- help do
82
- title "Taxlibris"
89
+ description <<~TEXT
90
+ Compile, validate, and evaluate rules.
91
+ TEXT
83
92
 
84
- description <<~TEXT
85
- Compile, validate, and evaluate tax rules.
86
- TEXT
93
+ epilogue "Documentation: https://example.com/my-cli"
87
94
 
88
- epilogue "Documentation: https://example.com/taxlibris"
95
+ color :auto
96
+ width :terminal
97
+ wrap true
98
+ end
89
99
 
90
- color :auto
91
- width :terminal
92
- wrap true
93
- end
100
+ module My
101
+ module CLI
102
+ extend Dry::CLI::Registry
94
103
 
95
- register "compile", Compile
104
+ register "compile", Compile
96
105
  register "validate", Validate
97
106
  register "evaluate", Evaluate
98
- register "version", Version, aliases: ["--version", "-v"]
107
+ register "version", Version, aliases: ["--version", "-v"]
99
108
  end
100
109
  end
101
110
  ```
102
111
 
103
- `taxlibris -h` then prints:
112
+ `my-cli -h` then prints:
104
113
 
105
114
  ```text
106
- Taxlibris
115
+ MyCLI
107
116
 
108
- Compile, validate, and evaluate tax rules.
117
+ Compile, validate, and evaluate rules.
109
118
 
110
119
  USAGE
111
- taxlibris COMMAND [OPTIONS]
120
+ my-cli COMMAND [OPTIONS]
112
121
 
113
122
  COMMANDS
114
123
  compile Compile tax rules
@@ -120,12 +129,12 @@ OPTIONS
120
129
  -h, --help Show help
121
130
  -v, --version Show version
122
131
 
123
- Documentation: https://example.com/taxlibris
132
+ Documentation: https://example.com/my-cli
124
133
  ```
125
134
 
126
- A command reachable as `--version` lists under Options by its dashed names.
135
+ A command reachable as `--version` lists under Options by its dashed names. Commands marked `hidden: true` in the registry stay out of every list, and a non-dashed alias prints next to its command, as in `build, b`.
127
136
 
128
- Settings for the whole process go through `configure`. A registry's `help` block overrides them:
137
+ A block that takes an argument receives the configuration instead of running against it:
129
138
 
130
139
  ```ruby
131
140
  Dry::CLI::Help.configure do |config|
@@ -134,34 +143,202 @@ Dry::CLI::Help.configure do |config|
134
143
  end
135
144
  ```
136
145
 
137
- A `help` block that takes an argument receives the configuration instead, so `help { |h| h.title = "Taxlibris" }` works too.
146
+ Every setting validates its value and raises `ArgumentError` on one it cannot use. `Dry::CLI::Help.config` returns the current settings, and `Dry::CLI::Help.reset!` forgets them all, which is handy between tests.
147
+
148
+ ## Examples
149
+
150
+ The [`examples`](examples) folder holds three single-file tools built on dry-cli. Each one loads and configures this gem only when you pass `--with-dry-cli-help`, or `-w` for short, so you can run the same command both ways and compare:
151
+
152
+ ```bash
153
+ examples/rbcheck -h # dry-cli's own help
154
+ examples/rbcheck -h --with-dry-cli-help # the same help, through this gem
155
+ ```
156
+
157
+ `examples/rbcheck` has two commands, `version` and `check-ruby-syntax [DIR]`. Condensed:
158
+
159
+ ```ruby
160
+ #!/usr/bin/env ruby
161
+ require "dry/cli"
162
+
163
+ if [ARGV.delete("--with-dry-cli-help"), ARGV.delete("-w")].any?
164
+ require "dry/cli/help"
165
+
166
+ Dry::CLI::Help.configure do
167
+ title "rbcheck"
168
+ description "Small checks for Ruby projects."
169
+ epilogue "Report bugs at https://example.com/rbcheck/issues"
170
+ end
171
+ end
172
+
173
+ module Rbcheck
174
+ extend Dry::CLI::Registry
175
+
176
+ class Version < Dry::CLI::Command
177
+ desc "Print the version"
178
+
179
+ def call(**) = puts("rbcheck 1.0.0")
180
+ end
181
+
182
+ class CheckRubySyntax < Dry::CLI::Command
183
+ desc "Check every Ruby file under a directory for syntax errors, and report "\
184
+ "each file that fails to parse with its line number"
185
+
186
+ argument :dir, desc: "Directory to scan", default: "."
187
+ option :exclude, type: :array, desc: "Glob patterns to skip, such as vendor/**"
188
+ option :quiet, type: :boolean, default: false, aliases: ["-q"],
189
+ desc: "Print only the files that fail"
190
+
191
+ example ["lib # check one directory", "--exclude=vendor/** # skip vendored gems"]
192
+
193
+ def call(dir:, quiet:, exclude: [], **)
194
+ # ... parses every file under dir with Prism ...
195
+ end
196
+ end
197
+
198
+ register "version", Version, aliases: ["--version", "-v"]
199
+ register "check-ruby-syntax", CheckRubySyntax
200
+ end
201
+
202
+ Dry::CLI.new(Rbcheck).call
203
+ ```
204
+
205
+ Every screen below comes from running it in an 80-column terminal.
206
+
207
+ ### `rbcheck -h`
208
+
209
+ dry-cli alone prints a list of commands to stderr and exits 1, and does not wrap the long description.
210
+
211
+ ```text
212
+ Commands:
213
+ rbcheck check-ruby-syntax [DIR] # Check every Ruby file under a directory for syntax errors, and report each file that fails to parse with its line number
214
+ rbcheck version # Print the version
215
+ ```
216
+
217
+ With this gem, `rbcheck -h` prints the help below to stdout and exits 0.
218
+
219
+ > [!NOTE]
220
+ > Whether a CLI run with no arguments should exit 1 or 0 is debatable. dry-cli exits 1, and so does this gem by default: plain `rbcheck` prints the same screen to stderr and exits 1. Set [`exit_code_without_arguments`](#dsl-based-configuration-api) to `0` to treat it like `-h` instead.
221
+
222
+ ```text
223
+ rbcheck
224
+
225
+ Small checks for Ruby projects.
226
+
227
+ USAGE
228
+ rbcheck COMMAND [OPTIONS]
229
+
230
+ COMMANDS
231
+ version Print the version
232
+ check-ruby-syntax Check every Ruby file under a directory for syntax errors,
233
+ and report each file that fails to parse with its line
234
+ number
235
+
236
+ OPTIONS
237
+ -h, --help Show help
238
+ -v, --version Print the version
239
+
240
+ Report bugs at https://example.com/rbcheck/issues
241
+ ```
242
+
243
+ - The title, description and epilogue come from the `configure` block.
244
+ - Commands list in the order they were registered, not alphabetically.
245
+ - The long description wraps to the terminal under its own column.
246
+ - `version` is also reachable as `--version` and `-v`, so it lists under Options too. dry-cli never shows those aliases.
247
+
248
+ ### `rbcheck check-ruby-syntax -h`
249
+
250
+ dry-cli alone:
251
+
252
+ ```text
253
+ Command:
254
+ rbcheck check-ruby-syntax
255
+
256
+ Usage:
257
+ rbcheck check-ruby-syntax [DIR]
258
+
259
+ Description:
260
+ Check every Ruby file under a directory for syntax errors, and report each file that fails to parse with its line number
261
+
262
+ Arguments:
263
+ DIR # Directory to scan
264
+
265
+ Options:
266
+ --exclude=VALUE1,VALUE2,.. # Glob patterns to skip, such as vendor/**
267
+ --[no-]quiet, -q # Print only the files that fail, default: false
268
+ --help, -h # Print this help
269
+
270
+ Examples:
271
+ rbcheck check-ruby-syntax lib # check one directory
272
+ rbcheck check-ruby-syntax --exclude=vendor/** # skip vendored gems
273
+ ```
274
+
275
+ With this gem:
276
+
277
+ ```text
278
+ USAGE
279
+ rbcheck check-ruby-syntax [DIR] [OPTIONS]
280
+
281
+ DESCRIPTION
282
+ Check every Ruby file under a directory for syntax errors, and report each
283
+ file that fails to parse with its line number
138
284
 
139
- ## Settings
285
+ ARGUMENTS
286
+ DIR Directory to scan (default: ".")
287
+
288
+ OPTIONS
289
+ --exclude=VALUE1,VALUE2,.. Glob patterns to skip, such as vendor/**
290
+ -q, --[no-]quiet Print only the files that fail (default: false)
291
+ -h, --help Show help
292
+
293
+ EXAMPLES
294
+ rbcheck check-ruby-syntax lib check one directory
295
+ rbcheck check-ruby-syntax --exclude=vendor/**
296
+ skip vendored gems
297
+ ```
140
298
 
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` |
299
+ - The `Command:` section repeats the usage line, so it is gone, and the usage line shows that the command takes options.
300
+ - The argument's default of `"."` shows up; dry-cli leaves it out.
301
+ - Short aliases come first (`-q, --[no-]quiet`), and every description lines up in one column instead of trailing a `#`.
302
+ - Each example's comment moves into a column of its own. An example longer than that column puts its comment on the next line.
303
+
304
+ In a terminal the headings print bold yellow, usage lines, commands and examples green, arguments and options cyan, and example comments bold black.
305
+
306
+ `examples/todo` shows nested commands, custom headings and a `styles` block. `examples/deploy` shows a fixed width, a banner above command help, reordered sections and help without a command exiting 0. [`examples/README.md`](examples/README.md) lists what each one demonstrates.
307
+
308
+ ## DSL-based Configuration API
309
+
310
+ The gem offers a compact DSL in the general spirit of Ruby and `dry-rb` in particular, and makes the following methods available within the `configure` block.
311
+
312
+ | Setting | Values | Default | What it does |
313
+ | ----------------------------- | -------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------- |
314
+ | `title` | String | none | Prints the first line of the banner, above the top-level help |
315
+ | `description` | String | none | Prints paragraphs under the title, reflowed to the wrap width |
316
+ | `epilogue` | String | none | Prints paragraphs at the very end of the top-level help |
317
+ | `color` | `true`, `false`, `:auto` | `:auto` | Paints headings, commands, arguments and options; `:auto` paints only a terminal |
318
+ | `wrap` | `true`, `false` | `true` | Wraps descriptions with a hanging indent; `false` prints each one on a single line as written |
319
+ | `width` | `:terminal`, Integer | `:terminal` | Sets the column text wraps at; `:terminal` follows the terminal's width |
320
+ | `margin` | Integer | `0` | Keeps that many columns free at the right edge when `width` is `:terminal` |
321
+ | `exit_code_without_arguments` | 0 to 255 | `1` | Sets the exit status of `my-cli` or `my-cli db` run with no command; `0` also prints the help to stdout instead of stderr |
322
+ | `banner_on_subcommands` | `true`, `false` | `false` | Prints the title and description above command help and group listings too, not only above the top-level help |
323
+ | `command_order` | `:registration`, `:alphabetical` | `:registration` | Lists commands in the order you registered them, or sorted by name as dry-cli does |
154
324
 
155
325
  `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
326
 
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.
327
+ Running the program, or a group such as `my-cli db`, with no command prints its help to stderr and exits 1, as dry-cli does. With
328
+
329
+ ```ruby
330
+ exit_code_without_arguments 0
331
+ ```
332
+
333
+ it prints to stdout and exits 0 instead. `-h` and `--help` always print to stdout and exit 0. A mistyped command prints dry-cli's suggestion, then the help, to stderr and exits 1.
334
+
335
+ The banner (title and description) and the epilogue print on the top-level help. A CLI built from a single command, `Dry::CLI.new(Deploy)`, counts as top level, so its command help gets both.
158
336
 
159
337
  ### Headings, sections and groups
160
338
 
161
339
  ```ruby
162
- help do
340
+ Dry::CLI::Help.configure do
163
341
  heading :commands, "Available commands"
164
- heading_case :capitalize
165
342
 
166
343
  group "Rules", "compile", "validate"
167
344
  group "Returns", "evaluate"
@@ -171,22 +348,58 @@ help do
171
348
  end
172
349
  ```
173
350
 
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"`.
351
+ - `heading` replaces one section's heading text. It takes `:usage`, `:description`, `:commands`, `:subcommands`, `:arguments`, `:options` or `:examples`; `banner` and `epilogue` have no heading. How headings are cased belongs to their style, below.
352
+ - `group` lists commands under a heading of their own, in the order given. Ungrouped commands stay under Commands, and groups print below them in the order declared. A group with no commands raises `ArgumentError`; a group whose commands a screen does not list prints no heading there. A nested command is named by its full path, such as `"db migrate"`, and its group shows on `my-cli db -h`.
176
353
  - `sections` sets the order; a section left out is hidden. `hide` hides sections without restating the order.
177
354
 
178
- The sections are `banner`, `usage`, `description`, `commands`, `subcommands`, `arguments`, `options`, `examples` and `epilogue`. Each screen prints the ones that apply to it.
355
+ The sections are `banner`, `usage`, `description`, `commands`, `subcommands`, `arguments`, `options`, `examples` and `epilogue`. Each screen prints the ones that apply to it:
356
+
357
+ | Screen | Sections it can print |
358
+ | --------------------------------------------- | ----------------------------------------------------------------------------------------------- |
359
+ | A listing: `my-cli`, `my-cli -h`, `my-cli db` | `banner`, `usage`, `commands`, `options`, `epilogue` |
360
+ | A command: `my-cli compile -h` | `banner`, `usage`, `description`, `subcommands`, `arguments`, `options`, `examples`, `epilogue` |
361
+
362
+ A few details of those screens:
363
+
364
+ - A command that also has subcommands gets a second usage line, `my-cli db COMMAND [OPTIONS]`, and a Subcommands section.
365
+ - A group registered without a command of its own describes itself by what it holds, as in `Subcommands: add, remove`.
366
+ - An array argument prints as `FILES...`.
367
+ - Every definition list on a screen shares one description column, never wider than half the wrap width. A term longer than that column puts its description on the next line.
368
+ - Text never wraps narrower than 20 columns, however small the terminal.
179
369
 
180
370
  ### Styles
181
371
 
372
+ Every element's look is declared together, in one `styles` block inside `configure`:
373
+
182
374
  ```ruby
183
- help do
184
- style :heading, :bold, :bright_blue
185
- style :comment # no styles: print it plain
375
+ Dry::CLI::Help.configure do
376
+ styles do
377
+ title :bold
378
+ heading :bold, :yellow, case: :UPPERCASE
379
+ usage :green
380
+ command :green
381
+ argument :cyan
382
+ option :cyan
383
+ example :green
384
+ example_comment :bold, :black
385
+ end
186
386
  end
187
387
  ```
188
388
 
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"`.
389
+ Those are the defaults. Name only the elements you want to change; a line with no styles, such as `example_comment`, prints that element plain.
390
+
391
+ | Element | Applies to |
392
+ | ----------------- | ---------------------------------------------------------------------------------------------------- |
393
+ | `title` | The banner title |
394
+ | `heading` | Every section and group heading |
395
+ | `usage` | Every usage line, whole |
396
+ | `command` | Command names in a list |
397
+ | `argument` | Argument names |
398
+ | `option` | Option names |
399
+ | `example` | The command line of an example |
400
+ | `example_comment` | The part of an example after the first `#` surrounded by spaces, as in `"lib # check one directory"` |
401
+
402
+ `heading` alone takes `case:`, one of `:UPPERCASE`, `:Capitalize`, `:lowercase` or `:as_is`, each written the way it cases. `:Capitalize` raises only the first letter. `heading case: :as_is` with no styles changes the case and keeps the colors.
190
403
 
191
404
  ### The Colors module
192
405
 
@@ -203,23 +416,33 @@ class Deploy < Dry::CLI::Command
203
416
  end
204
417
  ```
205
418
 
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.
419
+ 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` takes `true`, `false` or `:auto` (the default, which colors only a terminal and honors `NO_COLOR`). Help screens ignore that switch and follow the `color` setting.
207
420
 
208
421
  ## How it works
209
422
 
210
423
  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
424
 
212
425
  ```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]
426
+ ---
427
+ config:
428
+ layout: elk
429
+ theme: forest
430
+ ---
431
+ flowchart TB
432
+ argv["ARGV"] --> cli_call["Dry::CLI#call"]
433
+
434
+ cli_call -->|"command found, --help given"| help_method["#help"]
435
+ cli_call -->|"no command, a group, -h at a level, a typo"| spell_checker["#spell_checker"]
436
+
437
+ help_method --> command_screen["Screens::Command"]
438
+ spell_checker --> listing_screen["Screens::Listing"]
439
+
440
+ command_screen --> formatter["Formatter"]
441
+ listing_screen --> formatter
442
+
443
+ help_config["Help.configure"] --> formatter
444
+
445
+ formatter --> output["stdout or stderr"]
223
446
  ```
224
447
 
225
448
  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.
@@ -227,13 +450,16 @@ Both methods are `@api private` in dry-cli. `spec/dry/cli/help/dry_cli_contract_
227
450
  ## Development
228
451
 
229
452
  ```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
453
+ just install # bundle install
454
+ just test # the suite; a full run enforces 100% line and branch coverage
455
+ just test-coverage # measure coverage even for a partial run
456
+ just lint # rubocop
457
+ just ci # rubocop, then the suite with coverage
458
+ just lefthook # every pre-commit hook against every file
459
+ just format # mdformat --wrap no on every Markdown file, then rubocop -a
460
+ just doc # YARD documentation
461
+ just build # build the .gem into pkg/
462
+ bin/console # IRB with the gem loaded
237
463
  ```
238
464
 
239
465
  ## Contributing
@@ -241,7 +467,21 @@ bin/console # IRB with the gem loaded
241
467
  Bug reports and pull requests are welcome at <https://github.com/kigster/dry-cli-help>.
242
468
 
243
469
  > [!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.
470
+ > The `dry-` prefix and the `Dry::CLI::Help` namespace do not imply endorsement by dry-rb.
471
+ >
472
+ > This is an independent and opinionated gem that extends theirs.
473
+
474
+ ## Note to Dry-Rb Maintainers
475
+
476
+ First — hats off to all of you who tirelessly built out one of the most valuable collections of libraries in the Ruby ecosystem.
477
+
478
+ While I admire and would be willing to contribute any or all of the extension gem's code to the original gem, I feel that creating plugins and extensions allows the author to fully express their needs and wants, and then, if the authors of `dry-cli` become interested in any of them, I would be honored to submit a PR to `dry-cli` itself.
479
+
480
+ This method offered a very open road to extensibility and experimentation. If the code quality or design is not up to the level required for direct contributions to `dry-rb`, then let it be known that:
481
+
482
+ 1. We would be very happy to receive any feedback and improve, refactor, and update the gem assuming it improves it
483
+ 1. Roll any part of the codebase as a PR to the `dry-cli` core.
484
+ 1. We hold the authors of `dry-rb` in high regard, and generally would love to collaborate, as long as the feedback loop/cycle is not so long that the context of the changes gets lost in time, as with so many contributions made to other gems in the past.
245
485
 
246
486
  ## License
247
487
 
data/Rakefile CHANGED
@@ -28,7 +28,7 @@ end
28
28
  task build: :permissions
29
29
 
30
30
  YARD::Rake::YardocTask.new(:doc) do |t|
31
- t.files = %w[lib/**/*.rb - README.md LICENSE.txt CHANGELOG.md SPECIFICATION.md]
31
+ t.files = %w[lib/**/*.rb - README.md LICENSE.txt CHANGELOG.md]
32
32
  t.options.unshift("--title", '"dry-cli-help: configurable help screens for dry-cli"')
33
33
  t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
34
34
  end
@@ -3,19 +3,18 @@
3
3
  module Dry
4
4
  class CLI
5
5
  module Help
6
- # Help settings, readable and writable two ways:
6
+ # Every help setting, made once for the whole process, two ways:
7
7
  #
8
- # help do
9
- # title "Taxlibris" # inside a registry's help block
8
+ # Dry::CLI::Help.configure do
9
+ # title "MyCLI" # as a DSL
10
10
  # end
11
11
  #
12
12
  # Dry::CLI::Help.configure do |config|
13
- # config.title = "Taxlibris" # on the yielded object
13
+ # config.title = "MyCLI" # on the yielded object
14
14
  # end
15
15
  #
16
- # An instance stores only what was set on it. Everything else reads from
17
- # {DEFAULTS}, which is what lets a registry's settings sit over the
18
- # process-wide ones through {#merge} without either copying the other.
16
+ # Every element's look is declared in one {#styles} block. Anything not
17
+ # set reads from {DEFAULTS}, {HEADINGS} or {STYLES}.
19
18
  class Configuration
20
19
  # Every section, in default order.
21
20
  SECTIONS = %i[
@@ -37,13 +36,18 @@ module Dry
37
36
  STYLES = {
38
37
  title: %i[bold],
39
38
  heading: %i[bold yellow],
39
+ usage: %i[green],
40
40
  command: %i[green],
41
41
  argument: %i[cyan],
42
42
  option: %i[cyan],
43
- comment: %i[bright_black]
43
+ example: %i[green],
44
+ example_comment: %i[bold black]
44
45
  }.freeze
45
46
 
46
- HEADING_CASES = %i[upcase capitalize none].freeze
47
+ # How headings can be cased; each name is written the way it cases.
48
+ # `:Capitalize` raises only the first letter.
49
+ HEADING_CASES = %i[UPPERCASE Capitalize lowercase as_is].freeze
50
+ DEFAULT_HEADING_CASE = :UPPERCASE
47
51
  COMMAND_ORDERS = %i[registration alphabetical].freeze
48
52
 
49
53
  # Narrowest column text wraps at, however small the terminal.
@@ -63,7 +67,6 @@ module Dry
63
67
  margin: ->(value) { value.is_a?(Integer) && !value.negative? },
64
68
  exit_code_without_arguments: ->(value) { value.is_a?(Integer) && value.between?(0, 255) },
65
69
  banner_on_subcommands: BOOLEAN,
66
- heading_case: ->(value) { HEADING_CASES.include?(value) },
67
70
  command_order: ->(value) { COMMAND_ORDERS.include?(value) }
68
71
  }.freeze
69
72
 
@@ -77,7 +80,6 @@ module Dry
77
80
  margin: 0,
78
81
  exit_code_without_arguments: 1,
79
82
  banner_on_subcommands: false,
80
- heading_case: :upcase,
81
83
  command_order: :registration
82
84
  }.freeze
83
85
 
@@ -122,26 +124,36 @@ module Dry
122
124
  HEADINGS.merge(@values.fetch(:headings, {}))
123
125
  end
124
126
 
125
- # Replace the styles of one element. No styles prints it plain.
127
+ # Declare how elements look, all in one place. Each line names an
128
+ # element from {STYLES} and its styles from {Colors::STYLES}; no styles
129
+ # prints it plain. Elements left out keep their defaults. `heading` also
130
+ # takes `case:`, one of {HEADING_CASES}. A block taking an argument
131
+ # receives the declarations instead.
132
+ #
133
+ # @example
134
+ # styles do
135
+ # heading :bold, :blue, case: :Capitalize
136
+ # example_comment # plain
137
+ # end
126
138
  #
127
- # @param element [Symbol] a key of {STYLES}
128
- # @param names [Array<Symbol>] names from {Colors::STYLES}
129
- # @return [Array<Symbol>]
130
- def style(element, *names)
131
- raise ArgumentError, "#{element.inspect} is not a styled element" unless STYLES.key?(element)
132
-
133
- unknown = names - Colors::STYLES
134
- raise ArgumentError, "unknown styles: #{unknown.inspect}" unless unknown.empty?
135
-
136
- @values[:styles] = @values.fetch(:styles, {}).merge(element => names.freeze)
137
- names
138
- end
139
-
140
139
  # @return [Hash{Symbol => Array<Symbol>}] every element's styles
141
- def styles
140
+ def styles(&block)
141
+ if block
142
+ sheet = StyleSheet.new
143
+ block.arity == 1 ? yield(sheet) : sheet.instance_eval(&block)
144
+ @values[:styles] = @values.fetch(:styles, {}).merge(sheet.styles)
145
+ @values[:heading_case] = sheet.heading_case if sheet.heading_case
146
+ end
142
147
  STYLES.merge(@values.fetch(:styles, {}))
143
148
  end
144
149
 
150
+ # How every heading is cased, set by `heading ..., case:` in {#styles}.
151
+ #
152
+ # @return [Symbol] one of {HEADING_CASES}
153
+ def heading_case
154
+ @values.fetch(:heading_case, DEFAULT_HEADING_CASE)
155
+ end
156
+
145
157
  # List commands under a heading of their own, in the order given.
146
158
  # Groups print in the order they are declared, after ungrouped commands.
147
159
  #
@@ -206,27 +218,6 @@ module Dry
206
218
  [columns, MIN_WIDTH].max
207
219
  end
208
220
 
209
- # These settings with another instance's laid over them. Headings and
210
- # styles merge key by key, hidden sections add up, and any other setting
211
- # the other instance made replaces this one's.
212
- #
213
- # @param other [Configuration]
214
- # @return [Configuration] a new instance
215
- def merge(other)
216
- combined = @values.merge(other.values) do |key, mine, theirs|
217
- case key
218
- when :headings, :styles then mine.merge(theirs)
219
- when :hidden then mine | theirs
220
- else theirs
221
- end
222
- end
223
- self.class.new(combined)
224
- end
225
-
226
- protected
227
-
228
- attr_reader :values
229
-
230
221
  private
231
222
 
232
223
  def validate_sections(names)
@@ -235,6 +226,48 @@ module Dry
235
226
 
236
227
  names.freeze
237
228
  end
229
+
230
+ # What a {Configuration#styles} block runs against: one method per
231
+ # element, each validating what it is given.
232
+ class StyleSheet
233
+ # @return [Hash{Symbol => Array<Symbol>}] the elements declared
234
+ attr_reader :styles
235
+
236
+ # @return [Symbol, nil] the heading case, when declared
237
+ attr_reader :heading_case
238
+
239
+ def initialize
240
+ @styles = {}
241
+ end
242
+
243
+ STYLES.each_key do |element|
244
+ define_method(element) do |*names, **options|
245
+ declare(element, names.flatten, options)
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ def declare(element, names, options)
252
+ unknown = names - Colors::STYLES
253
+ raise ArgumentError, "unknown styles for #{element}: #{unknown.inspect}" unless unknown.empty?
254
+
255
+ @heading_case = letter_case(element, options) unless options.empty?
256
+ # `heading case: :as_is` alone changes the case and keeps the styles.
257
+ @styles[element] = names.freeze unless names.empty? && !options.empty?
258
+ end
259
+
260
+ def letter_case(element, options)
261
+ unless element == :heading && options.keys == [:case]
262
+ raise ArgumentError, "#{element} takes no #{options.keys.map { "#{it}:" }.join(', ')}"
263
+ end
264
+
265
+ value = options.fetch(:case)
266
+ return value if HEADING_CASES.include?(value)
267
+
268
+ raise ArgumentError, "case cannot be #{value.inspect}; use one of #{HEADING_CASES.inspect}"
269
+ end
270
+ end
238
271
  end
239
272
  end
240
273
  end
@@ -49,12 +49,15 @@ module Dry
49
49
  title(config.headings.fetch(section))
50
50
  end
51
51
 
52
- # A heading with the configured case and style, for any text.
52
+ # A heading in the configured case and style, for any text.
53
+ #
54
+ # @param text [String]
53
55
  # @return [String]
54
56
  def title(text)
55
57
  cased = case config.heading_case
56
- when :upcase then text.upcase
57
- when :capitalize then text.sub(/\A\p{Ll}/, &:upcase)
58
+ when :UPPERCASE then text.upcase
59
+ when :lowercase then text.downcase
60
+ when :Capitalize then text.sub(/\A\p{Ll}/, &:upcase)
58
61
  else text
59
62
  end
60
63
  paint(cased, :heading)
@@ -20,7 +20,7 @@ module Dry
20
20
  def help(command, prog_name)
21
21
  screen = Screens::Command.new(
22
22
  command:, prog_name:, top_level: !kommand.nil?,
23
- config: Help.config_for(registry), io: out
23
+ config: Help.config, io: out
24
24
  )
25
25
  out.puts screen.render
26
26
  exit(0)
@@ -30,7 +30,7 @@ module Dry
30
30
  # all, a group without a command of its own, `-h` or `--help` at a
31
31
  # registry level, or a typo.
32
32
  def spell_checker(result, arguments)
33
- config = Help.config_for(registry)
33
+ config = Help.config
34
34
  unmatched = arguments.drop(result.names.length)
35
35
 
36
36
  if unmatched.empty?
@@ -50,34 +50,6 @@ module Dry
50
50
  exit(status)
51
51
  end
52
52
  end
53
-
54
- # Included into Dry::CLI::Registry, so every registry can describe itself.
55
- module RegistryMethods
56
- # Configure this registry's help. A block taking an argument receives
57
- # the configuration; any other block runs against it.
58
- #
59
- # @example
60
- # help do
61
- # title "Taxlibris"
62
- # width 100
63
- # end
64
- #
65
- # @return [Configuration] this registry's settings
66
- def help(&block)
67
- @help_config ||= Configuration.new
68
- if block&.arity == 1
69
- yield @help_config
70
- elsif block
71
- @help_config.instance_eval(&block)
72
- end
73
- @help_config
74
- end
75
-
76
- # @return [Configuration, nil] nil until {#help} is called
77
- def help_config
78
- @help_config
79
- end
80
- end
81
53
  end
82
54
  end
83
55
  end
@@ -32,10 +32,9 @@ module Dry
32
32
  end
33
33
 
34
34
  def render_usage
35
- program = format.paint(prog_name, :command)
36
- lines = ["#{INDENT}#{program}#{usage_arguments} [OPTIONS]"]
37
- lines << "#{INDENT}#{program} COMMAND [OPTIONS]" if subcommand_rows.any?
38
- section(:usage, lines)
35
+ lines = ["#{prog_name}#{usage_arguments} [OPTIONS]"]
36
+ lines << "#{prog_name} COMMAND [OPTIONS]" if subcommand_rows.any?
37
+ section(:usage, lines.map { INDENT + format.paint(it, :usage) })
39
38
  end
40
39
 
41
40
  def render_description
@@ -59,7 +58,8 @@ module Dry
59
58
  def render_examples
60
59
  rows = command.examples.map do |example|
61
60
  line, comment = example.split(" # ", 2)
62
- Row.new(term: "#{prog_name} #{line.strip}", text: comment&.strip, text_style: :comment)
61
+ Row.new(term: "#{prog_name} #{line.strip}", text: comment&.strip,
62
+ term_style: :example, text_style: :example_comment)
63
63
  end
64
64
  section(:examples, format.definitions(rows, format.column_for(rows)))
65
65
  end
@@ -72,7 +72,7 @@ module Dry
72
72
  required = command.required_arguments.map { argument_name(it) }
73
73
  optional = command.optional_arguments.map { "[#{argument_name(it)}]" }
74
74
  names = [*required, *optional]
75
- " #{format.paint(names.join(' '), :argument)}" unless names.empty?
75
+ " #{names.join(' ')}" unless names.empty?
76
76
  end
77
77
 
78
78
  def subcommand_rows
@@ -32,8 +32,8 @@ module Dry
32
32
  end
33
33
 
34
34
  def render_usage
35
- program = format.paint(ProgramName.call(result.names), :command)
36
- section(:usage, ["#{INDENT}#{program} COMMAND [OPTIONS]"])
35
+ usage = format.paint("#{ProgramName.call(result.names)} COMMAND [OPTIONS]", :usage)
36
+ section(:usage, ["#{INDENT}#{usage}"])
37
37
  end
38
38
 
39
39
  def render_commands
@@ -9,7 +9,7 @@ module Dry
9
9
  # inherits from Object, so an empty reopening is compatible either way.
10
10
  class CLI
11
11
  module Help
12
- VERSION = "0.1.0"
12
+ VERSION = "0.5.0"
13
13
  end
14
14
  end
15
15
  end
data/lib/dry/cli/help.rb CHANGED
@@ -19,14 +19,22 @@ module Dry
19
19
  # Configurable, wrapped and colored help screens for dry-cli.
20
20
  #
21
21
  # Requiring this file changes the help every Dry::CLI in the process prints.
22
- # Settings come from {.configure} for the whole process, and from a
23
- # registry's `help` block for that registry.
22
+ # Every setting is made once, in {.configure}, so the way dry-cli declares
23
+ # registries, commands and options stays exactly as dry-cli ships it.
24
24
  module Help
25
25
  class << self
26
- # @yieldparam config [Configuration] the process-wide settings
26
+ # Make every setting. A block taking an argument receives the
27
+ # configuration; any other block runs against it.
28
+ #
29
+ # @example
30
+ # Dry::CLI::Help.configure do
31
+ # title "MyCLI"
32
+ # styles { heading :bold, :blue }
33
+ # end
34
+ #
27
35
  # @return [Configuration]
28
- def configure
29
- yield config
36
+ def configure(&block)
37
+ block.arity == 1 ? yield(config) : config.instance_eval(&block)
30
38
  config
31
39
  end
32
40
 
@@ -40,21 +48,9 @@ module Dry
40
48
  def reset!
41
49
  @config = nil
42
50
  end
43
-
44
- # The settings a registry renders with: its own `help` block over the
45
- # process-wide settings. A single command passed to Dry::CLI.new has no
46
- # `help` block, so it renders with the process-wide settings alone.
47
- #
48
- # @param registry [Module, Class, Dry::CLI::Command, nil]
49
- # @return [Configuration]
50
- def config_for(registry)
51
- own = registry.help_config if registry.respond_to?(:help_config)
52
- own ? config.merge(own) : config
53
- end
54
51
  end
55
52
  end
56
53
  end
57
54
  end
58
55
 
59
56
  Dry::CLI.prepend(Dry::CLI::Help::Integration::CLIMethods)
60
- Dry::CLI::Registry.include(Dry::CLI::Help::Integration::RegistryMethods)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-cli-help
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul
@@ -15,20 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 1.1.1
19
- - - "<"
20
- - !ruby/object:Gem::Version
21
- version: '2'
18
+ version: '0'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
23
  - - ">="
27
24
  - !ruby/object:Gem::Version
28
- version: 1.1.1
29
- - - "<"
30
- - !ruby/object:Gem::Version
31
- version: '2'
25
+ version: '0'
32
26
  - !ruby/object:Gem::Dependency
33
27
  name: pastel
34
28
  requirement: !ruby/object:Gem::Requirement
@@ -56,7 +50,6 @@ files:
56
50
  - LICENSE.txt
57
51
  - README.md
58
52
  - Rakefile
59
- - SPECIFICATION.md
60
53
  - lib/dry-cli-help.rb
61
54
  - lib/dry/cli/help.rb
62
55
  - lib/dry/cli/help/colors.rb
data/SPECIFICATION.md DELETED
@@ -1,200 +0,0 @@
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.