sai 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1856d33db7e7b71a3ad164df38fc64a732e806540584e5e207ed6ee6220d0e07
4
+ data.tar.gz: f2aa1b1182b297dcdd14c97b01928e33b56e2bee79268e3f344ea4df315a338f
5
+ SHA512:
6
+ metadata.gz: 318ead79460d995f10d20a031d8d9be16277bd5180d5663f32ba7a0f1aac3dbdfa5f7e7fba6c71c62662cbf9486db35b7cb95e6562ff3df9860cd837f2ca9125
7
+ data.tar.gz: a82a2ce509d1d6159cbac457d788e48bf88bb07498449beffedbf1fb10aa08a2e614c375f0af3207a22b4b6b32fe0b595137e6b36276f011e11489a786ec51af
data/.yardopts ADDED
@@ -0,0 +1,11 @@
1
+ lib/**/*.rb
2
+ --title Sai
3
+ --readme README.md
4
+ --no-private
5
+ --protected
6
+ --markup markdown
7
+ --markup-provider redcarpet
8
+ --embed-mixins
9
+ --tag rbs
10
+ --hide-tag rbs
11
+ --files LICENSE
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog], and this project adheres to [Break Versioning].
6
+
7
+ ## [Unreleased]
8
+
9
+ ## 0.1.0 - 2025-01-19
10
+
11
+ * Initial release
12
+
13
+ [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
14
+ [Break Versioning]: https://www.taoensso.com/break-versioning
15
+
16
+ <!-- versions -->
17
+
18
+ [Unreleased]: https://github.com/aaronmallen/sai/compare/0.1.0..HEAD
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Aaron Allen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # Sai
2
+
3
+ [![Sai Version](https://img.shields.io/gem/v/sai?style=for-the-badge&logo=rubygems&logoColor=white&logoSize=auto&label=Gem%20Version)](https://rubygems.org/gems/sai)
4
+ [![Sai License](https://img.shields.io/github/license/aaronmallen/sai?style=for-the-badge&logo=opensourceinitiative&logoColor=white&logoSize=auto)](./LICENSE)
5
+ [![Sai Docs](https://img.shields.io/badge/rubydoc-blue?style=for-the-badge&logo=readthedocs&logoColor=white&logoSize=auto&label=docs)](https://rubydoc.info/gems/sai/0.1.0)
6
+ [![Sai Open Issues](https://img.shields.io/github/issues-search/aaronmallen/sai?query=state%3Aopen&style=for-the-badge&logo=github&logoColor=white&logoSize=auto&label=issues&color=red)](https://github.com/aaronmallen/sai/issues?q=state%3Aopen%20)
7
+
8
+ An elegant color management system for crafting sophisticated CLI applications
9
+
10
+ ```ruby
11
+ puts Sai.rgb(255, 128, 0).bold.decorate('Create beautiful CLI applications')
12
+ puts Sai.hex('#4834d4').italic.decorate('with intuitive color management')
13
+ puts Sai.bright_cyan.on_blue.underline.decorate('that adapts to any terminal')
14
+ ```
15
+
16
+ Sai (彩) - meaning 'coloring' or 'paint' in Japanese - is a powerful and intuitive system for managing color output in
17
+ command-line applications. Drawing inspiration from traditional Japanese artistic techniques, Sai brings vibrancy and
18
+ harmony to terminal interfaces through its sophisticated color management.
19
+
20
+ Sai empowers developers to create beautiful, colorful CLI applications that maintain visual consistency across different
21
+ terminal capabilities. Like its artistic namesake, it combines simplicity and sophistication to bring rich, adaptive
22
+ color to your terminal interfaces.
23
+
24
+ ## Features
25
+
26
+ * Automatic color mode detection and downgrading
27
+ * Support for True Color (24-bit), 256 colors (8-bit), ANSI colors (4-bit), and basic colors (3-bit)
28
+ * Rich set of ANSI text styles
29
+ * Named color support with bright variants
30
+ * RGB and Hex color support
31
+ * Foreground and background colors
32
+ * Respects NO_COLOR environment variable
33
+ * Can be used directly or included in classes/modules
34
+
35
+ ## Installation
36
+
37
+ Add this line to your application's Gemfile:
38
+
39
+ ```ruby
40
+ gem 'sai'
41
+ ```
42
+
43
+ Or install it yourself as:
44
+
45
+ ```bash
46
+ gem install sai
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ Sai can be used directly or included in your own classes and modules.
52
+
53
+ ### Direct Usage
54
+
55
+ ```ruby
56
+ require 'sai'
57
+
58
+ # Using named colors
59
+ puts Sai.red.decorate('Error!')
60
+ puts Sai.bright_blue.on_white.decorate('Info')
61
+
62
+ # Using RGB colors
63
+ puts Sai.rgb(255, 128, 0).decorate('Custom color')
64
+ puts Sai.on_rgb(0, 255, 128).decorate('Custom background')
65
+
66
+ # Using hex colors
67
+ puts Sai.hex('#FF8000').decorate('Hex color')
68
+ puts Sai.on_hex('#00FF80').decorate('Hex background')
69
+
70
+ # Applying styles
71
+ puts Sai.bold.underline.decorate('Important')
72
+ puts Sai.red.bold.italic.decorate('Error!')
73
+
74
+ # Complex combinations
75
+ puts Sai.bright_cyan
76
+ .on_blue
77
+ .bold
78
+ .italic
79
+ .decorate('Styled text')
80
+ ```
81
+
82
+ ### Module Inclusion
83
+
84
+ ```ruby
85
+ class CLI
86
+ include Sai
87
+
88
+ def error(message)
89
+ puts decorator.red.bold.decorate(message)
90
+ end
91
+
92
+ def info(message)
93
+ puts decorator.bright_blue.decorate(message)
94
+ end
95
+
96
+ def success(message)
97
+ puts decorator.green.decorate(message)
98
+ end
99
+ end
100
+
101
+ cli = CLI.new
102
+ cli.error('Something went wrong!')
103
+ cli.info('Processing...')
104
+ cli.success('Done!')
105
+ ```
106
+
107
+ ### Defining Reusable Styles
108
+
109
+ Sai decorators can be assigned to constants to create reusable styles throughout your application:
110
+
111
+ ```ruby
112
+ module Style
113
+ ERROR = Sai.bold.bright_white.on_red
114
+ WARNING = Sai.black.on_yellow
115
+ SUCCESS = Sai.bright_green
116
+ INFO = Sai.bright_blue
117
+ HEADER = Sai.bold.bright_cyan.underline
118
+ end
119
+
120
+ # Use your defined styles
121
+ puts Style::ERROR.decorate('Something went wrong!')
122
+ puts Style::SUCCESS.decorate('Operation completed successfully')
123
+
124
+ # Styles can be further customized when needed
125
+ puts Style::ERROR.italic.decorate('Critical error!')
126
+ ```
127
+
128
+ > [!TIP]
129
+ > This pattern is particularly useful for maintaining consistent styling across your application and creating theme
130
+ > systems. You can even build on existing styles:
131
+ >
132
+ > ```ruby
133
+ > module Theme
134
+ > PRIMARY = Sai.rgb(63, 81, 181)
135
+ > SECONDARY = Sai.rgb(255, 87, 34)
136
+ >
137
+ > BUTTON = PRIMARY.bold
138
+ > LINK = SECONDARY.underline
139
+ > HEADER = PRIMARY.bold.underline
140
+ > end
141
+ > ```
142
+
143
+ ## Features
144
+
145
+ ### Color Support
146
+
147
+ Sai supports up to 16.7 million colors (true color/24-bit) depending on your terminal's capabilities. Colors can be
148
+ specified in several ways:
149
+
150
+ #### RGB Colors
151
+
152
+ ```ruby
153
+ # Specify any RGB color (0-255 per channel)
154
+ Sai.rgb(255, 128, 0).decorate('Orange text')
155
+ Sai.on_rgb(0, 255, 128).decorate('Custom green background')
156
+ ```
157
+
158
+ #### Hex Colors
159
+
160
+ ```ruby
161
+ # Use any hex color code
162
+ Sai.hex('#FF8000').decorate('Orange text')
163
+ Sai.on_hex('#00FF80').decorate('Custom green background')
164
+ ```
165
+
166
+ #### Named Color Shortcuts
167
+
168
+ For convenience, Sai provides shortcuts for common ANSI colors:
169
+
170
+ Standard colors:
171
+
172
+ ```ruby
173
+ Sai.red.decorate('Red text')
174
+ Sai.blue.decorate('Blue text')
175
+ Sai.on_green.decorate('Green background')
176
+ ```
177
+
178
+ Bright variants:
179
+
180
+ ```ruby
181
+ Sai.bright_red.decorate('Bright red text')
182
+ Sai.bright_blue.decorate('Bright blue text')
183
+ Sai.on_bright_green.decorate('Bright green background')
184
+ ```
185
+
186
+ Available named colors:
187
+
188
+ * black/bright_black
189
+ * red/bright_red
190
+ * green/bright_green
191
+ * yellow/bright_yellow
192
+ * blue/bright_blue
193
+ * magenta/bright_magenta
194
+ * cyan/bright_cyan
195
+ * white/bright_white
196
+
197
+ > [!TIP]
198
+ > While named colors provide convenient shortcuts, remember that Sai supports the full RGB color space. Don't feel
199
+ > limited to just these predefined colors!
200
+
201
+ ### Text Styles
202
+
203
+ Available styles:
204
+
205
+ * bold
206
+ * dim
207
+ * italic
208
+ * underline
209
+ * blink
210
+ * rapid_blink
211
+ * reverse
212
+ * conceal
213
+ * strike
214
+
215
+ Style removal:
216
+
217
+ * no_blink
218
+ * no_italic
219
+ * no_underline
220
+ * no_reverse
221
+ * no_conceal
222
+ * no_strike
223
+ * normal_intensity
224
+
225
+ ### Color Mode Detection & Downgrading
226
+
227
+ Sai automatically detects your terminal's color capabilities and adapts the output appropriately:
228
+
229
+ * True Color terminals (24-bit): Full access to all 16.7 million colors
230
+ * 256-color terminals (8-bit): Colors are mapped to the closest available color in the 256-color palette
231
+ * ANSI terminals (4-bit): Colors are mapped to the 16 standard ANSI colors
232
+ * Basic terminals (3-bit): Colors are mapped to the 8 basic ANSI colors
233
+ * NO_COLOR: All color sequences are stripped when the NO_COLOR environment variable is set
234
+
235
+ > [!NOTE]
236
+ > This automatic downgrading ensures your application looks great across all terminal types without any extra code!
237
+
238
+ ### Terminal Support Detection
239
+
240
+ You can check the terminal's capabilities:
241
+
242
+ ```ruby
243
+ # Using directly
244
+ Sai.support.true_color? # => true/false
245
+ Sai.support.bit8? # => true/false
246
+ Sai.support.ansi? # => true/false
247
+ Sai.support.basic? # => true/false
248
+ Sai.support.color? # => true/false
249
+
250
+ # Using included module
251
+ class CLI
252
+ include Sai
253
+
254
+ def check_support
255
+ if terminal_color_support.true_color?
256
+ puts "Terminal supports true color!"
257
+ end
258
+ end
259
+ end
260
+ ```
261
+
262
+ ## Contributing
263
+
264
+ We welcome contributions! Please see our [Contributing Guidelines](docs/CONTRIBUTING.md) for:
265
+
266
+ * Development setup and workflow
267
+ * Code style and documentation standards
268
+ * Testing requirements
269
+ * Pull request process
270
+
271
+ Before contributing, please review our [Code of Conduct](docs/CODE_OF_CONDUCT.md).
272
+
273
+ ## License
274
+
275
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
data/lib/sai/ansi.rb ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sai
4
+ # ANSI constants for encoding text styles and colors
5
+ #
6
+ # @author {https://aaronmallen.me Aaron Allen}
7
+ # @since unreleased
8
+ #
9
+ # @api private
10
+ module ANSI
11
+ # ANSI color code mappings
12
+ #
13
+ # @author {https://aaronmallen.me Aaron Allen}
14
+ # @since unreleased
15
+ #
16
+ # @api private
17
+ #
18
+ # @return [Hash{Symbol => Integer}] the color codes
19
+ COLOR_CODES = {
20
+ black: 0,
21
+ red: 1,
22
+ green: 2,
23
+ yellow: 3,
24
+ blue: 4,
25
+ magenta: 5,
26
+ cyan: 6,
27
+ white: 7
28
+ }.freeze # Hash[Symbol, Integer]
29
+
30
+ # Standard ANSI color names and their RGB values
31
+ #
32
+ # @author {https://aaronmallen.me Aaron Allen}
33
+ # @since unreleased
34
+ #
35
+ # @api private
36
+ #
37
+ # @return [Hash{Symbol => Array<Integer>}] the color names and RGB values
38
+ COLOR_NAMES = {
39
+ black: [0, 0, 0],
40
+ red: [205, 0, 0],
41
+ green: [0, 205, 0],
42
+ yellow: [205, 205, 0],
43
+ blue: [0, 0, 238],
44
+ magenta: [205, 0, 205],
45
+ cyan: [0, 205, 205],
46
+ white: [229, 229, 229],
47
+ bright_black: [127, 127, 127],
48
+ bright_red: [255, 0, 0],
49
+ bright_green: [0, 255, 0],
50
+ bright_yellow: [255, 255, 0],
51
+ bright_blue: [92, 92, 255],
52
+ bright_magenta: [255, 0, 255],
53
+ bright_cyan: [0, 255, 255],
54
+ bright_white: [255, 255, 255]
55
+ }.freeze # Hash[Symbol, Array[Integer]]
56
+
57
+ # ANSI escape sequence for resetting text formatting
58
+ #
59
+ # @author {https://aaronmallen.me Aaron Allen}
60
+ # @since unreleased
61
+ #
62
+ # @api private
63
+ #
64
+ # @return [String] the ANSI escape sequence
65
+ RESET = "\e[0m" # String
66
+
67
+ # Standard ANSI style codes
68
+ #
69
+ # @author {https://aaronmallen.me Aaron Allen}
70
+ # @since unreleased
71
+ #
72
+ # @api private
73
+ #
74
+ # @return [Hash{Symbol => Integer}] the style codes
75
+ STYLES = {
76
+ bold: 1,
77
+ dim: 2,
78
+ italic: 3,
79
+ underline: 4,
80
+ blink: 5,
81
+ rapid_blink: 6,
82
+ reverse: 7,
83
+ conceal: 8,
84
+ strike: 9,
85
+ normal_intensity: 22,
86
+ no_italic: 23,
87
+ no_underline: 24,
88
+ no_blink: 25,
89
+ no_reverse: 27,
90
+ no_conceal: 28,
91
+ no_strike: 29
92
+ }.freeze # Hash[Symbol, Integer]
93
+ end
94
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sai/ansi'
4
+ require 'sai/conversion/rgb'
5
+ require 'sai/terminal/color_mode'
6
+
7
+ module Sai
8
+ module Conversion
9
+ # ANSI escape sequence utilities
10
+ #
11
+ # @author {https://aaronmallen.me Aaron Allen}
12
+ # @since unreleased
13
+ #
14
+ # @api private
15
+ module ColorSequence
16
+ # @rbs!
17
+ # type style_type = :foreground | :background
18
+
19
+ class << self
20
+ # Convert a color to the appropriate ANSI escape sequence
21
+ #
22
+ # @author {https://aaronmallen.me Aaron Allen}
23
+ # @since unreleased
24
+ #
25
+ # @api private
26
+ #
27
+ # @param color [String, Array<Integer>] the color to convert
28
+ # @param mode [Integer] the terminal color mode
29
+ # @param style_type [Symbol] the type of color (foreground or background)
30
+ #
31
+ # @return [String] the ANSI escape sequence
32
+ # @rbs (Array[Integer] | String | Symbol color, Integer mode, ?style_type style_type) -> String
33
+ def resolve(color, mode, style_type = :foreground)
34
+ rgb = RGB.resolve(color)
35
+ style_type = validate_style_type(style_type) #: style_type
36
+
37
+ case mode
38
+ when Terminal::ColorMode::TRUE_COLOR then true_color(rgb, style_type)
39
+ when Terminal::ColorMode::BIT8 then bit8(rgb, style_type)
40
+ when Terminal::ColorMode::ANSI then ansi(rgb, style_type)
41
+ when Terminal::ColorMode::BASIC then basic(rgb, style_type)
42
+ else
43
+ ''
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Convert RGB values to a 4-bit ANSI color sequence
50
+ #
51
+ # @author {https://aaronmallen.me Aaron Allen}
52
+ # @since unreleased
53
+ #
54
+ # @api private
55
+ #
56
+ # @param rgb [Array<Integer>] the RGB components
57
+ # @param style_type [Symbol] the type of color (foreground or background)
58
+ #
59
+ # @return [String] the ANSI escape sequence
60
+ # @rbs (Array[Integer] rgb, style_type style_type) -> String
61
+ def ansi(rgb, style_type)
62
+ r, g, b = rgb.map { |c| c / 255.0 } #: [Float, Float, Float]
63
+ brightness = (r + g + b) / 3.0
64
+ is_bright = brightness > 0.5
65
+
66
+ color = RGB.closest_ansi_color(r, g, b)
67
+ code = base_color_for_style_type(ANSI::COLOR_CODES[color], style_type)
68
+ code += 60 if is_bright
69
+ "\e[#{code}m"
70
+ end
71
+
72
+ # Convert a base color to a foreground or background sequence
73
+ #
74
+ # @author {https://aaronmallen.me Aaron Allen}
75
+ # @since unreleased
76
+ #
77
+ # @api private
78
+ #
79
+ # @param base_code [Integer] the base color code
80
+ # @param style_type [Symbol] the type of color (foreground or background)
81
+ #
82
+ # @return [Integer] the code for the color sequence
83
+ # @rbs (Integer base_code, style_type style_type) -> Integer
84
+ def base_color_for_style_type(base_code, style_type)
85
+ style_type == :background ? base_code + 40 : base_code + 30
86
+ end
87
+
88
+ # Convert RGB values to a 3-bit basic color sequence
89
+ #
90
+ # @author {https://aaronmallen.me Aaron Allen}
91
+ # @since unreleased
92
+ #
93
+ # @api private
94
+ #
95
+ # @param rgb [Array<Integer>] the RGB components
96
+ # @param style_type [Symbol] the type of color (foreground or background)
97
+ #
98
+ # @return [String] the ANSI escape sequence
99
+ # @rbs (Array[Integer] rgb, style_type style_type) -> String
100
+ def basic(rgb, style_type)
101
+ r, g, b = rgb.map { |c| c / 255.0 } #: [Float, Float, Float]
102
+ color = RGB.closest_ansi_color(r, g, b)
103
+ code = base_color_for_style_type(ANSI::COLOR_CODES[color], style_type)
104
+ "\e[#{code}m"
105
+ end
106
+
107
+ # Convert RGB values to an 8-bit color sequence
108
+ #
109
+ # @author {https://aaronmallen.me Aaron Allen}
110
+ # @since unreleased
111
+ #
112
+ # @api private
113
+ #
114
+ # @param rgb [Array<Integer>] the RGB components
115
+ # @param style_type [Symbol] the type of color (foreground or background)
116
+ #
117
+ # @return [String] the ANSI escape sequence
118
+ # @rbs (Array[Integer] rgb, style_type type) -> String
119
+ def bit8(rgb, style_type)
120
+ code = style_type == :background ? 48 : 38
121
+ color_code = if rgb.uniq.size == 1
122
+ RGB.to_grayscale_index(rgb)
123
+ else
124
+ RGB.to_color_cube_index(rgb)
125
+ end
126
+
127
+ "\e[#{code};5;#{color_code}m"
128
+ end
129
+
130
+ # Convert RGB values to a true color (24-bit) sequence
131
+ #
132
+ # @author {https://aaronmallen.me Aaron Allen}
133
+ # @since unreleased
134
+ #
135
+ # @api private
136
+ #
137
+ # @param rgb [Array<Integer>] the RGB components
138
+ # @param style_type [Symbol] the type of color (foreground or background)
139
+ #
140
+ # @return [String] the ANSI escape sequence
141
+ # @rbs (Array[Integer] rgb, style_type type) -> String
142
+ def true_color(rgb, style_type)
143
+ code = style_type == :background ? 48 : 38
144
+ "\e[#{code};2;#{rgb.join(';')}m"
145
+ end
146
+
147
+ # Validate a color style type
148
+ #
149
+ # @author {https://aaronmallen.me Aaron Allen}
150
+ # @since unreleased
151
+ #
152
+ # @api private
153
+ #
154
+ # @param style_type [Symbol] the style type to validate
155
+ #
156
+ # @raise [ArgumentError] if the style type is invalid
157
+ # @return [Symbol] the validated style type
158
+ # @rbs (Symbol style_type) -> Symbol
159
+ def validate_style_type(style_type)
160
+ raise ArgumentError, "Invalid style type: #{style_type}" unless %i[foreground background].include?(style_type)
161
+
162
+ style_type
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end