Module: Fatty::Color

Defined in:
lib/fatty/colors/color.rb

Constant Summary collapse

DEFAULT_INDEX =
-1
RGB_TXT_PATH =

Expected location for a bundled X11 rgb.txt (you provide it in the repo). Recommended path:

lib/fatty/color/rgb.txt
File.expand_path("rgb.txt", __dir__)
ANSI_NAMES =

ANSI 0..15 names (de-facto standard names)

{
  "black" => 0,
  "red" => 1,
  "green" => 2,
  "yellow" => 3,
  "blue" => 4,
  "magenta" => 5,
  "cyan" => 6,
  "white" => 7,
  "bright_black" => 8,
  "bright_red" => 9,
  "bright_green" => 10,
  "bright_yellow" => 11,
  "bright_blue" => 12,
  "bright_magenta" => 13,
  "bright_cyan" => 14,
  "bright_white" => 15,
  "gray" => 8,
  "grey" => 8,
  "bright_gray" => 15,
  "bright_grey" => 15,
  "default" => DEFAULT_INDEX,
}.freeze
CANONICAL_ANSI_COLORS =
{
  "black" => 0,
  "red" => 1,
  "green" => 2,
  "yellow" => 3,
  "blue" => 4,
  "magenta" => 5,
  "cyan" => 6,
  "white" => 7,
  "bright_black" => 8,
  "bright_red" => 9,
  "bright_green" => 10,
  "bright_yellow" => 11,
  "bright_blue" => 12,
  "bright_magenta" => 13,
  "bright_cyan" => 14,
  "bright_white" => 15,
}.freeze
ALIASES_256 =

Small alias set (xterm-256 indices). Keep this small + opinionated. Users can always use integers/hex/X11 names.

{
  "navy" => 17,
  "dark_blue" => 18,
  "orange" => 208,
  "pink" => 205,
  "violet" => 141,
  "sky" => 117,
  "teal" => 37,
  "lime" => 118,
  "dark_grey" => 238,
  "dark_gray" => 238,
  "grey" => 244,
  "gray" => 244,
  "light_grey" => 250,
  "light_gray" => 250,
}.freeze
ANSI_RGB =

Approximate RGB for xterm-style ANSI 0..15. Used only when down-mapping to <=16 colors.

{
  0 => [0, 0, 0],
  1 => [205, 0, 0],
  2 => [0, 205, 0],
  3 => [205, 205, 0],
  4 => [0, 0, 238],
  5 => [205, 0, 205],
  6 => [0, 205, 205],
  7 => [229, 229, 229],
  8 => [127, 127, 127],
  9 => [255, 0, 0],
  10 => [0, 255, 0],
  11 => [255, 255, 0],
  12 => [92, 92, 255],
  13 => [255, 0, 255],
  14 => [0, 255, 255],
  15 => [255, 255, 255],
}.freeze

Class Method Summary collapse

Class Method Details

.ansi_colorsObject



97
98
99
# File 'lib/fatty/colors/color.rb', line 97

def ansi_colors
  CANONICAL_ANSI_COLORS.dup
end

.clamp_index(idx, available_colors:) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/fatty/colors/color.rb', line 229

def clamp_index(idx, available_colors:)
  i = idx.to_i

  result =
    if available_colors.to_i <= 0
      0
    elsif available_colors.to_i <= 16
      downmap_to_ansi16(i)
    else
      i.clamp(0, 255)
    end

  result
end

.contrasting_ansi_index(index) ⇒ Object



269
270
271
272
273
274
275
276
277
278
# File 'lib/fatty/colors/color.rb', line 269

def contrasting_ansi_index(index)
  rgb = xterm_rgb_for_index(index)
  fg = contrasting_rgb(rgb)

  if fg == [0, 0, 0]
    0
  else
    15
  end
end

.contrasting_ansi_index_for_rgb(rgb) ⇒ Object



244
245
246
247
# File 'lib/fatty/colors/color.rb', line 244

def contrasting_ansi_index_for_rgb(rgb)
  luminance = ((rgb[0] * 299) + (rgb[1] * 587) + (rgb[2] * 114)) / 1000
  luminance >= 140 ? 0 : 15
end

.contrasting_rgb(rgb) ⇒ Object



261
262
263
264
265
266
267
# File 'lib/fatty/colors/color.rb', line 261

def contrasting_rgb(rgb)
  if luminance_for_rgb(rgb) >= 140
    [0, 0, 0]
  else
    [255, 255, 255]
  end
end

.hex_for_index(index) ⇒ Object



253
254
255
# File 'lib/fatty/colors/color.rb', line 253

def hex_for_index(index)
  hex_for_rgb(xterm_rgb_for_index(index))
end

.hex_for_rgb(rgb) ⇒ Object



249
250
251
# File 'lib/fatty/colors/color.rb', line 249

def hex_for_rgb(rgb)
  format("#%02x%02x%02x", *rgb)
end

.luminance_for_rgb(rgb) ⇒ Object



257
258
259
# File 'lib/fatty/colors/color.rb', line 257

def luminance_for_rgb(rgb)
  ((rgb[0] * 299) + (rgb[1] * 587) + (rgb[2] * 114)) / 1000
end

.resolve(spec, available_colors: 256) ⇒ Object

Resolve a color specification to an integer color index suitable for curses init_pair.

Accepts:

  • Integer (e.g. 17, 226, -1)
  • ANSI name (e.g. "yellow", "bright_blue", "default")
  • Alias (e.g. "navy")
  • Hex string (#RRGGBB or #RGB)
  • X11 name (e.g. "MidnightBlue") resolved via bundled rgb.txt

available_colors:

  • If <= 16, any resolved 256-color index is down-mapped to nearest ANSI 0..15.
  • If > 16, returns xterm-256 indices 0..255 (or -1 for default).


141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/fatty/colors/color.rb', line 141

def resolve(spec, available_colors: 256)
  spec_norm = spec

  idx =
    if spec_norm.is_a?(Integer)
      spec_norm
    elsif spec_norm.nil?
      DEFAULT_INDEX
    else
      resolve_stringish(spec_norm.to_s, available_colors: available_colors)
    end

  if idx == DEFAULT_INDEX
    idx
  else
    clamp_index(idx, available_colors: available_colors)
  end
end

.rgb(spec) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/fatty/colors/color.rb', line 160

def rgb(spec)
  if spec.is_a?(Integer)
    xterm_rgb_for_index(spec)
  elsif spec.nil?
    nil
  else
    s = normalize_name(spec.to_s)

    if s.start_with?("ansi:")
      s = s.delete_prefix("ansi:")
    elsif s.start_with?("x11:")
      s = s.delete_prefix("x11:")
    end

    idx = ALIASES_256[s]
    if idx
      xterm_rgb_for_index(idx)
    else
      parsed = parse_hex(s)
      parsed ||= x11_rgb_for_name(s)
      parsed ||= begin
                   ansi = ANSI_NAMES[s]
                   xterm_rgb_for_index(ansi) unless ansi == DEFAULT_INDEX || ansi.nil?
                 end
      parsed
    end
  end
end

.x11_colorsObject



107
108
109
# File 'lib/fatty/colors/color.rb', line 107

def x11_colors
  x11_table.dup
end

.x11_colors_by_nameObject



111
112
113
114
115
# File 'lib/fatty/colors/color.rb', line 111

def x11_colors_by_name
  x11_colors.sort_by do |name, _rgb|
    color_name_sort_key(name)
  end.to_h
end

.xterm_color_name(index) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/fatty/colors/color.rb', line 117

def xterm_color_name(index)
  if index.between?(0, 15)
    CANONICAL_ANSI_COLORS.key(index)
  elsif index.between?(16, 231)
    "xterm_#{index}"
  elsif index.between?(232, 255)
    "gray_#{index}"
  else
    "color_#{index}"
  end
end

.xterm_colorsObject



101
102
103
104
105
# File 'lib/fatty/colors/color.rb', line 101

def xterm_colors
  (0..255).to_h do |index|
    [xterm_color_name(index), index]
  end
end

.xterm_index_for_rgb(r, g, b) ⇒ Object

Convert any RGB to an xterm-256 index (16..255). We consider both the 6x6x6 cube and the grayscale ramp and pick the closer.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/fatty/colors/color.rb', line 191

def xterm_index_for_rgb(r, g, b)
  rr = clamp_byte(r)
  gg = clamp_byte(g)
  bb = clamp_byte(b)

  cube = xterm_cube_index(rr, gg, bb)
  gray = xterm_gray_index(rr, gg, bb)

  cube_rgb = xterm_rgb_for_index(cube)
  gray_rgb = xterm_rgb_for_index(gray)

  if dist2(rr, gg, bb, gray_rgb[0], gray_rgb[1], gray_rgb[2]) <
     dist2(rr, gg, bb, cube_rgb[0], cube_rgb[1], cube_rgb[2])
    gray
  else
    cube
  end
end

.xterm_rgb_for_index(idx) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/fatty/colors/color.rb', line 210

def xterm_rgb_for_index(idx)
  i = idx.to_i

  if i.between?(232, 255)
    v = 8 + (i - 232) * 10
    [v, v, v]
  elsif i.between?(16, 231)
    j = i - 16
    r = j / 36
    g = (j % 36) / 6
    b = j % 6
    levels = [0, 95, 135, 175, 215, 255]
    [levels[r], levels[g], levels[b]]
  else
    # for 0..15 we use ANSI_RGB as an approximation
    ANSI_RGB[i] || [0, 0, 0]
  end
end