cli-ui 1.2.2 → 1.5.1
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/README.md +45 -1
- data/lib/cli/ui.rb +75 -29
- data/lib/cli/ui/ansi.rb +10 -6
- data/lib/cli/ui/color.rb +12 -7
- data/lib/cli/ui/formatter.rb +34 -21
- data/lib/cli/ui/frame.rb +111 -152
- data/lib/cli/ui/frame/frame_stack.rb +98 -0
- data/lib/cli/ui/frame/frame_style.rb +120 -0
- data/lib/cli/ui/frame/frame_style/box.rb +166 -0
- data/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
- data/lib/cli/ui/glyph.rb +23 -17
- data/lib/cli/ui/os.rb +67 -0
- data/lib/cli/ui/printer.rb +59 -0
- data/lib/cli/ui/progress.rb +9 -7
- data/lib/cli/ui/prompt.rb +97 -21
- data/lib/cli/ui/prompt/interactive_options.rb +75 -61
- data/lib/cli/ui/prompt/options_handler.rb +7 -2
- data/lib/cli/ui/spinner.rb +23 -5
- data/lib/cli/ui/spinner/spin_group.rb +34 -12
- data/lib/cli/ui/stdout_router.rb +13 -8
- data/lib/cli/ui/terminal.rb +26 -16
- data/lib/cli/ui/truncater.rb +4 -4
- data/lib/cli/ui/version.rb +1 -1
- data/lib/cli/ui/widgets.rb +77 -0
- data/lib/cli/ui/widgets/base.rb +27 -0
- data/lib/cli/ui/widgets/status.rb +61 -0
- data/lib/cli/ui/wrap.rb +56 -0
- metadata +17 -16
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -17
- data/.travis.yml +0 -5
- data/Gemfile +0 -16
- data/Rakefile +0 -20
- data/bin/console +0 -14
- data/cli-ui.gemspec +0 -27
- data/dev.yml +0 -14
- data/lib/cli/ui/box.rb +0 -15
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'cli/ui/frame'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module UI
|
5
|
+
module Frame
|
6
|
+
module FrameStyle
|
7
|
+
class << self
|
8
|
+
# rubocop:disable Style/ClassVars
|
9
|
+
@@loaded_styles = []
|
10
|
+
|
11
|
+
def loaded_styles
|
12
|
+
@@loaded_styles.map(&:name)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Lookup a frame style via its name
|
16
|
+
#
|
17
|
+
# ==== Attributes
|
18
|
+
#
|
19
|
+
# * +symbol+ - frame style name to lookup
|
20
|
+
def lookup(name)
|
21
|
+
@@loaded_styles
|
22
|
+
.find { |style| style.name.to_sym == name }
|
23
|
+
.tap { |style| raise InvalidFrameStyleName, name if style.nil? }
|
24
|
+
end
|
25
|
+
|
26
|
+
def extended(base)
|
27
|
+
@@loaded_styles << base
|
28
|
+
base.extend(Interface)
|
29
|
+
end
|
30
|
+
# rubocop:enable Style/ClassVars
|
31
|
+
end
|
32
|
+
|
33
|
+
class InvalidFrameStyleName < ArgumentError
|
34
|
+
def initialize(name)
|
35
|
+
super
|
36
|
+
@name = name
|
37
|
+
end
|
38
|
+
|
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
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public interface for FrameStyles
|
48
|
+
# Applied by extending FrameStyle
|
49
|
+
module Interface
|
50
|
+
def name
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
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
|
59
|
+
|
60
|
+
# Returns the printing width of the prefix
|
61
|
+
def prefix_width
|
62
|
+
CLI::UI::ANSI.printing_width(prefix)
|
63
|
+
end
|
64
|
+
|
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
|
78
|
+
|
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
|
93
|
+
|
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
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def print_at_x(x, str)
|
111
|
+
CLI::UI::ANSI.cursor_horizontal_absolute(1 + x) + str
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
require 'cli/ui/frame/frame_style/box'
|
120
|
+
require 'cli/ui/frame/frame_style/bracket'
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module CLI
|
2
|
+
module UI
|
3
|
+
module Frame
|
4
|
+
module FrameStyle
|
5
|
+
module Box
|
6
|
+
extend FrameStyle
|
7
|
+
|
8
|
+
VERTICAL = '┃'
|
9
|
+
HORIZONTAL = '━'
|
10
|
+
DIVIDER = '┣'
|
11
|
+
TOP_LEFT = '┏'
|
12
|
+
BOTTOM_LEFT = '┗'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def name
|
16
|
+
'box'
|
17
|
+
end
|
18
|
+
|
19
|
+
def prefix
|
20
|
+
VERTICAL
|
21
|
+
end
|
22
|
+
|
23
|
+
# Draws the "Open" line for this frame style
|
24
|
+
#
|
25
|
+
# ==== Attributes
|
26
|
+
#
|
27
|
+
# * +text+ - (required) the text/title to output in the frame
|
28
|
+
#
|
29
|
+
# ==== Options
|
30
|
+
#
|
31
|
+
# * +:color+ - (required) The color of the frame.
|
32
|
+
#
|
33
|
+
# ==== Output:
|
34
|
+
#
|
35
|
+
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
36
|
+
#
|
37
|
+
def open(text, color:)
|
38
|
+
edge(text, color: color, first: TOP_LEFT)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Draws a "divider" line for the current frame style
|
42
|
+
#
|
43
|
+
# ==== Attributes
|
44
|
+
#
|
45
|
+
# * +text+ - (required) the text/title to output in the frame
|
46
|
+
#
|
47
|
+
# ==== Options
|
48
|
+
#
|
49
|
+
# * +:color+ - (required) The color of the frame.
|
50
|
+
#
|
51
|
+
# ==== Output:
|
52
|
+
#
|
53
|
+
# ┣━━ Divider ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
54
|
+
#
|
55
|
+
def divider(text, color:)
|
56
|
+
edge(text, color: color, first: DIVIDER)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Draws the "Close" line for this frame style
|
60
|
+
#
|
61
|
+
# ==== Attributes
|
62
|
+
#
|
63
|
+
# * +text+ - (required) the text/title to output in the frame
|
64
|
+
#
|
65
|
+
# ==== Options
|
66
|
+
#
|
67
|
+
# * +:color+ - (required) The color of the frame.
|
68
|
+
# * +:right_text+ - Text to print at the right of the line. Defaults to nil
|
69
|
+
#
|
70
|
+
# ==== Output:
|
71
|
+
#
|
72
|
+
# ┗━━ Close ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
73
|
+
#
|
74
|
+
def close(text, color:, right_text: nil)
|
75
|
+
edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def edge(text, color:, first:, right_text: nil)
|
81
|
+
color = CLI::UI.resolve_color(color)
|
82
|
+
|
83
|
+
preamble = +''
|
84
|
+
|
85
|
+
preamble << color.code << first << (HORIZONTAL * 2)
|
86
|
+
|
87
|
+
text ||= ''
|
88
|
+
unless text.empty?
|
89
|
+
preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' '
|
90
|
+
end
|
91
|
+
|
92
|
+
termwidth = CLI::UI::Terminal.width
|
93
|
+
|
94
|
+
suffix = +''
|
95
|
+
|
96
|
+
if right_text
|
97
|
+
suffix << ' ' << right_text << ' '
|
98
|
+
end
|
99
|
+
|
100
|
+
preamble_width = CLI::UI::ANSI.printing_width(preamble)
|
101
|
+
preamble_start = Frame.prefix_width
|
102
|
+
# If prefix_width is non-zero, we need to subtract the width of
|
103
|
+
# the final space, since we're going to write over it.
|
104
|
+
preamble_start -= 1 unless preamble_start.zero?
|
105
|
+
preamble_end = preamble_start + preamble_width
|
106
|
+
|
107
|
+
suffix_width = CLI::UI::ANSI.printing_width(suffix)
|
108
|
+
suffix_end = termwidth - 2
|
109
|
+
suffix_start = suffix_end - suffix_width
|
110
|
+
|
111
|
+
if preamble_end > suffix_start
|
112
|
+
suffix = ''
|
113
|
+
# if preamble_end > termwidth
|
114
|
+
# we *could* truncate it, but let's just let it overflow to the
|
115
|
+
# next line and call it poor usage of this API.
|
116
|
+
end
|
117
|
+
|
118
|
+
o = +''
|
119
|
+
|
120
|
+
# Shopify's CI system supports terminal emulation, but not some of
|
121
|
+
# the fancier features that we normally use to draw frames
|
122
|
+
# extra-reliably, so we fall back to a less foolproof strategy. This
|
123
|
+
# is probably better in general for cases with impoverished terminal
|
124
|
+
# emulators and no active user.
|
125
|
+
unless [0, '', nil].include?(ENV['CI'])
|
126
|
+
linewidth = [0, termwidth - (preamble_end + suffix_width + 1)].max
|
127
|
+
|
128
|
+
o << color.code << preamble
|
129
|
+
o << color.code << (HORIZONTAL * linewidth)
|
130
|
+
o << color.code << suffix
|
131
|
+
o << CLI::UI::Color::RESET.code << "\n"
|
132
|
+
return o
|
133
|
+
end
|
134
|
+
|
135
|
+
# Jumping around the line can cause some unwanted flashes
|
136
|
+
o << CLI::UI::ANSI.hide_cursor
|
137
|
+
|
138
|
+
# reset to column 1 so that things like ^C don't ruin formatting
|
139
|
+
o << "\r"
|
140
|
+
|
141
|
+
# This code will print out a full line with the given preamble and
|
142
|
+
# suffix, as exemplified below.
|
143
|
+
#
|
144
|
+
# preamble_start suffix_start
|
145
|
+
# | preamble_end | suffix_end
|
146
|
+
# | | | | termwidth
|
147
|
+
# | | | | |
|
148
|
+
# V V V V V
|
149
|
+
# --- Preamble text --------------------- suffix text --
|
150
|
+
o << color.code
|
151
|
+
o << print_at_x(preamble_start, HORIZONTAL * (termwidth - preamble_start)) # draw a full line
|
152
|
+
o << print_at_x(preamble_start, preamble)
|
153
|
+
o << color.code
|
154
|
+
o << print_at_x(suffix_start, suffix)
|
155
|
+
o << CLI::UI::Color::RESET.code
|
156
|
+
o << CLI::UI::ANSI.show_cursor
|
157
|
+
o << "\n"
|
158
|
+
|
159
|
+
o
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module CLI
|
2
|
+
module UI
|
3
|
+
module Frame
|
4
|
+
module FrameStyle
|
5
|
+
module Bracket
|
6
|
+
extend FrameStyle
|
7
|
+
|
8
|
+
VERTICAL = '┃'
|
9
|
+
HORIZONTAL = '━'
|
10
|
+
DIVIDER = '┣'
|
11
|
+
TOP_LEFT = '┏'
|
12
|
+
BOTTOM_LEFT = '┗'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def name
|
16
|
+
'bracket'
|
17
|
+
end
|
18
|
+
|
19
|
+
def prefix
|
20
|
+
VERTICAL
|
21
|
+
end
|
22
|
+
|
23
|
+
# Draws the "Open" line for this frame style
|
24
|
+
#
|
25
|
+
# ==== Attributes
|
26
|
+
#
|
27
|
+
# * +text+ - (required) the text/title to output in the frame
|
28
|
+
#
|
29
|
+
# ==== Options
|
30
|
+
#
|
31
|
+
# * +:color+ - (required) The color of the frame.
|
32
|
+
#
|
33
|
+
# ==== Output
|
34
|
+
#
|
35
|
+
# ┏━━ Open
|
36
|
+
#
|
37
|
+
def open(text, color:)
|
38
|
+
edge(text, color: color, first: TOP_LEFT)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Draws a "divider" line for the current frame style
|
42
|
+
#
|
43
|
+
# ==== Attributes
|
44
|
+
#
|
45
|
+
# * +text+ - (required) the text/title to output in the frame
|
46
|
+
#
|
47
|
+
# ==== Options
|
48
|
+
#
|
49
|
+
# * +:color+ - (required) The color of the frame.
|
50
|
+
#
|
51
|
+
# ==== Output:
|
52
|
+
#
|
53
|
+
# ┣━━ Divider
|
54
|
+
#
|
55
|
+
def divider(text, color:)
|
56
|
+
edge(text, color: color, first: DIVIDER)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Draws the "Close" line for this frame style
|
60
|
+
#
|
61
|
+
# ==== Attributes
|
62
|
+
#
|
63
|
+
# * +text+ - (required) the text/title to output in the frame
|
64
|
+
#
|
65
|
+
# ==== Options
|
66
|
+
#
|
67
|
+
# * +:color+ - (required) The color of the frame.
|
68
|
+
# * +:right_text+ - Text to print at the right of the line. Defaults to nil
|
69
|
+
#
|
70
|
+
# ==== Output:
|
71
|
+
#
|
72
|
+
# ┗━━ Close
|
73
|
+
#
|
74
|
+
def close(text, color:, right_text: nil)
|
75
|
+
edge(text, color: color, right_text: right_text, first: BOTTOM_LEFT)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def edge(text, color:, first:, right_text: nil)
|
81
|
+
color = CLI::UI.resolve_color(color)
|
82
|
+
|
83
|
+
preamble = +''
|
84
|
+
|
85
|
+
preamble << color.code << first << (HORIZONTAL * 2)
|
86
|
+
|
87
|
+
text ||= ''
|
88
|
+
unless text.empty?
|
89
|
+
preamble << ' ' << CLI::UI.resolve_text("{{#{color.name}:#{text}}}") << ' '
|
90
|
+
end
|
91
|
+
|
92
|
+
suffix = +''
|
93
|
+
|
94
|
+
if right_text
|
95
|
+
suffix << ' ' << right_text << ' '
|
96
|
+
end
|
97
|
+
|
98
|
+
o = +''
|
99
|
+
|
100
|
+
# Shopify's CI system supports terminal emulation, but not some of
|
101
|
+
# the fancier features that we normally use to draw frames
|
102
|
+
# extra-reliably, so we fall back to a less foolproof strategy. This
|
103
|
+
# is probably better in general for cases with impoverished terminal
|
104
|
+
# emulators and no active user.
|
105
|
+
unless [0, '', nil].include?(ENV['CI'])
|
106
|
+
o << color.code << preamble
|
107
|
+
o << color.code << suffix
|
108
|
+
o << CLI::UI::Color::RESET.code
|
109
|
+
o << "\n"
|
110
|
+
|
111
|
+
return o
|
112
|
+
end
|
113
|
+
|
114
|
+
preamble_start = Frame.prefix_width
|
115
|
+
|
116
|
+
# If prefix_width is non-zero, we need to subtract the width of
|
117
|
+
# the final space, since we're going to write over it.
|
118
|
+
preamble_start -= 1 unless preamble_start.zero?
|
119
|
+
|
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.
|
123
|
+
o << CLI::UI::ANSI.hide_cursor
|
124
|
+
|
125
|
+
# reset to column 1 so that things like ^C don't ruin formatting
|
126
|
+
o << "\r"
|
127
|
+
o << color.code
|
128
|
+
o << print_at_x(preamble_start, preamble + color.code + suffix)
|
129
|
+
o << CLI::UI::Color::RESET.code
|
130
|
+
o << "\n"
|
131
|
+
|
132
|
+
o
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/cli/ui/glyph.rb
CHANGED
@@ -5,6 +5,7 @@ module CLI
|
|
5
5
|
class Glyph
|
6
6
|
class InvalidGlyphHandle < ArgumentError
|
7
7
|
def initialize(handle)
|
8
|
+
super
|
8
9
|
@handle = handle
|
9
10
|
end
|
10
11
|
|
@@ -15,7 +16,7 @@ module CLI
|
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
18
|
-
attr_reader :handle, :codepoint, :color, :
|
19
|
+
attr_reader :handle, :codepoint, :color, :to_s, :fmt
|
19
20
|
|
20
21
|
# Creates a new glyph
|
21
22
|
#
|
@@ -23,35 +24,40 @@ module CLI
|
|
23
24
|
#
|
24
25
|
# * +handle+ - The handle in the +MAP+ constant
|
25
26
|
# * +codepoint+ - The codepoint used to create the glyph (e.g. +0x2717+ for a ballot X)
|
27
|
+
# * +plain+ - A fallback plain string to be used in case glyphs are disabled
|
26
28
|
# * +color+ - What color to output the glyph. Check +CLI::UI::Color+ for options.
|
27
29
|
#
|
28
|
-
def initialize(handle, codepoint, color)
|
30
|
+
def initialize(handle, codepoint, plain, color)
|
29
31
|
@handle = handle
|
30
32
|
@codepoint = codepoint
|
31
33
|
@color = color
|
32
|
-
@
|
34
|
+
@plain = plain
|
35
|
+
@char = Array(codepoint).pack('U*')
|
33
36
|
@to_s = color.code + char + Color::RESET.code
|
34
37
|
@fmt = "{{#{color.name}:#{char}}}"
|
35
38
|
|
36
39
|
MAP[handle] = self
|
37
40
|
end
|
38
41
|
|
42
|
+
# Fetches the actual character(s) to be displayed for a glyph, based on the current OS support
|
43
|
+
#
|
44
|
+
# ==== Returns
|
45
|
+
# Returns the glyph string
|
46
|
+
def char
|
47
|
+
CLI::UI::OS.current.supports_emoji? ? @char : @plain
|
48
|
+
end
|
49
|
+
|
39
50
|
# Mapping of glyphs to terminal output
|
40
51
|
MAP = {}
|
41
|
-
#
|
42
|
-
|
43
|
-
# BLUE
|
44
|
-
|
45
|
-
#
|
46
|
-
|
47
|
-
#
|
48
|
-
|
49
|
-
#
|
50
|
-
X = new('x', 0x2717, Color::RED)
|
51
|
-
# Bug emoji (🐛)
|
52
|
-
BUG = new('b', 0x1f41b, Color::WHITE)
|
53
|
-
# RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (»)
|
54
|
-
CHEVRON = new('>', 0xbb, Color::YELLOW)
|
52
|
+
STAR = new('*', 0x2b51, '*', Color::YELLOW) # YELLOW SMALL STAR (⭑)
|
53
|
+
INFO = new('i', 0x1d4be, 'i', Color::BLUE) # BLUE MATHEMATICAL SCRIPT SMALL i (𝒾)
|
54
|
+
QUESTION = new('?', 0x003f, '?', Color::BLUE) # BLUE QUESTION MARK (?)
|
55
|
+
CHECK = new('v', 0x2713, '√', Color::GREEN) # GREEN CHECK MARK (✓)
|
56
|
+
X = new('x', 0x2717, 'X', Color::RED) # RED BALLOT X (✗)
|
57
|
+
BUG = new('b', 0x1f41b, '!', Color::WHITE) # Bug emoji (🐛)
|
58
|
+
CHEVRON = new('>', 0xbb, '»', Color::YELLOW) # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (»)
|
59
|
+
HOURGLASS = new('H', [0x231b, 0xfe0e], 'H', Color::BLUE) # HOURGLASS + VARIATION SELECTOR 15 (⌛︎)
|
60
|
+
WARNING = new('!', [0x26a0, 0xfe0f], '!', Color::YELLOW) # WARNING SIGN + VARIATION SELECTOR 16 (⚠️ )
|
55
61
|
|
56
62
|
# Looks up a glyph by name
|
57
63
|
#
|