dry-cli-ui 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b4b7ae4869fa6a7623aafc109ef5c26af41436021cc62e4efd5ba87602c64a3
4
- data.tar.gz: ecbc221a29f2a9be94a4041fd4642ac5367af9bd4b1e7cc4bff828da20e871c5
3
+ metadata.gz: d64ee9f8fedbc7de0fcd1875acf59d07876ef6acaffbfcc8574b197de0e8d6fd
4
+ data.tar.gz: 2f7e11eca1a0dff40b3df7cba685bca7f97d4884515db1107805036e746e20bc
5
5
  SHA512:
6
- metadata.gz: d46a4ec8716f6aaea3f17d8e2eda6ba29f90272516d812270b77ba604ff372e6327c840cbcc54b9ac7511eabd78dcf709c8c6072932927e6ff72ff6c35a5931d
7
- data.tar.gz: 0ce772edc045b9deca09aca086de303d7f01cc2ce7ff5ca912077eeb3a5753cd397bc9cdfc8c1b4704d0c125c67f8077176266fc693df6dcbe97a06a9f971850
6
+ metadata.gz: d9105f75325a1a7f8282e989d4c847320fb481880a7a529041d98d4bff9457dc5235d08a5c3b311c0b3d442a6dc4c3923b6c72844bd2fbb55ec04f2824b906e3
7
+ data.tar.gz: d095b784083ba57db9592e51ef2d56274dd9cb187fcac350d35aaea51b8e64c519a800e3f7f899b2b248acfe8035733c4fa63930b64086cdbebea17c8af4636b
data/CHANGELOG.md CHANGED
@@ -1,7 +1,18 @@
1
+ ## [0.6.1]
2
+
3
+ - 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.
4
+
1
5
  ## [0.6.0]
2
6
 
3
7
  - `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.
4
8
 
9
+ ## [0.6.0]
10
+
11
+ - `ui` reads the command's public `stdout`, `stderr` and `stdin`, the streams dry-cli was called with. Before, it looked for `out` and `err`, which dry-cli no longer has, and wrote to `$stdout` and `$stderr` instead. Prompts now read from the command's `stdin`.
12
+ - `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.
13
+ - `ui_options` configures the console `ui` builds, in place of overriding `ui`.
14
+ - Needs dry-cli with public command streams and `Dry::CLI::Launcher`; the Gemfile takes it from kigster/dry-cli until it is released.
15
+
5
16
  ## [0.5.1]
6
17
 
7
18
  - `ui.stoppable { |stop| ... }` makes Ctrl-C ask for a stop instead of interrupting, and `ui.multi_spinner` and `ui.multi_progress` take `stop:`. Once it is set, the jobs running finish, the rest are skipped, and the headline says `stopping`, then ends skipped. A second Ctrl-C interrupts. `Dry::CLI::UI::Stop` is the object behind it.
data/README.md CHANGED
@@ -86,7 +86,7 @@ class Import < ApplicationCommand
86
86
  end
87
87
  ```
88
88
 
89
- `ui` writes to the command's `out` and `err` when dry-cli has set them, and to `$stdout` and `$stderr` otherwise.
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
90
 
91
91
  ### Without dry-cli
92
92
 
@@ -424,6 +424,30 @@ Generating text...
424
424
 
425
425
  `m.spinner` without a block raises `ArgumentError`.
426
426
 
427
+ 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`.
428
+
429
+ ```ruby
430
+ ui.multi_progress("Generating text", concurrent: false, count: :jobs) do |m|
431
+ m.spinner("Finding PDFs") { pdfs = find_pdfs }
432
+ m.progress("Extracting", total: nil) do |bar|
433
+ bar.total = pdfs.size
434
+ failed = pdfs.count { |pdf| !extract(pdf).tap { bar.advance } }
435
+ bar.fail("#{failed} failed") if failed.positive?
436
+ end
437
+ m.spinner("Indexing") { index }
438
+ end
439
+ ```
440
+
441
+ Piped, with one PDF that would not extract:
442
+
443
+ ```text
444
+ Generating text...
445
+ [āœ“] Finding PDFs (0.0s)
446
+ [š˜…] Extracting 3/3: 1 failed (0.0s)
447
+ [āœ“] Indexing (0.0s)
448
+ š˜… Generating text 3/3 (0.0s)
449
+ ```
450
+
427
451
  Piped:
428
452
 
429
453
  ```text
@@ -750,7 +774,7 @@ Errors raised inside a block are never swallowed: the widget marks itself failed
750
774
  | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
751
775
  | `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
776
 
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(out: io, err: io)` captures everything.
777
+ `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
778
 
755
779
  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
780
 
@@ -805,20 +829,18 @@ end
805
829
 
806
830
  ### Console options
807
831
 
808
- Override `ui` to configure the console:
832
+ Override `ui_options` to configure the console `ui` builds. The streams come from the command:
809
833
 
810
834
  ```ruby
811
835
  class ApplicationCommand < Dry::CLI::Command
812
836
  include Dry::CLI::UI
813
837
 
814
- def ui
815
- @ui ||= Dry::CLI::UI::Console.new(
816
- out: out || $stdout,
817
- err: err || $stderr,
838
+ private def ui_options
839
+ {
818
840
  box_width: 72, # boxes are 72 columns rather than the whole terminal
819
841
  color: nil, # true or false to override detection
820
842
  animate: nil # true or false to override detection
821
- )
843
+ }
822
844
  end
823
845
  end
824
846
  ```
@@ -862,7 +884,15 @@ err.string # => "Loading...\nāœ“ Loading (0.0s)\nContinue? (y/N) "
862
884
  out.string # => the Success box
863
885
  ```
864
886
 
865
- Through dry-cli, `Dry::CLI.new(registry).call(arguments: %w[import], out: out, err: err)` gives every command's `ui` those streams.
887
+ Through dry-cli, `Dry::CLI.new(registry).call(arguments: %w[import], stdin: input, stdout: out, stderr: err)` gives every command's `ui` those streams.
888
+
889
+ 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:
890
+
891
+ ```ruby
892
+ Launcher = Dry::CLI::Launcher[MyCLI::Commands]
893
+
894
+ Launcher.new(%w[import], StringIO.new("y\n"), out, err, kernel).execute!
895
+ ```
866
896
 
867
897
  ## Relationship to dry-cli-help
868
898
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- dry-cli-ui (0.6.0)
4
+ dry-cli-ui (0.6.1)
5
5
  concurrent-ruby (~> 1.3)
6
6
  dry-cli (>= 1.0)
7
7
  pastel (~> 0.8)
@@ -84,7 +84,7 @@ CHECKSUMS
84
84
  dry-cli (1.4.1) sha256=b8015bb76c708aa8705a36faf694973e75eeeffca39b89c8e172dc6f66a7d874
85
85
  dry-cli-autocomplete (0.1.3) sha256=14f76447bda0000c71739b585c827dc0a0a763e30a68a8b4b2dff91f640768e4
86
86
  dry-cli-help (0.2.1) sha256=eaaaf6edb787ffe810425b4e4bb1a675d89397ccc759102d70b5b26e7bc692da
87
- dry-cli-ui (0.6.0)
87
+ dry-cli-ui (0.6.1)
88
88
  dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
89
89
  forwardable (1.4.0) sha256=f1cd40cc9812937980e1c76f1aa053660990a7c9b6a98fc37d945468afcce838
90
90
  pastel (0.8.0) sha256=481da9fb7d2f6e6b1a08faf11fa10363172dc40fd47848f096ae21209f805a75
@@ -11,7 +11,7 @@ module Dry
11
11
  # Presentation helpers for Dry::CLI commands.
12
12
  module UI
13
13
  # The gem version.
14
- VERSION = "0.6.0"
14
+ VERSION = "0.6.1"
15
15
  end
16
16
  end
17
17
  end
@@ -113,8 +113,8 @@ module Dry
113
113
  def call(job) = spinner?(job) ? Line.call(job.work, job.handle) : super
114
114
 
115
115
  # @param job [Job]
116
- # @return [Boolean]
117
- def reported_failure?(job) = spinner?(job) && job.handle.failed?
116
+ # @return [Boolean] whether its line or its bar was told to fail
117
+ def reported_failure?(job) = job.handle.failed?
118
118
 
119
119
  # @param job [Job]
120
120
  # @return [Progress::Handle, nil] nil for a spinner, which has no progress
@@ -131,11 +131,7 @@ module Dry
131
131
 
132
132
  # @param job [Job]
133
133
  # @return [String]
134
- def summary(job)
135
- return job.handle.summary(job.label) if spinner?(job)
136
-
137
- "#{job.label} #{job.handle.current}/#{job.handle.total || '?'}"
138
- end
134
+ def summary(job) = job.handle.summary(job.label)
139
135
 
140
136
  # @param width [Integer]
141
137
  # @return [String]
@@ -56,6 +56,33 @@ module Dry
56
56
  self
57
57
  end
58
58
 
59
+ # Ends the work as a failure when the block returns, without
60
+ # raising: for work that went on after some of its units failed.
61
+ #
62
+ # @param reason [#to_s, nil] said after the count, such as "2 failed"
63
+ # @return [self]
64
+ def fail(reason = nil)
65
+ @failed = true
66
+ @reason = reason&.to_s
67
+ self
68
+ end
69
+
70
+ # @return [Boolean] whether {#fail} was called
71
+ def failed? = @failed == true
72
+
73
+ # @return [String, nil] what {#fail} was given
74
+ attr_reader :reason
75
+
76
+ # The label and the count, with the failure's reason after it when
77
+ # there is one: `Extracting 1002/1002: 2 failed`.
78
+ #
79
+ # @param label [String]
80
+ # @return [String]
81
+ def summary(label)
82
+ count = "#{label} #{current}/#{total || '?'}"
83
+ failed? && !reason.to_s.empty? ? "#{count}: #{reason}" : count
84
+ end
85
+
59
86
  private
60
87
 
61
88
  # @return [TTY::ProgressBar, nil]
@@ -150,14 +177,13 @@ module Dry
150
177
  terminal.started(handle, label, progress: handle)
151
178
  ok = false
152
179
  result = yield handle
153
- ok = true
180
+ ok = !handle.failed?
154
181
  result
155
182
  ensure
156
183
  if handle
157
184
  bar&.stop
158
185
  terminal.finished(handle, ok)
159
- summary = "#{label} #{handle.current}/#{handle.total}"
160
- terminal.puts(Outcome.line(terminal, ok ? :done : :failed, summary, clock.call - started))
186
+ terminal.puts(Outcome.line(terminal, ok ? :done : :failed, handle.summary(label), clock.call - started))
161
187
  end
162
188
  end
163
189
 
data/lib/dry/cli/ui.rb CHANGED
@@ -69,23 +69,65 @@ module Dry
69
69
  def reset!
70
70
  @config = nil
71
71
  end
72
+
73
+ # The IO beneath a dry-cli stream, or the stream itself when it is not one.
74
+ #
75
+ # Asks for the class rather than for `raw`, which `io/console` also defines on every IO, to
76
+ # put a terminal into raw mode.
77
+ #
78
+ # @param stream [IO, Dry::CLI::Stream]
79
+ # @return [IO]
80
+ def raw(stream)
81
+ defined?(Dry::CLI::Stream) && stream.is_a?(Dry::CLI::Stream) ? stream.raw : stream
82
+ end
72
83
  end
73
84
 
74
- # The console this command presents through. Writes to the command's own
75
- # `out` and `err` when dry-cli has set them, and to `$stdout` and
76
- # `$stderr` otherwise.
85
+ # The console this command presents through.
77
86
  #
78
- # Override it to configure the console:
87
+ # In a dry-cli command it writes to the command's own `stdout` and `stderr`, and reads prompt
88
+ # answers from its `stdin`: the streams the CLI was called with. Anywhere else it uses
89
+ # `$stdout`, `$stderr` and `$stdin`.
79
90
  #
80
- # @example
81
- # def ui = @ui ||= Dry::CLI::UI::Console.new(box_width: 72)
91
+ # The console is built again whenever those streams change. A command registered as an
92
+ # instance is used for every call to its CLI, each time with that call's streams, as when a
93
+ # test suite runs the CLI in-process with a StringIO per test.
94
+ #
95
+ # Configure the console by overriding {#ui_options}.
82
96
  #
83
97
  # @return [Dry::CLI::UI::Console]
84
98
  def ui
85
- @ui ||= Console.new(
86
- out: (respond_to?(:out, true) && __send__(:out)) || $stdout,
87
- err: (respond_to?(:err, true) && __send__(:err)) || $stderr
88
- )
99
+ streams = ui_streams
100
+ @ui = nil unless @ui_streams && streams.all? { |name, io| @ui_streams[name].equal?(io) }
101
+ @ui_streams = streams
102
+ @ui ||= Console.new(**streams, **ui_options)
103
+ end
104
+
105
+ private
106
+
107
+ # The streams {#ui} presents through, as keywords for {Console#initialize}.
108
+ #
109
+ # A dry-cli command gives its own public `stdout`, `stderr` and `stdin`. Its output streams
110
+ # render dry-cli's own styles; the console writes to the IO beneath them, since it decides
111
+ # colour for itself.
112
+ #
113
+ # @return [Hash{Symbol => IO}] `out:`, `err:` and `input:`
114
+ def ui_streams
115
+ {
116
+ out: UI.raw(respond_to?(:stdout) ? stdout : $stdout),
117
+ err: UI.raw(respond_to?(:stderr) ? stderr : $stderr),
118
+ input: respond_to?(:stdin) ? stdin : $stdin
119
+ }
120
+ end
121
+
122
+ # Options for {Console#initialize} other than its streams. Override it to configure the
123
+ # console {#ui} builds.
124
+ #
125
+ # @example
126
+ # def ui_options = { box_width: 72 }
127
+ #
128
+ # @return [Hash{Symbol => Object}]
129
+ def ui_options
130
+ {}
89
131
  end
90
132
  end
91
133
  end
data/sig/dry/cli/ui.rbs CHANGED
@@ -11,6 +11,11 @@ module Dry
11
11
 
12
12
  def ui: () -> Console
13
13
 
14
+ private def ui_streams: () -> { out: untyped, err: untyped, input: untyped }
15
+ private def ui_options: () -> Hash[Symbol, untyped]
16
+
17
+ def self.raw: (untyped stream) -> untyped
18
+
14
19
  def self.configure: () { (Configuration config) -> void } -> Configuration
15
20
  def self.config: () -> Configuration
16
21
  def self.reset!: () -> void
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-cli-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul