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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.org +11 -29
  3. data/README.md +2207 -18
  4. data/README.org +2066 -104
  5. data/exe/fatty +236 -93
  6. data/help/help.md +233 -0
  7. data/lib/fatty/action.rb +3 -7
  8. data/lib/fatty/actionable.rb +2 -0
  9. data/lib/fatty/alert.rb +65 -39
  10. data/lib/fatty/ansi/renderer.rb +26 -6
  11. data/lib/fatty/ansi.rb +1 -0
  12. data/lib/fatty/api/alert.rb +15 -0
  13. data/lib/fatty/api/choose.rb +145 -0
  14. data/lib/fatty/api/environment.rb +12 -0
  15. data/lib/fatty/api/keytest.rb +11 -0
  16. data/lib/fatty/api/menu.rb +104 -0
  17. data/lib/fatty/api/output.rb +55 -0
  18. data/lib/fatty/api/progress.rb +24 -0
  19. data/lib/fatty/api/prompt.rb +77 -0
  20. data/lib/fatty/api/status.rb +55 -0
  21. data/lib/fatty/api.rb +38 -0
  22. data/lib/fatty/callback_environment.rb +74 -0
  23. data/lib/fatty/colors/color.rb +366 -232
  24. data/lib/fatty/colors/pairs.rb +2 -0
  25. data/lib/fatty/command.rb +78 -0
  26. data/lib/fatty/config.rb +227 -39
  27. data/lib/fatty/config_files/config.yml +4 -12
  28. data/lib/fatty/config_files/keydefs.yml +13 -23
  29. data/lib/fatty/config_files/themes/capuchin_monk.yml +140 -0
  30. data/lib/fatty/config_files/themes/catppuccin_latte.yml +140 -0
  31. data/lib/fatty/config_files/themes/catppuccin_mocha.yml +140 -0
  32. data/lib/fatty/config_files/themes/cyberpunk.yml +141 -0
  33. data/lib/fatty/config_files/themes/dracula.yml +140 -0
  34. data/lib/fatty/config_files/themes/everforest_dark.yml +140 -0
  35. data/lib/fatty/config_files/themes/gruvbox_dark.yml +140 -0
  36. data/lib/fatty/config_files/themes/gruvbox_light.yml +140 -0
  37. data/lib/fatty/config_files/themes/mono.yml +119 -47
  38. data/lib/fatty/config_files/themes/monokai.yml +140 -0
  39. data/lib/fatty/config_files/themes/nordic.yml +1 -0
  40. data/lib/fatty/config_files/themes/onedark.yml +140 -0
  41. data/lib/fatty/config_files/themes/solarized_dark.yml +1 -0
  42. data/lib/fatty/config_files/themes/solarized_light.yml +140 -0
  43. data/lib/fatty/config_files/themes/terminal.yml +67 -0
  44. data/lib/fatty/config_files/themes/tokyo_night.yml +140 -0
  45. data/lib/fatty/config_files/themes/wordperfect.yml +80 -9
  46. data/lib/fatty/core_ext/hash.rb +19 -0
  47. data/lib/fatty/core_ext/string.rb +19 -12
  48. data/lib/fatty/core_ext.rb +1 -0
  49. data/lib/fatty/curses/context.rb +86 -84
  50. data/lib/fatty/curses/event_source.rb +128 -40
  51. data/lib/fatty/curses/key_decoder.rb +77 -11
  52. data/lib/fatty/env.rb +21 -20
  53. data/lib/fatty/help.rb +1 -29
  54. data/lib/fatty/history/entry.rb +2 -0
  55. data/lib/fatty/history.rb +17 -14
  56. data/lib/fatty/input_buffer.rb +71 -47
  57. data/lib/fatty/input_field.rb +216 -149
  58. data/lib/fatty/key_event.rb +67 -66
  59. data/lib/fatty/key_map.rb +54 -16
  60. data/lib/fatty/keymaps/emacs.rb +40 -6
  61. data/lib/fatty/log_formats/json.rb +2 -0
  62. data/lib/fatty/log_formats/text.rb +2 -0
  63. data/lib/fatty/logger.rb +7 -2
  64. data/lib/fatty/markdown/ansi_renderer.rb +75 -6
  65. data/lib/fatty/markdown/render.rb +2 -3
  66. data/lib/fatty/mouse_event.rb +142 -2
  67. data/lib/fatty/output_buffer.rb +2 -0
  68. data/lib/fatty/pager.rb +158 -47
  69. data/lib/fatty/progress.rb +417 -0
  70. data/lib/fatty/renderer/curses.rb +340 -184
  71. data/lib/fatty/renderer/truecolor.rb +266 -163
  72. data/lib/fatty/renderer.rb +115 -97
  73. data/lib/fatty/screen.rb +2 -0
  74. data/lib/fatty/session/alert_session.rb +39 -21
  75. data/lib/fatty/session/isearch_session.rb +92 -38
  76. data/lib/fatty/session/keytest_session.rb +105 -85
  77. data/lib/fatty/session/modal_session.rb +38 -1
  78. data/lib/fatty/session/output_session.rb +472 -21
  79. data/lib/fatty/session/popup_session.rb +251 -134
  80. data/lib/fatty/session/prompt_session.rb +68 -26
  81. data/lib/fatty/session/search_session.rb +79 -31
  82. data/lib/fatty/session/shell_session.rb +383 -368
  83. data/lib/fatty/session/status_session.rb +167 -0
  84. data/lib/fatty/session.rb +95 -95
  85. data/lib/fatty/terminal/popup_owner.rb +14 -11
  86. data/lib/fatty/terminal.rb +515 -813
  87. data/lib/fatty/themes/loader.rb +4 -2
  88. data/lib/fatty/themes/manager.rb +4 -0
  89. data/lib/fatty/themes/registry.rb +2 -0
  90. data/lib/fatty/themes/resolver.rb +175 -162
  91. data/lib/fatty/version.rb +1 -1
  92. data/lib/fatty/viewport.rb +8 -1
  93. data/lib/fatty.rb +35 -7
  94. metadata +42 -40
  95. data/.envrc +0 -2
  96. data/.simplecov +0 -23
  97. data/.yardopts +0 -4
  98. data/CHANGELOG.md +0 -34
  99. data/Rakefile +0 -15
  100. data/TODO.org +0 -163
  101. data/examples/markdown/native-markdown.md +0 -370
  102. data/examples/markdown/ox-gfm-markdown.md +0 -373
  103. data/examples/markdown/ox-gfm-markdown.org +0 -376
  104. data/fatty.gemspec +0 -42
  105. data/lib/fatty/accept_env.rb +0 -32
  106. data/lib/fatty/config_files/help.md +0 -120
  107. data/lib/fatty/config_files/help.org +0 -124
  108. data/lib/fatty/config_files/themes/wordperfect_light.yml +0 -77
  109. data/lib/fatty/menu_env.rb +0 -22
  110. data/lib/fatty/session/input_session.rb +0 -99
  111. data/lib/fatty/sessions.rb +0 -14
  112. data/lib/fatty/terminal/progress.rb +0 -374
  113. data/lib/fatty/view/alert_view.rb +0 -14
  114. data/lib/fatty/view/cursor_view.rb +0 -18
  115. data/lib/fatty/view/input_view.rb +0 -9
  116. data/lib/fatty/view/output_view.rb +0 -9
  117. data/lib/fatty/view/status_view.rb +0 -14
  118. data/lib/fatty/view.rb +0 -33
  119. data/lib/fatty/views.rb +0 -9
  120. data/sig/fatty.rbs +0 -4
@@ -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
- # Resolve a color specification to an integer color index suitable for curses init_pair.
78
- #
79
- # Accepts:
80
- # - Integer (e.g. 17, 226, -1)
81
- # - ANSI name (e.g. "yellow", "bright_blue", "default")
82
- # - Alias (e.g. "navy")
83
- # - Hex string (#RRGGBB or #RGB)
84
- # - X11 name (e.g. "MidnightBlue") resolved via bundled rgb.txt
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
- if idx == DEFAULT_INDEX
102
- idx
103
- else
104
- clamp_index(idx, available_colors: available_colors)
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
- def self.resolve_stringish(str, available_colors: 256)
109
- s = normalize_name(str)
110
- # Disambiguation prefixes:
111
- # - "ansi:yellow" forces ANSI 0..15 names
112
- # - "x11:yellow" forces X11 rgb.txt lookup
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
- # Without a prefix, prefer X11 names when 256 colors are available so
115
- # common names like "yellow" match X11 "#FFFF00" rather than ANSI 3.
116
- mode = nil
117
- if s.start_with?("ansi:")
118
- mode = :ansi
119
- s = s.delete_prefix("ansi:")
120
- elsif s.start_with?("x11:")
121
- mode = :x11
122
- s = s.delete_prefix("x11:")
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
- idx = ALIASES_256[s]
126
- return idx unless idx.nil?
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
- rgb = parse_hex(s)
129
- return xterm_index_for_rgb(rgb[0], rgb[1], rgb[2]) if rgb
196
+ cube = xterm_cube_index(rr, gg, bb)
197
+ gray = xterm_gray_index(rr, gg, bb)
130
198
 
131
- prefer_x11 = (mode == :x11) || (mode.nil? && available_colors.to_i >= 256)
199
+ cube_rgb = xterm_rgb_for_index(cube)
200
+ gray_rgb = xterm_rgb_for_index(gray)
132
201
 
133
- if prefer_x11
134
- rgb2 = x11_rgb_for_name(s)
135
- return xterm_index_for_rgb(rgb2[0], rgb2[1], rgb2[2]) if rgb2
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 = ANSI_NAMES[s]
139
- return idx unless idx.nil?
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
- unless prefer_x11
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
- # Unknown name: treat as default.
147
- DEFAULT_INDEX
148
- end
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
- def self.normalize_name(str)
151
- # normalize spaces/underscores/hyphens and case, so:
152
- # "Light Sky Blue" == "light_sky_blue" == "lightskyblue"
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
- def self.parse_hex(s)
160
- # Accept "#rgb" or "#rrggbb"
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
- if hex.match?(/\A[0-9a-f]{3}\z/i)
167
- r = (hex[0] * 2).to_i(16)
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
- def self.clamp_index(idx, available_colors:)
180
- max = available_colors.to_i - 1
181
- 0 if max < 0
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
- i = idx.to_i
269
+ def contrasting_ansi_index(index)
270
+ rgb = xterm_rgb_for_index(index)
271
+ fg = contrasting_rgb(rgb)
184
272
 
185
- if available_colors.to_i <= 16
186
- downmap_to_ansi16(i)
187
- else
188
- i.clamp(0, 255)
273
+ if fg == [0, 0, 0]
274
+ 0
275
+ else
276
+ 15
277
+ end
189
278
  end
190
- end
191
279
 
192
- def self.rgb(spec)
193
- if spec.is_a?(Integer)
194
- xterm_rgb_for_index(spec)
195
- elsif spec.nil?
196
- nil
197
- else
198
- s = normalize_name(spec.to_s)
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
- if idx
208
- xterm_rgb_for_index(idx)
209
- else
210
- parsed = parse_hex(s)
211
- parsed ||= x11_rgb_for_name(s)
212
- parsed ||= begin
213
- ansi = ANSI_NAMES[s]
214
- xterm_rgb_for_index(ansi) unless ansi == DEFAULT_INDEX || ansi.nil?
215
- end
216
- parsed
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
- # Convert any RGB to an xterm-256 index (16..255).
222
- # We consider both the 6x6x6 cube and the grayscale ramp and pick the closer.
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
- cube = xterm_cube_index(rr, gg, bb)
229
- gray = xterm_gray_index(rr, gg, bb)
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
- cube_rgb = xterm_rgb_for_index(cube)
232
- gray_rgb = xterm_rgb_for_index(gray)
330
+ # Unknown name: treat as default.
331
+ DEFAULT_INDEX
332
+ end
233
333
 
234
- if dist2(rr, gg, bb, gray_rgb[0], gray_rgb[1], gray_rgb[2]) <
235
- dist2(rr, gg, bb, cube_rgb[0], cube_rgb[1], cube_rgb[2])
236
- gray
237
- else
238
- cube
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
- def self.xterm_cube_index(r, g, b)
243
- levels = [0, 95, 135, 175, 215, 255]
244
- ri = nearest_index(levels, r)
245
- gi = nearest_index(levels, g)
246
- bi = nearest_index(levels, b)
247
- 16 + (36 * ri) + (6 * gi) + bi
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
- def self.xterm_gray_index(r, g, b)
251
- # grayscale ramp 232..255 maps to levels 8 + 10*n
252
- avg = (r + g + b) / 3
253
- if avg < 8
254
- 16 # near black; keep in extended palette, not ANSI black
255
- elsif avg > 238
256
- 231 # near white; a cube white
257
- else
258
- n = ((avg - 8) / 10.0).round
259
- n = 0 if n < 0
260
- n = 23 if n > 23
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
- def self.xterm_rgb_for_index(idx)
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
- [levels[r], levels[g], levels[b]]
278
- else
279
- # for 0..15 we use ANSI_RGB as an approximation
280
- ANSI_RGB[i] || [0, 0, 0]
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
- def self.downmap_to_ansi16(idx)
285
- if idx == DEFAULT_INDEX
286
- DEFAULT_INDEX
287
- elsif idx.between?(0, 15)
288
- idx
289
- else
290
- rgb = xterm_rgb_for_index(idx)
291
- nearest_ansi_index(rgb[0], rgb[1], rgb[2])
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
- ANSI_RGB.each do |i, rgb|
300
- d = dist2(r, g, b, rgb[0], rgb[1], rgb[2])
301
- if best_d.nil? || d < best_d
302
- best_d = d
303
- best = i
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
- best
308
- end
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
- levels.each_with_index do |lvl, i|
316
- d = (lvl - v).abs
317
- if best_d.nil? || d < best_d
318
- best_d = d
319
- best_i = i
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
- best_i
324
- end
412
+ def nearest_index(levels, value)
413
+ v = value.to_i
414
+ best_i = 0
415
+ best_d = nil
325
416
 
326
- def self.clamp_byte(v)
327
- x = v.to_i
328
- x = 0 if x < 0
329
- x = 255 if x > 255
330
- x
331
- end
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
- def self.dist2(r1, g1, b1, r2, g2, b2)
334
- dr = r1 - r2
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
- def self.x11_rgb_for_name(name_norm)
341
- table = x11_table
342
- table[name_norm]
343
- end
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
- def self.x11_table
346
- path = RGB_TXT_PATH
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
- if @x11_table_path != path
349
- @x11_table = load_x11_rgb_txt(path)
350
- @x11_table_path = path
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
- def self.load_x11_rgb_txt(path)
356
- table = {}
457
+ def load_x11_rgb_txt(path)
458
+ table = {}
357
459
 
358
- if File.file?(path)
359
- File.foreach(path) do |line|
360
- next if line.strip.empty?
361
- next if line.lstrip.start_with?("!")
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
- # Format: R G B <name...>
364
- parts = line.strip.split(/\s+/)
365
- next if parts.length < 4
465
+ # Format: R G B <name...>
466
+ parts = line.strip.split(/\s+/)
467
+ next if parts.length < 4
366
468
 
367
- r = parts[0].to_i
368
- g = parts[1].to_i
369
- b = parts[2].to_i
370
- name = parts[3..].join(" ")
371
- key = normalize_name(name)
372
- table[key] = [r, g, b]
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
- table
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