cli-ui 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0.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('|')