grmenu 4.0.3 → 5.0.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.
data/grmenu.rb CHANGED
@@ -1,21 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # ==============================================================================
4
+ # GRmenu - Motor de menús interactivos, widgets y diálogos para la consola.
5
+ #
6
+ # GRmenu proporciona una API rica y expresiva para crear interfaces de línea de
7
+ # comandos atractivas con bordes personalizables, temas CSS, soporte completo
8
+ # de color TrueColor de 24 bits, animaciones, ratón y componentes de interfaz
9
+ # (tablas, checkboxes, sliders, inputs, diálogos, barras de progreso y banners).
10
+ # ==============================================================================
11
+
3
12
  require 'io/console'
4
13
  require 'json'
5
14
  require 'zlib'
6
15
  require 'open3'
7
16
 
8
17
  class GRmenu
9
- VERSION = "4.0.3"
10
- CLEAR_SCREEN_SEQUENCE = "\e[H\e[2J\e[3J"
11
- HIDE_CURSOR = "\e[?25l"
12
- SHOW_CURSOR = "\e[?25h"
13
- CURSOR_HOME = "\e[H"
14
- CLEAR_TO_EOL = "\e[K"
15
- CLEAR_TO_EOS = "\e[J"
16
- ENABLE_MOUSE = "\e[?1000h\e[?1002h\e[?1006h"
17
- DISABLE_MOUSE = "\e[?1006l\e[?1002l\e[?1000l"
18
-
18
+ # Busca y resuelve rutas absolutas para los archivos de datos JSON (colores, bordes, fuentes).
19
+ # Inspecciona primero en el subdirectorio data/ relativo a este archivo, o en el nivel superior.
19
20
  def self.find_data_file(filename)
20
21
  local_path = File.expand_path("data/#{filename}", __dir__)
21
22
  return local_path if File.exist?(local_path)
@@ -24,30 +25,7 @@ class GRmenu
24
25
  nil
25
26
  end
26
27
 
27
- def self.enable_windows_vt
28
- return unless Gem.win_platform? || RUBY_PLATFORM =~ /mswin|mingw|cygwin/
29
- require 'fiddle'
30
- kernel32 = Fiddle.dlopen('kernel32')
31
- get_std_handle = Fiddle::Function.new(kernel32['GetStdHandle'], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP)
32
- get_console_mode = Fiddle::Function.new(kernel32['GetConsoleMode'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
33
- set_console_mode = Fiddle::Function.new(kernel32['SetConsoleMode'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
34
-
35
- h_out = get_std_handle.call(-11)
36
- out_mode = ' ' * 4
37
- if get_console_mode.call(h_out, out_mode) != 0
38
- m = out_mode.unpack1('L')
39
- set_console_mode.call(h_out, m | 0x0004)
40
- end
41
-
42
- h_in = get_std_handle.call(-10)
43
- in_mode = ' ' * 4
44
- if get_console_mode.call(h_in, in_mode) != 0
45
- m = in_mode.unpack1('L')
46
- set_console_mode.call(h_in, m | 0x0200)
47
- end
48
- rescue StandardError
49
- end
50
-
28
+ # Carga de manera segura un archivo JSON de configuración retornando un Hash.
51
29
  def self.load_json_data(filename)
52
30
  path = find_data_file(filename)
53
31
  return {} unless path && File.exist?(path)
@@ -56,14 +34,42 @@ class GRmenu
56
34
  {}
57
35
  end
58
36
 
37
+ # Catálogos maestros cargados 100% dinámicamente desde sus respectivos JSON.
38
+ # Cualquier estilo, borde o fuente nuevo que se agregue a los archivos JSON
39
+ # estará disponible de inmediato sin necesidad de tocar una sola línea de código Ruby.
59
40
  COLORS = load_json_data('colors.json').freeze
60
- BORDERS = load_json_data('borders.json').transform_keys(&:to_i).transform_values { |v| v.is_a?(Hash) ? v.transform_keys(&:to_sym) : { h: v.to_s, v: v.to_s, tl: v.to_s, tr: v.to_s, bl: v.to_s, br: v.to_s } }.freeze
41
+ BORDERS = load_json_data('borders.json').transform_keys(&:to_i).transform_values { |v|
42
+ v.is_a?(Hash) ? v.transform_keys(&:to_sym) : { h: v.to_s, v: v.to_s, tl: v.to_s, tr: v.to_s, bl: v.to_s, br: v.to_s }
43
+ }.freeze
61
44
  FONTS = load_json_data('fonts.json').transform_keys(&:to_i).freeze
62
45
 
46
+ # Diccionario de estilos clásicos derivado automáticamente de borders.json
47
+ STYLES = BORDERS.transform_values { |cfg| cfg[:tl] || cfg[:h] || "#" }.freeze
48
+
49
+ # Genera constantes FONT_1, FONT_2, ... dinámicamente según las fuentes presentes en fonts.json
50
+ FONTS.each do |font_id, font_data|
51
+ const_set(:"FONT_#{font_id}", font_data)
52
+ end
53
+
54
+ # Resuelve dinámicamente cualquier referencia a FONT_X en caso de fuentes agregadas en caliente
55
+ def self.const_missing(const_name)
56
+ if const_name.to_s =~ /\AFONT_(\d+)\z/
57
+ id = $1.to_i
58
+ font = FONTS[id] || {}
59
+ const_set(const_name, font)
60
+ font
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ # Mapa RGB base para interpolaciones y efectos cromáticos / neón
63
67
  BASE_RGB = COLORS.each_with_object({}) do |(name, val), h|
64
68
  next if name == "reset"
65
69
  code = val.is_a?(Hash) ? (val["2"] || val[2] || val["1"] || val[1]) : val.to_s
66
- if code =~ /38;2;(\d+);(\d+);(\d+)/
70
+ if name == "black" || name == "negro"
71
+ h[name] = [0, 0, 0]
72
+ elsif code =~ /38;2;(\d+);(\d+);(\d+)/
67
73
  h[name] = [$1.to_i, $2.to_i, $3.to_i]
68
74
  elsif code == "90m" || code == "30m"
69
75
  h[name] = [100, 100, 100]
@@ -86,4173 +92,56 @@ class GRmenu
86
92
  else
87
93
  h[name] = [220, 220, 220]
88
94
  end
89
- end.freeze
90
-
91
- FONT_1 = FONTS[1] || {}
92
- FONT_2 = FONTS[2] || {}
93
- FONT_3 = FONTS[3] || {}
94
- FONT_4 = FONTS[4] || {}
95
- FONT_5 = FONTS[5] || {}
96
- FONT_6 = FONTS[6] || {}
97
- FONT_7 = FONTS[7] || {}
98
- FONT_8 = FONTS[8] || {}
99
- FONT_9 = FONTS[9] || {}
100
- FONT_10 = FONTS[10] || {}
101
-
102
- def self.rgb_color(tick, offset = 0.0)
103
- t = tick.to_f + offset.to_f
104
- r = (Math.sin(t) * 127 + 128).clamp(0, 255).to_i
105
- g = (Math.sin(t + 2.0943951) * 127 + 128).clamp(0, 255).to_i
106
- b = (Math.sin(t + 4.1887902) * 127 + 128).clamp(0, 255).to_i
107
- "\e[38;2;#{r};#{g};#{b}m"
108
- end
109
-
110
- def self.ansi_color(color_name, level = 1)
111
- name = color_name.to_s.downcase.strip
112
- if name.include?(":")
113
- parts = name.split(":")
114
- name = parts[0].strip
115
- level = parts[1].to_i if parts[1] && !parts[1].empty?
116
- end
117
- return rgb_color(0.0) if name == "rgb" || name == "rainbow" || name == "chroma"
118
- if name =~ /\A#?([0-9a-f]{6})\z/i
119
- hex = $1
120
- r = hex[0..1].to_i(16)
121
- g = hex[2..3].to_i(16)
122
- b = hex[4..5].to_i(16)
123
- return "\e[38;2;#{r};#{g};#{b}m"
124
- elsif name =~ /\A#?([0-9a-f]{3})\z/i
125
- hex = $1
126
- r = (hex[0] * 2).to_i(16)
127
- g = (hex[1] * 2).to_i(16)
128
- b = (hex[2] * 2).to_i(16)
129
- return "\e[38;2;#{r};#{g};#{b}m"
130
- end
131
- lvl_str = level.to_s
132
- code_raw = COLORS.dig(name, lvl_str) || COLORS.dig(name, level.to_i) || COLORS[name]
133
- return "\e[#{code_raw}" if code_raw
134
- "\e[37m"
135
- end
136
-
137
- def self.ansi_reset
138
- "\e[0m"
139
- end
140
-
141
- module Color
142
- RESET = "\e[0m"
143
- BOLD = "\e[1m"
144
- module_function
145
-
146
- def paint(text, color_name, level = 1)
147
- c_str = color_name.to_s.downcase
148
- if c_str == "rgb" || c_str == "rainbow" || c_str == "chroma"
149
- return rgb(text)
150
- end
151
- code = GRmenu.ansi_color(color_name, level) || "\e[37m"
152
- "#{code}#{text}#{RESET}"
153
- end
154
-
155
- def rgb(text, offset = 0.0)
156
- out = String.new("")
157
- idx = 0
158
- in_escape = false
159
- escape_buf = String.new("")
160
-
161
- text.to_s.each_char do |ch|
162
- if ch == "\e"
163
- in_escape = true
164
- escape_buf << ch
165
- next
166
- end
167
- if in_escape
168
- escape_buf << ch
169
- if ch =~ /[a-zA-Z]/
170
- in_escape = false
171
- out << escape_buf
172
- escape_buf.clear
173
- end
174
- next
175
- end
176
-
177
- if ch == " " || ch == "\n" || ch == "\r" || ch == "\t"
178
- out << ch
179
- else
180
- t = idx * 0.12 + offset
181
- r = (Math.sin(t) * 127 + 128).clamp(0, 255).to_i
182
- g = (Math.sin(t + 2.0943951) * 127 + 128).clamp(0, 255).to_i
183
- b = (Math.sin(t + 4.1887902) * 127 + 128).clamp(0, 255).to_i
184
- out << "\e[38;2;#{r};#{g};#{b}m#{ch}"
185
- idx += 1
186
- end
187
- end
188
- out << RESET
189
- out
190
- end
191
-
192
- def red(s); paint(s, :red, 1); end
193
- def bright_red(s); paint(s, :red, 2); end
194
- def dark_red(s); paint(s, :red, 1); end
195
-
196
- def green(s); paint(s, :green, 1); end
197
- def bright_green(s); paint(s, :green, 2); end
198
- def dark_green(s); paint(s, :green, 1); end
199
-
200
- def yellow(s); paint(s, :yellow, 1); end
201
- def bright_yellow(s); paint(s, :yellow, 2); end
202
-
203
- def blue(s); paint(s, :blue, 1); end
204
- def bright_blue(s); paint(s, :blue, 2); end
205
-
206
- def magenta(s); paint(s, :magenta, 1); end
207
- def bright_magenta(s); paint(s, :magenta, 2); end
208
-
209
- def purple(s); paint(s, :purple, 1); end
210
- def bright_purple(s); paint(s, :purple, 2); end
211
-
212
- def pink(s); paint(s, :pink, 1); end
213
- def bright_pink(s); paint(s, :pink, 2); end
214
-
215
- def cyan(s); paint(s, :cyan, 1); end
216
- def bright_cyan(s); paint(s, :cyan, 2); end
217
-
218
- def aqua(s); paint(s, :aqua, 1); end
219
- def bright_aqua(s); paint(s, :aqua, 2); end
220
-
221
- def orange(s); paint(s, :orange, 1); end
222
- def bright_orange(s); paint(s, :orange, 2); end
223
-
224
- def white(s); paint(s, :white, 1); end
225
- def bright_white(s); paint(s, :white, 2); end
226
-
227
- def black(s); paint(s, :black, 1); end
228
- def gray(s); paint(s, :gray, 1); end
229
- def bright_gray(s); paint(s, :gray, 2); end
230
- def grey(s); gray(s); end
231
-
232
- def neon_red(s); paint(s, :neon_red, 2); end
233
- def neon_green(s); paint(s, :neon_green, 2); end
234
- def neon_cyan(s); paint(s, :neon_cyan, 2); end
235
- def neon_blue(s); paint(s, :neon_blue, 2); end
236
- def neon_pink(s); paint(s, :neon_pink, 2); end
237
- def neon_yellow(s); paint(s, :neon_yellow, 2); end
238
- def neon_orange(s); paint(s, :neon_orange, 2); end
239
- def neon_purple(s); paint(s, :neon_purple, 2); end
240
- def neon_magenta(s); paint(s, :neon_magenta, 2); end
241
- def neon_aqua(s); paint(s, :neon_aqua, 2); end
242
- def neon_lime(s); paint(s, :neon_lime, 2); end
243
- def neon_white(s); paint(s, :neon_white, 2); end
244
-
245
- def r(s); bright_red(s); end
246
- def dr(s); dark_red(s); end
247
- def g(s); bright_green(s); end
248
- def y(s); bright_yellow(s); end
249
- def w(s); bright_white(s); end
250
- def gr(s); gray(s); end
251
- def cy(s); bright_cyan(s); end
252
- def mg(s); bright_magenta(s); end
253
- def bl(s); bright_blue(s); end
254
-
255
- def hex(code, text)
256
- c = GRmenu.ansi_color(code.to_s)
257
- "#{c}#{text}#{RESET}"
258
- end
259
-
260
- def respond_to_missing?(method_name, include_private = false)
261
- GRmenu::COLORS.key?(method_name.to_s) || super
262
- end
263
-
264
- def method_missing(method_name, *args, &block)
265
- m_str = method_name.to_s
266
- if GRmenu::COLORS.key?(m_str)
267
- text = args[0].to_s
268
- lvl = args[1] || 2
269
- paint(text, m_str, lvl)
270
- else
271
- super
272
- end
273
- end
274
- end
275
- C = Color
276
-
277
- STYLES = {
278
- 1 => "#", 2 => "┌", 3 => "╔", 4 => "┏", 5 => "╒",
279
- 6 => "╓", 7 => "╭", 8 => "▛", 9 => "▓", 10 => "▒",
280
- 11 => "░", 12 => "█", 13 => "*", 14 => "+", 15 => "=",
281
- 16 => "~", 17 => "-", 18 => "◆", 19 => "●", 20 => "★"
282
- }.freeze
283
-
284
- class PNGDecoder
285
- attr_reader :width, :height, :pixels
286
-
287
- def self.load(filepath)
288
- return nil unless filepath && File.exist?(filepath)
289
- new.parse(File.binread(filepath))
290
- rescue StandardError
291
- nil
292
- end
293
-
294
- def parse(data)
295
- return nil unless data && data[0, 8] == "\x89PNG\r\n\x1a\n".b
296
-
297
- offset = 8
298
- idat_data = String.new("".b)
299
- palette = nil
300
-
301
- while offset < data.bytesize
302
- len = data[offset, 4].unpack1("N")
303
- type = data[offset + 4, 4]
304
- chunk_data = data[offset + 8, len]
305
- offset += 12 + len
306
-
307
- case type
308
- when "IHDR"
309
- @width, @height, @bit_depth, @color_type = chunk_data.unpack("NNCC")
310
- when "PLTE"
311
- palette = chunk_data.bytes.each_slice(3).to_a
312
- when "IDAT"
313
- idat_data << chunk_data
314
- when "IEND"
315
- break
316
- end
317
- end
318
-
319
- channels = case @color_type
320
- when 0 then 1
321
- when 2 then 3
322
- when 3 then 1
323
- when 4 then 2
324
- when 6 then 4
325
- else return nil
326
- end
327
-
328
- bpp = [(@bit_depth * channels + 7) / 8, 1].max
329
- stride = (@width * channels * @bit_depth + 7) / 8
330
- scanline_len = stride + 1
331
-
332
- raw = Zlib::Inflate.inflate(idat_data)
333
- raw_bytes = raw.bytes
334
- return nil if raw_bytes.length < (@height * scanline_len)
335
-
336
- @pixels = Array.new(@height) { Array.new(@width) }
337
- prev_row = Array.new(stride, 0)
338
-
339
- @height.times do |y|
340
- row_start = y * scanline_len
341
- filter_type = raw_bytes[row_start]
342
- curr_filtered = raw_bytes[(row_start + 1)...(row_start + scanline_len)]
343
- curr_recon = Array.new(stride, 0)
344
-
345
- stride.times do |i|
346
- a = (i >= bpp) ? curr_recon[i - bpp] : 0
347
- b = prev_row[i]
348
- c = (i >= bpp) ? prev_row[i - bpp] : 0
349
- x = curr_filtered[i]
350
-
351
- recon_val = case filter_type
352
- when 0 then x
353
- when 1 then (x + a) & 0xFF
354
- when 2 then (x + b) & 0xFF
355
- when 3 then (x + ((a + b) / 2)) & 0xFF
356
- when 4
357
- p_val = a + b - c
358
- pa = (p_val - a).abs
359
- pb = (p_val - b).abs
360
- pc = (p_val - c).abs
361
- pr = if pa <= pb && pa <= pc
362
- a
363
- elsif pb <= pc
364
- b
365
- else
366
- c
367
- end
368
- (x + pr) & 0xFF
369
- else x
370
- end
371
- curr_recon[i] = recon_val
372
- end
373
-
374
- prev_row = curr_recon
375
-
376
- if @bit_depth == 16
377
- @width.times do |x|
378
- idx = x * channels * 2
379
- r = curr_recon[idx]
380
- g = (channels >= 3) ? curr_recon[idx + 2] : r
381
- b = (channels >= 3) ? curr_recon[idx + 4] : r
382
- a = (channels == 4) ? curr_recon[idx + 6] : (channels == 2 ? curr_recon[idx + 2] : 255)
383
- @pixels[y][x] = [r, g, b, a]
384
- end
385
- elsif @bit_depth == 8
386
- @width.times do |x|
387
- idx = x * channels
388
- if @color_type == 3
389
- p_idx = curr_recon[idx]
390
- rgb_val = palette ? (palette[p_idx] || [0, 0, 0]) : [0, 0, 0]
391
- @pixels[y][x] = [rgb_val[0], rgb_val[1], rgb_val[2], 255]
392
- else
393
- r = curr_recon[idx]
394
- g = (channels >= 3) ? curr_recon[idx + 1] : r
395
- b = (channels >= 3) ? curr_recon[idx + 2] : r
396
- a = (channels == 4 || channels == 2) ? curr_recon[idx + channels - 1] : 255
397
- @pixels[y][x] = [r, g, b, a]
398
- end
399
- end
400
- end
401
- end
402
- self
403
- end
404
-
405
- def resample(target_w, target_h)
406
- resampled = Array.new(target_h) { Array.new(target_w) }
407
- x_step = @width.to_f / target_w
408
- y_step = @height.to_f / target_h
409
-
410
- target_h.times do |ty|
411
- sy_start = (ty * y_step).to_i
412
- sy_end = [((ty + 1) * y_step).to_i, @height].min
413
-
414
- target_w.times do |tx|
415
- sx_start = (tx * x_step).to_i
416
- sx_end = [((tx + 1) * x_step).to_i, @width].min
417
-
418
- r_sum = g_sum = b_sum = a_sum = count = 0
419
-
420
- (sy_start...sy_end).each do |sy|
421
- (sx_start...sx_end).each do |sx|
422
- p = @pixels[sy][sx]
423
- next unless p
424
- if p[3] > 10
425
- r_sum += p[0]
426
- g_sum += p[1]
427
- b_sum += p[2]
428
- a_sum += p[3]
429
- count += 1
430
- end
431
- end
432
- end
433
-
434
- if count > 0
435
- resampled[ty][tx] = [(r_sum / count).clamp(0, 255), (g_sum / count).clamp(0, 255), (b_sum / count).clamp(0, 255), (a_sum / count).clamp(0, 255)]
436
- else
437
- mid_y = (sy_start + sy_end) / 2
438
- mid_x = (sx_start + sx_end) / 2
439
- resampled[ty][tx] = @pixels[mid_y][mid_x] || [0, 0, 0, 0]
440
- end
441
- end
442
- end
443
- resampled
444
- end
445
-
446
- def render_ansi_lines(target_w = 40, target_h = nil)
447
- target_h ||= [((@height.to_f / @width) * target_w).round, 2].max
448
- target_h += 1 if target_h.odd?
449
-
450
- grid = resample(target_w, target_h)
451
- lines = []
452
-
453
- (0...target_h).step(2) do |y|
454
- row_top = grid[y]
455
- row_bot = grid[y + 1] || grid[y]
456
- line = String.new("")
457
-
458
- target_w.times do |x|
459
- r1, g1, b1, a1 = row_top[x]
460
- r2, g2, b2, a2 = row_bot[x]
461
-
462
- if a1 < 32 && a2 < 32
463
- line << "\e[0m "
464
- elsif a1 < 32
465
- line << "\e[0m\e[38;2;#{r2};#{g2};#{b2}m▄"
466
- elsif a2 < 32
467
- line << "\e[0m\e[38;2;#{r1};#{g1};#{b1}m▀"
468
- else
469
- line << "\e[38;2;#{r1};#{g1};#{b1}m\e[48;2;#{r2};#{g2};#{b2}m▀"
470
- end
471
- end
472
- line << "\e[0m"
473
- lines << line
474
- end
475
-
476
- lines
477
- end
478
- end
479
-
480
- def self.char_width(char)
481
- code = char.ord
482
- return 0 if code == 0 || code == 0xFE0F || code == 0xFE0E || (code >= 0x0300 && code <= 0x036F) || (code >= 0x200B && code <= 0x200F)
483
- return 0 if code < 32 || (code >= 0x7F && code < 0xA0)
484
- return 1 if code == 0x1F5BC || code == 0x1F5B4 || code == 0x1F5B5 || code == 0x1F5C2
485
- if (code >= 0x1100 && code <= 0x115F) ||
486
- (code >= 0x2329 && code <= 0x232A) ||
487
- (code >= 0x2E80 && code <= 0xA4CF && code != 0x303F) ||
488
- (code >= 0xAC00 && code <= 0xD7A3) ||
489
- (code >= 0xF900 && code <= 0xFAFF) ||
490
- (code >= 0xFE10 && code <= 0xFE19) ||
491
- (code >= 0xFE30 && code <= 0xFE6F) ||
492
- (code >= 0xFF01 && code <= 0xFF60) ||
493
- (code >= 0xFFE0 && code <= 0xFFE6) ||
494
- (code >= 0x1F300 && code <= 0x1F6FF) ||
495
- (code >= 0x1F900 && code <= 0x1FAFF)
496
- 2
497
- else
498
- 1
499
- end
500
- end
501
-
502
- def self.display_width(str)
503
- clean = str.to_s.gsub(/\e\[[0-9;]*[a-zA-Z]/, '')
504
- clean.chars.map { |c| char_width(c) }.sum
505
- end
506
-
507
- def self.pad_to_width(str, target_width, align = :left)
508
- current_w = display_width(str)
509
- pad_needed = [target_width - current_w, 0].max
510
- case align
511
- when :right
512
- (" " * pad_needed) + str.to_s
513
- when :center
514
- left_pad = " " * (pad_needed / 2)
515
- right_pad = " " * (pad_needed - (pad_needed / 2))
516
- left_pad + str.to_s + right_pad
517
- else
518
- str.to_s + (" " * pad_needed)
519
- end
520
- end
521
-
522
- def self.load_and_render_image(filepath, width = 40, height = nil, max_cols = terminal_width)
523
- return [] unless filepath && File.exist?(filepath)
524
-
525
- req_w = [width.to_i, max_cols - 6].min
526
- req_w = [req_w, 10].max
527
-
528
- conv_bin = `which convert 2>/dev/null`.strip
529
- conv_bin = `which magick 2>/dev/null`.strip if conv_bin.empty?
530
-
531
- if !conv_bin.empty?
532
- info, _ = Open3.capture2("identify", "-format", "%w %h", filepath) rescue ["", nil]
533
- orig_w, orig_h = info.strip.split.map(&:to_f)
534
- aspect = (orig_w && orig_w > 0) ? (orig_h / orig_w) : 0.6
535
- scale_h = height || (req_w * aspect).round
536
- scale_h += 1 if scale_h.odd?
537
- scale_h = [scale_h, 2].max
538
-
539
- cmd = [conv_bin, filepath, "-filter", "Lanczos", "-resize", "#{req_w}x#{scale_h}!", "-depth", "8", "rgba:-"]
540
- stdout, status = Open3.capture2(*cmd) rescue [nil, nil]
541
- if status && status.success? && stdout.bytesize == (req_w * scale_h * 4)
542
- raw = stdout.bytes
543
- lines = []
544
- (0...scale_h).step(2) do |y|
545
- line = String.new("")
546
- req_w.times do |x|
547
- top_idx = (y * req_w + x) * 4
548
- bot_idx = ((y + 1) * req_w + x) * 4
549
- r1, g1, b1, a1 = raw[top_idx, 4]
550
- r2, g2, b2, a2 = raw[bot_idx, 4]
551
-
552
- if a1 < 32 && a2 < 32
553
- line << "\e[0m "
554
- elsif a1 < 32
555
- line << "\e[0m\e[38;2;#{r2};#{g2};#{b2}m▄"
556
- elsif a2 < 32
557
- line << "\e[0m\e[38;2;#{r1};#{g1};#{b1}m▀"
558
- else
559
- line << "\e[38;2;#{r1};#{g1};#{b1}m\e[48;2;#{r2};#{g2};#{b2}m▀"
560
- end
561
- end
562
- line << "\e[0m"
563
- lines << line
564
- end
565
- return lines
566
- end
567
- end
568
-
569
- png = PNGDecoder.load(filepath)
570
- if png
571
- return png.render_ansi_lines(req_w, height)
572
- end
573
-
574
- []
575
- rescue StandardError
576
- []
577
- end
578
-
579
- def self.image(filepath, width: 40, height: nil, style: 3, color: "cyan", center: true)
580
- term_w = terminal_width
581
- raw_lines = load_and_render_image(filepath, width, height, term_w)
582
- return nil if raw_lines.empty?
583
-
584
- img_w = display_width(raw_lines.first)
585
- box_w = img_w + 4
586
- margin = (center && term_w > box_w) ? (" " * ((term_w - box_w) / 2)) : ""
587
-
588
- if style && style > 0
589
- border_cfg = BORDERS[style] || BORDERS[3]
590
- is_rgb = (color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma")
591
- color_code = is_rgb ? "" : ansi_color(color, 2)
592
- reset_code = ansi_reset
593
-
594
- h_top = border_cfg[:ht] || border_cfg[:h]
595
- h_bot = border_cfg[:hb] || border_cfg[:h]
596
- v_l = border_cfg[:vl] || border_cfg[:v]
597
- v_r = border_cfg[:vr] || border_cfg[:v]
598
-
599
- top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
600
- bot_fill = (h_bot * ((box_w - 2).to_f / h_bot.length).ceil)[0...(box_w - 2)]
601
-
602
- if is_rgb
603
- Kernel.print("#{margin}#{Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")}\r\n")
604
- raw_lines.each do |line|
605
- Kernel.print("#{margin}#{Color.rgb(v_l)} #{line} #{Color.rgb(v_r)}\r\n")
606
- end
607
- Kernel.print("#{margin}#{Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")}\r\n")
608
- else
609
- Kernel.print("#{margin}#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
610
- raw_lines.each do |line|
611
- Kernel.print("#{margin}#{color_code}#{v_l}#{reset_code} #{line} #{color_code}#{v_r}#{reset_code}\r\n")
612
- end
613
- Kernel.print("#{margin}#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
614
- end
615
- else
616
- raw_lines.each do |line|
617
- Kernel.print("#{margin}#{line}\r\n")
618
- end
619
- end
620
-
621
- true
622
- end
623
-
624
- class ProgressBar
625
- attr_reader :total, :current, :title, :status
626
-
627
- def initialize(total = 100, title: nil, color: "cyan", level: 2, style: 3, width: nil)
628
- @total = [total.to_i, 1].max
629
- @current = 0
630
- @title = title
631
- @status = String.new("")
632
- @color = color.to_s.downcase
633
- @level = level.to_i
634
- @style = style.to_i
635
- @width = width
636
- @closed = false
637
- @drawn_lines_count = 0
638
- end
639
-
640
- def advance(step = 1, status: nil)
641
- return if @closed
642
- @current = [(@current + step), @total].min
643
- @status = status.to_s if status
644
- render
645
- end
646
- alias_method :increment, :advance
647
- alias_method :step, :advance
648
-
649
- def set(value, status: nil)
650
- return if @closed
651
- @current = [[value.to_i, 0].max, @total].min
652
- @status = status.to_s if status
653
- render
654
- end
655
-
656
- def render
657
- term_w = GRmenu.terminal_width
658
- box_w = @width || [term_w - 4, 60].min
659
- box_w = [box_w, 36].max
660
-
661
- is_rgb = (@color == "rgb" || @color == "rainbow" || @color == "chroma")
662
- tick = (@current.to_f / @total) * 6.2831853
663
-
664
- border_cfg = GRmenu::BORDERS[@style] || GRmenu::BORDERS[3]
665
-
666
- v_l = border_cfg[:vl] || border_cfg[:v]
667
- v_r = border_cfg[:vr] || border_cfg[:v]
668
- h_t = border_cfg[:ht] || border_cfg[:h]
669
- h_b = border_cfg[:hb] || border_cfg[:h]
670
-
671
- top_fill = (h_t * ((box_w - 2).to_f / h_t.length).ceil)[0...(box_w - 2)]
672
- bot_fill = (h_b * ((box_w - 2).to_f / h_b.length).ceil)[0...(box_w - 2)]
673
-
674
- pct = ((@current.to_f / @total) * 100).round
675
- pct_str = "#{pct}% (#{@current}/#{@total})"
676
-
677
- inner_w = box_w - 4
678
- bar_w = [inner_w - pct_str.length - 3, 10].max
679
- filled_len = ((@current.to_f / @total) * bar_w).round
680
- empty_len = bar_w - filled_len
681
-
682
- lines = []
683
- if is_rgb
684
- lines << Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}", tick)
685
- if @title && !@title.empty?
686
- t_str = @title.to_s
687
- if GRmenu.display_width(t_str) > inner_w
688
- t_str = t_str[0...[inner_w - 3, 1].max] + "..."
689
- end
690
- pad_t = [inner_w - GRmenu.display_width(t_str), 0].max
691
- l_p = " " * (pad_t / 2)
692
- r_p = " " * (pad_t - (pad_t / 2))
693
- lines << "#{Color.rgb(v_l, tick)} #{l_p}#{Color.rgb(t_str, tick + 0.4)}#{r_p} #{Color.rgb(v_r, tick)}"
694
- lines << Color.rgb("#{v_l}#{top_fill}#{v_r}", tick)
695
- end
696
-
697
- filled_part = Color.rgb("█" * filled_len, tick)
698
- empty_part = Color.gray("░" * empty_len)
699
- bar_raw_len = 2 + filled_len + empty_len + 1 + pct_str.length
700
- pad_bar_len = [inner_w - bar_raw_len, 0].max
701
- bar_line = "[#{filled_part}#{empty_part}] #{Color.bright_white(pct_str)}" + (" " * pad_bar_len)
702
-
703
- lines << "#{Color.rgb(v_l, tick)} #{bar_line} #{Color.rgb(v_r, tick)}"
704
- if @status && !@status.empty?
705
- st_str = @status.to_s
706
- if GRmenu.display_width(st_str) > inner_w
707
- st_str = st_str[0...[inner_w - 3, 1].max] + "..."
708
- end
709
- pad_st = [inner_w - GRmenu.display_width(st_str), 0].max
710
- st_line = st_str + (" " * pad_st)
711
- lines << "#{Color.rgb(v_l, tick)} #{Color.gray(st_line)} #{Color.rgb(v_r, tick)}"
712
- end
713
- lines << Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}", tick)
714
- else
715
- color_code = GRmenu.ansi_color(@color, @level)
716
- reset_code = GRmenu.ansi_reset
717
-
718
- bar_str = "[#{"█" * filled_len}#{"░" * empty_len}] #{pct_str}"
719
- pad_bar_len = [inner_w - GRmenu.display_width(bar_str), 0].max
720
- bar_line = bar_str + (" " * pad_bar_len)
721
-
722
- lines << "#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}"
723
- if @title && !@title.empty?
724
- t_str = @title.to_s
725
- if GRmenu.display_width(t_str) > inner_w
726
- t_str = t_str[0...[inner_w - 3, 1].max] + "..."
727
- end
728
- pad_t = [inner_w - GRmenu.display_width(t_str), 0].max
729
- l_p = " " * (pad_t / 2)
730
- r_p = " " * (pad_t - (pad_t / 2))
731
- lines << "#{color_code}#{v_l}#{reset_code} #{l_p}#{t_str}#{r_p} #{color_code}#{v_r}#{reset_code}"
732
- lines << "#{color_code}#{v_l}#{top_fill}#{v_r}#{reset_code}"
733
- end
734
- lines << "#{color_code}#{v_l}#{reset_code} #{color_code}#{bar_line}#{reset_code} #{color_code}#{v_r}#{reset_code}"
735
- if @status && !@status.empty?
736
- st_str = @status.to_s
737
- if GRmenu.display_width(st_str) > inner_w
738
- st_str = st_str[0...[inner_w - 3, 1].max] + "..."
739
- end
740
- pad_st = [inner_w - GRmenu.display_width(st_str), 0].max
741
- st_line = st_str + (" " * pad_st)
742
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.gray(st_line)} #{color_code}#{v_r}#{reset_code}"
743
- end
744
- lines << "#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}"
745
- end
746
-
747
- frame = lines.join("\r\n") + "\r\n"
748
-
749
- if @drawn_lines_count && @drawn_lines_count > 0
750
- Kernel.print("\e[#{@drawn_lines_count}A\e[J")
751
- end
752
- Kernel.print(frame)
753
- $stdout.flush
754
- @drawn_lines_count = lines.length
755
- end
756
-
757
- def finish(status: "¡Completado!")
758
- return if @closed
759
- set(@total, status: status)
760
- @closed = true
761
- Kernel.print(GRmenu::SHOW_CURSOR)
762
- end
763
- end
764
-
765
- def self.spinner(message_arg = nil, message: nil, color: "cyan", level: 2, delay: 0.08, &block)
766
- actual_message = message || message_arg || "Cargando..."
767
- frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
768
- is_rgb = (color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma")
769
- color_code = is_rgb ? "" : ansi_color(color, level)
770
- reset_code = ansi_reset
771
-
772
- stop_spinner = false
773
- spinner_thread = Thread.new do
774
- frame_idx = 0
775
- while !stop_spinner
776
- f = frames[frame_idx % frames.length]
777
- f_color = is_rgb ? rgb_color(frame_idx * 0.3) : color_code
778
- msg_out = is_rgb ? Color.rgb(actual_message, frame_idx * 0.1) : actual_message
779
- Kernel.print("\r\e[K#{f_color}#{f}#{reset_code} #{msg_out}")
780
- $stdout.flush
781
- frame_idx += 1
782
- sleep(delay)
783
- end
784
- end
785
-
786
- begin
787
- Kernel.print(HIDE_CURSOR)
788
- result = block ? block.call : nil
789
- stop_spinner = true
790
- spinner_thread.join
791
- success_color = ansi_color("green", 2)
792
- Kernel.print("\r\e[K#{success_color}[OK]#{reset_code} #{actual_message} #{Color.gray("Listo!")}\r\n")
793
- result
794
- rescue Exception => e
795
- stop_spinner = true
796
- spinner_thread.join rescue nil
797
- error_color = ansi_color("red", 2)
798
- Kernel.print("\r\e[K#{error_color}[ERROR]#{reset_code} #{actual_message} #{Color.bright_red("(Error: #{e.message})")}\r\n")
799
- raise e
800
- ensure
801
- stop_spinner = true
802
- Kernel.print(SHOW_CURSOR)
803
- end
804
- end
805
-
806
- def self.progress(total_arg = nil, total: nil, title: nil, color: "cyan", level: 2, style: 3, width: nil, &block)
807
- actual_total = total || total_arg || 100
808
- bar = ProgressBar.new(actual_total, title: title, color: color, level: level, style: style, width: width)
809
- Kernel.print(HIDE_CURSOR)
810
- bar.render
811
- begin
812
- result = block ? block.call(bar) : bar
813
- bar.finish
814
- result
815
- ensure
816
- Kernel.print(SHOW_CURSOR)
817
- end
818
- end
819
-
820
- def self.confirm(question_arg = nil, question: nil, default: true, color: "cyan", style: 3)
821
- actual_question = question || question_arg || "¿Confirmar acción?"
822
- choice = default ? 0 : 1
823
- term_w = terminal_width
824
- q_w = display_width(actual_question)
825
- box_w = [q_w + 8, term_w - 4, 38].max
826
- box_w = [box_w, 64].min
827
- inner_w = box_w - 4
828
-
829
- border_cfg = BORDERS[style] || BORDERS[3]
830
- h_top = border_cfg[:ht] || border_cfg[:h]
831
- h_bot = border_cfg[:hb] || border_cfg[:h]
832
- v_l = border_cfg[:vl] || border_cfg[:v]
833
- v_r = border_cfg[:vr] || border_cfg[:v]
834
-
835
- is_rgb = (color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma")
836
- color_code = is_rgb ? "" : ansi_color(color, 2)
837
- reset_code = ansi_reset
838
-
839
- top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
840
- bot_fill = (h_bot * ((box_w - 2).to_f / h_bot.length).ceil)[0...(box_w - 2)]
841
-
842
- drawn_lines = 0
843
-
844
- render_confirm = lambda do
845
- btn_yes = (choice == 0) ? Color.bright_green("> [ Sí ] <") : Color.gray(" [ Sí ] ")
846
- btn_no = (choice == 1) ? Color.bright_red("> [ No ] <") : Color.gray(" [ No ] ")
847
- raw_btns = (choice == 0 ? "> [ Sí ] <" : " [ Sí ] ") + " " + (choice == 1 ? "> [ No ] <" : " [ No ] ")
848
- btns_vis_w = display_width(raw_btns)
849
- pad_total = [inner_w - btns_vis_w, 0].max
850
- left_p = " " * (pad_total / 2)
851
- right_p = " " * (pad_total - (pad_total / 2))
852
- btn_formatted_line = "#{left_p}#{btn_yes} #{btn_no}#{right_p}"
853
-
854
- q_clean = actual_question.to_s
855
- if display_width(q_clean) > inner_w
856
- q_clean = q_clean[0...[inner_w - 3, 1].max] + "..."
857
- end
858
- pad_q = [inner_w - display_width(q_clean), 0].max
859
- q_left = " " * (pad_q / 2)
860
- q_right = " " * (pad_q - (pad_q / 2))
861
-
862
- lines = []
863
- if is_rgb
864
- lines << Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")
865
- lines << "#{Color.rgb(v_l)} #{q_left}#{q_clean}#{q_right} #{Color.rgb(v_r)}"
866
- lines << "#{Color.rgb(v_l)} #{' ' * inner_w} #{Color.rgb(v_r)}"
867
- lines << "#{Color.rgb(v_l)} #{btn_formatted_line} #{Color.rgb(v_r)}"
868
- lines << Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")
869
- else
870
- lines << "#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}"
871
- lines << "#{color_code}#{v_l}#{reset_code} #{q_left}#{q_clean}#{q_right} #{color_code}#{v_r}#{reset_code}"
872
- lines << "#{color_code}#{v_l}#{reset_code} #{' ' * inner_w} #{color_code}#{v_r}#{reset_code}"
873
- lines << "#{color_code}#{v_l}#{reset_code} #{btn_formatted_line} #{color_code}#{v_r}#{reset_code}"
874
- lines << "#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}"
875
- end
876
-
877
- frame = lines.join("\r\n") + "\r\n"
878
- Kernel.print("\e[#{drawn_lines}A\e[J") if drawn_lines > 0
879
- Kernel.print(frame)
880
- $stdout.flush
881
- drawn_lines = lines.length
882
- end
883
-
884
- is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
885
- result = false
886
-
887
- begin
888
- Kernel.print(HIDE_CURSOR)
889
- render_confirm.call
890
-
891
- reader = lambda do |stream|
892
- while (key = GRmenu.read_key_raw(stream))
893
- break if key == "q" || key == "Q" || key == "\x03" || key == "\e"
894
- if key == "s" || key == "S" || key == "y" || key == "Y"
895
- result = true
896
- break
897
- elsif key == "n" || key == "N"
898
- result = false
899
- break
900
- elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\t" || key == "\e[C" || key == "\eOC" || key == "\xe0M"
901
- choice = 1 - choice
902
- render_confirm.call
903
- elsif key == "\r" || key == "\n" || key == " "
904
- result = (choice == 0)
905
- break
906
- end
907
- end
908
- end
909
-
910
- if is_tty
911
- $stdin.raw { |s| reader.call(s) }
912
- else
913
- reader.call($stdin)
914
- end
915
- ensure
916
- Kernel.print(SHOW_CURSOR)
917
- end
918
-
919
- result
920
- end
921
-
922
- def self.input(prompt_or_title = nil, title: nil, label: nil, default: "", password: false, color: nil, border_color: nil, title_color: nil, label_color: nil, style: nil, width: nil)
923
- c_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "input")) || {}
924
- style_num = (style || c_sec["style"] || 3).to_i
925
- border_cfg = BORDERS[style_num] || BORDERS[3]
926
-
927
- h_top = border_cfg[:ht] || border_cfg[:h]
928
- h_bot = border_cfg[:hb] || border_cfg[:h]
929
- v_l = border_cfg[:vl] || border_cfg[:v]
930
- v_r = border_cfg[:vr] || border_cfg[:v]
931
-
932
- box_title = if title && !title.to_s.empty?
933
- title.to_s
934
- elsif prompt_or_title && !label
935
- prompt_or_title.to_s
936
- else
937
- c_sec["title"] || "Entrada de Datos"
938
- end
939
-
940
- input_label = if label && !label.to_s.empty?
941
- label.to_s
942
- elsif prompt_or_title && title
943
- prompt_or_title.to_s
944
- elsif c_sec["label"]
945
- c_sec["label"].to_s
946
- else
947
- "Valor:"
948
- end
949
-
950
- brd_col_name = (border_color || color || c_sec["border_color"] || c_sec["color"] || "cyan").to_s
951
- tit_col_name = (title_color || c_sec["title_color"] || "yellow").to_s
952
- lbl_col_name = (label_color || c_sec["label_color"] || "white").to_s
953
-
954
- is_rgb = (brd_col_name.downcase == "rgb" || brd_col_name.downcase == "rainbow" || brd_col_name.downcase == "chroma")
955
-
956
- text = String.new(default.to_s)
957
- term_w = terminal_width
958
- p_len = display_width(box_title) + 6
959
- c_len = display_width(input_label) + display_width(text) + 12
960
- box_w = width ? width.to_i : [p_len, c_len, 48].max
961
- box_w = [box_w, term_w - 4].min
962
- inner_w = [box_w - 2, 20].max
963
-
964
- drawn_lines = 0
965
-
966
- render_input = lambda do
967
- display_str = password ? ("*" * text.length) : text
968
- avail_inp_w = [inner_w - display_width(input_label) - 4, 4].max
969
- if display_width(display_str) > avail_inp_w
970
- display_str = "..." + display_str[-[avail_inp_w - 3, 1].max..-1]
971
- end
972
-
973
- brd_code = is_rgb ? "" : ansi_color(brd_col_name, 1)
974
- tit_code = ansi_color(tit_col_name, 2)
975
- lbl_code = ansi_color(lbl_col_name, 2)
976
- rst = ansi_reset
977
-
978
- t_clean = " #{box_title} "
979
- t_w = display_width(t_clean)
980
- l_pad = [(inner_w - t_w) / 2, 0].max
981
- r_pad = [inner_w - t_w - l_pad, 0].max
982
-
983
- top_fill_l = (h_top * l_pad)[0...l_pad]
984
- top_fill_r = (h_top * r_pad)[0...r_pad]
985
- bot_fill = (h_bot * inner_w)[0...inner_w]
986
-
987
- top_line = if is_rgb
988
- Color.rgb("#{border_cfg[:tl]}#{top_fill_l}") + tit_code + t_clean + Color.rgb("#{top_fill_r}#{border_cfg[:tr]}")
989
- else
990
- "#{brd_code}#{border_cfg[:tl]}#{top_fill_l}#{rst}#{tit_code}#{t_clean}#{rst}#{brd_code}#{top_fill_r}#{border_cfg[:tr]}#{rst}"
991
- end
992
-
993
- empty_line = if is_rgb
994
- Color.rgb("#{v_l}#{' ' * inner_w}#{v_r}")
995
- else
996
- "#{brd_code}#{v_l}#{rst}#{' ' * inner_w}#{brd_code}#{v_r}#{rst}"
997
- end
998
-
999
- raw_content = " #{input_label} #{display_str}█"
1000
- content_pad = [inner_w - display_width(raw_content), 0].max
1001
- content_line = if is_rgb
1002
- "#{Color.rgb(v_l)} #{lbl_code}#{input_label}#{rst} #{Color.bright_white(display_str)}█#{' ' * content_pad}#{Color.rgb(v_r)}"
1003
- else
1004
- "#{brd_code}#{v_l}#{rst} #{lbl_code}#{input_label}#{rst} #{Color.bright_white(display_str)}█#{' ' * content_pad}#{brd_code}#{v_r}#{rst}"
1005
- end
1006
-
1007
- bot_line = if is_rgb
1008
- Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")
1009
- else
1010
- "#{brd_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{rst}"
1011
- end
1012
-
1013
- lines = [top_line, empty_line, content_line, empty_line, bot_line]
1014
- frame = lines.join("\r\n") + "\r\n"
1015
- Kernel.print("\e[#{drawn_lines}A\e[J") if drawn_lines > 0
1016
- Kernel.print(frame)
1017
- $stdout.flush
1018
- drawn_lines = lines.length
1019
- end
1020
-
1021
- is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
1022
-
1023
- begin
1024
- Kernel.print(HIDE_CURSOR)
1025
- render_input.call
1026
-
1027
- reader = lambda do |stream|
1028
- while (key = GRmenu.read_key_raw(stream))
1029
- break if key == "\x03" || key == "\e"
1030
- if key == "\r" || key == "\n"
1031
- break
1032
- elsif key == "\x7f" || key == "\b" || key == "\x08"
1033
- text.chop!
1034
- render_input.call
1035
- elsif key == "\x15"
1036
- text.clear
1037
- render_input.call
1038
- elsif key =~ /^[[:print:]]$/
1039
- text << key if display_width(text) < (inner_w - display_width(input_label) - 6)
1040
- render_input.call
1041
- end
1042
- end
1043
- end
1044
-
1045
- if is_tty
1046
- $stdin.raw { |s| reader.call(s) }
1047
- else
1048
- reader.call($stdin)
1049
- end
1050
- ensure
1051
- Kernel.print(SHOW_CURSOR)
1052
- end
1053
-
1054
- text
1055
- end
1056
-
1057
- def self.checkbox(items_arg = nil, items: nil, title: "Selección Múltiple", subtitle: "Espacio: Marcar/Desmarcar | a: Todos | n: Ninguno | i: Invertir | Enter: Confirmar", color: nil, style: nil, page_size: nil, min_width: nil, preselected: [])
1058
- cb_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "checkbox")) || {}
1059
- actual_items = items || items_arg || []
1060
- item_list = actual_items.is_a?(Array) ? actual_items : Array(actual_items)
1061
- return [] if item_list.empty?
1062
- chk_mark = cb_sec["checked_mark"] || "[X]"
1063
- unchk_mark = cb_sec["unchecked_mark"] || "[ ]"
1064
-
1065
- parsed_items = item_list.map do |it|
1066
- case it
1067
- when Array
1068
- name = it[0].to_s
1069
- is_chk = it.length > 1 ? !!it[1] : false
1070
- desc = it.length > 2 ? it[2].to_s : ""
1071
- { name: name, checked: is_chk, desc: desc, original: it }
1072
- when Hash
1073
- name = (it[:name] || it["name"] || it[:title] || it["title"] || "Item").to_s
1074
- is_chk = !!(it[:checked] || it["checked"] || it[:selected] || it["selected"])
1075
- desc = (it[:desc] || it["desc"] || it[:description] || it["description"]).to_s
1076
- { name: name, checked: is_chk, desc: desc, original: it }
1077
- else
1078
- { name: it.to_s, checked: false, desc: "", original: it }
1079
- end
1080
- end
1081
-
1082
- preselected.each do |p|
1083
- if p.is_a?(Integer) && parsed_items[p]
1084
- parsed_items[p][:checked] = true
1085
- else
1086
- it = parsed_items.find { |pi| pi[:name] == p.to_s }
1087
- it[:checked] = true if it
1088
- end
1089
- end
1090
-
1091
- index = 0
1092
- rgb_tick = 0.0
1093
- drawn_lines = 0
1094
- cb_color = (color || cb_sec["color"] || "cyan").to_s
1095
- style_num = (style || cb_sec["style"] || 3).to_i
1096
- is_rgb = (cb_color.downcase == "rgb" || cb_color.downcase == "rainbow" || cb_color.downcase == "chroma")
1097
- border_cfg = BORDERS[style_num] || BORDERS[3]
1098
- h_top = border_cfg[:ht] || border_cfg[:h]
1099
- h_bot = border_cfg[:hb] || border_cfg[:h]
1100
- v_l = border_cfg[:vl] || border_cfg[:v]
1101
- v_r = border_cfg[:vr] || border_cfg[:v]
1102
-
1103
- render_frame = lambda do
1104
- term_w = terminal_width
1105
- term_h = terminal_height
1106
-
1107
- max_name_w = parsed_items.map { |it| display_width(it[:name]) }.max || 10
1108
- req_w = [max_name_w + 12, display_width(title) + 6, display_width(subtitle) + 4, min_width || 38].max
1109
- box_w = [req_w, term_w - 4].min
1110
- inner_w = box_w - 4
1111
-
1112
- top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
1113
- bot_fill = (h_bot * ((box_w - 2).to_f / h_bot.length).ceil)[0...(box_w - 2)]
1114
- mid_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
1115
-
1116
- total_items = parsed_items.length
1117
- max_visible = page_size ? [page_size, total_items, term_h - 10].min : [total_items, term_h - 10].min
1118
- max_visible = [max_visible, 1].max
1119
-
1120
- start_idx = 0
1121
- end_idx = total_items - 1
1122
- if total_items > max_visible
1123
- half = max_visible / 2
1124
- start_idx = [[index - half, 0].max, total_items - max_visible].min
1125
- end_idx = start_idx + max_visible - 1
1126
- end
1127
-
1128
- lines = []
1129
- if is_rgb
1130
- lines << Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}", rgb_tick)
1131
- unless title.to_s.empty?
1132
- t_clean = title.to_s
1133
- t_clean = t_clean[0...[inner_w - 3, 1].max] + "..." if display_width(t_clean) > inner_w
1134
- pad_t = [inner_w - display_width(t_clean), 0].max
1135
- t_line = (" " * (pad_t / 2)) + t_clean + (" " * (pad_t - (pad_t / 2)))
1136
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.rgb(t_line, rgb_tick + 0.2)} #{Color.rgb(v_r, rgb_tick)}"
1137
- lines << Color.rgb("#{v_l}#{mid_fill}#{v_r}", rgb_tick)
1138
- end
1139
- if start_idx > 0
1140
- up_t = "▲ (+#{start_idx} arriba)"
1141
- pad_u = [inner_w - display_width(up_t), 0].max
1142
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.gray(" " * (pad_u / 2) + up_t + " " * (pad_u - (pad_u / 2)))} #{Color.rgb(v_r, rgb_tick)}"
1143
- end
1144
- (start_idx..end_idx).each do |i|
1145
- it = parsed_items[i]
1146
- mark = it[:checked] ? chk_mark : unchk_mark
1147
- is_active = (i == index)
1148
- max_name_w = [inner_w - display_width(mark) - 4, 4].max
1149
- name_str = it[:name].to_s
1150
- name_str = name_str[0...[max_name_w - 3, 1].max] + "..." if display_width(name_str) > max_name_w
1151
- raw_line = "#{is_active ? '> ' : ' '}#{mark} #{name_str}"
1152
- line_padded = pad_to_width(raw_line, inner_w)
1153
- if is_active
1154
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.rgb(line_padded, rgb_tick + 0.4)} #{Color.rgb(v_r, rgb_tick)}"
1155
- elsif it[:checked]
1156
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.bright_green(line_padded)} #{Color.rgb(v_r, rgb_tick)}"
1157
- else
1158
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.white(line_padded)} #{Color.rgb(v_r, rgb_tick)}"
1159
- end
1160
- end
1161
- if end_idx < (total_items - 1)
1162
- rem = total_items - 1 - end_idx
1163
- dn_t = "▼ (+#{rem} abajo)"
1164
- pad_d = [inner_w - display_width(dn_t), 0].max
1165
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.gray(" " * (pad_d / 2) + dn_t + " " * (pad_d - (pad_d / 2)))} #{Color.rgb(v_r, rgb_tick)}"
1166
- end
1167
- unless subtitle.to_s.empty?
1168
- lines << Color.rgb("#{v_l}#{mid_fill}#{v_r}", rgb_tick)
1169
- s_clean = subtitle.to_s
1170
- s_clean = s_clean[0...[inner_w - 3, 1].max] + "..." if display_width(s_clean) > inner_w
1171
- pad_sub = [inner_w - display_width(s_clean), 0].max
1172
- sub_padded = (" " * (pad_sub / 2)) + s_clean + (" " * (pad_sub - (pad_sub / 2)))
1173
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.gray(sub_padded)} #{Color.rgb(v_r, rgb_tick)}"
1174
- end
1175
- lines << Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}", rgb_tick)
1176
- else
1177
- color_code = ansi_color(cb_color, 2)
1178
- reset_code = ansi_reset
1179
- lines << "#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}"
1180
- unless title.to_s.empty?
1181
- t_clean = title.to_s
1182
- t_clean = t_clean[0...[inner_w - 3, 1].max] + "..." if display_width(t_clean) > inner_w
1183
- pad_t = [inner_w - display_width(t_clean), 0].max
1184
- t_line = (" " * (pad_t / 2)) + t_clean + (" " * (pad_t - (pad_t / 2)))
1185
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.bright_yellow(t_line)} #{color_code}#{v_r}#{reset_code}"
1186
- lines << "#{color_code}#{v_l}#{top_fill}#{v_r}#{reset_code}"
1187
- end
1188
- if start_idx > 0
1189
- up_t = "▲ (+#{start_idx} arriba)"
1190
- pad_u = [inner_w - display_width(up_t), 0].max
1191
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.gray(" " * (pad_u / 2) + up_t + " " * (pad_u - (pad_u / 2)))} #{color_code}#{v_r}#{reset_code}"
1192
- end
1193
- (start_idx..end_idx).each do |i|
1194
- it = parsed_items[i]
1195
- mark = it[:checked] ? chk_mark : unchk_mark
1196
- is_active = (i == index)
1197
- max_name_w = [inner_w - display_width(mark) - 4, 4].max
1198
- name_str = it[:name].to_s
1199
- name_str = name_str[0...[max_name_w - 3, 1].max] + "..." if display_width(name_str) > max_name_w
1200
- raw_line = "#{is_active ? '> ' : ' '}#{mark} #{name_str}"
1201
- line_padded = pad_to_width(raw_line, inner_w)
1202
- if is_active
1203
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.bright_yellow(line_padded)} #{color_code}#{v_r}#{reset_code}"
1204
- elsif it[:checked]
1205
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.bright_green(line_padded)} #{color_code}#{v_r}#{reset_code}"
1206
- else
1207
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.white(line_padded)} #{color_code}#{v_r}#{reset_code}"
1208
- end
1209
- end
1210
- if end_idx < (total_items - 1)
1211
- rem = total_items - 1 - end_idx
1212
- dn_t = "▼ (+#{rem} abajo)"
1213
- pad_d = [inner_w - display_width(dn_t), 0].max
1214
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.gray(" " * (pad_d / 2) + dn_t + " " * (pad_d - (pad_d / 2)))} #{color_code}#{v_r}#{reset_code}"
1215
- end
1216
- unless subtitle.to_s.empty?
1217
- lines << "#{color_code}#{v_l}#{top_fill}#{v_r}#{reset_code}"
1218
- s_clean = subtitle.to_s
1219
- s_clean = s_clean[0...[inner_w - 3, 1].max] + "..." if display_width(s_clean) > inner_w
1220
- pad_sub = [inner_w - display_width(s_clean), 0].max
1221
- sub_padded = (" " * (pad_sub / 2)) + s_clean + (" " * (pad_sub - (pad_sub / 2)))
1222
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.gray(sub_padded)} #{color_code}#{v_r}#{reset_code}"
1223
- end
1224
- lines << "#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}"
1225
- end
1226
-
1227
- frame = lines.join("\r\n") + "\r\n"
1228
- Kernel.print("\e[#{drawn_lines}A\e[J") if drawn_lines > 0
1229
- Kernel.print(frame)
1230
- $stdout.flush
1231
- drawn_lines = lines.length
1232
- end
1233
-
1234
- is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
1235
- submitted = false
1236
-
1237
- begin
1238
- Kernel.print(HIDE_CURSOR)
1239
- render_frame.call
1240
-
1241
- reader = lambda do |stream|
1242
- while true
1243
- if is_rgb
1244
- ready = false
1245
- if stream.respond_to?(:to_io) || stream.is_a?(IO)
1246
- begin
1247
- sr = IO.select([stream], nil, nil, 0.035)
1248
- ready = true if sr && sr[0] && !sr[0].empty?
1249
- rescue StandardError
1250
- ready = true
1251
- end
1252
- else
1253
- ready = true
1254
- end
1255
- unless ready
1256
- rgb_tick += 0.08
1257
- render_frame.call
1258
- next
1259
- end
1260
- end
1261
-
1262
- key = GRmenu.read_key_raw(stream)
1263
- break if key.nil? || key == "\x03" || key == "\x04" || key == "q" || key == "Q" || key == "\e"
1264
-
1265
- if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
1266
- index = (index - 1) % parsed_items.length
1267
- render_frame.call
1268
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
1269
- index = (index + 1) % parsed_items.length
1270
- render_frame.call
1271
- elsif key == " "
1272
- parsed_items[index][:checked] = !parsed_items[index][:checked]
1273
- render_frame.call
1274
- elsif key == "a" || key == "A"
1275
- parsed_items.each { |it| it[:checked] = true }
1276
- render_frame.call
1277
- elsif key == "n" || key == "N"
1278
- parsed_items.each { |it| it[:checked] = false }
1279
- render_frame.call
1280
- elsif key == "i" || key == "I"
1281
- parsed_items.each { |it| it[:checked] = !it[:checked] }
1282
- render_frame.call
1283
- elsif key == "\r" || key == "\n"
1284
- submitted = true
1285
- break
1286
- end
1287
- end
1288
- end
1289
-
1290
- if is_tty
1291
- $stdin.raw { |s| reader.call(s) }
1292
- else
1293
- reader.call($stdin)
1294
- end
1295
- ensure
1296
- Kernel.print(SHOW_CURSOR)
1297
- end
1298
-
1299
- if submitted
1300
- selected = parsed_items.select { |it| it[:checked] }
1301
- selected.map { |it| it[:original] }
1302
- else
1303
- []
1304
- end
1305
- end
1306
- class << self
1307
- alias_method :select_multi, :checkbox
1308
- alias_method :multiselect, :checkbox
1309
- end
1310
-
1311
- def self.slider(prompt_arg = nil, prompt: nil, min: 0, max: 100, step: 1, default: nil, unit: "", color: nil, style: nil, width: 46)
1312
- actual_prompt = prompt || prompt_arg || "Selecciona un valor:"
1313
- sl_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "slider")) || {}
1314
- val = (default || min).to_f.clamp(min.to_f, max.to_f)
1315
- step_val = [step.to_f, 0.001].max
1316
- drawn_lines = 0
1317
- rgb_tick = 0.0
1318
- sl_color = (color || sl_sec["color"] || "cyan").to_s
1319
- style_num = (style || sl_sec["style"] || 3).to_i
1320
- is_rgb = (sl_color.downcase == "rgb" || sl_color.downcase == "rainbow" || sl_color.downcase == "chroma")
1321
-
1322
- border_cfg = BORDERS[style_num] || BORDERS[3]
1323
- h_top = border_cfg[:ht] || border_cfg[:h]
1324
- h_bot = border_cfg[:hb] || border_cfg[:h]
1325
- v_l = border_cfg[:vl] || border_cfg[:v]
1326
- v_r = border_cfg[:vr] || border_cfg[:v]
1327
-
1328
- render_slider = lambda do
1329
- term_w = terminal_width
1330
- box_w = [width, term_w - 4, display_width(actual_prompt) + 8, 38].max
1331
- box_w = [box_w, term_w - 2].min
1332
- inner_w = box_w - 4
1333
-
1334
- top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
1335
- bot_fill = (h_bot * ((box_w - 2).to_f / h_bot.length).ceil)[0...(box_w - 2)]
1336
-
1337
- val_display = (val % 1 == 0) ? val.to_i.to_s : val.round(2).to_s
1338
- val_str = unit.to_s.empty? ? val_display : "#{val_display} #{unit}"
1339
-
1340
- range_span = (max - min).to_f
1341
- range_span = 1.0 if range_span <= 0
1342
- fraction = ((val - min).to_f / range_span).clamp(0.0, 1.0)
1343
-
1344
- avail_bar_w = [inner_w - display_width(val_str) - 4, 6].max
1345
- filled_len = (fraction * avail_bar_w).round
1346
- empty_len = [avail_bar_w - filled_len, 0].max
1347
-
1348
- p_clean = actual_prompt.to_s
1349
- if display_width(p_clean) > inner_w
1350
- p_clean = p_clean[0...[inner_w - 3, 1].max] + "..."
1351
- end
1352
- pad_p = [inner_w - display_width(p_clean), 0].max
1353
- p_line = (" " * (pad_p / 2)) + p_clean + (" " * (pad_p - (pad_p / 2)))
1354
-
1355
- instr = (inner_w >= 30) ? "← / → Ajustar | Enter Guardar" : "←/→: Ajustar | Enter: Ok"
1356
- if display_width(instr) > inner_w
1357
- instr = instr[0...[inner_w - 3, 1].max] + "..."
1358
- end
1359
- pad_i = [inner_w - display_width(instr), 0].max
1360
- i_line = (" " * (pad_i / 2)) + instr + (" " * (pad_i - (pad_i / 2)))
1361
-
1362
- lines = []
1363
- if is_rgb
1364
- lines << Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}", rgb_tick)
1365
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.rgb(p_line, rgb_tick + 0.3)} #{Color.rgb(v_r, rgb_tick)}"
1366
- lines << "#{Color.rgb(v_l, rgb_tick)} #{' ' * inner_w} #{Color.rgb(v_r, rgb_tick)}"
1367
-
1368
- filled_part = Color.rgb("█" * filled_len, rgb_tick + 0.5)
1369
- empty_part = Color.gray("░" * empty_len)
1370
- bar_raw = "[#{filled_part}#{empty_part}] #{Color.bright_white(val_str)}"
1371
- bar_vis_w = 2 + filled_len + empty_len + 1 + display_width(val_str)
1372
- pad_b = [inner_w - bar_vis_w, 0].max
1373
- lines << "#{Color.rgb(v_l, rgb_tick)} #{bar_raw}#{' ' * pad_b} #{Color.rgb(v_r, rgb_tick)}"
1374
- lines << "#{Color.rgb(v_l, rgb_tick)} #{' ' * inner_w} #{Color.rgb(v_r, rgb_tick)}"
1375
- lines << "#{Color.rgb(v_l, rgb_tick)} #{Color.gray(i_line)} #{Color.rgb(v_r, rgb_tick)}"
1376
- lines << Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}", rgb_tick)
1377
- else
1378
- color_code = ansi_color(sl_color, 2)
1379
- reset_code = ansi_reset
1380
-
1381
- bar_raw = "[#{"█" * filled_len}#{"░" * empty_len}] #{val_str}"
1382
- pad_b = [inner_w - display_width(bar_raw), 0].max
1383
- bar_line = bar_raw + (" " * pad_b)
1384
-
1385
- lines << "#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}"
1386
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.bright_yellow(p_line)} #{color_code}#{v_r}#{reset_code}"
1387
- lines << "#{color_code}#{v_l}#{reset_code} #{' ' * inner_w} #{color_code}#{v_r}#{reset_code}"
1388
- lines << "#{color_code}#{v_l}#{reset_code} #{bar_line} #{color_code}#{v_r}#{reset_code}"
1389
- lines << "#{color_code}#{v_l}#{reset_code} #{' ' * inner_w} #{color_code}#{v_r}#{reset_code}"
1390
- lines << "#{color_code}#{v_l}#{reset_code} #{Color.gray(i_line)} #{color_code}#{v_r}#{reset_code}"
1391
- lines << "#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}"
1392
- end
1393
-
1394
- frame = lines.join("\r\n") + "\r\n"
1395
- Kernel.print("\e[#{drawn_lines}A\e[J") if drawn_lines > 0
1396
- Kernel.print(frame)
1397
- $stdout.flush
1398
- drawn_lines = lines.length
1399
- end
1400
-
1401
- is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
1402
-
1403
- begin
1404
- Kernel.print(HIDE_CURSOR)
1405
- render_slider.call
1406
-
1407
- reader = lambda do |stream|
1408
- while true
1409
- if is_rgb
1410
- ready = false
1411
- if stream.respond_to?(:to_io) || stream.is_a?(IO)
1412
- begin
1413
- sr = IO.select([stream], nil, nil, 0.035)
1414
- ready = true if sr && sr[0] && !sr[0].empty?
1415
- rescue StandardError
1416
- ready = true
1417
- end
1418
- else
1419
- ready = true
1420
- end
1421
- unless ready
1422
- rgb_tick += 0.08
1423
- render_slider.call
1424
- next
1425
- end
1426
- end
1427
-
1428
- key = GRmenu.read_key_raw(stream)
1429
- break if key.nil? || key == "\x03" || key == "\x04" || key == "q" || key == "Q" || key == "\e"
1430
-
1431
- if key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K" || key == "h" || key == "H"
1432
- val = (val - step_val).clamp(min.to_f, max.to_f)
1433
- render_slider.call
1434
- elsif key == "\e[C" || key == "\eOC" || key == "\xe0M" || key == "\x00M" || key == "l" || key == "L"
1435
- val = (val + step_val).clamp(min.to_f, max.to_f)
1436
- render_slider.call
1437
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
1438
- val = (val - step_val * 5).clamp(min.to_f, max.to_f)
1439
- render_slider.call
1440
- elsif key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
1441
- val = (val + step_val * 5).clamp(min.to_f, max.to_f)
1442
- render_slider.call
1443
- elsif key == "\r" || key == "\n"
1444
- break
1445
- end
1446
- end
1447
- end
1448
-
1449
- if is_tty
1450
- $stdin.raw { |s| reader.call(s) }
1451
- else
1452
- reader.call($stdin)
1453
- end
1454
- ensure
1455
- Kernel.print(SHOW_CURSOR)
1456
- end
1457
-
1458
- (val % 1 == 0) ? val.to_i : val.round(2)
1459
- end
1460
- class << self
1461
- alias_method :range, :slider
1462
- end
1463
-
1464
- def self.read_key_raw(input_stream)
1465
- is_tty = input_stream.respond_to?(:tty?) && input_stream.tty?
1466
- first_char = nil
1467
- begin
1468
- first_char = is_tty ? input_stream.getch : input_stream.read(1)
1469
- rescue EOFError, Errno::EPIPE
1470
- return nil
1471
- end
1472
- return nil if first_char.nil?
1473
-
1474
- if first_char == "\e"
1475
- seq = first_char.dup
1476
- if is_tty
1477
- while (ch = (input_stream.getch(min: 0, time: 0.1) rescue nil))
1478
- seq << ch
1479
- break if seq =~ /[a-zA-Z~]\z/
1480
- end
1481
- else
1482
- while (IO.select([input_stream], nil, nil, 0.05) rescue nil)
1483
- ch = (input_stream.read(1) rescue nil)
1484
- break if ch.nil?
1485
- seq << ch
1486
- break if seq =~ /[a-zA-Z~]\z/
1487
- end
1488
- end
1489
-
1490
- if seq == "\e[M"
1491
- cb = is_tty ? (input_stream.getch(min: 0, time: 0.1) rescue nil) : (input_stream.read(1) rescue nil)
1492
- cx = is_tty ? (input_stream.getch(min: 0, time: 0.1) rescue nil) : (input_stream.read(1) rescue nil)
1493
- cy = is_tty ? (input_stream.getch(min: 0, time: 0.1) rescue nil) : (input_stream.read(1) rescue nil)
1494
- if cb && cx && cy
1495
- btn_c = cb.ord - 32
1496
- col_c = cx.ord - 32
1497
- row_c = cy.ord - 32
1498
- return "\e[<#{btn_c};#{col_c};#{row_c}M"
1499
- end
1500
- end
1501
-
1502
- return seq
1503
- elsif first_char == "\x00" || first_char == "\xe0"
1504
- second_char = is_tty ? (input_stream.getch(min: 0, time: 0.1) rescue nil) : (input_stream.read(1) rescue nil)
1505
- return first_char + second_char if second_char
1506
- end
1507
-
1508
- first_char
1509
- rescue EOFError, Errno::EPIPE, Errno::ENOTTY
1510
- nil
1511
95
  end
1512
96
 
1513
- class SetStyle
1514
- def initialize(
1515
- border: { color: "cyan", level: 1 },
1516
- options: { color: "white", level: 1 },
1517
- focus: { color: "green", level: 2 },
1518
- title: { color: "yellow", level: 2 },
1519
- banner: { color: "magenta", level: 2 },
1520
- subtitle: { color: "cyan", level: 2 },
1521
- divider: { color: "blue", level: 1 },
1522
- font: 1,
1523
- desc_prefix: "[i]"
1524
- )
1525
- @border = border.dup
1526
- @options = options.dup
1527
- @focus = focus.dup
1528
- @title = title.dup
1529
- @banner = banner.dup
1530
- @subtitle = subtitle.dup
1531
- @divider = divider.dup
1532
- @font = font.to_i
1533
- @desc_prefix = desc_prefix.to_s
1534
- end
97
+ # Mapeo de variantes en español para BASE_RGB
98
+ BASE_RGB["negro"] ||= [0, 0, 0]
99
+ BASE_RGB["rojo"] ||= BASE_RGB["red"]
100
+ BASE_RGB["verde"] ||= BASE_RGB["green"]
101
+ BASE_RGB["azul"] ||= BASE_RGB["blue"]
102
+ BASE_RGB["amarillo"] ||= BASE_RGB["yellow"]
103
+ BASE_RGB["blanco"] ||= BASE_RGB["white"]
104
+ BASE_RGB["gris"] ||= BASE_RGB["gray"]
105
+ BASE_RGB["morado"] ||= BASE_RGB["purple"]
106
+ BASE_RGB["rosa"] ||= BASE_RGB["pink"]
107
+ BASE_RGB["naranja"] ||= BASE_RGB["orange"]
108
+ BASE_RGB["aqua"] ||= [0, 220, 220]
1535
109
 
1536
- def desc_prefix(prefix_str = nil)
1537
- return @desc_prefix if prefix_str.nil?
1538
- @desc_prefix = prefix_str.to_s
1539
- self
1540
- end
1541
- alias_method :description_prefix, :desc_prefix
1542
- alias_method :desc_prefix=, :desc_prefix
1543
- alias_method :description_prefix=, :desc_prefix
1544
-
1545
- def border(color_name = nil, brightness_level = 1)
1546
- return @border if color_name.nil?
1547
- @border = parse_color(color_name, brightness_level)
1548
- self
1549
- end
1550
- alias_method :Border, :border
1551
- alias_method :set_border, :border
1552
- alias_method :border=, :border
1553
-
1554
- def options(color_name = nil, brightness_level = 1)
1555
- return @options if color_name.nil?
1556
- @options = parse_color(color_name, brightness_level)
1557
- self
1558
- end
1559
- alias_method :Options, :options
1560
- alias_method :set_options, :options
1561
- alias_method :options=, :options
1562
-
1563
- def focus(color_name = nil, brightness_level = 2)
1564
- return @focus if color_name.nil?
1565
- @focus = parse_color(color_name, brightness_level)
1566
- self
1567
- end
1568
- alias_method :Focus, :focus
1569
- alias_method :set_focus, :focus
1570
- alias_method :focus=, :focus
1571
-
1572
- def title(color_name = nil, brightness_level = 2)
1573
- return @title if color_name.nil?
1574
- @title = parse_color(color_name, brightness_level)
1575
- self
1576
- end
1577
- alias_method :Title, :title
1578
- alias_method :set_title, :title
1579
- alias_method :title=, :title
1580
-
1581
- def banner(color_name = nil, brightness_level = 2)
1582
- return @banner if color_name.nil?
1583
- @banner = parse_color(color_name, brightness_level)
1584
- self
1585
- end
1586
- alias_method :Banner, :banner
1587
- alias_method :set_banner, :banner
1588
- alias_method :banner=, :banner
1589
-
1590
- def subtitle(color_name = nil, brightness_level = 2)
1591
- return @subtitle if color_name.nil?
1592
- @subtitle = parse_color(color_name, brightness_level)
1593
- self
1594
- end
1595
- alias_method :Subtitle, :subtitle
1596
- alias_method :set_subtitle, :subtitle
1597
- alias_method :subtitle=, :subtitle
1598
-
1599
- def divider(color_name = nil, brightness_level = 1)
1600
- return @divider if color_name.nil?
1601
- @divider = parse_color(color_name, brightness_level)
1602
- self
1603
- end
1604
- alias_method :Divider, :divider
1605
- alias_method :set_divider, :divider
1606
- alias_method :divider=, :divider
1607
-
1608
- def font(font_id = nil)
1609
- return @font if font_id.nil?
1610
- @font = font_id.to_i
1611
- self
1612
- end
1613
- alias_method :Font, :font
1614
- alias_method :set_font, :font
1615
- alias_method :font=, :font
1616
-
1617
- private
1618
-
1619
- def parse_color(color_val, default_level = 1)
1620
- if color_val.is_a?(Hash)
1621
- { color: (color_val[:color] || color_val["color"]).to_s, level: (color_val[:level] || color_val["level"] || default_level).to_i }
1622
- else
1623
- { color: color_val.to_s, level: default_level.to_i }
1624
- end
1625
- end
1626
-
1627
- class << self
1628
- def border(color_name = nil, brightness_level = 1)
1629
- @default_border ||= { color: "cyan", level: 1 }
1630
- return @default_border if color_name.nil?
1631
- @default_border = { color: color_name.to_s, level: brightness_level.to_i }
1632
- end
1633
- alias_method :Border, :border
1634
- alias_method :border=, :border
1635
-
1636
- def options(color_name = nil, brightness_level = 1)
1637
- @default_options ||= { color: "white", level: 1 }
1638
- return @default_options if color_name.nil?
1639
- @default_options = { color: color_name.to_s, level: brightness_level.to_i }
1640
- end
1641
- alias_method :Options, :options
1642
- alias_method :options=, :options
1643
-
1644
- def focus(color_name = nil, brightness_level = 2)
1645
- @default_focus ||= { color: "green", level: 2 }
1646
- return @default_focus if color_name.nil?
1647
- @default_focus = { color: color_name.to_s, level: brightness_level.to_i }
1648
- end
1649
- alias_method :Focus, :focus
1650
- alias_method :focus=, :focus
1651
-
1652
- def title(color_name = nil, brightness_level = 2)
1653
- @default_title ||= { color: "yellow", level: 2 }
1654
- return @default_title if color_name.nil?
1655
- @default_title = { color: color_name.to_s, level: brightness_level.to_i }
1656
- end
1657
- alias_method :Title, :title
1658
- alias_method :title=, :title
1659
-
1660
- def banner(color_name = nil, brightness_level = 2)
1661
- @default_banner ||= { color: "magenta", level: 2 }
1662
- return @default_banner if color_name.nil?
1663
- @default_banner = { color: color_name.to_s, level: brightness_level.to_i }
1664
- end
1665
- alias_method :Banner, :banner
1666
- alias_method :banner=, :banner
1667
-
1668
- def subtitle(color_name = nil, brightness_level = 2)
1669
- @default_subtitle ||= { color: "cyan", level: 2 }
1670
- return @default_subtitle if color_name.nil?
1671
- @default_subtitle = { color: color_name.to_s, level: brightness_level.to_i }
1672
- end
1673
- alias_method :Subtitle, :subtitle
1674
- alias_method :subtitle=, :subtitle
1675
-
1676
- def divider(color_name = nil, brightness_level = 1)
1677
- @default_divider ||= { color: "blue", level: 1 }
1678
- return @default_divider if color_name.nil?
1679
- @default_divider = { color: color_name.to_s, level: brightness_level.to_i }
1680
- end
1681
- alias_method :Divider, :divider
1682
- alias_method :divider=, :divider
1683
-
1684
- def font(font_id = nil)
1685
- @default_font ||= 1
1686
- return @default_font if font_id.nil?
1687
- @default_font = font_id.to_i
1688
- end
1689
- alias_method :Font, :font
1690
- alias_method :set_font, :font
1691
- alias_method :font=, :font
1692
-
1693
- def desc_prefix(prefix_str = nil)
1694
- @default_desc_prefix ||= "[i]"
1695
- return @default_desc_prefix if prefix_str.nil?
1696
- @default_desc_prefix = prefix_str.to_s
1697
- end
1698
- alias_method :description_prefix, :desc_prefix
1699
- alias_method :desc_prefix=, :desc_prefix
1700
- alias_method :description_prefix=, :desc_prefix
1701
- end
1702
- end
1703
-
1704
- module GRprint
1705
- module_function
1706
-
1707
- def p(text = "", ending = "\r\n")
1708
- Kernel.print("#{text}#{ending}")
1709
- end
1710
- end
1711
-
1712
- attr_accessor :functions, :title, :subtitle, :banner, :banner_style, :divider, :style, :index, :style_config, :center, :page_size, :search, :columns, :query, :image, :image_width
1713
-
1714
- alias_method :options, :functions
1715
- alias_method :options=, :functions=
1716
- alias_method :selected_index, :index
1717
- alias_method :selected_index=, :index=
1718
- alias_method :SetStyle, :style_config
1719
- alias_method :set_style, :style_config
1720
- alias_method :description, :subtitle
1721
- alias_method :description=, :subtitle=
1722
-
1723
- def self.STYLES; STYLES; end
1724
- def self.COLORS; COLORS; end
1725
- def self.BORDERS; BORDERS; end
1726
- def self.FONTS; FONTS; end
1727
- def self.FONT; FONT_1; end
1728
-
1729
- def self.terminal_width
1730
- cols = $stdout.winsize[1] rescue nil
1731
- cols = $stdin.winsize[1] rescue nil if cols.nil? || cols <= 0
1732
- (cols && cols > 0) ? cols : (ENV['COLUMNS'] ? ENV['COLUMNS'].to_i : 80)
1733
- rescue StandardError
1734
- 80
1735
- end
1736
-
1737
- def self.terminal_height
1738
- rows = $stdout.winsize[0] rescue nil
1739
- rows = $stdin.winsize[0] rescue nil if rows.nil? || rows <= 0
1740
- (rows && rows > 0) ? rows : (ENV['LINES'] ? ENV['LINES'].to_i : 24)
1741
- rescue StandardError
1742
- 24
1743
- end
1744
-
1745
- def self.clear_screen
1746
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
1747
- end
1748
- class << self
1749
- alias_method :clr, :clear_screen
1750
- end
1751
-
1752
- @@global_theme = {}
1753
-
1754
- def self.current_theme
1755
- @@global_theme
1756
- end
1757
-
1758
- def self.parse_config_text(text)
1759
- data = { global: {}, sections: {} }
1760
- current_sec = nil
1761
- current_sec_data = {}
1762
-
1763
- text.to_s.each_line do |line|
1764
- line = line.strip
1765
- next if line.empty? || line.start_with?("#")
1766
- next if line.start_with?("GRmenu::config")
1767
-
1768
- if line.start_with?("<<")
1769
- sec_name = line[2..-1].strip.downcase
1770
- current_sec = sec_name
1771
- current_sec_data = {}
1772
- elsif line == ">>"
1773
- if current_sec
1774
- data[:sections][current_sec] = current_sec_data
1775
- current_sec = nil
1776
- end
1777
- elsif line.include?("::")
1778
- key, _, val = line.partition("::")
1779
- key = key.strip.sub(/^@/, '').downcase
1780
- val = val.strip.sub(/^["']/, '').sub(/["']$/, '')
1781
- if current_sec
1782
- current_sec_data[key] = val
1783
- else
1784
- data[:global][key] = val
1785
- end
1786
- end
1787
- end
1788
- data
1789
- end
1790
-
1791
- def self.find_theme_file(path_or_name)
1792
- name = path_or_name.to_s
1793
- candidates = [
1794
- name,
1795
- "#{name}.gr",
1796
- find_data_file("themes/#{name}.gr"),
1797
- find_data_file("themes/#{name}"),
1798
- File.expand_path("data/themes/#{name}.gr", __dir__),
1799
- File.expand_path("data/themes/#{name}", __dir__),
1800
- File.expand_path("../data/themes/#{name}.gr", __dir__),
1801
- File.expand_path("../data/themes/#{name}", __dir__)
1802
- ].compact
1803
- candidates.find { |p| File.exist?(p) }
1804
- end
1805
-
1806
- def self.import_config(path_or_name)
1807
- path = find_theme_file(path_or_name)
1808
- raise "No se encontro el tema: #{path_or_name}" unless path && File.exist?(path)
1809
- text = File.read(path)
1810
- parsed = parse_config_text(text)
1811
- apply_parsed_theme(parsed)
1812
- @@global_theme = parsed
1813
- path
1814
- end
1815
-
1816
- def self.theme(name)
1817
- import_config(name)
1818
- end
1819
-
1820
- def self.extract_color_and_level(val, default_level = 1)
1821
- return ["white", default_level] if val.nil?
1822
- parts = val.to_s.split(":")
1823
- c_name = parts[0].to_s.strip
1824
- lvl = parts[1] ? parts[1].to_i : default_level
1825
- [c_name, lvl]
1826
- end
1827
-
1828
- def self.apply_parsed_theme(parsed)
1829
- sec = parsed[:sections] || {}
1830
- glob = parsed[:global] || {}
1831
- m = (sec["menu"] || {}).merge(glob)
1832
-
1833
- if m && !m.empty?
1834
- if m["border"] || m["border_color"]
1835
- c, l = extract_color_and_level(m["border"] || m["border_color"], 1)
1836
- SetStyle.border(c, l)
1837
- end
1838
- if m["title"] || m["title_color"]
1839
- c, l = extract_color_and_level(m["title"] || m["title_color"], 2)
1840
- SetStyle.title(c, l)
1841
- end
1842
- if m["focus"] || m["focus_color"]
1843
- c, l = extract_color_and_level(m["focus"] || m["focus_color"], 2)
1844
- SetStyle.focus(c, l)
1845
- end
1846
- if m["options"] || m["options_color"]
1847
- c, l = extract_color_and_level(m["options"] || m["options_color"], 1)
1848
- SetStyle.options(c, l)
1849
- end
1850
- if m["banner"] || m["banner_color"]
1851
- c, l = extract_color_and_level(m["banner"] || m["banner_color"], 2)
1852
- SetStyle.banner(c, l)
1853
- end
1854
- if m["subtitle"] || m["subtitle_color"]
1855
- c, l = extract_color_and_level(m["subtitle"] || m["subtitle_color"], 1)
1856
- SetStyle.subtitle(c, l)
1857
- end
1858
- if m["divider"] || m["divider_color"]
1859
- c, l = extract_color_and_level(m["divider"] || m["divider_color"], 1)
1860
- SetStyle.divider(c, l)
1861
- end
1862
- if m["desc_prefix"] || m["description_prefix"] || m["prefix"]
1863
- SetStyle.desc_prefix(m["desc_prefix"] || m["description_prefix"] || m["prefix"])
1864
- end
1865
- SetStyle.font(m["font"].to_i) if m["font"]
1866
- end
1867
-
1868
- sec.each do |k, v|
1869
- next unless v.is_a?(Hash)
1870
- c_val = v["color"] || v["border"] || v["options"] || v["focus"] || v["title"] || v["banner"] || v["subtitle"] || v["divider"]
1871
- c, l = extract_color_and_level(c_val, (v["level"] || 1).to_i)
1872
- case k
1873
- when "border"
1874
- SetStyle.border(c, l)
1875
- when "options"
1876
- SetStyle.options(c, l)
1877
- when "focus"
1878
- SetStyle.focus(c, l)
1879
- when "title"
1880
- SetStyle.title(c, l)
1881
- when "banner"
1882
- SetStyle.banner(c, l)
1883
- when "subtitle"
1884
- SetStyle.subtitle(c, l)
1885
- when "divider"
1886
- SetStyle.divider(c, l)
1887
- end
1888
- end
1889
- SetStyle.font(glob["font"].to_i) if glob["font"]
1890
- end
1891
-
1892
- def self.style(css_content)
1893
- parsed = parse_config_text(css_content)
1894
- apply_parsed_theme(parsed)
1895
- parsed
1896
- end
1897
-
1898
- def self.export_config(path = nil)
1899
- if path.nil?
1900
- caller_loc = caller_locations.find { |c| !c.path.include?(__FILE__) }
1901
- base = caller_loc ? caller_loc.path.sub(/\.rb$/, '') : "theme"
1902
- path = "#{base}.gr"
1903
- end
1904
- lines = ["GRmenu::config<-1->", ""]
1905
- lines << "@theme:: \"#{File.basename(path, '.gr').capitalize}\""
1906
- lines << "@author:: \"grcode\""
1907
- lines << "@version:: \"1.0\""
1908
- lines << ""
1909
- lines << "<<menu"
1910
- lines << " style:: 3"
1911
- lines << " banner_style:: 3"
1912
- lines << " font:: #{SetStyle.font}"
1913
- lines << " animate:: rgb"
1914
- lines << " center:: true"
1915
- lines << " border:: #{SetStyle.border[:color]}:#{SetStyle.border[:level]}"
1916
- lines << " title:: #{SetStyle.title[:color]}:#{SetStyle.title[:level]}"
1917
- lines << " focus:: #{SetStyle.focus[:color]}:#{SetStyle.focus[:level]}"
1918
- lines << " options:: #{SetStyle.options[:color]}:#{SetStyle.options[:level]}"
1919
- lines << " banner:: #{SetStyle.banner[:color]}:#{SetStyle.banner[:level]}"
1920
- lines << " subtitle:: #{SetStyle.subtitle[:color]}:#{SetStyle.subtitle[:level]}"
1921
- lines << " divider:: #{SetStyle.divider[:color]}:#{SetStyle.divider[:level]}"
1922
- lines << ">>"
1923
- lines << ""
1924
- lines << "<<table"
1925
- lines << " style:: 3"
1926
- lines << " header_color:: yellow:2"
1927
- lines << " border_color:: rgb:2"
1928
- lines << " selected_row:: green:2"
1929
- lines << " row_color:: white:1"
1930
- lines << " zebra_striping:: true"
1931
- lines << ">>"
1932
- lines << ""
1933
- lines << "<<card"
1934
- lines << " style:: 7"
1935
- lines << " border_color:: cyan:2"
1936
- lines << " title_color:: yellow:2"
1937
- lines << " content_color:: white:1"
1938
- lines << ">>"
1939
- lines << ""
1940
- lines << "<<slider"
1941
- lines << " style:: 3"
1942
- lines << " color:: rgb:2"
1943
- lines << " fill_char:: █"
1944
- lines << " empty_char:: ░"
1945
- lines << ">>"
1946
- lines << ""
1947
- lines << "<<checkbox"
1948
- lines << " style:: 3"
1949
- lines << " color:: rgb:2"
1950
- lines << " checked_mark:: [X]"
1951
- lines << " unchecked_mark:: [ ]"
1952
- lines << ">>"
1953
- lines << ""
1954
- File.write(path, lines.join("\n") + "\n")
1955
- path
1956
- end
1957
-
1958
- def self.export_from_file(source_file, target_path = nil)
1959
- raise "No existe #{source_file}" unless File.exist?(source_file)
1960
- orig_draw = instance_method(:draw) rescue nil
1961
- extracted = nil
1962
- define_method(:draw) do |*|
1963
- extracted = {
1964
- style: @style,
1965
- banner_style: @banner_style,
1966
- font: @style_config&.font,
1967
- animate: @animate,
1968
- border: @style_config&.border,
1969
- title: @style_config&.title,
1970
- focus: @style_config&.focus,
1971
- options: @style_config&.options,
1972
- banner: @style_config&.banner,
1973
- subtitle: @style_config&.subtitle,
1974
- divider: @style_config&.divider
1975
- }
1976
- throw :grmenu_export_completed
1977
- end
1978
- begin
1979
- catch(:grmenu_export_completed) do
1980
- load(File.expand_path(source_file))
1981
- end
1982
- ensure
1983
- define_method(:draw, orig_draw) if orig_draw
1984
- end
1985
- out = target_path || source_file.sub(/\.rb$/, '') + ".gr"
1986
- if extracted && extracted[:border]
1987
- lines = ["GRmenu::config<-1->", ""]
1988
- lines << "@theme:: \"#{File.basename(out, '.gr').capitalize}\""
1989
- lines << "@author:: \"grcode\""
1990
- lines << "@version:: \"1.0\""
1991
- lines << ""
1992
- lines << "<<menu"
1993
- lines << " style:: #{extracted[:style] || 3}"
1994
- lines << " banner_style:: #{extracted[:banner_style] || 3}"
1995
- lines << " font:: #{extracted[:font] || 1}"
1996
- lines << " animate:: #{extracted[:animate] || 'rgb'}"
1997
- lines << " center:: true"
1998
- lines << " border:: #{extracted[:border][:color]}:#{extracted[:border][:level]}"
1999
- lines << " title:: #{extracted[:title][:color]}:#{extracted[:title][:level]}"
2000
- lines << " focus:: #{extracted[:focus][:color]}:#{extracted[:focus][:level]}"
2001
- lines << " options:: #{extracted[:options][:color]}:#{extracted[:options][:level]}"
2002
- lines << " banner:: #{extracted[:banner][:color]}:#{extracted[:banner][:level]}"
2003
- lines << " subtitle:: #{extracted[:subtitle][:color]}:#{extracted[:subtitle][:level]}"
2004
- lines << " divider:: #{extracted[:divider][:color]}:#{extracted[:divider][:level]}"
2005
- lines << ">>"
2006
- lines << ""
2007
- lines << "<<table"
2008
- lines << " style:: #{extracted[:style] || 3}"
2009
- lines << " header_color:: yellow:2"
2010
- lines << " border_color:: rgb:2"
2011
- lines << " selected_row:: green:2"
2012
- lines << " row_color:: white:1"
2013
- lines << " zebra_striping:: true"
2014
- lines << ">>"
2015
- lines << ""
2016
- lines << "<<card"
2017
- lines << " style:: 7"
2018
- lines << " border_color:: cyan:2"
2019
- lines << " title_color:: yellow:2"
2020
- lines << " content_color:: white:1"
2021
- lines << ">>"
2022
- lines << ""
2023
- lines << "<<slider"
2024
- lines << " style:: 3"
2025
- lines << " color:: rgb:2"
2026
- lines << " fill_char:: █"
2027
- lines << " empty_char:: ░"
2028
- lines << ">>"
2029
- lines << ""
2030
- lines << "<<checkbox"
2031
- lines << " style:: 3"
2032
- lines << " color:: rgb:2"
2033
- lines << " checked_mark:: [X]"
2034
- lines << " unchecked_mark:: [ ]"
2035
- lines << ">>"
2036
- lines << ""
2037
- File.write(out, lines.join("\n") + "\n")
2038
- out
2039
- else
2040
- export_config(out)
2041
- end
2042
- end
2043
-
2044
- def self.split_ansi_chars(str)
2045
- segments = []
2046
- current_style = String.new("")
2047
- in_escape = false
2048
- escape_buf = String.new("")
2049
-
2050
- str.to_s.each_char do |ch|
2051
- if ch == "\e"
2052
- in_escape = true
2053
- escape_buf << ch
2054
- next
2055
- end
2056
- if in_escape
2057
- escape_buf << ch
2058
- if ch =~ /[a-zA-Z]/
2059
- in_escape = false
2060
- current_style = escape_buf.dup
2061
- escape_buf.clear
2062
- end
2063
- next
2064
- end
2065
- segments << { char: ch, style: current_style.dup }
2066
- end
2067
- segments
2068
- end
2069
-
2070
- def self.animate_render(lines, type = :diagonal, delay = 0.012)
2071
- type_str = type.to_s.downcase
2072
- return if lines.nil? || lines.empty?
2073
- rst = ansi_reset
2074
-
2075
- case type_str
2076
- when "diagonal"
2077
- parsed_rows = lines.map { |l| split_ansi_chars(l) }
2078
- max_len = parsed_rows.map(&:length).max || 0
2079
- total_steps = max_len + (parsed_rows.length * 2)
2080
- step = 0
2081
- while step <= total_steps
2082
- buffer = String.new(CURSOR_HOME)
2083
- parsed_rows.each_with_index do |row_segs, y|
2084
- rendered_row = String.new("")
2085
- row_segs.each_with_index do |seg, x|
2086
- if (x + y * 2) <= step
2087
- rendered_row << seg[:style] << seg[:char] << rst
2088
- else
2089
- rendered_row << " "
2090
- end
2091
- end
2092
- buffer << rendered_row << CLEAR_TO_EOL << "\r\n"
2093
- end
2094
- buffer << CLEAR_TO_EOS
2095
- Kernel.print(buffer)
2096
- $stdout.flush
2097
- sleep(delay)
2098
- step += 4
2099
- end
2100
- when "linear"
2101
- buffer = String.new(CURSOR_HOME)
2102
- lines.each do |line|
2103
- Kernel.print("#{line}#{CLEAR_TO_EOL}\r\n")
2104
- $stdout.flush
2105
- sleep(delay * 3)
2106
- end
2107
- when "fade"
2108
- [1, 2].each do |lvl|
2109
- buffer = String.new(CURSOR_HOME)
2110
- lines.each do |line|
2111
- clean = line.gsub(/\e\[[0-9;]*m/, '')
2112
- buffer << ansi_color("white", lvl) << clean << rst << CLEAR_TO_EOL << "\r\n"
2113
- end
2114
- buffer << CLEAR_TO_EOS
2115
- Kernel.print(buffer)
2116
- $stdout.flush
2117
- sleep(delay * 8)
2118
- end
2119
- end
2120
- end
2121
-
2122
- def self.alert(type, message, title: nil, style: 3, color: nil, border_color: nil, title_color: nil, pause: true)
2123
- type_sym = type.to_sym rescue :info
2124
- tag, def_col, def_title = case type_sym
2125
- when :success, :ok
2126
- ["[✔ EXITO]", "green", "Operacion Exitosa"]
2127
- when :error, :fail, :danger
2128
- ["[✖ ERROR]", "red", "Error en el Sistema"]
2129
- when :warning, :warn
2130
- ["[⚠ AVISO]", "yellow", "Advertencia"]
2131
- else
2132
- ["[ℹ INFO]", "cyan", "Informacion"]
2133
- end
2134
- card_col = border_color || color || def_col
2135
- card_title = title || "#{tag} #{def_title}"
2136
- card(title: card_title, content: message, style: style, color: card_col, title_color: title_color, pause: pause)
2137
- end
2138
-
2139
- def self.card(title_or_content = nil, content_arg = nil, title: nil, content: nil, style: nil, color: nil, border_color: nil, title_color: nil, content_color: nil, width: nil, pause: false)
2140
- c_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "card")) || {}
2141
- actual_title = title || (content_arg ? title_or_content : nil)
2142
- actual_content = (content || (content_arg ? content_arg : title_or_content) || "").to_s
2143
- style_num = (style || c_sec["style"] || 7).to_i
2144
- border_cfg = BORDERS[style_num] || BORDERS[7]
2145
- card_color = (border_color || color || c_sec["border_color"] || c_sec["color"] || "cyan").to_s
2146
- actual_title_color = (title_color || c_sec["title_color"] || "yellow").to_s
2147
- actual_content_color = (content_color || c_sec["content_color"] || "white").to_s
2148
- is_rgb = card_color.downcase == "rgb" || card_color.downcase == "rainbow" || card_color.downcase == "chroma"
2149
-
2150
- lines = actual_content.split("\n")
2151
- content_max = lines.map { |l| display_width(l) }.max || 0
2152
- box_w = width || [content_max + 6, actual_title ? display_width(actual_title) + 6 : 0, 46].max
2153
- box_w = [box_w, terminal_width - 2].min
2154
- inner_w = box_w - 2
2155
-
2156
- wrapped_lines = []
2157
- lines.each do |raw_l|
2158
- if display_width(raw_l) <= (inner_w - 2)
2159
- wrapped_lines << raw_l
2160
- else
2161
- cur = String.new("")
2162
- raw_l.split(" ").each do |w|
2163
- if cur.empty?
2164
- cur << w
2165
- elsif display_width("#{cur} #{w}") <= (inner_w - 2)
2166
- cur << " " << w
2167
- else
2168
- wrapped_lines << cur
2169
- cur = String.new(w)
2170
- end
2171
- end
2172
- wrapped_lines << cur unless cur.empty?
2173
- end
2174
- end
2175
-
2176
- tl = border_cfg[:tl] || "#"
2177
- tr = border_cfg[:tr] || "#"
2178
- bl = border_cfg[:bl] || "#"
2179
- br = border_cfg[:br] || "#"
2180
- h_char = border_cfg[:h] || "─"
2181
- v_char = border_cfg[:v] || "│"
2182
-
2183
- brd_col = is_rgb ? Color.rgb("").sub(/\e\[0m$/, '') : ansi_color(card_color, 1)
2184
- rst = ansi_reset
2185
-
2186
- top_str = if actual_title && !actual_title.empty?
2187
- t_clean = " #{actual_title} "
2188
- t_len = display_width(t_clean)
2189
- if t_len > inner_w
2190
- t_clean = " #{actual_title[0...[inner_w - 6, 1].max]}... "
2191
- t_len = display_width(t_clean)
2192
- end
2193
- l_len = [(inner_w - t_len) / 2, 0].max
2194
- r_len = [inner_w - t_len - l_len, 0].max
2195
- h_char * l_len + ansi_color(actual_title_color, 2) + t_clean + brd_col + h_char * r_len
2196
- else
2197
- h_char * inner_w
2198
- end
2199
-
2200
- out = +""
2201
- out << "#{brd_col}#{tl}#{top_str}#{tr}#{rst}\r\n"
2202
- wrapped_lines.each do |line|
2203
- pad_line = " " + line
2204
- out << "#{brd_col}#{v_char}#{rst}#{ansi_color(actual_content_color, 1)}#{pad_to_width(pad_line, inner_w)}#{rst}#{brd_col}#{v_char}#{rst}\r\n"
2205
- end
2206
- out << "#{brd_col}#{bl}#{h_char * inner_w}#{br}#{rst}\r\n"
2207
-
2208
- Kernel.print(out)
2209
- self.continue if pause
2210
- end
2211
-
2212
- def self.table(headers_arg = nil, rows_arg = nil, headers: nil, rows: nil, title: nil, style: nil, color: nil, header_color: nil, border_color: nil, selected_row: nil, page_size: nil, search: false, sort: false, animate: nil, width: nil, **kwargs)
2213
- t_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "table")) || {}
2214
- input_stream = $stdin
2215
- output_stream = $stdout
2216
- style_num = (style || t_sec["style"] || 3).to_i
2217
- border_cfg = BORDERS[style_num] || BORDERS[3]
2218
- tbl_color = (border_color || color || t_sec["border_color"] || t_sec["border"] || t_sec["color"] || "cyan").to_s
2219
- header_color = header_color || t_sec["header_color"] || "yellow"
2220
- focus_color = selected_row || t_sec["selected_row"] || t_sec["focus"] || "green"
2221
- page_size ||= (t_sec["page_size"] || 8).to_i
2222
-
2223
- resolved_headers = (headers || headers_arg || []).map(&:to_s)
2224
- resolved_raw_rows = (rows || rows_arg || []).map { |r| r.is_a?(Array) ? r.map(&:to_s) : r.values.map(&:to_s) }
2225
- headers = resolved_headers
2226
- raw_rows = resolved_raw_rows
2227
- filtered_rows = raw_rows.dup
2228
- selected_idx = 0
2229
- query = String.new("")
2230
- sort_col = nil
2231
- sort_asc = true
2232
- tick = 0.0
2233
-
2234
- calc_widths = lambda do
2235
- col_counts = [headers.length, raw_rows.map(&:length).max || 0].max
2236
- widths = Array.new(col_counts, 0)
2237
- headers.each_with_index { |h, i| widths[i] = [widths[i], display_width(h)].max }
2238
- filtered_rows.each do |row|
2239
- row.each_with_index { |cell, i| widths[i] = [widths[i], display_width(cell)].max }
2240
- end
2241
- widths.map { |w| w + 2 }
2242
- end
2243
-
2244
- draw_table = lambda do |t_tick|
2245
- is_rgb = tbl_color.downcase == "rgb" || tbl_color.downcase == "rainbow" || tbl_color.downcase == "chroma"
2246
- brd_color = is_rgb ? rgb_color(t_tick, 0.0) : ansi_color(tbl_color, 1)
2247
- hdr_color = is_rgb ? rgb_color(t_tick, 0.8) : ansi_color(header_color, 2)
2248
- foc_color = is_rgb ? rgb_color(t_tick, 1.4) : ansi_color(focus_color, 2)
2249
- rst = ansi_reset
2250
-
2251
- col_w = calc_widths.call
2252
- help_line = " ↑/↓: Moverse | Enter: Elegir | s: Ordenar | Esc: Salir"
2253
- tot_w = [col_w.sum + (col_w.length - 1) + 4, title ? display_width(title) + 8 : 0, display_width(help_line) + 4, 46].max
2254
- tot_w = [tot_w, terminal_width - 2].min
2255
- inner_w = tot_w - 2
2256
-
2257
- if inner_w < display_width(help_line)
2258
- help_line = " ↑/↓: Mover | Enter: Ok | Esc: Salir"
2259
- end
2260
-
2261
- tl = border_cfg[:tl] || "#"
2262
- tr = border_cfg[:tr] || "#"
2263
- bl = border_cfg[:bl] || "#"
2264
- br = border_cfg[:br] || "#"
2265
- h_char = border_cfg[:h] || "─"
2266
- v_char = border_cfg[:v] || "│"
2267
-
2268
- top_str = if title && !title.empty?
2269
- t_clean = " #{title} "
2270
- t_len = display_width(t_clean)
2271
- if t_len > inner_w
2272
- t_clean = " #{title[0...[(inner_w - 6), 1].max]}... "
2273
- t_len = display_width(t_clean)
2274
- end
2275
- left_len = [(inner_w - t_len) / 2, 0].max
2276
- right_len = [inner_w - t_len - left_len, 0].max
2277
- h_char * left_len + t_clean + h_char * right_len
2278
- else
2279
- h_char * inner_w
2280
- end
2281
-
2282
- out = String.new(CURSOR_HOME)
2283
- out << HIDE_CURSOR
2284
- out << "#{brd_color}#{tl}#{top_str}#{tr}#{rst}#{CLEAR_TO_EOL}\r\n"
2285
-
2286
- if search
2287
- s_line = " Buscar: #{query}█"
2288
- out << "#{brd_color}#{v_char}#{rst}#{pad_to_width(s_line, inner_w)}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2289
- out << "#{brd_color}#{v_char}#{h_char * inner_w}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2290
- end
2291
-
2292
- hdr_cells = headers.each_with_index.map do |h, i|
2293
- w = col_w[i] || 10
2294
- sort_indicator = sort_col == i ? (sort_asc ? " ▲" : " ▼") : ""
2295
- h_str = "#{h}#{sort_indicator}"
2296
- max_c = [w - 2, 2].max
2297
- h_str = h_str[0...[max_c - 2, 1].max] + ".." if display_width(h_str) > max_c
2298
- pad_to_width(" #{h_str}", w)
2299
- end
2300
- hdr_row_str = " " + hdr_cells.join("│")
2301
- hdr_row_str = hdr_row_str[0...inner_w] if display_width(hdr_row_str) > inner_w
2302
- out << "#{brd_color}#{v_char}#{rst}#{hdr_color}#{pad_to_width(hdr_row_str, inner_w)}#{rst}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2303
- out << "#{brd_color}#{v_char}#{h_char * inner_w}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2304
-
2305
- max_visible = page_size || 8
2306
- total_rows = filtered_rows.length
2307
- if total_rows == 0
2308
- empty_msg = " (Sin registros que coincidan con '#{query}')"
2309
- out << "#{brd_color}#{v_char}#{rst}#{pad_to_width(empty_msg, inner_w)}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2310
- else
2311
- start_idx = [(selected_idx - max_visible / 2), 0].max
2312
- start_idx = [start_idx, [total_rows - max_visible, 0].max].min
2313
- end_idx = [start_idx + max_visible - 1, total_rows - 1].min
2314
-
2315
- if start_idx > 0
2316
- up_str = " ▲ (+#{start_idx} arriba)"
2317
- out << "#{brd_color}#{v_char}#{rst}#{ansi_color('gray', 1)}#{pad_to_width(up_str, inner_w)}#{rst}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2318
- end
2319
-
2320
- (start_idx..end_idx).each do |r_i|
2321
- row = filtered_rows[r_i]
2322
- is_active = (r_i == selected_idx)
2323
- prefix = is_active ? "> " : " "
2324
-
2325
- row_cells = row.each_with_index.map do |cell, c_i|
2326
- w = col_w[c_i] || 10
2327
- c_str = cell.to_s
2328
- max_c = [w - 2, 2].max
2329
- c_str = c_str[0...[max_c - 2, 1].max] + ".." if display_width(c_str) > max_c
2330
- pad_to_width(" #{c_str}", w)
2331
- end
2332
- row_str = prefix + row_cells.join("│")[1..-1].to_s
2333
- row_str = row_str[0...inner_w] if display_width(row_str) > inner_w
2334
-
2335
- if is_active
2336
- out << "#{brd_color}#{v_char}#{rst}#{foc_color}#{pad_to_width(row_str, inner_w)}#{rst}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2337
- else
2338
- out << "#{brd_color}#{v_char}#{rst}#{pad_to_width(row_str, inner_w)}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2339
- end
2340
- end
2341
-
2342
- remaining_down = total_rows - 1 - end_idx
2343
- if remaining_down > 0
2344
- down_str = " ▼ (+#{remaining_down} abajo)"
2345
- out << "#{brd_color}#{v_char}#{rst}#{ansi_color('gray', 1)}#{pad_to_width(down_str, inner_w)}#{rst}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2346
- end
2347
- end
2348
-
2349
- out << "#{brd_color}#{v_char}#{h_char * inner_w}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2350
- out << "#{brd_color}#{v_char}#{rst}#{ansi_color('gray', 1)}#{pad_to_width(help_line, inner_w)}#{rst}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2351
- out << "#{brd_color}#{bl}#{h_char * inner_w}#{br}#{rst}#{CLEAR_TO_EOL}\r\n"
2352
- out << CLEAR_TO_EOS
2353
- output_stream.print(out)
2354
- output_stream.flush
2355
- end
2356
-
2357
- loop_res = nil
2358
- reader = lambda do |stream|
2359
- loop do
2360
- draw_table.call(tick)
2361
- is_anim = color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma"
2362
- if is_anim
2363
- ready = false
2364
- if stream.respond_to?(:to_io) || stream.is_a?(IO)
2365
- begin
2366
- res = IO.select([stream], nil, nil, 0.035)
2367
- ready = true if res && res[0] && !res[0].empty?
2368
- rescue StandardError
2369
- ready = true
2370
- end
2371
- else
2372
- ready = true
2373
- end
2374
- unless ready
2375
- tick += 0.08
2376
- next
2377
- end
2378
- end
2379
-
2380
- key = read_key_raw(stream)
2381
- break if key.nil? || key == "\x03" || key == "\x04"
2382
-
2383
- if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
2384
- if filtered_rows.length > 0
2385
- selected_idx = (selected_idx - 1) % filtered_rows.length
2386
- end
2387
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
2388
- if filtered_rows.length > 0
2389
- selected_idx = (selected_idx + 1) % filtered_rows.length
2390
- end
2391
- elsif key == "\e[5~" || key == "\e[D"
2392
- if filtered_rows.length > 0
2393
- selected_idx = [(selected_idx - (page_size || 8)), 0].max
2394
- end
2395
- elsif key == "\e[6~" || key == "\e[C"
2396
- if filtered_rows.length > 0
2397
- selected_idx = [(selected_idx + (page_size || 8)), filtered_rows.length - 1].min
2398
- end
2399
- elsif key == "\r" || key == "\n"
2400
- if filtered_rows.length > 0
2401
- loop_res = filtered_rows[selected_idx]
2402
- end
2403
- break
2404
- elsif key == "\e"
2405
- if search && !query.empty?
2406
- query.clear
2407
- filtered_rows = raw_rows.dup
2408
- selected_idx = 0
2409
- else
2410
- loop_res = nil
2411
- break
2412
- end
2413
- elsif key == "\x7f" || key == "\b" || key == "\x08"
2414
- if search && !query.empty?
2415
- query.chop!
2416
- if query.empty?
2417
- filtered_rows = raw_rows.dup
2418
- else
2419
- filtered_rows = raw_rows.select { |r| r.any? { |c| c.downcase.include?(query.downcase) } }
2420
- end
2421
- selected_idx = 0
2422
- end
2423
- elsif key == "\x15"
2424
- if search
2425
- query.clear
2426
- filtered_rows = raw_rows.dup
2427
- selected_idx = 0
2428
- end
2429
- elsif (!search || query.empty?) && (key == "s" || key == "S")
2430
- if sort
2431
- sort_col = ((sort_col || -1) + 1) % [headers.length, 1].max
2432
- filtered_rows.sort_by! { |r| r[sort_col] || "" }
2433
- selected_idx = 0
2434
- end
2435
- elsif (!search || query.empty?) && (key == "q" || key == "Q")
2436
- loop_res = nil
2437
- break
2438
- elsif search && key =~ /^[[:print:]]$/
2439
- query << key
2440
- filtered_rows = raw_rows.select { |r| r.any? { |c| c.downcase.include?(query.downcase) } }
2441
- selected_idx = 0
2442
- end
2443
- end
2444
- end
2445
-
2446
- begin
2447
- output_stream.print("#{HIDE_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
2448
- if input_stream.respond_to?(:raw) && input_stream.respond_to?(:tty?) && input_stream.tty?
2449
- input_stream.raw { |s| reader.call(s) }
2450
- else
2451
- reader.call(input_stream)
2452
- end
2453
- ensure
2454
- output_stream.print("#{SHOW_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
2455
- end
2456
- loop_res
2457
- end
2458
-
2459
- def self.div(long = nil, color = "blue", level = 1, char = "─")
2460
- width = long || [terminal_width - 2, 64].min
2461
- if color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma"
2462
- Kernel.print("#{Color.rgb(char * width)}\r\n")
2463
- else
2464
- color_code = ansi_color(color, level)
2465
- reset_code = ansi_reset
2466
- Kernel.print("#{color_code}#{char * width}#{reset_code}\r\n")
2467
- end
2468
- end
2469
-
2470
- def self.help(section = :all)
2471
- path = find_data_file("help.txt")
2472
- return unless path && File.exist?(path)
2473
- content = File.read(path)
2474
- COLORS.each do |c_name, lvls|
2475
- if lvls.is_a?(Hash)
2476
- content.gsub!("{#{c_name}}", ansi_color(c_name, 1))
2477
- content.gsub!("{bright_#{c_name}}", ansi_color(c_name, 2))
2478
- end
2479
- end
2480
- content.gsub!("{reset}", ansi_reset)
2481
- Kernel.print("\r\n#{content}\r\n")
2482
- end
2483
-
2484
- def help
2485
- self.class.help
2486
- end
2487
-
2488
- def self.continue(text = "Presiona cualquier tecla para continuar...")
2489
- Kernel.print("#{Color.gray(text)} ")
2490
- if $stdin.respond_to?(:raw) && $stdin.respond_to?(:tty?) && $stdin.tty?
2491
- $stdin.raw(&:getch)
2492
- elsif $stdin.respond_to?(:getch)
2493
- $stdin.getch
2494
- else
2495
- $stdin.read(1)
2496
- end
2497
- Kernel.print("\r\n")
2498
- end
2499
-
2500
- def self.build_ascii_lines(text, max_cols = terminal_width, font_id = 1)
2501
- target_font = FONTS[font_id.to_i] || FONTS[1]
2502
- clean_chars = text.to_s.upcase.chars.select { |c| target_font.key?(c) }
2503
- return [] if clean_chars.empty?
2504
-
2505
- font_height = target_font.values.first.length
2506
-
2507
- [2, 1, 0].each do |spacing|
2508
- lines = Array.new(font_height, "")
2509
- clean_chars.each_with_index do |c, idx|
2510
- fig = target_font[c]
2511
- pad = (idx == clean_chars.length - 1) ? "" : (" " * spacing)
2512
- font_height.times { |i| lines[i] += fig[i] + pad }
2513
- end
2514
-
2515
- max_len = lines.map { |l| display_width(l) }.max
2516
- return lines if (max_len + 6) <= max_cols
2517
- end
2518
-
2519
- nil
2520
- end
2521
-
2522
- def self.banner(text, delay = 0, color: "magenta", level: 2, style: 3, font: 1)
2523
- cols = terminal_width
2524
- is_rgb = (color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma")
2525
- color_code = is_rgb ? "" : ansi_color(color, level)
2526
- reset_code = ansi_reset
2527
-
2528
- ascii_rows = build_ascii_lines(text, cols, font)
2529
- border_cfg = BORDERS[style] || BORDERS[3]
2530
- h_top = border_cfg[:ht] || border_cfg[:h]
2531
- h_bot = border_cfg[:hb] || border_cfg[:h]
2532
- v_l = border_cfg[:vl] || border_cfg[:v]
2533
- v_r = border_cfg[:vr] || border_cfg[:v]
2534
-
2535
- if ascii_rows
2536
- max_len = ascii_rows.map { |r| display_width(r) }.max
2537
- top_fill = (h_top * ((max_len + 4).to_f / h_top.length).ceil)[0...(max_len + 4)]
2538
- bot_fill = (h_bot * ((max_len + 4).to_f / h_bot.length).ceil)[0...(max_len + 4)]
2539
-
2540
- if is_rgb
2541
- Kernel.print("#{Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")}\r\n")
2542
- ascii_rows.each_with_index do |line, idx|
2543
- pad = " " * (max_len - display_width(line))
2544
- row_content = " #{line}#{pad} "
2545
- Kernel.print("#{Color.rgb(v_l)}#{Color.rgb(row_content, idx * 0.2)}#{Color.rgb(v_r)}\r\n")
2546
- sleep(delay) if delay > 0
2547
- end
2548
- Kernel.print("#{Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")}\r\n")
2549
- else
2550
- Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
2551
- ascii_rows.each do |line|
2552
- pad = " " * (max_len - display_width(line))
2553
- Kernel.print("#{color_code}#{v_l} #{line}#{pad} #{v_r}#{reset_code}\r\n")
2554
- sleep(delay) if delay > 0
2555
- end
2556
- Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
2557
- end
2558
- else
2559
- clean_t = text.to_s.strip
2560
- box_w = [display_width(clean_t) + 6, cols - 2].min
2561
- top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
2562
- bot_fill = (h_bot * ((box_w - 2).to_f / h_bot.length).ceil)[0...(box_w - 2)]
2563
-
2564
- pad_t = [box_w - 4 - display_width(clean_t), 0].max
2565
- l_p = " " * (pad_t / 2)
2566
- r_p = " " * (pad_t - (pad_t / 2))
2567
-
2568
- if is_rgb
2569
- Kernel.print("#{Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")}\r\n")
2570
- Kernel.print("#{Color.rgb(v_l)} #{Color.rgb(l_p + clean_t + r_p)} #{Color.rgb(v_r)}\r\n")
2571
- Kernel.print("#{Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")}\r\n")
2572
- else
2573
- Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
2574
- Kernel.print("#{color_code}#{v_l} #{l_p}#{clean_t}#{r_p} #{v_r}#{reset_code}\r\n")
2575
- Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
2576
- end
2577
- end
2578
- end
2579
-
2580
- class << self
2581
- alias_method :message, :banner
2582
- alias_method :logo, :banner
2583
-
2584
- def tabs(tabs_hash, *args, **kwargs)
2585
- new([], *args, tabs: tabs_hash, **kwargs)
2586
- end
2587
- end
2588
-
2589
- def initialize(functions = [], *positional_arguments, title: nil, banner: nil, subtitle: nil, description: nil, divider: nil, style: nil, banner_style: nil, center: true, font: nil, page_size: nil, search: false, columns: 1, image: nil, image_width: nil, mouse: nil, tabs: nil, active_tab_color: nil, tab_color: nil, **keyword_arguments)
2590
- tabs_data = tabs || keyword_arguments[:tabs] || (functions.is_a?(Hash) ? functions : nil)
2591
- if tabs_data.is_a?(Hash) && !tabs_data.empty?
2592
- @tabs = tabs_data.keys.map(&:to_s)
2593
- @tab_contents = tabs_data.transform_keys(&:to_s)
2594
- @active_tab_idx = 0
2595
- @functions = @tab_contents[@tabs[@active_tab_idx]] || []
2596
- else
2597
- @tabs = nil
2598
- @tab_contents = nil
2599
- @active_tab_idx = nil
2600
- @functions = functions.is_a?(Array) ? functions : Array(functions)
2601
- end
2602
-
2603
- pos_title = positional_arguments[0]
2604
- pos_style = positional_arguments[1]
2605
-
2606
- @title = (title || pos_title || keyword_arguments[:title] || "").to_s
2607
- @banner = (banner || keyword_arguments[:banner] || "").to_s
2608
- @subtitle = (subtitle || description || keyword_arguments[:subtitle] || keyword_arguments[:description] || "").to_s
2609
- @divider = divider.nil? ? (!@banner.empty? || !@subtitle.empty?) : divider
2610
-
2611
- theme_menu_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, 'menu')) || {}
2612
- th_style = theme_menu_sec['style']&.to_i
2613
- th_bstyle = theme_menu_sec['banner_style']&.to_i
2614
-
2615
- @style = (style || pos_style || keyword_arguments[:style] || th_style || 19).to_i
2616
- @banner_style = (banner_style || keyword_arguments[:banner_style] || th_bstyle || 3).to_i
2617
- @center = center.nil? ? (theme_menu_sec.key?('center') ? (theme_menu_sec['center'].to_s != 'false') : true) : center
2618
- @page_size = (page_size || keyword_arguments[:page_size])&.to_i
2619
- @search = search || keyword_arguments[:search] || false
2620
- @columns = [(columns || keyword_arguments[:columns] || 1).to_i, 1].max
2621
- @image = image || keyword_arguments[:image]
2622
- @image_width = (image_width || keyword_arguments[:image_width])&.to_i
2623
- @animate = (keyword_arguments[:animate] || (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, 'menu', 'animate')) || false).to_s
2624
- m_val = mouse.nil? ? keyword_arguments[:mouse] : mouse
2625
- if m_val.nil?
2626
- @mouse = theme_menu_sec.key?('mouse') ? (theme_menu_sec['mouse'].to_s != 'false') : true
2627
- else
2628
- @mouse = (m_val == true || m_val.to_s == 'true')
2629
- end
2630
-
2631
- t_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "tabs")) || {}
2632
- @active_tab_color = (active_tab_color || keyword_arguments[:active_tab_color] || t_sec["active_tab"] || t_sec["active_tab_color"] || "yellow").to_s
2633
- @tab_color = (tab_color || keyword_arguments[:tab_color] || t_sec["tab_color"] || t_sec["inactive_tab"] || t_sec["color"] || "gray").to_s
2634
-
2635
- @query = String.new("")
2636
- @index = 0
2637
- @rgb_tick = 0.0
2638
-
2639
- @level = 0
2640
- @open_level = 0
2641
- @sub_index_1 = 0
2642
- @sub_index_2 = 0
2643
- @sub_1_hit_map = {}
2644
- @sub_2_hit_map = {}
2645
- @sub_1_col_rng = nil
2646
- @sub_2_col_rng = nil
2647
-
2648
- @active_panel = :main
2649
- @sub_index = 0
2650
- @submenu_open = false
2651
-
2652
- @row_hit_map = {}
2653
- @tab_ranges = {}
2654
- @tabs_row = nil
2655
- @up_arrow_row = nil
2656
- @down_arrow_row = nil
2657
- @sub_hit_map = {}
2658
-
2659
- @cached_image_lines = nil
2660
- @cached_image_cols = nil
2661
-
2662
- init_font = font || keyword_arguments[:font_style] || SetStyle.font || 1
2663
- init_pfx = @desc_prefix || (@@global_theme.is_a?(Hash) && (@@global_theme.dig(:sections, 'menu', 'desc_prefix') || @@global_theme.dig(:sections, 'menu', 'prefix'))) || SetStyle.desc_prefix || "[i]"
2664
-
2665
- @style_config = SetStyle.new(
2666
- border: SetStyle.border.dup,
2667
- options: SetStyle.options.dup,
2668
- focus: SetStyle.focus.dup,
2669
- title: SetStyle.title.dup,
2670
- banner: SetStyle.banner.dup,
2671
- subtitle: SetStyle.subtitle.dup,
2672
- divider: SetStyle.divider.dup,
2673
- font: init_font,
2674
- desc_prefix: init_pfx
2675
- )
2676
- end
2677
-
2678
- def current_matching_indices
2679
- if @search && !@query.empty?
2680
- indices = []
2681
- @functions.each_with_index do |func, idx|
2682
- name = extract_name_from_action(func)
2683
- indices << idx if name.downcase.include?(@query.downcase)
2684
- end
2685
- indices
2686
- else
2687
- (0...@functions.length).to_a
2688
- end
2689
- end
2690
-
2691
- def move_up
2692
- matching = current_matching_indices
2693
- return @index if matching.empty?
2694
- cols = @columns
2695
- pos = matching.index(@index) || 0
2696
- if cols <= 1
2697
- new_pos = (pos - 1) % matching.length
2698
- else
2699
- new_pos = pos - cols
2700
- if new_pos < 0
2701
- new_pos = pos
2702
- while (new_pos + cols) < matching.length
2703
- new_pos += cols
2704
- end
2705
- end
2706
- end
2707
- @index = matching[new_pos]
2708
- end
2709
- alias_method :_up, :move_up
2710
-
2711
- def move_down
2712
- matching = current_matching_indices
2713
- return @index if matching.empty?
2714
- cols = @columns
2715
- pos = matching.index(@index) || 0
2716
- if cols <= 1
2717
- new_pos = (pos + 1) % matching.length
2718
- else
2719
- new_pos = pos + cols
2720
- if new_pos >= matching.length
2721
- new_pos = pos % cols
2722
- end
2723
- end
2724
- @index = matching[new_pos]
2725
- end
2726
- alias_method :_down, :move_down
2727
-
2728
- def move_left
2729
- matching = current_matching_indices
2730
- return @index if matching.empty?
2731
- cols = @columns
2732
- pos = matching.index(@index) || 0
2733
- if cols <= 1
2734
- new_pos = (pos - 1) % matching.length
2735
- else
2736
- if (pos % cols) == 0
2737
- new_pos = [pos + (cols - 1), matching.length - 1].min
2738
- else
2739
- new_pos = pos - 1
2740
- end
2741
- end
2742
- @index = matching[new_pos]
2743
- end
2744
-
2745
- def move_right
2746
- matching = current_matching_indices
2747
- return @index if matching.empty?
2748
- cols = @columns
2749
- pos = matching.index(@index) || 0
2750
- if cols <= 1
2751
- new_pos = (pos + 1) % matching.length
2752
- else
2753
- if (pos % cols) == (cols - 1) || pos == (matching.length - 1)
2754
- new_pos = pos - (pos % cols)
2755
- else
2756
- new_pos = pos + 1
2757
- end
2758
- end
2759
- @index = matching[new_pos]
2760
- end
2761
-
2762
- def colorize(text, color_config, phase_offset = 0.0)
2763
- return text.to_s if color_config.nil? || color_config.empty?
2764
-
2765
- color_name = (color_config[:color] || color_config["color"]).to_s.downcase.strip
2766
- brightness_level = (color_config[:level] || color_config["level"] || 1).to_i
2767
-
2768
- if color_name.include?(":")
2769
- parts = color_name.split(":")
2770
- color_name = parts[0].strip
2771
- brightness_level = parts[1].to_i if parts[1] && !parts[1].empty?
2772
- end
2773
-
2774
- is_anim_active = @animate && ["neon", "fade"].include?(@animate.to_s.downcase)
2775
- is_chroma = color_name == "rgb" || color_name == "rainbow" || color_name == "chroma" || @animate.to_s.downcase == "rgb"
2776
-
2777
- if is_chroma
2778
- tick = @rgb_tick || 0.0
2779
- out = String.new("")
2780
- char_count = 0
2781
- in_escape = false
2782
- escape_buf = String.new("")
2783
-
2784
- text.to_s.each_char do |ch|
2785
- if ch == "\e"
2786
- in_escape = true
2787
- escape_buf << ch
2788
- next
2789
- end
2790
- if in_escape
2791
- escape_buf << ch
2792
- if ch =~ /[a-zA-Z]/
2793
- in_escape = false
2794
- out << escape_buf
2795
- escape_buf.clear
2796
- end
2797
- next
2798
- end
2799
-
2800
- if ch == " " || ch == "\t" || ch == "\r" || ch == "\n"
2801
- out << ch
2802
- else
2803
- c_code = self.class.rgb_color(tick, char_count * 0.12 + phase_offset)
2804
- out << "#{c_code}#{ch}"
2805
- char_count += 1
2806
- end
2807
- end
2808
- out << self.class.ansi_reset
2809
- return out
2810
- end
2811
-
2812
- if is_anim_active
2813
- tick = @rgb_tick || 0.0
2814
- base = if color_name =~ /\A#?([0-9a-f]{6})\z/i
2815
- h = $1
2816
- [h[0..1].to_i(16), h[2..3].to_i(16), h[4..5].to_i(16)]
2817
- elsif color_name =~ /\A#?([0-9a-f]{3})\z/i
2818
- h = $1
2819
- [(h[0] * 2).to_i(16), (h[1] * 2).to_i(16), (h[2] * 2).to_i(16)]
2820
- else
2821
- BASE_RGB[color_name] || [255, 255, 255]
2822
- end
2823
-
2824
- factor = (Math.sin(tick + phase_offset) + 1.0) / 2.0
2825
- f = 0.35 + 0.65 * factor
2826
- r = (base[0] * f).clamp(0, 255).to_i
2827
- g = (base[1] * f).clamp(0, 255).to_i
2828
- b = (base[2] * f).clamp(0, 255).to_i
2829
- glow = (factor > 0.85) ? ";1" : ""
2830
- return "\e[38;2;#{r};#{g};#{b}#{glow}m#{text}#{self.class.ansi_reset}"
2831
- end
2832
-
2833
- color_code = self.class.ansi_color(color_name, brightness_level)
2834
- return text.to_s unless color_code
2835
-
2836
- "#{color_code}#{text}#{self.class.ansi_reset}"
2837
- end
2838
- alias_method :_colorize, :colorize
2839
-
2840
- def build_horizontal_line(pattern, target_width)
2841
- return "" if target_width <= 0 || pattern.nil? || pattern.empty?
2842
-
2843
- pattern_length = pattern.length
2844
- repetitions_needed = (target_width.to_f / pattern_length).ceil + 1
2845
- (pattern * repetitions_needed)[0...target_width]
2846
- end
2847
- alias_method :_hline, :build_horizontal_line
2848
-
2849
- def render_banner_lines(term_cols)
2850
- return [[], 0] if @banner.nil? || @banner.empty?
2851
-
2852
- font_id = @style_config.font || 1
2853
- ascii_rows = self.class.build_ascii_lines(@banner, term_cols, font_id)
2854
- banner_border = BORDERS[@banner_style] || BORDERS[3]
2855
- banner_color_cfg = @style_config.banner
2856
-
2857
- h_top = banner_border[:ht] || banner_border[:h]
2858
- h_bot = banner_border[:hb] || banner_border[:h]
2859
- v_l = banner_border[:vl] || banner_border[:v]
2860
- v_r = banner_border[:vr] || banner_border[:v]
2861
-
2862
- lines = []
2863
- box_w = 0
2864
- if ascii_rows
2865
- content_w = ascii_rows.map { |r| GRmenu.display_width(r) }.max
2866
- box_w = content_w + 6
2867
- top_fill = build_horizontal_line(h_top, content_w + 4)
2868
- bot_fill = build_horizontal_line(h_bot, content_w + 4)
2869
-
2870
- lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg, 0.0)
2871
- ascii_rows.each_with_index do |row, r_i|
2872
- pad = " " * (content_w - GRmenu.display_width(row))
2873
- lines << colorize("#{v_l} #{row}#{pad} #{v_r}", banner_color_cfg, r_i * 0.2)
2874
- end
2875
- lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg, 1.2)
2876
- else
2877
- clean_b = @banner.strip
2878
- b_vis_w = GRmenu.display_width(clean_b)
2879
- box_w = [b_vis_w + 6, term_cols - 2].min
2880
- top_fill = build_horizontal_line(h_top, box_w - 2)
2881
- bot_fill = build_horizontal_line(h_bot, box_w - 2)
2882
-
2883
- pad_b = [box_w - 4 - b_vis_w, 0].max
2884
- l_p = " " * (pad_b / 2)
2885
- r_p = " " * (pad_b - (pad_b / 2))
2886
-
2887
- lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg, 0.0)
2888
- lines << colorize("#{v_l} #{l_p}#{clean_b}#{r_p} #{v_r}", banner_color_cfg, 0.4)
2889
- lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg, 0.8)
2890
- end
2891
- [lines, box_w]
2892
- end
2893
-
2894
- def render_image_lines(term_cols)
2895
- return @cached_image_lines if @cached_image_lines && @cached_image_cols == term_cols
2896
-
2897
- return [[], 0] unless @image && File.exist?(@image)
2898
-
2899
- raw_lines = self.class.load_and_render_image(@image, @image_width || 40, nil, term_cols)
2900
- return [[], 0] if raw_lines.empty?
2901
-
2902
- img_w = self.class.display_width(raw_lines.first)
2903
- box_w = img_w + 4
2904
- banner_border = BORDERS[@banner_style] || BORDERS[3]
2905
- banner_color_cfg = @style_config.banner
2906
-
2907
- h_top = banner_border[:ht] || banner_border[:h]
2908
- h_bot = banner_border[:hb] || banner_border[:h]
2909
- v_l = banner_border[:vl] || banner_border[:v]
2910
- v_r = banner_border[:vr] || banner_border[:v]
2911
-
2912
- top_fill = build_horizontal_line(h_top, img_w + 2)
2913
- bot_fill = build_horizontal_line(h_bot, img_w + 2)
2914
-
2915
- lines = []
2916
- lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg)
2917
- raw_lines.each do |r_line|
2918
- lines << "#{colorize(v_l, banner_color_cfg)} #{r_line} #{colorize(v_r, banner_color_cfg)}"
2919
- end
2920
- lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg)
2921
-
2922
- @cached_image_cols = term_cols
2923
- @cached_image_lines = [lines, box_w]
2924
- @cached_image_lines
2925
- end
2926
-
2927
- def is_submenu_item?(action)
2928
- if action.is_a?(Array)
2929
- target = action[1]
2930
- return true if target.is_a?(Array) && (target.empty? || target.first.is_a?(Array) || target.first.is_a?(Proc) || target.first.is_a?(Method) || target.first.is_a?(Symbol))
2931
- return true if target.is_a?(GRmenu)
2932
- elsif action.is_a?(Hash)
2933
- return true if action[:submenu] || action["submenu"]
2934
- end
2935
- false
2936
- end
2937
-
2938
- def get_submenu_actions(action)
2939
- if action.is_a?(Array)
2940
- action[1].is_a?(GRmenu) ? action[1].instance_variable_get(:@functions) : action[1]
2941
- elsif action.is_a?(Hash)
2942
- action[:submenu] || action["submenu"]
2943
- end
2944
- end
2945
-
2946
- def render_submenu_box(sub_actions, parent_title = "", is_active: false, selected_idx: 0)
2947
- sub_names = sub_actions.map { |a| extract_name_from_action(a) }
2948
- max_name_len = sub_names.empty? ? 10 : sub_names.map { |n| GRmenu.display_width(n) }.max
2949
- sub_title = parent_title.to_s
2950
- sub_w = [max_name_len + 8, GRmenu.display_width(sub_title) + 6, 20].max
2951
- sub_w = [sub_w, 36].min
2952
- inner_w = [sub_w - 2, 8].max
2953
-
2954
- s_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "submenu")) || {}
2955
- sub_style = (s_sec["style"] || @style || 3).to_i
2956
- sub_border = BORDERS[sub_style] || BORDERS[3]
2957
- h_top = sub_border[:ht] || sub_border[:h]
2958
- h_bot = sub_border[:hb] || sub_border[:h]
2959
- v_l = sub_border[:vl] || sub_border[:v]
2960
- v_r = sub_border[:vr] || sub_border[:v]
2961
-
2962
- s_brd_col = s_sec["border"] || s_sec["border_color"] || @style_config.border[:color] || "cyan"
2963
- s_foc_col = s_sec["focus"] || s_sec["focus_color"] || @style_config.focus[:color] || "yellow"
2964
- s_opt_col = s_sec["options"] || s_sec["options_color"] || @style_config.options[:color] || "white"
2965
-
2966
- s_brd_cfg = { color: s_brd_col, level: 1 }
2967
- s_foc_cfg = { color: s_foc_col, level: 2 }
2968
- s_opt_cfg = { color: s_opt_col, level: 1 }
2969
- s_held_cfg = { color: "yellow", level: 1 }
2970
-
2971
- lines = []
2972
- top_fill = build_horizontal_line(h_top, inner_w)
2973
- bot_fill = build_horizontal_line(h_bot, inner_w)
2974
-
2975
- top_border_line = sub_border[:tl] + top_fill + sub_border[:tr]
2976
- lines << colorize(top_border_line, s_brd_cfg, 0.0)
2977
-
2978
- unless sub_title.empty?
2979
- pad_t = [inner_w - 2 - GRmenu.display_width(sub_title), 0].max
2980
- t_padded = " " * (pad_t / 2) + sub_title + " " * (pad_t - (pad_t / 2))
2981
- c_title = colorize(t_padded, s_foc_cfg, 0.2)
2982
- v_l_col = colorize(v_l, s_brd_cfg, 0.2)
2983
- v_r_col = colorize(v_r, s_brd_cfg, 0.2)
2984
- lines << "#{v_l_col} #{c_title} #{v_r_col}"
2985
- mid_fill = build_horizontal_line(h_top, inner_w)
2986
- lines << colorize(v_l + mid_fill + v_r, s_brd_cfg, 0.4)
2987
- end
2988
-
2989
- parent_item_row = nil
2990
- sub_names.each_with_index do |s_name, s_idx|
2991
- is_sub = is_submenu_item?(sub_actions[s_idx])
2992
- arrow = is_sub ? "▶" : ""
2993
- is_selected = (s_idx == selected_idx)
2994
- parent_item_row = lines.length if is_selected
2995
-
2996
- prefix = is_selected ? "> " : " "
2997
- pad_s = [inner_w - 2 - prefix.length - GRmenu.display_width(s_name) - (is_sub ? 2 : 0), 0].max
2998
- raw_item = if is_sub
2999
- "#{prefix}#{s_name}#{' ' * pad_s} #{arrow}"
3000
- else
3001
- "#{prefix}#{s_name}#{' ' * pad_s}"
3002
- end
3003
-
3004
- item_cfg = if is_selected
3005
- is_active ? s_foc_cfg : s_held_cfg
3006
- else
3007
- s_opt_cfg
3008
- end
3009
-
3010
- colored_item = colorize(raw_item, item_cfg, s_idx * 0.2)
3011
- v_l_col = colorize(v_l, s_brd_cfg, 0.2)
3012
- v_r_col = colorize(v_r, s_brd_cfg, 0.8)
3013
- lines << "#{v_l_col} #{colored_item} #{v_r_col}"
3014
- end
3015
-
3016
- bot_border_line = sub_border[:bl] + bot_fill + sub_border[:br]
3017
- lines << colorize(bot_border_line, s_brd_cfg, 0.6)
3018
-
3019
- [lines, sub_w, parent_item_row]
3020
- end
3021
-
3022
- def render_lines(size_max = 20)
3023
- term_cols = self.class.terminal_width
3024
- term_rows = self.class.terminal_height
3025
- rendered_lines = []
3026
- @row_hit_map = {}
3027
- @tab_ranges = {}
3028
- @tabs_row = nil
3029
- @up_arrow_row = nil
3030
- @down_arrow_row = nil
3031
- @sub_hit_map = {}
3032
- @sub_1_hit_map = {}
3033
- @sub_2_hit_map = {}
3034
- @sub_1_col_rng = nil
3035
- @sub_2_col_rng = nil
3036
-
3037
- header_box_width = 0
3038
- header_lines_count = 0
3039
-
3040
- if @image && File.exist?(@image)
3041
- img_lines, img_box_w = render_image_lines(term_cols)
3042
- unless img_lines.empty?
3043
- rendered_lines.concat(img_lines)
3044
- rendered_lines << ""
3045
- header_lines_count += img_lines.length + 1
3046
- header_box_width = [header_box_width, img_box_w].max
3047
- end
3048
- end
3049
-
3050
- if @banner && !@banner.empty?
3051
- banner_lines, banner_box_w = render_banner_lines(term_cols)
3052
- unless banner_lines.empty?
3053
- b_pad = (@center && term_cols > banner_box_w) ? " " * ((term_cols - banner_box_w) / 2) : ""
3054
- rendered_lines.concat(banner_lines.map { |l| "#{b_pad}#{l}" })
3055
- rendered_lines << ""
3056
- header_lines_count += banner_lines.length + 1
3057
- header_box_width = [header_box_width, banner_box_w].max
3058
- end
3059
- end
3060
-
3061
- matching_indices = current_matching_indices
3062
- all_names = @functions.map { |func| extract_name_from_action(func) }
3063
- all_descriptions = @functions.map { |func| extract_description_from_action(func) }
3064
-
3065
- active_desc = all_descriptions[@index] || ""
3066
-
3067
- cols = @columns
3068
- max_item_len = all_names.empty? ? 10 : all_names.map { |n| GRmenu.display_width(n) }.max
3069
- grid_suggested_w = (max_item_len + 6) * cols + 4
3070
-
3071
- calculated_width = [size_max, GRmenu.display_width(@title) + 4, grid_suggested_w].max
3072
- calculated_width = ([calculated_width, GRmenu.display_width(active_desc) + 8].max) unless active_desc.empty?
3073
- calculated_width = ([calculated_width, GRmenu.display_width(@query) + 16].max) if @search
3074
- total_width = [calculated_width, term_cols - 2].min
3075
-
3076
- sub_1_preview_w = 0
3077
- sub_2_preview_w = 0
3078
- if @open_level >= 1 && is_submenu_item?(@functions[@index])
3079
- s1_acts = get_submenu_actions(@functions[@index])
3080
- if s1_acts && !s1_acts.empty?
3081
- s1_names = s1_acts.map { |a| extract_name_from_action(a) }
3082
- s1_max = s1_names.empty? ? 10 : s1_names.map { |n| GRmenu.display_width(n) }.max
3083
- s1_title = all_names[@index] || ""
3084
- sub_1_preview_w = [s1_max + 8, GRmenu.display_width(s1_title) + 6, 20].max
3085
- sub_1_preview_w = [sub_1_preview_w, 36].min
3086
-
3087
- if @open_level >= 2 && is_submenu_item?(s1_acts[@sub_index_1])
3088
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1])
3089
- if s2_acts && !s2_acts.empty?
3090
- s2_names = s2_acts.map { |a| extract_name_from_action(a) }
3091
- s2_max = s2_names.empty? ? 10 : s2_names.map { |n| GRmenu.display_width(n) }.max
3092
- s2_title = extract_name_from_action(s1_acts[@sub_index_1]) || ""
3093
- sub_2_preview_w = [s2_max + 8, GRmenu.display_width(s2_title) + 6, 20].max
3094
- sub_2_preview_w = [sub_2_preview_w, 36].min
3095
- end
3096
- end
3097
- end
3098
- end
3099
-
3100
- total_chain_w = total_width
3101
- total_chain_w += 2 + sub_1_preview_w if sub_1_preview_w > 0
3102
- total_chain_w += 2 + sub_2_preview_w if sub_2_preview_w > 0
3103
-
3104
- margin_left = if @center && term_cols > total_chain_w
3105
- " " * ((term_cols - total_chain_w) / 2)
3106
- elsif @center && term_cols > total_width
3107
- " " * ((term_cols - total_width) / 2)
3108
- else
3109
- ""
3110
- end
3111
-
3112
- subtitle_lines_count = 0
3113
- if @subtitle && !@subtitle.empty?
3114
- subtitle_lines = @subtitle.lines.map(&:chomp)
3115
- ref_sub_w = header_box_width > 0 ? header_box_width : total_chain_w
3116
- div_w = @divider.is_a?(Numeric) ? @divider.to_i : [ref_sub_w, term_cols - 2].min
3117
- sub_pad = (@center && term_cols > div_w) ? " " * ((term_cols - div_w) / 2) : ""
3118
-
3119
- if @divider
3120
- rendered_lines << "#{sub_pad}#{colorize("─" * div_w, @style_config.divider, 0.0)}"
3121
- subtitle_lines_count += 1
3122
- end
3123
-
3124
- subtitle_lines.each_with_index do |sub_line, s_i|
3125
- pad_sub = [div_w - GRmenu.display_width(sub_line), 0].max
3126
- formatted_sub = @center ? (" " * (pad_sub / 2) + sub_line + " " * (pad_sub - (pad_sub / 2))) : sub_line
3127
- rendered_lines << "#{sub_pad}#{colorize(formatted_sub, @style_config.subtitle, s_i * 0.3)}"
3128
- subtitle_lines_count += 1
3129
- end
3130
-
3131
- if @divider
3132
- rendered_lines << "#{sub_pad}#{colorize("─" * div_w, @style_config.divider, 0.6)}"
3133
- subtitle_lines_count += 1
3134
- end
3135
- rendered_lines << ""
3136
- subtitle_lines_count += 1
3137
- end
3138
-
3139
- border_color_cfg = @style_config.border
3140
- options_color_cfg = @style_config.options
3141
- focus_color_cfg = @style_config.focus
3142
- title_color_cfg = @style_config.title
3143
-
3144
- box_border = BORDERS[@style]
3145
-
3146
- overhead = header_lines_count + subtitle_lines_count + 6
3147
- overhead += 2 unless active_desc.empty?
3148
- overhead += 2 if @search
3149
- available_rows = [term_rows - overhead - 2, 2].max
3150
-
3151
- rows_data = matching_indices.each_slice(cols).to_a
3152
- total_rows = rows_data.length
3153
-
3154
- effective_page_rows = if @page_size && @page_size > 0
3155
- [@page_size, total_rows, available_rows].min
3156
- else
3157
- [total_rows, available_rows].min
3158
- end
3159
- effective_page_rows = [effective_page_rows, 1].max
3160
-
3161
- curr_matching_pos = matching_indices.index(@index) || 0
3162
- curr_row = total_rows > 0 ? (curr_matching_pos / cols) : 0
3163
-
3164
- start_row = 0
3165
- end_row = [total_rows - 1, 0].max
3166
- if total_rows > effective_page_rows
3167
- half_r = effective_page_rows / 2
3168
- start_row = [[curr_row - half_r, 0].max, total_rows - effective_page_rows].min
3169
- end_row = start_row + effective_page_rows - 1
3170
- end
3171
-
3172
- visible_rows_data = rows_data[start_row..end_row] || []
3173
- has_more_above = start_row > 0
3174
- has_more_below = end_row < (total_rows - 1)
3175
-
3176
- avail_w = [total_width - 4, 1].max
3177
- col_w = [(avail_w - (cols - 1) * 2) / cols, 1].max
3178
-
3179
- if @tabs && !@tabs.empty?
3180
- tab_strs = []
3181
- raw_tab_strs = []
3182
- @tab_ranges = {}
3183
- @tabs.each_with_index do |t_name, t_i|
3184
- is_active = (t_i == @active_tab_idx)
3185
- raw_t = is_active ? "[ #{t_name} ]" : " #{t_name} "
3186
- colored_t = if is_active
3187
- colorize(raw_t, { color: @active_tab_color, level: 2 }, 0.0)
3188
- else
3189
- colorize(raw_t, { color: @tab_color, level: 1 }, 0.0)
3190
- end
3191
- tab_strs << colored_t
3192
- raw_tab_strs << raw_t
3193
- end
3194
- raw_tabs_joined = raw_tab_strs.join(" ")
3195
- tabs_w = GRmenu.display_width(raw_tabs_joined)
3196
- l_tabs_pad = [(total_width - tabs_w) / 2, 0].max
3197
- formatted_tabs = (" " * l_tabs_pad) + tab_strs.join(" ")
3198
-
3199
- start_col = margin_left.length + l_tabs_pad
3200
- cur_x = start_col
3201
- @tabs.each_with_index do |_t_name, t_i|
3202
- t_len = GRmenu.display_width(raw_tab_strs[t_i])
3203
- @tab_ranges[t_i] = (cur_x + 1)..(cur_x + t_len)
3204
- cur_x += t_len + 3
3205
- end
3206
-
3207
- @tabs_row = rendered_lines.length + 1
3208
- rendered_lines << "#{margin_left}#{formatted_tabs}"
3209
- rendered_lines << ""
3210
- end
3211
-
3212
- if box_border
3213
- h_top = box_border[:ht] || box_border[:h]
3214
- h_bot = box_border[:hb] || box_border[:h]
3215
- v_l_raw = box_border[:vl] || box_border[:v]
3216
- v_r_raw = box_border[:vr] || box_border[:v]
3217
-
3218
- top_fill = build_horizontal_line(h_top, total_width - 2)
3219
- bot_fill = build_horizontal_line(h_bot, total_width - 2)
3220
- mid_fill = build_horizontal_line(h_top, total_width - 2)
3221
-
3222
- v_left = colorize(v_l_raw, border_color_cfg, 0.2)
3223
- v_right = colorize(v_r_raw, border_color_cfg, 0.8)
3224
-
3225
- main_box_start = rendered_lines.length
3226
- top_border_line = box_border[:tl] + top_fill + box_border[:tr]
3227
- rendered_lines << "#{margin_left}#{colorize(top_border_line, border_color_cfg, 0.0)}"
3228
-
3229
- unless @title.empty?
3230
- pad_t = [total_width - 4 - GRmenu.display_width(@title), 0].max
3231
- title_padded = " " * (pad_t / 2) + @title + " " * (pad_t - (pad_t / 2))
3232
- centered_title = colorize(title_padded, title_color_cfg, 0.4)
3233
- rendered_lines << "#{margin_left}#{v_left} #{centered_title} #{v_right}"
3234
-
3235
- separator_line = v_l_raw + mid_fill + v_r_raw
3236
- rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg, 0.6)}"
3237
- end
3238
-
3239
- if @search
3240
- search_prompt = "Buscar: #{@query}█"
3241
- pad_s = [avail_w - GRmenu.display_width(search_prompt), 0].max
3242
- search_padded = search_prompt + (" " * pad_s)
3243
- rendered_lines << "#{margin_left}#{v_left} #{Color.bright_yellow(search_padded)} #{v_right}"
3244
- separator_line = v_l_raw + mid_fill + v_r_raw
3245
- rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg, 0.8)}"
3246
- end
3247
-
3248
- if has_more_above
3249
- @up_arrow_row = rendered_lines.length + 1
3250
- up_text = "▲ (+#{start_row} #{cols > 1 ? 'filas' : 'arriba'})"
3251
- pad_up = [avail_w - GRmenu.display_width(up_text), 0].max
3252
- up_indicator = colorize(" " * (pad_up / 2) + up_text + " " * (pad_up - (pad_up / 2)), { color: "gray", level: 2 })
3253
- rendered_lines << "#{margin_left}#{v_left} #{up_indicator} #{v_right}"
3254
- end
3255
-
3256
- parent_row_idx = nil
3257
- if rows_data.empty?
3258
- no_res_txt = "(Sin resultados)"
3259
- pad_no = [avail_w - GRmenu.display_width(no_res_txt), 0].max
3260
- no_res = colorize(" " * (pad_no / 2) + no_res_txt + " " * (pad_no - (pad_no / 2)), { color: "gray", level: 1 })
3261
- rendered_lines << "#{margin_left}#{v_left} #{no_res} #{v_right}"
3262
- else
3263
- visible_rows_data.each_with_index do |row_indices, r_idx|
3264
- cells = []
3265
- cols.times do |c_idx|
3266
- item_idx = row_indices[c_idx]
3267
- if item_idx
3268
- op_name = all_names[item_idx]
3269
- is_sub = is_submenu_item?(@functions[item_idx])
3270
- arrow = is_sub ? "▶" : ""
3271
- if @index == item_idx
3272
- parent_row_idx = rendered_lines.length
3273
- cell_raw = if is_sub
3274
- pad_sub = [col_w - 2 - GRmenu.display_width(op_name) - 2, 0].max
3275
- "> #{op_name}#{' ' * pad_sub}#{arrow}"
3276
- else
3277
- "> #{op_name}"
3278
- end
3279
- pad_c = [col_w - GRmenu.display_width(cell_raw), 0].max
3280
- cells << colorize(cell_raw + (" " * pad_c), focus_color_cfg, r_idx * 0.3)
3281
- else
3282
- cell_raw = if is_sub
3283
- pad_sub = [col_w - 2 - GRmenu.display_width(op_name) - 2, 0].max
3284
- " #{op_name}#{' ' * pad_sub}#{arrow}"
3285
- else
3286
- " #{op_name}"
3287
- end
3288
- pad_c = [col_w - GRmenu.display_width(cell_raw), 0].max
3289
- cells << colorize(cell_raw + (" " * pad_c), options_color_cfg, r_idx * 0.2)
3290
- end
3291
- else
3292
- cells << (" " * col_w)
3293
- end
3294
- end
3295
- row_str = cells.join(" ")
3296
- pad_r = [avail_w - (col_w * cols + (cols - 1) * 2), 0].max
3297
- row_padded = row_str + (" " * pad_r)
3298
- cur_line_num = rendered_lines.length + 1
3299
- @row_hit_map[cur_line_num] = { row_indices: row_indices, cols: cols, col_w: col_w, margin_left: margin_left.length }
3300
- rendered_lines << "#{margin_left}#{v_left} #{row_padded} #{v_right}"
3301
- end
3302
- end
3303
-
3304
- if has_more_below
3305
- @down_arrow_row = rendered_lines.length + 1
3306
- remaining_below = total_rows - 1 - end_row
3307
- down_text = "▼ (+#{remaining_below} #{cols > 1 ? 'filas' : 'abajo'})"
3308
- pad_down = [avail_w - GRmenu.display_width(down_text), 0].max
3309
- down_indicator = colorize(" " * (pad_down / 2) + down_text + " " * (pad_down - (pad_down / 2)), { color: "gray", level: 2 })
3310
- rendered_lines << "#{margin_left}#{v_left} #{down_indicator} #{v_right}"
3311
- end
3312
-
3313
- unless active_desc.empty?
3314
- separator_line = v_l_raw + mid_fill + v_r_raw
3315
- rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg, 1.0)}"
3316
- pfx = (@style_config&.desc_prefix || @desc_prefix || SetStyle.desc_prefix || "[i]").to_s
3317
- pfx = "#{pfx} " unless pfx.end_with?(" ")
3318
- raw_desc = "#{pfx}#{active_desc}"
3319
- pad_d = [avail_w - GRmenu.display_width(raw_desc), 0].max
3320
- desc_text = colorize(raw_desc + (" " * pad_d), { color: "cyan", level: 1 })
3321
- rendered_lines << "#{margin_left}#{v_left} #{desc_text} #{v_right}"
3322
- end
3323
-
3324
- bottom_border_line = box_border[:bl] + bot_fill + box_border[:br]
3325
- rendered_lines << "#{margin_left}#{colorize(bottom_border_line, border_color_cfg, 1.4)}"
3326
- else
3327
- symbol_char = STYLES[@style] || "#"
3328
- solid_border = colorize(symbol_char, border_color_cfg)
3329
- solid_line = symbol_char * total_width
3330
-
3331
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3332
-
3333
- unless @title.empty?
3334
- pad_t = [total_width - 4 - GRmenu.display_width(@title), 0].max
3335
- title_padded = " " * (pad_t / 2) + @title + " " * (pad_t - (pad_t / 2))
3336
- centered_title = colorize(title_padded, title_color_cfg)
3337
- rendered_lines << "#{margin_left}#{solid_border} #{centered_title} #{solid_border}"
3338
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3339
- end
3340
-
3341
- if @search
3342
- search_prompt = "Buscar: #{@query}█"
3343
- pad_s = [avail_w - GRmenu.display_width(search_prompt), 0].max
3344
- search_padded = search_prompt + (" " * pad_s)
3345
- rendered_lines << "#{margin_left}#{solid_border} #{Color.bright_yellow(search_padded)} #{solid_border}"
3346
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3347
- end
3348
-
3349
- if has_more_above
3350
- @up_arrow_row = rendered_lines.length + 1
3351
- up_text = "▲ (+#{start_row} #{cols > 1 ? 'filas' : 'arriba'})"
3352
- pad_up = [avail_w - GRmenu.display_width(up_text), 0].max
3353
- up_indicator = colorize(" " * (pad_up / 2) + up_text + " " * (pad_up - (pad_up / 2)), { color: "gray", level: 2 })
3354
- rendered_lines << "#{margin_left}#{solid_border} #{up_indicator} #{solid_border}"
3355
- end
3356
-
3357
- parent_row_idx = nil
3358
- if rows_data.empty?
3359
- no_res_txt = "(Sin resultados)"
3360
- pad_no = [avail_w - GRmenu.display_width(no_res_txt), 0].max
3361
- no_res = colorize(" " * (pad_no / 2) + no_res_txt + " " * (pad_no - (pad_no / 2)), { color: "gray", level: 1 })
3362
- rendered_lines << "#{margin_left}#{solid_border} #{no_res} #{solid_border}"
3363
- else
3364
- visible_rows_data.each_with_index do |row_indices, r_idx|
3365
- cells = []
3366
- cols.times do |c_idx|
3367
- item_idx = row_indices[c_idx]
3368
- if item_idx
3369
- op_name = all_names[item_idx]
3370
- is_sub = is_submenu_item?(@functions[item_idx])
3371
- arrow = is_sub ? "▶" : ""
3372
- if @index == item_idx
3373
- parent_row_idx = rendered_lines.length
3374
- cell_raw = if is_sub
3375
- pad_sub = [col_w - 2 - GRmenu.display_width(op_name) - 2, 0].max
3376
- "> #{op_name}#{' ' * pad_sub}#{arrow}"
3377
- else
3378
- "> #{op_name}"
3379
- end
3380
- pad_c = [col_w - GRmenu.display_width(cell_raw), 0].max
3381
- cells << colorize(cell_raw + (" " * pad_c), focus_color_cfg, r_idx * 0.3)
3382
- else
3383
- cell_raw = if is_sub
3384
- pad_sub = [col_w - 2 - GRmenu.display_width(op_name) - 2, 0].max
3385
- " #{op_name}#{' ' * pad_sub}#{arrow}"
3386
- else
3387
- " #{op_name}"
3388
- end
3389
- pad_c = [col_w - GRmenu.display_width(cell_raw), 0].max
3390
- cells << colorize(cell_raw + (" " * pad_c), options_color_cfg, r_idx * 0.2)
3391
- end
3392
- else
3393
- cells << (" " * col_w)
3394
- end
3395
- end
3396
- row_str = cells.join(" ")
3397
- pad_r = [avail_w - (col_w * cols + (cols - 1) * 2), 0].max
3398
- row_padded = row_str + (" " * pad_r)
3399
- cur_line_num = rendered_lines.length + 1
3400
- @row_hit_map[cur_line_num] = { row_indices: row_indices, cols: cols, col_w: col_w, margin_left: margin_left.length }
3401
- rendered_lines << "#{margin_left}#{solid_border} #{row_padded} #{solid_border}"
3402
- end
3403
- end
3404
-
3405
- if has_more_below
3406
- @down_arrow_row = rendered_lines.length + 1
3407
- remaining_below = total_rows - 1 - end_row
3408
- down_text = "▼ (+#{remaining_below} #{cols > 1 ? 'filas' : 'abajo'})"
3409
- pad_down = [avail_w - GRmenu.display_width(down_text), 0].max
3410
- down_indicator = colorize(" " * (pad_down / 2) + down_text + " " * (pad_down - (pad_down / 2)), { color: "gray", level: 2 })
3411
- rendered_lines << "#{margin_left}#{solid_border} #{down_indicator} #{solid_border}"
3412
- end
3413
-
3414
- unless active_desc.empty?
3415
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3416
- pfx = (@style_config&.desc_prefix || @desc_prefix || SetStyle.desc_prefix || "[i]").to_s
3417
- pfx = "#{pfx} " unless pfx.end_with?(" ")
3418
- raw_desc = "#{pfx}#{active_desc}"
3419
- pad_d = [avail_w - GRmenu.display_width(raw_desc), 0].max
3420
- desc_text = colorize(raw_desc + (" " * pad_d), { color: "cyan", level: 1 })
3421
- rendered_lines << "#{margin_left}#{solid_border} #{desc_text} #{solid_border}"
3422
- end
3423
-
3424
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3425
- end
3426
-
3427
- if @open_level >= 1 && is_submenu_item?(@functions[@index]) && parent_row_idx
3428
- sub_1_acts = get_submenu_actions(@functions[@index])
3429
- if sub_1_acts && !sub_1_acts.empty?
3430
- sub_1_title = all_names[@index] || "Submenu"
3431
- sub_1_lines, sub_1_w, p_row_1 = render_submenu_box(sub_1_acts, sub_1_title, is_active: (@level == 1), selected_idx: @sub_index_1)
3432
-
3433
- sub_2_acts = nil
3434
- sub_2_lines = nil
3435
- sub_2_w = 0
3436
- p_row_2 = nil
3437
- if @open_level >= 2 && is_submenu_item?(sub_1_acts[@sub_index_1])
3438
- sub_2_acts = get_submenu_actions(sub_1_acts[@sub_index_1])
3439
- if sub_2_acts && !sub_2_acts.empty?
3440
- sub_2_title = extract_name_from_action(sub_1_acts[@sub_index_1]) || "Submenu"
3441
- sub_2_lines, sub_2_w, p_row_2 = render_submenu_box(sub_2_acts, sub_2_title, is_active: (@level == 2), selected_idx: @sub_index_2)
3442
- end
3443
- end
3444
-
3445
- if sub_2_lines
3446
- total_3_w = margin_left.length + total_width + 2 + sub_1_w + 2 + sub_2_w
3447
- if total_3_w <= term_cols
3448
- sub_1_start = main_box_start
3449
- p1_abs_row = sub_1_start + (p_row_1 || 1)
3450
- sub_2_start = main_box_start
3451
- max_3_lines = [rendered_lines.length, sub_1_start + sub_1_lines.length, sub_2_start + sub_2_lines.length].max
3452
-
3453
- x1_start = margin_left.length + total_width + 2
3454
- x1_end = x1_start + sub_1_w
3455
- x2_start = x1_end + 2
3456
- x2_end = x2_start + sub_2_w
3457
- @sub_1_col_rng = (x1_start..x1_end)
3458
- @sub_2_col_rng = (x2_start..x2_end)
3459
-
3460
- sub_1_offset = sub_1_title.empty? ? 1 : 3
3461
- sub_2_offset = sub_2_title.empty? ? 1 : 3
3462
-
3463
- stitched_lines = []
3464
- max_3_lines.times do |i|
3465
- l0 = rendered_lines[i] || ("#{margin_left}#{' ' * total_width}")
3466
-
3467
- l1 = " " * sub_1_w
3468
- br1 = " "
3469
- if i >= sub_1_start && i < (sub_1_start + sub_1_lines.length)
3470
- s1_idx = i - sub_1_start
3471
- l1 = sub_1_lines[s1_idx]
3472
- br1 = (i == parent_row_idx) ? "──" : " "
3473
- s1_item = s1_idx - sub_1_offset
3474
- if s1_item >= 0 && s1_item < sub_1_acts.length
3475
- @sub_1_hit_map[i + 1] = s1_item
3476
- @sub_hit_map[i + 1] = s1_item
3477
- end
3478
- end
3479
-
3480
- l2 = ""
3481
- br2 = ""
3482
- if i >= sub_2_start && i < (sub_2_start + sub_2_lines.length)
3483
- s2_idx = i - sub_2_start
3484
- l2 = sub_2_lines[s2_idx]
3485
- br2 = (i == p1_abs_row) ? "──" : " "
3486
- s2_item = s2_idx - sub_2_offset
3487
- if s2_item >= 0 && s2_item < sub_2_acts.length
3488
- @sub_2_hit_map[i + 1] = s2_item
3489
- end
3490
- end
3491
-
3492
- stitched_lines << "#{l0}#{br1}#{l1}#{br2}#{l2}".rstrip
3493
- end
3494
- rendered_lines = stitched_lines
3495
- elsif (sub_1_w + 2 + sub_2_w) <= term_cols
3496
- p1_abs_row = (p_row_1 || 1)
3497
- sub_2_start = main_box_start
3498
- max_2_lines = [sub_1_lines.length, sub_2_start + sub_2_lines.length].max
3499
-
3500
- x1_start = margin_left.length
3501
- x1_end = x1_start + sub_1_w
3502
- x2_start = x1_end + 2
3503
- x2_end = x2_start + sub_2_w
3504
- @sub_1_col_rng = (x1_start..x1_end)
3505
- @sub_2_col_rng = (x2_start..x2_end)
3506
-
3507
- sub_1_offset = sub_1_title.empty? ? 1 : 3
3508
- sub_2_offset = sub_2_title.empty? ? 1 : 3
3509
-
3510
- stitched_lines = []
3511
- max_2_lines.times do |i|
3512
- l1 = sub_1_lines[i] || (" " * sub_1_w)
3513
- l2 = ""
3514
- br2 = ""
3515
- if i >= sub_2_start && i < (sub_2_start + sub_2_lines.length)
3516
- s2_idx = i - sub_2_start
3517
- l2 = sub_2_lines[s2_idx]
3518
- br2 = (i == p1_abs_row) ? "──" : " "
3519
- s2_item = s2_idx - sub_2_offset
3520
- @sub_2_hit_map[i + 1] = s2_item if s2_item >= 0 && s2_item < sub_2_acts.length
3521
- end
3522
- s1_item = i - sub_1_offset
3523
- if s1_item >= 0 && s1_item < sub_1_acts.length
3524
- @sub_1_hit_map[i + 1] = s1_item
3525
- @sub_hit_map[i + 1] = s1_item
3526
- end
3527
- stitched_lines << "#{margin_left}#{l1}#{br2}#{l2}".rstrip
3528
- end
3529
- rendered_lines = stitched_lines
3530
- else
3531
- active_box = (@level == 2) ? sub_2_lines : sub_1_lines
3532
- rendered_lines = active_box.map { |l| "#{margin_left}#{l}" }
3533
- end
3534
- else
3535
- total_2_w = margin_left.length + total_width + 2 + sub_1_w
3536
- if total_2_w <= term_cols
3537
- sub_start_line = main_box_start
3538
- max_total_lines = [rendered_lines.length, sub_start_line + sub_1_lines.length].max
3539
-
3540
- x1_start = margin_left.length + total_width + 2
3541
- x1_end = x1_start + sub_1_w
3542
- @sub_1_col_rng = (x1_start..x1_end)
3543
-
3544
- sub_item_offset = sub_1_title.empty? ? 1 : 3
3545
-
3546
- stitched_lines = []
3547
- max_total_lines.times do |i|
3548
- main_line = rendered_lines[i] || ("#{margin_left}#{' ' * total_width}")
3549
- if i >= sub_start_line && i < (sub_start_line + sub_1_lines.length)
3550
- sub_idx = i - sub_start_line
3551
- sub_l = sub_1_lines[sub_idx]
3552
- bridge = (i == parent_row_idx) ? "──" : " "
3553
- stitched_lines << "#{main_line}#{bridge}#{sub_l}"
3554
-
3555
- s_item_idx = sub_idx - sub_item_offset
3556
- if s_item_idx >= 0 && s_item_idx < sub_1_acts.length
3557
- @sub_1_hit_map[i + 1] = s_item_idx
3558
- @sub_hit_map[i + 1] = s_item_idx
3559
- end
3560
- else
3561
- stitched_lines << main_line
3562
- end
3563
- end
3564
- rendered_lines = stitched_lines
3565
- else
3566
- active_box = (@level == 1) ? sub_1_lines : rendered_lines
3567
- rendered_lines = active_box.map { |l| "#{margin_left}#{l}" }
3568
- end
3569
- end
3570
- end
3571
- end
3572
-
3573
- rendered_lines
3574
- end
3575
-
3576
- def has_rgb_animation?
3577
- configs = [
3578
- @style_config.border,
3579
- @style_config.options,
3580
- @style_config.focus,
3581
- @style_config.title,
3582
- @style_config.banner,
3583
- @style_config.subtitle,
3584
- @style_config.divider
3585
- ]
3586
- configs.any? do |c|
3587
- if c.is_a?(Hash)
3588
- val = (c[:color] || c["color"]).to_s.downcase
3589
- val == "rgb" || val == "rainbow" || val == "chroma"
3590
- else
3591
- false
3592
- end
3593
- end
3594
- end
3595
-
3596
- def has_active_animation?
3597
- return true if @animate && ["diagonal", "linear", "fade", "rgb", "rainbow", "chroma", "neon"].include?(@animate.to_s.downcase)
3598
- return true if has_rgb_animation?
3599
- return true if @active_tab_color && @active_tab_color.to_s.downcase.start_with?("neon")
3600
- configs = [
3601
- @style_config.border,
3602
- @style_config.options,
3603
- @style_config.focus,
3604
- @style_config.title,
3605
- @style_config.banner,
3606
- @style_config.subtitle,
3607
- @style_config.divider
3608
- ]
3609
- configs.any? do |c|
3610
- if c.is_a?(Hash)
3611
- val = (c[:color] || c["color"]).to_s.downcase.strip
3612
- val.start_with?("neon")
3613
- else
3614
- false
3615
- end
3616
- end
3617
- end
3618
-
3619
- def style(css_content)
3620
- parsed = self.class.parse_config_text(css_content)
3621
- m = ((parsed[:sections] && parsed[:sections]["menu"]) || {}).merge(parsed[:global] || {})
3622
- if m["style"]
3623
- @style = m["style"].to_i
3624
- @border_config = BORDERS[@style] || BORDERS[3]
3625
- end
3626
- @banner_style = m["banner_style"].to_i if m["banner_style"]
3627
- @animate = m["animate"].to_s if m["animate"]
3628
- @center = (m["center"].to_s != "false") if m.key?("center")
3629
- if m["border"] || m["border_color"]
3630
- c, l = self.class.extract_color_and_level(m["border"] || m["border_color"], 1)
3631
- @style_config.border(c, l)
3632
- end
3633
- if m["options"] || m["options_color"]
3634
- c, l = self.class.extract_color_and_level(m["options"] || m["options_color"], 1)
3635
- @style_config.options(c, l)
3636
- end
3637
- if m["focus"] || m["focus_color"]
3638
- c, l = self.class.extract_color_and_level(m["focus"] || m["focus_color"], 2)
3639
- @style_config.focus(c, l)
3640
- end
3641
- if m["title"] || m["title_color"]
3642
- c, l = self.class.extract_color_and_level(m["title"] || m["title_color"], 2)
3643
- @style_config.title(c, l)
3644
- end
3645
- if m["banner"] || m["banner_color"]
3646
- c, l = self.class.extract_color_and_level(m["banner"] || m["banner_color"], 2)
3647
- @style_config.banner(c, l)
3648
- end
3649
- if m["subtitle"] || m["subtitle_color"]
3650
- c, l = self.class.extract_color_and_level(m["subtitle"] || m["subtitle_color"], 1)
3651
- @style_config.subtitle(c, l)
3652
- end
3653
- if m["divider"] || m["divider_color"]
3654
- c, l = self.class.extract_color_and_level(m["divider"] || m["divider_color"], 1)
3655
- @style_config.divider(c, l)
3656
- end
3657
- if m["desc_prefix"] || m["description_prefix"] || m["prefix"]
3658
- @style_config.desc_prefix(m["desc_prefix"] || m["description_prefix"] || m["prefix"])
3659
- end
3660
- @style_config.font(m["font"].to_i) if m["font"]
3661
- @mouse = (m["mouse"].to_s == "true") if m.key?("mouse")
3662
- if parsed[:sections] && parsed[:sections]["tabs"]
3663
- t_sec = parsed[:sections]["tabs"]
3664
- @active_tab_color = t_sec["active_tab"] || t_sec["active_tab_color"] || @active_tab_color if (t_sec["active_tab"] || t_sec["active_tab_color"])
3665
- @tab_color = t_sec["tab_color"] || t_sec["inactive_tab"] || t_sec["color"] || @tab_color if (t_sec["tab_color"] || t_sec["inactive_tab"] || t_sec["color"])
3666
- end
3667
- self
3668
- end
3669
-
3670
- def export_config(path = nil)
3671
- if path.nil?
3672
- caller_loc = caller_locations.find { |c| !c.path.include?(__FILE__) }
3673
- base = caller_loc ? caller_loc.path.sub(/\.rb$/, '') : "theme"
3674
- path = "#{base}.gr"
3675
- end
3676
- b_cfg = @style_config&.border || SetStyle.border
3677
- t_cfg = @style_config&.title || SetStyle.title
3678
- f_cfg = @style_config&.focus || SetStyle.focus
3679
- o_cfg = @style_config&.options || SetStyle.options
3680
- bn_cfg = @style_config&.banner || SetStyle.banner
3681
- s_cfg = @style_config&.subtitle || SetStyle.subtitle
3682
- d_cfg = @style_config&.divider || SetStyle.divider
3683
- dp_val = @style_config&.desc_prefix || SetStyle.desc_prefix
3684
-
3685
- lines = ["GRmenu::config<-1->", ""]
3686
- lines << "@theme:: \"#{File.basename(path, '.gr').capitalize}\""
3687
- lines << "@author:: \"grcode\""
3688
- lines << "@version:: \"1.0\""
3689
- lines << ""
3690
- lines << "<<menu"
3691
- lines << " style:: #{@style || 3}"
3692
- lines << " banner_style:: #{@banner_style || 3}"
3693
- lines << " font:: #{@style_config&.font || SetStyle.font}"
3694
- lines << " animate:: #{@animate || 'rgb'}"
3695
- lines << " center:: #{@center.nil? ? true : @center}"
3696
- lines << " desc_prefix:: #{dp_val}"
3697
- lines << " mouse:: #{@mouse}" if @mouse
3698
- lines << " border:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3699
- lines << " title:: #{t_cfg[:color]}:#{t_cfg[:level]}"
3700
- lines << " focus:: #{f_cfg[:color]}:#{f_cfg[:level]}"
3701
- lines << " options:: #{o_cfg[:color]}:#{o_cfg[:level]}"
3702
- lines << " banner:: #{bn_cfg[:color]}:#{bn_cfg[:level]}"
3703
- lines << " subtitle:: #{s_cfg[:color]}:#{s_cfg[:level]}"
3704
- lines << " divider:: #{d_cfg[:color]}:#{d_cfg[:level]}"
3705
- lines << ">>"
3706
- lines << ""
3707
- lines << "<<submenu"
3708
- lines << " style:: #{@style || 3}"
3709
- lines << " border:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3710
- lines << " focus:: #{f_cfg[:color]}:#{f_cfg[:level]}"
3711
- lines << " options:: #{o_cfg[:color]}:#{o_cfg[:level]}"
3712
- lines << ">>"
3713
- lines << ""
3714
- lines << "<<tabs"
3715
- lines << " active_tab:: #{@active_tab_color || 'yellow'}:2"
3716
- lines << " tab_color:: #{@tab_color || 'gray'}:1"
3717
- lines << ">>"
3718
- lines << ""
3719
- lines << "<<input"
3720
- lines << " style:: 3"
3721
- lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3722
- lines << " title_color:: #{t_cfg[:color]}:#{t_cfg[:level]}"
3723
- lines << " label_color:: white:1"
3724
- lines << ">>"
3725
- lines << ""
3726
- lines << "<<table"
3727
- lines << " style:: #{@style || 3}"
3728
- lines << " header_color:: yellow:2"
3729
- lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3730
- lines << " selected_row:: #{f_cfg[:color]}:#{f_cfg[:level]}"
3731
- lines << " row_color:: white:1"
3732
- lines << " zebra_striping:: true"
3733
- lines << ">>"
3734
- lines << ""
3735
- lines << "<<card"
3736
- lines << " style:: 7"
3737
- lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3738
- lines << " title_color:: #{t_cfg[:color]}:#{t_cfg[:level]}"
3739
- lines << " content_color:: white:1"
3740
- lines << ">>"
3741
- lines << ""
3742
- lines << "<<slider"
3743
- lines << " style:: #{@style || 3}"
3744
- lines << " color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3745
- lines << " fill_char:: █"
3746
- lines << " empty_char:: ░"
3747
- lines << ">>"
3748
- lines << ""
3749
- lines << "<<checkbox"
3750
- lines << " style:: #{@style || 3}"
3751
- lines << " color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3752
- lines << " checked_mark:: [X]"
3753
- lines << " unchecked_mark:: [ ]"
3754
- lines << ">>"
3755
- lines << ""
3756
- File.write(path, lines.join("\n") + "\n")
3757
- path
3758
- end
3759
- alias_method :export_theme, :export_config
3760
-
3761
- def draw(size_max: 20, min_width: nil)
3762
- if ARGV.any? { |a| ["-theme", "--theme", "-ex", "--export-theme"].include?(a.to_s.downcase) }
3763
- out_idx = ARGV.index { |a| ["-o", "--out", "--output"].include?(a.to_s.downcase) }
3764
- target_file = out_idx ? ARGV[out_idx + 1] : "tema_exportado.gr"
3765
- export_config(target_file)
3766
- Kernel.puts Color.bright_green("[OK] Tema exportado exitosamente a: #{target_file}")
3767
- exit(0)
3768
- end
3769
-
3770
- target_width = min_width || size_max || 20
3771
- action_to_execute = nil
3772
-
3773
- self.class.enable_windows_vt
3774
- $stdout.sync = true
3775
-
3776
- is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
3777
-
3778
- begin
3779
- Kernel.print("#{HIDE_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
3780
- Kernel.print(ENABLE_MOUSE) if @mouse
3781
- $stdout.flush
3782
-
3783
- if @animate && !["false", "rgb", "", "nil"].include?(@animate.downcase)
3784
- intro_lines = render_lines(target_width)
3785
- self.class.animate_render(intro_lines, @animate)
3786
- end
3787
-
3788
- if is_tty
3789
- $stdin.raw do |raw_input_stream|
3790
- action_to_execute = run_interactive_loop(raw_input_stream, target_width)
3791
- end
3792
- else
3793
- action_to_execute = run_interactive_loop($stdin, target_width)
3794
- end
3795
- ensure
3796
- Kernel.print(DISABLE_MOUSE) if @mouse
3797
- Kernel.print(SHOW_CURSOR)
3798
- $stdout.flush
3799
- end
3800
-
3801
- if action_to_execute
3802
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
3803
- $stdout.flush
3804
- execute_action(action_to_execute)
3805
- else
3806
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
3807
- $stdout.flush
3808
- end
3809
- rescue Interrupt
3810
- Kernel.print(DISABLE_MOUSE) if @mouse
3811
- Kernel.print("#{SHOW_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
3812
- $stdout.flush
3813
- nil
3814
- end
3815
-
3816
- private
3817
-
3818
- def draw_frame(target_width)
3819
- lines = render_lines(target_width)
3820
- buffer = String.new(CURSOR_HOME)
3821
- lines.each_with_index do |line, idx|
3822
- buffer << line << CLEAR_TO_EOL
3823
- buffer << "\r\n" if idx < lines.length - 1
3824
- end
3825
- buffer << CLEAR_TO_EOS
3826
- Kernel.print(buffer)
3827
- $stdout.flush
3828
- end
3829
-
3830
- def run_interactive_loop(input_stream, target_width)
3831
- matching = current_matching_indices
3832
- @index = matching.first || 0 unless matching.include?(@index)
3833
- @rgb_tick = 0.0
3834
- draw_frame(target_width)
3835
-
3836
- animating = has_active_animation?
3837
-
3838
- while true
3839
- if animating
3840
- ready = false
3841
- if input_stream.respond_to?(:to_io) || input_stream.is_a?(IO)
3842
- begin
3843
- select_res = IO.select([input_stream], nil, nil, 0.035)
3844
- ready = true if select_res && select_res[0] && !select_res[0].empty?
3845
- rescue StandardError
3846
- ready = true
3847
- end
3848
- else
3849
- ready = true
3850
- end
3851
-
3852
- unless ready
3853
- @rgb_tick += 0.08
3854
- draw_frame(target_width)
3855
- next
3856
- end
3857
- end
3858
-
3859
- key = read_single_key(input_stream)
3860
- break if key.nil? || key == "\x03" || key == "\x04"
3861
-
3862
- if key =~ /\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/
3863
- btn = $1.to_i
3864
- col = $2.to_i
3865
- row = $3.to_i
3866
- act = $4
3867
-
3868
- if btn == 64
3869
- if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
3870
- s1_acts = get_submenu_actions(@functions[@index])
3871
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
3872
- @sub_index_2 = (@sub_index_2 - 1) % s2_acts.length if s2_acts && !s2_acts.empty?
3873
- @level = 2
3874
- elsif @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
3875
- s1_acts = get_submenu_actions(@functions[@index])
3876
- @sub_index_1 = (@sub_index_1 - 1) % s1_acts.length if s1_acts && !s1_acts.empty?
3877
- @sub_index_2 = 0
3878
- @level = 1
3879
- else
3880
- move_up
3881
- @sub_index_1 = 0
3882
- @sub_index_2 = 0
3883
- @level = 0
3884
- end
3885
- draw_frame(target_width)
3886
- next
3887
- elsif btn == 65
3888
- if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
3889
- s1_acts = get_submenu_actions(@functions[@index])
3890
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
3891
- @sub_index_2 = (@sub_index_2 + 1) % s2_acts.length if s2_acts && !s2_acts.empty?
3892
- @level = 2
3893
- elsif @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
3894
- s1_acts = get_submenu_actions(@functions[@index])
3895
- @sub_index_1 = (@sub_index_1 + 1) % s1_acts.length if s1_acts && !s1_acts.empty?
3896
- @sub_index_2 = 0
3897
- @level = 1
3898
- else
3899
- move_down
3900
- @sub_index_1 = 0
3901
- @sub_index_2 = 0
3902
- @level = 0
3903
- end
3904
- draw_frame(target_width)
3905
- next
3906
- elsif btn == 0 && act == "M"
3907
- if @tabs && !@tabs.empty? && @tabs_row && row == @tabs_row
3908
- clicked_tab = @tab_ranges.find { |_idx, rng| rng.cover?(col) }
3909
- if clicked_tab
3910
- @active_tab_idx = clicked_tab[0]
3911
- @functions = @tab_contents[@tabs[@active_tab_idx]] || []
3912
- @index = 0
3913
- @level = 0
3914
- @open_level = 0
3915
- @sub_index_1 = 0
3916
- @sub_index_2 = 0
3917
- @active_panel = :main
3918
- @submenu_open = false
3919
- draw_frame(target_width)
3920
- next
3921
- end
3922
- end
3923
-
3924
- if @up_arrow_row && row == @up_arrow_row
3925
- move_up
3926
- draw_frame(target_width)
3927
- next
3928
- end
3929
-
3930
- if @down_arrow_row && row == @down_arrow_row
3931
- move_down
3932
- draw_frame(target_width)
3933
- next
3934
- end
3935
-
3936
- if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && @sub_2_hit_map[row]
3937
- s2_clicked = @sub_2_hit_map[row]
3938
- s1_acts = get_submenu_actions(@functions[@index])
3939
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
3940
- if s2_acts && s2_clicked < s2_acts.length
3941
- @level = 2
3942
- @sub_index_2 = s2_clicked
3943
- return s2_acts[s2_clicked]
3944
- end
3945
- end
3946
-
3947
- if @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && @sub_1_hit_map[row]
3948
- s1_clicked = @sub_1_hit_map[row]
3949
- s1_acts = get_submenu_actions(@functions[@index])
3950
- if s1_acts && s1_clicked < s1_acts.length
3951
- t_act = s1_acts[s1_clicked]
3952
- @level = 1
3953
- @sub_index_1 = s1_clicked
3954
- if is_submenu_item?(t_act)
3955
- @open_level = 2
3956
- @level = 2
3957
- @sub_index_2 = 0
3958
- draw_frame(target_width)
3959
- next
3960
- else
3961
- return t_act
3962
- end
3963
- end
3964
- end
3965
-
3966
- if @row_hit_map && @row_hit_map[row]
3967
- hit = @row_hit_map[row]
3968
- x_in_box = col - hit[:margin_left]
3969
- if x_in_box > 0 && (hit[:total_w].nil? || x_in_box <= hit[:total_w])
3970
- c_idx = [[((x_in_box - 2) / ([hit[:col_w], 1].max + 2)).to_i, 0].max, hit[:cols] - 1].min
3971
- clicked_item = hit[:row_indices][c_idx]
3972
- if clicked_item
3973
- @index = clicked_item
3974
- if is_submenu_item?(@functions[clicked_item])
3975
- @open_level = 1
3976
- @level = 1
3977
- @sub_index_1 = 0
3978
- @sub_index_2 = 0
3979
- @active_panel = :sub
3980
- @submenu_open = true
3981
- draw_frame(target_width)
3982
- next
3983
- else
3984
- return @functions[clicked_item]
3985
- end
3986
- end
3987
- end
3988
- end
3989
- end
3990
- next
3991
- end
3992
-
3993
- if @tabs && !@tabs.empty?
3994
- if key == "\t"
3995
- @active_tab_idx = (@active_tab_idx + 1) % @tabs.length
3996
- @functions = @tab_contents[@tabs[@active_tab_idx]] || []
3997
- @index = 0
3998
- @level = 0
3999
- @open_level = 0
4000
- @sub_index_1 = 0
4001
- @sub_index_2 = 0
4002
- @active_panel = :main
4003
- @submenu_open = false
4004
- draw_frame(target_width)
4005
- next
4006
- elsif key == "\e[Z"
4007
- @active_tab_idx = (@active_tab_idx - 1) % @tabs.length
4008
- @functions = @tab_contents[@tabs[@active_tab_idx]] || []
4009
- @index = 0
4010
- @level = 0
4011
- @open_level = 0
4012
- @sub_index_1 = 0
4013
- @sub_index_2 = 0
4014
- @active_panel = :main
4015
- @submenu_open = false
4016
- draw_frame(target_width)
4017
- next
4018
- end
4019
- end
4020
-
4021
- if @level == 2 && @open_level >= 2 && is_submenu_item?(@functions[@index])
4022
- s1_acts = get_submenu_actions(@functions[@index])
4023
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
4024
- if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
4025
- @sub_index_2 = (@sub_index_2 - 1) % s2_acts.length if s2_acts && !s2_acts.empty?
4026
- draw_frame(target_width)
4027
- next
4028
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
4029
- @sub_index_2 = (@sub_index_2 + 1) % s2_acts.length if s2_acts && !s2_acts.empty?
4030
- draw_frame(target_width)
4031
- next
4032
- elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K" || key == "\e"
4033
- @open_level = 1
4034
- @level = 1
4035
- draw_frame(target_width)
4036
- next
4037
- elsif key == "\r" || key == "\n"
4038
- return s2_acts[@sub_index_2] if s2_acts && @sub_index_2 < s2_acts.length
4039
- end
4040
- end
4041
-
4042
- if @level == 1 && @open_level >= 1 && is_submenu_item?(@functions[@index])
4043
- s1_acts = get_submenu_actions(@functions[@index])
4044
- if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
4045
- @sub_index_1 = (@sub_index_1 - 1) % s1_acts.length if s1_acts && !s1_acts.empty?
4046
- @sub_index_2 = 0
4047
- draw_frame(target_width)
4048
- next
4049
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
4050
- @sub_index_1 = (@sub_index_1 + 1) % s1_acts.length if s1_acts && !s1_acts.empty?
4051
- @sub_index_2 = 0
4052
- draw_frame(target_width)
4053
- next
4054
- elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K" || key == "\e"
4055
- @open_level = 0
4056
- @level = 0
4057
- @active_panel = :main
4058
- @submenu_open = false
4059
- draw_frame(target_width)
4060
- next
4061
- elsif key == "\e[C" || key == "\eOC" || key == "\xe0M" || key == "\x00M"
4062
- cur_l1 = s1_acts[@sub_index_1] if s1_acts
4063
- if is_submenu_item?(cur_l1)
4064
- @open_level = 2
4065
- @level = 2
4066
- @sub_index_2 = 0
4067
- draw_frame(target_width)
4068
- next
4069
- end
4070
- elsif key == "\r" || key == "\n"
4071
- cur_l1 = s1_acts[@sub_index_1] if s1_acts
4072
- if is_submenu_item?(cur_l1)
4073
- @open_level = 2
4074
- @level = 2
4075
- @sub_index_2 = 0
4076
- draw_frame(target_width)
4077
- next
4078
- else
4079
- return cur_l1
4080
- end
4081
- end
4082
- end
4083
-
4084
- if !@search && (key == "q" || key == "Q")
4085
- break
4086
- end
4087
-
4088
- if key == "\e"
4089
- if @search && !@query.empty?
4090
- @query.clear
4091
- matching = current_matching_indices
4092
- @index = matching.first || 0
4093
- draw_frame(target_width)
4094
- else
4095
- break
4096
- end
4097
- elsif key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
4098
- move_up
4099
- @sub_index_1 = 0
4100
- @sub_index_2 = 0
4101
- draw_frame(target_width)
4102
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
4103
- move_down
4104
- @sub_index_1 = 0
4105
- @sub_index_2 = 0
4106
- draw_frame(target_width)
4107
- elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K"
4108
- if @open_level > 0
4109
- @open_level = 0
4110
- @level = 0
4111
- @active_panel = :main
4112
- @submenu_open = false
4113
- draw_frame(target_width)
4114
- else
4115
- move_left
4116
- draw_frame(target_width)
4117
- end
4118
- elsif key == "\e[C" || key == "\eOC" || key == "\xe0M" || key == "\x00M"
4119
- if is_submenu_item?(@functions[@index])
4120
- @open_level = 1
4121
- @level = 1
4122
- @sub_index_1 = 0
4123
- @sub_index_2 = 0
4124
- @active_panel = :sub
4125
- @submenu_open = true
4126
- draw_frame(target_width)
4127
- else
4128
- move_right
4129
- draw_frame(target_width)
4130
- end
4131
- elsif key == "\x7f" || key == "\b" || key == "\x08"
4132
- if @search && !@query.empty?
4133
- @query.chop!
4134
- matching = current_matching_indices
4135
- @index = matching.first || 0
4136
- draw_frame(target_width)
4137
- end
4138
- elsif key == "\x15"
4139
- if @search
4140
- @query.clear
4141
- matching = current_matching_indices
4142
- @index = matching.first || 0
4143
- draw_frame(target_width)
4144
- end
4145
- elsif key == "\r" || key == "\n"
4146
- matching = current_matching_indices
4147
- if matching.include?(@index)
4148
- if is_submenu_item?(@functions[@index])
4149
- @open_level = 1
4150
- @level = 1
4151
- @sub_index_1 = 0
4152
- @sub_index_2 = 0
4153
- @active_panel = :sub
4154
- @submenu_open = true
4155
- draw_frame(target_width)
4156
- else
4157
- return @functions[@index]
4158
- end
4159
- end
4160
- elsif @search && key =~ /^[[:print:]]$/
4161
- @query << key
4162
- matching = current_matching_indices
4163
- @index = matching.first || 0
4164
- draw_frame(target_width)
4165
- end
4166
- end
4167
-
4168
- nil
4169
- end
4170
-
4171
- def read_single_key(input_stream)
4172
- GRmenu.read_key_raw(input_stream)
4173
- end
4174
-
4175
- def format_auto_name(raw_name)
4176
- cleaned = raw_name.to_s.gsub(/[_-]+/, ' ').strip
4177
- cleaned.split(' ').map(&:capitalize).join(' ')
4178
- end
4179
-
4180
- def extract_name_from_action(action)
4181
- case action
4182
- when Array
4183
- action[0].to_s
4184
- when Hash
4185
- (action[:name] || action[:title] || action["name"] || action["title"] || "Opcion").to_s
4186
- when Method
4187
- format_auto_name(action.name)
4188
- when Symbol
4189
- format_auto_name(action)
4190
- when Proc
4191
- if action.respond_to?(:name) && action.name
4192
- format_auto_name(action.name)
4193
- else
4194
- "Opcion"
4195
- end
4196
- else
4197
- if action.respond_to?(:name)
4198
- format_auto_name(action.name)
4199
- elsif action.respond_to?(:title)
4200
- action.title.to_s
4201
- else
4202
- format_auto_name(action)
4203
- end
4204
- end
4205
- end
4206
-
4207
- def extract_description_from_action(action)
4208
- if action.is_a?(Array) && action.length >= 3
4209
- action[2].to_s
4210
- elsif action.is_a?(Hash)
4211
- (action[:desc] || action[:description] || action["desc"] || action["description"]).to_s
4212
- else
4213
- ""
4214
- end
4215
- end
4216
-
4217
- def execute_action(action)
4218
- case action
4219
- when Method, Proc
4220
- action.call
4221
- when Symbol
4222
- if Object.respond_to?(action, true)
4223
- Object.send(action)
4224
- elsif Kernel.respond_to?(action, true)
4225
- Kernel.send(action)
4226
- end
4227
- when Array
4228
- callable = action[1]
4229
- if callable.is_a?(Symbol)
4230
- if Object.respond_to?(callable, true)
4231
- Object.send(callable)
4232
- elsif Kernel.respond_to?(callable, true)
4233
- Kernel.send(callable)
4234
- end
4235
- elsif callable.respond_to?(:call)
4236
- callable.call
4237
- end
4238
- when Hash
4239
- callable = action[:action] || action[:call] || action["action"] || action["call"]
4240
- if callable.is_a?(Symbol)
4241
- if Object.respond_to?(callable, true)
4242
- Object.send(callable)
4243
- elsif Kernel.respond_to?(callable, true)
4244
- Kernel.send(callable)
4245
- end
4246
- elsif callable.respond_to?(:call)
4247
- callable.call
4248
- end
4249
- else
4250
- action.call if action.respond_to?(:call)
110
+ COLORS.each_key do |k|
111
+ if k.start_with?("chromatic_")
112
+ suffix = k.sub(/\Achromatic_/, "")
113
+ BASE_RGB["cromatico_#{suffix}"] ||= BASE_RGB[k]
114
+ elsif k == "chromatic"
115
+ BASE_RGB["cromatico"] ||= BASE_RGB["chromatic"]
4251
116
  end
4252
117
  end
118
+ BASE_RGB.freeze
4253
119
  end
4254
120
 
4255
- Color = GRmenu::Color unless defined?(Color)
4256
- Colors = GRmenu::Color unless defined?(Colors)
4257
- C = GRmenu::Color unless defined?(C)
4258
- Grmenu = GRmenu unless defined?(Grmenu)
121
+ # Carga de submódulos y componentes del sistema
122
+ require_relative 'grmenu/version'
123
+ require_relative 'grmenu/terminal'
124
+ require_relative 'grmenu/window'
125
+ require_relative 'grmenu/color'
126
+ require_relative 'grmenu/set_style'
127
+ require_relative 'grmenu/theme'
128
+ require_relative 'grmenu/banner'
129
+ require_relative 'grmenu/image'
130
+ require_relative 'grmenu/animation'
131
+ require_relative 'grmenu/widgets/dialogs'
132
+ require_relative 'grmenu/widgets/progress'
133
+ require_relative 'grmenu/widgets/input'
134
+ require_relative 'grmenu/widgets/checkbox'
135
+ require_relative 'grmenu/widgets/slider'
136
+ require_relative 'grmenu/widgets/table'
137
+ require_relative 'grmenu/widgets/modal'
138
+ require_relative 'grmenu/widgets/form'
139
+ require_relative 'grmenu/renderer'
140
+ require_relative 'grmenu/interactive'
141
+
142
+ # Alias globales para ergonomía y total compatibilidad hacia atrás
143
+ Color = GRmenu::Color unless defined?(Color)
144
+ Colors = GRmenu::Color unless defined?(Colors)
145
+ C = GRmenu::Color unless defined?(C)
146
+ Grmenu = GRmenu unless defined?(Grmenu)
147
+ SetStyle = GRmenu::SetStyle unless defined?(SetStyle)