ansi256 0.5.0 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e0c06de0d942deeb5906752ed223248b699408b93a2bba463bbb848d6b06aea
4
- data.tar.gz: a7ea18e9b3902806d43fe46e7b388c8d0152e4e0e365bdb552ad35d11064b12a
3
+ metadata.gz: 5390f3352df81912c5dfb618a66256f8f41b21fab087748185b313a7f09913b3
4
+ data.tar.gz: a8bf49a8391b3b5221611026505fa27451a5a683526763776ad0d9344bf4f7f8
5
5
  SHA512:
6
- metadata.gz: 8d10be011d96e91b58ccfef63608a466052dfaca7bd25e05ee1c6b83cabc6deaeeefc1e0de4e9f99b19267714f0b88a04b42bac031fe4c5937fc39b9c078c043
7
- data.tar.gz: b15f5eab61b1f5be118a69bd9cdbd5972a7ea0adc13a51e462665aaf282c71b4967d917d6ebbc8b17c7e6eb97ecbdae76e2a1d7349ef014330177926ff4829b6
6
+ metadata.gz: 2078048aa2d6064a9f3886de024c485c12e266dc9bc37f0fc69ea95be98e6371e2e3e183c3ca357fc1fcc6a543070217cdc109b2c81d973f583e444f318af8e1
7
+ data.tar.gz: 9c7011943787d75ee81005820f1521317c869aa922136d5c6d0be5fc50070ddcb87ad324f698cf1c7a54656b5584505d706b3b994f06586f99ed683f47f519b9
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.0'
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,8 +34,12 @@ 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
@@ -45,88 +55,223 @@ module Ansi256
45
55
  end
46
56
  end
47
57
 
58
+ # Override underline to support styles and colors
59
+ def underline(str = nil, *props)
60
+ if str.nil?
61
+ build_underline_sgr(*props)
62
+ else
63
+ wrap str, build_underline_sgr(*props)
64
+ end
65
+ end
66
+
48
67
  def plain(str)
49
68
  str.gsub(PATTERN, '')
50
69
  end
51
70
 
52
71
  private
53
72
 
54
- PATTERN = /\e\[[0-9;]+m/
55
- MULTI_PATTERN = /(?:\e\[[0-9;]+m)+/
56
- EMPTY_TRIPLE = [nil, nil, Set.new].freeze
73
+ PATTERN = /\e\[[0-9;:]+m/
74
+ MULTI_PATTERN = /(?:\e\[[0-9;:]+m)+/
75
+ # State: [fg, bg, ul_style, ul_color, attrs_set]
76
+ # ul_style is only for sub-parameter styles (4:1, 4:2, etc.)
77
+ # Basic underline (4) stays in attrs_set for backward compat
78
+ EMPTY_STATE = [nil, nil, nil, nil, Set.new].freeze
79
+
80
+ def build_underline_sgr(*args)
81
+ style = nil
82
+ color = nil
83
+
84
+ args.each do |arg|
85
+ case arg
86
+ when nil
87
+ next
88
+ when Symbol
89
+ if UNDERLINE_STYLES[arg]
90
+ style = arg
91
+ elsif NAMED_COLORS.include?(arg)
92
+ color = arg
93
+ else
94
+ raise ArgumentError, "Invalid underline argument: #{arg}"
95
+ end
96
+ when Integer, String
97
+ color = arg
98
+ else
99
+ raise ArgumentError, "Invalid underline argument: #{arg}"
100
+ end
101
+ end
102
+
103
+ return "\e[4m" unless style || color
104
+
105
+ parts = []
106
+ parts << (style ? UNDERLINE_STYLES[style] : '4')
107
+ parts << underline_color_code(color) if color
108
+
109
+ "\e[#{parts.join(';')}m"
110
+ end
111
+
112
+ def underline_color_code(color)
113
+ case color
114
+ when Symbol
115
+ raise ArgumentError, "Invalid underline color: #{color}" unless NAMED_COLORS.include?(color)
116
+ "58;5;#{CODE[color] - 30}"
117
+ when Integer
118
+ raise ArgumentError, "Invalid underline color: #{color}" unless (0..255).include?(color)
119
+ "58;5;#{color}"
120
+ when String
121
+ rgb = parse_rgb(color)
122
+ raise ArgumentError, "Invalid underline color: #{color}" unless rgb
123
+ if truecolor
124
+ "58;2;#{rgb.join(';')}"
125
+ else
126
+ "58;5;#{ansicode_for_rgb(color)}"
127
+ end
128
+ else
129
+ raise ArgumentError, "Invalid underline color: #{color}"
130
+ end
131
+ end
57
132
 
58
133
  def ansify(prev, curr)
59
134
  nums = []
60
135
  nums << curr[0] if prev[0] != curr[0]
61
136
  nums << curr[1] if prev[1] != curr[1]
62
- nums.concat curr[2].to_a if prev[2] != curr[2]
137
+ nums << curr[2] if prev[2] != curr[2]
138
+ nums << curr[3] if prev[3] != curr[3]
139
+ nums.concat curr[4].to_a if prev[4] != curr[4]
63
140
  "\e[#{nums.compact.join ';'}m"
64
141
  end
65
142
 
66
- def ansicode_for_rgb(rgb)
67
- return unless rgb.is_a?(String) &&
68
- rgb =~ /^#?([0-9a-f]+)$/i
143
+ def parse_rgb(code)
144
+ return unless code.is_a?(String) &&
145
+ code =~ /^#?([0-9a-f]+)$/i
69
146
 
70
- rgb = ::Regexp.last_match(1)
147
+ hex = ::Regexp.last_match(1)
71
148
 
72
- case rgb.length
149
+ case hex.length
73
150
  when 2
74
- m = (256 - 231) * rgb.to_i(16) / 256
75
- return m == 0 ? 16 : (231 + m)
151
+ v = hex.to_i(16)
152
+ [v, v, v]
76
153
  when 3
77
- r, g, b = rgb.each_char.map { |e| (e * 2).to_i(16) }
154
+ hex.each_char.map { |e| (e * 2).to_i(16) }
78
155
  when 6
79
- r, g, b = rgb.each_char.each_slice(2).map { |e| e.join.to_i(16) }
80
- else
81
- return
156
+ hex.each_char.each_slice(2).map { |e| e.join.to_i(16) }
82
157
  end
158
+ end
83
159
 
160
+ def rgb_to_ansi256(r, g, b)
84
161
  r, g, b = [r, g, b].map { |e| 6 * e / 256 }
85
162
  r * 36 + g * 6 + b + 16
86
163
  end
87
164
 
165
+ def ansicode_for_rgb(code)
166
+ return unless code.is_a?(String) &&
167
+ code =~ /^#?([0-9a-f]+)$/i
168
+
169
+ hex = ::Regexp.last_match(1)
170
+
171
+ # 2-char monochrome uses grayscale ramp (232-255)
172
+ if hex.length == 2
173
+ m = 25 * hex.to_i(16) / 256
174
+ return m == 0 ? 16 : (231 + m)
175
+ end
176
+
177
+ rgb = parse_rgb(code)
178
+ return unless rgb
179
+
180
+ rgb_to_ansi256(*rgb)
181
+ end
182
+
88
183
  def wrap(str, color)
89
- current = [nil, nil, Set.new]
184
+ current = [nil, nil, nil, nil, Set.new]
90
185
 
91
186
  (color + str.gsub(PATTERN) do |m|
92
- if m =~ /\e\[[^m]*\b0m/
187
+ if m == "\e[0m"
93
188
  m + color
94
189
  else
95
190
  m
96
191
  end
97
192
  end << reset).gsub(MULTI_PATTERN) do |ansi|
98
193
  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
194
+ prev[4] = prev[4].dup
195
+
196
+ # Extract individual SGR sequences and parse each
197
+ seqs = ansi.scan(/\e\[([^\e]*?)m/).map { |cap| cap[0] }
198
+ seqs.each do |seq|
199
+ # Split by semicolon, preserving colon sub-params within groups
200
+ groups = seq.split(';')
201
+
202
+ idx = -1
203
+ while (idx += 1) < groups.length
204
+ group = groups[idx]
205
+
206
+ if group.include?(':')
207
+ # Sub-parameter group (e.g., "4:3")
208
+ main, sub = group.split(':', 2)
209
+ main_i = main.to_i
210
+ if main_i == 4
211
+ if sub == '0'
212
+ current[2] = nil
213
+ current[4].delete(4)
214
+ else
215
+ current[2] = group
216
+ current[4].delete(4)
217
+ end
218
+ end
219
+ else
220
+ code = group.to_i
221
+ case code
222
+ when 38
223
+ if groups[idx + 1] == '2'
224
+ current[0] = groups[idx, 5].join(';')
225
+ idx += 4
226
+ else
227
+ current[0] = groups[idx, 3].join(';')
228
+ idx += 2
229
+ end
230
+ when 48
231
+ if groups[idx + 1] == '2'
232
+ current[1] = groups[idx, 5].join(';')
233
+ idx += 4
234
+ else
235
+ current[1] = groups[idx, 3].join(';')
236
+ idx += 2
237
+ end
238
+ when 58
239
+ if groups[idx + 1] == '2'
240
+ current[3] = groups[idx, 5].join(';')
241
+ idx += 4
242
+ else
243
+ current[3] = groups[idx, 3].join(';')
244
+ idx += 2
245
+ end
246
+ when 30..37
247
+ current[0] = code
248
+ when 40..47
249
+ current[1] = code
250
+ when 0
251
+ current[0] = current[1] = current[2] = current[3] = nil
252
+ current[4].clear
253
+ when 24
254
+ current[2] = nil
255
+ current[4].delete(4)
256
+ when 59
257
+ current[3] = nil
258
+ when 4
259
+ # Basic underline goes in attrs set for backward compat
260
+ current[4] << 4
261
+ else
262
+ current[4] << code
263
+ end
264
+ end
120
265
  end
121
266
  end
122
267
 
123
268
  if current == prev
124
269
  ''
125
- elsif current == EMPTY_TRIPLE
270
+ elsif current == EMPTY_STATE
126
271
  reset
127
272
  else
128
- if (0..1).any? { |i| prev[i] && !current[i] } || current[2].proper_subset?(prev[2])
129
- prev = EMPTY_TRIPLE
273
+ if (0..3).any? { |i| prev[i] && !current[i] } || current[4].proper_subset?(prev[4])
274
+ prev = EMPTY_STATE
130
275
  reset
131
276
  else
132
277
  ''
@@ -138,3 +283,4 @@ module Ansi256
138
283
  end
139
284
 
140
285
  Ansi256.enabled = true
286
+ 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.0
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.3
93
93
  specification_version: 4
94
94
  summary: Colorize text using 256-color ANSI codes
95
95
  test_files: