cli-ui 1.3.0 → 1.4.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,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
@@ -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, :char, :to_s, :fmt
19
+ attr_reader :handle, :codepoint, :color, :to_s, :fmt
19
20
 
20
21
  # Creates a new glyph
21
22
  #
@@ -23,12 +24,14 @@ 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
34
+ @plain = plain
32
35
  @char = Array(codepoint).pack('U*')
33
36
  @to_s = color.code + char + Color::RESET.code
34
37
  @fmt = "{{#{color.name}:#{char}}}"
@@ -36,16 +39,24 @@ module CLI
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
- STAR = new('*', 0x2b51, Color::YELLOW) # YELLOW SMALL STAR (⭑)
42
- INFO = new('i', 0x1d4be, Color::BLUE) # BLUE MATHEMATICAL SCRIPT SMALL i (𝒾)
43
- QUESTION = new('?', 0x003f, Color::BLUE) # BLUE QUESTION MARK (?)
44
- CHECK = new('v', 0x2713, Color::GREEN) # GREEN CHECK MARK (✓)
45
- X = new('x', 0x2717, Color::RED) # RED BALLOT X (✗)
46
- BUG = new('b', 0x1f41b, Color::WHITE) # Bug emoji (🐛)
47
- CHEVRON = new('>', 0xbb, Color::YELLOW) # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (»)
48
- HOURGLASS = new('H', [0x231b, 0xfe0e], Color::BLUE) # HOURGLASS + VARIATION SELECTOR 15 (⌛︎)
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 (⌛︎)
49
60
 
50
61
  # Looks up a glyph by name
51
62
  #