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 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
+ [![Ruby](https://github.com/kigster/dry-cli-help/actions/workflows/main.yml/badge.svg)](https://github.com/kigster/dry-cli-help/actions/workflows/main.yml) ![Coverage](docs/img/badge.svg)
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