cli-ui 1.5.1 → 2.2.3

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,10 +1,14 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
3
+
2
4
  require('cli/ui')
3
5
  require('strscan')
4
6
 
5
7
  module CLI
6
8
  module UI
7
9
  class Formatter
10
+ extend T::Sig
11
+
8
12
  # Available mappings of formattings
9
13
  # To use any of them, you can use {{<key>:<string>}}
10
14
  # There are presentational (colours and formatters)
@@ -19,6 +23,8 @@ module CLI
19
23
  'blue' => '94', # 9x = high-intensity fg color x
20
24
  'magenta' => '35',
21
25
  'cyan' => '36',
26
+ 'gray' => '38;5;244',
27
+ 'white' => '97',
22
28
  'bold' => '1',
23
29
  'italic' => '3',
24
30
  'underline' => '4',
@@ -49,12 +55,21 @@ module CLI
49
55
 
50
56
  DISCARD_BRACES = 0..-3
51
57
 
52
- LITERAL_BRACES = :__literal_braces__
58
+ LITERAL_BRACES = Class.new
59
+
60
+ Stack = T.type_alias { T::Array[T.any(String, LITERAL_BRACES)] }
53
61
 
54
62
  class FormatError < StandardError
55
- attr_accessor :input, :index
63
+ extend T::Sig
64
+
65
+ sig { returns(String) }
66
+ attr_accessor :input
56
67
 
57
- def initialize(message = nil, input = nil, index = nil)
68
+ sig { returns(Integer) }
69
+ attr_accessor :index
70
+
71
+ sig { params(message: String, input: String, index: Integer).void }
72
+ def initialize(message, input, index)
58
73
  super(message)
59
74
  @input = input
60
75
  @index = index
@@ -67,8 +82,10 @@ module CLI
67
82
  #
68
83
  # * +text+ - the text to format
69
84
  #
85
+ sig { params(text: String).void }
70
86
  def initialize(text)
71
87
  @text = text
88
+ @nodes = T.let([], T::Array[[String, Stack]])
72
89
  end
73
90
 
74
91
  # Format the text using a map.
@@ -81,10 +98,11 @@ module CLI
81
98
  #
82
99
  # * +:enable_color+ - enable color output? Default is true unless output is redirected
83
100
  #
101
+ sig { params(sgr_map: T::Hash[String, String], enable_color: T::Boolean).returns(String) }
84
102
  def format(sgr_map = SGR_MAP, enable_color: CLI::UI.enable_color?)
85
- @nodes = []
103
+ @nodes.replace([])
86
104
  stack = parse_body(StringScanner.new(@text))
87
- prev_fmt = nil
105
+ prev_fmt = T.let(nil, T.nilable(Stack))
88
106
  content = @nodes.each_with_object(+'') do |(text, fmt), str|
89
107
  if prev_fmt != fmt && enable_color
90
108
  text = apply_format(text, fmt, sgr_map)
@@ -93,12 +111,12 @@ module CLI
93
111
  prev_fmt = fmt
94
112
  end
95
113
 
96
- stack.reject! { |e| e == LITERAL_BRACES }
114
+ stack.reject! { |e| e.is_a?(LITERAL_BRACES) }
97
115
 
98
116
  return content unless enable_color
99
117
  return content if stack == prev_fmt
100
118
 
101
- unless stack.empty? && (@nodes.size.zero? || @nodes.last[1].empty?)
119
+ unless stack.empty? && (@nodes.empty? || T.must(@nodes.last)[1].empty?)
102
120
  content << apply_format('', stack, sgr_map)
103
121
  end
104
122
  content
@@ -106,25 +124,28 @@ module CLI
106
124
 
107
125
  private
108
126
 
127
+ sig { params(text: String, fmt: Stack, sgr_map: T::Hash[String, String]).returns(String) }
109
128
  def apply_format(text, fmt, sgr_map)
110
129
  sgr = fmt.each_with_object(+'0') do |name, str|
111
- next if name == LITERAL_BRACES
130
+ next if name.is_a?(LITERAL_BRACES)
131
+
112
132
  begin
113
133
  str << ';' << sgr_map.fetch(name)
114
134
  rescue KeyError
115
135
  raise FormatError.new(
116
136
  "invalid format specifier: #{name}",
117
137
  @text,
118
- -1
138
+ -1,
119
139
  )
120
140
  end
121
141
  end
122
142
  CLI::UI::ANSI.sgr(sgr) + text
123
143
  end
124
144
 
145
+ sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
125
146
  def parse_expr(sc, stack)
126
147
  if (match = sc.scan(SCAN_GLYPH))
127
- glyph_handle = match[0]
148
+ glyph_handle = T.must(match[0])
128
149
  begin
129
150
  glyph = Glyph.lookup(glyph_handle)
130
151
  emit(glyph.char, [glyph.color.name.to_s])
@@ -133,20 +154,21 @@ module CLI
133
154
  raise FormatError.new(
134
155
  "invalid glyph handle at index #{index}: '#{glyph_handle}'",
135
156
  @text,
136
- index
157
+ index,
137
158
  )
138
159
  end
139
160
  elsif (match = sc.scan(SCAN_WIDGET))
140
- match_data = SCAN_WIDGET.match(match) # Regexp.last_match doesn't work here
141
- widget_handle = match_data['handle']
161
+ match_data = T.must(SCAN_WIDGET.match(match)) # Regexp.last_match doesn't work here
162
+ widget_handle = T.must(match_data['handle'])
142
163
  begin
143
164
  widget = Widgets.lookup(widget_handle)
144
- emit(widget.call(match_data['args']), stack)
165
+ emit(widget.call(T.must(match_data['args'])), stack)
145
166
  rescue Widgets::InvalidWidgetHandle
146
167
  index = sc.pos - 2 # rewind past '}}'
147
168
  raise(FormatError.new(
148
169
  "invalid widget handle at index #{index}: '#{widget_handle}'",
149
- @text, index,
170
+ @text,
171
+ index,
150
172
  ))
151
173
  end
152
174
  elsif (match = sc.scan(SCAN_FUNCNAME))
@@ -158,20 +180,21 @@ module CLI
158
180
  # We do kind of assume that the text will probably have balanced
159
181
  # pairs of {{ }} at least.
160
182
  emit('{{', stack)
161
- stack.push(LITERAL_BRACES)
183
+ stack.push(LITERAL_BRACES.new)
162
184
  end
163
185
  parse_body(sc, stack)
164
186
  stack
165
187
  end
166
188
 
189
+ sig { params(sc: StringScanner, stack: Stack).returns(Stack) }
167
190
  def parse_body(sc, stack = [])
168
191
  match = sc.scan(SCAN_BODY)
169
192
  if match&.end_with?(BEGIN_EXPR)
170
- emit(match[DISCARD_BRACES], stack)
193
+ emit(T.must(match[DISCARD_BRACES]), stack)
171
194
  parse_expr(sc, stack)
172
195
  elsif match&.end_with?(END_EXPR)
173
- emit(match[DISCARD_BRACES], stack)
174
- if stack.pop == LITERAL_BRACES
196
+ emit(T.must(match[DISCARD_BRACES]), stack)
197
+ if stack.pop.is_a?(LITERAL_BRACES)
175
198
  emit('}}', stack)
176
199
  end
177
200
  parse_body(sc, stack)
@@ -183,9 +206,11 @@ module CLI
183
206
  stack
184
207
  end
185
208
 
209
+ sig { params(text: String, stack: Stack).void }
186
210
  def emit(text, stack)
187
- return if text.nil? || text.empty?
188
- @nodes << [text, stack.reject { |n| n == LITERAL_BRACES }]
211
+ return if text.empty?
212
+
213
+ @nodes << [text, stack.reject { |n| n.is_a?(LITERAL_BRACES) }]
189
214
  end
190
215
  end
191
216
  end
@@ -1,13 +1,22 @@
1
+ # typed: true
2
+
1
3
  module CLI
2
4
  module UI
3
5
  module Frame
4
6
  module FrameStack
5
- COLOR_ENVVAR = 'CLI_FRAME_STACK'
6
- STYLE_ENVVAR = 'CLI_STYLE_STACK'
7
-
8
7
  class StackItem
9
- attr_reader :color, :frame_style
8
+ extend T::Sig
9
+
10
+ sig { returns(CLI::UI::Color) }
11
+ attr_reader :color
10
12
 
13
+ sig { returns(CLI::UI::Frame::FrameStyle) }
14
+ attr_reader :frame_style
15
+
16
+ sig do
17
+ params(color_name: CLI::UI::Colorable, style_name: FrameStylable)
18
+ .void
19
+ end
11
20
  def initialize(color_name, style_name)
12
21
  @color = CLI::UI.resolve_color(color_name)
13
22
  @frame_style = CLI::UI.resolve_style(style_name)
@@ -15,14 +24,12 @@ module CLI
15
24
  end
16
25
 
17
26
  class << self
27
+ extend T::Sig
28
+
18
29
  # Fetch all items off the frame stack
30
+ sig { returns(T::Array[StackItem]) }
19
31
  def items
20
- colors = ENV.fetch(COLOR_ENVVAR, '').split(':').map(&:to_sym)
21
- styles = ENV.fetch(STYLE_ENVVAR, '').split(':').map(&:to_sym)
22
-
23
- colors.length.times.map do |i|
24
- StackItem.new(colors[i], styles[i] || Frame.frame_style)
25
- end
32
+ Thread.current[:cliui_frame_stack] ||= []
26
33
  end
27
34
 
28
35
  # Push a new item onto the frame stack.
@@ -43,53 +50,26 @@ module CLI
43
50
  # If both an item and a color/style pair are given, raises an +ArgumentError+
44
51
  # If the given item is not a +StackItem+, raises an +ArgumentError+
45
52
  #
53
+ sig do
54
+ params(
55
+ item: T.nilable(StackItem),
56
+ color: T.nilable(CLI::UI::Color),
57
+ style: T.nilable(CLI::UI::Frame::FrameStyle),
58
+ )
59
+ .void
60
+ end
46
61
  def push(item = nil, color: nil, style: nil)
47
- unless item.nil?
48
- unless item.is_a?(StackItem)
49
- raise ArgumentError, 'item must be a StackItem'
50
- end
51
-
52
- unless color.nil? && style.nil?
53
- raise ArgumentError, 'Must give one of item or color: and style:'
54
- end
62
+ if color.nil? != style.nil? || item.nil? == color.nil?
63
+ raise ArgumentError, 'Must give one of item or color: and style:'
55
64
  end
56
65
 
57
- item ||= StackItem.new(color, style)
58
-
59
- curr = items
60
- curr << item
61
-
62
- serialize(curr)
66
+ items.push(item || StackItem.new(T.must(color), T.must(style)))
63
67
  end
64
68
 
65
69
  # Removes and returns the last stack item off the stack
70
+ sig { returns(T.nilable(StackItem)) }
66
71
  def pop
67
- curr = items
68
- ret = curr.pop
69
-
70
- serialize(curr)
71
-
72
- ret.nil? ? nil : ret
73
- end
74
-
75
- private
76
-
77
- # Serializes the item stack into two ENV variables.
78
- #
79
- # This is done to preserve backward compatibility with earlier versions of cli/ui.
80
- # This ensures that any code that relied upon previous stack behavior should continue
81
- # to work.
82
- def serialize(items)
83
- colors = []
84
- styles = []
85
-
86
- items.each do |item|
87
- colors << item.color.name
88
- styles << item.frame_style.name
89
- end
90
-
91
- ENV[COLOR_ENVVAR] = colors.join(':')
92
- ENV[STYLE_ENVVAR] = styles.join(':')
72
+ items.pop
93
73
  end
94
74
  end
95
75
  end
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  module CLI
2
4
  module UI
3
5
  module Frame
@@ -12,10 +14,14 @@ module CLI
12
14
  BOTTOM_LEFT = '┗'
13
15
 
14
16
  class << self
15
- def name
16
- 'box'
17
+ extend T::Sig
18
+
19
+ sig { override.returns(Symbol) }
20
+ def style_name
21
+ :box
17
22
  end
18
23
 
24
+ sig { override.returns(String) }
19
25
  def prefix
20
26
  VERTICAL
21
27
  end
@@ -34,7 +40,8 @@ module CLI
34
40
  #
35
41
  # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36
42
  #
37
- def open(text, color:)
43
+ sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
44
+ def start(text, color:)
38
45
  edge(text, color: color, first: TOP_LEFT)
39
46
  end
40
47
 
@@ -52,6 +59,7 @@ module CLI
52
59
  #
53
60
  # ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54
61
  #
62
+ sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
55
63
  def divider(text, color:)
56
64
  edge(text, color: color, first: DIVIDER)
57
65
  end
@@ -71,12 +79,16 @@ module CLI
71
79
  #
72
80
  # ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73
81
  #
82
+ sig { override.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) }
74
83
  def close(text, color:, right_text: nil)
75
84
  edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT)
76
85
  end
77
86
 
78
87
  private
79
88
 
89
+ sig do
90
+ params(text: String, color: CLI::UI::Color, first: String, right_text: T.nilable(String)).returns(String)
91
+ end
80
92
  def edge(text, color:, first:, right_text: nil)
81
93
  color = CLI::UI.resolve_color(color)
82
94
 
@@ -84,7 +96,6 @@ module CLI
84
96
 
85
97
  preamble << color.code << first << (HORIZONTAL * 2)
86
98
 
87
- text ||= ''
88
99
  unless text.empty?
89
100
  preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' '
90
101
  end
@@ -101,7 +112,7 @@ module CLI
101
112
  preamble_start = Frame.prefix_width
102
113
  # If prefix_width is non-zero, we need to subtract the width of
103
114
  # the final space, since we're going to write over it.
104
- preamble_start -= 1 unless preamble_start.zero?
115
+ preamble_start -= 1 if preamble_start.nonzero?
105
116
  preamble_end = preamble_start + preamble_width
106
117
 
107
118
  suffix_width = CLI::UI::ANSI.printing_width(suffix)
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  module CLI
2
4
  module UI
3
5
  module Frame
@@ -12,10 +14,14 @@ module CLI
12
14
  BOTTOM_LEFT = '┗'
13
15
 
14
16
  class << self
15
- def name
16
- 'bracket'
17
+ extend T::Sig
18
+
19
+ sig { override.returns(Symbol) }
20
+ def style_name
21
+ :bracket
17
22
  end
18
23
 
24
+ sig { override.returns(String) }
19
25
  def prefix
20
26
  VERTICAL
21
27
  end
@@ -34,7 +40,8 @@ module CLI
34
40
  #
35
41
  # ┏━━ Open
36
42
  #
37
- def open(text, color:)
43
+ sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
44
+ def start(text, color:)
38
45
  edge(text, color: color, first: TOP_LEFT)
39
46
  end
40
47
 
@@ -52,6 +59,7 @@ module CLI
52
59
  #
53
60
  # ┣━━ Divider
54
61
  #
62
+ sig { override.params(text: String, color: CLI::UI::Color).returns(String) }
55
63
  def divider(text, color:)
56
64
  edge(text, color: color, first: DIVIDER)
57
65
  end
@@ -71,12 +79,16 @@ module CLI
71
79
  #
72
80
  # ┗━━ Close
73
81
  #
82
+ sig { override.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) }
74
83
  def close(text, color:, right_text: nil)
75
84
  edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT)
76
85
  end
77
86
 
78
87
  private
79
88
 
89
+ sig do
90
+ params(text: String, color: CLI::UI::Color, first: String, right_text: T.nilable(String)).returns(String)
91
+ end
80
92
  def edge(text, color:, first:, right_text: nil)
81
93
  color = CLI::UI.resolve_color(color)
82
94
 
@@ -84,7 +96,6 @@ module CLI
84
96
 
85
97
  preamble << color.code << first << (HORIZONTAL * 2)
86
98
 
87
- text ||= ''
88
99
  unless text.empty?
89
100
  preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' '
90
101
  end
@@ -115,18 +126,18 @@ module CLI
115
126
 
116
127
  # If prefix_width is non-zero, we need to subtract the width of
117
128
  # the final space, since we're going to write over it.
118
- preamble_start -= 1 unless preamble_start.zero?
129
+ preamble_start -= 1 if preamble_start.nonzero?
119
130
 
120
- # Prefix_width includes the width of the terminal space, which we
121
- # want to remove. The clamping is done to avoid a negative
122
- # preamble start which can occur for the first frame.
131
+ # Jumping around the line can cause some unwanted flashes
123
132
  o << CLI::UI::ANSI.hide_cursor
124
133
 
125
134
  # reset to column 1 so that things like ^C don't ruin formatting
126
135
  o << "\r"
136
+
127
137
  o << color.code
128
138
  o << print_at_x(preamble_start, preamble + color.code + suffix)
129
139
  o << CLI::UI::Color::RESET.code
140
+ o << CLI::UI::ANSI.show_cursor
130
141
  o << "\n"
131
142
 
132
143
  o
@@ -1,120 +1,117 @@
1
+ # typed: true
2
+
1
3
  require 'cli/ui/frame'
2
4
 
3
5
  module CLI
4
6
  module UI
5
7
  module Frame
6
8
  module FrameStyle
7
- class << self
8
- # rubocop:disable Style/ClassVars
9
- @@loaded_styles = []
9
+ include Kernel
10
+ extend T::Sig
11
+ extend T::Helpers
12
+ abstract!
10
13
 
11
- def loaded_styles
12
- @@loaded_styles.map(&:name)
13
- end
14
+ autoload(:Box, 'cli/ui/frame/frame_style/box')
15
+ autoload(:Bracket, 'cli/ui/frame/frame_style/bracket')
16
+
17
+ MAP = {
18
+ box: -> { FrameStyle::Box },
19
+ bracket: -> { FrameStyle::Bracket },
20
+ }
21
+
22
+ class << self
23
+ extend T::Sig
14
24
 
15
25
  # Lookup a frame style via its name
16
26
  #
17
27
  # ==== Attributes
18
28
  #
19
29
  # * +symbol+ - frame style name to lookup
30
+ sig { params(name: T.any(String, Symbol)).returns(FrameStyle) }
20
31
  def lookup(name)
21
- @@loaded_styles
22
- .find { |style| style.name.to_sym == name }
23
- .tap { |style| raise InvalidFrameStyleName, name if style.nil? }
32
+ MAP.fetch(name.to_sym).call
33
+ rescue KeyError
34
+ raise(InvalidFrameStyleName, name)
24
35
  end
25
-
26
- def extended(base)
27
- @@loaded_styles << base
28
- base.extend(Interface)
29
- end
30
- # rubocop:enable Style/ClassVars
31
36
  end
32
37
 
33
- class InvalidFrameStyleName < ArgumentError
34
- def initialize(name)
35
- super
36
- @name = name
37
- end
38
+ sig { abstract.returns(Symbol) }
39
+ def style_name; end
38
40
 
39
- def message
40
- keys = FrameStyle.loaded_styles.map(&:inspect).join(',')
41
- "invalid frame style: #{@name.inspect}" \
42
- ' -- must be one of CLI::UI::Frame::FrameStyle.loaded_styles ' \
43
- "(#{keys})"
44
- end
41
+ # Returns the character(s) that should be printed at the beginning
42
+ # of lines inside this frame
43
+ sig { abstract.returns(String) }
44
+ def prefix; end
45
+
46
+ # Returns the printing width of the prefix
47
+ sig { returns(Integer) }
48
+ def prefix_width
49
+ CLI::UI::ANSI.printing_width(prefix)
45
50
  end
46
51
 
47
- # Public interface for FrameStyles
48
- # Applied by extending FrameStyle
49
- module Interface
50
- def name
51
- raise NotImplementedError
52
- end
52
+ # Draws the "Open" line for this frame style
53
+ #
54
+ # ==== Attributes
55
+ #
56
+ # * +text+ - (required) the text/title to output in the frame
57
+ #
58
+ # ==== Options
59
+ #
60
+ # * +:color+ - (required) The color of the frame.
61
+ #
62
+ sig { abstract.params(text: String, color: CLI::UI::Color).returns(String) }
63
+ def start(text, color:); end
53
64
 
54
- # Returns the character(s) that should be printed at the beginning
55
- # of lines inside this frame
56
- def prefix
57
- raise NotImplementedError
58
- end
65
+ # Draws the "Close" line for this frame style
66
+ #
67
+ # ==== Attributes
68
+ #
69
+ # * +text+ - (required) the text/title to output in the frame
70
+ #
71
+ # ==== Options
72
+ #
73
+ # * +:color+ - (required) The color of the frame.
74
+ # * +:right_text+ - Text to print at the right of the line. Defaults to nil
75
+ #
76
+ sig { abstract.params(text: String, color: CLI::UI::Color, right_text: T.nilable(String)).returns(String) }
77
+ def close(text, color:, right_text: nil); end
59
78
 
60
- # Returns the printing width of the prefix
61
- def prefix_width
62
- CLI::UI::ANSI.printing_width(prefix)
63
- end
79
+ # Draws a "divider" line for the current frame style
80
+ #
81
+ # ==== Attributes
82
+ #
83
+ # * +text+ - (required) the text/title to output in the frame
84
+ #
85
+ # ==== Options
86
+ #
87
+ # * +:color+ - (required) The color of the frame.
88
+ #
89
+ sig { abstract.params(text: String, color: CLI::UI::Color).returns(String) }
90
+ def divider(text, color:); end
64
91
 
65
- # Draws the "Open" line for this frame style
66
- #
67
- # ==== Attributes
68
- #
69
- # * +text+ - (required) the text/title to output in the frame
70
- #
71
- # ==== Options
72
- #
73
- # * +:color+ - (required) The color of the frame.
74
- #
75
- def open(text, color:)
76
- raise NotImplementedError
77
- end
92
+ sig { params(x: Integer, str: String).returns(String) }
93
+ def print_at_x(x, str)
94
+ CLI::UI::ANSI.cursor_horizontal_absolute(1 + x) + str
95
+ end
78
96
 
79
- # Draws the "Close" line for this frame style
80
- #
81
- # ==== Attributes
82
- #
83
- # * +text+ - (required) the text/title to output in the frame
84
- #
85
- # ==== Options
86
- #
87
- # * +:color+ - (required) The color of the frame.
88
- # * +:right_text+ - Text to print at the right of the line. Defaults to nil
89
- #
90
- def close(text, color:, right_text: nil)
91
- raise NotImplementedError
92
- end
97
+ class InvalidFrameStyleName < ArgumentError
98
+ extend T::Sig
93
99
 
94
- # Draws a "divider" line for the current frame style
95
- #
96
- # ==== Attributes
97
- #
98
- # * +text+ - (required) the text/title to output in the frame
99
- #
100
- # ==== Options
101
- #
102
- # * +:color+ - (required) The color of the frame.
103
- #
104
- def divider(text, color: nil)
105
- raise NotImplementedError
100
+ sig { params(name: T.any(String, Symbol)).void }
101
+ def initialize(name)
102
+ super
103
+ @name = name
106
104
  end
107
105
 
108
- private
109
-
110
- def print_at_x(x, str)
111
- CLI::UI::ANSI.cursor_horizontal_absolute(1 + x) + str
106
+ sig { returns(String) }
107
+ def message
108
+ keys = FrameStyle::MAP.keys.map(&:inspect).join(', ')
109
+ "invalid frame style: #{@name.inspect}" \
110
+ ' -- must be one of CLI::UI::Frame::FrameStyle::MAP ' \
111
+ "(#{keys})"
112
112
  end
113
113
  end
114
114
  end
115
115
  end
116
116
  end
117
117
  end
118
-
119
- require 'cli/ui/frame/frame_style/box'
120
- require 'cli/ui/frame/frame_style/bracket'