rfix 2.0.4 → 3.0.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/exe/rfix +11 -90
  3. data/lib/rfix.rb +10 -9
  4. data/lib/rfix/branch/reference.rb +2 -2
  5. data/lib/rfix/branch/upstream.rb +2 -4
  6. data/lib/rfix/cli/command.rb +14 -1
  7. data/lib/rfix/cli/command/all.rb +21 -0
  8. data/lib/rfix/cli/command/base.rb +30 -19
  9. data/lib/rfix/cli/command/branch.rb +2 -0
  10. data/lib/rfix/cli/command/help.rb +2 -0
  11. data/lib/rfix/cli/command/info.rb +6 -1
  12. data/lib/rfix/cli/command/local.rb +2 -0
  13. data/lib/rfix/cli/command/origin.rb +2 -0
  14. data/lib/rfix/cli/command/setup.rb +2 -0
  15. data/lib/rfix/cli/command/status.rb +39 -0
  16. data/lib/rfix/collector.rb +69 -0
  17. data/lib/rfix/diff.rb +69 -0
  18. data/lib/rfix/extension/comment_config.rb +15 -0
  19. data/lib/rfix/extension/offense.rb +17 -14
  20. data/lib/rfix/extension/pastel.rb +7 -4
  21. data/lib/rfix/extension/progresbar.rb +15 -0
  22. data/lib/rfix/extension/strings.rb +10 -2
  23. data/lib/rfix/file.rb +5 -3
  24. data/lib/rfix/file/base.rb +21 -14
  25. data/lib/rfix/file/deleted.rb +2 -0
  26. data/lib/rfix/file/ignored.rb +2 -0
  27. data/lib/rfix/file/null.rb +17 -0
  28. data/lib/rfix/file/tracked.rb +39 -23
  29. data/lib/rfix/file/undefined.rb +17 -0
  30. data/lib/rfix/file/untracked.rb +3 -1
  31. data/lib/rfix/formatter.rb +67 -71
  32. data/lib/rfix/highlighter.rb +1 -3
  33. data/lib/rfix/rake/gemfile.rb +26 -23
  34. data/lib/rfix/repository.rb +59 -96
  35. data/lib/rfix/types.rb +24 -14
  36. data/lib/rfix/version.rb +1 -1
  37. data/rfix.gemspec +11 -3
  38. data/vendor/cli-ui/Gemfile +17 -0
  39. data/vendor/cli-ui/Gemfile.lock +60 -0
  40. data/vendor/cli-ui/LICENSE.txt +21 -0
  41. data/vendor/cli-ui/README.md +224 -0
  42. data/vendor/cli-ui/Rakefile +20 -0
  43. data/vendor/cli-ui/bin/console +14 -0
  44. data/vendor/cli-ui/cli-ui.gemspec +25 -0
  45. data/vendor/cli-ui/dev.yml +14 -0
  46. data/vendor/cli-ui/lib/cli/ui.rb +233 -0
  47. data/vendor/cli-ui/lib/cli/ui/ansi.rb +157 -0
  48. data/vendor/cli-ui/lib/cli/ui/color.rb +84 -0
  49. data/vendor/cli-ui/lib/cli/ui/formatter.rb +192 -0
  50. data/vendor/cli-ui/lib/cli/ui/frame.rb +269 -0
  51. data/vendor/cli-ui/lib/cli/ui/frame/frame_stack.rb +98 -0
  52. data/vendor/cli-ui/lib/cli/ui/frame/frame_style.rb +120 -0
  53. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/box.rb +166 -0
  54. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
  55. data/vendor/cli-ui/lib/cli/ui/glyph.rb +84 -0
  56. data/vendor/cli-ui/lib/cli/ui/os.rb +67 -0
  57. data/vendor/cli-ui/lib/cli/ui/printer.rb +59 -0
  58. data/vendor/cli-ui/lib/cli/ui/progress.rb +90 -0
  59. data/vendor/cli-ui/lib/cli/ui/prompt.rb +297 -0
  60. data/vendor/cli-ui/lib/cli/ui/prompt/interactive_options.rb +484 -0
  61. data/vendor/cli-ui/lib/cli/ui/prompt/options_handler.rb +29 -0
  62. data/vendor/cli-ui/lib/cli/ui/spinner.rb +66 -0
  63. data/vendor/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
  64. data/vendor/cli-ui/lib/cli/ui/spinner/spin_group.rb +263 -0
  65. data/vendor/cli-ui/lib/cli/ui/stdout_router.rb +232 -0
  66. data/vendor/cli-ui/lib/cli/ui/terminal.rb +46 -0
  67. data/vendor/cli-ui/lib/cli/ui/truncater.rb +102 -0
  68. data/vendor/cli-ui/lib/cli/ui/version.rb +5 -0
  69. data/vendor/cli-ui/lib/cli/ui/widgets.rb +77 -0
  70. data/vendor/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
  71. data/vendor/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
  72. data/vendor/cli-ui/lib/cli/ui/wrap.rb +56 -0
  73. data/vendor/cli-ui/test/cli/ui/ansi_test.rb +32 -0
  74. data/vendor/cli-ui/test/cli/ui/cli_ui_test.rb +23 -0
  75. data/vendor/cli-ui/test/cli/ui/color_test.rb +40 -0
  76. data/vendor/cli-ui/test/cli/ui/formatter_test.rb +79 -0
  77. data/vendor/cli-ui/test/cli/ui/glyph_test.rb +68 -0
  78. data/vendor/cli-ui/test/cli/ui/printer_test.rb +103 -0
  79. data/vendor/cli-ui/test/cli/ui/progress_test.rb +46 -0
  80. data/vendor/cli-ui/test/cli/ui/prompt/options_handler_test.rb +39 -0
  81. data/vendor/cli-ui/test/cli/ui/prompt_test.rb +348 -0
  82. data/vendor/cli-ui/test/cli/ui/spinner/spin_group_test.rb +39 -0
  83. data/vendor/cli-ui/test/cli/ui/spinner_test.rb +141 -0
  84. data/vendor/cli-ui/test/cli/ui/stdout_router_test.rb +32 -0
  85. data/vendor/cli-ui/test/cli/ui/terminal_test.rb +26 -0
  86. data/vendor/cli-ui/test/cli/ui/truncater_test.rb +31 -0
  87. data/vendor/cli-ui/test/cli/ui/widgets/status_test.rb +49 -0
  88. data/vendor/cli-ui/test/cli/ui/widgets_test.rb +15 -0
  89. data/vendor/cli-ui/test/test_helper.rb +53 -0
  90. data/vendor/cli-ui/tmp/cache/bootsnap/compile-cache/d9/c036af0f3dc494 +0 -0
  91. data/vendor/cli-ui/tmp/cache/bootsnap/load-path-cache +0 -0
  92. data/vendor/dry-cli/lib/dry/cli/command.rb +2 -1
  93. data/vendor/dry-cli/tmp/cache/bootsnap/compile-cache/ff/a22a5daafbd74c +0 -0
  94. data/vendor/dry-cli/tmp/cache/bootsnap/load-path-cache +0 -0
  95. data/vendor/strings-ansi/tmp/cache/bootsnap/compile-cache/79/49cf49407b370e +0 -0
  96. data/vendor/strings-ansi/tmp/cache/bootsnap/load-path-cache +0 -0
  97. metadata +170 -9
  98. data/lib/rfix/extension/string.rb +0 -12
  99. data/lib/rfix/indicator.rb +0 -19
@@ -0,0 +1,84 @@
1
+ require 'cli/ui'
2
+
3
+ module CLI
4
+ module UI
5
+ class Color
6
+ attr_reader :sgr, :name, :code
7
+
8
+ # Creates a new color mapping
9
+ # Signatures can be found here:
10
+ # https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
11
+ #
12
+ # ==== Attributes
13
+ #
14
+ # * +sgr+ - The color signature
15
+ # * +name+ - The name of the color
16
+ #
17
+ def initialize(sgr, name)
18
+ @sgr = sgr
19
+ @code = CLI::UI::ANSI.sgr(sgr)
20
+ @name = name
21
+ end
22
+
23
+ RED = new('31', :red)
24
+ GREEN = new('32', :green)
25
+ YELLOW = new('33', :yellow)
26
+ # default blue is low-contrast against black in some default terminal color scheme
27
+ BLUE = new('94', :blue) # 9x = high-intensity fg color x
28
+ MAGENTA = new('35', :magenta)
29
+ CYAN = new('36', :cyan)
30
+ RESET = new('0', :reset)
31
+ BOLD = new('1', :bold)
32
+ WHITE = new('97', :white)
33
+
34
+ # 240 is very dark gray; 255 is very light gray. 244 is somewhat dark.
35
+ GRAY = new('38;5;244', :grey)
36
+
37
+ MAP = {
38
+ red: RED,
39
+ green: GREEN,
40
+ yellow: YELLOW,
41
+ blue: BLUE,
42
+ magenta: MAGENTA,
43
+ cyan: CYAN,
44
+ reset: RESET,
45
+ bold: BOLD,
46
+ gray: GRAY,
47
+ }.freeze
48
+
49
+ class InvalidColorName < ArgumentError
50
+ def initialize(name)
51
+ super
52
+ @name = name
53
+ end
54
+
55
+ def message
56
+ keys = Color.available.map(&:inspect).join(',')
57
+ "invalid color: #{@name.inspect} " \
58
+ "-- must be one of CLI::UI::Color.available (#{keys})"
59
+ end
60
+ end
61
+
62
+ # Looks up a color code by name
63
+ #
64
+ # ==== Raises
65
+ # Raises a InvalidColorName if the color is not available
66
+ # You likely need to add it to the +MAP+ or you made a typo
67
+ #
68
+ # ==== Returns
69
+ # Returns a color code
70
+ #
71
+ def self.lookup(name)
72
+ MAP.fetch(name)
73
+ rescue KeyError
74
+ raise InvalidColorName, name
75
+ end
76
+
77
+ # All available colors by name
78
+ #
79
+ def self.available
80
+ MAP.keys
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+ require('cli/ui')
3
+ require('strscan')
4
+
5
+ module CLI
6
+ module UI
7
+ class Formatter
8
+ # Available mappings of formattings
9
+ # To use any of them, you can use {{<key>:<string>}}
10
+ # There are presentational (colours and formatters)
11
+ # and semantic (error, info, command) formatters available
12
+ #
13
+ SGR_MAP = {
14
+ # presentational
15
+ 'red' => '31',
16
+ 'green' => '32',
17
+ 'yellow' => '33',
18
+ # default blue is low-contrast against black in some default terminal color scheme
19
+ 'blue' => '94', # 9x = high-intensity fg color x
20
+ 'magenta' => '35',
21
+ 'cyan' => '36',
22
+ 'bold' => '1',
23
+ 'italic' => '3',
24
+ 'underline' => '4',
25
+ 'reset' => '0',
26
+
27
+ # semantic
28
+ 'error' => '31', # red
29
+ 'success' => '32', # success
30
+ 'warning' => '33', # yellow
31
+ 'info' => '94', # bright blue
32
+ 'command' => '36', # cyan
33
+ }.freeze
34
+
35
+ BEGIN_EXPR = '{{'
36
+ END_EXPR = '}}'
37
+
38
+ SCAN_WIDGET = %r[@widget/(?<handle>\w+):(?<args>.*?)}}]
39
+ SCAN_FUNCNAME = /\w+:/
40
+ SCAN_GLYPH = /.}}/
41
+ SCAN_BODY = %r{
42
+ .*?
43
+ (
44
+ #{BEGIN_EXPR} |
45
+ #{END_EXPR} |
46
+ \z
47
+ )
48
+ }mx
49
+
50
+ DISCARD_BRACES = 0..-3
51
+
52
+ LITERAL_BRACES = :__literal_braces__
53
+
54
+ class FormatError < StandardError
55
+ attr_accessor :input, :index
56
+
57
+ def initialize(message = nil, input = nil, index = nil)
58
+ super(message)
59
+ @input = input
60
+ @index = index
61
+ end
62
+ end
63
+
64
+ # Initialize a formatter with text.
65
+ #
66
+ # ===== Attributes
67
+ #
68
+ # * +text+ - the text to format
69
+ #
70
+ def initialize(text)
71
+ @text = text
72
+ end
73
+
74
+ # Format the text using a map.
75
+ #
76
+ # ===== Attributes
77
+ #
78
+ # * +sgr_map+ - the mapping of the formattings. Defaults to +SGR_MAP+
79
+ #
80
+ # ===== Options
81
+ #
82
+ # * +:enable_color+ - enable color output? Default is true unless output is redirected
83
+ #
84
+ def format(sgr_map = SGR_MAP, enable_color: CLI::UI.enable_color?)
85
+ @nodes = []
86
+ stack = parse_body(StringScanner.new(@text))
87
+ prev_fmt = nil
88
+ content = @nodes.each_with_object(+'') do |(text, fmt), str|
89
+ if prev_fmt != fmt && enable_color
90
+ text = apply_format(text, fmt, sgr_map)
91
+ end
92
+ str << text
93
+ prev_fmt = fmt
94
+ end
95
+
96
+ stack.reject! { |e| e == LITERAL_BRACES }
97
+
98
+ return content unless enable_color
99
+ return content if stack == prev_fmt
100
+
101
+ unless stack.empty? && (@nodes.size.zero? || @nodes.last[1].empty?)
102
+ content << apply_format('', stack, sgr_map)
103
+ end
104
+ content
105
+ end
106
+
107
+ private
108
+
109
+ def apply_format(text, fmt, sgr_map)
110
+ sgr = fmt.each_with_object(+'0') do |name, str|
111
+ next if name == LITERAL_BRACES
112
+ begin
113
+ str << ';' << sgr_map.fetch(name)
114
+ rescue KeyError
115
+ raise FormatError.new(
116
+ "invalid format specifier: #{name}",
117
+ @text,
118
+ -1
119
+ )
120
+ end
121
+ end
122
+ CLI::UI::ANSI.sgr(sgr) + text
123
+ end
124
+
125
+ def parse_expr(sc, stack)
126
+ if (match = sc.scan(SCAN_GLYPH))
127
+ glyph_handle = match[0]
128
+ begin
129
+ glyph = Glyph.lookup(glyph_handle)
130
+ emit(glyph.char, [glyph.color.name.to_s])
131
+ rescue Glyph::InvalidGlyphHandle
132
+ index = sc.pos - 2 # rewind past '}}'
133
+ raise FormatError.new(
134
+ "invalid glyph handle at index #{index}: '#{glyph_handle}'",
135
+ @text,
136
+ index
137
+ )
138
+ end
139
+ 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']
142
+ begin
143
+ widget = Widgets.lookup(widget_handle)
144
+ emit(widget.call(match_data['args']), stack)
145
+ rescue Widgets::InvalidWidgetHandle
146
+ index = sc.pos - 2 # rewind past '}}'
147
+ raise(FormatError.new(
148
+ "invalid widget handle at index #{index}: '#{widget_handle}'",
149
+ @text, index,
150
+ ))
151
+ end
152
+ elsif (match = sc.scan(SCAN_FUNCNAME))
153
+ funcname = match.chop
154
+ stack.push(funcname)
155
+ else
156
+ # We read a {{ but it's not apparently Formatter syntax.
157
+ # We could error, but it's nicer to just pass through as text.
158
+ # We do kind of assume that the text will probably have balanced
159
+ # pairs of {{ }} at least.
160
+ emit('{{', stack)
161
+ stack.push(LITERAL_BRACES)
162
+ end
163
+ parse_body(sc, stack)
164
+ stack
165
+ end
166
+
167
+ def parse_body(sc, stack = [])
168
+ match = sc.scan(SCAN_BODY)
169
+ if match&.end_with?(BEGIN_EXPR)
170
+ emit(match[DISCARD_BRACES], stack)
171
+ parse_expr(sc, stack)
172
+ elsif match&.end_with?(END_EXPR)
173
+ emit(match[DISCARD_BRACES], stack)
174
+ if stack.pop == LITERAL_BRACES
175
+ emit('}}', stack)
176
+ end
177
+ parse_body(sc, stack)
178
+ elsif match
179
+ emit(match, stack)
180
+ else
181
+ emit(sc.rest, stack)
182
+ end
183
+ stack
184
+ end
185
+
186
+ def emit(text, stack)
187
+ return if text.nil? || text.empty?
188
+ @nodes << [text, stack.reject { |n| n == LITERAL_BRACES }]
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,269 @@
1
+ # coding: utf-8
2
+ require 'cli/ui'
3
+ require 'cli/ui/frame/frame_stack'
4
+ require 'cli/ui/frame/frame_style'
5
+
6
+ module CLI
7
+ module UI
8
+ module Frame
9
+ class UnnestedFrameException < StandardError; end
10
+ class << self
11
+ DEFAULT_FRAME_COLOR = CLI::UI.resolve_color(:cyan)
12
+
13
+ def frame_style
14
+ @frame_style ||= FrameStyle::Box
15
+ end
16
+
17
+ # Set the default frame style.
18
+ #
19
+ # Raises ArgumentError if +frame_style+ is not valid
20
+ #
21
+ # ==== Attributes
22
+ #
23
+ # * +symbol+ or +FrameStyle+ - the default frame style to use for frames
24
+ #
25
+ def frame_style=(frame_style)
26
+ @frame_style = CLI::UI.resolve_style(frame_style)
27
+ end
28
+
29
+ # Opens a new frame. Can be nested
30
+ # Can be invoked in two ways: block and blockless
31
+ # * In block form, the frame is closed automatically when the block returns
32
+ # * In blockless form, caller MUST call +Frame.close+ when the frame is logically done
33
+ # * Blockless form is strongly discouraged in cases where block form can be made to work
34
+ #
35
+ # https://user-images.githubusercontent.com/3074765/33799861-cb5dcb5c-dd01-11e7-977e-6fad38cee08c.png
36
+ #
37
+ # The return value of the block determines if the block is a "success" or a "failure"
38
+ #
39
+ # ==== Attributes
40
+ #
41
+ # * +text+ - (required) the text/title to output in the frame
42
+ #
43
+ # ==== Options
44
+ #
45
+ # * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+
46
+ # * +:failure_text+ - If the block failed, what do we output? Defaults to nil
47
+ # * +:success_text+ - If the block succeeds, what do we output? Defaults to nil
48
+ # * +:timing+ - How long did the frame content take? Invalid for blockless. Defaults to true for the block form
49
+ # * +frame_style+ - The frame style to use for this frame
50
+ #
51
+ # ==== Example
52
+ #
53
+ # ===== Block Form (Assumes +CLI::UI::StdoutRouter.enable+ has been called)
54
+ #
55
+ # CLI::UI::Frame.open('Open') { puts 'hi' }
56
+ #
57
+ # Default Output:
58
+ # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
59
+ # ┃ hi
60
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (0.0s) ━━
61
+ #
62
+ # ===== Blockless Form
63
+ #
64
+ # CLI::UI::Frame.open('Open')
65
+ #
66
+ # Default Output:
67
+ # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68
+ #
69
+ #
70
+ def open(
71
+ text,
72
+ color: DEFAULT_FRAME_COLOR,
73
+ failure_text: nil,
74
+ success_text: nil,
75
+ timing: nil,
76
+ frame_style: self.frame_style
77
+ )
78
+ frame_style = CLI::UI.resolve_style(frame_style)
79
+ color = CLI::UI.resolve_color(color)
80
+
81
+ unless block_given?
82
+ if failure_text
83
+ raise ArgumentError, 'failure_text is not compatible with blockless invocation'
84
+ elsif success_text
85
+ raise ArgumentError, 'success_text is not compatible with blockless invocation'
86
+ elsif timing
87
+ raise ArgumentError, 'timing is not compatible with blockless invocation'
88
+ end
89
+ end
90
+
91
+ t_start = Time.now
92
+ CLI::UI.raw do
93
+ print(prefix.chop)
94
+ puts frame_style.open(text, color: color)
95
+ end
96
+ FrameStack.push(color: color, style: frame_style)
97
+
98
+ return unless block_given?
99
+
100
+ closed = false
101
+ begin
102
+ success = false
103
+ success = yield
104
+ rescue
105
+ closed = true
106
+ t_diff = elasped(t_start, timing)
107
+ close(failure_text, color: :red, elapsed: t_diff)
108
+ raise
109
+ else
110
+ success
111
+ ensure
112
+ unless closed
113
+ t_diff = elasped(t_start, timing)
114
+ if success != false
115
+ close(success_text, color: color, elapsed: t_diff)
116
+ else
117
+ close(failure_text, color: :red, elapsed: t_diff)
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ # Adds a divider in a frame
124
+ # Used to separate information within a single frame
125
+ #
126
+ # ==== Attributes
127
+ #
128
+ # * +text+ - (required) the text/title to output in the frame
129
+ #
130
+ # ==== Options
131
+ #
132
+ # * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+
133
+ # * +frame_style+ - The frame style to use for this frame
134
+ #
135
+ # ==== Example
136
+ #
137
+ # CLI::UI::Frame.open('Open') { CLI::UI::Frame.divider('Divider') }
138
+ #
139
+ # Default Output:
140
+ # ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
141
+ # ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
142
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
143
+ #
144
+ # ==== Raises
145
+ #
146
+ # MUST be inside an open frame or it raises a +UnnestedFrameException+
147
+ #
148
+ def divider(text, color: nil, frame_style: nil)
149
+ fs_item = FrameStack.pop
150
+ raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item
151
+
152
+ color = CLI::UI.resolve_color(color) || fs_item.color
153
+ frame_style = CLI::UI.resolve_style(frame_style) || fs_item.frame_style
154
+
155
+ CLI::UI.raw do
156
+ print(prefix.chop)
157
+ puts frame_style.divider(text, color: color)
158
+ end
159
+
160
+ FrameStack.push(fs_item)
161
+ end
162
+
163
+ # Closes a frame
164
+ # Automatically called for a block-form +open+
165
+ #
166
+ # ==== Attributes
167
+ #
168
+ # * +text+ - (required) the text/title to output in the frame
169
+ #
170
+ # ==== Options
171
+ #
172
+ # * +:color+ - The color of the frame. Defaults to nil
173
+ # * +:elapsed+ - How long did the frame take? Defaults to nil
174
+ # * +frame_style+ - The frame style to use for this frame. Defaults to nil
175
+ #
176
+ # ==== Example
177
+ #
178
+ # CLI::UI::Frame.close('Close')
179
+ #
180
+ # Default Output:
181
+ # ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
182
+ #
183
+ # ==== Raises
184
+ #
185
+ # MUST be inside an open frame or it raises a +UnnestedFrameException+
186
+ #
187
+ def close(text, color: nil, elapsed: nil, frame_style: nil)
188
+ fs_item = FrameStack.pop
189
+ raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item
190
+
191
+ color = CLI::UI.resolve_color(color) || fs_item.color
192
+ frame_style = CLI::UI.resolve_style(frame_style) || fs_item.frame_style
193
+
194
+ kwargs = {}
195
+ if elapsed
196
+ kwargs[:right_text] = "(#{elapsed.round(2)}s)"
197
+ end
198
+
199
+ CLI::UI.raw do
200
+ print(prefix.chop)
201
+ puts frame_style.close(text, color: color, **kwargs)
202
+ end
203
+ end
204
+
205
+ # Determines the prefix of a frame entry taking multi-nested frames into account
206
+ #
207
+ # ==== Options
208
+ #
209
+ # * +:color+ - The color of the prefix. Defaults to +Thread.current[:cliui_frame_color_override]+
210
+ #
211
+ def prefix(color: Thread.current[:cliui_frame_color_override])
212
+ +''.tap do |output|
213
+ items = FrameStack.items
214
+
215
+ items[0..-2].each do |item|
216
+ output << item.color.code << item.frame_style.prefix
217
+ end
218
+
219
+ if (item = items.last)
220
+ final_color = color || item.color
221
+ output << CLI::UI.resolve_color(final_color).code \
222
+ << item.frame_style.prefix \
223
+ << ' ' \
224
+ << CLI::UI::Color::RESET.code
225
+ end
226
+ end
227
+ end
228
+
229
+ # The width of a prefix given the number of Frames in the stack
230
+ def prefix_width
231
+ w = FrameStack.items.reduce(0) do |width, item|
232
+ width + item.frame_style.prefix_width
233
+ end
234
+
235
+ w.zero? ? w : w + 1
236
+ end
237
+
238
+ # Override a color for a given thread.
239
+ #
240
+ # ==== Attributes
241
+ #
242
+ # * +color+ - The color to override to
243
+ #
244
+ def with_frame_color_override(color)
245
+ prev = Thread.current[:cliui_frame_color_override]
246
+ Thread.current[:cliui_frame_color_override] = color
247
+ yield
248
+ ensure
249
+ Thread.current[:cliui_frame_color_override] = prev
250
+ end
251
+
252
+ private
253
+
254
+ # If timing is:
255
+ # Numeric: return it
256
+ # false: return nil
257
+ # true or nil: defaults to Time.new
258
+ # Time: return the difference with start
259
+ def elasped(start, timing)
260
+ return timing if timing.is_a?(Numeric)
261
+ return if timing.is_a?(FalseClass)
262
+
263
+ timing = Time.new if timing.is_a?(TrueClass) || timing.nil?
264
+ timing - start
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end