ansi256 0.4.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: cf28c245bf8d2c7a888e05572ea1270a590bd2c0a906e37171099388ff4eb3a3
4
- data.tar.gz: 1e5e1531cd270090d5ccaf931774f4d0cc54afbe2f0e1ba2d298829f5c5de651
3
+ metadata.gz: 5390f3352df81912c5dfb618a66256f8f41b21fab087748185b313a7f09913b3
4
+ data.tar.gz: a8bf49a8391b3b5221611026505fa27451a5a683526763776ad0d9344bf4f7f8
5
5
  SHA512:
6
- metadata.gz: a4209529d49b0ce7ef7acdbfd7072264840c276502281bef42944fe07609828ca02de4b3542d4fafc68f0cfaeb7ef17ddfef24db5fcdcd5e2b12f13caf1723f6
7
- data.tar.gz: 59462bdc1e6c8072957674d8fcf7548a0eb20c76cf7021f82a6bffdb93befd7012704991f9190cac4b7587bd27eb1b9b62142a7246d9176cd0e07c1ac5a16686
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
@@ -1,28 +1,38 @@
1
1
  module Ansi256
2
2
  CODE = {
3
- :reset => 0,
4
- :bold => 1,
5
- :dim => 2,
6
- :italic => 3,
7
- :underline => 4,
3
+ reset: 0,
4
+ bold: 1,
5
+ dim: 2,
6
+ italic: 3,
7
+ underline: 4,
8
+ inverse: 7,
9
+ strikethrough: 9,
8
10
 
9
- :black => 30,
10
- :red => 31,
11
- :green => 32,
12
- :yellow => 33,
13
- :blue => 34,
14
- :magenta => 35,
15
- :cyan => 36,
16
- :white => 37,
11
+ black: 30,
12
+ red: 31,
13
+ green: 32,
14
+ yellow: 33,
15
+ blue: 34,
16
+ magenta: 35,
17
+ cyan: 36,
18
+ white: 37,
17
19
 
18
- :on_black => 40,
19
- :on_red => 41,
20
- :on_green => 42,
21
- :on_yellow => 43,
22
- :on_blue => 44,
23
- :on_magenta => 45,
24
- :on_cyan => 46,
25
- :on_white => 47,
20
+ on_black: 40,
21
+ on_red: 41,
22
+ on_green: 42,
23
+ on_yellow: 43,
24
+ on_blue: 44,
25
+ on_magenta: 45,
26
+ on_cyan: 46,
27
+ on_white: 47
28
+ }.freeze
29
+
30
+ UNDERLINE_STYLES = {
31
+ single: '4:1',
32
+ double: '4:2',
33
+ curly: '4:3',
34
+ dotted: '4:4',
35
+ dashed: '4:5',
26
36
  }.freeze
27
37
 
28
38
  NAMED_COLORS = Set[
data/lib/ansi256/mixin.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ansi256
2
4
  class << self
3
5
  def enabled= bool
@@ -25,6 +27,12 @@ module Ansi256
25
27
  Ansi256.send name, self
26
28
  end
27
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
28
36
  end
29
37
 
30
38
  Ansi256.class_eval do
@@ -56,6 +64,12 @@ module Ansi256
56
64
  self
57
65
  end
58
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
59
73
  end
60
74
 
61
75
  Ansi256.class_eval do
@@ -72,4 +86,3 @@ module Ansi256
72
86
  end
73
87
  end
74
88
  end
75
-
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ansi256
2
- VERSION = "0.4.0"
4
+ VERSION = '0.6.0'
3
5
  end
data/lib/ansi256.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
  require 'ansi256/version'
3
5
  require 'ansi256/code'
@@ -5,29 +7,39 @@ require 'ansi256/mixin'
5
7
 
6
8
  module Ansi256
7
9
  class << self
8
- def fg code, str = nil
10
+ attr_accessor :truecolor
11
+
12
+ def fg(code, str = nil)
9
13
  if str
10
14
  wrap str, Ansi256.fg(code)
11
15
  elsif NAMED_COLORS.include?(code)
12
16
  "\e[#{CODE[code]}m"
13
17
  elsif code.is_a?(Integer) && (0..255).include?(code)
14
18
  "\e[38;5;#{code}m"
15
- elsif ansirgb = ansicode_for_rgb(code)
16
- "\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
17
25
  else
18
26
  raise ArgumentError, "Invalid color code: #{code}"
19
27
  end
20
28
  end
21
29
 
22
- def bg code, str = nil
30
+ def bg(code, str = nil)
23
31
  if str
24
32
  wrap str, Ansi256.bg(code)
25
33
  elsif NAMED_COLORS.include?(code)
26
34
  "\e[#{CODE[code] + 10}m"
27
35
  elsif code.is_a?(Integer) && (0..255).include?(code)
28
36
  "\e[48;5;#{code}m"
29
- elsif ansirgb = ansicode_for_rgb(code)
30
- "\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
31
43
  else
32
44
  raise ArgumentError, "Invalid color code: #{code}"
33
45
  end
@@ -43,94 +55,232 @@ module Ansi256
43
55
  end
44
56
  end
45
57
 
46
- def plain str
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
+
67
+ def plain(str)
47
68
  str.gsub(PATTERN, '')
48
69
  end
49
70
 
50
- private
51
- PATTERN = /\e\[[0-9;]+m/.freeze
52
- MULTI_PATTERN = /(?:\e\[[0-9;]+m)+/.freeze
53
- EMPTY_TRIPLE = [nil, nil, Set.new].freeze
71
+ private
72
+
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
54
83
 
55
- def ansify prev, curr
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
132
+
133
+ def ansify(prev, curr)
56
134
  nums = []
57
135
  nums << curr[0] if prev[0] != curr[0]
58
136
  nums << curr[1] if prev[1] != curr[1]
59
- 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]
60
140
  "\e[#{nums.compact.join ';'}m"
61
141
  end
62
142
 
63
- def ansicode_for_rgb rgb
64
- return unless rgb.is_a?(String) &&
65
- rgb =~ /^#?([0-9a-f]+)$/i
66
- rgb = $1
143
+ def parse_rgb(code)
144
+ return unless code.is_a?(String) &&
145
+ code =~ /^#?([0-9a-f]+)$/i
146
+
147
+ hex = ::Regexp.last_match(1)
67
148
 
68
- case rgb.length
149
+ case hex.length
69
150
  when 2
70
- m = (256 - 231) * rgb.to_i(16) / 256
71
- return m == 0 ? 16 : (231 + m)
151
+ v = hex.to_i(16)
152
+ [v, v, v]
72
153
  when 3
73
- r, g, b = rgb.each_char.map { |e| (e * 2).to_i(16) }
154
+ hex.each_char.map { |e| (e * 2).to_i(16) }
74
155
  when 6
75
- r, g, b = rgb.each_char.each_slice(2).map { |e| e.join.to_i(16) }
76
- else
77
- return
156
+ hex.each_char.each_slice(2).map { |e| e.join.to_i(16) }
78
157
  end
158
+ end
79
159
 
160
+ def rgb_to_ansi256(r, g, b)
80
161
  r, g, b = [r, g, b].map { |e| 6 * e / 256 }
81
162
  r * 36 + g * 6 + b + 16
82
163
  end
83
164
 
84
- def wrap str, color
85
- current = [nil, nil, Set.new]
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
+
183
+ def wrap(str, color)
184
+ current = [nil, nil, nil, nil, Set.new]
86
185
 
87
- (color + str.gsub(PATTERN) { |m|
88
- if m =~ /\e\[[^m]*\b0m/
186
+ (color + str.gsub(PATTERN) do |m|
187
+ if m == "\e[0m"
89
188
  m + color
90
189
  else
91
190
  m
92
191
  end
93
- } << reset).gsub(MULTI_PATTERN) { |ansi|
192
+ end << reset).gsub(MULTI_PATTERN) do |ansi|
94
193
  prev = current.dup
95
- prev[2] = prev[2].dup
96
- codes = ansi.scan(/\d+/).map(&:to_i)
97
-
98
- idx = -1
99
- while (idx += 1) < codes.length
100
- case code = codes[idx]
101
- when 38
102
- current[0] = codes[idx, 3].join ';'
103
- idx += 2 # 38;5;11
104
- when 48
105
- current[1] = codes[idx, 3].join ';'
106
- idx += 2 # 38;5;11
107
- when 30..37
108
- current[0] = codes[idx]
109
- when 40..47
110
- current[1] = codes[idx]
111
- when 0
112
- current[0] = current[1] = nil
113
- current[2].clear
114
- else
115
- 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
116
265
  end
117
266
  end
118
267
 
119
268
  if current == prev
120
269
  ''
121
- elsif current == EMPTY_TRIPLE
270
+ elsif current == EMPTY_STATE
122
271
  reset
123
272
  else
124
- if (0..1).any? { |i| prev[i] && !current[i] } || current[2].proper_subset?(prev[2])
125
- prev = EMPTY_TRIPLE
273
+ if (0..3).any? { |i| prev[i] && !current[i] } || current[4].proper_subset?(prev[4])
274
+ prev = EMPTY_STATE
126
275
  reset
127
276
  else
128
277
  ''
129
278
  end + ansify(prev, current)
130
279
  end
131
- }
280
+ end
132
281
  end
133
282
  end
134
283
  end
135
284
 
136
285
  Ansi256.enabled = true
286
+ Ansi256.truecolor = true
data/test/test_ansi256.rb CHANGED
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  $VERBOSE = true
2
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
3
5
  require 'ansi256'
4
6
  require 'minitest/autorun'
5
7
 
6
8
  class TestAnsi256 < Minitest::Test
7
- def cfmt col
9
+ def cfmt(col)
8
10
  col.to_s.rjust(5).fg(232).bg(col)
9
11
  end
10
12
 
@@ -28,40 +30,40 @@ class TestAnsi256 < Minitest::Test
28
30
 
29
31
  def test_nesting_hello_world
30
32
  # Nesting
31
- puts world = "World".bg(226).fg(232).underline
33
+ puts world = 'World'.bg(226).fg(232).underline
32
34
  puts hello = "Hello #{world} !".fg(230).bg(75)
33
35
  puts say_hello_world = "Say '#{hello}'".fg(30)
34
36
  puts say_hello_world.plain.fg(27)
35
37
  end
36
38
 
37
39
  def test_nesting_hello_world2
38
- puts world = "World".bg(226).blue.underline
40
+ puts world = 'World'.bg(226).blue.underline
39
41
  puts hello = "Hello #{world} Hello".white.bold
40
42
  puts say_hello_world = "Say '#{hello}'".fg(30).underline
41
43
  puts say_hello_world.plain.fg(27)
42
44
  end
43
45
 
44
46
  def test_nesting_hello_world3
45
- puts world = "World".blue.underline
47
+ puts world = 'World'.blue.underline
46
48
  puts hello = "Hello #{world} Hello".blue.bold
47
49
  puts say_hello_world = "Say '#{hello}'".fg(30).underline
48
50
  puts say_hello_world.plain.fg(27)
49
51
  end
50
52
 
51
53
  def test_named_colors
52
- s = "Colorize me"
53
- puts [ s.black, s.black.bold, s.white.bold.on_black ].join ' '
54
- puts [ s.red, s.red.bold, s.white.bold.on_red ].join ' '
55
- puts [ s.green, s.green.bold, s.white.bold.on_green ].join ' '
56
- puts [ s.yellow, s.yellow.bold, s.white.bold.on_yellow ].join ' '
57
- puts [ s.blue, s.blue.bold, s.white.bold.on_blue ].join ' '
58
- puts [ s.magenta, s.magenta.bold, s.white.bold.on_magenta ].join ' '
59
- puts [ s.cyan, s.cyan.bold, s.white.bold.on_cyan ].join ' '
60
- puts [ s.white, s.white.bold, s.white.bold.on_white ].join ' '
54
+ s = 'Colorize me'
55
+ puts [s.black, s.black.bold, s.white.bold.on_black].join ' '
56
+ puts [s.red, s.red.bold, s.white.bold.on_red].join ' '
57
+ puts [s.green, s.green.bold, s.white.bold.on_green].join ' '
58
+ puts [s.yellow, s.yellow.bold, s.white.bold.on_yellow].join ' '
59
+ puts [s.blue, s.blue.bold, s.white.bold.on_blue].join ' '
60
+ puts [s.magenta, s.magenta.bold, s.white.bold.on_magenta].join ' '
61
+ puts [s.cyan, s.cyan.bold, s.white.bold.on_cyan].join ' '
62
+ puts [s.white, s.white.bold, s.white.bold.on_white].join ' '
61
63
  end
62
64
 
63
65
  def test_named_color_code_with_fg_bg
64
- puts "Colorize me".fg(:green).bg(:red).bold.underline
66
+ puts 'Colorize me'.fg(:green).bg(:red).bold.underline
65
67
  end
66
68
 
67
69
  def test_just_code
@@ -70,6 +72,8 @@ class TestAnsi256 < Minitest::Test
70
72
  assert_equal "\e[2m", Ansi256.dim
71
73
  assert_equal "\e[3m", Ansi256.italic
72
74
  assert_equal "\e[4m", Ansi256.underline
75
+ assert_equal "\e[7m", Ansi256.inverse
76
+ assert_equal "\e[9m", Ansi256.strikethrough
73
77
 
74
78
  assert_equal "\e[30m", Ansi256.black
75
79
  assert_equal "\e[31m", Ansi256.red
@@ -130,21 +134,100 @@ class TestAnsi256 < Minitest::Test
130
134
 
131
135
  assert_equal "\e[38;5;100mHello\e[38;5;200m world\e[0m", "#{'Hello'.fg(100)} world".fg(200)
132
136
  assert_equal "\e[38;5;200;4mWow \e[38;5;100mhello\e[38;5;200m world\e[0m",
133
- "Wow #{'hello'.fg(100)} world".fg(200).underline
137
+ "Wow #{'hello'.fg(100)} world".fg(200).underline
134
138
  assert_equal "\e[38;5;200mWow \e[38;5;100;4mhello\e[0m\e[38;5;200m world\e[0m",
135
- "Wow #{'hello'.fg(100).underline} world".fg(200)
139
+ "Wow #{'hello'.fg(100).underline} world".fg(200)
136
140
  assert_equal "\e[38;5;200mWow \e[38;5;100;48;5;50;4mhello\e[0m\e[38;5;200m world\e[0m",
137
- "Wow #{'hello'.fg(100).underline.bg(50)} world".fg(200)
141
+ "Wow #{'hello'.fg(100).underline.bg(50)} world".fg(200)
138
142
  assert_equal "\e[38;5;200;48;5;250mWow \e[38;5;100;48;5;50;4mhello\e[0m\e[38;5;200;48;5;250m world\e[0m",
139
- "Wow #{'hello'.fg(100).underline.bg(50)} world".fg(200).bg(250)
140
- assert_equal "Wow hello world",
141
- "Wow #{'hello'.fg(100).underline.bg(50)} world".fg(200).bg(250).plain
143
+ "Wow #{'hello'.fg(100).underline.bg(50)} world".fg(200).bg(250)
144
+ assert_equal 'Wow hello world',
145
+ "Wow #{'hello'.fg(100).underline.bg(50)} world".fg(200).bg(250).plain
142
146
 
143
147
  assert_equal "\e[32;48;5;200;1mWow \e[38;5;100;44;1;4mhello\e[0m\e[32;48;5;200;1m world\e[0m",
144
- "Wow #{'hello'.fg(100).underline.on_blue} world".green.bold.bg(200)
148
+ "Wow #{'hello'.fg(100).underline.on_blue} world".green.bold.bg(200)
145
149
  end
146
150
 
147
151
  def test_rgb
152
+ # Truecolor mode (default): 38;2;R;G;B
153
+ {
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
+ 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
185
+ %i[bg fg].each do |m|
186
+ (0..255).each do |r|
187
+ color = r.to_s(16).rjust(2, '0') * 3
188
+ print Ansi256.send(m, color, color + ' ')
189
+ end
190
+ (0..255).each do |r|
191
+ color = r.to_s(16).rjust(2, '0')
192
+ print Ansi256.send(m, color, color + ' ')
193
+ end
194
+ end
195
+ 0.step(255, 20) do |r|
196
+ 0.step(255, 20) do |g|
197
+ 0.step(255, 20) do |b|
198
+ color = [r, g, b].map { |c| c.to_s(16).rjust(2, '0') }.join
199
+ print Ansi256.bg(color, color + ' ')
200
+ end
201
+ end
202
+ end
203
+ puts 'RGB Color'.fg('ff9900').bg('003366')
204
+ puts 'RGB Color'.fg('f90').bg('#036')
205
+ puts 'RGB Color'.fg('#ff9900').bg('#003366')
206
+ puts 'RGB Color (Monochrome)'.fg('ef').bg('3a')
207
+ end
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
148
231
  {
149
232
  '00' => 16,
150
233
  '000000' => 16,
@@ -157,7 +240,6 @@ class TestAnsi256 < Minitest::Test
157
240
  '22' => 234,
158
241
  '202020' => 16,
159
242
  '222222' => 16,
160
- '222222' => 16,
161
243
  'ff' => 255,
162
244
  'ffffff' => 231,
163
245
  'FFFFFF' => 231,
@@ -173,46 +255,114 @@ class TestAnsi256 < Minitest::Test
173
255
  assert_equal ansi, Ansi256.fg(rgb).scan(/[0-9]+/).last.to_i
174
256
  assert_equal ansi, Ansi256.bg(rgb).scan(/[0-9]+/).last.to_i
175
257
  end
176
- [:bg, :fg].each do |m|
177
- (0..255).each do |r|
178
- color = r.to_s(16).rjust(2, '0') * 3
179
- print Ansi256.send(m, color, color + ' ')
180
- end
181
- (0..255).each do |r|
182
- color = r.to_s(16).rjust(2, '0')
183
- print Ansi256.send(m, color, color + ' ')
184
- end
185
- end
186
- 0.step(255, 20) do |r|
187
- 0.step(255, 20) do |g|
188
- 0.step(255, 20) do |b|
189
- color = [r, g, b].map { |c| c.to_s(16).rjust(2, '0') }.join
190
- print Ansi256.bg(color, color + ' ')
191
- end
192
- end
193
- end
194
- puts "RGB Color".fg('ff9900').bg('003366')
195
- puts "RGB Color".fg('f90').bg('#036')
196
- puts "RGB Color".fg('#ff9900').bg('#003366')
197
- puts "RGB Color (Monochrome)".fg('ef').bg('3a')
258
+ ensure
259
+ Ansi256.truecolor = true
198
260
  end
199
261
 
200
262
  def test_enabled
201
263
  2.times do
202
- [:fg, :bg].each do |m|
264
+ %i[fg bg].each do |m|
203
265
  assert Ansi256.enabled?
204
- hello = "hello".send(m, 'f90')
266
+ hello = 'hello'.send(m, 'f90')
205
267
  assert_equal 'hello', hello.plain
206
268
 
207
269
  Ansi256.enabled = false
208
270
  assert !Ansi256.enabled?
209
- assert hello.length > "hello".send(m, 'f90').length
271
+ assert hello.length > 'hello'.send(m, 'f90').length
210
272
  assert_equal 'hello', hello.plain
211
273
 
212
274
  Ansi256.enabled = true
213
275
  assert Ansi256.enabled?
214
- assert_equal hello, "hello".send(m, 'f90')
276
+ assert_equal hello, 'hello'.send(m, 'f90')
215
277
  end
216
278
  end
217
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
218
368
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ansi256
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Junegunn Choi
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler
@@ -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.2
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: