cli-ui 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  #