ansi256 0.5.0 → 0.6.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e0c06de0d942deeb5906752ed223248b699408b93a2bba463bbb848d6b06aea
4
- data.tar.gz: a7ea18e9b3902806d43fe46e7b388c8d0152e4e0e365bdb552ad35d11064b12a
3
+ metadata.gz: 193fb48fa8076a77689d28fc6923216d4d144fa0614d209a7c8c10289564f5e2
4
+ data.tar.gz: 9f6159c21e0a94c54dc14f9306251f55faba36e596b298286ba995a84602d5fd
5
5
  SHA512:
6
- metadata.gz: 8d10be011d96e91b58ccfef63608a466052dfaca7bd25e05ee1c6b83cabc6deaeeefc1e0de4e9f99b19267714f0b88a04b42bac031fe4c5937fc39b9c078c043
7
- data.tar.gz: b15f5eab61b1f5be118a69bd9cdbd5972a7ea0adc13a51e462665aaf282c71b4967d917d6ebbc8b17c7e6eb97ecbdae76e2a1d7349ef014330177926ff4829b6
6
+ metadata.gz: 9b3dd2434b21092bac623baf058ee31725ce9f3c131687150f03936b6e5aa0d863ea11dee9ba683bda5322faa160e7115b046a19503c8ae69d778622f8455ab3
7
+ data.tar.gz: 7d0cfc94e225eb7da1aba42fd82e2d6c4733d36f5dd26a9054fc3b520d7c911064b368e4f2b434204712873b111b66386280b5393bcab1cdabd1241d689e0bb2
data/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # ansi256
2
2
 
3
- ansi256 is a rubygem for colorizing text with 256-color ANSI codes.
3
+ ansi256 is a rubygem for colorizing text with ANSI escape codes.
4
4
 
5
5
  Features:
6
6
 
7
- - Supports both named color codes and numeric 256-color codes
7
+ - True 24-bit color, named colors, and 256-color support
8
+ - Underline styles (curly, double, dotted, dashed) and underline colors
8
9
  - Allows nesting of colored text
9
10
  - Generates optimal(shortest) code sequence
10
11
 
@@ -29,6 +30,13 @@ puts "Colorize me".fg(111).bg(226)
29
30
  # Also with underline
30
31
  puts "Colorize me".fg(111).bg(226).underline
31
32
 
33
+ # Underline styles (single, double, curly, dotted, dashed)
34
+ puts "Colorize me".underline(:curly)
35
+
36
+ # Underline with color (256-color index or hex RGB)
37
+ puts "Colorize me".underline(:curly, 'ff9900')
38
+ puts "Colorize me".underline(214)
39
+
32
40
  # Strip ANSI codes
33
41
  puts "Colorize me".fg(111).bg(226).underline.plain
34
42
  ```
@@ -51,7 +59,9 @@ puts [ s.white, s.white.bold, s.white.bold.on_white ].join ' '
51
59
 
52
60
  ![colorize-me-16](https://github.com/junegunn/ansi256/raw/master/colorize-me-16.png)
53
61
 
54
- ### RGB color approximated to 256-color ANSI code
62
+ ### RGB hex colors
63
+
64
+ RGB hex strings produce true 24-bit color (`\e[38;2;R;G;Bm`) by default.
55
65
 
56
66
  ```ruby
57
67
  puts "RGB Color (RRGGBB)".fg('ff9930').bg('203366')
@@ -61,6 +71,12 @@ puts "RGB Color (R-G-B-)".fg('f90').bg('036')
61
71
  puts "RGB Color (Monochrome)".fg('ef').bg('3f')
62
72
  ```
63
73
 
74
+ To approximate RGB to 256-color indices instead (for legacy terminals):
75
+
76
+ ```ruby
77
+ Ansi256.truecolor = false
78
+ ```
79
+
64
80
  Nesting
65
81
  -------
66
82
 
@@ -110,43 +126,6 @@ Ansi256.enabled = $stdout.tty?
110
126
  # Hello
111
127
  ```
112
128
 
113
- ansi256 executable
114
- ------------------
115
-
116
- Ansi256 comes with `ansi256` script which can be used as follows
117
-
118
- ```bash
119
- usage: ansi256 [-b] [-d] [-i] [-u] <[fg][/bg]> [message]
120
-
121
- # Numeric color codes
122
- ansi256 232 "Hello world"
123
- ansi256 /226 "Hello world"
124
- ansi256 232/226 "Hello world"
125
-
126
- # Named color codes
127
- ansi256 yellow "Hello world"
128
- ansi256 /blue "Hello world"
129
- ansi256 yellow/blue "Hello world"
130
-
131
- # RGB colors (only support 6-letter hex codes)
132
- ansi256 ff9900/000033 "Hello world"
133
-
134
- # Mixed color codes
135
- ansi256 yellow/232 "Hello world"
136
-
137
- # Bold yellow
138
- ansi256 yellow/232 -b "Hello world"
139
-
140
- # With underline
141
- ansi256 yellow/232 -b -u "Hello world"
142
-
143
- # Colorizing STDIN
144
- find / | ansi256 -u /226
145
-
146
- # Nesting
147
- ansi256 30 "Say '$(ansi256 230/75 "Hello $(ansi256 -u 232/226 World)")'"
148
- ```
149
-
150
129
  Color chart
151
130
  -----------
152
131
 
data/bin/ansi256 CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # Deprecated: this CLI is no longer maintained.
5
+ # Use the library API directly instead.
6
+
4
7
  require 'rubygems'
5
8
  require 'ansi256'
6
9
 
data/lib/ansi256/code.rb CHANGED
@@ -27,6 +27,14 @@ module Ansi256
27
27
  on_white: 47
28
28
  }.freeze
29
29
 
30
+ UNDERLINE_STYLES = {
31
+ single: '4:1',
32
+ double: '4:2',
33
+ curly: '4:3',
34
+ dotted: '4:4',
35
+ dashed: '4:5',
36
+ }.freeze
37
+
30
38
  NAMED_COLORS = Set[
31
39
  :black,
32
40
  :red,
data/lib/ansi256/mixin.rb CHANGED
@@ -27,6 +27,12 @@ module Ansi256
27
27
  Ansi256.send name, self
28
28
  end
29
29
  end
30
+
31
+ # Override underline to accept style and color
32
+ undef_method(:underline) if method_defined?(:underline)
33
+ define_method :underline do |*props|
34
+ Ansi256.underline(self, *props)
35
+ end
30
36
  end
31
37
 
32
38
  Ansi256.class_eval do
@@ -58,6 +64,12 @@ module Ansi256
58
64
  self
59
65
  end
60
66
  end
67
+
68
+ # Override underline to accept style and color (disabled mode)
69
+ undef_method(:underline) if method_defined?(:underline)
70
+ define_method :underline do |*props|
71
+ self
72
+ end
61
73
  end
62
74
 
63
75
  Ansi256.class_eval do
@@ -74,4 +86,3 @@ module Ansi256
74
86
  end
75
87
  end
76
88
  end
77
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ansi256
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.1'
5
5
  end
data/lib/ansi256.rb CHANGED
@@ -7,6 +7,8 @@ require 'ansi256/mixin'
7
7
 
8
8
  module Ansi256
9
9
  class << self
10
+ attr_accessor :truecolor
11
+
10
12
  def fg(code, str = nil)
11
13
  if str
12
14
  wrap str, Ansi256.fg(code)
@@ -14,8 +16,12 @@ module Ansi256
14
16
  "\e[#{CODE[code]}m"
15
17
  elsif code.is_a?(Integer) && (0..255).include?(code)
16
18
  "\e[38;5;#{code}m"
17
- elsif ansirgb = ansicode_for_rgb(code)
18
- "\e[38;5;#{ansirgb}m"
19
+ elsif rgb = parse_rgb(code)
20
+ if truecolor
21
+ "\e[38;2;#{rgb.join(';')}m"
22
+ else
23
+ "\e[38;5;#{ansicode_for_rgb(code)}m"
24
+ end
19
25
  else
20
26
  raise ArgumentError, "Invalid color code: #{code}"
21
27
  end
@@ -28,14 +34,20 @@ module Ansi256
28
34
  "\e[#{CODE[code] + 10}m"
29
35
  elsif code.is_a?(Integer) && (0..255).include?(code)
30
36
  "\e[48;5;#{code}m"
31
- elsif ansirgb = ansicode_for_rgb(code)
32
- "\e[48;5;#{ansirgb}m"
37
+ elsif rgb = parse_rgb(code)
38
+ if truecolor
39
+ "\e[48;2;#{rgb.join(';')}m"
40
+ else
41
+ "\e[48;5;#{ansicode_for_rgb(code)}m"
42
+ end
33
43
  else
34
44
  raise ArgumentError, "Invalid color code: #{code}"
35
45
  end
36
46
  end
37
47
 
38
48
  CODE.each do |name, code|
49
+ next if name == :underline # defined below
50
+
39
51
  define_method name do |*arg|
40
52
  if str = arg.first
41
53
  wrap str, "\e[#{code}m"
@@ -45,88 +57,223 @@ module Ansi256
45
57
  end
46
58
  end
47
59
 
60
+ # Underline with optional styles and colors
61
+ def underline(str = nil, *props)
62
+ if str.nil?
63
+ build_underline_sgr(*props)
64
+ else
65
+ wrap str, build_underline_sgr(*props)
66
+ end
67
+ end
68
+
48
69
  def plain(str)
49
70
  str.gsub(PATTERN, '')
50
71
  end
51
72
 
52
73
  private
53
74
 
54
- PATTERN = /\e\[[0-9;]+m/
55
- MULTI_PATTERN = /(?:\e\[[0-9;]+m)+/
56
- EMPTY_TRIPLE = [nil, nil, Set.new].freeze
75
+ PATTERN = /\e\[[0-9;:]+m/
76
+ MULTI_PATTERN = /(?:\e\[[0-9;:]+m)+/
77
+ # State: [fg, bg, ul_style, ul_color, attrs_set]
78
+ # ul_style is only for sub-parameter styles (4:1, 4:2, etc.)
79
+ # Basic underline (4) stays in attrs_set for backward compat
80
+ EMPTY_STATE = [nil, nil, nil, nil, Set.new].freeze
81
+
82
+ def build_underline_sgr(*args)
83
+ style = nil
84
+ color = nil
85
+
86
+ args.each do |arg|
87
+ case arg
88
+ when nil
89
+ next
90
+ when Symbol
91
+ if UNDERLINE_STYLES[arg]
92
+ style = arg
93
+ elsif NAMED_COLORS.include?(arg)
94
+ color = arg
95
+ else
96
+ raise ArgumentError, "Invalid underline argument: #{arg}"
97
+ end
98
+ when Integer, String
99
+ color = arg
100
+ else
101
+ raise ArgumentError, "Invalid underline argument: #{arg}"
102
+ end
103
+ end
104
+
105
+ return "\e[4m" unless style || color
106
+
107
+ parts = []
108
+ parts << (style ? UNDERLINE_STYLES[style] : '4')
109
+ parts << underline_color_code(color) if color
110
+
111
+ "\e[#{parts.join(';')}m"
112
+ end
113
+
114
+ def underline_color_code(color)
115
+ case color
116
+ when Symbol
117
+ raise ArgumentError, "Invalid underline color: #{color}" unless NAMED_COLORS.include?(color)
118
+ "58;5;#{CODE[color] - 30}"
119
+ when Integer
120
+ raise ArgumentError, "Invalid underline color: #{color}" unless (0..255).include?(color)
121
+ "58;5;#{color}"
122
+ when String
123
+ rgb = parse_rgb(color)
124
+ raise ArgumentError, "Invalid underline color: #{color}" unless rgb
125
+ if truecolor
126
+ "58;2;#{rgb.join(';')}"
127
+ else
128
+ "58;5;#{ansicode_for_rgb(color)}"
129
+ end
130
+ else
131
+ raise ArgumentError, "Invalid underline color: #{color}"
132
+ end
133
+ end
57
134
 
58
135
  def ansify(prev, curr)
59
136
  nums = []
60
137
  nums << curr[0] if prev[0] != curr[0]
61
138
  nums << curr[1] if prev[1] != curr[1]
62
- nums.concat curr[2].to_a if prev[2] != curr[2]
139
+ nums << curr[2] if prev[2] != curr[2]
140
+ nums << curr[3] if prev[3] != curr[3]
141
+ nums.concat curr[4].to_a if prev[4] != curr[4]
63
142
  "\e[#{nums.compact.join ';'}m"
64
143
  end
65
144
 
66
- def ansicode_for_rgb(rgb)
67
- return unless rgb.is_a?(String) &&
68
- rgb =~ /^#?([0-9a-f]+)$/i
145
+ def parse_rgb(code)
146
+ return unless code.is_a?(String) &&
147
+ code =~ /^#?([0-9a-f]+)$/i
69
148
 
70
- rgb = ::Regexp.last_match(1)
149
+ hex = ::Regexp.last_match(1)
71
150
 
72
- case rgb.length
151
+ case hex.length
73
152
  when 2
74
- m = (256 - 231) * rgb.to_i(16) / 256
75
- return m == 0 ? 16 : (231 + m)
153
+ v = hex.to_i(16)
154
+ [v, v, v]
76
155
  when 3
77
- r, g, b = rgb.each_char.map { |e| (e * 2).to_i(16) }
156
+ hex.each_char.map { |e| (e * 2).to_i(16) }
78
157
  when 6
79
- r, g, b = rgb.each_char.each_slice(2).map { |e| e.join.to_i(16) }
80
- else
81
- return
158
+ hex.each_char.each_slice(2).map { |e| e.join.to_i(16) }
82
159
  end
160
+ end
83
161
 
162
+ def rgb_to_ansi256(r, g, b)
84
163
  r, g, b = [r, g, b].map { |e| 6 * e / 256 }
85
164
  r * 36 + g * 6 + b + 16
86
165
  end
87
166
 
167
+ def ansicode_for_rgb(code)
168
+ return unless code.is_a?(String) &&
169
+ code =~ /^#?([0-9a-f]+)$/i
170
+
171
+ hex = ::Regexp.last_match(1)
172
+
173
+ # 2-char monochrome uses grayscale ramp (232-255)
174
+ if hex.length == 2
175
+ m = 25 * hex.to_i(16) / 256
176
+ return m == 0 ? 16 : (231 + m)
177
+ end
178
+
179
+ rgb = parse_rgb(code)
180
+ return unless rgb
181
+
182
+ rgb_to_ansi256(*rgb)
183
+ end
184
+
88
185
  def wrap(str, color)
89
- current = [nil, nil, Set.new]
186
+ current = [nil, nil, nil, nil, Set.new]
90
187
 
91
188
  (color + str.gsub(PATTERN) do |m|
92
- if m =~ /\e\[[^m]*\b0m/
189
+ if m == "\e[0m"
93
190
  m + color
94
191
  else
95
192
  m
96
193
  end
97
194
  end << reset).gsub(MULTI_PATTERN) do |ansi|
98
195
  prev = current.dup
99
- prev[2] = prev[2].dup
100
- codes = ansi.scan(/\d+/).map(&:to_i)
101
-
102
- idx = -1
103
- while (idx += 1) < codes.length
104
- case code = codes[idx]
105
- when 38
106
- current[0] = codes[idx, 3].join ';'
107
- idx += 2 # 38;5;11
108
- when 48
109
- current[1] = codes[idx, 3].join ';'
110
- idx += 2 # 38;5;11
111
- when 30..37
112
- current[0] = codes[idx]
113
- when 40..47
114
- current[1] = codes[idx]
115
- when 0
116
- current[0] = current[1] = nil
117
- current[2].clear
118
- else
119
- current[2] << code
196
+ prev[4] = prev[4].dup
197
+
198
+ # Extract individual SGR sequences and parse each
199
+ seqs = ansi.scan(/\e\[([^\e]*?)m/).map { |cap| cap[0] }
200
+ seqs.each do |seq|
201
+ # Split by semicolon, preserving colon sub-params within groups
202
+ groups = seq.split(';')
203
+
204
+ idx = -1
205
+ while (idx += 1) < groups.length
206
+ group = groups[idx]
207
+
208
+ if group.include?(':')
209
+ # Sub-parameter group (e.g., "4:3")
210
+ main, sub = group.split(':', 2)
211
+ main_i = main.to_i
212
+ if main_i == 4
213
+ if sub == '0'
214
+ current[2] = nil
215
+ current[4].delete(4)
216
+ else
217
+ current[2] = group
218
+ current[4].delete(4)
219
+ end
220
+ end
221
+ else
222
+ code = group.to_i
223
+ case code
224
+ when 38
225
+ if groups[idx + 1] == '2'
226
+ current[0] = groups[idx, 5].join(';')
227
+ idx += 4
228
+ else
229
+ current[0] = groups[idx, 3].join(';')
230
+ idx += 2
231
+ end
232
+ when 48
233
+ if groups[idx + 1] == '2'
234
+ current[1] = groups[idx, 5].join(';')
235
+ idx += 4
236
+ else
237
+ current[1] = groups[idx, 3].join(';')
238
+ idx += 2
239
+ end
240
+ when 58
241
+ if groups[idx + 1] == '2'
242
+ current[3] = groups[idx, 5].join(';')
243
+ idx += 4
244
+ else
245
+ current[3] = groups[idx, 3].join(';')
246
+ idx += 2
247
+ end
248
+ when 30..37
249
+ current[0] = code
250
+ when 40..47
251
+ current[1] = code
252
+ when 0
253
+ current[0] = current[1] = current[2] = current[3] = nil
254
+ current[4].clear
255
+ when 24
256
+ current[2] = nil
257
+ current[4].delete(4)
258
+ when 59
259
+ current[3] = nil
260
+ when 4
261
+ # Basic underline goes in attrs set for backward compat
262
+ current[4] << 4
263
+ else
264
+ current[4] << code
265
+ end
266
+ end
120
267
  end
121
268
  end
122
269
 
123
270
  if current == prev
124
271
  ''
125
- elsif current == EMPTY_TRIPLE
272
+ elsif current == EMPTY_STATE
126
273
  reset
127
274
  else
128
- if (0..1).any? { |i| prev[i] && !current[i] } || current[2].proper_subset?(prev[2])
129
- prev = EMPTY_TRIPLE
275
+ if (0..3).any? { |i| prev[i] && !current[i] } || current[4].proper_subset?(prev[4])
276
+ prev = EMPTY_STATE
130
277
  reset
131
278
  else
132
279
  ''
@@ -138,3 +285,4 @@ module Ansi256
138
285
  end
139
286
 
140
287
  Ansi256.enabled = true
288
+ Ansi256.truecolor = true
data/test/test_ansi256.rb CHANGED
@@ -149,34 +149,39 @@ class TestAnsi256 < Minitest::Test
149
149
  end
150
150
 
151
151
  def test_rgb
152
+ # Truecolor mode (default): 38;2;R;G;B
152
153
  {
153
- '00' => 16,
154
- '000000' => 16,
155
- '111' => 16,
156
- '11' => 232,
157
- '101010' => 16,
158
- '1a' => 233,
159
- '1a1a1a' => 16,
160
- '20' => 234,
161
- '22' => 234,
162
- '202020' => 16,
163
- '222222' => 16,
164
- '222222' => 16,
165
- 'ff' => 255,
166
- 'ffffff' => 231,
167
- 'FFFFFF' => 231,
168
- 'ff0' => 226,
169
- 'ffff00' => 226,
170
- 'ff0000' => 196,
171
- '00ff00' => 46,
172
- '0000ff' => 21,
173
- 'ff9900' => 214,
174
- '00ffff' => 51,
175
- '0ff' => 51
176
- }.each do |rgb, ansi|
177
- assert_equal ansi, Ansi256.fg(rgb).scan(/[0-9]+/).last.to_i
178
- assert_equal ansi, Ansi256.bg(rgb).scan(/[0-9]+/).last.to_i
154
+ '00' => [0, 0, 0],
155
+ '000000' => [0, 0, 0],
156
+ '111' => [17, 17, 17],
157
+ '11' => [17, 17, 17],
158
+ '101010' => [16, 16, 16],
159
+ '1a' => [26, 26, 26],
160
+ '1a1a1a' => [26, 26, 26],
161
+ '20' => [32, 32, 32],
162
+ '22' => [34, 34, 34],
163
+ '202020' => [32, 32, 32],
164
+ '222222' => [34, 34, 34],
165
+ 'ff' => [255, 255, 255],
166
+ 'ffffff' => [255, 255, 255],
167
+ 'FFFFFF' => [255, 255, 255],
168
+ 'ff0' => [255, 255, 0],
169
+ 'ffff00' => [255, 255, 0],
170
+ 'ff0000' => [255, 0, 0],
171
+ '00ff00' => [0, 255, 0],
172
+ '0000ff' => [0, 0, 255],
173
+ 'ff9900' => [255, 153, 0],
174
+ '00ffff' => [0, 255, 255],
175
+ '0ff' => [0, 255, 255],
176
+ }.each do |hex, (r, g, b)|
177
+ assert_equal "\e[38;2;#{r};#{g};#{b}m", Ansi256.fg(hex)
178
+ assert_equal "\e[48;2;#{r};#{g};#{b}m", Ansi256.bg(hex)
179
179
  end
180
+ # # prefix
181
+ assert_equal "\e[38;2;255;153;0m", Ansi256.fg('#ff9900')
182
+ assert_equal "\e[48;2;0;51;102m", Ansi256.bg('#003366')
183
+
184
+ # Visual output
180
185
  %i[bg fg].each do |m|
181
186
  (0..255).each do |r|
182
187
  color = r.to_s(16).rjust(2, '0') * 3
@@ -201,6 +206,59 @@ class TestAnsi256 < Minitest::Test
201
206
  puts 'RGB Color (Monochrome)'.fg('ef').bg('3a')
202
207
  end
203
208
 
209
+ def test_rgb_nesting
210
+ # Truecolor inner nested inside truecolor outer
211
+ assert_equal "\e[38;2;0;51;102mHello \e[38;2;255;153;0mWorld\e[38;2;0;51;102m !\e[0m",
212
+ "Hello #{'World'.fg('ff9900')} !".fg('003366')
213
+
214
+ # Truecolor fg with integer bg
215
+ assert_equal "\e[38;2;255;153;0;48;5;100mHello\e[0m",
216
+ 'Hello'.fg('ff9900').bg(100)
217
+
218
+ # Integer fg nested inside truecolor fg
219
+ assert_equal "\e[38;2;0;51;102mHello \e[38;5;100mWorld\e[38;2;0;51;102m !\e[0m",
220
+ "Hello #{'World'.fg(100)} !".fg('003366')
221
+
222
+ # Truecolor underline color in nesting
223
+ inner = 'world'.underline(:curly, 'ff0000')
224
+ result = "hello #{inner} end".fg('003366')
225
+ assert_equal "\e[38;2;0;51;102mhello \e[4:3;58;2;255;0;0mworld\e[0m\e[38;2;0;51;102m end\e[0m", result
226
+ end
227
+
228
+ def test_rgb_approximation
229
+ # When truecolor is disabled, fall back to 256-color approximation
230
+ Ansi256.truecolor = false
231
+ {
232
+ '00' => 16,
233
+ '000000' => 16,
234
+ '111' => 16,
235
+ '11' => 232,
236
+ '101010' => 16,
237
+ '1a' => 233,
238
+ '1a1a1a' => 16,
239
+ '20' => 234,
240
+ '22' => 234,
241
+ '202020' => 16,
242
+ '222222' => 16,
243
+ 'ff' => 255,
244
+ 'ffffff' => 231,
245
+ 'FFFFFF' => 231,
246
+ 'ff0' => 226,
247
+ 'ffff00' => 226,
248
+ 'ff0000' => 196,
249
+ '00ff00' => 46,
250
+ '0000ff' => 21,
251
+ 'ff9900' => 214,
252
+ '00ffff' => 51,
253
+ '0ff' => 51,
254
+ }.each do |rgb, ansi|
255
+ assert_equal ansi, Ansi256.fg(rgb).scan(/[0-9]+/).last.to_i
256
+ assert_equal ansi, Ansi256.bg(rgb).scan(/[0-9]+/).last.to_i
257
+ end
258
+ ensure
259
+ Ansi256.truecolor = true
260
+ end
261
+
204
262
  def test_enabled
205
263
  2.times do
206
264
  %i[fg bg].each do |m|
@@ -219,4 +277,92 @@ class TestAnsi256 < Minitest::Test
219
277
  end
220
278
  end
221
279
  end
280
+
281
+ # --- Extended underline tests ---
282
+
283
+ def test_underline_backward_compat
284
+ # No-arg: returns SGR code
285
+ assert_equal "\e[4m", Ansi256.underline
286
+
287
+ # String arg: wraps with basic underline
288
+ assert_equal "\e[4mhello\e[0m", Ansi256.underline('hello')
289
+ assert_equal "\e[4mhello\e[0m", 'hello'.underline
290
+ end
291
+
292
+ def test_underline_styles
293
+ assert_equal "\e[4:1mhello\e[0m", 'hello'.underline(:single)
294
+ assert_equal "\e[4:2mhello\e[0m", 'hello'.underline(:double)
295
+ assert_equal "\e[4:3mhello\e[0m", 'hello'.underline(:curly)
296
+ assert_equal "\e[4:4mhello\e[0m", 'hello'.underline(:dotted)
297
+ assert_equal "\e[4:5mhello\e[0m", 'hello'.underline(:dashed)
298
+ end
299
+
300
+ def test_underline_color_integer
301
+ assert_equal "\e[58;5;214;4mhello\e[0m", 'hello'.underline(214)
302
+ end
303
+
304
+ def test_underline_color_hex
305
+ assert_equal "\e[58;2;255;153;0;4mhello\e[0m", 'hello'.underline('ff9900')
306
+ assert_equal "\e[58;2;255;153;0;4mhello\e[0m", 'hello'.underline('#ff9900')
307
+ assert_equal "\e[58;2;255;153;0;4mhello\e[0m", 'hello'.underline('f90')
308
+ end
309
+
310
+ def test_underline_style_with_color
311
+ assert_equal "\e[4:3;58;2;255;153;0mhello\e[0m", 'hello'.underline(:curly, 'ff9900')
312
+ assert_equal "\e[4:2;58;5;100mhello\e[0m", 'hello'.underline(:double, 100)
313
+ end
314
+
315
+ def test_underline_invalid_style
316
+ assert_raises(ArgumentError) { 'hello'.underline(:wavy) }
317
+ end
318
+
319
+ def test_underline_invalid_color
320
+ assert_raises(ArgumentError) { 'hello'.underline(:curly, 'zzzzzz') }
321
+ assert_raises(ArgumentError) { 'hello'.underline(:curly, -1) }
322
+ assert_raises(ArgumentError) { 'hello'.underline(:curly, 256) }
323
+ end
324
+
325
+ def test_underline_style_nesting_with_fg
326
+ # Styled underline nested inside fg color
327
+ inner = 'world'.underline(:curly)
328
+ result = "hello #{inner} end".fg(100)
329
+ assert_equal "\e[38;5;100mhello \e[4:3mworld\e[0m\e[38;5;100m end\e[0m", result
330
+ end
331
+
332
+ def test_underline_color_nesting_with_fg
333
+ inner = 'world'.underline(:curly, 196)
334
+ result = "hello #{inner} end".fg(100)
335
+ assert_equal "\e[38;5;100mhello \e[4:3;58;5;196mworld\e[0m\e[38;5;100m end\e[0m", result
336
+ end
337
+
338
+ def test_underline_disabled_mode
339
+ Ansi256.enabled = false
340
+ assert_equal 'hello', 'hello'.underline
341
+ assert_equal 'hello', 'hello'.underline(:curly)
342
+ assert_equal 'hello', 'hello'.underline(:curly, 'ff9900')
343
+ assert_equal 'hello', 'hello'.underline(214)
344
+ Ansi256.enabled = true
345
+ end
346
+
347
+ def test_underline_module_method_with_style
348
+ assert_equal "\e[4:3mhello\e[0m", Ansi256.underline('hello', :curly)
349
+ assert_equal "\e[4:3;58;2;255;153;0mhello\e[0m", Ansi256.underline('hello', :curly, 'ff9900')
350
+ assert_equal "\e[58;5;214;4mhello\e[0m", Ansi256.underline('hello', 214)
351
+ end
352
+
353
+ def test_underline_named_color
354
+ # Named color as sole argument (basic underline + color)
355
+ assert_equal "\e[58;5;1;4mhello\e[0m", 'hello'.underline(:red)
356
+ assert_equal "\e[58;5;4;4mhello\e[0m", 'hello'.underline(:blue)
357
+ assert_equal "\e[58;5;0;4mhello\e[0m", 'hello'.underline(:black)
358
+ assert_equal "\e[58;5;7;4mhello\e[0m", 'hello'.underline(:white)
359
+
360
+ # Named color as second argument (style + color)
361
+ assert_equal "\e[4:3;58;5;1mhello\e[0m", 'hello'.underline(:curly, :red)
362
+ assert_equal "\e[4:2;58;5;4mhello\e[0m", 'hello'.underline(:double, :blue)
363
+
364
+ # Swapped order: color first, style second
365
+ assert_equal "\e[4:3;58;5;1mhello\e[0m", 'hello'.underline(:red, :curly)
366
+ assert_equal "\e[4:2;58;5;4mhello\e[0m", 'hello'.underline(:blue, :double)
367
+ end
222
368
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ansi256
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Junegunn Choi
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  - !ruby/object:Gem::Version
90
90
  version: '0'
91
91
  requirements: []
92
- rubygems_version: 3.6.9
92
+ rubygems_version: 4.0.16
93
93
  specification_version: 4
94
94
  summary: Colorize text using 256-color ANSI codes
95
95
  test_files: