rfix 2.0.4 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|