cli-ui 1.5.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,21 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui'
2
4
  require 'stringio'
3
5
 
4
6
  module CLI
5
7
  module UI
6
8
  module StdoutRouter
7
- class << self
8
- attr_accessor :duplicate_output_to
9
- end
10
-
11
9
  class Writer
10
+ extend T::Sig
11
+
12
+ sig { params(stream: IOLike, name: Symbol).void }
12
13
  def initialize(stream, name)
13
14
  @stream = stream
14
15
  @name = name
15
16
  end
16
17
 
18
+ sig { params(args: String).void }
17
19
  def write(*args)
18
20
  args = args.map do |str|
19
21
  if auto_frame_inset?
@@ -31,34 +33,42 @@ module CLI
31
33
  return if hook.call(args.map(&:to_s).join, @name) == false
32
34
  end
33
35
 
34
- @stream.write_without_cli_ui(*prepend_id(@stream, args))
36
+ T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream, args))
35
37
  if (dup = StdoutRouter.duplicate_output_to)
36
- dup.write(*prepend_id(dup, args))
38
+ T.unsafe(dup).write(*prepend_id(dup, args))
37
39
  end
38
40
  end
39
41
 
40
42
  private
41
43
 
44
+ sig { params(stream: IOLike, args: T::Array[String]).returns(T::Array[String]) }
42
45
  def prepend_id(stream, args)
43
46
  return args unless prepend_id_for_stream(stream)
47
+
44
48
  args.map do |a|
45
49
  next a if a.chomp.empty? # allow new lines to be new lines
50
+
46
51
  "[#{Thread.current[:cliui_output_id][:id]}] #{a}"
47
52
  end
48
53
  end
49
54
 
55
+ sig { params(stream: IOLike).returns(T::Boolean) }
50
56
  def prepend_id_for_stream(stream)
51
57
  return false unless Thread.current[:cliui_output_id]
52
58
  return true if Thread.current[:cliui_output_id][:streams].include?(stream)
59
+
53
60
  false
54
61
  end
55
62
 
63
+ sig { returns(T::Boolean) }
56
64
  def auto_frame_inset?
57
65
  !Thread.current[:no_cliui_frame_inset]
58
66
  end
59
67
 
68
+ sig { params(str: String, prefix: String).returns(String) }
60
69
  def apply_line_prefix(str, prefix)
61
70
  return '' if str.empty?
71
+
62
72
  prefixed = +''
63
73
  str.force_encoding(Encoding::UTF_8).lines.each do |line|
64
74
  if @pending_newline
@@ -74,39 +84,52 @@ module CLI
74
84
  end
75
85
 
76
86
  class Capture
87
+ extend T::Sig
88
+
77
89
  @m = Mutex.new
78
90
  @active_captures = 0
79
91
  @saved_stdin = nil
80
92
 
81
- def self.with_stdin_masked
82
- @m.synchronize do
83
- if @active_captures.zero?
84
- @saved_stdin = $stdin
85
- $stdin, w = IO.pipe
86
- $stdin.close
87
- w.close
93
+ class << self
94
+ extend T::Sig
95
+
96
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
97
+ def with_stdin_masked(&block)
98
+ @m.synchronize do
99
+ if @active_captures.zero?
100
+ @saved_stdin = $stdin
101
+ $stdin, w = IO.pipe
102
+ $stdin.close
103
+ w.close
104
+ end
105
+ @active_captures += 1
88
106
  end
89
- @active_captures += 1
90
- end
91
107
 
92
- yield
93
- ensure
94
- @m.synchronize do
95
- @active_captures -= 1
96
- if @active_captures.zero?
97
- $stdin = @saved_stdin
108
+ yield
109
+ ensure
110
+ @m.synchronize do
111
+ @active_captures -= 1
112
+ if @active_captures.zero?
113
+ $stdin = @saved_stdin
114
+ end
98
115
  end
99
116
  end
100
117
  end
101
118
 
102
- def initialize(*block_args, with_frame_inset: true, &block)
119
+ sig do
120
+ params(with_frame_inset: T::Boolean, block: T.proc.void).void
121
+ end
122
+ def initialize(with_frame_inset: true, &block)
103
123
  @with_frame_inset = with_frame_inset
104
- @block_args = block_args
105
124
  @block = block
125
+ @stdout = ''
126
+ @stderr = ''
106
127
  end
107
128
 
129
+ sig { returns(String) }
108
130
  attr_reader :stdout, :stderr
109
131
 
132
+ sig { returns(T.untyped) }
110
133
  def run
111
134
  require 'stringio'
112
135
 
@@ -134,7 +157,7 @@ module CLI
134
157
  end
135
158
 
136
159
  begin
137
- @block.call(*@block_args)
160
+ @block.call
138
161
  ensure
139
162
  @stdout = out.string
140
163
  @stderr = err.string
@@ -147,39 +170,44 @@ module CLI
147
170
  end
148
171
 
149
172
  class << self
173
+ extend T::Sig
174
+
150
175
  WRITE_WITHOUT_CLI_UI = :write_without_cli_ui
151
176
 
152
177
  NotEnabled = Class.new(StandardError)
153
178
 
154
- def with_id(on_streams:)
155
- unless on_streams.is_a?(Array) && on_streams.all? { |s| s.respond_to?(:write) }
156
- raise ArgumentError, <<~EOF
157
- on_streams must be an array of objects that respond to `write`
158
- These do not respond to write
159
- #{on_streams.reject { |s| s.respond_to?(:write) }.map.with_index { |s| s.class.to_s }.join("\n")}
160
- EOF
161
- end
179
+ sig { returns(T.nilable(IOLike)) }
180
+ attr_accessor :duplicate_output_to
162
181
 
182
+ sig do
183
+ type_parameters(:T)
184
+ .params(on_streams: T::Array[IOLike], block: T.proc.params(id: String).returns(T.type_parameter(:T)))
185
+ .returns(T.type_parameter(:T))
186
+ end
187
+ def with_id(on_streams:, &block)
163
188
  require 'securerandom'
164
189
  id = format('%05d', rand(10**5))
165
190
  Thread.current[:cliui_output_id] = {
166
191
  id: id,
167
- streams: on_streams,
192
+ streams: on_streams.map { |stream| T.cast(stream, IOLike) },
168
193
  }
169
194
  yield(id)
170
195
  ensure
171
196
  Thread.current[:cliui_output_id] = nil
172
197
  end
173
198
 
199
+ sig { returns(T.nilable(T::Hash[Symbol, T.any(String, IOLike)])) }
174
200
  def current_id
175
201
  Thread.current[:cliui_output_id]
176
202
  end
177
203
 
204
+ sig { void }
178
205
  def assert_enabled!
179
206
  raise NotEnabled unless enabled?
180
207
  end
181
208
 
182
- def with_enabled
209
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
210
+ def with_enabled(&block)
183
211
  enable
184
212
  yield
185
213
  ensure
@@ -187,23 +215,29 @@ module CLI
187
215
  end
188
216
 
189
217
  # TODO: remove this
218
+ sig { void }
190
219
  def ensure_activated
191
220
  enable unless enabled?
192
221
  end
193
222
 
223
+ sig { returns(T::Boolean) }
194
224
  def enable
195
225
  return false if enabled?($stdout) || enabled?($stderr)
226
+
196
227
  activate($stdout, :stdout)
197
228
  activate($stderr, :stderr)
198
229
  true
199
230
  end
200
231
 
232
+ sig { params(stream: IOLike).returns(T::Boolean) }
201
233
  def enabled?(stream = $stdout)
202
234
  stream.respond_to?(WRITE_WITHOUT_CLI_UI)
203
235
  end
204
236
 
237
+ sig { returns(T::Boolean) }
205
238
  def disable
206
239
  return false unless enabled?($stdout) && enabled?($stderr)
240
+
207
241
  deactivate($stdout)
208
242
  deactivate($stderr)
209
243
  true
@@ -211,16 +245,19 @@ module CLI
211
245
 
212
246
  private
213
247
 
248
+ sig { params(stream: IOLike).void }
214
249
  def deactivate(stream)
215
250
  sc = stream.singleton_class
216
251
  sc.send(:remove_method, :write)
217
252
  sc.send(:alias_method, :write, WRITE_WITHOUT_CLI_UI)
218
253
  end
219
254
 
255
+ sig { params(stream: IOLike, streamname: Symbol).void }
220
256
  def activate(stream, streamname)
221
257
  writer = StdoutRouter::Writer.new(stream, streamname)
222
258
 
223
259
  raise if stream.respond_to?(WRITE_WITHOUT_CLI_UI)
260
+
224
261
  stream.singleton_class.send(:alias_method, WRITE_WITHOUT_CLI_UI, :write)
225
262
  stream.define_singleton_method(:write) do |*args|
226
263
  writer.write(*args)
@@ -1,44 +1,56 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui'
2
4
  require 'io/console'
3
5
 
4
6
  module CLI
5
7
  module UI
6
8
  module Terminal
9
+ extend T::Sig
10
+
7
11
  DEFAULT_WIDTH = 80
8
12
  DEFAULT_HEIGHT = 24
9
13
 
10
- # Returns the width of the terminal, if possible
11
- # Otherwise will return DEFAULT_WIDTH
12
- #
13
- def self.width
14
- winsize[1]
15
- end
14
+ class << self
15
+ extend T::Sig
16
16
 
17
- # Returns the width of the terminal, if possible
18
- # Otherwise, will return DEFAULT_HEIGHT
19
- #
20
- def self.height
21
- winsize[0]
22
- end
17
+ # Returns the width of the terminal, if possible
18
+ # Otherwise will return DEFAULT_WIDTH
19
+ #
20
+ sig { returns(Integer) }
21
+ def width
22
+ winsize[1]
23
+ end
23
24
 
24
- def self.winsize
25
- @winsize ||= begin
26
- winsize = IO.console.winsize
27
- setup_winsize_trap
25
+ # Returns the width of the terminal, if possible
26
+ # Otherwise, will return DEFAULT_HEIGHT
27
+ #
28
+ sig { returns(Integer) }
29
+ def height
30
+ winsize[0]
31
+ end
32
+
33
+ sig { returns([Integer, Integer]) }
34
+ def winsize
35
+ @winsize ||= begin
36
+ winsize = IO.console.winsize
37
+ setup_winsize_trap
28
38
 
29
- if winsize.any?(&:zero?)
39
+ if winsize.any?(&:zero?)
40
+ [DEFAULT_HEIGHT, DEFAULT_WIDTH]
41
+ else
42
+ winsize
43
+ end
44
+ rescue
30
45
  [DEFAULT_HEIGHT, DEFAULT_WIDTH]
31
- else
32
- winsize
33
46
  end
34
- rescue
35
- [DEFAULT_HEIGHT, DEFAULT_WIDTH]
36
47
  end
37
- end
38
48
 
39
- def self.setup_winsize_trap
40
- @winsize_trap ||= Signal.trap('WINCH') do
41
- @winsize = nil
49
+ sig { void }
50
+ def setup_winsize_trap
51
+ @winsize_trap ||= Signal.trap('WINCH') do
52
+ @winsize = nil
53
+ end
42
54
  end
43
55
  end
44
56
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require 'cli/ui'
@@ -27,12 +28,15 @@ module CLI
27
28
  TRUNCATED = "\x1b[0m…"
28
29
 
29
30
  class << self
31
+ extend T::Sig
32
+
33
+ sig { params(text: String, printing_width: Integer).returns(String) }
30
34
  def call(text, printing_width)
31
35
  return text if text.size <= printing_width
32
36
 
33
37
  width = 0
34
38
  mode = PARSE_ROOT
35
- truncation_index = nil
39
+ truncation_index = T.let(nil, T.nilable(Integer))
36
40
 
37
41
  codepoints = text.codepoints
38
42
  codepoints.each.with_index do |cp, index|
@@ -83,11 +87,12 @@ module CLI
83
87
  # the end of the string.
84
88
  return text if !truncation_index || width <= printing_width
85
89
 
86
- codepoints[0...truncation_index].pack('U*') + TRUNCATED
90
+ T.must(codepoints[0...truncation_index]).pack('U*') + TRUNCATED
87
91
  end
88
92
 
89
93
  private
90
94
 
95
+ sig { params(printable_codepoint: Integer).returns(Integer) }
91
96
  def width(printable_codepoint)
92
97
  case printable_codepoint
93
98
  when EMOJI_RANGE
@@ -1,5 +1,7 @@
1
+ # typed: true
2
+
1
3
  module CLI
2
4
  module UI
3
- VERSION = '1.5.1'
5
+ VERSION = '2.1.0'
4
6
  end
5
7
  end
@@ -1,26 +1,45 @@
1
+ # typed: true
2
+
1
3
  require('cli/ui')
2
4
 
3
5
  module CLI
4
6
  module UI
5
7
  module Widgets
6
8
  class Base
7
- def self.call(argstring)
8
- new(argstring).render
9
+ extend T::Sig
10
+ extend T::Helpers
11
+ abstract!
12
+
13
+ class << self
14
+ extend T::Sig
15
+
16
+ sig { params(argstring: String).returns(String) }
17
+ def call(argstring)
18
+ new(argstring).render
19
+ end
9
20
  end
10
21
 
22
+ sig { params(argstring: String).void }
11
23
  def initialize(argstring)
12
24
  pat = self.class.argparse_pattern
13
25
  unless (@match_data = pat.match(argstring))
14
26
  raise(Widgets::InvalidWidgetArguments.new(argstring, pat))
15
27
  end
28
+
16
29
  @match_data.names.each do |name|
17
30
  instance_variable_set(:"@#{name}", @match_data[name])
18
31
  end
19
32
  end
20
33
 
21
- def self.argparse_pattern
22
- const_get(:ARGPARSE_PATTERN)
34
+ class << self
35
+ extend T::Sig
36
+
37
+ sig { abstract.returns(Regexp) }
38
+ def argparse_pattern; end
23
39
  end
40
+
41
+ sig { abstract.returns(String) }
42
+ def render; end
24
43
  end
25
44
  end
26
45
  end
@@ -1,4 +1,6 @@
1
- # frozen-string-literal: true
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
2
4
  require('cli/ui')
3
5
 
4
6
  module CLI
@@ -19,6 +21,16 @@ module CLI
19
21
  SPINNER_STOPPED = '⠿'
20
22
  EMPTY_SET = '∅'
21
23
 
24
+ class << self
25
+ extend T::Sig
26
+
27
+ sig { override.returns(Regexp) }
28
+ def argparse_pattern
29
+ ARGPARSE_PATTERN
30
+ end
31
+ end
32
+
33
+ sig { override.returns(String) }
22
34
  def render
23
35
  if zero?(@succeeded) && zero?(@failed) && zero?(@working) && zero?(@pending)
24
36
  Color::RESET.code + Color::BOLD.code + EMPTY_SET + Color::RESET.code
@@ -30,28 +42,34 @@ module CLI
30
42
 
31
43
  private
32
44
 
45
+ sig { params(num_str: String).returns(T::Boolean) }
33
46
  def zero?(num_str)
34
47
  num_str == '0'
35
48
  end
36
49
 
50
+ sig { params(num_str: String, rune: String, color: Color).returns(String) }
37
51
  def colorize_if_nonzero(num_str, rune, color)
38
52
  color = Color::GRAY if zero?(num_str)
39
53
  color.code + num_str + rune
40
54
  end
41
55
 
56
+ sig { returns(String) }
42
57
  def succeeded_part
43
58
  colorize_if_nonzero(@succeeded, Glyph::CHECK.char, Color::GREEN)
44
59
  end
45
60
 
61
+ sig { returns(String) }
46
62
  def failed_part
47
63
  colorize_if_nonzero(@failed, Glyph::X.char, Color::RED)
48
64
  end
49
65
 
66
+ sig { returns(String) }
50
67
  def working_part
51
68
  rune = zero?(@working) ? SPINNER_STOPPED : Spinner.current_rune
52
69
  colorize_if_nonzero(@working, rune, Color::BLUE)
53
70
  end
54
71
 
72
+ sig { returns(String) }
55
73
  def pending_part
56
74
  colorize_if_nonzero(@pending, Glyph::HOURGLASS.char, Color::WHITE)
57
75
  end
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  require('cli/ui')
2
4
 
3
5
  module CLI
@@ -16,57 +18,74 @@ module CLI
16
18
  # CLI::UI::Widgets.register('my-widget') { MyWidget }
17
19
  # puts(CLI::UI.fmt("{{@widget/my-widget:args}}"))
18
20
  module Widgets
21
+ extend T::Sig
22
+
19
23
  MAP = {}
20
24
 
21
25
  autoload(:Base, 'cli/ui/widgets/base')
26
+ autoload(:Status, 'cli/ui/widgets/status')
22
27
 
23
- def self.register(name, &cb)
24
- MAP[name] = cb
25
- end
28
+ class << self
29
+ extend T::Sig
26
30
 
27
- autoload(:Status, 'cli/ui/widgets/status')
28
- register('status') { Widgets::Status }
31
+ sig { params(name: String, cb: T.proc.returns(T.class_of(Widgets::Base))).void }
32
+ def register(name, &cb)
33
+ MAP[name] = cb
34
+ end
29
35
 
30
- # Looks up a widget by handle
31
- #
32
- # ==== Raises
33
- # Raises InvalidWidgetHandle if the widget is not available.
34
- #
35
- # ==== Returns
36
- # A callable widget, to be invoked like `.call(argstring)`
37
- #
38
- def self.lookup(handle)
39
- MAP.fetch(handle.to_s).call
40
- rescue KeyError, NameError
41
- raise(InvalidWidgetHandle, handle)
42
- end
36
+ # Looks up a widget by handle
37
+ #
38
+ # ==== Raises
39
+ # Raises InvalidWidgetHandle if the widget is not available.
40
+ #
41
+ # ==== Returns
42
+ # A callable widget, to be invoked like `.call(argstring)`
43
+ #
44
+ sig { params(handle: String).returns(T.class_of(Widgets::Base)) }
45
+ def lookup(handle)
46
+ MAP.fetch(handle).call
47
+ rescue KeyError, NameError
48
+ raise(InvalidWidgetHandle, handle)
49
+ end
43
50
 
44
- # All available widgets by name
45
- #
46
- def self.available
47
- MAP.keys
51
+ # All available widgets by name
52
+ #
53
+ sig { returns(T::Array[String]) }
54
+ def available
55
+ MAP.keys
56
+ end
48
57
  end
49
58
 
59
+ register('status') { Widgets::Status }
60
+
50
61
  class InvalidWidgetHandle < ArgumentError
62
+ extend T::Sig
63
+
64
+ sig { params(handle: String).void }
51
65
  def initialize(handle)
52
66
  super
53
67
  @handle = handle
54
68
  end
55
69
 
70
+ sig { returns(String) }
56
71
  def message
57
- keys = Widget.available.join(',')
72
+ keys = Widgets.available.join(',')
58
73
  "invalid widget handle: #{@handle} " \
59
74
  "-- must be one of CLI::UI::Widgets.available (#{keys})"
60
75
  end
61
76
  end
62
77
 
63
78
  class InvalidWidgetArguments < ArgumentError
79
+ extend T::Sig
80
+
81
+ sig { params(argstring: String, pattern: Regexp).void }
64
82
  def initialize(argstring, pattern)
65
83
  super
66
84
  @argstring = argstring
67
85
  @pattern = pattern
68
86
  end
69
87
 
88
+ sig { returns(String) }
70
89
  def message
71
90
  "invalid widget arguments: #{@argstring} " \
72
91
  "-- must match pattern: #{@pattern.inspect}"
data/lib/cli/ui/wrap.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  # coding: utf-8
2
+
3
+ # typed: true
4
+
2
5
  require 'cli/ui'
3
6
  require 'cli/ui/frame/frame_stack'
4
7
  require 'cli/ui/frame/frame_style'
@@ -6,13 +9,17 @@ require 'cli/ui/frame/frame_style'
6
9
  module CLI
7
10
  module UI
8
11
  class Wrap
12
+ extend T::Sig
13
+
14
+ sig { params(input: String).void }
9
15
  def initialize(input)
10
16
  @input = input
11
17
  end
12
18
 
19
+ sig { returns(String) }
13
20
  def wrap
14
21
  max_width = Terminal.width - Frame.prefix_width
15
- width = 0
22
+ width = T.let(0, Integer)
16
23
  final = []
17
24
  # Create an alternation of format codes of parameter lengths 1-20, since + and {1,n} not allowed in lookbehind
18
25
  format_codes = (1..20).map { |n| /\x1b\[[\d;]{#{n}}m/ }.join('|')