dry-cli-ui 0.6.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +246 -18
- data/examples/Gemfile.lock +20 -7
- data/lib/dry/cli/ui/configuration.rb +27 -2
- data/lib/dry/cli/ui/console.rb +31 -4
- data/lib/dry/cli/ui/flags.rb +143 -0
- data/lib/dry/cli/ui/invocation.rb +100 -0
- data/lib/dry/cli/ui/logging.rb +192 -0
- data/lib/dry/cli/ui/report.rb +16 -0
- data/lib/dry/cli/ui/reporting.rb +134 -0
- data/lib/dry/cli/ui/terminal.rb +19 -2
- data/lib/dry/cli/ui/version.rb +1 -1
- data/lib/dry/cli/ui/widgets/legend.rb +48 -0
- data/lib/dry/cli/ui/widgets/multi.rb +20 -3
- data/lib/dry/cli/ui/widgets/multi_progress.rb +36 -28
- data/lib/dry/cli/ui/widgets/outcome.rb +4 -2
- data/lib/dry/cli/ui/widgets/progress.rb +291 -43
- data/lib/dry/cli/ui/widgets/prompt.rb +6 -2
- data/lib/dry/cli/ui/widgets.rb +1 -0
- data/lib/dry/cli/ui.rb +76 -10
- data/sig/dry/cli/ui.rbs +91 -2
- metadata +49 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef89bfadc9ccc7f7d737abdf585a9dfa57780b085dc12635be3149c623a965ea
|
|
4
|
+
data.tar.gz: e91a1f2ad8f9ebc4f174f7df2627fba76db005fc3d278b3f50d63d760c39c53f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3b9332b0cc87c0fa50a8bae398492c5994edf410168be89ae41c6ecf41104cc0d3f664257a890e7a066faad0ffa19790716b1fa74d35872c89bc3ad3c8e0107
|
|
7
|
+
data.tar.gz: a8985df9aa1c143c538557c9195dec1d060d3f54263b5041246810c2203007ddd30b2e246abdffea467cd21fae37f6db3081d21d317fc843401869ede4518b29
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
- `ui` reads the command's public `stdout`, `stderr` and `stdin`, the streams dry-cli was called with, when dry-cli has them. Prompts then read from the command's `stdin`.
|
|
4
|
+
- Under released dry-cli (1.4 and earlier), `ui` writes to the command's protected `out` and `err`, as before, and reads prompts from `$stdin`. dry-cli with public command streams is no longer required.
|
|
5
|
+
- `ui` builds its console again when those streams change, so a command registered as an instance, or run in-process by `Dry::CLI::Launcher`, writes each call to that call's streams.
|
|
6
|
+
- `ui_options` configures the console `ui` builds, in place of overriding `ui`.
|
|
7
|
+
- CI runs the suite against both kigster/dry-cli and the latest dry-cli release, and against the latest release every week.
|
|
8
|
+
- Reserved flags: `extend Dry::CLI::UI::Flags` and `flags :dry_run, :yes, :output, :log` give a command `-n/--dry-run`, `-y/--yes`, `-o/--output [FILE]`, `-l/--log [FILE]`, `-L/--log-level LEVEL` and `--log-format FORMAT`, the same in every CLI. Loading `Flags` prepends a hook onto `Dry::CLI` that lets `-o` and `-l` go without a file, for those commands only; `Flags.arguments(argv)` is the rewrite on its own.
|
|
9
|
+
- `ui.output(output) { |io| ... }` writes the command's report (what goes to `out`) to `log/<executable>-<action>.<YYYY-MM-DD>.<HHMMSS>.log` at the repository root for a bare `-o`, or to the file given, in colour, ending with the time it was closed. Spinners, bars and prompts stay on `err`.
|
|
10
|
+
- `ui.logging(log, level:, format:) { ... }` logs through SemanticLogger to `log/<executable>-<action>.log`, a file, or `out` while the block runs. `ui.logger` is the command's logger, and `ui.log_exception(e)` logs an exception, with each frame's local variables when it was raised at `-L debug`. `ui.with_flags(**options) { |io| ... }` does both.
|
|
11
|
+
- `ui.confirm` takes `yes:`, which answers it without asking, and on a terminal offers YES and NO as a list.
|
|
12
|
+
- A stream answering `color?` with true gets colour although it is no TTY. `Terminal#inspect` leaves out the environment.
|
|
13
|
+
- New dependencies: `semantic_logger`, `logger` (which semantic_logger needs on Ruby 4.0 and does not declare) and `binding_of_caller`. They load the first time a command logs.
|
|
14
|
+
- A progress handle counts each unit as an outcome: `bar.advance(step = 1, as: :ok)` takes `:ok`, `:aux` (relevant but auxiliary) or `:failed`, and `bar.counts` returns `{ ok:, aux:, failed: }`. Plain `advance(n)` counts as `:ok`, as before. Works in `ui.progress` and in every row of `ui.multi_progress`.
|
|
15
|
+
- A bar draws its failed units first, in red, then its auxiliary ones, in yellow, then the rest in its own colour, each part as wide as its share. A bar with neither draws as before. `Dry::CLI::UI.configure` takes `bar_failed_color` and `bar_aux_color`.
|
|
16
|
+
- A bar ends `✓` even when every unit failed, and its outcome line adds the breakdown after the time: `✓ Placing forms 100/100 (2.1s) 20 failed, 5 auxiliary`. The `multi_progress` headline, when it counts units, adds the sum of its rows'.
|
|
17
|
+
- A bar whose total is known and 0 when its job ends now ends `𝘅 label 0/0: nothing to process`, unless the job called `bar.fail` with a reason of its own. Before, it ended `✓`. In `multi_progress` the headline then ends `𝘅` too.
|
|
18
|
+
- `ui.progress` turns a spinner before its label while it runs, as each `multi_progress` row does, and draws the bar itself rather than through `TTY::ProgressBar`. Piped output is unchanged.
|
|
19
|
+
- `ui.legend(failed:, aux:, ok:)` prints one line to `err` saying what each colour of a bar means: `Color Mapping: [ red: errors and invalid files | yellow: relevant but auxiliary | green: forms ]`, or each label on its colour's background on a terminal.
|
|
20
|
+
|
|
21
|
+
## [0.6.1]
|
|
22
|
+
|
|
23
|
+
- A progress handle takes `fail(reason = nil)`, as a `Line` does: the bar ends `𝘅 label 3/3: reason` without raising, in `ui.progress` and in a row of `ui.multi_progress`, where the jobs after it still run and the headline ends `𝘅`. For work that carries on after some of its units failed.
|
|
24
|
+
|
|
1
25
|
## [0.6.0]
|
|
2
26
|
|
|
3
27
|
- `m.spinner(label)` inside `ui.multi_progress` declares a row with a spinner in place of a bar, for a phase whose size is never known, so one widget can show phases of known and unknown size together. The row shows its `Line`'s detail while it runs and ends `[✓] label`; it counts as a job and as no units on the headline.
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ A long-running command has more to say than `puts` can show well: what it is doi
|
|
|
15
15
|
gem "dry-cli-ui"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Requires Ruby 4.0 or later and `dry-cli` 1.0 or later. The rendering comes from the [TTY toolkit](https://ttytoolkit.org) (`tty-box`, `tty-spinner`, `tty-progressbar`, `tty-table`, `tty-prompt`, `tty-cursor`, `tty-screen`), `pastel`, `strings` and `concurrent-ruby`,
|
|
18
|
+
Requires Ruby 4.0 or later and `dry-cli` 1.0 or later. The rendering comes from the [TTY toolkit](https://ttytoolkit.org) (`tty-box`, `tty-spinner`, `tty-progressbar`, `tty-table`, `tty-prompt`, `tty-cursor`, `tty-screen`), `pastel`, `strings` and `concurrent-ruby`, and the [reserved flags](#reserved-flags) log through `semantic_logger` (with `logger`) and `binding_of_caller`. Bundler installs them all with the gem.
|
|
19
19
|
|
|
20
20
|
Either require works, and both load the same file:
|
|
21
21
|
|
|
@@ -86,7 +86,9 @@ class Import < ApplicationCommand
|
|
|
86
86
|
end
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
`ui` writes to the command's `
|
|
89
|
+
`ui` writes to the command's `stdout` and `stderr`, and reads prompt answers from its `stdin`: the streams dry-cli was called with. Outside a dry-cli command it uses `$stdout`, `$stderr` and `$stdin`. The console is built again when those streams change, so a command registered as an instance writes each call to that call's streams.
|
|
90
|
+
|
|
91
|
+
Released dry-cli, up to 1.4, gives a command only `out` and `err`. There `ui` writes to those and reads prompt answers from `$stdin`; `Dry::CLI.new(registry).call(out: io, err: io)` still captures everything. `stdin:`, `stdout:`, `stderr:` and `Dry::CLI::Launcher` need dry-cli with public command streams ([dry-rb/dry-cli#175](https://github.com/dry-rb/dry-cli/pull/175) to [#176](https://github.com/dry-rb/dry-cli/pull/176)).
|
|
90
92
|
|
|
91
93
|
### Without dry-cli
|
|
92
94
|
|
|
@@ -252,9 +254,9 @@ ui.progress("Importing rules", total: rules.size) do |bar|
|
|
|
252
254
|
end
|
|
253
255
|
```
|
|
254
256
|
|
|
255
|
-
The bar shows percent, `current/total` and ETA,
|
|
257
|
+
The bar shows a turning spinner, percent, `current/total` and ETA, `⠋ Importing rules [◼◼◼◼◼◼ ] 61% 1159/1900 ETA 2.7s`, and ends with `✓ Importing rules 1900/1900 (4.2s)`. On a terminal the `◼`s are green, between brackets, on no background; see [Configuration](#configuration) to change either.
|
|
256
258
|
|
|
257
|
-
The block is given a `Dry::CLI::UI::Widgets::Progress::Handle`, never
|
|
259
|
+
The block is given a `Dry::CLI::UI::Widgets::Progress::Handle`, never a TTY toolkit object:
|
|
258
260
|
|
|
259
261
|
```ruby
|
|
260
262
|
ui.progress("Copying", total: files.sum(&:size)) do |bar|
|
|
@@ -266,15 +268,72 @@ ui.progress("Copying", total: files.sum(&:size)) do |bar|
|
|
|
266
268
|
end
|
|
267
269
|
```
|
|
268
270
|
|
|
269
|
-
- `advance(step = 1)` adds to `current` and returns the handle; `current` never passes `total`.
|
|
271
|
+
- `advance(step = 1, as: :ok)` adds to `current` and returns the handle; `current` never passes `total`. `as:` counts the units as `:ok`, `:aux` or `:failed`; see [Failed and auxiliary units](#failed-and-auxiliary-units).
|
|
270
272
|
- `total = n` sets the total once the work finds out, such as a download learning its size, and lowers `current` to fit. Anything but a non-negative Integer raises `ArgumentError`.
|
|
271
273
|
- `total:` must be a non-negative Integer, or `progress` raises `ArgumentError`.
|
|
272
|
-
- `total: 0` draws no bar, and ends
|
|
274
|
+
- `total: 0` draws no bar, and ends `𝘅 Copying 0/0: nothing to process`, since there was nothing to do. A block that calls `bar.fail(reason)` first keeps its own reason.
|
|
273
275
|
- `color:` paints this bar's finished part in any Pastel style, such as `color: :red`, instead of the configured `bar_color`. A style Pastel does not know raises `ArgumentError` before the block runs.
|
|
274
|
-
- The outcome is `✓` whenever the block returns, even short of the total (`✓ Copying 12/20`), and `𝘅` when it raises.
|
|
276
|
+
- The outcome is `✓` whenever the block returns, even short of the total (`✓ Copying 12/20`) and even when every unit failed, and `𝘅` when it raises or calls `bar.fail`.
|
|
275
277
|
|
|
276
278
|
Piped, it prints `Importing rules...` when it starts and the outcome line when it ends, with no bar in between.
|
|
277
279
|
|
|
280
|
+
### Failed and auxiliary units
|
|
281
|
+
|
|
282
|
+
Work that goes through many files rarely succeeds on all of them. `bar.advance(as:)` counts each unit as one of three outcomes:
|
|
283
|
+
|
|
284
|
+
- `:ok`, the default: the unit is what the work is for, such as a form placed.
|
|
285
|
+
- `:aux`: relevant but auxiliary, such as a scaffold created or an instruction filed elsewhere.
|
|
286
|
+
- `:failed`: an error, or an invalid file.
|
|
287
|
+
|
|
288
|
+
Anything else raises `ArgumentError`. `bar.counts` returns the units so far by outcome, `{ ok: 7, aux: 1, failed: 2 }`, adding up to `bar.current`.
|
|
289
|
+
|
|
290
|
+
The bar's finished part is drawn left to right in red for the failed units, yellow for the auxiliary ones, and the bar's own colour for the rest, each part as wide as its share of the units done: half way through, with a fifth of the files failed, the first fifth of the filled part is red and the rest green. The bar ends with `✓` and the breakdown after the time, with the numbers in their colours on a terminal. `ui.legend` prints a line saying what each colour means; call it before the first bar. Each of `failed:`, `aux:` and `ok:` is optional, and a label not given is left out. On a terminal each label is drawn on its colour's background.
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
ui.legend(failed: "errors and invalid files", aux: "relevant but auxiliary", ok: "forms")
|
|
294
|
+
|
|
295
|
+
ui.progress("Placing forms", total: files.size) do |bar|
|
|
296
|
+
files.each do |file|
|
|
297
|
+
case place(file)
|
|
298
|
+
in :placed then bar.advance
|
|
299
|
+
in :scaffold then bar.advance(as: :aux)
|
|
300
|
+
in :invalid then bar.advance(as: :failed)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Piped, with two invalid files and one scaffold among ten:
|
|
307
|
+
|
|
308
|
+
```text
|
|
309
|
+
Color Mapping: [ red: errors and invalid files | yellow: relevant but auxiliary | green: forms ]
|
|
310
|
+
Placing forms...
|
|
311
|
+
✓ Placing forms 10/10 (0.0s) 2 failed, 1 auxiliary
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`m.progress` inside `ui.multi_progress` takes the same `as:`. Each row ends with its own breakdown, and the headline, when it counts units, with the sum of them all. A row whose total is 0 when its job ends fails with `nothing to process`, and so the headline ends `𝘅`:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
ui.multi_progress("Placing forms", concurrent: false) do |m|
|
|
318
|
+
m.progress("us", total: federal.size) { |bar| federal.each { bar.advance(as: place(it) == :invalid ? :failed : :ok) } }
|
|
319
|
+
states.each do |state, forms|
|
|
320
|
+
m.progress(state, total: forms.size) { |bar| forms.each { bar.advance(as: :failed) } }
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Piped, where `us-ny` has no forms:
|
|
326
|
+
|
|
327
|
+
```text
|
|
328
|
+
Placing forms...
|
|
329
|
+
[✓] us 6/6 (0.0s) 2 failed
|
|
330
|
+
[✓] us-ca 2/2 (0.0s) 2 failed
|
|
331
|
+
[𝘅] us-ny 0/0: nothing to process (0.0s)
|
|
332
|
+
𝘅 Placing forms 8/8 (0.0s) 4 failed
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The colours are `bar_failed_color` and `bar_aux_color` in the [Configuration](#configuration).
|
|
336
|
+
|
|
278
337
|
### Several spinners at once
|
|
279
338
|
|
|
280
339
|
```ruby
|
|
@@ -358,7 +417,7 @@ The same shape as `multi_spinner`, with a bar per job and a headline bar that co
|
|
|
358
417
|
└─ [ ] video.mp4
|
|
359
418
|
```
|
|
360
419
|
|
|
361
|
-
Each job is given the same handle as `ui.progress`, with `advance(step = 1)`, `current` and `total`. `m.progress` takes `color:` as `ui.progress` does, so bars side by side can differ:
|
|
420
|
+
Each job is given the same handle as `ui.progress`, with `advance(step = 1, as: :ok)`, `counts`, `current` and `total`. `m.progress` takes `color:` as `ui.progress` does, so bars side by side can differ:
|
|
362
421
|
|
|
363
422
|
```ruby
|
|
364
423
|
ui.multi_progress("Probing #{hosts.size} hosts", total: hosts.size) do |m|
|
|
@@ -424,6 +483,30 @@ Generating text...
|
|
|
424
483
|
|
|
425
484
|
`m.spinner` without a block raises `ArgumentError`.
|
|
426
485
|
|
|
486
|
+
A job that carries on after some of its units failed ends its row with `bar.fail(reason)` rather than raising, so the jobs after it still run. Its row ends `[𝘅] label 3/3: reason`, the headline ends `𝘅`, and the call returns as usual. `ui.progress` takes the same `bar.fail`.
|
|
487
|
+
|
|
488
|
+
```ruby
|
|
489
|
+
ui.multi_progress("Generating text", concurrent: false, count: :jobs) do |m|
|
|
490
|
+
m.spinner("Finding PDFs") { pdfs = find_pdfs }
|
|
491
|
+
m.progress("Extracting", total: nil) do |bar|
|
|
492
|
+
bar.total = pdfs.size
|
|
493
|
+
failed = pdfs.count { |pdf| !extract(pdf).tap { bar.advance } }
|
|
494
|
+
bar.fail("#{failed} failed") if failed.positive?
|
|
495
|
+
end
|
|
496
|
+
m.spinner("Indexing") { index }
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Piped, with one PDF that would not extract:
|
|
501
|
+
|
|
502
|
+
```text
|
|
503
|
+
Generating text...
|
|
504
|
+
[✓] Finding PDFs (0.0s)
|
|
505
|
+
[𝘅] Extracting 3/3: 1 failed (0.0s)
|
|
506
|
+
[✓] Indexing (0.0s)
|
|
507
|
+
𝘅 Generating text 3/3 (0.0s)
|
|
508
|
+
```
|
|
509
|
+
|
|
427
510
|
Piped:
|
|
428
511
|
|
|
429
512
|
```text
|
|
@@ -699,7 +782,7 @@ ui.confirm("Deploy to #{env}?", default: false)
|
|
|
699
782
|
|
|
700
783
|
- `prompt` returns the answer as a String, or the default for an empty answer.
|
|
701
784
|
- With an Array of `choices:`, it returns the chosen name; with a Hash, the value the chosen name maps to (`:pro` above). `default:` is the name of a choice.
|
|
702
|
-
- `confirm` returns `true` or `false`, and `default:` is `false` unless given.
|
|
785
|
+
- `confirm` returns `true` or `false`, and `default:` is `false` unless given. On a terminal it is a list to pick YES or NO from, starting on the default. `yes: true`, which is what the reserved `-y/--yes` flag gives (see [Reserved flags](#reserved-flags)), returns `true` without asking.
|
|
703
786
|
|
|
704
787
|
When both standard input and `err` are terminals, these use arrow-key menus and line editing (TTY::Prompt). Otherwise they print the question to `err` and read lines from standard input, so answers can be piped:
|
|
705
788
|
|
|
@@ -750,10 +833,145 @@ Errors raised inside a block are never swallowed: the widget marks itself failed
|
|
|
750
833
|
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
751
834
|
| `info`, `success`, `box`, `table`, `status` at those levels | `debug`, `warn`, `error`, `fatal`, `popup`, spinners, progress bars, their multi forms, task trees, the status bar, prompts |
|
|
752
835
|
|
|
753
|
-
`mycli export > rules.csv` therefore writes only the command's results to the file, while its progress stays on the screen. `ui` writes to the streams dry-cli was called with, so `Dry::CLI.new(registry).call(
|
|
836
|
+
`mycli export > rules.csv` therefore writes only the command's results to the file, while its progress stays on the screen. `ui` writes to the streams dry-cli was called with, so `Dry::CLI.new(registry).call(stdout: io, stderr: io)` captures everything.
|
|
754
837
|
|
|
755
838
|
A stream that is not a terminal, or runs under `TERM=dumb`, gets no animation, no cursor movement and no escape codes. [`NO_COLOR`](https://no-color.org) turns colour off and leaves animation on.
|
|
756
839
|
|
|
840
|
+
## Reserved flags
|
|
841
|
+
|
|
842
|
+
A few flags mean the same thing in every CLI built on this gem. A command takes the ones it needs:
|
|
843
|
+
|
|
844
|
+
| Flag | Long | Reaches `call` as | Meaning |
|
|
845
|
+
| ---- | --------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
846
|
+
| `-n` | `--dry-run` | `dry_run:` | change nothing, print what would happen |
|
|
847
|
+
| `-y` | `--yes` | `yes:` | skip every interactive prompt |
|
|
848
|
+
| `-o` | `--output [FILE]` | `output:` | write the command's report (tables, lists, summaries) to FILE, in colour |
|
|
849
|
+
| `-l` | `--log [FILE]` | `log:` | log to FILE through [SemanticLogger](https://logger.rocketjob.io) |
|
|
850
|
+
| `-L` | `--log-level LEVEL` | `log_level:` | `debug`, `info` (the default), `warn`, `error` or `fatal` |
|
|
851
|
+
| | `--log-format FORMAT` | `log_format:` | `standard` (the default, one line per entry), `json`, or any other SemanticLogger formatter, such as `logfmt` or `color` |
|
|
852
|
+
|
|
853
|
+
```ruby
|
|
854
|
+
require "dry/cli"
|
|
855
|
+
require "dry-cli-ui"
|
|
856
|
+
|
|
857
|
+
class Export < Dry::CLI::Command
|
|
858
|
+
include Dry::CLI::UI
|
|
859
|
+
extend Dry::CLI::UI::Flags
|
|
860
|
+
|
|
861
|
+
desc "Export the rules"
|
|
862
|
+
flags :dry_run, :yes, :output, :log # :log brings -L and --log-format with it
|
|
863
|
+
|
|
864
|
+
RULES = [["standard_deduction", 14_600], ["salt_cap", 10_000]].freeze
|
|
865
|
+
|
|
866
|
+
def call(dry_run:, yes:, **options)
|
|
867
|
+
ui.with_flags(**options) do |io|
|
|
868
|
+
ui.logger.info("Exporting", rules: RULES.size)
|
|
869
|
+
next ui.status("Would export #{RULES.size} rules", level: :warn) if dry_run
|
|
870
|
+
next unless ui.confirm("Export #{RULES.size} rules?", yes: yes)
|
|
871
|
+
|
|
872
|
+
ui.table(RULES, header: %w[Rule Amount])
|
|
873
|
+
io.puts "#{RULES.size} rules exported"
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
end
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
`flags` declares ordinary dry-cli options, so they show in the command's help, and an unknown name raises `ArgumentError`:
|
|
880
|
+
|
|
881
|
+
```text
|
|
882
|
+
Options:
|
|
883
|
+
--dry-run, -n # Change nothing; print what would happen, default: false
|
|
884
|
+
--yes, -y # Answer yes to every prompt, default: false
|
|
885
|
+
--output=VALUE, -o VALUE # Write the report to FILE: log/ by default, - for STDOUT
|
|
886
|
+
--log=VALUE, -l VALUE # Log to FILE: log/ by default, - for STDOUT
|
|
887
|
+
--log-level=VALUE, -L VALUE # Log level: (debug, info, warn, error, fatal), default: "info"
|
|
888
|
+
--log-format=VALUE # Log format: standard, json, or another SemanticLogger formatter, default: "standard"
|
|
889
|
+
--help, -h # Print this help
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
`-o` and `-l` may be given without a file. dry-cli requires a value for every option that takes one, so loading `Dry::CLI::UI::Flags` prepends a small hook onto `Dry::CLI` that, for the commands extending it and nothing else, turns a bare `-o`, `--output`, `-l` or `--log` into an empty value before dry-cli parses the line. A switch is bare when it comes last, or when the next argument starts with `-` and is not exactly `-`. `-o x`, `-o=x`, `--output=x`, `-ox` and `-o -` are left alone, as is everything after `--`. The rewrite is also a function of its own:
|
|
893
|
+
|
|
894
|
+
```ruby
|
|
895
|
+
Dry::CLI::UI::Flags.arguments(%w[export -o -l run.log]) # => ["export", "-o", "", "-l", "run.log"]
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
The hook also records the names a command was called by, which name its default files: `<executable>-<action>`, where the action is the command's names joined with `-` (`law-cli generate text` gives `law-cli-generate-text`).
|
|
899
|
+
|
|
900
|
+
### Where the report goes
|
|
901
|
+
|
|
902
|
+
`ui.output(output) { |io| ... }` sends what the block writes to `out` (tables, boxes and statuses at `info` and `success`) to the file `-o` named, and gives the block that file as `io` for writing to it directly. Spinners, bars, prompts and warnings stay on `err`, and so on the screen.
|
|
903
|
+
|
|
904
|
+
| `-o` given | `output:` | The report goes to |
|
|
905
|
+
| ------------------ | ----------------- | ------------------------------------------------------------------------------------------------ |
|
|
906
|
+
| not at all | `nil` | the command's `out`, as usual |
|
|
907
|
+
| `-o -` | `"-"` | the command's `out` |
|
|
908
|
+
| `-o` | `""` | `log/<executable>-<action>.<YYYY-MM-DD>.<HHMMSS>.log`, stamped with the time the process started |
|
|
909
|
+
| `-o reports/x.txt` | `"reports/x.txt"` | that file, relative to the current directory |
|
|
910
|
+
|
|
911
|
+
`log/` is at the repository root: the nearest directory up from the current one holding `.git`, else the current directory. It is created when missing; add `/log/*` to the repository's `.gitignore` once. A file is written in colour although it is no terminal (unless `NO_COLOR` is set), its last line says when it was closed, and a line on `err` says where it is:
|
|
912
|
+
|
|
913
|
+
```text
|
|
914
|
+
$ mycli export -y -o | cat
|
|
915
|
+
ℹ Report written to log/mycli-export.2026-10-08.031215.log
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
```text
|
|
919
|
+
$ cat log/mycli-export.2026-10-08.031215.log
|
|
920
|
+
┌────────────────────┬────────┐
|
|
921
|
+
│ Rule │ Amount │
|
|
922
|
+
├────────────────────┼────────┤
|
|
923
|
+
│ standard_deduction │ 14600 │
|
|
924
|
+
│ salt_cap │ 10000 │
|
|
925
|
+
└────────────────────┴────────┘
|
|
926
|
+
2 rules exported
|
|
927
|
+
Closed at 2026-10-08 03:12:15 -0700
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### Logging
|
|
931
|
+
|
|
932
|
+
`ui.logging(log, level: log_level, format: log_format) { ... }` adds a SemanticLogger appender while the block runs, then flushes and removes it. `ui.logger` is a SemanticLogger logger named after the command's class, and writes nothing until an appender is added.
|
|
933
|
+
|
|
934
|
+
| `-l` given | `log:` | The log goes to |
|
|
935
|
+
| ------------ | ----------- | -------------------------------------------- |
|
|
936
|
+
| not at all | `nil` | nowhere: no appender is added |
|
|
937
|
+
| `-l -` | `"-"` | the command's `out` |
|
|
938
|
+
| `-l` | `""` | `log/<executable>-<action>.log`, appended to |
|
|
939
|
+
| `-l run.log` | `"run.log"` | that file |
|
|
940
|
+
|
|
941
|
+
```text
|
|
942
|
+
$ mycli export -n -l - | cat
|
|
943
|
+
⚠ Would export 2 rules
|
|
944
|
+
2026-10-08 03:11:21.436756 I [22306:1056] Export -- Exporting -- {rules: 2}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
An unknown level or format raises `ArgumentError` naming the ones there are, as in `unknown log format "yaml", expected one of color, ecs, fluentd, json, logfmt, ...`.
|
|
948
|
+
|
|
949
|
+
`ui.log_exception(e, message = nil, level: :error)` logs an exception with its backtrace. At `-L debug` the gem also records, for every exception raised while the block runs, the local variables of each frame it was raised through (with [binding_of_caller](https://github.com/banister/binding_of_caller)), and `log_exception` adds them as the payload's `locals`. Each value is its `inspect`, cut at 200 characters, for at most 25 frames, leaving out the gem's own. Recording costs time on every `raise`, which is why it happens at `debug` only. The values are whatever the program held, secrets included, so treat a debug log as you would a core dump.
|
|
950
|
+
|
|
951
|
+
```ruby
|
|
952
|
+
def check(amount, limit: 10_000)
|
|
953
|
+
raise ArgumentError, "over the cap" if amount > limit
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
ui.logging("-", level: "debug") do
|
|
957
|
+
check(14_600)
|
|
958
|
+
rescue ArgumentError => e
|
|
959
|
+
ui.log_exception(e, "Check failed")
|
|
960
|
+
end
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
```text
|
|
964
|
+
2026-10-08 03:11:32.024362 E [22511:792 reporting.rb:107] check.rb -- Check failed -- {locals: [{frame: "check.rb:4", locals: {amount: "14600", limit: "10000"}}, {frame: "check.rb:8", locals: {e: "nil"}}]} -- Exception: ArgumentError: over the cap
|
|
965
|
+
check.rb:4:in 'Object#check'
|
|
966
|
+
check.rb:8:in 'block in <main>'
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### All of them at once
|
|
970
|
+
|
|
971
|
+
`ui.with_flags(**options) { |io| ... }` is `ui.logging` around `ui.output`, taking the whole options hash `call` receives and ignoring what it does not use, as in the `Export` command above. `dry_run:` and `yes:` are the command's to act on: return before changing anything, and pass `yes:` to `ui.confirm`.
|
|
972
|
+
|
|
973
|
+
SemanticLogger and binding_of_caller load the first time a command logs, and binding_of_caller only at `debug`, so declaring the flags costs a command nothing at boot.
|
|
974
|
+
|
|
757
975
|
## Configuration
|
|
758
976
|
|
|
759
977
|
Spinners and bars look the same everywhere, and are set once for the whole process:
|
|
@@ -764,10 +982,12 @@ Dry::CLI::UI.configure do
|
|
|
764
982
|
bar_format(complete: "◼", incomplete: " ") # or any TTY::ProgressBar bar format name, such as :box
|
|
765
983
|
bar_color :green # the finished part: any Pastel style, or nil
|
|
766
984
|
bar_background nil # the whole bar: any Pastel style, or nil
|
|
985
|
+
bar_failed_color :red # units counted as: :failed: any Pastel style, or nil
|
|
986
|
+
bar_aux_color :yellow # units counted as: :aux: any Pastel style, or nil
|
|
767
987
|
end
|
|
768
988
|
```
|
|
769
989
|
|
|
770
|
-
Those are the defaults: spinners turn through `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏` ten times a second, and bars draw a green `◼` for each finished part, with nothing behind them, and brackets show where the bar begins and ends. Every spinner reads the same format, including `multi_spinner`, task trees and the status bar, and every bar reads the same characters and colours, except a bar given its own `color:`. The formats also take a definition of your own:
|
|
990
|
+
Those are the defaults: spinners turn through `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏` ten times a second, and bars draw a green `◼` for each finished part, with nothing behind them, and brackets show where the bar begins and ends. Units counted as failed or auxiliary are drawn red and yellow, and `ui.legend` names the same colours. Every spinner reads the same format, including `multi_spinner`, task trees and the status bar, and every bar reads the same characters and colours, except a bar given its own `color:`. The formats also take a definition of your own:
|
|
771
991
|
|
|
772
992
|
```ruby
|
|
773
993
|
Dry::CLI::UI.configure do |config|
|
|
@@ -805,20 +1025,18 @@ end
|
|
|
805
1025
|
|
|
806
1026
|
### Console options
|
|
807
1027
|
|
|
808
|
-
Override `
|
|
1028
|
+
Override `ui_options` to configure the console `ui` builds. The streams come from the command:
|
|
809
1029
|
|
|
810
1030
|
```ruby
|
|
811
1031
|
class ApplicationCommand < Dry::CLI::Command
|
|
812
1032
|
include Dry::CLI::UI
|
|
813
1033
|
|
|
814
|
-
def
|
|
815
|
-
|
|
816
|
-
out: out || $stdout,
|
|
817
|
-
err: err || $stderr,
|
|
1034
|
+
private def ui_options
|
|
1035
|
+
{
|
|
818
1036
|
box_width: 72, # boxes are 72 columns rather than the whole terminal
|
|
819
1037
|
color: nil, # true or false to override detection
|
|
820
1038
|
animate: nil # true or false to override detection
|
|
821
|
-
|
|
1039
|
+
}
|
|
822
1040
|
end
|
|
823
1041
|
end
|
|
824
1042
|
```
|
|
@@ -838,6 +1056,8 @@ Every option `Console.new` takes:
|
|
|
838
1056
|
| `clock:` | monotonic clock | any object whose `call` returns seconds, for elapsed times |
|
|
839
1057
|
| `config:` | `Dry::CLI::UI.config` | a `Dry::CLI::UI::Configuration` for this console alone |
|
|
840
1058
|
|
|
1059
|
+
`Console.new` also takes `invocation:`, a `Dry::CLI::UI::Invocation` naming the files `-o` and `-l` write (see [Reserved flags](#reserved-flags)); `ui` builds one from the command, and a console given none names them after the program alone.
|
|
1060
|
+
|
|
841
1061
|
`config:` gives one console a look of its own without changing the process-wide one:
|
|
842
1062
|
|
|
843
1063
|
```ruby
|
|
@@ -862,7 +1082,15 @@ err.string # => "Loading...\n✓ Loading (0.0s)\nContinue? (y/N) "
|
|
|
862
1082
|
out.string # => the Success box
|
|
863
1083
|
```
|
|
864
1084
|
|
|
865
|
-
Through dry-cli, `Dry::CLI.new(registry).call(arguments: %w[import],
|
|
1085
|
+
Through dry-cli, `Dry::CLI.new(registry).call(arguments: %w[import], stdin: input, stdout: out, stderr: err)` gives every command's `ui` those streams.
|
|
1086
|
+
|
|
1087
|
+
To run the whole CLI in the same process as its specs, as Aruba's in-process launcher does, bind a `Dry::CLI::Launcher` to it. Each run gets its own streams, and exits are recorded rather than ending the process:
|
|
1088
|
+
|
|
1089
|
+
```ruby
|
|
1090
|
+
Launcher = Dry::CLI::Launcher[MyCLI::Commands]
|
|
1091
|
+
|
|
1092
|
+
Launcher.new(%w[import], StringIO.new("y\n"), out, err, kernel).execute!
|
|
1093
|
+
```
|
|
866
1094
|
|
|
867
1095
|
## Relationship to dry-cli-help
|
|
868
1096
|
|
data/examples/Gemfile.lock
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
dry-cli-ui (0.
|
|
4
|
+
dry-cli-ui (0.7.0)
|
|
5
|
+
binding_of_caller (~> 2.0)
|
|
5
6
|
concurrent-ruby (~> 1.3)
|
|
6
7
|
dry-cli (>= 1.0)
|
|
8
|
+
logger (~> 1.6)
|
|
7
9
|
pastel (~> 0.8)
|
|
10
|
+
semantic_logger (~> 5.1)
|
|
8
11
|
strings (~> 0.2)
|
|
9
12
|
tty-box (~> 0.7)
|
|
10
13
|
tty-cursor (~> 0.7)
|
|
@@ -17,21 +20,27 @@ PATH
|
|
|
17
20
|
GEM
|
|
18
21
|
remote: https://rubygems.org/
|
|
19
22
|
specs:
|
|
23
|
+
binding_of_caller (2.0.0)
|
|
24
|
+
debug_inspector (>= 1.2.0)
|
|
20
25
|
concurrent-ruby (1.3.8)
|
|
26
|
+
debug_inspector (1.2.0)
|
|
21
27
|
dry-cli (1.4.1)
|
|
22
|
-
dry-cli-autocomplete (0.
|
|
28
|
+
dry-cli-autocomplete (0.6.0)
|
|
23
29
|
dry-cli (>= 1.0)
|
|
24
30
|
dry-inflector (>= 1.0)
|
|
25
|
-
dry-cli-help (0.
|
|
26
|
-
dry-cli
|
|
31
|
+
dry-cli-help (0.6.0)
|
|
32
|
+
dry-cli
|
|
27
33
|
pastel (~> 0.8)
|
|
28
34
|
dry-inflector (1.3.1)
|
|
29
35
|
forwardable (1.4.0)
|
|
36
|
+
logger (1.7.0)
|
|
30
37
|
pastel (0.8.0)
|
|
31
38
|
tty-color (~> 0.5)
|
|
32
39
|
prime (0.1.4)
|
|
33
40
|
forwardable
|
|
34
41
|
singleton
|
|
42
|
+
semantic_logger (5.1.0)
|
|
43
|
+
concurrent-ruby (~> 1.0)
|
|
35
44
|
singleton (0.3.0)
|
|
36
45
|
strings (0.2.1)
|
|
37
46
|
strings-ansi (~> 0.2)
|
|
@@ -79,16 +88,20 @@ DEPENDENCIES
|
|
|
79
88
|
prime
|
|
80
89
|
|
|
81
90
|
CHECKSUMS
|
|
91
|
+
binding_of_caller (2.0.0) sha256=e53eebf8428a85587aede7b43a83e7419eb85b8b690938a96aa330d03052d517
|
|
82
92
|
bundler (4.1.0.beta1) sha256=8a2dac8ca276ff4eff5efc2c6457b0044080fc1e431534ad7c5cc6187232b800
|
|
83
93
|
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
|
|
94
|
+
debug_inspector (1.2.0) sha256=9bdfa02eebc3da163833e6a89b154084232f5766087e59573b70521c77ea68a2
|
|
84
95
|
dry-cli (1.4.1) sha256=b8015bb76c708aa8705a36faf694973e75eeeffca39b89c8e172dc6f66a7d874
|
|
85
|
-
dry-cli-autocomplete (0.
|
|
86
|
-
dry-cli-help (0.
|
|
87
|
-
dry-cli-ui (0.
|
|
96
|
+
dry-cli-autocomplete (0.6.0) sha256=4a8b904cc802168ad1657a941cdf8dbec1915b4c825a35eb049fb0ac037947ca
|
|
97
|
+
dry-cli-help (0.6.0) sha256=10def4049bdae8c19e7412f3cefc062829048dbfa3a42155788b333c381766c9
|
|
98
|
+
dry-cli-ui (0.7.0)
|
|
88
99
|
dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
|
|
89
100
|
forwardable (1.4.0) sha256=f1cd40cc9812937980e1c76f1aa053660990a7c9b6a98fc37d945468afcce838
|
|
101
|
+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
90
102
|
pastel (0.8.0) sha256=481da9fb7d2f6e6b1a08faf11fa10363172dc40fd47848f096ae21209f805a75
|
|
91
103
|
prime (0.1.4) sha256=4d755ebf7c2994a6f3a3fee0d072063be3fff2d4042ebff6cd5eebd4747a225e
|
|
104
|
+
semantic_logger (5.1.0) sha256=9a8170fc0212ad47ef29e0760219cf416cb5b9973061981733234f7148b6d6ea
|
|
92
105
|
singleton (0.3.0) sha256=83ea1bca5f4aa34d00305ab842a7862ea5a8a11c73d362cb52379d94e9615778
|
|
93
106
|
strings (0.2.1) sha256=933293b3c95cf85b81eb44b3cf673e3087661ba739bbadfeadf442083158d6fb
|
|
94
107
|
strings-ansi (0.2.0) sha256=90262d760ea4a94cc2ae8d58205277a343409c288cbe7c29416b1826bd511c88
|
|
@@ -14,6 +14,8 @@ module Dry
|
|
|
14
14
|
# bar_format :box # any TTY::ProgressBar bar format name
|
|
15
15
|
# bar_color :cyan # any Pastel style, or nil
|
|
16
16
|
# bar_background nil # any Pastel style, or nil
|
|
17
|
+
# bar_failed_color :red # any Pastel style, or nil
|
|
18
|
+
# bar_aux_color :yellow # any Pastel style, or nil
|
|
17
19
|
# end
|
|
18
20
|
#
|
|
19
21
|
# Dry::CLI::UI.configure do |config|
|
|
@@ -24,12 +26,15 @@ module Dry
|
|
|
24
26
|
# Anything not set reads from {DEFAULTS}.
|
|
25
27
|
class Configuration
|
|
26
28
|
# What a setting reads before it is set: a green `◼` for each finished
|
|
27
|
-
# part of a bar, with no background behind it
|
|
29
|
+
# part of a bar, with no background behind it, red for the units that
|
|
30
|
+
# failed and yellow for the auxiliary ones.
|
|
28
31
|
DEFAULTS = {
|
|
29
32
|
spinner_format: :dots,
|
|
30
33
|
bar_format: { complete: "◼", incomplete: " " }.freeze,
|
|
31
34
|
bar_color: :green,
|
|
32
|
-
bar_background: nil
|
|
35
|
+
bar_background: nil,
|
|
36
|
+
bar_failed_color: :red,
|
|
37
|
+
bar_aux_color: :yellow
|
|
33
38
|
}.freeze
|
|
34
39
|
|
|
35
40
|
# Every style name Pastel knows, for checking colour settings.
|
|
@@ -61,6 +66,14 @@ module Dry
|
|
|
61
66
|
# Reads the background the whole bar is drawn on, or sets it.
|
|
62
67
|
# @param value [Symbol, nil] a Pastel style, such as :on_blue; nil for none
|
|
63
68
|
# @return [Symbol, nil]
|
|
69
|
+
# @!method bar_failed_color(value = UNSET)
|
|
70
|
+
# Reads the colour a bar's failed units are drawn in, or sets it.
|
|
71
|
+
# @param value [Symbol, nil] a Pastel style, such as :red; nil for none
|
|
72
|
+
# @return [Symbol, nil]
|
|
73
|
+
# @!method bar_aux_color(value = UNSET)
|
|
74
|
+
# Reads the colour a bar's auxiliary units are drawn in, or sets it.
|
|
75
|
+
# @param value [Symbol, nil] a Pastel style, such as :yellow; nil for none
|
|
76
|
+
# @return [Symbol, nil]
|
|
64
77
|
DEFAULTS.each_key do |name|
|
|
65
78
|
define_method(name) do |value = UNSET|
|
|
66
79
|
return @values.fetch(name) { DEFAULTS.fetch(name) } if UNSET.equal?(value)
|
|
@@ -93,6 +106,18 @@ module Dry
|
|
|
93
106
|
@values[:bar_background] = style(:bar_background, value)
|
|
94
107
|
end
|
|
95
108
|
|
|
109
|
+
# @param value [Symbol, nil] see {#bar_failed_color}
|
|
110
|
+
# @raise [ArgumentError] for a style Pastel does not know
|
|
111
|
+
def bar_failed_color=(value)
|
|
112
|
+
@values[:bar_failed_color] = style(:bar_failed_color, value)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @param value [Symbol, nil] see {#bar_aux_color}
|
|
116
|
+
# @raise [ArgumentError] for a style Pastel does not know
|
|
117
|
+
def bar_aux_color=(value)
|
|
118
|
+
@values[:bar_aux_color] = style(:bar_aux_color, value)
|
|
119
|
+
end
|
|
120
|
+
|
|
96
121
|
# @return [Array<String>] the frames a spinner cycles through
|
|
97
122
|
def spinner_frames
|
|
98
123
|
frames = spinner_definition(spinner_format).fetch(:frames)
|
data/lib/dry/cli/ui/console.rb
CHANGED
|
@@ -24,6 +24,8 @@ module Dry
|
|
|
24
24
|
# end
|
|
25
25
|
# ui.success "Imported #{rules.size} rules"
|
|
26
26
|
class Console
|
|
27
|
+
include Reporting
|
|
28
|
+
|
|
27
29
|
# @!method debug(*paragraphs, width: nil)
|
|
28
30
|
# A grey "Debug" box on `err`.
|
|
29
31
|
# @param paragraphs [Array<#to_s>] each one wrapped on its own, separated by a blank line
|
|
@@ -65,10 +67,12 @@ module Dry
|
|
|
65
67
|
# @param box_width [Integer, nil] box width in columns; nil fills the terminal
|
|
66
68
|
# @param clock [#call] returns monotonic seconds
|
|
67
69
|
# @param config [Configuration] spinner and bar formats; {UI.config} by default
|
|
70
|
+
# @param invocation [Invocation, nil] names the report and log files; nil for the program alone
|
|
68
71
|
def initialize(out: $stdout, err: $stderr, input: $stdin, env: ENV, color: nil, animate: nil,
|
|
69
|
-
width: nil, box_width: nil, clock: Duration::CLOCK, config: UI.config)
|
|
72
|
+
width: nil, box_width: nil, clock: Duration::CLOCK, config: UI.config, invocation: nil)
|
|
70
73
|
@out = Terminal.new(out, env: env, color: color, animate: animate, width: width)
|
|
71
74
|
@err = Terminal.new(err, env: env, color: color, animate: animate, width: width)
|
|
75
|
+
@invocation = invocation
|
|
72
76
|
@input = input
|
|
73
77
|
@box_width = box_width
|
|
74
78
|
@clock = clock
|
|
@@ -184,6 +188,24 @@ module Dry
|
|
|
184
188
|
Widgets::Progress.new(err, clock: clock, config: config).run(label, total: total, color: color, &)
|
|
185
189
|
end
|
|
186
190
|
|
|
191
|
+
# Prints one line to `err` saying what each colour of a progress bar
|
|
192
|
+
# means, for a command to print before its first bar. Each label is
|
|
193
|
+
# optional; a label not given is left out. See {Widgets::Legend}.
|
|
194
|
+
#
|
|
195
|
+
# @example
|
|
196
|
+
# ui.legend(failed: "errors and invalid files", aux: "relevant but auxiliary", ok: "forms")
|
|
197
|
+
# # Color Mapping: [ red: errors and invalid files | yellow: relevant but auxiliary | green: forms ]
|
|
198
|
+
#
|
|
199
|
+
# @param failed [#to_s, nil] what the units counted `as: :failed` are
|
|
200
|
+
# @param aux [#to_s, nil] what the units counted `as: :aux` are
|
|
201
|
+
# @param ok [#to_s, nil] what the units counted `as: :ok` are
|
|
202
|
+
# @return [nil]
|
|
203
|
+
# @raise [ArgumentError] without any label
|
|
204
|
+
def legend(failed: nil, aux: nil, ok: nil) # rubocop:disable Naming/MethodParameterName
|
|
205
|
+
err.puts(Widgets::Legend.line(err, config, { failed: failed, aux: aux, ok: ok }))
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
187
209
|
# Runs several jobs at once, each with a progress bar of its own,
|
|
188
210
|
# beneath a headline bar that counts them all. Each job is given a
|
|
189
211
|
# {Widgets::Progress::Handle}. See {Widgets::MultiProgress}.
|
|
@@ -309,13 +331,18 @@ module Dry
|
|
|
309
331
|
prompter.ask(question, default: default, choices: choices)
|
|
310
332
|
end
|
|
311
333
|
|
|
312
|
-
# Asks a yes/no question.
|
|
334
|
+
# Asks a yes/no question: on a terminal, a list to pick YES or NO from. `yes: true`, which
|
|
335
|
+
# is what the reserved `-y/--yes` flag gives, answers it without asking.
|
|
336
|
+
#
|
|
337
|
+
# @example
|
|
338
|
+
# exit 1 unless ui.confirm("Drop the table?", yes: yes)
|
|
313
339
|
#
|
|
314
340
|
# @param question [String]
|
|
315
341
|
# @param default [Boolean]
|
|
342
|
+
# @param yes [Boolean] answer yes without asking
|
|
316
343
|
# @return [Boolean]
|
|
317
|
-
def confirm(question, default: false)
|
|
318
|
-
prompter.confirm(question, default: default)
|
|
344
|
+
def confirm(question, default: false, yes: false)
|
|
345
|
+
yes || prompter.confirm(question, default: default)
|
|
319
346
|
end
|
|
320
347
|
|
|
321
348
|
private
|