tint_me 1.0.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop_todo.yml +7 -0
  3. data/.serena/project.yml +68 -0
  4. data/.simplecov +24 -0
  5. data/.yardopts +6 -0
  6. data/AGENTS.md +60 -0
  7. data/CHANGELOG.md +29 -0
  8. data/CLAUDE.md +1 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +175 -0
  11. data/RELEASING.md +202 -0
  12. data/Rakefile +18 -0
  13. data/benchmark/2025-09-08-style-caching-optimization/01_baseline_results.txt +39 -0
  14. data/benchmark/2025-09-08-style-caching-optimization/02_with_full_caching_results.txt +54 -0
  15. data/benchmark/2025-09-08-style-caching-optimization/03_prefix_only_caching_results.txt +107 -0
  16. data/benchmark/2025-09-08-style-caching-optimization/04_baseline_vs_optimized_analysis.txt +65 -0
  17. data/benchmark/2025-09-08-style-caching-optimization/05_caching_approaches_comparison.txt +59 -0
  18. data/benchmark/2025-09-08-style-caching-optimization/06_append_operator_results.txt +107 -0
  19. data/benchmark/2025-09-08-style-caching-optimization/07_string_concatenation_comparison.txt +66 -0
  20. data/benchmark/2025-09-08-style-caching-optimization/08_with_freeze_optimization_results.txt +107 -0
  21. data/benchmark/2025-09-08-style-caching-optimization/09_freeze_optimization_analysis.txt +49 -0
  22. data/benchmark/2025-09-08-style-caching-optimization/10_constant_access_results.txt +107 -0
  23. data/benchmark/2025-09-08-style-caching-optimization/11_constant_vs_cache_analysis.txt +74 -0
  24. data/benchmark/2025-09-08-style-caching-optimization/12_empty_prefix_analysis.txt +81 -0
  25. data/benchmark/2025-09-08-style-caching-optimization/13_nil_check_optimization_results.txt +107 -0
  26. data/benchmark/2025-09-08-style-caching-optimization/14_nil_vs_empty_check_analysis.txt +81 -0
  27. data/benchmark/2025-09-08-style-caching-optimization/README.md +45 -0
  28. data/benchmark/2025-09-08-style-caching-optimization/benchmark_script.rb +180 -0
  29. data/docs/agents/git-pr.md +298 -0
  30. data/docs/agents/languages.md +388 -0
  31. data/docs/agents/rubocop.md +55 -0
  32. data/lib/tint_me/error.rb +6 -0
  33. data/lib/tint_me/sgr_builder.rb +259 -0
  34. data/lib/tint_me/style/schema.rb +22 -0
  35. data/lib/tint_me/style/types.rb +50 -0
  36. data/lib/tint_me/style.rb +286 -0
  37. data/lib/tint_me/version.rb +8 -0
  38. data/lib/tint_me.rb +62 -0
  39. data/mise.toml +5 -0
  40. data/sig/tint_me.rbs +61 -0
  41. metadata +131 -0
@@ -0,0 +1,55 @@
1
+ # RuboCop Fix Workflow for AI Agents
2
+
3
+ This guide outlines a safe, repeatable process to fix RuboCop offenses while preserving behavior and keeping PRs focused.
4
+
5
+ ## Goals
6
+ - Keep changes minimal and behavior‑preserving; prefer small, focused PRs.
7
+ - Use safe autocorrect first; apply unsafe corrections only when reviewed and covered by tests.
8
+ - Centralize rule changes in `.rubocop.yml`; never hand‑edit `.rubocop_todo.yml`.
9
+
10
+ ## Quick Commands
11
+ - Full lint: `rake rubocop`
12
+ - File/dir lint: `rubocop path/to/file.rb`
13
+ - Safe autocorrect: `rubocop -a`
14
+ - Targeted unsafe: `rubocop -A --only Cop/Name`
15
+ - Run tests: `rake spec`
16
+ - Regenerate TODO: `docquet regenerate-todo`
17
+ - Temporarily enable a TODO‑disabled cop: comment out its block in `.rubocop_todo.yml`
18
+
19
+ ## Workflow
20
+ 1) Reproduce locally
21
+ - Run `rake rubocop` and note failing cops/files.
22
+
23
+ 2) Safe autocorrect first
24
+ - Run `rubocop -a` on specific files or directories to reduce blast radius.
25
+ - Re‑run `rake rubocop` and `rake spec` to verify no behavior changes.
26
+
27
+ 3) Target specific cops (if still failing)
28
+ - For stylistic issues, fix manually or run `rubocop -A --only Cop/Name` on the smallest scope.
29
+ - For logic‑sensitive cops (e.g., Performance, Lint), prefer manual fixes with tests.
30
+
31
+ 3a) If the cop is disabled in `.rubocop_todo.yml`
32
+ - Open `.rubocop_todo.yml`, locate the `Cop/Name` entry, and temporarily comment out (or remove locally) that entire block. RuboCop respects TODO disables even with `--only`, so this is required to surface offenses.
33
+ - Run `rubocop --only Cop/Name` (optionally `-A`) on a narrow path and fix findings.
34
+ - Validate with `rake rubocop` and `rake spec`.
35
+ - Refresh the TODO: run `docquet regenerate-todo` to update/remove the entry based on remaining offenses. Commit code changes and the regenerated `.rubocop_todo.yml` together in the same commit/PR.
36
+
37
+ 4) Update configuration (rare)
38
+ - If a rule conflicts with project style, adjust `.rubocop.yml` with rationale in the PR body.
39
+ - Do not edit `.rubocop_todo.yml` manually. After large cleanups, run `docquet regenerate-todo` and commit only that file.
40
+
41
+ 5) No inline disables
42
+ - Do not use comment-based disables (`# rubocop:disable ...`). This project forbids inline disables.
43
+ - If a finding cannot be safely fixed, pause and ask the maintainer for guidance. Options may include refining the rule in `.rubocop.yml` or adjusting the approach.
44
+
45
+ 6) Validate and commit
46
+ - Ensure `rake` (tests + lint) is green.
47
+ - Commit messages must start with a GitHub `:emoji:` and use imperative mood.
48
+ Examples:
49
+ - `:lipstick: RuboCop: safe autocorrect in lib/tint_me/style.rb`
50
+ - `:rotating_light: RuboCop: fix Lint/UnusedMethodArgument`
51
+
52
+ ## PR Guidance
53
+ - Keep changes small and single‑purpose (e.g., “fix Style/StringLiterals in lib/tint_me”).
54
+ - Include before/after snippets if unsafe autocorrect or refactor was applied.
55
+ - Link any rule changes to rationale and project conventions.
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIntMe
4
+ # Base error class for TIntMe
5
+ class Error < StandardError; end
6
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIntMe
4
+ # Builds ANSI SGR (Select Graphic Rendition) escape sequences for terminal styling
5
+ #
6
+ # This class is responsible for generating the correct ANSI escape sequences
7
+ # for colors and text effects. It supports standard colors, bright colors,
8
+ # RGB colors, and various text decorations.
9
+ #
10
+ # @api private
11
+ class SGRBuilder
12
+ # ANSI escape sequence components
13
+ ESC = "\e"
14
+ public_constant :ESC
15
+
16
+ CSI = "#{ESC}[".freeze # Control Sequence Introducer
17
+ public_constant :CSI
18
+
19
+ SGR_END = "m"
20
+ public_constant :SGR_END
21
+
22
+ RESET_CODE = "#{CSI}0#{SGR_END}".freeze
23
+ public_constant :RESET_CODE
24
+
25
+ # Standard colors + bright colors
26
+ COLORS = {
27
+ black: 30,
28
+ red: 31,
29
+ green: 32,
30
+ yellow: 33,
31
+ blue: 34,
32
+ magenta: 35,
33
+ cyan: 36,
34
+ white: 37,
35
+ default: 39,
36
+ gray: 90,
37
+ # Bright colors (90-97)
38
+ bright_black: 90,
39
+ bright_red: 91,
40
+ bright_green: 92,
41
+ bright_yellow: 93,
42
+ bright_blue: 94,
43
+ bright_magenta: 95,
44
+ bright_cyan: 96,
45
+ bright_white: 97
46
+ }.freeze
47
+ public_constant :COLORS
48
+
49
+ # Effects mapping (SGR parameters)
50
+ EFFECTS = {
51
+ bold: 1,
52
+ faint: 2,
53
+ italic: 3,
54
+ underline: 4,
55
+ blink: 5,
56
+ inverse: 7,
57
+ conceal: 8,
58
+ overline: 53,
59
+ double_underline: 21
60
+ }.freeze
61
+ public_constant :EFFECTS
62
+
63
+ # Returns the singleton instance of SGRBuilder
64
+ # @return [SGRBuilder] The singleton instance
65
+ def self.instance
66
+ @instance ||= new
67
+ end
68
+
69
+ # Make new private to enforce singleton pattern
70
+ private_class_method :new
71
+
72
+ # Generates SGR prefix codes for the given styles
73
+ #
74
+ # Parameters are processed in this order for predictable output:
75
+ # 1. Foreground color (30-37, 90-97, or 38;2;r;g;b for RGB)
76
+ # 2. Background color (40-47, 100-107, or 48;2;r;g;b for RGB)
77
+ # 3. Text effects in numerical SGR code order:
78
+ # bold(1), faint(2), italic(3), underline(4), blink(5), inverse(7),
79
+ # conceal(8), double_underline(21), overline(53)
80
+ #
81
+ # @param foreground [Symbol, String, nil] Foreground color
82
+ # - Symbol: :red, :green, :blue, :yellow, :magenta, :cyan, :white, :black,
83
+ # :gray, :bright_red, :bright_green, etc.
84
+ # - String: Hex colors like "#FF0000", "FF0000", "#F00", "F00"
85
+ # @param background [Symbol, String, nil] Background color (same format as foreground)
86
+ # @param bold [Boolean, nil] Bold text effect
87
+ # @param faint [Boolean, nil] Faint text effect
88
+ # @param italic [Boolean, nil] Italic text effect
89
+ # @param underline [Boolean, Symbol, nil] Underline effect (true, :double, or nil)
90
+ # @param blink [Boolean, nil] Blink effect
91
+ # @param inverse [Boolean, nil] Inverse/reverse effect
92
+ # @param conceal [Boolean, nil] Conceal/hide effect
93
+ # @param overline [Boolean, nil] Overline effect
94
+ # @return [String] The SGR escape sequence string
95
+ #
96
+ # @example Standard colors
97
+ # prefix_codes(foreground: :red, bold: true)
98
+ # # => "\e[31;1m"
99
+ #
100
+ # @example 256-color RGB (true color)
101
+ # prefix_codes(foreground: "#FF6B35", background: "#F7931E")
102
+ # # => "\e[38;2;255;107;53;48;2;247;147;30m"
103
+ #
104
+ # @example Multiple effects with colors (colors first, then effects in numerical order)
105
+ # prefix_codes(foreground: :red, background: :blue, bold: true, italic: true)
106
+ # # => "\e[31;44;1;3m" (red=31, blue_bg=44, bold=1, italic=3)
107
+ #
108
+ # @example Double underline
109
+ # prefix_codes(foreground: :blue, underline: :double)
110
+ # # => "\e[34;21m"
111
+ def prefix_codes(
112
+ foreground: nil,
113
+ background: nil,
114
+ bold: nil,
115
+ faint: nil,
116
+ italic: nil,
117
+ underline: nil,
118
+ blink: nil,
119
+ inverse: nil,
120
+ conceal: nil,
121
+ overline: nil
122
+ )
123
+ parameters = build_parameters(
124
+ foreground:,
125
+ background:,
126
+ bold:,
127
+ faint:,
128
+ italic:,
129
+ underline:,
130
+ blink:,
131
+ inverse:,
132
+ conceal:,
133
+ overline:
134
+ )
135
+ build_sgr_sequence(parameters)
136
+ end
137
+
138
+ # Returns the SGR reset code
139
+ # @return [String] The reset escape sequence
140
+ def reset_code
141
+ RESET_CODE
142
+ end
143
+
144
+ # Converts RGB values to SGR parameters
145
+ # @param red [Integer] Red component (0-255)
146
+ # @param green [Integer] Green component (0-255)
147
+ # @param blue [Integer] Blue component (0-255)
148
+ # @param background [Boolean] Whether this is for background color
149
+ # @return [String] The SGR parameter string for RGB color
150
+ private def rgb_to_sgr(red, green, blue, background: false)
151
+ base = background ? 48 : 38
152
+ "#{base};2;#{red};#{green};#{blue}"
153
+ end
154
+
155
+ private def build_parameters(
156
+ foreground:,
157
+ background:,
158
+ bold:,
159
+ faint:,
160
+ italic:,
161
+ underline:,
162
+ blink:,
163
+ inverse:,
164
+ conceal:,
165
+ overline:
166
+ )
167
+ parameters = []
168
+
169
+ # Process foreground color
170
+ parameters << process_foreground_color(foreground) if foreground
171
+
172
+ # Process background color
173
+ parameters << process_background_color(background) if background
174
+
175
+ # Collect effects and sort by SGR code for predictable order
176
+ effects = []
177
+ effects << EFFECTS[:bold] if bold == true
178
+ effects << EFFECTS[:faint] if faint == true
179
+ effects << EFFECTS[:italic] if italic == true
180
+ case underline
181
+ when true
182
+ effects << EFFECTS[:underline]
183
+ when :double
184
+ effects << EFFECTS[:double_underline]
185
+ when nil
186
+ # No underline
187
+ else
188
+ raise ArgumentError, "Invalid underline value: #{underline.inspect}"
189
+ end
190
+ effects << EFFECTS[:blink] if blink == true
191
+ effects << EFFECTS[:inverse] if inverse == true
192
+ effects << EFFECTS[:conceal] if conceal == true
193
+ effects << EFFECTS[:overline] if overline == true
194
+
195
+ parameters.concat(effects.sort)
196
+ end
197
+
198
+ private def process_foreground_color(foreground)
199
+ case foreground
200
+ when String
201
+ raise ArgumentError, "Invalid hex color: #{foreground}" unless hex_color?(foreground)
202
+
203
+ red, green, blue = hex_to_rgb(foreground)
204
+ rgb_to_sgr(red, green, blue, background: false)
205
+ when Symbol
206
+ raise ArgumentError, "Unknown foreground color: #{foreground}" unless COLORS.key?(foreground)
207
+
208
+ COLORS[foreground]
209
+ else
210
+ raise ArgumentError, "Invalid foreground type: #{foreground.class}"
211
+ end
212
+ end
213
+
214
+ private def process_background_color(background)
215
+ case background
216
+ when String
217
+ raise ArgumentError, "Invalid hex color: #{background}" unless hex_color?(background)
218
+
219
+ red, green, blue = hex_to_rgb(background)
220
+ rgb_to_sgr(red, green, blue, background: true)
221
+ when Symbol
222
+ raise ArgumentError, "Unknown background color: #{background}" unless COLORS.key?(background)
223
+
224
+ color_code = COLORS[background]
225
+ # Convert to background color code
226
+ color_code += 10 if color_code.between?(30, 37)
227
+ color_code += 10 if color_code.between?(90, 97)
228
+ color_code
229
+ else
230
+ raise ArgumentError, "Invalid background type: #{background.class}"
231
+ end
232
+ end
233
+
234
+ private def build_sgr_sequence(parameters)
235
+ return "" if parameters.empty?
236
+
237
+ "#{CSI}#{parameters.join(";")}#{SGR_END}"
238
+ end
239
+
240
+ private def hex_color?(value)
241
+ value.match?(/\A#?[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?\z/)
242
+ end
243
+
244
+ private def hex_to_rgb(hex)
245
+ # Remove # if present
246
+ hex = hex.delete_prefix("#")
247
+
248
+ # Expand 3-digit hex to 6-digit
249
+ hex = hex.chars.map {|c| c * 2 }.join if hex.length == 3
250
+
251
+ # Convert to RGB values
252
+ [
253
+ hex[0..1].to_i(16),
254
+ hex[2..3].to_i(16),
255
+ hex[4..5].to_i(16)
256
+ ]
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-schema"
4
+
5
+ module TIntMe
6
+ class Style
7
+ # Schema for validating Style initialization arguments
8
+ Schema = Dry::Schema.define {
9
+ optional(:foreground).maybe(Types::Color)
10
+ optional(:background).maybe(Types::Color)
11
+ optional(:inverse).value(Types::BooleanOption)
12
+ optional(:bold).value(Types::BooleanOption)
13
+ optional(:faint).value(Types::BooleanOption)
14
+ optional(:underline).value(Types::UnderlineOption)
15
+ optional(:overline).value(Types::BooleanOption)
16
+ optional(:blink).value(Types::BooleanOption)
17
+ optional(:italic).value(Types::BooleanOption)
18
+ optional(:conceal).value(Types::BooleanOption)
19
+ }
20
+ private_constant :Schema
21
+ end
22
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-types"
4
+
5
+ module TIntMe
6
+ class Style
7
+ # Type definitions for Style attributes using dry-types
8
+ module Types
9
+ include Dry.Types()
10
+
11
+ # Standard ANSI color names + bright colors
12
+ ColorSymbol = Symbol.enum(
13
+ :default,
14
+ :reset,
15
+ :black,
16
+ :red,
17
+ :green,
18
+ :yellow,
19
+ :blue,
20
+ :magenta,
21
+ :cyan,
22
+ :white,
23
+ :gray,
24
+ # Bright colors (90-97)
25
+ :bright_black,
26
+ :bright_red,
27
+ :bright_green,
28
+ :bright_yellow,
29
+ :bright_blue,
30
+ :bright_magenta,
31
+ :bright_cyan,
32
+ :bright_white
33
+ )
34
+ public_constant :ColorSymbol
35
+
36
+ # Hex color strings (3 or 6 digits, with or without #)
37
+ ColorString = String.constrained(format: /\A#?\h{3}(?:\h{3})?\z/)
38
+ public_constant :ColorString
39
+
40
+ Color = ColorSymbol | ColorString
41
+ public_constant :Color
42
+
43
+ BooleanOption = (Bool | Symbol.enum(:reset)).optional
44
+ public_constant :BooleanOption
45
+
46
+ UnderlineOption = (Bool | Symbol.enum(:double, :reset)).optional
47
+ public_constant :UnderlineOption
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIntMe
4
+ Style = Data.define(
5
+ :foreground,
6
+ :background,
7
+ :inverse,
8
+ :bold,
9
+ :faint,
10
+ :underline,
11
+ :overline,
12
+ :blink,
13
+ :italic,
14
+ :conceal
15
+ )
16
+
17
+ # A style class for applying ANSI colors and text effects to terminal output.
18
+ #
19
+ # This class provides an immutable way to define and compose terminal styling options.
20
+ # It supports foreground/background colors, text decorations (bold, italic, underline, etc.),
21
+ # and composition via the >> operator for layering styles.
22
+ #
23
+ # @example Basic usage
24
+ # style = Style.new(foreground: :red, bold: true)
25
+ # puts style.call("Hello") # or style["Hello"] or style.("Hello")
26
+ #
27
+ # @example Style composition
28
+ # base = Style.new(foreground: :blue)
29
+ # emphasis = Style.new(bold: true, underline: true)
30
+ # combined = base >> emphasis
31
+ # puts combined.call("Styled text") # or combined["Styled text"] or combined.("Styled text")
32
+ #
33
+ # @example Color and decoration options
34
+ # Style.new(
35
+ # foreground: :red, # :default, :red, :green, :blue, etc. or hex "#FF0000"
36
+ # background: :yellow, # same as foreground
37
+ # bold: true, # nil (unset), false (off), true (on)
38
+ # faint: false, # mutually exclusive with bold
39
+ # underline: :double, # nil, false, true, :double
40
+ # italic: true, # nil, false, true
41
+ # inverse: true, # nil, false, true
42
+ # # ... other boolean effects: overline, blink, conceal
43
+ # )
44
+ class Style
45
+ # Returns the singleton instance of SGRBuilder
46
+ # @return [SGRBuilder] The SGR builder instance
47
+ # @api private
48
+ def self.sgr_builder
49
+ SGRBuilder.instance
50
+ end
51
+ # @!attribute [r] foreground
52
+ # @return [Symbol, String] The foreground color (:red, :blue, :default, :reset, hex "#FF0000", etc.)
53
+
54
+ # @!attribute [r] background
55
+ # @return [Symbol, String] The background color (same format as foreground)
56
+
57
+ # @!attribute [r] inverse
58
+ # @return [nil, true, false, :reset] Whether to reverse foreground/background colors
59
+
60
+ # @!attribute [r] bold
61
+ # @return [nil, true, false, :reset] Whether text is bold (mutually exclusive with faint)
62
+
63
+ # @!attribute [r] faint
64
+ # @return [nil, true, false, :reset] Whether text is faint/dim (mutually exclusive with bold)
65
+
66
+ # @!attribute [r] underline
67
+ # @return [nil, true, false, :double, :reset] Underline decoration type
68
+
69
+ # @!attribute [r] overline
70
+ # @return [nil, true, false, :reset] Whether text has overline decoration
71
+
72
+ # @!attribute [r] blink
73
+ # @return [nil, true, false, :reset] Whether text blinks
74
+
75
+ # @!attribute [r] italic
76
+ # @return [nil, true, false, :reset] Whether text is italic
77
+
78
+ # @!attribute [r] conceal
79
+ # @return [nil, true, false, :reset] Whether text is hidden/concealed
80
+
81
+ # Initialize a new Style with the given attributes
82
+ #
83
+ # @param foreground [Symbol, String] Foreground color. Accepts color names (:red, :green, :blue, etc.),
84
+ # :default for terminal default, :reset for composition clearing, or hex strings ("#FF0000", "FF0000")
85
+ # @param background [Symbol, String] Background color. Same format as foreground
86
+ # @param inverse [nil, true, false, :reset] Reverse foreground/background colors
87
+ # @param bold [nil, true, false, :reset] Bold text (mutually exclusive with faint)
88
+ # @param faint [nil, true, false, :reset] Faint/dim text (mutually exclusive with bold)
89
+ # @param underline [nil, true, false, :double, :reset] Underline decoration
90
+ # @param overline [nil, true, false, :reset] Overline decoration
91
+ # @param blink [nil, true, false, :reset] Blinking text
92
+ # @param italic [nil, true, false, :reset] Italic text
93
+ # @param conceal [nil, true, false, :reset] Hidden/concealed text
94
+ # @raise [ArgumentError] If both bold and faint are true
95
+ # @raise [ArgumentError] If any parameter has invalid type or value
96
+ # @example Valid usage
97
+ # Style.new(foreground: :red, bold: true)
98
+ # Style.new(underline: :double, background: "#FF0000")
99
+ # @example Invalid usage (raises ArgumentError)
100
+ # Style.new(foreground: 123) # Invalid color type
101
+ # Style.new(bold: "true") # Invalid boolean type
102
+ # Style.new(underline: :invalid) # Invalid underline option
103
+ # Style.new(bold: true, faint: true) # Mutually exclusive options
104
+ def initialize(
105
+ foreground: nil,
106
+ background: nil,
107
+ inverse: nil,
108
+ bold: nil,
109
+ faint: nil,
110
+ underline: nil,
111
+ overline: nil,
112
+ blink: nil,
113
+ italic: nil,
114
+ conceal: nil
115
+ )
116
+ # Schema validation
117
+ result = Schema.call({
118
+ foreground:,
119
+ background:,
120
+ inverse:,
121
+ bold:,
122
+ faint:,
123
+ underline:,
124
+ overline:,
125
+ blink:,
126
+ italic:,
127
+ conceal:
128
+ })
129
+
130
+ raise ArgumentError, result.errors.to_h unless result.success?
131
+
132
+ # Handle bold/faint mutual exclusion
133
+ if bold && faint
134
+ raise ArgumentError, "Cannot specify both bold and faint simultaneously"
135
+ end
136
+
137
+ # Pre-compute SGR sequences before freezing
138
+ # (Data.define freezes the instance after super)
139
+ sgr_builder = self.class.sgr_builder
140
+
141
+ # Prepare color values
142
+ foreground_color = foreground if foreground && foreground != :default
143
+ background_color = background if background && background != :default
144
+
145
+ # Handle underline effect
146
+ underline_effect = case underline
147
+ when true then true
148
+ when :double then :double
149
+ when nil, false then nil
150
+ else raise ArgumentError, "Invalid underline value: #{underline.inspect}"
151
+ end
152
+
153
+ # Calculate prefix once
154
+ @prefix = sgr_builder.prefix_codes(
155
+ foreground: foreground_color,
156
+ background: background_color,
157
+ bold: bold == true ? true : nil,
158
+ faint: faint == true ? true : nil,
159
+ italic: italic == true ? true : nil,
160
+ underline: underline_effect,
161
+ blink: blink == true ? true : nil,
162
+ inverse: inverse == true ? true : nil,
163
+ conceal: conceal == true ? true : nil,
164
+ overline: overline == true ? true : nil
165
+ )
166
+
167
+ # Pre-compute reset code
168
+ @reset_code = sgr_builder.reset_code
169
+
170
+ super
171
+ end
172
+
173
+ # Apply the style to the given text using native ANSI escape sequences
174
+ #
175
+ # @param text [String] The text to apply styling to
176
+ # @return [String] The styled text with ANSI escape codes, or original text if no styles are set
177
+ # @example
178
+ # style = Style.new(foreground: :red, bold: true)
179
+ # style.call("Hello") # => "\e[31;1mHello\e[0m"
180
+ # style["World"] # => "\e[31;1mWorld\e[0m" (alias)
181
+ def call(text)
182
+ return text if @prefix.empty?
183
+
184
+ "#{@prefix}#{text}#{@reset_code}"
185
+ end
186
+
187
+ alias [] call
188
+
189
+ # Compose this style with another style, creating a new Style instance.
190
+ # The right-hand style takes precedence for non-nil values.
191
+ # Handles bold/faint mutual exclusion automatically.
192
+ #
193
+ # == Composition Rules
194
+ #
195
+ # For `a >> b`, the resulting value for each attribute is determined by:
196
+ #
197
+ # <table>
198
+ # <tr>
199
+ # <th>b's value</th>
200
+ # <th>Result</th>
201
+ # <th>Description</th>
202
+ # </tr>
203
+ # <tr>
204
+ # <td><code>nil</code></td>
205
+ # <td><code>a</code>'s value</td>
206
+ # <td>Preserves the original value</td>
207
+ # </tr>
208
+ # <tr>
209
+ # <td><code>:reset</code></td>
210
+ # <td><code>nil</code></td>
211
+ # <td>Explicitly resets to no styling</td>
212
+ # </tr>
213
+ # <tr>
214
+ # <td>any other</td>
215
+ # <td><code>b</code>'s value</td>
216
+ # <td>Adopts the new value</td>
217
+ # </tr>
218
+ # </table>
219
+ #
220
+ # This applies to all attributes: colors (foreground, background) and
221
+ # style attributes (bold, italic, underline, inverse, overline, blink, conceal).
222
+ #
223
+ # @param other [Style] The style to compose with this one
224
+ # @return [Style] A new Style instance with composed attributes
225
+ #
226
+ # @example Basic composition
227
+ # base = Style.new(foreground: :red, bold: true)
228
+ # overlay = Style.new(background: :blue, underline: true)
229
+ # result = base >> overlay # => red text, blue background, bold and underlined
230
+ #
231
+ # @example Using nil to preserve values
232
+ # styled = Style.new(foreground: :red, bold: true)
233
+ # partial = Style.new(foreground: nil, italic: true) # nil preserves red
234
+ # result = styled >> partial # => foreground: :red, bold: true, italic: true
235
+ #
236
+ # @example Using :reset to clear styles
237
+ # styled = Style.new(foreground: :red, bold: true)
238
+ # reset = Style.new(foreground: :reset, bold: :reset)
239
+ # result = styled >> reset # => foreground: nil, bold: nil
240
+ #
241
+ # @example Bold/faint mutual exclusion
242
+ # bold_style = Style.new(bold: true)
243
+ # faint_style = Style.new(faint: true)
244
+ # result1 = bold_style >> faint_style # => faint wins (right-hand takes precedence)
245
+ # result2 = faint_style >> bold_style # => bold wins (right-hand takes precedence)
246
+ def >>(other)
247
+ # Handle bold/faint mutual exclusion in composition
248
+ composed_bold = compose_attribute(bold, other.bold)
249
+ composed_faint = compose_attribute(faint, other.faint)
250
+
251
+ # If other explicitly sets bold, clear faint; if other explicitly sets faint, clear bold
252
+ if other.bold == true
253
+ composed_faint = false
254
+ elsif other.faint == true
255
+ composed_bold = false
256
+ elsif other.bold == :reset
257
+ composed_bold = nil
258
+ elsif other.faint == :reset
259
+ composed_faint = nil
260
+ end
261
+
262
+ Style.new(
263
+ foreground: compose_attribute(foreground, other.foreground),
264
+ background: compose_attribute(background, other.background),
265
+ inverse: compose_attribute(inverse, other.inverse),
266
+ bold: composed_bold,
267
+ faint: composed_faint,
268
+ underline: compose_attribute(underline, other.underline),
269
+ overline: compose_attribute(overline, other.overline),
270
+ blink: compose_attribute(blink, other.blink),
271
+ italic: compose_attribute(italic, other.italic),
272
+ conceal: compose_attribute(conceal, other.conceal)
273
+ )
274
+ end
275
+
276
+ private def compose_attribute(current, other)
277
+ if other.nil?
278
+ current
279
+ elsif other == :reset
280
+ nil
281
+ else
282
+ other
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TIntMe
4
+ # Current version of the TIntMe gem
5
+ # @return [String] The semantic version string
6
+ VERSION = "1.0.0"
7
+ public_constant :VERSION
8
+ end