fatty 0.99.0 → 0.99.3
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 +4 -4
- data/CHANGELOG.org +11 -29
- data/README.md +2207 -18
- data/README.org +2066 -104
- data/exe/fatty +236 -93
- data/help/help.md +233 -0
- data/lib/fatty/action.rb +3 -7
- data/lib/fatty/actionable.rb +2 -0
- data/lib/fatty/alert.rb +65 -39
- data/lib/fatty/ansi/renderer.rb +26 -6
- data/lib/fatty/ansi.rb +1 -0
- data/lib/fatty/api/alert.rb +15 -0
- data/lib/fatty/api/choose.rb +145 -0
- data/lib/fatty/api/environment.rb +12 -0
- data/lib/fatty/api/keytest.rb +11 -0
- data/lib/fatty/api/menu.rb +104 -0
- data/lib/fatty/api/output.rb +55 -0
- data/lib/fatty/api/progress.rb +24 -0
- data/lib/fatty/api/prompt.rb +77 -0
- data/lib/fatty/api/status.rb +55 -0
- data/lib/fatty/api.rb +38 -0
- data/lib/fatty/callback_environment.rb +74 -0
- data/lib/fatty/colors/color.rb +366 -232
- data/lib/fatty/colors/pairs.rb +2 -0
- data/lib/fatty/command.rb +78 -0
- data/lib/fatty/config.rb +227 -39
- data/lib/fatty/config_files/config.yml +4 -12
- data/lib/fatty/config_files/keydefs.yml +13 -23
- data/lib/fatty/config_files/themes/capuchin_monk.yml +140 -0
- data/lib/fatty/config_files/themes/catppuccin_latte.yml +140 -0
- data/lib/fatty/config_files/themes/catppuccin_mocha.yml +140 -0
- data/lib/fatty/config_files/themes/cyberpunk.yml +141 -0
- data/lib/fatty/config_files/themes/dracula.yml +140 -0
- data/lib/fatty/config_files/themes/everforest_dark.yml +140 -0
- data/lib/fatty/config_files/themes/gruvbox_dark.yml +140 -0
- data/lib/fatty/config_files/themes/gruvbox_light.yml +140 -0
- data/lib/fatty/config_files/themes/mono.yml +119 -47
- data/lib/fatty/config_files/themes/monokai.yml +140 -0
- data/lib/fatty/config_files/themes/nordic.yml +1 -0
- data/lib/fatty/config_files/themes/onedark.yml +140 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +1 -0
- data/lib/fatty/config_files/themes/solarized_light.yml +140 -0
- data/lib/fatty/config_files/themes/terminal.yml +67 -0
- data/lib/fatty/config_files/themes/tokyo_night.yml +140 -0
- data/lib/fatty/config_files/themes/wordperfect.yml +80 -9
- data/lib/fatty/core_ext/hash.rb +19 -0
- data/lib/fatty/core_ext/string.rb +19 -12
- data/lib/fatty/core_ext.rb +1 -0
- data/lib/fatty/curses/context.rb +86 -84
- data/lib/fatty/curses/event_source.rb +128 -40
- data/lib/fatty/curses/key_decoder.rb +77 -11
- data/lib/fatty/env.rb +21 -20
- data/lib/fatty/help.rb +1 -29
- data/lib/fatty/history/entry.rb +2 -0
- data/lib/fatty/history.rb +17 -14
- data/lib/fatty/input_buffer.rb +71 -47
- data/lib/fatty/input_field.rb +216 -149
- data/lib/fatty/key_event.rb +67 -66
- data/lib/fatty/key_map.rb +54 -16
- data/lib/fatty/keymaps/emacs.rb +40 -6
- data/lib/fatty/log_formats/json.rb +2 -0
- data/lib/fatty/log_formats/text.rb +2 -0
- data/lib/fatty/logger.rb +7 -2
- data/lib/fatty/markdown/ansi_renderer.rb +75 -6
- data/lib/fatty/markdown/render.rb +2 -3
- data/lib/fatty/mouse_event.rb +142 -2
- data/lib/fatty/output_buffer.rb +2 -0
- data/lib/fatty/pager.rb +158 -47
- data/lib/fatty/progress.rb +417 -0
- data/lib/fatty/renderer/curses.rb +340 -184
- data/lib/fatty/renderer/truecolor.rb +266 -163
- data/lib/fatty/renderer.rb +115 -97
- data/lib/fatty/screen.rb +2 -0
- data/lib/fatty/session/alert_session.rb +39 -21
- data/lib/fatty/session/isearch_session.rb +92 -38
- data/lib/fatty/session/keytest_session.rb +105 -85
- data/lib/fatty/session/modal_session.rb +38 -1
- data/lib/fatty/session/output_session.rb +472 -21
- data/lib/fatty/session/popup_session.rb +251 -134
- data/lib/fatty/session/prompt_session.rb +68 -26
- data/lib/fatty/session/search_session.rb +79 -31
- data/lib/fatty/session/shell_session.rb +383 -368
- data/lib/fatty/session/status_session.rb +167 -0
- data/lib/fatty/session.rb +95 -95
- data/lib/fatty/terminal/popup_owner.rb +14 -11
- data/lib/fatty/terminal.rb +515 -813
- data/lib/fatty/themes/loader.rb +4 -2
- data/lib/fatty/themes/manager.rb +4 -0
- data/lib/fatty/themes/registry.rb +2 -0
- data/lib/fatty/themes/resolver.rb +175 -162
- data/lib/fatty/version.rb +1 -1
- data/lib/fatty/viewport.rb +8 -1
- data/lib/fatty.rb +35 -7
- metadata +42 -40
- data/.envrc +0 -2
- data/.simplecov +0 -23
- data/.yardopts +0 -4
- data/CHANGELOG.md +0 -34
- data/Rakefile +0 -15
- data/TODO.org +0 -163
- data/examples/markdown/native-markdown.md +0 -370
- data/examples/markdown/ox-gfm-markdown.md +0 -373
- data/examples/markdown/ox-gfm-markdown.org +0 -376
- data/fatty.gemspec +0 -42
- data/lib/fatty/accept_env.rb +0 -32
- data/lib/fatty/config_files/help.md +0 -120
- data/lib/fatty/config_files/help.org +0 -124
- data/lib/fatty/config_files/themes/wordperfect_light.yml +0 -77
- data/lib/fatty/menu_env.rb +0 -22
- data/lib/fatty/session/input_session.rb +0 -99
- data/lib/fatty/sessions.rb +0 -14
- data/lib/fatty/terminal/progress.rb +0 -374
- data/lib/fatty/view/alert_view.rb +0 -14
- data/lib/fatty/view/cursor_view.rb +0 -18
- data/lib/fatty/view/input_view.rb +0 -9
- data/lib/fatty/view/output_view.rb +0 -9
- data/lib/fatty/view/status_view.rb +0 -14
- data/lib/fatty/view.rb +0 -33
- data/lib/fatty/views.rb +0 -9
- data/sig/fatty.rbs +0 -4
data/lib/fatty/colors/color.rb
CHANGED
|
@@ -34,6 +34,25 @@ module Fatty
|
|
|
34
34
|
"default" => DEFAULT_INDEX,
|
|
35
35
|
}.freeze
|
|
36
36
|
|
|
37
|
+
CANONICAL_ANSI_COLORS = {
|
|
38
|
+
"black" => 0,
|
|
39
|
+
"red" => 1,
|
|
40
|
+
"green" => 2,
|
|
41
|
+
"yellow" => 3,
|
|
42
|
+
"blue" => 4,
|
|
43
|
+
"magenta" => 5,
|
|
44
|
+
"cyan" => 6,
|
|
45
|
+
"white" => 7,
|
|
46
|
+
"bright_black" => 8,
|
|
47
|
+
"bright_red" => 9,
|
|
48
|
+
"bright_green" => 10,
|
|
49
|
+
"bright_yellow" => 11,
|
|
50
|
+
"bright_blue" => 12,
|
|
51
|
+
"bright_magenta" => 13,
|
|
52
|
+
"bright_cyan" => 14,
|
|
53
|
+
"bright_white" => 15,
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
37
56
|
# Small alias set (xterm-256 indices). Keep this small + opinionated.
|
|
38
57
|
# Users can always use integers/hex/X11 names.
|
|
39
58
|
ALIASES_256 = {
|
|
@@ -74,306 +93,421 @@ module Fatty
|
|
|
74
93
|
15 => [255, 255, 255],
|
|
75
94
|
}.freeze
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
# available_colors:
|
|
87
|
-
# - If <= 16, any resolved 256-color index is down-mapped to nearest ANSI 0..15.
|
|
88
|
-
# - If > 16, returns xterm-256 indices 0..255 (or -1 for default).
|
|
89
|
-
def self.resolve(spec, available_colors: 256)
|
|
90
|
-
spec_norm = spec
|
|
91
|
-
|
|
92
|
-
idx =
|
|
93
|
-
if spec_norm.is_a?(Integer)
|
|
94
|
-
spec_norm
|
|
95
|
-
elsif spec_norm.nil?
|
|
96
|
-
DEFAULT_INDEX
|
|
97
|
-
else
|
|
98
|
-
resolve_stringish(spec_norm.to_s, available_colors: available_colors)
|
|
96
|
+
class << self
|
|
97
|
+
def ansi_colors
|
|
98
|
+
CANONICAL_ANSI_COLORS.dup
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def xterm_colors
|
|
102
|
+
(0..255).to_h do |index|
|
|
103
|
+
[xterm_color_name(index), index]
|
|
99
104
|
end
|
|
105
|
+
end
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
def x11_colors
|
|
108
|
+
x11_table.dup
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def x11_colors_by_name
|
|
112
|
+
x11_colors.sort_by do |name, _rgb|
|
|
113
|
+
color_name_sort_key(name)
|
|
114
|
+
end.to_h
|
|
105
115
|
end
|
|
106
|
-
end
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
def xterm_color_name(index)
|
|
118
|
+
if index.between?(0, 15)
|
|
119
|
+
CANONICAL_ANSI_COLORS.key(index)
|
|
120
|
+
elsif index.between?(16, 231)
|
|
121
|
+
"xterm_#{index}"
|
|
122
|
+
elsif index.between?(232, 255)
|
|
123
|
+
"gray_#{index}"
|
|
124
|
+
else
|
|
125
|
+
"color_#{index}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Resolve a color specification to an integer color index suitable for curses init_pair.
|
|
130
|
+
#
|
|
131
|
+
# Accepts:
|
|
132
|
+
# - Integer (e.g. 17, 226, -1)
|
|
133
|
+
# - ANSI name (e.g. "yellow", "bright_blue", "default")
|
|
134
|
+
# - Alias (e.g. "navy")
|
|
135
|
+
# - Hex string (#RRGGBB or #RGB)
|
|
136
|
+
# - X11 name (e.g. "MidnightBlue") resolved via bundled rgb.txt
|
|
113
137
|
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
# available_colors:
|
|
139
|
+
# - If <= 16, any resolved 256-color index is down-mapped to nearest ANSI 0..15.
|
|
140
|
+
# - If > 16, returns xterm-256 indices 0..255 (or -1 for default).
|
|
141
|
+
def resolve(spec, available_colors: 256)
|
|
142
|
+
spec_norm = spec
|
|
143
|
+
|
|
144
|
+
idx =
|
|
145
|
+
if spec_norm.is_a?(Integer)
|
|
146
|
+
spec_norm
|
|
147
|
+
elsif spec_norm.nil?
|
|
148
|
+
DEFAULT_INDEX
|
|
149
|
+
else
|
|
150
|
+
resolve_stringish(spec_norm.to_s, available_colors: available_colors)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if idx == DEFAULT_INDEX
|
|
154
|
+
idx
|
|
155
|
+
else
|
|
156
|
+
clamp_index(idx, available_colors: available_colors)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def rgb(spec)
|
|
161
|
+
if spec.is_a?(Integer)
|
|
162
|
+
xterm_rgb_for_index(spec)
|
|
163
|
+
elsif spec.nil?
|
|
164
|
+
nil
|
|
165
|
+
else
|
|
166
|
+
s = normalize_name(spec.to_s)
|
|
167
|
+
|
|
168
|
+
if s.start_with?("ansi:")
|
|
169
|
+
s = s.delete_prefix("ansi:")
|
|
170
|
+
elsif s.start_with?("x11:")
|
|
171
|
+
s = s.delete_prefix("x11:")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
idx = ALIASES_256[s]
|
|
175
|
+
if idx
|
|
176
|
+
xterm_rgb_for_index(idx)
|
|
177
|
+
else
|
|
178
|
+
parsed = parse_hex(s)
|
|
179
|
+
parsed ||= x11_rgb_for_name(s)
|
|
180
|
+
parsed ||= begin
|
|
181
|
+
ansi = ANSI_NAMES[s]
|
|
182
|
+
xterm_rgb_for_index(ansi) unless ansi == DEFAULT_INDEX || ansi.nil?
|
|
183
|
+
end
|
|
184
|
+
parsed
|
|
185
|
+
end
|
|
186
|
+
end
|
|
123
187
|
end
|
|
124
188
|
|
|
125
|
-
|
|
126
|
-
|
|
189
|
+
# Convert any RGB to an xterm-256 index (16..255).
|
|
190
|
+
# We consider both the 6x6x6 cube and the grayscale ramp and pick the closer.
|
|
191
|
+
def xterm_index_for_rgb(r, g, b)
|
|
192
|
+
rr = clamp_byte(r)
|
|
193
|
+
gg = clamp_byte(g)
|
|
194
|
+
bb = clamp_byte(b)
|
|
127
195
|
|
|
128
|
-
|
|
129
|
-
|
|
196
|
+
cube = xterm_cube_index(rr, gg, bb)
|
|
197
|
+
gray = xterm_gray_index(rr, gg, bb)
|
|
130
198
|
|
|
131
|
-
|
|
199
|
+
cube_rgb = xterm_rgb_for_index(cube)
|
|
200
|
+
gray_rgb = xterm_rgb_for_index(gray)
|
|
132
201
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
202
|
+
if dist2(rr, gg, bb, gray_rgb[0], gray_rgb[1], gray_rgb[2]) <
|
|
203
|
+
dist2(rr, gg, bb, cube_rgb[0], cube_rgb[1], cube_rgb[2])
|
|
204
|
+
gray
|
|
205
|
+
else
|
|
206
|
+
cube
|
|
207
|
+
end
|
|
136
208
|
end
|
|
137
209
|
|
|
138
|
-
idx
|
|
139
|
-
|
|
210
|
+
def xterm_rgb_for_index(idx)
|
|
211
|
+
i = idx.to_i
|
|
212
|
+
|
|
213
|
+
if i.between?(232, 255)
|
|
214
|
+
v = 8 + (i - 232) * 10
|
|
215
|
+
[v, v, v]
|
|
216
|
+
elsif i.between?(16, 231)
|
|
217
|
+
j = i - 16
|
|
218
|
+
r = j / 36
|
|
219
|
+
g = (j % 36) / 6
|
|
220
|
+
b = j % 6
|
|
221
|
+
levels = [0, 95, 135, 175, 215, 255]
|
|
222
|
+
[levels[r], levels[g], levels[b]]
|
|
223
|
+
else
|
|
224
|
+
# for 0..15 we use ANSI_RGB as an approximation
|
|
225
|
+
ANSI_RGB[i] || [0, 0, 0]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def clamp_index(idx, available_colors:)
|
|
230
|
+
i = idx.to_i
|
|
231
|
+
|
|
232
|
+
result =
|
|
233
|
+
if available_colors.to_i <= 0
|
|
234
|
+
0
|
|
235
|
+
elsif available_colors.to_i <= 16
|
|
236
|
+
downmap_to_ansi16(i)
|
|
237
|
+
else
|
|
238
|
+
i.clamp(0, 255)
|
|
239
|
+
end
|
|
140
240
|
|
|
141
|
-
|
|
142
|
-
rgb2 = x11_rgb_for_name(s)
|
|
143
|
-
return xterm_index_for_rgb(rgb2[0], rgb2[1], rgb2[2]) if rgb2
|
|
241
|
+
result
|
|
144
242
|
end
|
|
145
243
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
244
|
+
def contrasting_ansi_index_for_rgb(rgb)
|
|
245
|
+
luminance = ((rgb[0] * 299) + (rgb[1] * 587) + (rgb[2] * 114)) / 1000
|
|
246
|
+
luminance >= 140 ? 0 : 15
|
|
247
|
+
end
|
|
149
248
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
s = str.to_s.strip.downcase
|
|
154
|
-
s = s.tr("-", "_")
|
|
155
|
-
s = s.gsub(/\s+/, "")
|
|
156
|
-
s
|
|
157
|
-
end
|
|
249
|
+
def hex_for_rgb(rgb)
|
|
250
|
+
format("#%02x%02x%02x", *rgb)
|
|
251
|
+
end
|
|
158
252
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
hex = s
|
|
162
|
-
if hex.start_with?("#")
|
|
163
|
-
hex = hex[1..]
|
|
253
|
+
def hex_for_index(index)
|
|
254
|
+
hex_for_rgb(xterm_rgb_for_index(index))
|
|
164
255
|
end
|
|
165
256
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
g = (hex[1] * 2).to_i(16)
|
|
169
|
-
b = (hex[2] * 2).to_i(16)
|
|
170
|
-
[r, g, b]
|
|
171
|
-
elsif hex.match?(/\A[0-9a-f]{6}\z/i)
|
|
172
|
-
r = hex[0, 2].to_i(16)
|
|
173
|
-
g = hex[2, 2].to_i(16)
|
|
174
|
-
b = hex[4, 2].to_i(16)
|
|
175
|
-
[r, g, b]
|
|
257
|
+
def luminance_for_rgb(rgb)
|
|
258
|
+
((rgb[0] * 299) + (rgb[1] * 587) + (rgb[2] * 114)) / 1000
|
|
176
259
|
end
|
|
177
|
-
end
|
|
178
260
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
261
|
+
def contrasting_rgb(rgb)
|
|
262
|
+
if luminance_for_rgb(rgb) >= 140
|
|
263
|
+
[0, 0, 0]
|
|
264
|
+
else
|
|
265
|
+
[255, 255, 255]
|
|
266
|
+
end
|
|
267
|
+
end
|
|
182
268
|
|
|
183
|
-
|
|
269
|
+
def contrasting_ansi_index(index)
|
|
270
|
+
rgb = xterm_rgb_for_index(index)
|
|
271
|
+
fg = contrasting_rgb(rgb)
|
|
184
272
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
273
|
+
if fg == [0, 0, 0]
|
|
274
|
+
0
|
|
275
|
+
else
|
|
276
|
+
15
|
|
277
|
+
end
|
|
189
278
|
end
|
|
190
|
-
end
|
|
191
279
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
280
|
+
private
|
|
281
|
+
|
|
282
|
+
def color_name_sort_key(name)
|
|
283
|
+
name.to_s.split(/(\d+)/).map do |part|
|
|
284
|
+
if part.match?(/\A\d+\z/)
|
|
285
|
+
[0, part.to_i]
|
|
286
|
+
else
|
|
287
|
+
[1, part]
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
199
291
|
|
|
292
|
+
def resolve_stringish(str, available_colors: 256)
|
|
293
|
+
s = normalize_name(str)
|
|
294
|
+
# Disambiguation prefixes:
|
|
295
|
+
# - "ansi:yellow" forces ANSI 0..15 names
|
|
296
|
+
# - "x11:yellow" forces X11 rgb.txt lookup
|
|
297
|
+
#
|
|
298
|
+
# Without a prefix, prefer X11 names when 256 colors are available so
|
|
299
|
+
# common names like "yellow" match X11 "#FFFF00" rather than ANSI 3.
|
|
300
|
+
mode = nil
|
|
200
301
|
if s.start_with?("ansi:")
|
|
302
|
+
mode = :ansi
|
|
201
303
|
s = s.delete_prefix("ansi:")
|
|
202
304
|
elsif s.start_with?("x11:")
|
|
305
|
+
mode = :x11
|
|
203
306
|
s = s.delete_prefix("x11:")
|
|
204
307
|
end
|
|
205
308
|
|
|
206
309
|
idx = ALIASES_256[s]
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
310
|
+
return idx unless idx.nil?
|
|
311
|
+
|
|
312
|
+
rgb = parse_hex(s)
|
|
313
|
+
return xterm_index_for_rgb(rgb[0], rgb[1], rgb[2]) if rgb
|
|
314
|
+
|
|
315
|
+
prefer_x11 = (mode == :x11) || (mode.nil? && available_colors.to_i >= 256)
|
|
316
|
+
|
|
317
|
+
if prefer_x11
|
|
318
|
+
rgb2 = x11_rgb_for_name(s)
|
|
319
|
+
return xterm_index_for_rgb(rgb2[0], rgb2[1], rgb2[2]) if rgb2
|
|
217
320
|
end
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
321
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def self.xterm_index_for_rgb(r, g, b)
|
|
224
|
-
rr = clamp_byte(r)
|
|
225
|
-
gg = clamp_byte(g)
|
|
226
|
-
bb = clamp_byte(b)
|
|
322
|
+
idx = ANSI_NAMES[s]
|
|
323
|
+
return idx unless idx.nil?
|
|
227
324
|
|
|
228
|
-
|
|
229
|
-
|
|
325
|
+
unless prefer_x11
|
|
326
|
+
rgb2 = x11_rgb_for_name(s)
|
|
327
|
+
return xterm_index_for_rgb(rgb2[0], rgb2[1], rgb2[2]) if rgb2
|
|
328
|
+
end
|
|
230
329
|
|
|
231
|
-
|
|
232
|
-
|
|
330
|
+
# Unknown name: treat as default.
|
|
331
|
+
DEFAULT_INDEX
|
|
332
|
+
end
|
|
233
333
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
334
|
+
def normalize_name(str)
|
|
335
|
+
# normalize spaces/underscores/hyphens and case, so:
|
|
336
|
+
# "Light Sky Blue" == "light_sky_blue" == "lightskyblue"
|
|
337
|
+
s = str.to_s.strip.downcase
|
|
338
|
+
s = s.tr("-", "_")
|
|
339
|
+
s = s.gsub(/\s+/, "")
|
|
340
|
+
s
|
|
239
341
|
end
|
|
240
|
-
end
|
|
241
342
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
end
|
|
343
|
+
def parse_hex(s)
|
|
344
|
+
# Accept "#rgb" or "#rrggbb"
|
|
345
|
+
hex = s
|
|
346
|
+
if hex.start_with?("#")
|
|
347
|
+
hex = hex[1..]
|
|
348
|
+
end
|
|
249
349
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
232 + n
|
|
350
|
+
if hex.match?(/\A[0-9a-f]{3}\z/i)
|
|
351
|
+
r = (hex[0] * 2).to_i(16)
|
|
352
|
+
g = (hex[1] * 2).to_i(16)
|
|
353
|
+
b = (hex[2] * 2).to_i(16)
|
|
354
|
+
[r, g, b]
|
|
355
|
+
elsif hex.match?(/\A[0-9a-f]{6}\z/i)
|
|
356
|
+
r = hex[0, 2].to_i(16)
|
|
357
|
+
g = hex[2, 2].to_i(16)
|
|
358
|
+
b = hex[4, 2].to_i(16)
|
|
359
|
+
[r, g, b]
|
|
360
|
+
end
|
|
262
361
|
end
|
|
263
|
-
end
|
|
264
362
|
|
|
265
|
-
|
|
266
|
-
i = idx.to_i
|
|
267
|
-
|
|
268
|
-
if i.between?(232, 255)
|
|
269
|
-
v = 8 + (i - 232) * 10
|
|
270
|
-
[v, v, v]
|
|
271
|
-
elsif i.between?(16, 231)
|
|
272
|
-
j = i - 16
|
|
273
|
-
r = j / 36
|
|
274
|
-
g = (j % 36) / 6
|
|
275
|
-
b = j % 6
|
|
363
|
+
def xterm_cube_index(r, g, b)
|
|
276
364
|
levels = [0, 95, 135, 175, 215, 255]
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
365
|
+
ri = nearest_index(levels, r)
|
|
366
|
+
gi = nearest_index(levels, g)
|
|
367
|
+
bi = nearest_index(levels, b)
|
|
368
|
+
16 + (36 * ri) + (6 * gi) + bi
|
|
281
369
|
end
|
|
282
|
-
end
|
|
283
370
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
371
|
+
def xterm_gray_index(r, g, b)
|
|
372
|
+
# grayscale ramp 232..255 maps to levels 8 + 10*n
|
|
373
|
+
avg = (r + g + b) / 3
|
|
374
|
+
if avg < 8
|
|
375
|
+
16 # near black; keep in extended palette, not ANSI black
|
|
376
|
+
elsif avg > 238
|
|
377
|
+
231 # near white; a cube white
|
|
378
|
+
else
|
|
379
|
+
n = ((avg - 8) / 10.0).round
|
|
380
|
+
n = 0 if n < 0
|
|
381
|
+
n = 23 if n > 23
|
|
382
|
+
232 + n
|
|
383
|
+
end
|
|
292
384
|
end
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def self.nearest_ansi_index(r, g, b)
|
|
296
|
-
best = 0
|
|
297
|
-
best_d = nil
|
|
298
385
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
386
|
+
def downmap_to_ansi16(idx)
|
|
387
|
+
if idx == DEFAULT_INDEX
|
|
388
|
+
DEFAULT_INDEX
|
|
389
|
+
elsif idx.between?(0, 15)
|
|
390
|
+
idx
|
|
391
|
+
else
|
|
392
|
+
rgb = xterm_rgb_for_index(idx)
|
|
393
|
+
nearest_ansi_index(rgb[0], rgb[1], rgb[2])
|
|
304
394
|
end
|
|
305
395
|
end
|
|
306
396
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def self.nearest_index(levels, value)
|
|
311
|
-
v = value.to_i
|
|
312
|
-
best_i = 0
|
|
313
|
-
best_d = nil
|
|
397
|
+
def nearest_ansi_index(r, g, b)
|
|
398
|
+
best = 0
|
|
399
|
+
best_d = nil
|
|
314
400
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
401
|
+
ANSI_RGB.each do |i, rgb|
|
|
402
|
+
d = dist2(r, g, b, rgb[0], rgb[1], rgb[2])
|
|
403
|
+
if best_d.nil? || d < best_d
|
|
404
|
+
best_d = d
|
|
405
|
+
best = i
|
|
406
|
+
end
|
|
320
407
|
end
|
|
408
|
+
|
|
409
|
+
best
|
|
321
410
|
end
|
|
322
411
|
|
|
323
|
-
|
|
324
|
-
|
|
412
|
+
def nearest_index(levels, value)
|
|
413
|
+
v = value.to_i
|
|
414
|
+
best_i = 0
|
|
415
|
+
best_d = nil
|
|
325
416
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
417
|
+
levels.each_with_index do |lvl, i|
|
|
418
|
+
d = (lvl - v).abs
|
|
419
|
+
if best_d.nil? || d < best_d
|
|
420
|
+
best_d = d
|
|
421
|
+
best_i = i
|
|
422
|
+
end
|
|
423
|
+
end
|
|
332
424
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
dg = g1 - g2
|
|
336
|
-
db = b1 - b2
|
|
337
|
-
(dr * dr) + (dg * dg) + (db * db)
|
|
338
|
-
end
|
|
425
|
+
best_i
|
|
426
|
+
end
|
|
339
427
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
428
|
+
def clamp_byte(v)
|
|
429
|
+
x = v.to_i
|
|
430
|
+
x = 0 if x < 0
|
|
431
|
+
x = 255 if x > 255
|
|
432
|
+
x
|
|
433
|
+
end
|
|
344
434
|
|
|
345
|
-
|
|
346
|
-
|
|
435
|
+
def dist2(r1, g1, b1, r2, g2, b2)
|
|
436
|
+
dr = r1 - r2
|
|
437
|
+
dg = g1 - g2
|
|
438
|
+
db = b1 - b2
|
|
439
|
+
(dr * dr) + (dg * dg) + (db * db)
|
|
440
|
+
end
|
|
347
441
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
442
|
+
def x11_rgb_for_name(name_norm)
|
|
443
|
+
table = x11_table
|
|
444
|
+
table[name_norm]
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def x11_table
|
|
448
|
+
path = RGB_TXT_PATH
|
|
449
|
+
|
|
450
|
+
if @x11_table_path != path
|
|
451
|
+
@x11_table = load_x11_rgb_txt(path)
|
|
452
|
+
@x11_table_path = path
|
|
453
|
+
end
|
|
454
|
+
@x11_table
|
|
351
455
|
end
|
|
352
|
-
@x11_table
|
|
353
|
-
end
|
|
354
456
|
|
|
355
|
-
|
|
356
|
-
|
|
457
|
+
def load_x11_rgb_txt(path)
|
|
458
|
+
table = {}
|
|
357
459
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
460
|
+
if File.file?(path)
|
|
461
|
+
File.foreach(path) do |line|
|
|
462
|
+
next if line.strip.empty?
|
|
463
|
+
next if line.lstrip.start_with?("!")
|
|
362
464
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
465
|
+
# Format: R G B <name...>
|
|
466
|
+
parts = line.strip.split(/\s+/)
|
|
467
|
+
next if parts.length < 4
|
|
366
468
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
469
|
+
r = parts[0].to_i
|
|
470
|
+
g = parts[1].to_i
|
|
471
|
+
b = parts[2].to_i
|
|
472
|
+
name = parts[3..].join(" ")
|
|
473
|
+
key = normalize_name(name)
|
|
474
|
+
table[key] = [r, g, b]
|
|
475
|
+
end
|
|
373
476
|
end
|
|
477
|
+
|
|
478
|
+
table
|
|
374
479
|
end
|
|
375
480
|
|
|
376
|
-
|
|
481
|
+
def load_x11_rgb_txt(path)
|
|
482
|
+
table = {}
|
|
483
|
+
|
|
484
|
+
if File.file?(path)
|
|
485
|
+
File.foreach(path) do |line|
|
|
486
|
+
next if line.strip.empty?
|
|
487
|
+
next if line.lstrip.start_with?("!")
|
|
488
|
+
next if line.lstrip.start_with?("#")
|
|
489
|
+
|
|
490
|
+
parts = line.strip.split(/\s+/, 4)
|
|
491
|
+
next unless parts.length == 4
|
|
492
|
+
next unless parts[0].match?(/\A\d+\z/)
|
|
493
|
+
next unless parts[1].match?(/\A\d+\z/)
|
|
494
|
+
next unless parts[2].match?(/\A\d+\z/)
|
|
495
|
+
|
|
496
|
+
r = parts[0].to_i
|
|
497
|
+
g = parts[1].to_i
|
|
498
|
+
b = parts[2].to_i
|
|
499
|
+
next unless r.between?(0, 255)
|
|
500
|
+
next unless g.between?(0, 255)
|
|
501
|
+
next unless b.between?(0, 255)
|
|
502
|
+
|
|
503
|
+
name = parts[3]
|
|
504
|
+
key = normalize_name(name)
|
|
505
|
+
table[key] = [r, g, b]
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
table
|
|
510
|
+
end
|
|
377
511
|
end
|
|
378
512
|
end
|
|
379
513
|
end
|