sai 0.1.0 → 0.3.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.
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