simplecolor 0.2.0 → 0.3.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 +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
|