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.
- checksums.yaml +4 -4
- data/exe/rfix +11 -90
- data/lib/rfix.rb +10 -9
- data/lib/rfix/branch/reference.rb +2 -2
- data/lib/rfix/branch/upstream.rb +2 -4
- data/lib/rfix/cli/command.rb +14 -1
- data/lib/rfix/cli/command/all.rb +21 -0
- data/lib/rfix/cli/command/base.rb +30 -19
- data/lib/rfix/cli/command/branch.rb +2 -0
- data/lib/rfix/cli/command/help.rb +2 -0
- data/lib/rfix/cli/command/info.rb +6 -1
- data/lib/rfix/cli/command/local.rb +2 -0
- data/lib/rfix/cli/command/origin.rb +2 -0
- data/lib/rfix/cli/command/setup.rb +2 -0
- data/lib/rfix/cli/command/status.rb +39 -0
- data/lib/rfix/collector.rb +69 -0
- data/lib/rfix/diff.rb +69 -0
- data/lib/rfix/extension/comment_config.rb +15 -0
- data/lib/rfix/extension/offense.rb +17 -14
- data/lib/rfix/extension/pastel.rb +7 -4
- data/lib/rfix/extension/progresbar.rb +15 -0
- data/lib/rfix/extension/strings.rb +10 -2
- data/lib/rfix/file.rb +5 -3
- data/lib/rfix/file/base.rb +21 -14
- data/lib/rfix/file/deleted.rb +2 -0
- data/lib/rfix/file/ignored.rb +2 -0
- data/lib/rfix/file/null.rb +17 -0
- data/lib/rfix/file/tracked.rb +39 -23
- data/lib/rfix/file/undefined.rb +17 -0
- data/lib/rfix/file/untracked.rb +3 -1
- data/lib/rfix/formatter.rb +67 -71
- data/lib/rfix/highlighter.rb +1 -3
- data/lib/rfix/rake/gemfile.rb +26 -23
- data/lib/rfix/repository.rb +59 -96
- data/lib/rfix/types.rb +24 -14
- data/lib/rfix/version.rb +1 -1
- data/rfix.gemspec +11 -3
- data/vendor/cli-ui/Gemfile +17 -0
- data/vendor/cli-ui/Gemfile.lock +60 -0
- data/vendor/cli-ui/LICENSE.txt +21 -0
- data/vendor/cli-ui/README.md +224 -0
- data/vendor/cli-ui/Rakefile +20 -0
- data/vendor/cli-ui/bin/console +14 -0
- data/vendor/cli-ui/cli-ui.gemspec +25 -0
- data/vendor/cli-ui/dev.yml +14 -0
- data/vendor/cli-ui/lib/cli/ui.rb +233 -0
- data/vendor/cli-ui/lib/cli/ui/ansi.rb +157 -0
- data/vendor/cli-ui/lib/cli/ui/color.rb +84 -0
- data/vendor/cli-ui/lib/cli/ui/formatter.rb +192 -0
- data/vendor/cli-ui/lib/cli/ui/frame.rb +269 -0
- data/vendor/cli-ui/lib/cli/ui/frame/frame_stack.rb +98 -0
- data/vendor/cli-ui/lib/cli/ui/frame/frame_style.rb +120 -0
- data/vendor/cli-ui/lib/cli/ui/frame/frame_style/box.rb +166 -0
- data/vendor/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
- data/vendor/cli-ui/lib/cli/ui/glyph.rb +84 -0
- data/vendor/cli-ui/lib/cli/ui/os.rb +67 -0
- data/vendor/cli-ui/lib/cli/ui/printer.rb +59 -0
- data/vendor/cli-ui/lib/cli/ui/progress.rb +90 -0
- data/vendor/cli-ui/lib/cli/ui/prompt.rb +297 -0
- data/vendor/cli-ui/lib/cli/ui/prompt/interactive_options.rb +484 -0
- data/vendor/cli-ui/lib/cli/ui/prompt/options_handler.rb +29 -0
- data/vendor/cli-ui/lib/cli/ui/spinner.rb +66 -0
- data/vendor/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
- data/vendor/cli-ui/lib/cli/ui/spinner/spin_group.rb +263 -0
- data/vendor/cli-ui/lib/cli/ui/stdout_router.rb +232 -0
- data/vendor/cli-ui/lib/cli/ui/terminal.rb +46 -0
- data/vendor/cli-ui/lib/cli/ui/truncater.rb +102 -0
- data/vendor/cli-ui/lib/cli/ui/version.rb +5 -0
- data/vendor/cli-ui/lib/cli/ui/widgets.rb +77 -0
- data/vendor/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
- data/vendor/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
- data/vendor/cli-ui/lib/cli/ui/wrap.rb +56 -0
- data/vendor/cli-ui/test/cli/ui/ansi_test.rb +32 -0
- data/vendor/cli-ui/test/cli/ui/cli_ui_test.rb +23 -0
- data/vendor/cli-ui/test/cli/ui/color_test.rb +40 -0
- data/vendor/cli-ui/test/cli/ui/formatter_test.rb +79 -0
- data/vendor/cli-ui/test/cli/ui/glyph_test.rb +68 -0
- data/vendor/cli-ui/test/cli/ui/printer_test.rb +103 -0
- data/vendor/cli-ui/test/cli/ui/progress_test.rb +46 -0
- data/vendor/cli-ui/test/cli/ui/prompt/options_handler_test.rb +39 -0
- data/vendor/cli-ui/test/cli/ui/prompt_test.rb +348 -0
- data/vendor/cli-ui/test/cli/ui/spinner/spin_group_test.rb +39 -0
- data/vendor/cli-ui/test/cli/ui/spinner_test.rb +141 -0
- data/vendor/cli-ui/test/cli/ui/stdout_router_test.rb +32 -0
- data/vendor/cli-ui/test/cli/ui/terminal_test.rb +26 -0
- data/vendor/cli-ui/test/cli/ui/truncater_test.rb +31 -0
- data/vendor/cli-ui/test/cli/ui/widgets/status_test.rb +49 -0
- data/vendor/cli-ui/test/cli/ui/widgets_test.rb +15 -0
- data/vendor/cli-ui/test/test_helper.rb +53 -0
- data/vendor/cli-ui/tmp/cache/bootsnap/compile-cache/d9/c036af0f3dc494 +0 -0
- data/vendor/cli-ui/tmp/cache/bootsnap/load-path-cache +0 -0
- data/vendor/dry-cli/lib/dry/cli/command.rb +2 -1
- data/vendor/dry-cli/tmp/cache/bootsnap/compile-cache/ff/a22a5daafbd74c +0 -0
- data/vendor/dry-cli/tmp/cache/bootsnap/load-path-cache +0 -0
- data/vendor/strings-ansi/tmp/cache/bootsnap/compile-cache/79/49cf49407b370e +0 -0
- data/vendor/strings-ansi/tmp/cache/bootsnap/load-path-cache +0 -0
- metadata +170 -9
- data/lib/rfix/extension/string.rb +0 -12
- 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
|