sai 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sai/terminal/color_mode'
4
+
5
+ module Sai
6
+ # Determine the color capabilities of the terminal
7
+ #
8
+ # @author {https://aaronmallen.me Aaron Allen}
9
+ # @since unreleased
10
+ #
11
+ # @api public
12
+ class Support
13
+ # Initialize a new instance of Support
14
+ #
15
+ # @author {https://aaronmallen.me Aaron Allen}
16
+ # @since unreleased
17
+ #
18
+ # @api private
19
+ #
20
+ # @param color_mode [Integer] the color mode
21
+ #
22
+ # @return [Support] the new instance of support
23
+ # @rbs (Integer color_mode) -> void
24
+ def initialize(color_mode)
25
+ @color_mode = color_mode
26
+ end
27
+
28
+ # Check if the terminal supports ANSI colors (4-bit)
29
+ #
30
+ # @author {https://aaronmallen.me Aaron Allen}
31
+ # @since unreleased
32
+ #
33
+ # @api public
34
+ #
35
+ # @example Check if the terminal supports ANSI colors
36
+ # Sai.ansi? # => true
37
+ #
38
+ # @return [Boolean] `true` if the terminal supports ANSI colors (4-bit), otherwise `false`
39
+ # @rbs () -> bool
40
+ def ansi?
41
+ @color_mode >= Terminal::ColorMode::ANSI
42
+ end
43
+ alias bit4? ansi?
44
+ alias four_bit? ansi?
45
+
46
+ # Check if the terminal supports basic colors (3-bit)
47
+ #
48
+ # @author {https://aaronmallen.me Aaron Allen}
49
+ # @since unreleased
50
+ #
51
+ # @api public
52
+ #
53
+ # @example Check if the terminal supports basic colors
54
+ # Sai.basic? # => true
55
+ #
56
+ # @return [Boolean] `true` if the terminal supports basic colors (3-bit), otherwise `false`
57
+ # @rbs () -> bool
58
+ def basic?
59
+ @color_mode >= Terminal::ColorMode::BASIC
60
+ end
61
+ alias bit3? basic?
62
+ alias three_bit? basic?
63
+
64
+ # Check if the terminal supports 256 colors (8-bit)
65
+ #
66
+ # @author {https://aaronmallen.me Aaron Allen}
67
+ # @since unreleased
68
+ #
69
+ # @api public
70
+ #
71
+ # @example Check if the terminal supports 256 colors
72
+ # Sai.bit_8? # => true
73
+ #
74
+ # @return [Boolean] `true` if the terminal supports 256 colors (8-bit), otherwise `false`
75
+ # @rbs () -> bool
76
+ def bit8?
77
+ @color_mode >= Terminal::ColorMode::BIT8
78
+ end
79
+ alias eight_bit? bit8?
80
+
81
+ # Check if the terminal supports color output
82
+ #
83
+ # @author {https://aaronmallen.me Aaron Allen}
84
+ # @since unreleased
85
+ #
86
+ # @api public
87
+ #
88
+ # @example Check if the terminal supports color
89
+ # Sai.color? # => true
90
+ #
91
+ # @return [Boolean] `true` if the terminal supports color output, otherwise `false`
92
+ # @rbs () -> bool
93
+ def color?
94
+ @color_mode > Terminal::ColorMode::NO_COLOR
95
+ end
96
+
97
+ # Check if the terminal supports true color (24-bit)
98
+ #
99
+ # @author {https://aaronmallen.me Aaron Allen}
100
+ # @since unreleased
101
+ #
102
+ # @api public
103
+ #
104
+ # @example Check if the terminal supports true color
105
+ # Sai.true_color? # => true
106
+ #
107
+ # @return [Boolean] `true` if the terminal supports true color (24-bit), otherwise `false`
108
+ # @rbs () -> bool
109
+ def true_color?
110
+ @color_mode >= Terminal::ColorMode::TRUE_COLOR
111
+ end
112
+ alias bit24? true_color?
113
+ alias twenty_four_bit? true_color?
114
+ end
115
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sai/terminal/color_mode'
4
+
5
+ module Sai
6
+ module Terminal
7
+ # Detect the color capabilities of the terminal
8
+ #
9
+ # @author {https://aaronmallen.me Aaron Allen}
10
+ # @since unreleased
11
+ #
12
+ # @api private
13
+ module Capabilities
14
+ class << self
15
+ # Detect the color capabilities of the current terminal
16
+ #
17
+ # @author {https://aaronmallen.me Aaron Allen}
18
+ # @since unreleased
19
+ #
20
+ # @api private
21
+ #
22
+ # @return [Integer] the {ColorMode} of the terminal
23
+ # @rbs () -> Integer
24
+ def detect_color_support
25
+ return ColorMode::NO_COLOR if no_color?
26
+ return ColorMode::TRUE_COLOR if true_color?
27
+ return ColorMode::BIT8 if bit8?
28
+ return ColorMode::ANSI if ansi?
29
+ return ColorMode::BASIC if basic?
30
+
31
+ ColorMode::NO_COLOR
32
+ end
33
+
34
+ private
35
+
36
+ # Check for ANSI color support
37
+ #
38
+ # @author {https://aaronmallen.me Aaron Allen}
39
+ # @since unreleased
40
+ #
41
+ # @api private
42
+ #
43
+ # @return [Boolean] `true` if the terminal supports basic ANSI colors, otherwise `false`
44
+ # @rbs () -> bool
45
+ def ansi?
46
+ return true if ENV.fetch('TERM', '').match?(/^(xterm|screen|vt100|ansi)/)
47
+
48
+ !ENV.fetch('COLORTERM', '').empty?
49
+ end
50
+
51
+ # Check for basic color support
52
+ #
53
+ # @author {https://aaronmallen.me Aaron Allen}
54
+ # @since unreleased
55
+ #
56
+ # @api private
57
+ #
58
+ # @return [Boolean] `true` if the terminal supports basic colors, otherwise `false`
59
+ # @rbs () -> bool
60
+ def basic?
61
+ !ENV.fetch('TERM', '').empty?
62
+ end
63
+
64
+ # Check for 256 color (8-bit) support
65
+ #
66
+ # @author {https://aaronmallen.me Aaron Allen}
67
+ # @since unreleased
68
+ #
69
+ # @api private
70
+ #
71
+ # @return [Boolean] `true` if the terminal supports 256 colors, otherwise `false`
72
+ # @rbs () -> bool
73
+ def bit8?
74
+ return true if ENV.fetch('TERM', '').end_with?('-256color')
75
+
76
+ ENV.fetch('COLORTERM', '0').to_i >= 256
77
+ end
78
+
79
+ # Check for NO_COLOR environment variable
80
+ #
81
+ # @author {https://aaronmallen.me Aaron Allen}
82
+ # @since unreleased
83
+ #
84
+ # @api private
85
+ #
86
+ # @see https://no-color.org
87
+ #
88
+ # @return [Boolean] `true` if the NO_COLOR environment variable is set, otherwise `false`
89
+ # @rbs () -> bool
90
+ def no_color?
91
+ !ENV.fetch('NO_COLOR', '').empty? || !$stdout.tty?
92
+ end
93
+
94
+ # Check for true color (24-bit) support
95
+ #
96
+ # @author {https://aaronmallen.me Aaron Allen}
97
+ # @since unreleased
98
+ #
99
+ # @api private
100
+ #
101
+ # @return [Boolean] `true` if the terminal supports true color, otherwise `false`
102
+ # @rbs () -> bool
103
+ def true_color?
104
+ return true if ENV.fetch('COLORTERM', '').match?(/^(truecolor|24bit)$/)
105
+
106
+ case ENV.fetch('TERM', nil)
107
+ when 'xterm-direct', 'xterm-truecolor'
108
+ return true
109
+ end
110
+
111
+ case ENV.fetch('TERM_PROGRAM', nil)
112
+ when 'iTerm.app', 'WezTerm', 'vscode'
113
+ return true
114
+ end
115
+
116
+ false
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sai
4
+ module Terminal
5
+ # Represents different color support levels for terminal interfaces
6
+ #
7
+ # @author {https://aaronmallen.me Aaron Allen}
8
+ # @since unreleased
9
+ #
10
+ # @api private
11
+ module ColorMode
12
+ # The terminal does not support color output
13
+ #
14
+ # @author {https://aaronmallen.me Aaron Allen}
15
+ # @since unreleased
16
+ #
17
+ # @api private
18
+ #
19
+ # @return [Integer] the color mode
20
+ NO_COLOR = 0 #: Integer
21
+
22
+ # The terminal supports 8 colors (3-bit)
23
+ #
24
+ # @author {https://aaronmallen.me Aaron Allen}
25
+ # @since unreleased
26
+ #
27
+ # @api private
28
+ #
29
+ # @return [Integer] the color mode
30
+ BASIC = 1 #: Integer
31
+
32
+ # The terminal supports 16 colors (4-bit)
33
+ #
34
+ # @author {https://aaronmallen.me Aaron Allen}
35
+ # @since unreleased
36
+ #
37
+ # @api private
38
+ #
39
+ # @return [Integer] the color mode
40
+ ANSI = 2 #: Integer
41
+
42
+ # The terminal supports 256 colors (8-bit)
43
+ #
44
+ # @author {https://aaronmallen.me Aaron Allen}
45
+ # @since unreleased
46
+ #
47
+ # @api private
48
+ #
49
+ # @return [Integer] the color mode
50
+ BIT8 = 3 #: Integer
51
+
52
+ # The terminal supports 16 million colors (24-bit)
53
+ #
54
+ # @author {https://aaronmallen.me Aaron Allen}
55
+ # @since unreleased
56
+ #
57
+ # @api private
58
+ #
59
+ # @return [Integer] the color mode
60
+ TRUE_COLOR = 4 #: Integer
61
+ end
62
+ end
63
+ end
data/lib/sai.rb ADDED
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sai/decorator'
4
+ require 'sai/support'
5
+ require 'sai/terminal/capabilities'
6
+ require 'singleton'
7
+
8
+ # An elegant color management system for crafting sophisticated CLI applications
9
+ #
10
+ # Sai (彩) - meaning 'coloring' or 'paint' in Japanese - is a powerful and intuitive system for managing color output in
11
+ # command-line applications. Drawing inspiration from traditional Japanese artistic techniques, Sai brings vibrancy and
12
+ # harmony to terminal interfaces through its sophisticated color management
13
+ #
14
+ # Sai empowers developers to create beautiful, colorful CLI applications that maintain visual consistency across
15
+ # different terminal capabilities. Like its artistic namesake, it combines simplicity and sophistication to bring rich,
16
+ # adaptive color to your terminal interfaces
17
+ #
18
+ # When included in a class or module, Sai provides the following instance methods:
19
+ # * {#decorator} - Returns a new instance of {Decorator} for method chaining
20
+ # * {#terminal_color_support} - Returns the color support capabilities of the current terminal
21
+ #
22
+ # The Sai module itself responds to all the same methods as {Decorator}, excluding methods used for applying
23
+ # decorations (apply, call, decorate, encode). These methods are directly delegated to a new {Decorator} instance
24
+ #
25
+ # @author {https://aaronmallen.me Aaron Allen}
26
+ # @since unreleased
27
+ #
28
+ # @api public
29
+ #
30
+ # @example Using Sai as a module
31
+ # class MyClass
32
+ # include Sai
33
+ # end
34
+ #
35
+ # my_class = MyClass.new
36
+ # my_class.decorator.red.on_blue.bold.decorate('Hello, World!')
37
+ # #=> "\e[38;2;205;0;0m\e[48;2;0;0;238m\e[1mHello, World!\e[0m"
38
+ #
39
+ # my_class.terminal_color_support.true_color? # => true
40
+ #
41
+ # @example Using Sai directly
42
+ # Sai.red.on_blue.bold.decorate('Hello, World!')
43
+ # #=> "\e[38;2;205;0;0m\e[48;2;0;0;238m\e[1mHello, World!\e[0m"
44
+ #
45
+ # Sai.support.true_color? # => true
46
+ module Sai
47
+ class << self
48
+ ignored_decorator_methods = %i[apply call decorate encode]
49
+ Decorator.instance_methods(false).reject { |m| ignored_decorator_methods.include?(m) }.each do |method|
50
+ define_method(method) do |*arguments, **keyword_arguments|
51
+ Decorator.new(send(:color_mode)).public_send(method, *arguments, **keyword_arguments)
52
+ end
53
+ end
54
+
55
+ # @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
104
+
105
+ # The supported color modes for the terminal
106
+ #
107
+ # @author {https://aaronmallen.me Aaron Allen}
108
+ # @since unreleased
109
+ #
110
+ # @api public
111
+ #
112
+ # @example Check the color support of the terminal
113
+ # Sai.support.ansi? # => true
114
+ # Sai.support.basic? # => true
115
+ # Sai.support.bit8? # => true
116
+ # Sai.support.no_color? # => false
117
+ # Sai.support.true_color? # => true
118
+ #
119
+ # @return [Support] the color support
120
+ # @rbs () -> Support
121
+ def support
122
+ @support ||= Support.new(color_mode).freeze
123
+ end
124
+
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
139
+ end
140
+
141
+ # A helper method to initialize an instance of {Decorator}
142
+ #
143
+ # @author {https://aaronmallen.me Aaron Allen}
144
+ # @since unreleased
145
+ #
146
+ # @api public
147
+ #
148
+ # @example Initialize a new instance of {Decorator}
149
+ # class MyClass
150
+ # include Sai
151
+ # end
152
+ #
153
+ # MyClass.new.decorator.blue.on_red.bold.decorate('Hello, world!')
154
+ # #=> "\e[38;5;21m\e[48;5;160m\e[1mHello, world!\e[0m"
155
+ #
156
+ # @return [Decorator] the Decorator instance
157
+ # @rbs () -> Decorator
158
+ def decorator
159
+ Decorator.new(Terminal::Capabilities.detect_color_support)
160
+ end
161
+
162
+ # The supported color modes for the terminal
163
+ #
164
+ # @author {https://aaronmallen.me Aaron Allen}
165
+ # @since unreleased
166
+ #
167
+ # @api public
168
+ #
169
+ # @example Check the color support of the terminal
170
+ # class MyClass
171
+ # include Sai
172
+ # end
173
+ #
174
+ # MyClass.new.terminal_color_support.ansi? # => true
175
+ # MyClass.new.terminal_color_support.basic? # => true
176
+ # MyClass.new.terminal_color_support.bit8? # => true
177
+ # MyClass.new.terminal_color_support.no_color? # => false
178
+ # MyClass.new.terminal_color_support.true_color? # => true
179
+ #
180
+ # @return [Support] the color support
181
+ # @rbs () -> Support
182
+ def terminal_color_support
183
+ Sai.support
184
+ end
185
+ end
data/sig/sai/ansi.rbs ADDED
@@ -0,0 +1,51 @@
1
+ # Generated from lib/sai/ansi.rb with RBS::Inline
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: untyped
20
+
21
+ # Standard ANSI color names and their RGB values
22
+ #
23
+ # @author {https://aaronmallen.me Aaron Allen}
24
+ # @since unreleased
25
+ #
26
+ # @api private
27
+ #
28
+ # @return [Hash{Symbol => Array<Integer>}] the color names and RGB values
29
+ COLOR_NAMES: untyped
30
+
31
+ # ANSI escape sequence for resetting text formatting
32
+ #
33
+ # @author {https://aaronmallen.me Aaron Allen}
34
+ # @since unreleased
35
+ #
36
+ # @api private
37
+ #
38
+ # @return [String] the ANSI escape sequence
39
+ RESET: ::String
40
+
41
+ # Standard ANSI style codes
42
+ #
43
+ # @author {https://aaronmallen.me Aaron Allen}
44
+ # @since unreleased
45
+ #
46
+ # @api private
47
+ #
48
+ # @return [Hash{Symbol => Integer}] the style codes
49
+ STYLES: untyped
50
+ end
51
+ end
@@ -0,0 +1,114 @@
1
+ # Generated from lib/sai/conversion/color_sequence.rb with RBS::Inline
2
+
3
+ module Sai
4
+ module Conversion
5
+ # ANSI escape sequence utilities
6
+ #
7
+ # @author {https://aaronmallen.me Aaron Allen}
8
+ # @since unreleased
9
+ #
10
+ # @api private
11
+ module ColorSequence
12
+ type style_type = :foreground | :background
13
+
14
+ # Convert a color to the appropriate ANSI escape sequence
15
+ #
16
+ # @author {https://aaronmallen.me Aaron Allen}
17
+ # @since unreleased
18
+ #
19
+ # @api private
20
+ #
21
+ # @param color [String, Array<Integer>] the color to convert
22
+ # @param mode [Integer] the terminal color mode
23
+ # @param style_type [Symbol] the type of color (foreground or background)
24
+ #
25
+ # @return [String] the ANSI escape sequence
26
+ # @rbs (Array[Integer] | String | Symbol color, Integer mode, ?style_type style_type) -> String
27
+ def self.resolve: (Array[Integer] | String | Symbol color, Integer mode, ?style_type style_type) -> String
28
+
29
+ # Convert RGB values to a 4-bit ANSI color sequence
30
+ #
31
+ # @author {https://aaronmallen.me Aaron Allen}
32
+ # @since unreleased
33
+ #
34
+ # @api private
35
+ #
36
+ # @param rgb [Array<Integer>] the RGB components
37
+ # @param style_type [Symbol] the type of color (foreground or background)
38
+ #
39
+ # @return [String] the ANSI escape sequence
40
+ # @rbs (Array[Integer] rgb, style_type style_type) -> String
41
+ private def self.ansi: (Array[Integer] rgb, style_type style_type) -> String
42
+
43
+ # Convert a base color to a foreground or background sequence
44
+ #
45
+ # @author {https://aaronmallen.me Aaron Allen}
46
+ # @since unreleased
47
+ #
48
+ # @api private
49
+ #
50
+ # @param base_code [Integer] the base color code
51
+ # @param style_type [Symbol] the type of color (foreground or background)
52
+ #
53
+ # @return [Integer] the code for the color sequence
54
+ # @rbs (Integer base_code, style_type style_type) -> Integer
55
+ private def self.base_color_for_style_type: (Integer base_code, style_type style_type) -> Integer
56
+
57
+ # Convert RGB values to a 3-bit basic color sequence
58
+ #
59
+ # @author {https://aaronmallen.me Aaron Allen}
60
+ # @since unreleased
61
+ #
62
+ # @api private
63
+ #
64
+ # @param rgb [Array<Integer>] the RGB components
65
+ # @param style_type [Symbol] the type of color (foreground or background)
66
+ #
67
+ # @return [String] the ANSI escape sequence
68
+ # @rbs (Array[Integer] rgb, style_type style_type) -> String
69
+ private def self.basic: (Array[Integer] rgb, style_type style_type) -> String
70
+
71
+ # Convert RGB values to an 8-bit color sequence
72
+ #
73
+ # @author {https://aaronmallen.me Aaron Allen}
74
+ # @since unreleased
75
+ #
76
+ # @api private
77
+ #
78
+ # @param rgb [Array<Integer>] the RGB components
79
+ # @param style_type [Symbol] the type of color (foreground or background)
80
+ #
81
+ # @return [String] the ANSI escape sequence
82
+ # @rbs (Array[Integer] rgb, style_type type) -> String
83
+ private def self.bit8: (Array[Integer] rgb, style_type type) -> String
84
+
85
+ # Convert RGB values to a true color (24-bit) sequence
86
+ #
87
+ # @author {https://aaronmallen.me Aaron Allen}
88
+ # @since unreleased
89
+ #
90
+ # @api private
91
+ #
92
+ # @param rgb [Array<Integer>] the RGB components
93
+ # @param style_type [Symbol] the type of color (foreground or background)
94
+ #
95
+ # @return [String] the ANSI escape sequence
96
+ # @rbs (Array[Integer] rgb, style_type type) -> String
97
+ private def self.true_color: (Array[Integer] rgb, style_type type) -> String
98
+
99
+ # Validate a color style type
100
+ #
101
+ # @author {https://aaronmallen.me Aaron Allen}
102
+ # @since unreleased
103
+ #
104
+ # @api private
105
+ #
106
+ # @param style_type [Symbol] the style type to validate
107
+ #
108
+ # @raise [ArgumentError] if the style type is invalid
109
+ # @return [Symbol] the validated style type
110
+ # @rbs (Symbol style_type) -> Symbol
111
+ private def self.validate_style_type: (Symbol style_type) -> Symbol
112
+ end
113
+ end
114
+ end