sai 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sai.rb CHANGED
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'sai/ansi'
4
+ require 'sai/ansi/sequence_processor'
5
+ require 'sai/ansi/sequenced_string'
6
+ require 'sai/conversion/color_sequence'
7
+ require 'sai/conversion/rgb'
3
8
  require 'sai/decorator'
9
+ require 'sai/mode_selector'
4
10
  require 'sai/support'
5
11
  require 'sai/terminal/capabilities'
6
- require 'singleton'
12
+ require 'sai/terminal/color_mode'
7
13
 
8
14
  # An elegant color management system for crafting sophisticated CLI applications
9
15
  #
@@ -16,6 +22,7 @@ require 'singleton'
16
22
  # adaptive color to your terminal interfaces
17
23
  #
18
24
  # When included in a class or module, Sai provides the following instance methods:
25
+ # * {#color_mode} - Returns an interface to select Sai color modes
19
26
  # * {#decorator} - Returns a new instance of {Decorator} for method chaining
20
27
  # * {#terminal_color_support} - Returns the color support capabilities of the current terminal
21
28
  #
@@ -23,7 +30,7 @@ require 'singleton'
23
30
  # decorations (apply, call, decorate, encode). These methods are directly delegated to a new {Decorator} instance
24
31
  #
25
32
  # @author {https://aaronmallen.me Aaron Allen}
26
- # @since unreleased
33
+ # @since 0.1.0
27
34
  #
28
35
  # @api public
29
36
  #
@@ -48,100 +55,139 @@ module Sai
48
55
  ignored_decorator_methods = %i[apply call decorate encode]
49
56
  Decorator.instance_methods(false).reject { |m| ignored_decorator_methods.include?(m) }.each do |method|
50
57
  define_method(method) do |*arguments, **keyword_arguments|
51
- Decorator.new(send(:color_mode)).public_send(method, *arguments, **keyword_arguments)
58
+ Decorator.new(mode: Sai.mode.auto).public_send(method, *arguments, **keyword_arguments)
52
59
  end
53
60
  end
54
61
 
62
+ # The Sai {ModeSelector mode selector}
63
+ #
64
+ # @author {https://aaronmallen.me Aaron Allen}
65
+ # @since 0.2.0
66
+ #
67
+ # @api public
68
+ #
69
+ # @example
70
+ # Sai.mode.auto #=> 4
71
+ #
72
+ # @return [ModeSelector] the mode selector
73
+ # @rbs () -> singleton(ModeSelector)
74
+ def mode
75
+ ModeSelector
76
+ end
77
+
55
78
  # @rbs!
56
- # def black: () -> self
57
- # def blink: () -> self
58
- # def blue: () -> self
59
- # def bold: () -> self
60
- # def bright_black: () -> self
61
- # def bright_blue: () -> self
62
- # def bright_cyan: () -> self
63
- # def bright_green: () -> self
64
- # def bright_magenta: () -> self
65
- # def bright_red: () -> self
66
- # def bright_white: () -> self
67
- # def bright_yellow: () -> self
68
- # def conceal: () -> self
69
- # def cyan: () -> self
70
- # def dim: () -> self
71
- # def green: () -> self
72
- # def italic: () -> self
73
- # def magenta: () -> self
74
- # def no_blink: () -> self
75
- # def no_conceal: () -> self
76
- # def no_italic: () -> self
77
- # def no_reverse: () -> self
78
- # def no_strike: () -> self
79
- # def no_underline: () -> self
80
- # def normal_intensity: () -> self
81
- # def on_black: () -> self
82
- # def on_blue: () -> self
83
- # def on_bright_black: () -> self
84
- # def on_bright_blue: () -> self
85
- # def on_bright_cyan: () -> self
86
- # def on_bright_green: () -> self
87
- # def on_bright_magenta: () -> self
88
- # def on_bright_red: () -> self
89
- # def on_bright_white: () -> self
90
- # def on_bright_yellow: () -> self
91
- # def on_cyan: () -> self
92
- # def on_green: () -> self
93
- # def on_magenta: () -> self
94
- # def on_red: () -> self
95
- # def on_white: () -> self
96
- # def on_yellow: () -> self
97
- # def rapid_blink: () -> self
98
- # def red: () -> self
99
- # def reverse: () -> self
100
- # def strike: () -> self
101
- # def underline: () -> self
102
- # def white: () -> self
103
- # def yellow: () -> self
79
+ # def black: () -> Decorator
80
+ # def blink: () -> Decorator
81
+ # def blue: () -> Decorator
82
+ # def bold: () -> Decorator
83
+ # def bright_black: () -> Decorator
84
+ # def bright_blue: () -> Decorator
85
+ # def bright_cyan: () -> Decorator
86
+ # def bright_green: () -> Decorator
87
+ # def bright_magenta: () -> Decorator
88
+ # def bright_red: () -> Decorator
89
+ # def bright_white: () -> Decorator
90
+ # def bright_yellow: () -> Decorator
91
+ # def conceal: () -> Decorator
92
+ # def cyan: () -> Decorator
93
+ # def dim: () -> Decorator
94
+ # def green: () -> Decorator
95
+ # def italic: () -> Decorator
96
+ # def magenta: () -> Decorator
97
+ # def no_blink: () -> Decorator
98
+ # def no_conceal: () -> Decorator
99
+ # def no_italic: () -> Decorator
100
+ # def no_reverse: () -> Decorator
101
+ # def no_strike: () -> Decorator
102
+ # def no_underline: () -> Decorator
103
+ # def normal_intensity: () -> Decorator
104
+ # def on_black: () -> Decorator
105
+ # def on_blue: () -> Decorator
106
+ # def on_bright_black: () -> Decorator
107
+ # def on_bright_blue: () -> Decorator
108
+ # def on_bright_cyan: () -> Decorator
109
+ # def on_bright_green: () -> Decorator
110
+ # def on_bright_magenta: () -> Decorator
111
+ # def on_bright_red: () -> Decorator
112
+ # def on_bright_white: () -> Decorator
113
+ # def on_bright_yellow: () -> Decorator
114
+ # def on_cyan: () -> Decorator
115
+ # def on_green: () -> Decorator
116
+ # def on_magenta: () -> Decorator
117
+ # def on_red: () -> Decorator
118
+ # def on_white: () -> Decorator
119
+ # def on_yellow: () -> Decorator
120
+ # def rapid_blink: () -> Decorator
121
+ # def red: () -> Decorator
122
+ # def reverse: () -> Decorator
123
+ # def strike: () -> Decorator
124
+ # def underline: () -> Decorator
125
+ # def white: () -> Decorator
126
+ # def yellow: () -> Decorator
127
+
128
+ # Sequence a string with ANSI escape codes
129
+ #
130
+ # @author {https://aaronmallen.me Aaron Allen}
131
+ # @since 0.3.0
132
+ #
133
+ # @api public
134
+ #
135
+ # @example Sequence a string with ANSI escape codes
136
+ # Sai.sequence("\e[38;2;205;0;0mHello, World!\e[0m") #=> #<Sai::ANSI::SequencedString:0x123>
137
+ #
138
+ # @param text [String] the text to sequence
139
+ #
140
+ # @return [ANSI::SequencedString] the sequenced string
141
+ # @rbs (String text) -> ANSI::SequencedString
142
+ def sequence(text)
143
+ ANSI::SequencedString.new(text)
144
+ end
104
145
 
105
146
  # The supported color modes for the terminal
106
147
  #
107
148
  # @author {https://aaronmallen.me Aaron Allen}
108
- # @since unreleased
149
+ # @since 0.1.0
109
150
  #
110
151
  # @api public
111
152
  #
112
153
  # @example Check the color support of the terminal
113
154
  # Sai.support.ansi? # => true
114
155
  # Sai.support.basic? # => true
115
- # Sai.support.bit8? # => true
156
+ # Sai.support.advanced? # => true
116
157
  # Sai.support.no_color? # => false
117
158
  # Sai.support.true_color? # => true
118
159
  #
119
160
  # @return [Support] the color support
120
- # @rbs () -> Support
161
+ # @rbs () -> singleton(Support)
121
162
  def support
122
- @support ||= Support.new(color_mode).freeze
163
+ Support
123
164
  end
165
+ end
124
166
 
125
- private
126
-
127
- # Detect the color capabilities of the terminal
128
- #
129
- # @author {https://aaronmallen.me Aaron Allen}
130
- # @since unreleased
131
- #
132
- # @api private
133
- #
134
- # @return [Integer] the color mode
135
- # @rbs () -> Integer
136
- def color_mode
137
- Thread.current[:sai_color_mode] ||= Terminal::Capabilities.detect_color_support
138
- end
167
+ # A helper method that provides Sai color modes
168
+ #
169
+ # @author {https://aaronmallen.me Aaron Allen}
170
+ # @since 0.2.0
171
+ #
172
+ # @api public
173
+ #
174
+ # @example
175
+ # class MyClass
176
+ # include Sai
177
+ # end
178
+ #
179
+ # MyClass.new.color_mode.ansi #=> 2
180
+ #
181
+ # @return [ModeSelector] the mode selector
182
+ # @rbs () -> singleton(ModeSelector)
183
+ def color_mode
184
+ ModeSelector
139
185
  end
140
186
 
141
187
  # A helper method to initialize an instance of {Decorator}
142
188
  #
143
189
  # @author {https://aaronmallen.me Aaron Allen}
144
- # @since unreleased
190
+ # @since 0.1.0
145
191
  #
146
192
  # @api public
147
193
  #
@@ -153,16 +199,21 @@ module Sai
153
199
  # MyClass.new.decorator.blue.on_red.bold.decorate('Hello, world!')
154
200
  # #=> "\e[38;5;21m\e[48;5;160m\e[1mHello, world!\e[0m"
155
201
  #
202
+ # MyClass.new.decorator(mode: Sai.mode.no_color)
203
+ # #=> "Hello, world!"
204
+ #
205
+ # @param mode [Integer] the color mode to use
206
+ #
156
207
  # @return [Decorator] the Decorator instance
157
- # @rbs () -> Decorator
158
- def decorator
159
- Decorator.new(Terminal::Capabilities.detect_color_support)
208
+ # @rbs (?mode: Integer) -> Decorator
209
+ def decorator(mode: Sai.mode.auto)
210
+ Decorator.new(mode:)
160
211
  end
161
212
 
162
213
  # The supported color modes for the terminal
163
214
  #
164
215
  # @author {https://aaronmallen.me Aaron Allen}
165
- # @since unreleased
216
+ # @since 0.1.0
166
217
  #
167
218
  # @api public
168
219
  #
@@ -173,13 +224,13 @@ module Sai
173
224
  #
174
225
  # MyClass.new.terminal_color_support.ansi? # => true
175
226
  # MyClass.new.terminal_color_support.basic? # => true
176
- # MyClass.new.terminal_color_support.bit8? # => true
227
+ # MyClass.new.terminal_color_support.advanced? # => true
177
228
  # MyClass.new.terminal_color_support.no_color? # => false
178
229
  # MyClass.new.terminal_color_support.true_color? # => true
179
230
  #
180
231
  # @return [Support] the color support
181
- # @rbs () -> Support
232
+ # @rbs () -> singleton(Support)
182
233
  def terminal_color_support
183
- Sai.support
234
+ Support
184
235
  end
185
236
  end
data/sig/manifest.yaml ADDED
@@ -0,0 +1,3 @@
1
+ dependencies:
2
+ - name: forwardable
3
+ - name: strscan
@@ -0,0 +1,253 @@
1
+ # Generated from lib/sai/ansi/sequence_processor.rb with RBS::Inline
2
+
3
+ module Sai
4
+ module ANSI
5
+ # Extract ANSI sequence information from a string
6
+ #
7
+ # @author {https://aaronmallen.me Aaron Allen}
8
+ # @since 0.3.0
9
+ #
10
+ # @api private
11
+ class SequenceProcessor
12
+ # The pattern to extract ANSI sequences from a string
13
+ #
14
+ # @author {https://aaronmallen.me Aaron Allen}
15
+ # @since 0.3.0
16
+ #
17
+ # @api private
18
+ #
19
+ # @return [Regexp] the pattern
20
+ SEQUENCE_PATTERN: Regexp
21
+
22
+ # Matches the code portion of style sequences
23
+ #
24
+ # @author {https://aaronmallen.me Aaron Allen}
25
+ # @since 0.3.0
26
+ #
27
+ # @api private
28
+ #
29
+ # @return [Regexp] the pattern
30
+ STYLE_CODE_PATTERN: Regexp
31
+
32
+ # Initialize a new instance of SequenceProcessor and parse the provided string
33
+ #
34
+ # @author {https://aaronmallen.me Aaron Allen}
35
+ # @since 0.3.0
36
+ #
37
+ # @api private
38
+ #
39
+ # @param string [String] the string to parse
40
+ #
41
+ # @return [Array<Hash{Symbol => Object}>] the segments
42
+ # @rbs (String string) -> Array[Hash[Symbol, untyped]]
43
+ def self.process: (String string) -> Array[Hash[Symbol, untyped]]
44
+
45
+ # Initialize a new instance of SequenceProcessor
46
+ #
47
+ # @author {https://aaronmallen.me Aaron Allen}
48
+ # @since 0.3.0
49
+ #
50
+ # @api private
51
+ #
52
+ # @param string [String] the string to parse
53
+ #
54
+ # @return [SequenceProcessor] the new instance of SequenceProcessor
55
+ # @rbs (String string) -> void
56
+ def initialize: (String string) -> void
57
+
58
+ # Parse a string and return a hash of segments
59
+ #
60
+ # @author {https://aaronmallen.me Aaron Allen}
61
+ # @since 0.3.0
62
+ #
63
+ # @api private
64
+ #
65
+ # @return [Array<Hash{Symbol => Object}>] the segments
66
+ # @rbs () -> Array[Hash[Symbol, untyped]]
67
+ def process: () -> Array[Hash[Symbol, untyped]]
68
+
69
+ private
70
+
71
+ # Applies 24-bit truecolor (e.g., 38;2;R;G;B or 48;2;R;G;B)
72
+ #
73
+ # @author {https://aaronmallen.me Aaron Allen}
74
+ # @since 0.3.0
75
+ #
76
+ # @api private
77
+ #
78
+ # @param codes_array [Array<Integer>]
79
+ # @param index [Integer]
80
+ #
81
+ # @return [Integer] the updated index (consumed 5 codes)
82
+ # @rbs (Array[Integer] codes_array, Integer index) -> Integer
83
+ def apply_24bit_color: (Array[Integer] codes_array, Integer index) -> Integer
84
+
85
+ # Applies 256-color mode (e.g., 38;5;160 or 48;5;21)
86
+ #
87
+ # @author {https://aaronmallen.me Aaron Allen}
88
+ # @since 0.3.0
89
+ #
90
+ # @api private
91
+ #
92
+ # @param codes_array [Array<Integer>]
93
+ # @param index [Integer]
94
+ #
95
+ # @return [Integer] the updated index (consumed 3 codes)
96
+ # @rbs (Array[Integer] codes_array, Integer index) -> Integer
97
+ def apply_256_color: (Array[Integer] codes_array, Integer index) -> Integer
98
+
99
+ # Applies the appropriate action for the provided ANSI sequence
100
+ #
101
+ # @author {https://aaronmallen.me Aaron Allen}
102
+ # @since 0.3.0
103
+ #
104
+ # @api private
105
+ #
106
+ # @param sequence [String] an ANSI sequence (e.g. "\e[31m", "\e[0m")
107
+ #
108
+ # @return [void]
109
+ # @rbs (String sequence) -> void
110
+ def apply_ansi_sequence: (String sequence) -> void
111
+
112
+ # Applies a basic color (FG or BG) in the range 30..37 (FG) or 40..47 (BG)
113
+ #
114
+ # @author {https://aaronmallen.me Aaron Allen}
115
+ # @since 0.3.0
116
+ #
117
+ # @api private
118
+ #
119
+ # @param code [Integer] the numeric color code
120
+ #
121
+ # @return [void]
122
+ # @rbs (Integer code) -> void
123
+ def apply_basic_color: (Integer code) -> void
124
+
125
+ # Parse all numeric codes in the provided string, applying them in order (just like a real ANSI terminal)
126
+ #
127
+ # @author {https://aaronmallen.me Aaron Allen}
128
+ # @since 0.3.0
129
+ #
130
+ # @api private
131
+ #
132
+ # @param codes_string [String] e.g. "38;5;160;48;5;21;1"
133
+ #
134
+ # @return [void]
135
+ # @rbs (String codes_string) -> void
136
+ def apply_codes: (String codes_string) -> void
137
+
138
+ # Applies a single code (or group) from the array. This might be:
139
+ # - 0 => reset
140
+ # - 30..37 => basic FG color
141
+ # - 40..47 => basic BG color
142
+ # - 38 or 48 => extended color sequence
143
+ # - otherwise => style code (bold, underline, etc.)
144
+ #
145
+ # @author {https://aaronmallen.me Aaron Allen}
146
+ # @since 0.3.0
147
+ #
148
+ # @api private
149
+ #
150
+ # @param codes_array [Array<Integer>] the list of numeric codes
151
+ # @param index [Integer] the current index
152
+ #
153
+ # @return [Integer] the updated index after consuming needed codes
154
+ # @rbs (Array[Integer] codes_array, Integer index) -> Integer
155
+ def apply_single_code: (Array[Integer] codes_array, Integer index) -> Integer
156
+
157
+ # Applies a single style code (e.g. 1=bold, 2=dim, 4=underline, etc.) if it matches
158
+ #
159
+ # @author {https://aaronmallen.me Aaron Allen}
160
+ # @since 0.3.0
161
+ #
162
+ # @api private
163
+ #
164
+ # @param code [Integer] the numeric code to check
165
+ #
166
+ # @return [void]
167
+ # @rbs (Integer code) -> void
168
+ def apply_style_code: (Integer code) -> void
169
+
170
+ # Creates and returns a fresh, blank segment
171
+ #
172
+ # @author {https://aaronmallen.me Aaron Allen}
173
+ # @since 0.3.0
174
+ #
175
+ # @api private
176
+ #
177
+ # @return [Hash{Symbol => Object}] a new, empty segment
178
+ # @rbs () -> Hash[Symbol, untyped]
179
+ def blank_segment: () -> Hash[Symbol, untyped]
180
+
181
+ # Scans the string for ANSI sequences or individual characters
182
+ #
183
+ # @author {https://aaronmallen.me Aaron Allen}
184
+ # @since 0.3.0
185
+ #
186
+ # @api private
187
+ #
188
+ # @return [void]
189
+ # @rbs () -> void
190
+ def consume_tokens: () -> void
191
+
192
+ # Finalizes the current segment if any text is present, then resets it
193
+ #
194
+ # @author {https://aaronmallen.me Aaron Allen}
195
+ # @since 0.3.0
196
+ #
197
+ # @api private
198
+ #
199
+ # @return [void]
200
+ # @rbs () -> void
201
+ def finalize_segment_if_text!: () -> void
202
+
203
+ # Attempts to capture an ANSI sequence from the scanner If found, finalizes
204
+ # the current text segment and applies the sequence
205
+ #
206
+ # @author {https://aaronmallen.me Aaron Allen}
207
+ # @since 0.3.0
208
+ #
209
+ # @api private
210
+ #
211
+ # @return [Boolean] `true` if a sequence was found, `false` if otherwise
212
+ # @rbs () -> bool
213
+ def handle_ansi_sequence: () -> bool
214
+
215
+ # Reads a single character from the scanner and appends it to the current segment
216
+ #
217
+ # @author {https://aaronmallen.me Aaron Allen}
218
+ # @since 0.3.0
219
+ #
220
+ # @api private
221
+ #
222
+ # @return [void]
223
+ # @rbs () -> void
224
+ def handle_character: () -> void
225
+
226
+ # Parse extended color codes from the array, e.g. 38;5;160 (256-color) or 38;2;R;G;B (24-bit),
227
+ # and apply them to foreground or background
228
+ #
229
+ # @author {https://aaronmallen.me Aaron Allen}
230
+ # @since 0.3.0
231
+ #
232
+ # @api private
233
+ #
234
+ # @param codes_array [Array<Integer>] the array of codes
235
+ # @param index [Integer] the current position (where we saw 38 or 48)
236
+ #
237
+ # @return [Integer] the updated position in the codes array
238
+ # @rbs (Array[Integer] codes_array, Integer index) -> Integer
239
+ def parse_extended_color: (Array[Integer] codes_array, Integer index) -> Integer
240
+
241
+ # Resets the current segment to a fresh, blank state
242
+ #
243
+ # @author {https://aaronmallen.me Aaron Allen}
244
+ # @since 0.3.0
245
+ #
246
+ # @api private
247
+ #
248
+ # @return [void]
249
+ # @rbs () -> void
250
+ def reset_segment!: () -> void
251
+ end
252
+ end
253
+ end