cli-ui 1.2.1 → 1.5.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.
@@ -0,0 +1,98 @@
1
+ module CLI
2
+ module UI
3
+ module Frame
4
+ module FrameStack
5
+ COLOR_ENVVAR = 'CLI_FRAME_STACK'
6
+ STYLE_ENVVAR = 'CLI_STYLE_STACK'
7
+
8
+ class StackItem
9
+ attr_reader :color, :frame_style
10
+
11
+ def initialize(color_name, style_name)
12
+ @color = CLI::UI.resolve_color(color_name)
13
+ @frame_style = CLI::UI.resolve_style(style_name)
14
+ end
15
+ end
16
+
17
+ class << self
18
+ # Fetch all items off the frame stack
19
+ 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
26
+ end
27
+
28
+ # Push a new item onto the frame stack.
29
+ #
30
+ # Either an item or a :color/:style pair should be pushed onto the stack.
31
+ #
32
+ # ==== Attributes
33
+ #
34
+ # * +item+ a +StackItem+ to push onto the stack. Defaults to nil
35
+ #
36
+ # ==== Options
37
+ #
38
+ # * +:color+ the color of the new stack item. Defaults to nil
39
+ # * +:style+ the style of the new stack item. Defaults to nil
40
+ #
41
+ # ==== Raises
42
+ #
43
+ # If both an item and a color/style pair are given, raises an +ArgumentError+
44
+ # If the given item is not a +StackItem+, raises an +ArgumentError+
45
+ #
46
+ 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
55
+ end
56
+
57
+ item ||= StackItem.new(color, style)
58
+
59
+ curr = items
60
+ curr << item
61
+
62
+ serialize(curr)
63
+ end
64
+
65
+ # Removes and returns the last stack item off the stack
66
+ 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(':')
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -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