cli-ui 2.2.3 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 140781de33cc19ef1c5bc946a0a887b3707e54475823affad8f5568e98fbc0dd
4
- data.tar.gz: b754caad8da6b0d37ea17d807aef78351518c4f17d511a39decbfb29b0512791
3
+ metadata.gz: 9933dc93d8bd52dd2cd910ecc126476fabe857f44d2ceb411d75ba1db5b6eec6
4
+ data.tar.gz: 4eba28b0b33f775b0e941109529946b1c14b5bebeb09084d6ee69ea0663635c4
5
5
  SHA512:
6
- metadata.gz: 8d841fb206c3de7427c4903aa5bb350241c5fe10bbf39c04cb1980eada288deda4b235bf1d5d99a293ebaa6cd78482dd2f49849d65ddd3e7929d63d2f35894bc
7
- data.tar.gz: 6a99dc71d996abbff3fccdda8ce9ede069a7868ba467d86296bed3575e5726f50f76b65d4d427757f5ddaea211b7926d25045402ae5304e1593ae2fb6bc656fb
6
+ metadata.gz: 50b7afe928e8d18e2159864cd646a0d5be3df82f456506682b061027e5bb047246e97dc22f04610cc2dc06091cb437e0b3bff713b4b3f4244f4b106df0cc6194
7
+ data.tar.gz: 8f4d5c54bdb5dff79d6d588f9082455913a2c6081d9b6ebb8e575839375c05ecb0560d15a3b6b305cda5cdd10e22d0a2fe37772c4a7c67ac507fc0162509f756
data/README.md CHANGED
@@ -162,7 +162,7 @@ CLI::UI.frame_style = :box
162
162
  To style an individual frame:
163
163
 
164
164
  ```ruby
165
- CLI::UI.frame('New Style!', frame_style: :bracket) { puts 'It's pretty cool!' }
165
+ CLI::UI.frame('New Style!', frame_style: :bracket) { puts "It's pretty cool!" }
166
166
  ```
167
167
 
168
168
  The default style - `:box` - is what has been used up until now. The other style - `:bracket` - looks like this:
data/lib/cli/ui/ansi.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'cli/ui'
4
5
 
@@ -45,7 +46,7 @@ module CLI
45
46
  #
46
47
  sig { params(str: String).returns(String) }
47
48
  def strip_codes(str)
48
- str.gsub(/\x1b\[[\d;]+[A-z]|\r/, '')
49
+ str.gsub(/\x1b\[[\d;]+[A-Za-z]|\x1b\][\d;]+.*?\x1b\\|\r/, '')
49
50
  end
50
51
 
51
52
  # Returns an ANSI control sequence
@@ -145,7 +146,7 @@ module CLI
145
146
 
146
147
  sig { returns(Regexp) }
147
148
  def match_alternate_screen
148
- /#{Regexp.escape(control('?1049', ''))}[hl]/
149
+ /#{Regexp.escape(control("?1049", ""))}[hl]/
149
150
  end
150
151
 
151
152
  # Show the cursor
@@ -187,13 +188,34 @@ module CLI
187
188
  #
188
189
  sig { returns(String) }
189
190
  def previous_line
190
- cursor_up + cursor_horizontal_absolute
191
+ previous_lines(1)
192
+ end
193
+
194
+ # Move to the previous n lines
195
+ #
196
+ # ==== Attributes
197
+ #
198
+ # * +n+ - number of lines by which to move the cursor up
199
+ #
200
+ sig { params(n: Integer).returns(String) }
201
+ def previous_lines(n = 1)
202
+ cursor_up(n) + cursor_horizontal_absolute
191
203
  end
192
204
 
193
205
  sig { returns(String) }
194
206
  def clear_to_end_of_line
195
207
  control('', 'K')
196
208
  end
209
+
210
+ sig { returns(String) }
211
+ def insert_line
212
+ insert_lines(1)
213
+ end
214
+
215
+ sig { params(n: Integer).returns(String) }
216
+ def insert_lines(n = 1)
217
+ control(n.to_s, 'L')
218
+ end
197
219
  end
198
220
  end
199
221
  end
data/lib/cli/ui/color.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'cli/ui'
4
5
 
@@ -42,6 +43,8 @@ module CLI
42
43
 
43
44
  # 240 is very dark gray; 255 is very light gray. 244 is somewhat dark.
44
45
  GRAY = new('38;5;244', :gray)
46
+ # Using color 214 from the 256-color palette for a more distinct orange
47
+ ORANGE = new('38;5;214', :orange)
45
48
 
46
49
  MAP = {
47
50
  red: RED,
@@ -28,6 +28,7 @@ module CLI
28
28
  'bold' => '1',
29
29
  'italic' => '3',
30
30
  'underline' => '4',
31
+ 'strikethrough' => '9',
31
32
  'reset' => '0',
32
33
 
33
34
  # semantic
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  module CLI
4
5
  module UI
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  module CLI
4
5
  module UI
@@ -94,7 +95,8 @@ module CLI
94
95
 
95
96
  preamble = +''
96
97
 
97
- preamble << color.code << first << (HORIZONTAL * 2)
98
+ preamble << color.code if CLI::UI.enable_color?
99
+ preamble << first << (HORIZONTAL * 2)
98
100
 
99
101
  unless text.empty?
100
102
  preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' '
@@ -128,18 +130,17 @@ module CLI
128
130
 
129
131
  o = +''
130
132
 
131
- # Shopify's CI system supports terminal emulation, but not some of
132
- # the fancier features that we normally use to draw frames
133
- # extra-reliably, so we fall back to a less foolproof strategy. This
134
- # is probably better in general for cases with impoverished terminal
135
- # emulators and no active user.
136
- unless [0, '', nil].include?(ENV['CI'])
133
+ unless CLI::UI.enable_cursor?
137
134
  linewidth = [0, termwidth - (preamble_end + suffix_width + 1)].max
138
135
 
139
- o << color.code << preamble
140
- o << color.code << (HORIZONTAL * linewidth)
141
- o << color.code << suffix
142
- o << CLI::UI::Color::RESET.code << "\n"
136
+ o << color.code if CLI::UI.enable_color?
137
+ o << preamble
138
+ o << color.code if CLI::UI.enable_color?
139
+ o << (HORIZONTAL * linewidth)
140
+ o << color.code if CLI::UI.enable_color?
141
+ o << suffix
142
+ o << CLI::UI::Color::RESET.code if CLI::UI.enable_color?
143
+ o << "\n"
143
144
  return o
144
145
  end
145
146
 
@@ -158,12 +159,12 @@ module CLI
158
159
  # | | | | |
159
160
  # V V V V V
160
161
  # --- Preamble text --------------------- suffix text --
161
- o << color.code
162
+ o << color.code if CLI::UI.enable_color?
162
163
  o << print_at_x(preamble_start, HORIZONTAL * (termwidth - preamble_start)) # draw a full line
163
164
  o << print_at_x(preamble_start, preamble)
164
- o << color.code
165
+ o << color.code if CLI::UI.enable_color?
165
166
  o << print_at_x(suffix_start, suffix)
166
- o << CLI::UI::Color::RESET.code
167
+ o << CLI::UI::Color::RESET.code if CLI::UI.enable_color?
167
168
  o << CLI::UI::ANSI.show_cursor
168
169
  o << "\n"
169
170
 
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  module CLI
4
5
  module UI
@@ -94,7 +95,8 @@ module CLI
94
95
 
95
96
  preamble = +''
96
97
 
97
- preamble << color.code << first << (HORIZONTAL * 2)
98
+ preamble << color.code if CLI::UI.enable_color?
99
+ preamble << first << (HORIZONTAL * 2)
98
100
 
99
101
  unless text.empty?
100
102
  preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' '
@@ -108,15 +110,12 @@ module CLI
108
110
 
109
111
  o = +''
110
112
 
111
- # Shopify's CI system supports terminal emulation, but not some of
112
- # the fancier features that we normally use to draw frames
113
- # extra-reliably, so we fall back to a less foolproof strategy. This
114
- # is probably better in general for cases with impoverished terminal
115
- # emulators and no active user.
116
- unless [0, '', nil].include?(ENV['CI'])
117
- o << color.code << preamble
118
- o << color.code << suffix
119
- o << CLI::UI::Color::RESET.code
113
+ unless CLI::UI.enable_cursor?
114
+ o << color.code if CLI::UI.enable_color?
115
+ o << preamble
116
+ o << color.code if CLI::UI.enable_color?
117
+ o << suffix
118
+ o << CLI::UI::Color::RESET.code if CLI::UI.enable_color?
120
119
  o << "\n"
121
120
 
122
121
  return o
@@ -134,9 +133,11 @@ module CLI
134
133
  # reset to column 1 so that things like ^C don't ruin formatting
135
134
  o << "\r"
136
135
 
137
- o << color.code
138
- o << print_at_x(preamble_start, preamble + color.code + suffix)
139
- o << CLI::UI::Color::RESET.code
136
+ o << color.code if CLI::UI.enable_color?
137
+ o << print_at_x(preamble_start, preamble)
138
+ o << color.code if CLI::UI.enable_color?
139
+ o << suffix
140
+ o << CLI::UI::Color::RESET.code if CLI::UI.enable_color?
140
141
  o << CLI::UI::ANSI.show_cursor
141
142
  o << "\n"
142
143
 
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'cli/ui/frame'
4
5
 
@@ -106,8 +107,8 @@ module CLI
106
107
  sig { returns(String) }
107
108
  def message
108
109
  keys = FrameStyle::MAP.keys.map(&:inspect).join(', ')
109
- "invalid frame style: #{@name.inspect}" \
110
- ' -- must be one of CLI::UI::Frame::FrameStyle::MAP ' \
110
+ "invalid frame style: #{@name.inspect} " \
111
+ '-- must be one of CLI::UI::Frame::FrameStyle::MAP ' \
111
112
  "(#{keys})"
112
113
  end
113
114
  end
data/lib/cli/ui/frame.rb CHANGED
@@ -1,6 +1,5 @@
1
- # coding: utf-8
2
-
3
1
  # typed: true
2
+ # frozen_string_literal: true
4
3
 
5
4
  require 'cli/ui'
6
5
  require 'cli/ui/frame/frame_stack'
@@ -250,19 +249,21 @@ module CLI
250
249
  #
251
250
  sig { params(color: T.nilable(Colorable)).returns(String) }
252
251
  def prefix(color: Thread.current[:cliui_frame_color_override])
253
- +''.tap do |output|
252
+ (+'').tap do |output|
254
253
  items = FrameStack.items
255
254
 
256
255
  items[0..-2].to_a.each do |item|
257
- output << item.color.code << item.frame_style.prefix
256
+ output << item.color.code if CLI::UI.enable_color?
257
+ output << item.frame_style.prefix
258
+ output << CLI::UI::Color::RESET.code if CLI::UI.enable_color?
258
259
  end
259
260
 
260
261
  if (item = items.last)
261
262
  final_color = color || item.color
262
- output << CLI::UI.resolve_color(final_color).code \
263
- << item.frame_style.prefix \
264
- << ' ' \
265
- << CLI::UI::Color::RESET.code
263
+ output << CLI::UI.resolve_color(final_color).code if CLI::UI.enable_color?
264
+ output << item.frame_style.prefix
265
+ output << CLI::UI::Color::RESET.code if CLI::UI.enable_color?
266
+ output << ' '
266
267
  end
267
268
  end
268
269
  end
data/lib/cli/ui/glyph.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'cli/ui'
4
5
 
@@ -63,7 +64,7 @@ module CLI
63
64
  X = new('x', 0x2717, 'X', Color::RED) # RED BALLOT X (✗)
64
65
  BUG = new('b', 0x1f41b, '!', Color::WHITE) # Bug emoji (🐛)
65
66
  CHEVRON = new('>', 0xbb, '»', Color::YELLOW) # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (»)
66
- HOURGLASS = new('H', [0x231b, 0xfe0e], 'H', Color::BLUE) # HOURGLASS + VARIATION SELECTOR 15 (⌛︎)
67
+ HOURGLASS = new('H', 0x29d6, 'H', Color::ORANGE) # HOURGLASS ()
67
68
  WARNING = new('!', [0x26a0, 0xfe0f], '!', Color::YELLOW) # WARNING SIGN + VARIATION SELECTOR 16 (⚠️ )
68
69
 
69
70
  class << self
data/lib/cli/ui/os.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'rbconfig'
4
5
 
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'cli/ui'
4
5
 
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'cli/ui'
4
5
 
@@ -35,13 +36,23 @@ module CLI
35
36
  # CLI::UI::Progress.progress do |bar|
36
37
  # bar.tick(percent: 0.05)
37
38
  # end
39
+ #
40
+ # Update the title
41
+ # CLI::UI::Progress.progress('Title') do |bar|
42
+ # bar.tick(percent: 0.05)
43
+ # bar.update_title('New title')
44
+ # end
38
45
  sig do
39
46
  type_parameters(:T)
40
- .params(width: Integer, block: T.proc.params(bar: Progress).returns(T.type_parameter(:T)))
47
+ .params(
48
+ title: T.nilable(String),
49
+ width: Integer,
50
+ block: T.proc.params(bar: Progress).returns(T.type_parameter(:T)),
51
+ )
41
52
  .returns(T.type_parameter(:T))
42
53
  end
43
- def progress(width: Terminal.width, &block)
44
- bar = Progress.new(width: width)
54
+ def progress(title = nil, width: Terminal.width, &block)
55
+ bar = Progress.new(title, width: width)
45
56
  print(CLI::UI::ANSI.hide_cursor)
46
57
  yield(bar)
47
58
  ensure
@@ -55,13 +66,14 @@ module CLI
55
66
  # Initialize a progress bar. Typically used in a +Progress.progress+ block
56
67
  #
57
68
  # ==== Options
58
- # One of the follow can be used, but not both together
59
69
  #
70
+ # * +:title+ - The title of the progress bar
60
71
  # * +:width+ - The width of the terminal
61
72
  #
62
- sig { params(width: Integer).void }
63
- def initialize(width: Terminal.width)
73
+ sig { params(title: T.nilable(String), width: Integer).void }
74
+ def initialize(title = nil, width: Terminal.width)
64
75
  @percent_done = T.let(0, Numeric)
76
+ @title = title
65
77
  @max_width = width
66
78
  end
67
79
 
@@ -84,7 +96,20 @@ module CLI
84
96
  @percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0
85
97
 
86
98
  print(self)
87
- print(CLI::UI::ANSI.previous_line + "\n")
99
+
100
+ printed_lines = @title ? 2 : 1
101
+ print(CLI::UI::ANSI.previous_lines(printed_lines) + "\n")
102
+ end
103
+
104
+ # Update the progress bar title
105
+ #
106
+ # ==== Attributes
107
+ #
108
+ # * +new_title+ - title to change the progress bar to
109
+ #
110
+ sig { params(new_title: String).void }
111
+ def update_title(new_title)
112
+ @title = new_title
88
113
  end
89
114
 
90
115
  # Format the progress bar to be printed to terminal
@@ -96,11 +121,14 @@ module CLI
96
121
  filled = [(@percent_done * workable_width.to_f).ceil, 0].max
97
122
  unfilled = [workable_width - filled, 0].max
98
123
 
99
- CLI::UI.resolve_text([
124
+ title = CLI::UI.resolve_text(@title, truncate_to: @max_width - Frame.prefix_width) if @title
125
+ bar = CLI::UI.resolve_text([
100
126
  FILLED_BAR + ' ' * filled,
101
127
  UNFILLED_BAR + ' ' * unfilled,
102
128
  CLI::UI::Color::RESET.code + suffix,
103
129
  ].join)
130
+
131
+ [title, bar].compact.join("\n")
104
132
  end
105
133
  end
106
134
  end
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
-
3
2
  # typed: true
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'io/console'
6
6
 
@@ -59,7 +59,11 @@ module CLI
59
59
  end
60
60
  def initialize(options, multiple: false, default: nil)
61
61
  @options = options
62
- @active = 1
62
+ @active = if default && (i = options.index(default))
63
+ i + 1
64
+ else
65
+ 1
66
+ end
63
67
  @marker = '>'
64
68
  @answer = nil
65
69
  @state = :root
@@ -114,23 +118,20 @@ module CLI
114
118
  # determine how many extra lines would be taken up by them.
115
119
  #
116
120
  # To accomplish this we split the string by new lines and add the
117
- # extra characters to the first line.
118
- # Then we calculate how many lines would be needed to render the string
119
- # based on the terminal width
120
- # 3 = space before the number, the . after the number, the space after the .
121
- # multiple check is for the space for the checkbox, if rendered
122
- # options.count.to_s.size gets us the max size of the number we will display
123
- extra_chars = @marker.length + 3 + @options.count.to_s.size + (@multiple ? 1 : 0)
121
+ # prefix to the first line. We use the options count as the number since
122
+ # it will be the widest number we will display, and we pad the others to
123
+ # align with it. Then we calculate how many lines would be needed to
124
+ # render the string based on the terminal width.
125
+ prefix = "#{@marker} #{@options.count}. #{@multiple ? "☐ " : ""}"
124
126
 
125
127
  @option_lengths = @options.map do |text|
126
128
  next 1 if text.empty?
127
129
 
128
130
  # Find the length of all the lines in this string
129
- non_empty_line_lengths = text.split("\n").reject(&:empty?).map do |line|
131
+ non_empty_line_lengths = "#{prefix}#{text}".split("\n").reject(&:empty?).map do |line|
130
132
  CLI::UI.fmt(line, enable_color: false).length
131
133
  end
132
- # The first line has the marker and number, so we add that so we can take it into account
133
- non_empty_line_lengths[0] += extra_chars
134
+
134
135
  # Finally, we need to calculate how many lines each one will take. We can do that by dividing each one
135
136
  # by the width of the terminal, rounding up to the nearest natural number
136
137
  non_empty_line_lengths.sum { |length| (length.to_f / @terminal_width_at_calculation_time).ceil }
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  module CLI
4
5
  module UI
data/lib/cli/ui/prompt.rb CHANGED
@@ -1,23 +1,13 @@
1
1
  # coding: utf-8
2
-
3
2
  # typed: true
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'cli/ui'
6
- require 'readline'
7
-
8
- module Readline
9
- unless const_defined?(:FILENAME_COMPLETION_PROC)
10
- FILENAME_COMPLETION_PROC = proc do |input|
11
- directory = input[-1] == '/' ? input : File.dirname(input)
12
- filename = input[-1] == '/' ? '' : File.basename(input)
13
-
14
- (Dir.entries(directory).select do |fp|
15
- fp.start_with?(filename)
16
- end - (input[-1] == '.' ? [] : ['.', '..'])).map do |fp|
17
- File.join(directory, fp).gsub(/\A\.\//, '')
18
- end
19
- end
20
- end
6
+ begin
7
+ require 'reline' # For 2.7+
8
+ rescue LoadError
9
+ require 'readline' # For 2.6
10
+ Object.const_set(:Reline, Readline)
21
11
  end
22
12
 
23
13
  module CLI
@@ -137,10 +127,6 @@ module CLI
137
127
  &options_proc
138
128
  )
139
129
  has_options = !!(options || block_given?)
140
- if has_options && default && !multiple
141
- raise(ArgumentError, 'conflicting arguments: default may not be provided with options when not multiple')
142
- end
143
-
144
130
  if has_options && is_file
145
131
  raise(ArgumentError, 'conflicting arguments: is_file is only useful when options are not provided')
146
132
  end
@@ -310,6 +296,8 @@ module CLI
310
296
  instructions += ", filter with 'f'" if filter_ui
311
297
  instructions += ", enter option with 'e'" if select_ui && (options.size > 9)
312
298
 
299
+ resp = T.let([], T.any(String, T::Array[String]))
300
+
313
301
  CLI::UI::StdoutRouter::Capture.in_alternate_screen do
314
302
  puts_question("#{question} " + instructions_color.code + "(#{instructions})" + Color::RESET.code)
315
303
  resp = interactive_prompt(options, multiple: multiple, default: default)
@@ -334,12 +322,12 @@ module CLI
334
322
  resp
335
323
  end
336
324
  puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
325
+ end
337
326
 
338
- if block_given?
339
- T.must(handler).call(resp)
340
- else
341
- resp
342
- end
327
+ if block_given?
328
+ T.must(handler).call(resp)
329
+ else
330
+ resp
343
331
  end
344
332
  end
345
333
 
@@ -375,11 +363,20 @@ module CLI
375
363
  sig { params(is_file: T::Boolean).returns(String) }
376
364
  def readline(is_file: false)
377
365
  if is_file
378
- Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
379
- Readline.completion_append_character = ''
366
+ Reline.completion_proc = proc do |input|
367
+ directory = input[-1] == '/' ? input : File.dirname(input)
368
+ filename = input[-1] == '/' ? '' : File.basename(input)
369
+
370
+ (Dir.entries(directory).select do |fp|
371
+ fp.start_with?(filename)
372
+ end - (input[-1] == '.' ? [] : ['.', '..'])).map do |fp|
373
+ File.join(directory, fp).gsub(/\A\.\//, '')
374
+ end
375
+ end
376
+ Reline.completion_append_character = ''
380
377
  else
381
- Readline.completion_proc = proc { |*| nil }
382
- Readline.completion_append_character = ' '
378
+ Reline.completion_proc = proc {}
379
+ Reline.completion_append_character = ' '
383
380
  end
384
381
 
385
382
  # because Readline is a C library, CLI::UI's hooks into $stdout don't
@@ -393,7 +390,7 @@ module CLI
393
390
  prompt += CLI::UI::Color::YELLOW.code if CLI::UI::OS.current.use_color_prompt?
394
391
 
395
392
  begin
396
- line = Readline.readline(prompt, true)
393
+ line = Reline.readline(prompt, true)
397
394
  print(CLI::UI::Color::RESET.code)
398
395
  line.to_s.chomp
399
396
  rescue Interrupt
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  module CLI
4
5
  module UI