cli-ui 2.4.0 → 2.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cafdb9c85ec15ecd6a745f73d8b65379e20f9961b74be9a8dd8790b2162c6580
4
- data.tar.gz: 825406bad23758d820de77588e4b2fa227e0aab72ba7ddb8fe6486bd9ffbffb7
3
+ metadata.gz: 3a1c6da3c7bdbea0fb1cc70677acac4182c40196cf65b69127c40804bed532d4
4
+ data.tar.gz: 6ba1a30edf06fdb24d53869a70ba505f492a4ea70792fe5bfd0a3cddc2ca4b50
5
5
  SHA512:
6
- metadata.gz: b27c798109a9e573dd3a126ea65768ccc444bd065eff36da13abfb64617cd5ef31c0904a4b6d81e1d7ddf586e66c9a55dec3a38aba350e2d2e1a27d9a3185444
7
- data.tar.gz: f25439683ba4ed81b09166f47d622fc5d21ae84e7a836169657b0db116bafe9480407ddf30b0e0f689da4a0c6d102e923fb473be59f3276e69977aa3ed861584
6
+ metadata.gz: a514afedeb744836daa7f3df1d20960c0b80fd47483d6f9f5407443c58cca58313363cface002ac7b2dafe6846b53c10507739dd9f72256f0a8742735b165b3e
7
+ data.tar.gz: a8e467997d9b5309787c7bcb4c30bf4d025c4aa968179938c1a673c712ccecceb37f06edfc70daaca4232f9def8f72e102808aed4f2ccfd4da7e245b87f382eb
data/lib/cli/ui/ansi.rb CHANGED
@@ -6,21 +6,22 @@ require 'cli/ui'
6
6
  module CLI
7
7
  module UI
8
8
  module ANSI
9
- extend T::Sig
10
-
11
9
  ESC = "\x1b"
10
+ # https://ghostty.org/docs/vt/concepts/sequences#csi-sequences
11
+ CSI_SEQUENCE = /\x1b\[[\d;:]+[\x20-\x2f]*?[\x40-\x7e]/
12
+ # https://ghostty.org/docs/vt/concepts/sequences#osc-sequences
13
+ # OSC sequences can be terminated with either ST (\x1b\x5c) or BEL (\x07)
14
+ OSC_SEQUENCE = /\x1b\][^\x07\x1b]*?(?:\x07|\x1b\x5c)/
12
15
 
13
16
  class << self
14
- extend T::Sig
15
-
16
17
  # ANSI escape sequences (like \x1b[31m) have zero width.
17
18
  # when calculating the padding width, we must exclude them.
18
19
  # This also implements a basic version of utf8 character width calculation like
19
20
  # we could get for real from something like utf8proc.
20
21
  #
21
- sig { params(str: String).returns(Integer) }
22
+ #: (String str) -> Integer
22
23
  def printing_width(str)
23
- zwj = T.let(false, T::Boolean)
24
+ zwj = false #: bool
24
25
  strip_codes(str).codepoints.reduce(0) do |acc, cp|
25
26
  if zwj
26
27
  zwj = false
@@ -44,9 +45,9 @@ module CLI
44
45
  #
45
46
  # - +str+ - The string from which to strip codes
46
47
  #
47
- sig { params(str: String).returns(String) }
48
+ #: (String str) -> String
48
49
  def strip_codes(str)
49
- str.gsub(/\x1b\[[\d;]+[A-Za-z]|\x1b\][\d;]+.*?\x1b\\|\r/, '')
50
+ str.gsub(Regexp.union(CSI_SEQUENCE, OSC_SEQUENCE, /\r/), '')
50
51
  end
51
52
 
52
53
  # Returns an ANSI control sequence
@@ -56,13 +57,13 @@ module CLI
56
57
  # - +args+ - Argument to pass to the ANSI control sequence
57
58
  # - +cmd+ - ANSI control sequence Command
58
59
  #
59
- sig { params(args: String, cmd: String).returns(String) }
60
+ #: (String args, String cmd) -> String
60
61
  def control(args, cmd)
61
62
  ESC + '[' + args + cmd
62
63
  end
63
64
 
64
65
  # https://en.wikipedia.org/wiki/ANSI_escape_code#graphics
65
- sig { params(params: String).returns(String) }
66
+ #: (String params) -> String
66
67
  def sgr(params)
67
68
  control(params, 'm')
68
69
  end
@@ -75,7 +76,7 @@ module CLI
75
76
  #
76
77
  # * +n+ - number of lines by which to move the cursor up
77
78
  #
78
- sig { params(n: Integer).returns(String) }
79
+ #: (?Integer n) -> String
79
80
  def cursor_up(n = 1)
80
81
  return '' if n.zero?
81
82
 
@@ -88,7 +89,7 @@ module CLI
88
89
  #
89
90
  # * +n+ - number of lines by which to move the cursor down
90
91
  #
91
- sig { params(n: Integer).returns(String) }
92
+ #: (?Integer n) -> String
92
93
  def cursor_down(n = 1)
93
94
  return '' if n.zero?
94
95
 
@@ -101,7 +102,7 @@ module CLI
101
102
  #
102
103
  # * +n+ - number of columns by which to move the cursor forward
103
104
  #
104
- sig { params(n: Integer).returns(String) }
105
+ #: (?Integer n) -> String
105
106
  def cursor_forward(n = 1)
106
107
  return '' if n.zero?
107
108
 
@@ -114,7 +115,7 @@ module CLI
114
115
  #
115
116
  # * +n+ - number of columns by which to move the cursor back
116
117
  #
117
- sig { params(n: Integer).returns(String) }
118
+ #: (?Integer n) -> String
118
119
  def cursor_back(n = 1)
119
120
  return '' if n.zero?
120
121
 
@@ -127,66 +128,66 @@ module CLI
127
128
  #
128
129
  # * +n+ - The column to move to
129
130
  #
130
- sig { params(n: Integer).returns(String) }
131
+ #: (?Integer n) -> String
131
132
  def cursor_horizontal_absolute(n = 1)
132
133
  cmd = control(n.to_s, 'G')
133
134
  cmd += cursor_back if CLI::UI::OS.current.shift_cursor_back_on_horizontal_absolute?
134
135
  cmd
135
136
  end
136
137
 
137
- sig { returns(String) }
138
+ #: -> String
138
139
  def enter_alternate_screen
139
140
  control('?1049', 'h')
140
141
  end
141
142
 
142
- sig { returns(String) }
143
+ #: -> String
143
144
  def exit_alternate_screen
144
145
  control('?1049', 'l')
145
146
  end
146
147
 
147
- sig { returns(Regexp) }
148
+ #: -> Regexp
148
149
  def match_alternate_screen
149
150
  /#{Regexp.escape(control("?1049", ""))}[hl]/
150
151
  end
151
152
 
152
153
  # Show the cursor
153
154
  #
154
- sig { returns(String) }
155
+ #: -> String
155
156
  def show_cursor
156
157
  control('', '?25h')
157
158
  end
158
159
 
159
160
  # Hide the cursor
160
161
  #
161
- sig { returns(String) }
162
+ #: -> String
162
163
  def hide_cursor
163
164
  control('', '?25l')
164
165
  end
165
166
 
166
167
  # Save the cursor position
167
168
  #
168
- sig { returns(String) }
169
+ #: -> String
169
170
  def cursor_save
170
171
  control('', 's')
171
172
  end
172
173
 
173
174
  # Restore the saved cursor position
174
175
  #
175
- sig { returns(String) }
176
+ #: -> String
176
177
  def cursor_restore
177
178
  control('', 'u')
178
179
  end
179
180
 
180
181
  # Move to the next line
181
182
  #
182
- sig { returns(String) }
183
+ #: -> String
183
184
  def next_line
184
185
  cursor_down + cursor_horizontal_absolute
185
186
  end
186
187
 
187
188
  # Move to the previous line
188
189
  #
189
- sig { returns(String) }
190
+ #: -> String
190
191
  def previous_line
191
192
  previous_lines(1)
192
193
  end
@@ -197,22 +198,22 @@ module CLI
197
198
  #
198
199
  # * +n+ - number of lines by which to move the cursor up
199
200
  #
200
- sig { params(n: Integer).returns(String) }
201
+ #: (?Integer n) -> String
201
202
  def previous_lines(n = 1)
202
203
  cursor_up(n) + cursor_horizontal_absolute
203
204
  end
204
205
 
205
- sig { returns(String) }
206
+ #: -> String
206
207
  def clear_to_end_of_line
207
208
  control('', 'K')
208
209
  end
209
210
 
210
- sig { returns(String) }
211
+ #: -> String
211
212
  def insert_line
212
213
  insert_lines(1)
213
214
  end
214
215
 
215
- sig { params(n: Integer).returns(String) }
216
+ #: (?Integer n) -> String
216
217
  def insert_lines(n = 1)
217
218
  control(n.to_s, 'L')
218
219
  end
data/lib/cli/ui/color.rb CHANGED
@@ -6,12 +6,10 @@ require 'cli/ui'
6
6
  module CLI
7
7
  module UI
8
8
  class Color
9
- extend T::Sig
10
-
11
- sig { returns(String) }
9
+ #: String
12
10
  attr_reader :sgr, :code
13
11
 
14
- sig { returns(Symbol) }
12
+ #: Symbol
15
13
  attr_reader :name
16
14
 
17
15
  # Creates a new color mapping
@@ -23,7 +21,7 @@ module CLI
23
21
  # * +sgr+ - The color signature
24
22
  # * +name+ - The name of the color
25
23
  #
26
- sig { params(sgr: String, name: Symbol).void }
24
+ #: (String sgr, Symbol name) -> void
27
25
  def initialize(sgr, name)
28
26
  @sgr = sgr
29
27
  @code = CLI::UI::ANSI.sgr(sgr)
@@ -60,15 +58,13 @@ module CLI
60
58
  }.freeze
61
59
 
62
60
  class InvalidColorName < ArgumentError
63
- extend T::Sig
64
-
65
- sig { params(name: Symbol).void }
61
+ #: (Symbol name) -> void
66
62
  def initialize(name)
67
63
  super
68
64
  @name = name
69
65
  end
70
66
 
71
- sig { returns(String) }
67
+ #: -> String
72
68
  def message
73
69
  keys = Color.available.map(&:inspect).join(',')
74
70
  "invalid color: #{@name.inspect} " \
@@ -77,8 +73,6 @@ module CLI
77
73
  end
78
74
 
79
75
  class << self
80
- extend T::Sig
81
-
82
76
  # Looks up a color code by name
83
77
  #
84
78
  # ==== Raises
@@ -88,7 +82,7 @@ module CLI
88
82
  # ==== Returns
89
83
  # Returns a color code
90
84
  #
91
- sig { params(name: T.any(Symbol, String)).returns(Color) }
85
+ #: ((Symbol | String) name) -> Color
92
86
  def lookup(name)
93
87
  MAP.fetch(name.to_sym)
94
88
  rescue KeyError
@@ -97,7 +91,7 @@ module CLI
97
91
 
98
92
  # All available colors by name
99
93
  #
100
- sig { returns(T::Array[Symbol]) }
94
+ #: -> Array[Symbol]
101
95
  def available
102
96
  MAP.keys
103
97
  end
@@ -7,8 +7,6 @@ require('strscan')
7
7
  module CLI
8
8
  module UI
9
9
  class Formatter
10
- extend T::Sig
11
-
12
10
  # Available mappings of formattings
13
11
  # To use any of them, you can use {{<key>:<string>}}
14
12
  # There are presentational (colours and formatters)
@@ -59,18 +57,16 @@ module CLI
59
57
 
60
58
  LITERAL_BRACES = Class.new
61
59
 
62
- Stack = T.type_alias { T::Array[T.any(String, LITERAL_BRACES)] }
60
+ #: type stack = Array[String | LITERAL_BRACES]
63
61
 
64
62
  class FormatError < StandardError
65
- extend T::Sig
66
-
67
- sig { returns(String) }
63
+ #: String
68
64
  attr_accessor :input
69
65
 
70
- sig { returns(Integer) }
66
+ #: Integer
71
67
  attr_accessor :index
72
68
 
73
- sig { params(message: String, input: String, index: Integer).void }
69
+ #: (String message, String input, Integer index) -> void
74
70
  def initialize(message, input, index)
75
71
  super(message)
76
72
  @input = input
@@ -84,10 +80,10 @@ module CLI
84
80
  #
85
81
  # * +text+ - the text to format
86
82
  #
87
- sig { params(text: String).void }
83
+ #: (String text) -> void
88
84
  def initialize(text)
89
85
  @text = text
90
- @nodes = T.let([], T::Array[[String, Stack]])
86
+ @nodes = [] #: Array[[String, stack]]
91
87
  end
92
88
 
93
89
  # Format the text using a map.
@@ -100,11 +96,11 @@ module CLI
100
96
  #
101
97
  # * +:enable_color+ - enable color output? Default is true unless output is redirected
102
98
  #
103
- sig { params(sgr_map: T::Hash[String, String], enable_color: T::Boolean).returns(String) }
99
+ #: (?Hash[String, String] sgr_map, ?enable_color: bool) -> String
104
100
  def format(sgr_map = SGR_MAP, enable_color: CLI::UI.enable_color?)
105
101
  @nodes.replace([])
106
102
  stack = parse_body(StringScanner.new(@text))
107
- prev_fmt = T.let(nil, T.nilable(Stack))
103
+ prev_fmt = nil #: stack?
108
104
  content = @nodes.each_with_object(+'') do |(text, fmt), str|
109
105
  if prev_fmt != fmt && enable_color
110
106
  text = apply_format(text, fmt, sgr_map)
@@ -118,7 +114,8 @@ module CLI
118
114
  return content unless enable_color
119
115
  return content if stack == prev_fmt
120
116
 
121
- unless stack.empty? && (@nodes.empty? || T.must(@nodes.last)[1].empty?)
117
+ last_node = @nodes.last #: as !nil
118
+ unless stack.empty? && (@nodes.empty? || last_node[1].empty?)
122
119
  content << apply_format('', stack, sgr_map)
123
120
  end
124
121
  content
@@ -126,7 +123,7 @@ module CLI
126
123
 
127
124
  private
128
125
 
129
- sig { params(text: String, fmt: Stack, sgr_map: T::Hash[String, String]).returns(String) }
126
+ #: (String text, stack fmt, Hash[String, String] sgr_map) -> String
130
127
  def apply_format(text, fmt, sgr_map)
131
128
  sgr = fmt.each_with_object(+'0') do |name, str|
132
129
  next if name.is_a?(LITERAL_BRACES)
@@ -144,10 +141,10 @@ module CLI
144
141
  CLI::UI::ANSI.sgr(sgr) + text
145
142
  end
146
143
 
147
- sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
144
+ #: (StringScanner sc, stack stack) -> stack
148
145
  def parse_expr(sc, stack)
149
146
  if (match = sc.scan(SCAN_GLYPH))
150
- glyph_handle = T.must(match[0])
147
+ glyph_handle = match[0] #: as !nil
151
148
  begin
152
149
  glyph = Glyph.lookup(glyph_handle)
153
150
  emit(glyph.char, [glyph.color.name.to_s])
@@ -160,11 +157,12 @@ module CLI
160
157
  )
161
158
  end
162
159
  elsif (match = sc.scan(SCAN_WIDGET))
163
- match_data = T.must(SCAN_WIDGET.match(match)) # Regexp.last_match doesn't work here
164
- widget_handle = T.must(match_data['handle'])
160
+ match_data = SCAN_WIDGET.match(match) #: as !nil # Regexp.last_match doesn't work here
161
+ widget_handle = match_data['handle'] #: as !nil
165
162
  begin
166
163
  widget = Widgets.lookup(widget_handle)
167
- emit(widget.call(T.must(match_data['args'])), stack)
164
+ args = match_data['args'] #: as !nil
165
+ emit(widget.call(args), stack)
168
166
  rescue Widgets::InvalidWidgetHandle
169
167
  index = sc.pos - 2 # rewind past '}}'
170
168
  raise(FormatError.new(
@@ -188,14 +186,16 @@ module CLI
188
186
  stack
189
187
  end
190
188
 
191
- sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
189
+ #: (StringScanner sc, ?stack stack) -> stack
192
190
  def parse_body(sc, stack = [])
193
191
  match = sc.scan(SCAN_BODY)
194
192
  if match&.end_with?(BEGIN_EXPR)
195
- emit(T.must(match[DISCARD_BRACES]), stack)
193
+ text = match[DISCARD_BRACES] #: as !nil
194
+ emit(text, stack)
196
195
  parse_expr(sc, stack)
197
196
  elsif match&.end_with?(END_EXPR)
198
- emit(T.must(match[DISCARD_BRACES]), stack)
197
+ text = match[DISCARD_BRACES] #: as !nil
198
+ emit(text, stack)
199
199
  if stack.pop.is_a?(LITERAL_BRACES)
200
200
  emit('}}', stack)
201
201
  end
@@ -208,7 +208,7 @@ module CLI
208
208
  stack
209
209
  end
210
210
 
211
- sig { params(text: String, stack: Stack).void }
211
+ #: (String text, stack stack) -> void
212
212
  def emit(text, stack)
213
213
  return if text.empty?
214
214
 
@@ -6,18 +6,13 @@ module CLI
6
6
  module Frame
7
7
  module FrameStack
8
8
  class StackItem
9
- extend T::Sig
10
-
11
- sig { returns(CLI::UI::Color) }
9
+ #: CLI::UI::Color
12
10
  attr_reader :color
13
11
 
14
- sig { returns(CLI::UI::Frame::FrameStyle) }
12
+ #: CLI::UI::Frame::FrameStyle
15
13
  attr_reader :frame_style
16
14
 
17
- sig do
18
- params(color_name: CLI::UI::Colorable, style_name: FrameStylable)
19
- .void
20
- end
15
+ #: (CLI::UI::colorable color_name, frame_stylable style_name) -> void
21
16
  def initialize(color_name, style_name)
22
17
  @color = CLI::UI.resolve_color(color_name)
23
18
  @frame_style = CLI::UI.resolve_style(style_name)
@@ -25,10 +20,8 @@ module CLI
25
20
  end
26
21
 
27
22
  class << self
28
- extend T::Sig
29
-
30
23
  # Fetch all items off the frame stack
31
- sig { returns(T::Array[StackItem]) }
24
+ #: -> Array[StackItem]
32
25
  def items
33
26
  Thread.current[:cliui_frame_stack] ||= []
34
27
  end
@@ -51,24 +44,19 @@ module CLI
51
44
  # If both an item and a color/style pair are given, raises an +ArgumentError+
52
45
  # If the given item is not a +StackItem+, raises an +ArgumentError+
53
46
  #
54
- sig do
55
- params(
56
- item: T.nilable(StackItem),
57
- color: T.nilable(CLI::UI::Color),
58
- style: T.nilable(CLI::UI::Frame::FrameStyle),
59
- )
60
- .void
61
- end
47
+ #: (?StackItem? item, ?color: CLI::UI::Color?, ?style: CLI::UI::Frame::FrameStyle?) -> void
62
48
  def push(item = nil, color: nil, style: nil)
63
49
  if color.nil? != style.nil? || item.nil? == color.nil?
64
50
  raise ArgumentError, 'Must give one of item or color: and style:'
65
51
  end
66
52
 
67
- items.push(item || StackItem.new(T.must(color), T.must(style)))
53
+ c = color #: as !nil
54
+ s = style #: as !nil
55
+ items.push(item || StackItem.new(c, s))
68
56
  end
69
57
 
70
58
  # Removes and returns the last stack item off the stack
71
- sig { returns(T.nilable(StackItem)) }
59
+ #: -> StackItem?
72
60
  def pop
73
61
  items.pop
74
62
  end
@@ -15,14 +15,14 @@ module CLI
15
15
  BOTTOM_LEFT = '┗'
16
16
 
17
17
  class << self
18
- extend T::Sig
19
-
20
- sig { override.returns(Symbol) }
18
+ # @override
19
+ #: -> Symbol
21
20
  def style_name
22
21
  :box
23
22
  end
24
23
 
25
- sig { override.returns(String) }
24
+ # @override
25
+ #: -> String
26
26
  def prefix
27
27
  VERTICAL
28
28
  end
@@ -41,7 +41,8 @@ module CLI
41
41
  #
42
42
  # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43
43
  #
44
- sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
44
+ # @override
45
+ #: (String text, color: CLI::UI::Color) -> String
45
46
  def start(text, color:)
46
47
  edge(text, color: color, first: TOP_LEFT)
47
48
  end
@@ -60,7 +61,8 @@ module CLI
60
61
  #
61
62
  # ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
62
63
  #
63
- sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
64
+ # @override
65
+ #: (String text, color: CLI::UI::Color) -> String
64
66
  def divider(text, color:)
65
67
  edge(text, color: color, first: DIVIDER)
66
68
  end
@@ -80,16 +82,15 @@ module CLI
80
82
  #
81
83
  # ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82
84
  #
83
- sig { override.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) }
85
+ # @override
86
+ #: (String text, color: CLI::UI::Color, ?right_text: String?) -> String
84
87
  def close(text, color:, right_text: nil)
85
88
  edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT)
86
89
  end
87
90
 
88
91
  private
89
92
 
90
- sig do
91
- params(text: String, color: CLI::UI::Color, first: String, right_text: T.nilable(String)).returns(String)
92
- end
93
+ #: (String text, color: CLI::UI::Color, first: String, ?right_text: String?) -> String
93
94
  def edge(text, color:, first:, right_text: nil)
94
95
  color = CLI::UI.resolve_color(color)
95
96
 
@@ -15,14 +15,14 @@ module CLI
15
15
  BOTTOM_LEFT = '┗'
16
16
 
17
17
  class << self
18
- extend T::Sig
19
-
20
- sig { override.returns(Symbol) }
18
+ # @override
19
+ #: -> Symbol
21
20
  def style_name
22
21
  :bracket
23
22
  end
24
23
 
25
- sig { override.returns(String) }
24
+ # @override
25
+ #: -> String
26
26
  def prefix
27
27
  VERTICAL
28
28
  end
@@ -41,7 +41,8 @@ module CLI
41
41
  #
42
42
  # ┏━━ Open
43
43
  #
44
- sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
44
+ # @override
45
+ #: (String text, color: CLI::UI::Color) -> String
45
46
  def start(text, color:)
46
47
  edge(text, color: color, first: TOP_LEFT)
47
48
  end
@@ -60,7 +61,8 @@ module CLI
60
61
  #
61
62
  # ┣━━ Divider
62
63
  #
63
- sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
64
+ # @override
65
+ #: (String text, color: CLI::UI::Color) -> String
64
66
  def divider(text, color:)
65
67
  edge(text, color: color, first: DIVIDER)
66
68
  end
@@ -80,16 +82,15 @@ module CLI
80
82
  #
81
83
  # ┗━━ Close
82
84
  #
83
- sig { override.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) }
85
+ # @override
86
+ #: (String text, color: CLI::UI::Color, ?right_text: String?) -> String
84
87
  def close(text, color:, right_text: nil)
85
88
  edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT)
86
89
  end
87
90
 
88
91
  private
89
92
 
90
- sig do
91
- params(text: String, color: CLI::UI::Color, first: String, right_text: T.nilable(String)).returns(String)
92
- end
93
+ #: (String text, color: CLI::UI::Color, first: String, ?right_text: String?) -> String
93
94
  def edge(text, color:, first:, right_text: nil)
94
95
  color = CLI::UI.resolve_color(color)
95
96