dry-cli-ui 0.3.1 → 0.4.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 +6 -0
- data/README.md +219 -18
- data/SPECIFICATION.md +71 -11
- data/lib/dry/cli/ui/configuration.rb +160 -0
- data/lib/dry/cli/ui/console.rb +76 -4
- data/lib/dry/cli/ui/status_bar.rb +314 -0
- data/lib/dry/cli/ui/terminal.rb +38 -2
- data/lib/dry/cli/ui/theme.rb +21 -6
- data/lib/dry/cli/ui/version.rb +1 -1
- data/lib/dry/cli/ui/widgets/multi.rb +300 -0
- data/lib/dry/cli/ui/widgets/multi_progress.rb +119 -0
- data/lib/dry/cli/ui/widgets/multi_spinner.rb +69 -0
- data/lib/dry/cli/ui/widgets/outcome.rb +1 -1
- data/lib/dry/cli/ui/widgets/pool.rb +82 -0
- data/lib/dry/cli/ui/widgets/progress.rb +41 -6
- data/lib/dry/cli/ui/widgets/spinner.rb +11 -3
- data/lib/dry/cli/ui/widgets/tasks.rb +17 -61
- data/lib/dry/cli/ui/widgets.rb +4 -0
- data/lib/dry/cli/ui.rb +32 -0
- data/sig/dry/cli/ui.rbs +25 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71d205edc6e78bba4bffbeebe70fc24ba7c992cd08a4f9856f1d9c1e4fd20ae4
|
|
4
|
+
data.tar.gz: 4070740b8964f52e87b33f2094cc5638d4323eeaad6882723c3593db5d1b77f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e053b8d527886759bbc2c72a8825d3e99ff0b5abe54211acc798ceab0b15238d0a0bd2442717d851d3c713e96117290100b8bd7e77a78a3f333cc3216b2d3dda
|
|
7
|
+
data.tar.gz: feeafcdddf3d3f620acf6e45b2fb77ef0cbc00b9bf1028b515f0d7d8a6c56a72a34a7c86e1e928320cb5755056ad72e3edafeaa96780b5dcdae023bb98d10b8b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- `ui.multi_spinner(title, concurrent: true)` runs several jobs at once, each under a spinner of its own, beneath a headline spinner. `ui.multi_progress(title, concurrent: true)` does the same with a progress bar per job and a headline bar that counts them all. The block declares the jobs with `m.spinner(label)` or `m.progress(label, total:)`; they run once it returns, all at once or at most `concurrent:` at a time, and the call returns what each job returned. Waiting jobs show `[ ]`, a job that raises is marked `[𝘅]`, jobs not yet started are skipped, and the first error is re-raised.
|
|
4
|
+
- Task trees and the multi widgets mark every row in brackets: `[ ]` while it waits and a turning `[⠏]` while it runs, both bold yellow, then a green `[✓]`, a red `[𝘅]`, or a yellow `[—]` for work that was skipped. Outcome lines use the same glyphs, so a failed spinner now ends `𝘅 Loading (0.5s)`.
|
|
5
|
+
- `Dry::CLI::UI.configure` sets `spinner_format` (a TTY::Spinner format name, or `{ interval:, frames: }`), `bar_format` (a TTY::ProgressBar bar format name, or `{ complete:, incomplete: }`), `bar_color` and `bar_background` (Pastel styles, or nil) for every spinner and bar. Spinners default to `:dots`. Bars now draw a green `◼` for each finished part on a gray track, between brackets, rather than `███░░░`. `Console.new` takes `config:` for a configuration of its own.
|
|
6
|
+
- `ui.status_bar(title, hints:)` keeps a status line at the bottom of the screen while its block runs: what started last, how many things are running, done and failed, overall progress, elapsed time and hints. Every widget inside the block reports to it. It sets no scroll region, so scrollback stays intact, and does nothing when `err` is not animated.
|
|
7
|
+
- `ui.multi_progress` right-aligns every count, so `8/503` and `1/8` end in the same column.
|
|
8
|
+
|
|
3
9
|
## [0.2.0]
|
|
4
10
|
|
|
5
11
|
- `ui.spinner` and every `ui.tasks` task give their block a `Dry::CLI::UI::Line`. `line.detail = "..."` shows text after the label while the work runs, redrawn in place on an animated terminal and never printed otherwise. `line.fail("reason")` ends the work as `✗ label: reason` without raising; the spinner still returns the block's value, and a task tree runs on past a task that fails this way.
|
data/README.md
CHANGED
|
@@ -51,7 +51,7 @@ When the output is piped, and the import fails part way:
|
|
|
51
51
|
Loading tax rules...
|
|
52
52
|
✓ Loading tax rules (0.3s)
|
|
53
53
|
Importing rules...
|
|
54
|
-
|
|
54
|
+
𝘅 Importing rules 1482/1900 (4.1s)
|
|
55
55
|
┌─ Error ──────────────────────────────────────────────────┐
|
|
56
56
|
│ │
|
|
57
57
|
│ Import failed │
|
|
@@ -62,7 +62,7 @@ Importing rules...
|
|
|
62
62
|
└──────────────────────────────────────────────────────────┘
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
On a terminal the spinner turns and the bar fills in place, with percent, count and ETA, and each is replaced by the same `✓` or
|
|
65
|
+
On a terminal the spinner turns and the bar fills in place, with percent, count and ETA, and each is replaced by the same `✓` or `𝘅` line when its block ends.
|
|
66
66
|
|
|
67
67
|
Include the module once in a base class and every command has `ui`. Including it loads nothing: the TTY gems load the first time `ui` is used.
|
|
68
68
|
|
|
@@ -107,7 +107,7 @@ ui.status "Disk nearly full", level: :warn # ⚠ Disk nearly full
|
|
|
107
107
|
rules = ui.spinner("Loading tax rules") { load_rules }
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
Returns the block's value. Leaves `✓ Loading tax rules (0.3s)` behind, or
|
|
110
|
+
Returns the block's value. Leaves `✓ Loading tax rules (0.3s)` behind, or `𝘅` and the re-raised error when the block fails.
|
|
111
111
|
|
|
112
112
|
The block is given a `Dry::CLI::UI::Line`, for work that has more to say while it runs, or that can fail without raising:
|
|
113
113
|
|
|
@@ -118,7 +118,7 @@ ui.spinner("Importing rules") do |line|
|
|
|
118
118
|
end
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
`line.detail = "..."` shows text after the label, redrawn in place as it changes. Piped, the detail is kept and never printed, since it can change many times a second. `line.fail(reason)` ends the spinner as
|
|
121
|
+
`line.detail = "..."` shows text after the label, redrawn in place as it changes. Piped, the detail is kept and never printed, since it can change many times a second. `line.fail(reason)` ends the spinner as `𝘅 Importing rules: 3 rules skipped (4.1s)` without raising, and the block's value is still returned. `line.failed?` and `line.reason` read it back. Every `Line` method is safe to call from any thread.
|
|
122
122
|
|
|
123
123
|
### Progress bars
|
|
124
124
|
|
|
@@ -131,7 +131,126 @@ ui.progress("Importing rules", total: rules.size) do |bar|
|
|
|
131
131
|
end
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
The bar shows percent, `current/total` and ETA, and ends with `✓ Importing rules 1900/1900 (4.2s)`.
|
|
134
|
+
The bar shows 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 and the whole bar sits on a gray background; see [Configuration](#configuration) to change either.
|
|
135
|
+
|
|
136
|
+
### Several spinners at once
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
ui.multi_spinner("Fetching", concurrent: 2) do |m|
|
|
140
|
+
m.spinner("fonts") { fetch(:fonts) }
|
|
141
|
+
m.spinner("images") do |line|
|
|
142
|
+
fetch(:images) { |done, all| line.detail = "#{done} of #{all}" }
|
|
143
|
+
end
|
|
144
|
+
m.spinner("video") { fetch(:video) }
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The block declares the jobs; they run once it returns, all at once by default, or at most `concurrent: 2` at a time. It returns what each job returned, in declaration order. While they run, every job has a row of its own under a headline spinner, and a job still waiting shows `[ ]`:
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
[⠹] Fetching
|
|
152
|
+
├─ [⠹] fonts
|
|
153
|
+
├─ [⠹] images 12 of 40
|
|
154
|
+
└─ [ ] video
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Once they finish, the headline ends `✓`, or `𝘅` when any job failed:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
[𝘅] Fetching (0.5s)
|
|
161
|
+
├─ [✓] fonts (0.3s)
|
|
162
|
+
├─ [𝘅] images: 2 timed out (0.3s)
|
|
163
|
+
└─ [✓] video (0.2s)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Each job is given a `Line`, as a single spinner's block is. When a job raises, jobs already running finish, jobs not yet started are marked skipped (`[—]`), and the error is re-raised. Piped, it prints `Fetching...`, then each job's outcome as it ends, then the headline's.
|
|
167
|
+
|
|
168
|
+
### Several progress bars at once
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
ui.multi_progress("Downloading") do |m|
|
|
172
|
+
files.each do |file|
|
|
173
|
+
m.progress(file.name, total: file.size) do |bar|
|
|
174
|
+
download(file) { |bytes| bar.advance(bytes) }
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The same shape as `multi_spinner`, with a bar per job and a headline bar that counts them all. Bars start and end in the same columns, and counts are right-aligned:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
[⠋] Downloading [◼◼◼◼ ] 27% 66/240 ETA 2.1s
|
|
184
|
+
├─ [⠋] fonts.zip [◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ ] 95% 38/40 ETA 0.1s
|
|
185
|
+
├─ [⠋] images.tar.gz [◼◼◼ ] 23% 28/120 ETA 2.4s
|
|
186
|
+
└─ [ ] video.mp4
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Each job is given the same handle as `ui.progress`, with `advance(step = 1)`, `current` and `total`. A finished job's row reads `[✓] fonts.zip 40/40 (0.1s)`, and the headline's `[✓] Downloading 240/240 (0.3s)`.
|
|
190
|
+
|
|
191
|
+
### Example: fetching many URLs
|
|
192
|
+
|
|
193
|
+
With `multi_spinner`, each URL gets a spinner, and the call returns every page in the same order as `urls`. Nothing writes to a shared file from several threads:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
bodies = ui.multi_spinner("Fetching #{urls.size} URLs", concurrent: 8) do |m|
|
|
197
|
+
urls.each { |url| m.spinner(url) { fetch(url) } }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
File.write("urls.txt", bodies.join("\n"))
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
With `multi_progress`, each URL gets a bar that fills one byte at a time as the body arrives. A bar's `total` is fixed when it is declared, so a `HEAD` request asks each URL for its size first:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
found = urls.filter_map do |url|
|
|
207
|
+
[url, content_length(url)]
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
ui.status "#{url}: #{e.message}", level: :warn
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
bodies = ui.multi_progress("Fetching #{found.size} URLs", concurrent: 8) do |m|
|
|
214
|
+
found.each do |url, size|
|
|
215
|
+
m.progress(url, total: size || 1) do |bar|
|
|
216
|
+
body = download(url) { |bytes| bytes.times { bar.advance } if size }
|
|
217
|
+
bar.advance unless size # no Content-Length: done in one step
|
|
218
|
+
body
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
File.write("urls.txt", bodies.join("\n"))
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The two helpers, with `Net::HTTP`:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
require "net/http"
|
|
230
|
+
|
|
231
|
+
def content_length(url)
|
|
232
|
+
uri = URI(url)
|
|
233
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
234
|
+
http.head(uri.request_uri).content_length # nil when the server does not say
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def download(url)
|
|
239
|
+
uri = URI(url)
|
|
240
|
+
body = +""
|
|
241
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
242
|
+
http.request_get(uri.request_uri) do |response|
|
|
243
|
+
response.read_body do |chunk|
|
|
244
|
+
body << chunk
|
|
245
|
+
yield chunk.bytesize
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
body
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
In both, at most eight requests run at once and the rest wait as `[ ]` rows. The headline counts every URL, and in `multi_progress` every byte. A server that sends no `Content-Length` gets a bar of one unit, which sits at 0% and fills when its download ends. These helpers are kept short: a real one follows redirects, and sends `Accept-Encoding: identity` so the bytes counted match `Content-Length`. With more URLs than the screen has rows, each finished URL prints one line instead.
|
|
135
254
|
|
|
136
255
|
### Task trees
|
|
137
256
|
|
|
@@ -156,19 +275,19 @@ On a terminal, once it finishes:
|
|
|
156
275
|
|
|
157
276
|
```text
|
|
158
277
|
Deploy
|
|
159
|
-
├─ ✓ Build assets (0.4s)
|
|
160
|
-
├─ ✓ Migrate (0.3s)
|
|
161
|
-
│ ├─ ✓ users (0.1s)
|
|
162
|
-
│ └─ ✓ orders (0.2s)
|
|
163
|
-
├─ ✓ Warm caches (0.5s)
|
|
164
|
-
│ ├─ ✓ fonts (0.5s)
|
|
165
|
-
│ └─ ✓ images (0.3s)
|
|
166
|
-
└─ ✓ Restart (0.3s)
|
|
278
|
+
├─ [✓] Build assets (0.4s)
|
|
279
|
+
├─ [✓] Migrate (0.3s)
|
|
280
|
+
│ ├─ [✓] users (0.1s)
|
|
281
|
+
│ └─ [✓] orders (0.2s)
|
|
282
|
+
├─ [✓] Warm caches (0.5s)
|
|
283
|
+
│ ├─ [✓] fonts (0.5s)
|
|
284
|
+
│ └─ [✓] images (0.3s)
|
|
285
|
+
└─ [✓] Restart (0.3s)
|
|
167
286
|
```
|
|
168
287
|
|
|
169
|
-
While it runs, the tree redraws in place and every running task has its own spinner. Piped, each line is printed once it is final, and a group's line appears as
|
|
288
|
+
While it runs, the tree redraws in place and every running task has its own spinner. Every row is marked in brackets, in bold yellow while it waits, `[ ]`, and while it runs, a turning `[⠏]`; then a green `[✓]` when it is done, a red `[𝘅]` when it failed, or a yellow `[—]` when it was skipped. Piped, each line is printed once it is final, and a group's line appears as `[▸]` when it starts. `concurrent: true` runs a group's tasks at the same time, on a group or on `ui.tasks` itself, and `concurrent: 3` runs at most three at once. When a task raises, it is marked `[𝘅]`, tasks already running finish, the rest are marked skipped (`[—]`), and the error is re-raised.
|
|
170
289
|
|
|
171
|
-
Each task is given a `Line`, as a spinner's block is. Its detail is drawn after the task's name while it runs, and `line.fail(reason)` marks the task
|
|
290
|
+
Each task is given a `Line`, as a spinner's block is. Its detail is drawn after the task's name while it runs, and `line.fail(reason)` marks the task `𝘅 name: reason` and its groups `𝘅`, while the rest of the tree runs on:
|
|
172
291
|
|
|
173
292
|
```ruby
|
|
174
293
|
ui.tasks("Fetching", concurrent: 4) do |t|
|
|
@@ -182,6 +301,66 @@ ui.tasks("Fetching", concurrent: 4) do |t|
|
|
|
182
301
|
end
|
|
183
302
|
```
|
|
184
303
|
|
|
304
|
+
### Status bar
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
ui.status_bar("deploy", hints: ["^C cancel"]) do
|
|
308
|
+
ui.spinner("Building assets") { build }
|
|
309
|
+
ui.multi_progress("Uploading", concurrent: 2) { |m| ... }
|
|
310
|
+
ui.tasks("Migrate") { |t| ... }
|
|
311
|
+
end
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
While the block runs, the bottom of the screen shows how the whole command is doing, under a rule: what started last, how many things are running, done and failed, a bar over every progress bar so far, the elapsed time, and your hints at the right edge. Everything the widgets print scrolls above it, and it disappears when the block ends:
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
✓ Building assets (0.2s)
|
|
318
|
+
[⠙] Uploading [◼◼◼◼◼◼◼◼◼◼◼◼◼◼ ] 37% 49/132 ETA 0.3s
|
|
319
|
+
├─ [✓] app.js 40/40 (0.2s)
|
|
320
|
+
├─ [⠙] app.css [◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼◼ ] 75% 9/12 ETA 0.1s
|
|
321
|
+
└─ [⠙] fonts.zip [ ] 0% 0/80 ETA --
|
|
322
|
+
──────────────────────────────────────────────────────────────────────────────────────────
|
|
323
|
+
⠸ deploy · fonts.zip · 2 running · 2 done · [◼◼◼ ] 37% · 0.4s ^C cancel
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Nothing reports to it by hand: every `spinner`, `progress`, `multi_spinner`, `multi_progress` and task started inside the block does so on its own. Hints that do not fit are left out, and a status that does not fit is cut short with `…`.
|
|
327
|
+
|
|
328
|
+
It sets no scroll region, so the scrollback keeps everything, and an interrupted command leaves nothing behind. Piped, or without animation, it just runs the block. Write through `ui` while it runs: a bare `puts` lands where the bar is, until the next `ui` call draws the bar again.
|
|
329
|
+
|
|
330
|
+
### Putting it together
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
class Deploy < Dry::CLI::Command
|
|
334
|
+
include Dry::CLI::UI
|
|
335
|
+
|
|
336
|
+
def call(**)
|
|
337
|
+
ui.status_bar("deploy", hints: ["^C cancel"]) do
|
|
338
|
+
assets = ui.spinner("Building assets") { build_assets }
|
|
339
|
+
|
|
340
|
+
ui.multi_progress("Uploading", concurrent: 3) do |m|
|
|
341
|
+
assets.each do |asset|
|
|
342
|
+
m.progress(asset.name, total: asset.bytesize) do |bar|
|
|
343
|
+
upload(asset) { |sent| bar.advance(sent) }
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
ui.multi_spinner("Warming caches") do |m|
|
|
349
|
+
regions.each do |region|
|
|
350
|
+
m.spinner(region) do |line|
|
|
351
|
+
warm(region) { |host| line.detail = host }
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
ui.success "Deployed #{assets.size} assets"
|
|
358
|
+
rescue => e
|
|
359
|
+
ui.error("Deploy failed", e.message)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
```
|
|
363
|
+
|
|
185
364
|
### Tables
|
|
186
365
|
|
|
187
366
|
```ruby
|
|
@@ -218,9 +397,9 @@ When the input runs out, a prompt returns its default, or raises `Dry::CLI::UI::
|
|
|
218
397
|
|
|
219
398
|
## Where output goes
|
|
220
399
|
|
|
221
|
-
| To `out` (results) | To `err` (everything else)
|
|
222
|
-
| ----------------------------------------------------------- |
|
|
223
|
-
| `info`, `success`, `box`, `table`, `status` at those levels | `debug`, `warn`, `error`, `fatal`, `popup`, spinners, progress bars, task trees, prompts |
|
|
400
|
+
| To `out` (results) | To `err` (everything else) |
|
|
401
|
+
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
402
|
+
| `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 |
|
|
224
403
|
|
|
225
404
|
`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(out: io, err: io)` captures everything.
|
|
226
405
|
|
|
@@ -228,6 +407,28 @@ A stream that is not a terminal, or runs under `TERM=dumb`, gets no animation, n
|
|
|
228
407
|
|
|
229
408
|
## Configuration
|
|
230
409
|
|
|
410
|
+
Spinners and bars look the same everywhere, and are set once for the whole process:
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
Dry::CLI::UI.configure do
|
|
414
|
+
spinner_format :dots # any TTY::Spinner format name
|
|
415
|
+
bar_format(complete: "◼", incomplete: " ") # or any TTY::ProgressBar bar format name, such as :box
|
|
416
|
+
bar_color :green # the finished part: any Pastel style, or nil
|
|
417
|
+
bar_background :on_bright_black # the whole bar: any Pastel style, or nil
|
|
418
|
+
end
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Those are the defaults: spinners turn through `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏` ten times a second, and bars draw a green `◼` for each finished part on a gray track. Without colour the track is blank, and the brackets still show where the bar 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. The formats also take a definition of your own:
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
Dry::CLI::UI.configure do |config|
|
|
425
|
+
config.spinner_format = { interval: 8, frames: %w[◐ ◓ ◑ ◒] } # frames per second, and the frames
|
|
426
|
+
config.bar_format = { complete: "#", incomplete: "." }
|
|
427
|
+
end
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
An unknown name or a malformed definition raises `ArgumentError` when it is set.
|
|
431
|
+
|
|
231
432
|
Override `ui` to configure the console:
|
|
232
433
|
|
|
233
434
|
```ruby
|
data/SPECIFICATION.md
CHANGED
|
@@ -63,7 +63,7 @@ Example output, piped, when the import fails part way:
|
|
|
63
63
|
Loading tax rules...
|
|
64
64
|
✓ Loading tax rules (0.3s)
|
|
65
65
|
Importing rules...
|
|
66
|
-
|
|
66
|
+
𝘅 Importing rules 1482/1900 (4.1s)
|
|
67
67
|
┌─ Error ──────────────────────────────────────────────────┐
|
|
68
68
|
│ │
|
|
69
69
|
│ Import failed │
|
|
@@ -89,8 +89,11 @@ ui.error(...)
|
|
|
89
89
|
ui.fatal(...)
|
|
90
90
|
|
|
91
91
|
ui.spinner(...)
|
|
92
|
+
ui.multi_spinner(...)
|
|
92
93
|
ui.progress(...)
|
|
94
|
+
ui.multi_progress(...)
|
|
93
95
|
ui.status(...)
|
|
96
|
+
ui.status_bar(...)
|
|
94
97
|
|
|
95
98
|
ui.box(...)
|
|
96
99
|
ui.popup(...)
|
|
@@ -226,10 +229,10 @@ Only files under `widgets/` and `terminal.rb` touch a TTY class. A future render
|
|
|
226
229
|
|
|
227
230
|
Results go to `out`; everything about the command's own progress goes to `err`. Piping a command therefore captures its results and nothing else.
|
|
228
231
|
|
|
229
|
-
| `out` | `err`
|
|
230
|
-
| --------------------------------- |
|
|
231
|
-
| `info`, `success`, `box`, `table` | `debug`, `warn`, `error`, `fatal`, `popup`, `spinner`, `progress`, `tasks`, prompts |
|
|
232
|
-
| `status` at `info` or `success` | `status` at `debug`, `warn`, `error` or `fatal`
|
|
232
|
+
| `out` | `err` |
|
|
233
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
234
|
+
| `info`, `success`, `box`, `table` | `debug`, `warn`, `error`, `fatal`, `popup`, `spinner`, `multi_spinner`, `progress`, `multi_progress`, `tasks`, `status_bar`, prompts |
|
|
235
|
+
| `status` at `info` or `success` | `status` at `debug`, `warn`, `error` or `fatal` |
|
|
233
236
|
|
|
234
237
|
`#ui` uses the command's own `out` and `err` when dry-cli has set them (`Dry::CLI#call(out:, err:)`), and `$stdout` and `$stderr` otherwise. Every write flushes, so the two streams stay in order when both are piped to the same place.
|
|
235
238
|
|
|
@@ -248,8 +251,10 @@ Each stream is judged on its own:
|
|
|
248
251
|
|
|
249
252
|
| Widget | Plain output |
|
|
250
253
|
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
251
|
-
| spinner | `Label...` before the block, `✓ Label (1.2s)` or
|
|
254
|
+
| spinner | `Label...` before the block, `✓ Label (1.2s)` or `𝘅 Label (1.2s)` after it; a `Line`'s detail is never printed, a `Line#fail` reason follows the label: `𝘅 Label: reason (1.2s)` |
|
|
252
255
|
| progress | `Label...` before, `✓ Label 1900/1900 (4.2s)` after, with the count reached |
|
|
256
|
+
| multi_spinner | `Title...` before, each job's outcome indented as it ends, skipped ones at the end, then the headline's outcome |
|
|
257
|
+
| multi_progress | as multi_spinner, each outcome with its count: `✓ a.zip 40/40 (0.1s)`, and the headline with the total count |
|
|
253
258
|
| tasks | each line printed once final: a group when it starts, a task when it ends, skipped ones at the end |
|
|
254
259
|
| prompts | the question on `err`, one line read from input |
|
|
255
260
|
| boxes, tables, status | unchanged apart from colour |
|
|
@@ -257,7 +262,7 @@ Each stream is judged on its own:
|
|
|
257
262
|
|
|
258
263
|
### Spinners and progress bars
|
|
259
264
|
|
|
260
|
-
Both run a block, return what it returns, and re-raise what it raises after marking the outcome
|
|
265
|
+
Both run a block, return what it returns, and re-raise what it raises after marking the outcome `𝘅`. The elapsed time comes from a monotonic clock. `ui.progress` yields a handle with `advance(step = 1)`, `current` and `total`; progress is clamped to `0..total`, and `total: 0` is allowed.
|
|
261
266
|
|
|
262
267
|
`ui.spinner` yields a `Dry::CLI::UI::Line`, the same handle every task in a tree is given:
|
|
263
268
|
|
|
@@ -265,7 +270,7 @@ Both run a block, return what it returns, and re-raise what it raises after mark
|
|
|
265
270
|
| --------------- | -------------------------------------------------------------------------------------------------------- |
|
|
266
271
|
| `detail = text` | Text after the label while the work runs, redrawn in place when animated; kept, never printed, otherwise |
|
|
267
272
|
| `detail` | The current text, `""` for none |
|
|
268
|
-
| `fail(reason)` | Ends the work as
|
|
273
|
+
| `fail(reason)` | Ends the work as `𝘅 label: reason` when the block returns, without raising; the reason is optional |
|
|
269
274
|
| `failed?` | Whether `fail` was called |
|
|
270
275
|
| `reason` | What `fail` was given |
|
|
271
276
|
|
|
@@ -273,6 +278,58 @@ Every method may be called from any thread, which is how work that reports from
|
|
|
273
278
|
|
|
274
279
|
A spinner whose block calls `fail` still returns the block's value. That is the difference from raising: the work finished and has a result, and the result is that it did not succeed.
|
|
275
280
|
|
|
281
|
+
### Several at once: `multi_spinner` and `multi_progress`
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
ui.multi_spinner("Fetching", concurrent: 2) do |m|
|
|
285
|
+
m.spinner("fonts") { fetch(:fonts) }
|
|
286
|
+
m.spinner("images") { |line| fetch(:images) { |n| line.detail = "#{n} of 40" } }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
ui.multi_progress("Downloading") do |m|
|
|
290
|
+
files.each { |f| m.progress(f.name, total: f.size) { |bar| download(f) { |n| bar.advance(n) } } }
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- The block declares the jobs; nothing runs until it returns, so every row, including those of jobs waiting under a concurrency limit, is drawn before the first job starts. TTY::Spinner::Multi and TTY::ProgressBar::Multi give a row only to a job that has started and move the cursor relative to the last row drawn, which is why these widgets draw their rows themselves, as task trees do.
|
|
295
|
+
- Jobs run all at once by default. `concurrent:` takes the same values as on `ui.tasks`, with the same meaning.
|
|
296
|
+
- The call returns what each job returned, in declaration order, and `nil` for a job that never ran.
|
|
297
|
+
- The headline turns while any job runs and ends `✓` when every job succeeded, `𝘅` otherwise. Every row, the headline's included, is marked in brackets as task rows are: `[ ]` while waiting, a turning `[⠏]` while running, then `[✓]`, `[𝘅]` or `[—]` with its elapsed time.
|
|
298
|
+
- `multi_spinner` gives each job a `Line`; its detail follows the label while the job runs, and `fail` marks the job `𝘅 label: reason` without raising. `multi_progress` gives each job a `Widgets::Progress::Handle`; its row shows a bar, a percentage, a count and an ETA, and the headline's bar counts every job. Labels are padded so every bar starts and ends in the same columns.
|
|
299
|
+
- When a job raises, jobs already running finish, jobs not yet started are marked skipped, and the first error is re-raised.
|
|
300
|
+
- As with task trees, rows are redrawn in place only when they all fit on the screen; otherwise they print as without animation.
|
|
301
|
+
|
|
302
|
+
### Configuration
|
|
303
|
+
|
|
304
|
+
`Dry::CLI::UI.configure` sets how every spinner and bar in the process looks. A `Console` reads `Dry::CLI::UI.config` unless given `config:`.
|
|
305
|
+
|
|
306
|
+
| Setting | Takes | Default | Draws |
|
|
307
|
+
| ---------------- | ---------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------ |
|
|
308
|
+
| `spinner_format` | A `TTY::Formats::FORMATS` name, or `{ interval:, frames: }` | `:dots` | `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏`, 10 a second |
|
|
309
|
+
| `bar_format` | A `TTY::ProgressBar::Formats::FORMATS` name, or `{ complete:, incomplete: }` | `{ complete: "◼", incomplete: " " }` | `[◼◼◼ ]` |
|
|
310
|
+
| `bar_color` | A Pastel style, or nil | `:green` | The finished part of every bar |
|
|
311
|
+
| `bar_background` | A Pastel style, or nil | `:on_bright_black` | The whole of every bar, a gray track |
|
|
312
|
+
|
|
313
|
+
An unknown name, a malformed definition or a style Pastel does not know raises `ArgumentError` when it is set, not when a spinner first turns. Loading the configuration loads only the two format tables and Pastel, never a TTY widget. Colours follow the stream, as everything else does: without colour a bar is its characters alone, and the brackets show its extent.
|
|
314
|
+
|
|
315
|
+
Every count in a `multi_progress` is right-aligned to the widest any row can show, the headline's total, so `8/503` and `1/8` end in the same column.
|
|
316
|
+
|
|
317
|
+
### Status bar
|
|
318
|
+
|
|
319
|
+
`ui.status_bar(title = nil, hints: [])` keeps two rows at the bottom of the screen while its block runs: a rule, then
|
|
320
|
+
|
|
321
|
+
```text
|
|
322
|
+
⠸ deploy · fonts.zip · 2 running · 2 done · [◼◼◼ ] 37% · 0.4s ^C cancel
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
- The glyph turns while anything runs. Then come the title in bold, the label of the work started most recently, the counts of what is running, done and failed, a bar over every progress bar reported so far, finished or not, and the elapsed time. Hints are right-aligned when they fit and left out when they do not; a status too wide for the screen is truncated with `…`.
|
|
326
|
+
- Every `spinner`, `progress`, `multi_spinner` and `multi_progress` job, and every task (not group) in a tree, reports its start and end to the bar through its `Terminal`. Commands supply only the title and the hints.
|
|
327
|
+
- It is drawn at the bottom rather than pinned to the top. Pinning a top row needs a scroll region, terminals such as iTerm2 and tmux drop lines scrolled out of a region from the scrollback, and a crash that skips cleanup leaves the region set. A bottom line needs neither.
|
|
328
|
+
- While it runs, the console's terminals write through a `StatusBar::Output`. Each write clears from the cursor to the end of the screen, writes, draws the rule and status line below, and moves the cursor back to where the write left it, in one write to the stream. The column is followed through the text, carriage returns, `CSI n G/C/D` and cursor save and restore, so a spinner redrawing its own line keeps working. The status line alone is redrawn ten times a second.
|
|
329
|
+
- `Terminal#height` is two rows short while it runs, so live widgets fall back to plain output two rows sooner.
|
|
330
|
+
- `out` writes through it too when `out` is animated, since both streams share the screen. Writes that bypass the console land where the bar is until the next write draws over them.
|
|
331
|
+
- Without an animated `err`, and inside another status bar, it only runs the block.
|
|
332
|
+
|
|
276
333
|
### Task trees
|
|
277
334
|
|
|
278
335
|
The block declares the tree; nothing runs until it returns. Knowing the whole shape first is what lets the tree draw `├─` and `└─` correctly before the first task starts.
|
|
@@ -293,8 +350,8 @@ end
|
|
|
293
350
|
```
|
|
294
351
|
|
|
295
352
|
- Tasks run in order. A group declared `concurrent: true`, or `ui.tasks(concurrent: true)` at the top level, runs its tasks at the same time on `concurrent-ruby` futures. `concurrent: 3` runs at most three at once, taking tasks in declaration order as each finishes. Anything but `true`, `false` or a positive Integer raises `ArgumentError`.
|
|
296
|
-
- Each task is given a `Line`. Its detail is drawn after the task's name while it runs, on a live tree only. A task that calls `fail` is marked
|
|
297
|
-
-
|
|
353
|
+
- Each task is given a `Line`. Its detail is drawn after the task's name while it runs, on a live tree only. A task that calls `fail` is marked `𝘅 name: reason`, every group above it ends `𝘅`, and the rest of the tree runs on: nothing is skipped and nothing is raised.
|
|
354
|
+
- Each row is marked with its state in brackets: pending `[ ]` and running `[▸]` in bold yellow, done `[✓]` in green, failed `[𝘅]` in red and skipped `[—]` in yellow. On an animated terminal a running task shows a turning spinner instead, `[⠏]`, drawn from the configured spinner format, so a concurrent group is a multi-spinner.
|
|
298
355
|
- When a task raises, it and its enclosing groups are marked failed, tasks already running beside it finish, tasks not yet started are marked skipped, and the first error is re-raised. Under a concurrency limit, no further task is started once one has raised.
|
|
299
356
|
- The live tree is redrawn in place with cursor movement, which cannot reach above the top of the screen. A tree with as many rows as the screen, or more, is printed line by line instead.
|
|
300
357
|
- Task blocks should not write to the terminal while a live tree is drawn; the next redraw overwrites their output.
|
|
@@ -336,6 +393,9 @@ With a fixed width, TTY::Box 0.7 wraps text but sizes the box from the unwrapped
|
|
|
336
393
|
- [x] `debug`, `info`, `success`, `warn`, `error` and `fatal` draw white single-line boxes titled by level, wrapped, as wide as configured or the terminal less a margin, and never lose text.
|
|
337
394
|
- [x] `spinner`, `progress` and `tasks` return their block's value, re-raise its error, and leave an outcome line with the elapsed time; progress shows percent, count and ETA.
|
|
338
395
|
- [x] Task trees nest, run groups concurrently when asked, at most as many at once as asked, and mark failed and skipped tasks.
|
|
396
|
+
- [x] `multi_spinner` and `multi_progress` run declared jobs at once, at most as many as asked, draw every row including waiting ones, return each job's value, and mark failed and skipped jobs.
|
|
397
|
+
- [x] `Dry::CLI::UI.configure` sets the spinner frames, bar characters and bar colours every widget draws with, and rejects unknown formats and styles when set.
|
|
398
|
+
- [x] `status_bar` keeps an automatically fed status line below everything the command prints, restores the cursor after every write, leaves the scrollback intact, and removes itself when the block ends or raises.
|
|
339
399
|
- [x] Spinner and task blocks get a thread-safe `Line` whose detail is drawn while they run, and which can fail them without raising.
|
|
340
400
|
- [x] `popup` draws a content-sized, centred box that leaves the cursor where it was, and a plain box without animation.
|
|
341
401
|
- [x] Tables render rows and a header without truncation.
|
|
@@ -346,6 +406,6 @@ With a fixed width, TTY::Box 0.7 wraps text but sizes the box from the unwrapped
|
|
|
346
406
|
|
|
347
407
|
## Out of scope
|
|
348
408
|
|
|
349
|
-
- Full-screen applications, alternate screen buffers, and a public cursor-positioning API. TTY::Cursor and TTY::Screen are used internally only; `popup` positions itself, and takes no coordinates.
|
|
409
|
+
- Full-screen applications, alternate screen buffers, scroll regions, and a public cursor-positioning API. TTY::Cursor and TTY::Screen are used internally only; `popup` positions itself, and takes no coordinates.
|
|
350
410
|
- Keyboard input beyond prompts.
|
|
351
411
|
- Renderers other than the TTY toolkit. The widget boundary allows one later.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
require "tty/spinner/formats"
|
|
5
|
+
require "tty/progressbar/formats"
|
|
6
|
+
|
|
7
|
+
module Dry
|
|
8
|
+
class CLI
|
|
9
|
+
module UI
|
|
10
|
+
# Process-wide settings, made once through {UI.configure}:
|
|
11
|
+
#
|
|
12
|
+
# Dry::CLI::UI.configure do
|
|
13
|
+
# spinner_format :dots # any TTY::Spinner format name
|
|
14
|
+
# bar_format :box # any TTY::ProgressBar bar format name
|
|
15
|
+
# bar_color :cyan # any Pastel style, or nil
|
|
16
|
+
# bar_background nil # any Pastel style, or nil
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Dry::CLI::UI.configure do |config|
|
|
20
|
+
# config.spinner_format = { interval: 8, frames: %w[◐ ◓ ◑ ◒] }
|
|
21
|
+
# config.bar_format = { complete: "#", incomplete: "." }
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# Anything not set reads from {DEFAULTS}.
|
|
25
|
+
class Configuration
|
|
26
|
+
# What a setting reads before it is set: a green `◼` for each finished
|
|
27
|
+
# part of a bar, over a gray track the whole bar's width.
|
|
28
|
+
DEFAULTS = {
|
|
29
|
+
spinner_format: :dots,
|
|
30
|
+
bar_format: { complete: "◼", incomplete: " " }.freeze,
|
|
31
|
+
bar_color: :green,
|
|
32
|
+
bar_background: :on_bright_black
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# Every style name Pastel knows, for checking colour settings.
|
|
36
|
+
STYLES = Pastel.new(enabled: true).styles.keys.freeze
|
|
37
|
+
|
|
38
|
+
# Marks a DSL call made without a value, which reads instead of writes.
|
|
39
|
+
UNSET = Object.new.freeze
|
|
40
|
+
private_constant :UNSET
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@values = {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @!method spinner_format(value = UNSET)
|
|
47
|
+
# Reads the spinner format, or sets it when given a value.
|
|
48
|
+
# @param value [Symbol, Hash] a key of `TTY::Formats::FORMATS`, or
|
|
49
|
+
# `{ interval:, frames: }`: frames per second, and the frames
|
|
50
|
+
# @return [Symbol, Hash]
|
|
51
|
+
# @!method bar_format(value = UNSET)
|
|
52
|
+
# Reads the bar format, or sets it when given a value.
|
|
53
|
+
# @param value [Symbol, Hash] a key of `TTY::ProgressBar::Formats::FORMATS`,
|
|
54
|
+
# or `{ complete:, incomplete: }`
|
|
55
|
+
# @return [Symbol, Hash]
|
|
56
|
+
# @!method bar_color(value = UNSET)
|
|
57
|
+
# Reads the colour a bar's finished part is drawn in, or sets it.
|
|
58
|
+
# @param value [Symbol, nil] a Pastel style, such as :green; nil for none
|
|
59
|
+
# @return [Symbol, nil]
|
|
60
|
+
# @!method bar_background(value = UNSET)
|
|
61
|
+
# Reads the background the whole bar is drawn on, or sets it.
|
|
62
|
+
# @param value [Symbol, nil] a Pastel style, such as :on_bright_black; nil for none
|
|
63
|
+
# @return [Symbol, nil]
|
|
64
|
+
DEFAULTS.each_key do |name|
|
|
65
|
+
define_method(name) do |value = UNSET|
|
|
66
|
+
return @values.fetch(name) { DEFAULTS.fetch(name) } if UNSET.equal?(value)
|
|
67
|
+
|
|
68
|
+
public_send(:"#{name}=", value)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param value [Symbol, Hash] see {#spinner_format}
|
|
73
|
+
# @raise [ArgumentError] for an unknown name or a malformed Hash
|
|
74
|
+
def spinner_format=(value)
|
|
75
|
+
@values[:spinner_format] = spinner_definition(value) && value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param value [Symbol, Hash] see {#bar_format}
|
|
79
|
+
# @raise [ArgumentError] for an unknown name or a malformed Hash
|
|
80
|
+
def bar_format=(value)
|
|
81
|
+
@values[:bar_format] = bar_definition(value) && value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param value [Symbol, nil] see {#bar_color}
|
|
85
|
+
# @raise [ArgumentError] for a style Pastel does not know
|
|
86
|
+
def bar_color=(value)
|
|
87
|
+
@values[:bar_color] = style(:bar_color, value)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param value [Symbol, nil] see {#bar_background}
|
|
91
|
+
# @raise [ArgumentError] for a style Pastel does not know
|
|
92
|
+
def bar_background=(value)
|
|
93
|
+
@values[:bar_background] = style(:bar_background, value)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @return [Array<String>] the frames a spinner cycles through
|
|
97
|
+
def spinner_frames
|
|
98
|
+
frames = spinner_definition(spinner_format).fetch(:frames)
|
|
99
|
+
frames.is_a?(String) ? frames.chars : frames
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Float] seconds between two spinner frames
|
|
103
|
+
def spinner_frame_seconds
|
|
104
|
+
1.0 / spinner_definition(spinner_format).fetch(:interval)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [String] what a finished part of a bar is drawn with
|
|
108
|
+
def bar_complete
|
|
109
|
+
bar_definition(bar_format).fetch(:complete)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [String] what an unfinished part of a bar is drawn with
|
|
113
|
+
def bar_incomplete
|
|
114
|
+
bar_definition(bar_format).fetch(:incomplete)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# @param value [Symbol, Hash]
|
|
120
|
+
# @return [Hash{Symbol => Object}] with :interval and :frames
|
|
121
|
+
def spinner_definition(value)
|
|
122
|
+
return TTY::Formats::FORMATS.fetch(value) { unknown(:spinner_format, value) } if value.is_a?(Symbol)
|
|
123
|
+
|
|
124
|
+
valid = value.is_a?(Hash) && value[:interval].is_a?(Numeric) && value[:interval].positive? &&
|
|
125
|
+
(value[:frames].is_a?(String) || value[:frames].is_a?(Array)) && !value[:frames].empty?
|
|
126
|
+
valid ? value : malformed(:spinner_format, value, "{ interval: Numeric, frames: Array }")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @param value [Symbol, Hash]
|
|
130
|
+
# @return [Hash{Symbol => String}] with :complete and :incomplete
|
|
131
|
+
def bar_definition(value)
|
|
132
|
+
return TTY::ProgressBar::Formats::FORMATS.fetch(value) { unknown(:bar_format, value) } if value.is_a?(Symbol)
|
|
133
|
+
|
|
134
|
+
valid = value.is_a?(Hash) && value[:complete].is_a?(String) && value[:incomplete].is_a?(String)
|
|
135
|
+
valid ? value : malformed(:bar_format, value, "{ complete: String, incomplete: String }")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @param setting [Symbol]
|
|
139
|
+
# @param value [Symbol, nil]
|
|
140
|
+
# @return [Symbol, nil] the value
|
|
141
|
+
# @raise [ArgumentError] for anything but nil or a style Pastel knows
|
|
142
|
+
def style(setting, value)
|
|
143
|
+
return value if value.nil? || STYLES.include?(value)
|
|
144
|
+
|
|
145
|
+
raise ArgumentError, "#{setting} must be a Pastel style or nil, got #{value.inspect}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @raise [ArgumentError]
|
|
149
|
+
def unknown(setting, value)
|
|
150
|
+
raise ArgumentError, "#{setting} #{value.inspect} is not a known format"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @raise [ArgumentError]
|
|
154
|
+
def malformed(setting, value, shape)
|
|
155
|
+
raise ArgumentError, "#{setting} must be a format name or #{shape}, got #{value.inspect}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|