simplecolor 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE.txt +1 -1
- data/README.md +11 -7
- data/Rakefile +7 -10
- data/bin/simplecolor.rb +218 -0
- data/data/NBS-ISCC_colors.json +1 -0
- data/data/Resene2010_colors.json +1 -0
- data/data/rgb_colors.json.gz +0 -0
- data/data/rgb_colors.rb +90 -0
- data/data/x11_colors.json +1 -0
- data/data/xkcd_colors.json +1 -0
- data/gemspec.yml +2 -0
- data/lib/simplecolor.rb +166 -135
- data/lib/simplecolor/colorer.rb +228 -0
- data/lib/simplecolor/colors.rb +63 -101
- data/lib/simplecolor/mixin.rb +2 -0
- data/lib/simplecolor/rgb.rb +318 -0
- data/lib/simplecolor/rgb_colors.rb +111 -0
- data/lib/simplecolor/version.rb +1 -1
- data/simplecolor.gemspec +2 -0
- data/test/test_rgb.rb +103 -0
- data/test/test_simplecolor.rb +192 -7
- metadata +18 -5
@@ -0,0 +1,228 @@
|
|
1
|
+
module SimpleColor
|
2
|
+
ColorerError=Class.new(SimpleColorError)
|
3
|
+
WrongColor=Class.new(ColorerError)
|
4
|
+
WrongParameter=Class.new(ColorerError)
|
5
|
+
|
6
|
+
# The Colorer module handle all color outputs
|
7
|
+
module Colorer
|
8
|
+
extend self
|
9
|
+
def colors
|
10
|
+
COLORS
|
11
|
+
end
|
12
|
+
# A color name can be:
|
13
|
+
# - an array of rgb data (truecolor)
|
14
|
+
# (start with :on to specify background)
|
15
|
+
# - a String:
|
16
|
+
# rgb:10-20-30 (foreground truecolor)
|
17
|
+
# on_rgb:10-20-30 (background truecolor)
|
18
|
+
# t:rgb... (don't fallback to lower color mode)
|
19
|
+
# (on_)rgb256:r:g:b (force 256 color mode)
|
20
|
+
# (on_)rgb256:grey3 (256 grey scale)
|
21
|
+
# (on_)rgb256:5 (direct color code)
|
22
|
+
# (t:)(on_)#AABBCC (hex code, truecolor)
|
23
|
+
# (t:)(on_)#ABC (reduced hex code, truecolor)
|
24
|
+
# (t:)(on_)name (X11 color name, truecolor)
|
25
|
+
# A color attribute can be:
|
26
|
+
# - a symbol (looked in at COLORS)
|
27
|
+
# - an integer (direct color code)
|
28
|
+
# - a color escape sequence
|
29
|
+
# - a String
|
30
|
+
def color_attributes(*args, mode: :text, color_mode: :truecolor, abbreviations: {}, rgb_class: RGB, **rgb_parse_opts)
|
31
|
+
return "" if mode==:disabled or mode==false #early abort
|
32
|
+
abbreviations={} if abbreviations.nil?
|
33
|
+
colors=self.colors
|
34
|
+
accu=[]
|
35
|
+
buffer=""
|
36
|
+
flush=lambda {r=accu.join(";"); accu=[]; r.empty? || r="\e["+r+"m"; buffer<<r} #Note: "\e"="\x1b"
|
37
|
+
|
38
|
+
parse_rgb=lambda do |s|
|
39
|
+
symbol=s.is_a?(Symbol)
|
40
|
+
truecol=/(?<truecol>(?:truecolor|true|t):)?/
|
41
|
+
on=/(?<on>on_)?/
|
42
|
+
s.match(/\A#{truecol}#{on}(?<rest>.*)\z/) do |m|
|
43
|
+
tcol=m[:truecol]; on=m[:on]; string=m[:rest]
|
44
|
+
lcolormode = tcol ? :truecolor : color_mode
|
45
|
+
string=string.to_sym if symbol
|
46
|
+
accu << rgb_class.parse(string, **rgb_parse_opts).ansi(background: !!on, convert: lcolormode)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
parse=lambda do |*cols, abbrevs: abbreviations|
|
51
|
+
cols.each do |col|
|
52
|
+
if (scol=abbrevs[col])
|
53
|
+
# Array are special, in a non abbreviation they mean an rgb mode but for abbreviations it just combine several color attributes
|
54
|
+
parse.call(*scol, abbrevs: {}) #we erase abbreviations so :red = :red do not get an infinite loop
|
55
|
+
next
|
56
|
+
end
|
57
|
+
case col
|
58
|
+
when Proc
|
59
|
+
scol=col.call(buffer, accu)
|
60
|
+
parse.call(*scol)
|
61
|
+
when Symbol
|
62
|
+
if colors.key?(col) #ANSI effects
|
63
|
+
accu << colors[col]
|
64
|
+
else
|
65
|
+
parse_rgb.call(col)
|
66
|
+
end
|
67
|
+
when Integer #direct ansi code
|
68
|
+
accu << col.to_s
|
69
|
+
when Array
|
70
|
+
background=false
|
71
|
+
if col.first == :on
|
72
|
+
background=true; col.shift
|
73
|
+
end
|
74
|
+
accu << rgb_class.new(col).ansi(convert: color_mode, background: background)
|
75
|
+
when rgb_class
|
76
|
+
accu << col.ansi(convert: color_mode)
|
77
|
+
when COLOR_REGEXP
|
78
|
+
flush.call
|
79
|
+
buffer<<col
|
80
|
+
when String
|
81
|
+
parse_rgb.call(col)
|
82
|
+
when nil # skip
|
83
|
+
else
|
84
|
+
raise WrongColor.new(col)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
parse.call(*args)
|
90
|
+
flush.call
|
91
|
+
|
92
|
+
case mode
|
93
|
+
when :shell
|
94
|
+
"%{"+buffer+"%}"
|
95
|
+
when :disabled
|
96
|
+
"" # already handled above
|
97
|
+
when :text, :enabled, true
|
98
|
+
buffer
|
99
|
+
else
|
100
|
+
raise WrongParameter.new(mode)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def regexp(type=:color, mode: :text, **_rest)
|
105
|
+
case type
|
106
|
+
when :color
|
107
|
+
if mode == :shell
|
108
|
+
m=regexp(:ansi, mode: mode)
|
109
|
+
/#{m}+/
|
110
|
+
else
|
111
|
+
COLOR_REGEXP
|
112
|
+
end
|
113
|
+
when :match
|
114
|
+
if mode == :shell
|
115
|
+
m=regexp(:ansi, mode: mode)
|
116
|
+
/#{m}*/
|
117
|
+
else
|
118
|
+
COLORMATCH_REGEXP
|
119
|
+
end
|
120
|
+
when :ansi
|
121
|
+
if mode == :shell
|
122
|
+
/%{#{ANSICOLOR_REGEXP}%}/
|
123
|
+
else
|
124
|
+
ANSICOLOR_REGEXP
|
125
|
+
end
|
126
|
+
when :clear
|
127
|
+
if mode == :shell
|
128
|
+
/%{#{CLEAR_REGEXP}%}/
|
129
|
+
else
|
130
|
+
CLEAR_REGEXP
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns a colored version of the string (modified in place),
|
136
|
+
# according to attributes
|
137
|
+
def colorer(s, *attributes, global_color: :after, local_color: :before, **kwds)
|
138
|
+
if s.nil?
|
139
|
+
color_attributes(*attributes,**kwds)
|
140
|
+
elsif s.empty?
|
141
|
+
# We don't color an empty string; use nil to get color attributes
|
142
|
+
s
|
143
|
+
else
|
144
|
+
match_reg=regexp(:match, **kwds)
|
145
|
+
color_reg=regexp(:color, **kwds)
|
146
|
+
clear_reg=regexp(:clear, **kwds)
|
147
|
+
colors=color_attributes(*attributes,**kwds)
|
148
|
+
clear=color_attributes(:clear,**kwds)
|
149
|
+
|
150
|
+
split=SimpleColor.color_strings(s, color_regexp: color_reg)
|
151
|
+
sp, i=split.each_with_index.find do |sp, i|
|
152
|
+
i>=1 && sp.match(/#{color_reg}/)
|
153
|
+
end
|
154
|
+
global=true unless sp and (i<split.length-1 || !sp.match(/#{clear_reg}$/))
|
155
|
+
|
156
|
+
if global
|
157
|
+
case global_color
|
158
|
+
when :keep
|
159
|
+
matched = s.match(match_reg)
|
160
|
+
s.insert(0, colors) unless matched.end(0)>0
|
161
|
+
when :before
|
162
|
+
s.insert(0, colors)
|
163
|
+
when :after
|
164
|
+
# we need to insert the ANSI sequences after existing ones so that
|
165
|
+
# the new colors have precedence
|
166
|
+
matched = s.match(match_reg) #since this has a '*' it matches at the beginning
|
167
|
+
s.insert(matched.end(0), colors)
|
168
|
+
end
|
169
|
+
else
|
170
|
+
pos=0
|
171
|
+
split.each_with_index do |sp,i|
|
172
|
+
if sp.match(/#{clear_reg}$/)
|
173
|
+
#don't reinsert ourselves at end
|
174
|
+
unless i==split.length-1
|
175
|
+
pos+=sp.length
|
176
|
+
s.insert(pos, colors)
|
177
|
+
pos+=colors.length
|
178
|
+
end
|
179
|
+
elsif sp.match(/#{color_reg}/)
|
180
|
+
case local_color
|
181
|
+
when :keep
|
182
|
+
if i!=0
|
183
|
+
# we need to clear ourselves
|
184
|
+
s.insert(pos, clear)
|
185
|
+
pos+=clear.length
|
186
|
+
end
|
187
|
+
pos+=sp.length
|
188
|
+
when :before
|
189
|
+
#we already inserted ourselves except if we are on the
|
190
|
+
#first block
|
191
|
+
if i==0
|
192
|
+
s.insert(pos, colors)
|
193
|
+
pos+=colors.length
|
194
|
+
end
|
195
|
+
pos+=sp.length
|
196
|
+
when :after
|
197
|
+
pos+=sp.length
|
198
|
+
s.insert(pos, colors)
|
199
|
+
pos+=colors.length
|
200
|
+
end
|
201
|
+
else
|
202
|
+
if i==0
|
203
|
+
s.insert(pos, colors)
|
204
|
+
pos+=colors.length
|
205
|
+
end
|
206
|
+
pos+=sp.length
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
s.concat(clear) unless s =~ /#{clear_reg}$/ or colors.empty?
|
211
|
+
s
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Returns an uncolored version of the string, that is all
|
216
|
+
# ANSI-sequences are stripped from the string.
|
217
|
+
# @see: colorer
|
218
|
+
def uncolorer(s, **kwds)
|
219
|
+
s.to_str.gsub!(regexp(:color, **kwds), '') || s.to_str
|
220
|
+
rescue ArgumentError #rescue from "invalid byte sequence in UTF-8"
|
221
|
+
s.to_str
|
222
|
+
end
|
223
|
+
|
224
|
+
def colored?(s, **kwds)
|
225
|
+
!! (s =~ regexp(:color, **kwds))
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
data/lib/simplecolor/colors.rb
CHANGED
@@ -1,109 +1,71 @@
|
|
1
1
|
module SimpleColor
|
2
|
+
module Colorer
|
2
3
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
# Regular expression to scan if there is a clear ANSI effect
|
5
|
+
CLEAR = "\e\[0m"
|
6
|
+
CLEAR_REGEXP = /\e\[0m/
|
7
|
+
# Regular expression that is used to scan for ANSI-sequences
|
8
|
+
ANSICOLOR_REGEXP = /\e\[(?:[\d;]*)m/
|
9
|
+
COLOR_REGEXP = /#{ANSICOLOR_REGEXP}+/
|
10
|
+
COLORMATCH_REGEXP = /#{ANSICOLOR_REGEXP}*/
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
+
#Originally stolen from the paint gem.
|
13
|
+
#See also http://en.wikipedia.org/wiki/ANSI_escape_code
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
ANSI_EFFECTS = {
|
29
|
-
:reset => 0, :nothing => 0, # usually supported
|
30
|
-
:clear => 0, :normal => 0, # usually supported
|
31
|
-
:bright => 1, :bold => 1, # usually supported
|
32
|
-
:faint => 2,
|
33
|
-
:italic => 3,
|
34
|
-
:underline => 4, # usually supported
|
35
|
-
:blink => 5, :slow_blink => 5,
|
36
|
-
:rapid_blink => 6,
|
37
|
-
:inverse => 7, :swap => 7, # usually supported
|
38
|
-
:conceal => 8, :hide => 9,
|
39
|
-
:default_font => 10,
|
40
|
-
:font0 => 10, :font1 => 11, :font2 => 12, :font3 => 13, :font4 => 14,
|
41
|
-
:font5 => 15, :font6 => 16, :font7 => 17, :font8 => 18, :font9 => 19,
|
42
|
-
:fraktur => 20,
|
43
|
-
:bright_off => 21, :bold_off => 21, :double_underline => 21,
|
44
|
-
:clean => 22,
|
45
|
-
:italic_off => 23, :fraktur_off => 23,
|
46
|
-
:underline_off => 24,
|
47
|
-
:blink_off => 25,
|
48
|
-
:inverse_off => 26, :positive => 26,
|
49
|
-
:conceal_off => 27, :show => 27, :reveal => 27,
|
50
|
-
:crossed_off => 29, :crossed_out_off => 29,
|
51
|
-
:frame => 51,
|
52
|
-
:encircle => 52,
|
53
|
-
:overline => 53,
|
54
|
-
:frame_off => 54, :encircle_off => 54,
|
55
|
-
:overline_off => 55,
|
56
|
-
}
|
15
|
+
# Basic colors (often, the color differs when using the bright effect)
|
16
|
+
# Final color will be 30 + value for foreground and 40 + value for background
|
17
|
+
# 90+value for intense foreground, 100+value for intense background
|
18
|
+
ANSI_COLORS = {
|
19
|
+
:black => 0,
|
20
|
+
:red => 1,
|
21
|
+
:green => 2,
|
22
|
+
:yellow => 3,
|
23
|
+
:blue => 4,
|
24
|
+
:magenta => 5,
|
25
|
+
:cyan => 6,
|
26
|
+
:white => 7,
|
27
|
+
:default => 9,
|
28
|
+
}
|
57
29
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
:blue => 34,
|
64
|
-
:magenta => 35,
|
65
|
-
:cyan => 36,
|
66
|
-
:white => 37,
|
67
|
-
:default => 39,
|
68
|
-
}
|
30
|
+
ANSI_COLORS_FOREGROUND = Hash[ANSI_COLORS.map {|k,v| [k, 30+v]}]
|
31
|
+
ANSI_COLORS_BACKGROUND = Hash[ANSI_COLORS.map {|k,v| [:"on_#{k}", 40+v]}]
|
32
|
+
# aixterm (not standard)
|
33
|
+
ANSI_COLORS_INTENSE_FOREGROUND = Hash[ANSI_COLORS.map {|k,v| [:"intense_#{k}", 90+v]}]
|
34
|
+
ANSI_COLORS_INTENSE_BACKGROUND = Hash[ANSI_COLORS.map {|k,v| [:"on_intense_#{k}", 100+v]}]
|
69
35
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
:on_intense_cyan => 106,
|
102
|
-
:on_intense_white => 107,
|
103
|
-
:on_intense_default => 109,
|
104
|
-
}
|
105
|
-
|
106
|
-
#attributes that can be specified to the color method
|
107
|
-
COLORS = [ANSI_EFFECTS,ANSI_COLORS_FOREGROUND, ANSI_COLORS_BACKGROUND, ANSI_COLORS_INTENSE_FOREGROUND, ANSI_COLORS_INTENSE_BACKGROUND].inject({}){ |a,b| a.merge(b) }
|
36
|
+
ANSI_EFFECTS = {
|
37
|
+
:reset => 0, :nothing => 0, # usually supported
|
38
|
+
:clear => 0, :normal => 0, # usually supported
|
39
|
+
:bright => 1, :bold => 1, # usually supported
|
40
|
+
:faint => 2,
|
41
|
+
:italic => 3,
|
42
|
+
:underline => 4, # usually supported
|
43
|
+
:blink => 5, :slow_blink => 5,
|
44
|
+
:rapid_blink => 6,
|
45
|
+
:inverse => 7, :reverse => 7, :swap => 7, # usually supported
|
46
|
+
:conceal => 8, :hide => 8,
|
47
|
+
:crossed => 9, :crossed_out => 9,
|
48
|
+
:default_font => 10,
|
49
|
+
:font0 => 10, :font1 => 11, :font2 => 12, :font3 => 13, :font4 => 14,
|
50
|
+
:font5 => 15, :font6 => 16, :font7 => 17, :font8 => 18, :font9 => 19,
|
51
|
+
:fraktur => 20,
|
52
|
+
:bright_off => 21, :bold_off => 21, :double_underline => 21,
|
53
|
+
:clean => 22, :regular => 22, #neither bold or faint
|
54
|
+
:italic_off => 23, :fraktur_off => 23,
|
55
|
+
:underline_off => 24,
|
56
|
+
:blink_off => 25,
|
57
|
+
:inverse_off => 26, :positive => 26,
|
58
|
+
:conceal_off => 27, :show => 27, :reveal => 27,
|
59
|
+
:crossed_off => 29, :crossed_out_off => 29,
|
60
|
+
:frame => 51, :framed => 51,
|
61
|
+
:encircle => 52, :encircled => 52,
|
62
|
+
:overline => 53, :overlined => 53,
|
63
|
+
:frame_off => 54, :encircle_off => 54,
|
64
|
+
:framed_off => 54, :encircled_off => 54,
|
65
|
+
:overline_off => 55, :overlined_off => 55,
|
66
|
+
}
|
108
67
|
|
68
|
+
#attributes that can be specified to the color method
|
69
|
+
COLORS = [ANSI_EFFECTS,ANSI_COLORS_FOREGROUND, ANSI_COLORS_BACKGROUND, ANSI_COLORS_INTENSE_FOREGROUND, ANSI_COLORS_INTENSE_BACKGROUND].inject({}){ |a,b| a.merge(b) }
|
70
|
+
end
|
109
71
|
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require 'json'
|
2
|
+
require "zlib"
|
3
|
+
require 'simplecolor/rgb_colors'
|
4
|
+
|
5
|
+
# rgb color conversion
|
6
|
+
# taken from the paint gem, all copyright belong to its author
|
7
|
+
|
8
|
+
module SimpleColor
|
9
|
+
RGBError=Class.new(SimpleColorError)
|
10
|
+
WrongRGBColor=Class.new(RGBError)
|
11
|
+
WrongRGBParameter=Class.new(RGBError)
|
12
|
+
|
13
|
+
class RGB
|
14
|
+
|
15
|
+
module Parsers
|
16
|
+
# Creates RGB color from a HTML-like color definition string
|
17
|
+
def rgb_hex(string)
|
18
|
+
case string.size
|
19
|
+
when 6
|
20
|
+
string.each_char.each_slice(2).map{ |hex_color| hex_color.join.to_i(16) }
|
21
|
+
when 3
|
22
|
+
string.each_char.map{ |hex_color_half| (hex_color_half*2).to_i(16) }
|
23
|
+
else
|
24
|
+
raise WrongRGBColor.new(string)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse(col, color_names: proc {|c| find_color(c)})
|
29
|
+
case col
|
30
|
+
when self
|
31
|
+
return col
|
32
|
+
when Proc
|
33
|
+
scol=col.call(self)
|
34
|
+
return parse(scol, color_names: color_names)
|
35
|
+
when Symbol
|
36
|
+
if ANSI_COLORS_16.key?(col)
|
37
|
+
return self.new(col, mode: 16)
|
38
|
+
end
|
39
|
+
when Array
|
40
|
+
return self.new(col, mode: true)
|
41
|
+
when String
|
42
|
+
if (m=col.match(/\A(?:rgb)?256[+:-]?(?<grey>gr[ae]y)?(?<red>\d+)(?:[+:-](?<green>\d+)[+:-](?<blue>\d+))?\z/))
|
43
|
+
grey=m[:grey]; red=m[:red]&.to_i; green=m[:green]&.to_i; blue=m[:blue]&.to_i
|
44
|
+
if grey
|
45
|
+
return self.new(GREY256+red, mode: 256)
|
46
|
+
elsif green and blue
|
47
|
+
return self.new([red, green, blue], mode: 256)
|
48
|
+
else
|
49
|
+
raise WrongRGBColor.new(col) if green
|
50
|
+
return self.new(red, mode: 256)
|
51
|
+
end
|
52
|
+
elsif (m=col.match(/\A(?:rgb[+:-]?)?(?<red>\d+)[+:-](?<green>\d+)[+:-](?<blue>\d+)\z/))
|
53
|
+
red=m[:red]&.to_i; green=m[:green]&.to_i; blue=m[:blue]&.to_i
|
54
|
+
return self.new([red, green, blue], mode: :truecolor)
|
55
|
+
elsif (m=col.match(/\A#?(?<hex_color>[[:xdigit:]]{3}{1,2})\z/)) # 3 or 6 hex chars
|
56
|
+
return self.new(rgb_hex(m[:hex_color]), mode: :truecolor)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
if color_names && (c=color_names[col])
|
60
|
+
return self.parse(c, color_names: nil)
|
61
|
+
else
|
62
|
+
# fallback to parsing col.to_s?
|
63
|
+
raise WrongRGBColor.new(col)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
extend Parsers
|
68
|
+
|
69
|
+
module Utils
|
70
|
+
def rgb_random
|
71
|
+
RGB.new((1..3).map { Random.rand(256) })
|
72
|
+
end
|
73
|
+
|
74
|
+
#c=16 + 36 × r + 6 × g + b
|
75
|
+
def color256_to_rgb(c)
|
76
|
+
bgr=(c-16).digits(6)
|
77
|
+
bgr+=[0]*(3-rgb.length)
|
78
|
+
bgr.reverse
|
79
|
+
end
|
80
|
+
|
81
|
+
def rgb_values(c)
|
82
|
+
case c
|
83
|
+
when self
|
84
|
+
c.to_truecolor.color
|
85
|
+
else
|
86
|
+
self.parse(c).to_truecolor.color
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
extend Utils
|
91
|
+
|
92
|
+
module RGB16
|
93
|
+
def rgb_colors16
|
94
|
+
RGB_COLORS_ANSI_16
|
95
|
+
end
|
96
|
+
def rgb_colors8
|
97
|
+
RGB_COLORS_ANSI
|
98
|
+
end
|
99
|
+
end
|
100
|
+
extend RGB16
|
101
|
+
extend RGB16
|
102
|
+
|
103
|
+
attr_accessor :color, :mode, :background
|
104
|
+
|
105
|
+
private def color_mode(mode)
|
106
|
+
case mode
|
107
|
+
when true, :truecolor, TRUE_COLOR
|
108
|
+
mode=:truecolor
|
109
|
+
end
|
110
|
+
case mode
|
111
|
+
when 8, 16, 256, :truecolor
|
112
|
+
yield mode if block_given?
|
113
|
+
return mode
|
114
|
+
else
|
115
|
+
raise WrongRGBParameter.new(mode)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def initialize(*rgb, mode: :truecolor, background: false)
|
120
|
+
raise WrongRGBColor.new(rgb) if rgb.empty?
|
121
|
+
rgb=rgb.first if rgb.length==1
|
122
|
+
raise WrongRGBColor.new(rgb) if rgb.nil?
|
123
|
+
|
124
|
+
@init=rgb
|
125
|
+
@color=rgb #should be an array for truecolor, a number otherwise
|
126
|
+
@mode=color_mode(mode)
|
127
|
+
@background=!!background
|
128
|
+
|
129
|
+
case @mode
|
130
|
+
when :truecolor
|
131
|
+
unless @color&.size == 3 && @color&.all?{ |n| n.is_a? Numeric }
|
132
|
+
raise WrongRGBColor.new(rgb)
|
133
|
+
end
|
134
|
+
raise WrongRGBColor.new(rgb) unless rgb.all? do |c|
|
135
|
+
(0..255).include?(c)
|
136
|
+
end
|
137
|
+
when 256 #for 256 colors we are more lenient
|
138
|
+
case rgb
|
139
|
+
when Array
|
140
|
+
unless @color&.size == 3 && @color&.all?{ |n| n.is_a? Numeric }
|
141
|
+
raise WrongRGBColor.new(rgb)
|
142
|
+
end
|
143
|
+
raise WrongRGBColor.new(rgb) unless rgb.all? do |c|
|
144
|
+
(0..5).include?(c)
|
145
|
+
end
|
146
|
+
red, green, blue=rgb
|
147
|
+
@color=16 + 36 * red.to_i + 6 * green.to_i + blue.to_i
|
148
|
+
when String, Symbol #for grey mode
|
149
|
+
if (m=rgb.to_s.match(/\Agr[ae]y(\d+)\z/))
|
150
|
+
@color=GREY256+m[1].to_i
|
151
|
+
else
|
152
|
+
raise WrongRGBColor.new(rgb)
|
153
|
+
end
|
154
|
+
else
|
155
|
+
raise WrongRGBColor.new(rgb) unless @color.is_a?(Numeric)
|
156
|
+
end
|
157
|
+
when 8,16
|
158
|
+
@color=ANSI_COLORS_16[rgb] if ANSI_COLORS_16.key?(rgb)
|
159
|
+
raise WrongRGBColor.new(rgb) unless @color.is_a?(Numeric)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def ==(other)
|
164
|
+
[:color, :mode, :background].all? do |sym|
|
165
|
+
self.public_send(sym) == other.public_send(sym)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def truecolor?
|
170
|
+
@mode == :truecolor
|
171
|
+
end
|
172
|
+
|
173
|
+
def nbcolors
|
174
|
+
return TRUE_COLOR if @mode == :truecolor
|
175
|
+
return @mode
|
176
|
+
end
|
177
|
+
|
178
|
+
def to_hex
|
179
|
+
r,g,b=rgb
|
180
|
+
hexa = ->(c) {h=c.to_s(16).upcase; h.length == 1 ? "0#{h}" : h}
|
181
|
+
"\##{hexa[r]}#{hexa[g]}#{hexa[b]}"
|
182
|
+
end
|
183
|
+
|
184
|
+
def rgb
|
185
|
+
to_truecolor.color
|
186
|
+
end
|
187
|
+
|
188
|
+
# For RGB 256 colors,
|
189
|
+
# Foreground = "\e[38;5;#{fg}m", Background = "\e[48;5;#{bg}m"
|
190
|
+
# where the color code is
|
191
|
+
# 0- 7: standard colors (as in ESC [ 30–37 m)
|
192
|
+
# 8- 15: high intensity colors (as in ESC [ 90–97 m)
|
193
|
+
# 16-231: 6 × 6 × 6 cube (216 colors): 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
|
194
|
+
# 232-255: grayscale from black to white in 24 steps
|
195
|
+
|
196
|
+
#For true colors:
|
197
|
+
# ESC[ 38;2;<r>;<g>;<b> m Select RGB foreground color
|
198
|
+
# ESC[ 48;2;<r>;<g>;<b> m Select RGB background color
|
199
|
+
def ansi(background: @background, convert: nil)
|
200
|
+
return self.convert(convert, only_down: true).ansi(background: background, convert: nil) if convert
|
201
|
+
case @mode
|
202
|
+
when 8, 16
|
203
|
+
if @color < 8
|
204
|
+
"#{background ? 4 : 3}#{@color}"
|
205
|
+
else
|
206
|
+
# Note that on_intense_* is not supported by terminals
|
207
|
+
# it is usually better to convert to 256 colors palette which is
|
208
|
+
# better supported
|
209
|
+
"#{background ? 10 : 9}#{@color-8}"
|
210
|
+
end
|
211
|
+
when 256
|
212
|
+
"#{background ? 48 : 38};5;#{@color}"
|
213
|
+
when :truecolor
|
214
|
+
red, green, blue=@color
|
215
|
+
"#{background ? 48 : 38};2;#{red};#{green};#{blue}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def convert(mode, only_down: false)
|
220
|
+
case color_mode(mode)
|
221
|
+
when 8
|
222
|
+
return to_8
|
223
|
+
when 16
|
224
|
+
return to_16 unless only_down and nbcolors < 16
|
225
|
+
when 256
|
226
|
+
return to_256 unless only_down and nbcolors < 256
|
227
|
+
when :truecolor
|
228
|
+
return to_truecolor unless only_down and nbcolors < TRUE_COLOR
|
229
|
+
end
|
230
|
+
self
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_truecolor
|
234
|
+
case @mode
|
235
|
+
when 8, 16
|
236
|
+
name=ANSI_COLORS_16.key(@color)
|
237
|
+
rgb=self.class.rgb_colors16[name]
|
238
|
+
self.class.new(rgb)
|
239
|
+
when 256
|
240
|
+
if @color < 16
|
241
|
+
to_16.to_truecolor
|
242
|
+
elsif @color < GREY256
|
243
|
+
red, green, blue=self.class.color256_to_rgb(@color)
|
244
|
+
self.class.new([red, green, blue].map {|c| (c * 256.0/7.0).round})
|
245
|
+
else
|
246
|
+
grey=@color-GREY256
|
247
|
+
self.class.new([(grey*256.0/24.0).round]*3)
|
248
|
+
end
|
249
|
+
else
|
250
|
+
self
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def to_256
|
255
|
+
case @mode
|
256
|
+
when 256
|
257
|
+
return self
|
258
|
+
when 8, 16
|
259
|
+
return self.class.new(@color, mode: 256)
|
260
|
+
else
|
261
|
+
red,green,blue=@color
|
262
|
+
|
263
|
+
gray_possible = true
|
264
|
+
sep = 42.5
|
265
|
+
|
266
|
+
while gray_possible
|
267
|
+
if red < sep || green < sep || blue < sep
|
268
|
+
gray = red < sep && green < sep && blue < sep
|
269
|
+
gray_possible = false
|
270
|
+
end
|
271
|
+
sep += 42.5
|
272
|
+
end
|
273
|
+
|
274
|
+
col=if gray
|
275
|
+
GREY256 + ((red.to_f + green.to_f + blue.to_f)/33).round
|
276
|
+
else # rgb
|
277
|
+
[16, *[red, green, blue].zip([36, 6, 1]).map{ |color, mod|
|
278
|
+
(6 * (color.to_f / 256)).to_i * mod
|
279
|
+
}].inject(:+)
|
280
|
+
end
|
281
|
+
self.class.new(col, mode: 256)
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def to_8
|
287
|
+
rgb8=self.class.rgb_colors8
|
288
|
+
color_pool = rgb8.values
|
289
|
+
closest=rgb_to_pool(color_pool)
|
290
|
+
name=rgb8.key(closest)
|
291
|
+
self.class.new(ANSI_COLORS_16[name], mode: 8)
|
292
|
+
end
|
293
|
+
|
294
|
+
def to_16
|
295
|
+
rgb16=self.class.rgb_colors16
|
296
|
+
color_pool = rgb16.values
|
297
|
+
closest=rgb_to_pool(color_pool)
|
298
|
+
name=rgb16.key(closest)
|
299
|
+
self.class.new(ANSI_COLORS_16[name], mode: 16)
|
300
|
+
end
|
301
|
+
|
302
|
+
def rgb_color_distance(rgb2)
|
303
|
+
if truecolor?
|
304
|
+
@color.zip(self.class.rgb_values(rgb2)).inject(0){ |acc, (cur1, cur2)| acc + (cur1 - cur2)**2 }
|
305
|
+
else
|
306
|
+
to_truecolor.rgb_color_distance(rgb2)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def rgb_to_pool(color_pool)
|
311
|
+
if truecolor?
|
312
|
+
color_pool.min_by{ |col| rgb_color_distance(col) }
|
313
|
+
else
|
314
|
+
to_truecolor.rgb_to_pool(color_pool)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|