grmenu 4.1.0 → 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.1.0"
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,4313 +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
- @explicitly_set = {}
1535
- 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]
1536
109
 
1537
- def explicitly_set?(key)
1538
- @explicitly_set ||= {}
1539
- @explicitly_set[key.to_sym] == true || self.class.explicitly_set?(key)
1540
- end
1541
-
1542
- def mark_explicit(key)
1543
- @explicitly_set ||= {}
1544
- @explicitly_set[key.to_sym] = true
1545
- end
1546
-
1547
- def desc_prefix(prefix_str = nil, from_css: false)
1548
- return @desc_prefix if prefix_str.nil?
1549
- return self if from_css && explicitly_set?(:desc_prefix)
1550
- @explicitly_set ||= {}
1551
- @explicitly_set[:desc_prefix] = true unless from_css
1552
- @desc_prefix = prefix_str.to_s
1553
- self
1554
- end
1555
- alias_method :description_prefix, :desc_prefix
1556
- alias_method :desc_prefix=, :desc_prefix
1557
- alias_method :description_prefix=, :desc_prefix
1558
-
1559
- def border(color_name = nil, brightness_level = 1, from_css: false)
1560
- return @border if color_name.nil?
1561
- return self if from_css && explicitly_set?(:border)
1562
- @explicitly_set ||= {}
1563
- @explicitly_set[:border] = true unless from_css
1564
- @border = parse_color(color_name, brightness_level)
1565
- self
1566
- end
1567
- alias_method :Border, :border
1568
- alias_method :set_border, :border
1569
- alias_method :border=, :border
1570
-
1571
- def options(color_name = nil, brightness_level = 1, from_css: false)
1572
- return @options if color_name.nil?
1573
- return self if from_css && explicitly_set?(:options)
1574
- @explicitly_set ||= {}
1575
- @explicitly_set[:options] = true unless from_css
1576
- @options = parse_color(color_name, brightness_level)
1577
- self
1578
- end
1579
- alias_method :Options, :options
1580
- alias_method :set_options, :options
1581
- alias_method :options=, :options
1582
-
1583
- def focus(color_name = nil, brightness_level = 2, from_css: false)
1584
- return @focus if color_name.nil?
1585
- return self if from_css && explicitly_set?(:focus)
1586
- @explicitly_set ||= {}
1587
- @explicitly_set[:focus] = true unless from_css
1588
- @focus = parse_color(color_name, brightness_level)
1589
- self
1590
- end
1591
- alias_method :Focus, :focus
1592
- alias_method :set_focus, :focus
1593
- alias_method :focus=, :focus
1594
-
1595
- def title(color_name = nil, brightness_level = 2, from_css: false)
1596
- return @title if color_name.nil?
1597
- return self if from_css && explicitly_set?(:title)
1598
- @explicitly_set ||= {}
1599
- @explicitly_set[:title] = true unless from_css
1600
- @title = parse_color(color_name, brightness_level)
1601
- self
1602
- end
1603
- alias_method :Title, :title
1604
- alias_method :set_title, :title
1605
- alias_method :title=, :title
1606
-
1607
- def banner(color_name = nil, brightness_level = 2, from_css: false)
1608
- return @banner if color_name.nil?
1609
- return self if from_css && explicitly_set?(:banner)
1610
- @explicitly_set ||= {}
1611
- @explicitly_set[:banner] = true unless from_css
1612
- @banner = parse_color(color_name, brightness_level)
1613
- self
1614
- end
1615
- alias_method :Banner, :banner
1616
- alias_method :set_banner, :banner
1617
- alias_method :banner=, :banner
1618
-
1619
- def subtitle(color_name = nil, brightness_level = 2, from_css: false)
1620
- return @subtitle if color_name.nil?
1621
- return self if from_css && explicitly_set?(:subtitle)
1622
- @explicitly_set ||= {}
1623
- @explicitly_set[:subtitle] = true unless from_css
1624
- @subtitle = parse_color(color_name, brightness_level)
1625
- self
1626
- end
1627
- alias_method :Subtitle, :subtitle
1628
- alias_method :set_subtitle, :subtitle
1629
- alias_method :subtitle=, :subtitle
1630
-
1631
- def divider(color_name = nil, brightness_level = 1, from_css: false)
1632
- return @divider if color_name.nil?
1633
- return self if from_css && explicitly_set?(:divider)
1634
- @explicitly_set ||= {}
1635
- @explicitly_set[:divider] = true unless from_css
1636
- @divider = parse_color(color_name, brightness_level)
1637
- self
1638
- end
1639
- alias_method :Divider, :divider
1640
- alias_method :set_divider, :divider
1641
- alias_method :divider=, :divider
1642
-
1643
- def font(font_id = nil, from_css: false)
1644
- return @font if font_id.nil?
1645
- return self if from_css && explicitly_set?(:font)
1646
- @explicitly_set ||= {}
1647
- @explicitly_set[:font] = true unless from_css
1648
- @font = font_id.to_i
1649
- self
1650
- end
1651
- alias_method :Font, :font
1652
- alias_method :set_font, :font
1653
- alias_method :font=, :font
1654
-
1655
- private
1656
-
1657
- def parse_color(color_val, default_level = 1)
1658
- if color_val.is_a?(Hash)
1659
- { color: (color_val[:color] || color_val["color"]).to_s, level: (color_val[:level] || color_val["level"] || default_level).to_i }
1660
- else
1661
- { color: color_val.to_s, level: default_level.to_i }
1662
- end
1663
- end
1664
-
1665
- class << self
1666
- def explicitly_set?(key)
1667
- @explicitly_set ||= {}
1668
- @explicitly_set[key.to_sym] == true
1669
- end
1670
-
1671
- def mark_explicit(key)
1672
- @explicitly_set ||= {}
1673
- @explicitly_set[key.to_sym] = true
1674
- end
1675
-
1676
- def border(color_name = nil, brightness_level = 1, from_css: false)
1677
- @default_border ||= { color: "cyan", level: 1 }
1678
- return @default_border if color_name.nil?
1679
- return @default_border if from_css && explicitly_set?(:border)
1680
- @explicitly_set ||= {}
1681
- @explicitly_set[:border] = true unless from_css
1682
- @default_border = { color: color_name.to_s, level: brightness_level.to_i }
1683
- end
1684
- alias_method :Border, :border
1685
- alias_method :border=, :border
1686
-
1687
- def options(color_name = nil, brightness_level = 1, from_css: false)
1688
- @default_options ||= { color: "white", level: 1 }
1689
- return @default_options if color_name.nil?
1690
- return @default_options if from_css && explicitly_set?(:options)
1691
- @explicitly_set ||= {}
1692
- @explicitly_set[:options] = true unless from_css
1693
- @default_options = { color: color_name.to_s, level: brightness_level.to_i }
1694
- end
1695
- alias_method :Options, :options
1696
- alias_method :options=, :options
1697
-
1698
- def focus(color_name = nil, brightness_level = 2, from_css: false)
1699
- @default_focus ||= { color: "green", level: 2 }
1700
- return @default_focus if color_name.nil?
1701
- return @default_focus if from_css && explicitly_set?(:focus)
1702
- @explicitly_set ||= {}
1703
- @explicitly_set[:focus] = true unless from_css
1704
- @default_focus = { color: color_name.to_s, level: brightness_level.to_i }
1705
- end
1706
- alias_method :Focus, :focus
1707
- alias_method :focus=, :focus
1708
-
1709
- def title(color_name = nil, brightness_level = 2, from_css: false)
1710
- @default_title ||= { color: "yellow", level: 2 }
1711
- return @default_title if color_name.nil?
1712
- return @default_title if from_css && explicitly_set?(:title)
1713
- @explicitly_set ||= {}
1714
- @explicitly_set[:title] = true unless from_css
1715
- @default_title = { color: color_name.to_s, level: brightness_level.to_i }
1716
- end
1717
- alias_method :Title, :title
1718
- alias_method :title=, :title
1719
-
1720
- def banner(color_name = nil, brightness_level = 2, from_css: false)
1721
- @default_banner ||= { color: "magenta", level: 2 }
1722
- return @default_banner if color_name.nil?
1723
- return @default_banner if from_css && explicitly_set?(:banner)
1724
- @explicitly_set ||= {}
1725
- @explicitly_set[:banner] = true unless from_css
1726
- @default_banner = { color: color_name.to_s, level: brightness_level.to_i }
1727
- end
1728
- alias_method :Banner, :banner
1729
- alias_method :banner=, :banner
1730
-
1731
- def subtitle(color_name = nil, brightness_level = 2, from_css: false)
1732
- @default_subtitle ||= { color: "cyan", level: 2 }
1733
- return @default_subtitle if color_name.nil?
1734
- return @default_subtitle if from_css && explicitly_set?(:subtitle)
1735
- @explicitly_set ||= {}
1736
- @explicitly_set[:subtitle] = true unless from_css
1737
- @default_subtitle = { color: color_name.to_s, level: brightness_level.to_i }
1738
- end
1739
- alias_method :Subtitle, :subtitle
1740
- alias_method :subtitle=, :subtitle
1741
-
1742
- def divider(color_name = nil, brightness_level = 1, from_css: false)
1743
- @default_divider ||= { color: "blue", level: 1 }
1744
- return @default_divider if color_name.nil?
1745
- return @default_divider if from_css && explicitly_set?(:divider)
1746
- @explicitly_set ||= {}
1747
- @explicitly_set[:divider] = true unless from_css
1748
- @default_divider = { color: color_name.to_s, level: brightness_level.to_i }
1749
- end
1750
- alias_method :Divider, :divider
1751
- alias_method :divider=, :divider
1752
-
1753
- def font(font_id = nil, from_css: false)
1754
- @default_font ||= 1
1755
- return @default_font if font_id.nil?
1756
- return @default_font if from_css && explicitly_set?(:font)
1757
- @explicitly_set ||= {}
1758
- @explicitly_set[:font] = true unless from_css
1759
- @default_font = font_id.to_i
1760
- end
1761
- alias_method :Font, :font
1762
- alias_method :set_font, :font
1763
- alias_method :font=, :font
1764
-
1765
- def desc_prefix(prefix_str = nil, from_css: false)
1766
- @default_desc_prefix ||= "[i]"
1767
- return @default_desc_prefix if prefix_str.nil?
1768
- return @default_desc_prefix if from_css && explicitly_set?(:desc_prefix)
1769
- @explicitly_set ||= {}
1770
- @explicitly_set[:desc_prefix] = true unless from_css
1771
- @default_desc_prefix = prefix_str.to_s
1772
- end
1773
- alias_method :description_prefix, :desc_prefix
1774
- alias_method :desc_prefix=, :desc_prefix
1775
- alias_method :description_prefix=, :desc_prefix
1776
- end
1777
- end
1778
-
1779
- module GRprint
1780
- module_function
1781
-
1782
- def p(text = "", ending = "\r\n")
1783
- Kernel.print("#{text}#{ending}")
1784
- end
1785
- end
1786
-
1787
- attr_accessor :functions, :title, :subtitle, :banner, :banner_style, :divider, :index, :style_config, :center, :page_size, :search, :columns, :query, :image, :image_width
1788
-
1789
- alias_method :options, :functions
1790
- alias_method :options=, :functions=
1791
- alias_method :selected_index, :index
1792
- alias_method :selected_index=, :index=
1793
- alias_method :SetStyle, :style_config
1794
- alias_method :set_style, :style_config
1795
- alias_method :description, :subtitle
1796
- alias_method :description=, :subtitle=
1797
-
1798
- def desc_prefix
1799
- @desc_prefix || @style_config&.desc_prefix || SetStyle.desc_prefix
1800
- end
1801
-
1802
- def desc_prefix=(val)
1803
- @desc_prefix = val.to_s
1804
- @style_config&.desc_prefix(val)
1805
- end
1806
- alias_method :prefix, :desc_prefix
1807
- alias_method :prefix=, :desc_prefix=
1808
- alias_method :description_prefix, :desc_prefix
1809
- alias_method :description_prefix=, :desc_prefix=
1810
-
1811
- def self.STYLES; STYLES; end
1812
- def self.COLORS; COLORS; end
1813
- def self.BORDERS; BORDERS; end
1814
- def self.FONTS; FONTS; end
1815
- def self.FONT; FONT_1; end
1816
-
1817
- def self.terminal_width
1818
- cols = $stdout.winsize[1] rescue nil
1819
- cols = $stdin.winsize[1] rescue nil if cols.nil? || cols <= 0
1820
- (cols && cols > 0) ? cols : (ENV['COLUMNS'] ? ENV['COLUMNS'].to_i : 80)
1821
- rescue StandardError
1822
- 80
1823
- end
1824
-
1825
- def self.terminal_height
1826
- rows = $stdout.winsize[0] rescue nil
1827
- rows = $stdin.winsize[0] rescue nil if rows.nil? || rows <= 0
1828
- (rows && rows > 0) ? rows : (ENV['LINES'] ? ENV['LINES'].to_i : 24)
1829
- rescue StandardError
1830
- 24
1831
- end
1832
-
1833
- def self.clear_screen
1834
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
1835
- end
1836
- class << self
1837
- alias_method :clr, :clear_screen
1838
- end
1839
-
1840
- @@global_theme = {}
1841
-
1842
- def self.current_theme
1843
- @@global_theme
1844
- end
1845
-
1846
- def self.parse_config_text(text)
1847
- data = { global: {}, sections: {} }
1848
- current_sec = nil
1849
- current_sec_data = {}
1850
-
1851
- text.to_s.each_line do |line|
1852
- line = line.strip
1853
- next if line.empty? || line.start_with?("#")
1854
- next if line.start_with?("GRmenu::config")
1855
-
1856
- if line.start_with?("<<")
1857
- sec_name = line[2..-1].strip.downcase
1858
- current_sec = sec_name
1859
- current_sec_data = {}
1860
- elsif line == ">>"
1861
- if current_sec
1862
- data[:sections][current_sec] = current_sec_data
1863
- current_sec = nil
1864
- end
1865
- elsif line.include?("::")
1866
- key, _, val = line.partition("::")
1867
- key = key.strip.sub(/^@/, '').downcase
1868
- val = val.strip.sub(/^["']/, '').sub(/["']$/, '')
1869
- if current_sec
1870
- current_sec_data[key] = val
1871
- else
1872
- data[:global][key] = val
1873
- end
1874
- end
1875
- end
1876
- data
1877
- end
1878
-
1879
- def self.find_theme_file(path_or_name)
1880
- name = path_or_name.to_s
1881
- candidates = [
1882
- name,
1883
- "#{name}.gr",
1884
- find_data_file("themes/#{name}.gr"),
1885
- find_data_file("themes/#{name}"),
1886
- File.expand_path("data/themes/#{name}.gr", __dir__),
1887
- File.expand_path("data/themes/#{name}", __dir__),
1888
- File.expand_path("../data/themes/#{name}.gr", __dir__),
1889
- File.expand_path("../data/themes/#{name}", __dir__)
1890
- ].compact
1891
- candidates.find { |p| File.exist?(p) }
1892
- end
1893
-
1894
- def self.import_config(path_or_name)
1895
- path = find_theme_file(path_or_name)
1896
- raise "No se encontro el tema: #{path_or_name}" unless path && File.exist?(path)
1897
- text = File.read(path)
1898
- parsed = parse_config_text(text)
1899
- apply_parsed_theme(parsed)
1900
- @@global_theme = parsed
1901
- path
1902
- end
1903
-
1904
- def self.theme(name)
1905
- import_config(name)
1906
- end
1907
-
1908
- def self.extract_color_and_level(val, default_level = 1)
1909
- return ["white", default_level] if val.nil?
1910
- parts = val.to_s.split(":")
1911
- c_name = parts[0].to_s.strip
1912
- lvl = parts[1] ? parts[1].to_i : default_level
1913
- [c_name, lvl]
1914
- end
1915
-
1916
- def self.apply_parsed_theme(parsed)
1917
- sec = parsed[:sections] || {}
1918
- glob = parsed[:global] || {}
1919
- m = (sec["menu"] || {}).merge(glob)
1920
-
1921
- if m && !m.empty?
1922
- if (m["border"] || m["border_color"]) && !SetStyle.explicitly_set?(:border)
1923
- c, l = extract_color_and_level(m["border"] || m["border_color"], 1)
1924
- SetStyle.border(c, l, from_css: true)
1925
- end
1926
- if (m["title"] || m["title_color"]) && !SetStyle.explicitly_set?(:title)
1927
- c, l = extract_color_and_level(m["title"] || m["title_color"], 2)
1928
- SetStyle.title(c, l, from_css: true)
1929
- end
1930
- if (m["focus"] || m["focus_color"]) && !SetStyle.explicitly_set?(:focus)
1931
- c, l = extract_color_and_level(m["focus"] || m["focus_color"], 2)
1932
- SetStyle.focus(c, l, from_css: true)
1933
- end
1934
- if (m["options"] || m["options_color"]) && !SetStyle.explicitly_set?(:options)
1935
- c, l = extract_color_and_level(m["options"] || m["options_color"], 1)
1936
- SetStyle.options(c, l, from_css: true)
1937
- end
1938
- if (m["banner"] || m["banner_color"]) && !SetStyle.explicitly_set?(:banner)
1939
- c, l = extract_color_and_level(m["banner"] || m["banner_color"], 2)
1940
- SetStyle.banner(c, l, from_css: true)
1941
- end
1942
- if (m["subtitle"] || m["subtitle_color"]) && !SetStyle.explicitly_set?(:subtitle)
1943
- c, l = extract_color_and_level(m["subtitle"] || m["subtitle_color"], 1)
1944
- SetStyle.subtitle(c, l, from_css: true)
1945
- end
1946
- if (m["divider"] || m["divider_color"]) && !SetStyle.explicitly_set?(:divider)
1947
- c, l = extract_color_and_level(m["divider"] || m["divider_color"], 1)
1948
- SetStyle.divider(c, l, from_css: true)
1949
- end
1950
- if (m["desc_prefix"] || m["description_prefix"] || m["prefix"]) && !SetStyle.explicitly_set?(:desc_prefix)
1951
- SetStyle.desc_prefix(m["desc_prefix"] || m["description_prefix"] || m["prefix"], from_css: true)
1952
- end
1953
- if m["font"] && !SetStyle.explicitly_set?(:font)
1954
- SetStyle.font(m["font"].to_i, from_css: true)
1955
- end
1956
- end
1957
-
1958
- sec.each do |k, v|
1959
- next unless v.is_a?(Hash)
1960
- c_val = v["color"] || v["border"] || v["options"] || v["focus"] || v["title"] || v["banner"] || v["subtitle"] || v["divider"]
1961
- c, l = extract_color_and_level(c_val, (v["level"] || 1).to_i)
1962
- case k
1963
- when "border"
1964
- SetStyle.border(c, l, from_css: true) unless SetStyle.explicitly_set?(:border)
1965
- when "options"
1966
- SetStyle.options(c, l, from_css: true) unless SetStyle.explicitly_set?(:options)
1967
- when "focus"
1968
- SetStyle.focus(c, l, from_css: true) unless SetStyle.explicitly_set?(:focus)
1969
- when "title"
1970
- SetStyle.title(c, l, from_css: true) unless SetStyle.explicitly_set?(:title)
1971
- when "banner"
1972
- SetStyle.banner(c, l, from_css: true) unless SetStyle.explicitly_set?(:banner)
1973
- when "subtitle"
1974
- SetStyle.subtitle(c, l, from_css: true) unless SetStyle.explicitly_set?(:subtitle)
1975
- when "divider"
1976
- SetStyle.divider(c, l, from_css: true) unless SetStyle.explicitly_set?(:divider)
1977
- end
1978
- end
1979
- SetStyle.font(glob["font"].to_i, from_css: true) if glob["font"] && !SetStyle.explicitly_set?(:font)
1980
- end
1981
-
1982
- def self.style(css_content)
1983
- parsed = parse_config_text(css_content)
1984
- apply_parsed_theme(parsed)
1985
- parsed
1986
- end
1987
-
1988
- def self.export_config(path = nil)
1989
- if path.nil?
1990
- caller_loc = caller_locations.find { |c| !c.path.include?(__FILE__) }
1991
- base = caller_loc ? caller_loc.path.sub(/\.rb$/, '') : "theme"
1992
- path = "#{base}.gr"
1993
- end
1994
- lines = ["GRmenu::config<-1->", ""]
1995
- lines << "@theme:: \"#{File.basename(path, '.gr').capitalize}\""
1996
- lines << "@author:: \"grcode\""
1997
- lines << "@version:: \"1.0\""
1998
- lines << ""
1999
- lines << "<<menu"
2000
- lines << " style:: 3"
2001
- lines << " banner_style:: 3"
2002
- lines << " font:: #{SetStyle.font}"
2003
- lines << " animate:: rgb"
2004
- lines << " center:: true"
2005
- lines << " border:: #{SetStyle.border[:color]}:#{SetStyle.border[:level]}"
2006
- lines << " title:: #{SetStyle.title[:color]}:#{SetStyle.title[:level]}"
2007
- lines << " focus:: #{SetStyle.focus[:color]}:#{SetStyle.focus[:level]}"
2008
- lines << " options:: #{SetStyle.options[:color]}:#{SetStyle.options[:level]}"
2009
- lines << " banner:: #{SetStyle.banner[:color]}:#{SetStyle.banner[:level]}"
2010
- lines << " subtitle:: #{SetStyle.subtitle[:color]}:#{SetStyle.subtitle[:level]}"
2011
- lines << " divider:: #{SetStyle.divider[:color]}:#{SetStyle.divider[:level]}"
2012
- lines << ">>"
2013
- lines << ""
2014
- lines << "<<table"
2015
- lines << " style:: 3"
2016
- lines << " header_color:: yellow:2"
2017
- lines << " border_color:: rgb:2"
2018
- lines << " selected_row:: green:2"
2019
- lines << " row_color:: white:1"
2020
- lines << " zebra_striping:: true"
2021
- lines << ">>"
2022
- lines << ""
2023
- lines << "<<card"
2024
- lines << " style:: 7"
2025
- lines << " border_color:: cyan:2"
2026
- lines << " title_color:: yellow:2"
2027
- lines << " content_color:: white:1"
2028
- lines << ">>"
2029
- lines << ""
2030
- lines << "<<slider"
2031
- lines << " style:: 3"
2032
- lines << " color:: rgb:2"
2033
- lines << " fill_char:: █"
2034
- lines << " empty_char:: ░"
2035
- lines << ">>"
2036
- lines << ""
2037
- lines << "<<checkbox"
2038
- lines << " style:: 3"
2039
- lines << " color:: rgb:2"
2040
- lines << " checked_mark:: [X]"
2041
- lines << " unchecked_mark:: [ ]"
2042
- lines << ">>"
2043
- lines << ""
2044
- File.write(path, lines.join("\n") + "\n")
2045
- path
2046
- end
2047
-
2048
- def self.export_from_file(source_file, target_path = nil)
2049
- raise "No existe #{source_file}" unless File.exist?(source_file)
2050
- orig_draw = instance_method(:draw) rescue nil
2051
- extracted = nil
2052
- define_method(:draw) do |*|
2053
- extracted = {
2054
- style: @style,
2055
- banner_style: @banner_style,
2056
- font: @style_config&.font,
2057
- animate: @animate,
2058
- border: @style_config&.border,
2059
- title: @style_config&.title,
2060
- focus: @style_config&.focus,
2061
- options: @style_config&.options,
2062
- banner: @style_config&.banner,
2063
- subtitle: @style_config&.subtitle,
2064
- divider: @style_config&.divider
2065
- }
2066
- throw :grmenu_export_completed
2067
- end
2068
- begin
2069
- catch(:grmenu_export_completed) do
2070
- load(File.expand_path(source_file))
2071
- end
2072
- ensure
2073
- define_method(:draw, orig_draw) if orig_draw
2074
- end
2075
- out = target_path || source_file.sub(/\.rb$/, '') + ".gr"
2076
- if extracted && extracted[:border]
2077
- lines = ["GRmenu::config<-1->", ""]
2078
- lines << "@theme:: \"#{File.basename(out, '.gr').capitalize}\""
2079
- lines << "@author:: \"grcode\""
2080
- lines << "@version:: \"1.0\""
2081
- lines << ""
2082
- lines << "<<menu"
2083
- lines << " style:: #{extracted[:style] || 3}"
2084
- lines << " banner_style:: #{extracted[:banner_style] || 3}"
2085
- lines << " font:: #{extracted[:font] || 1}"
2086
- lines << " animate:: #{extracted[:animate] || 'rgb'}"
2087
- lines << " center:: true"
2088
- lines << " border:: #{extracted[:border][:color]}:#{extracted[:border][:level]}"
2089
- lines << " title:: #{extracted[:title][:color]}:#{extracted[:title][:level]}"
2090
- lines << " focus:: #{extracted[:focus][:color]}:#{extracted[:focus][:level]}"
2091
- lines << " options:: #{extracted[:options][:color]}:#{extracted[:options][:level]}"
2092
- lines << " banner:: #{extracted[:banner][:color]}:#{extracted[:banner][:level]}"
2093
- lines << " subtitle:: #{extracted[:subtitle][:color]}:#{extracted[:subtitle][:level]}"
2094
- lines << " divider:: #{extracted[:divider][:color]}:#{extracted[:divider][:level]}"
2095
- lines << ">>"
2096
- lines << ""
2097
- lines << "<<table"
2098
- lines << " style:: #{extracted[:style] || 3}"
2099
- lines << " header_color:: yellow:2"
2100
- lines << " border_color:: rgb:2"
2101
- lines << " selected_row:: green:2"
2102
- lines << " row_color:: white:1"
2103
- lines << " zebra_striping:: true"
2104
- lines << ">>"
2105
- lines << ""
2106
- lines << "<<card"
2107
- lines << " style:: 7"
2108
- lines << " border_color:: cyan:2"
2109
- lines << " title_color:: yellow:2"
2110
- lines << " content_color:: white:1"
2111
- lines << ">>"
2112
- lines << ""
2113
- lines << "<<slider"
2114
- lines << " style:: 3"
2115
- lines << " color:: rgb:2"
2116
- lines << " fill_char:: █"
2117
- lines << " empty_char:: ░"
2118
- lines << ">>"
2119
- lines << ""
2120
- lines << "<<checkbox"
2121
- lines << " style:: 3"
2122
- lines << " color:: rgb:2"
2123
- lines << " checked_mark:: [X]"
2124
- lines << " unchecked_mark:: [ ]"
2125
- lines << ">>"
2126
- lines << ""
2127
- File.write(out, lines.join("\n") + "\n")
2128
- out
2129
- else
2130
- export_config(out)
2131
- end
2132
- end
2133
-
2134
- def self.split_ansi_chars(str)
2135
- segments = []
2136
- current_style = String.new("")
2137
- in_escape = false
2138
- escape_buf = String.new("")
2139
-
2140
- str.to_s.each_char do |ch|
2141
- if ch == "\e"
2142
- in_escape = true
2143
- escape_buf << ch
2144
- next
2145
- end
2146
- if in_escape
2147
- escape_buf << ch
2148
- if ch =~ /[a-zA-Z]/
2149
- in_escape = false
2150
- current_style = escape_buf.dup
2151
- escape_buf.clear
2152
- end
2153
- next
2154
- end
2155
- segments << { char: ch, style: current_style.dup }
2156
- end
2157
- segments
2158
- end
2159
-
2160
- def self.animate_render(lines, type = :diagonal, delay = 0.012)
2161
- type_str = type.to_s.downcase
2162
- return if lines.nil? || lines.empty?
2163
- rst = ansi_reset
2164
-
2165
- case type_str
2166
- when "diagonal"
2167
- parsed_rows = lines.map { |l| split_ansi_chars(l) }
2168
- max_len = parsed_rows.map(&:length).max || 0
2169
- total_steps = max_len + (parsed_rows.length * 2)
2170
- step = 0
2171
- while step <= total_steps
2172
- buffer = String.new(CURSOR_HOME)
2173
- parsed_rows.each_with_index do |row_segs, y|
2174
- rendered_row = String.new("")
2175
- row_segs.each_with_index do |seg, x|
2176
- if (x + y * 2) <= step
2177
- rendered_row << seg[:style] << seg[:char] << rst
2178
- else
2179
- rendered_row << " "
2180
- end
2181
- end
2182
- buffer << rendered_row << CLEAR_TO_EOL << "\r\n"
2183
- end
2184
- buffer << CLEAR_TO_EOS
2185
- Kernel.print(buffer)
2186
- $stdout.flush
2187
- sleep(delay)
2188
- step += 4
2189
- end
2190
- when "linear"
2191
- buffer = String.new(CURSOR_HOME)
2192
- lines.each do |line|
2193
- Kernel.print("#{line}#{CLEAR_TO_EOL}\r\n")
2194
- $stdout.flush
2195
- sleep(delay * 3)
2196
- end
2197
- when "fade"
2198
- [1, 2].each do |lvl|
2199
- buffer = String.new(CURSOR_HOME)
2200
- lines.each do |line|
2201
- clean = line.gsub(/\e\[[0-9;]*m/, '')
2202
- buffer << ansi_color("white", lvl) << clean << rst << CLEAR_TO_EOL << "\r\n"
2203
- end
2204
- buffer << CLEAR_TO_EOS
2205
- Kernel.print(buffer)
2206
- $stdout.flush
2207
- sleep(delay * 8)
2208
- end
2209
- end
2210
- end
2211
-
2212
- def self.alert(type, message, title: nil, style: 3, color: nil, border_color: nil, title_color: nil, pause: true)
2213
- type_sym = type.to_sym rescue :info
2214
- tag, def_col, def_title = case type_sym
2215
- when :success, :ok
2216
- ["[✔ EXITO]", "green", "Operacion Exitosa"]
2217
- when :error, :fail, :danger
2218
- ["[✖ ERROR]", "red", "Error en el Sistema"]
2219
- when :warning, :warn
2220
- ["[⚠ AVISO]", "yellow", "Advertencia"]
2221
- else
2222
- ["[ℹ INFO]", "cyan", "Informacion"]
2223
- end
2224
- card_col = border_color || color || def_col
2225
- card_title = title || "#{tag} #{def_title}"
2226
- card(title: card_title, content: message, style: style, color: card_col, title_color: title_color, pause: pause)
2227
- end
2228
-
2229
- 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)
2230
- c_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "card")) || {}
2231
- actual_title = title || (content_arg ? title_or_content : nil)
2232
- actual_content = (content || (content_arg ? content_arg : title_or_content) || "").to_s
2233
- style_num = (style || c_sec["style"] || 7).to_i
2234
- border_cfg = BORDERS[style_num] || BORDERS[7]
2235
- card_color = (border_color || color || c_sec["border_color"] || c_sec["color"] || "cyan").to_s
2236
- actual_title_color = (title_color || c_sec["title_color"] || "yellow").to_s
2237
- actual_content_color = (content_color || c_sec["content_color"] || "white").to_s
2238
- is_rgb = card_color.downcase == "rgb" || card_color.downcase == "rainbow" || card_color.downcase == "chroma"
2239
-
2240
- lines = actual_content.split("\n")
2241
- content_max = lines.map { |l| display_width(l) }.max || 0
2242
- box_w = width || [content_max + 6, actual_title ? display_width(actual_title) + 6 : 0, 46].max
2243
- box_w = [box_w, terminal_width - 2].min
2244
- inner_w = box_w - 2
2245
-
2246
- wrapped_lines = []
2247
- lines.each do |raw_l|
2248
- if display_width(raw_l) <= (inner_w - 2)
2249
- wrapped_lines << raw_l
2250
- else
2251
- cur = String.new("")
2252
- raw_l.split(" ").each do |w|
2253
- if cur.empty?
2254
- cur << w
2255
- elsif display_width("#{cur} #{w}") <= (inner_w - 2)
2256
- cur << " " << w
2257
- else
2258
- wrapped_lines << cur
2259
- cur = String.new(w)
2260
- end
2261
- end
2262
- wrapped_lines << cur unless cur.empty?
2263
- end
2264
- end
2265
-
2266
- tl = border_cfg[:tl] || "#"
2267
- tr = border_cfg[:tr] || "#"
2268
- bl = border_cfg[:bl] || "#"
2269
- br = border_cfg[:br] || "#"
2270
- h_char = border_cfg[:h] || "─"
2271
- v_char = border_cfg[:v] || "│"
2272
-
2273
- brd_col = is_rgb ? Color.rgb("").sub(/\e\[0m$/, '') : ansi_color(card_color, 1)
2274
- rst = ansi_reset
2275
-
2276
- top_str = if actual_title && !actual_title.empty?
2277
- t_clean = " #{actual_title} "
2278
- t_len = display_width(t_clean)
2279
- if t_len > inner_w
2280
- t_clean = " #{actual_title[0...[inner_w - 6, 1].max]}... "
2281
- t_len = display_width(t_clean)
2282
- end
2283
- l_len = [(inner_w - t_len) / 2, 0].max
2284
- r_len = [inner_w - t_len - l_len, 0].max
2285
- h_char * l_len + ansi_color(actual_title_color, 2) + t_clean + brd_col + h_char * r_len
2286
- else
2287
- h_char * inner_w
2288
- end
2289
-
2290
- out = +""
2291
- out << "#{brd_col}#{tl}#{top_str}#{tr}#{rst}\r\n"
2292
- wrapped_lines.each do |line|
2293
- pad_line = " " + line
2294
- 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"
2295
- end
2296
- out << "#{brd_col}#{bl}#{h_char * inner_w}#{br}#{rst}\r\n"
2297
-
2298
- Kernel.print(out)
2299
- self.continue if pause
2300
- end
2301
-
2302
- 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)
2303
- t_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "table")) || {}
2304
- input_stream = $stdin
2305
- output_stream = $stdout
2306
- style_num = (style || t_sec["style"] || 3).to_i
2307
- border_cfg = BORDERS[style_num] || BORDERS[3]
2308
- tbl_color = (border_color || color || t_sec["border_color"] || t_sec["border"] || t_sec["color"] || "cyan").to_s
2309
- header_color = header_color || t_sec["header_color"] || "yellow"
2310
- focus_color = selected_row || t_sec["selected_row"] || t_sec["focus"] || "green"
2311
- page_size ||= (t_sec["page_size"] || 8).to_i
2312
-
2313
- resolved_headers = (headers || headers_arg || []).map(&:to_s)
2314
- resolved_raw_rows = (rows || rows_arg || []).map { |r| r.is_a?(Array) ? r.map(&:to_s) : r.values.map(&:to_s) }
2315
- headers = resolved_headers
2316
- raw_rows = resolved_raw_rows
2317
- filtered_rows = raw_rows.dup
2318
- selected_idx = 0
2319
- query = String.new("")
2320
- sort_col = nil
2321
- sort_asc = true
2322
- tick = 0.0
2323
-
2324
- calc_widths = lambda do
2325
- col_counts = [headers.length, raw_rows.map(&:length).max || 0].max
2326
- widths = Array.new(col_counts, 0)
2327
- headers.each_with_index { |h, i| widths[i] = [widths[i], display_width(h)].max }
2328
- filtered_rows.each do |row|
2329
- row.each_with_index { |cell, i| widths[i] = [widths[i], display_width(cell)].max }
2330
- end
2331
- widths.map { |w| w + 2 }
2332
- end
2333
-
2334
- draw_table = lambda do |t_tick|
2335
- is_rgb = tbl_color.downcase == "rgb" || tbl_color.downcase == "rainbow" || tbl_color.downcase == "chroma"
2336
- brd_color = is_rgb ? rgb_color(t_tick, 0.0) : ansi_color(tbl_color, 1)
2337
- hdr_color = is_rgb ? rgb_color(t_tick, 0.8) : ansi_color(header_color, 2)
2338
- foc_color = is_rgb ? rgb_color(t_tick, 1.4) : ansi_color(focus_color, 2)
2339
- rst = ansi_reset
2340
-
2341
- col_w = calc_widths.call
2342
- help_line = " ↑/↓: Moverse | Enter: Elegir | s: Ordenar | Esc: Salir"
2343
- tot_w = [col_w.sum + (col_w.length - 1) + 4, title ? display_width(title) + 8 : 0, display_width(help_line) + 4, 46].max
2344
- tot_w = [tot_w, terminal_width - 2].min
2345
- inner_w = tot_w - 2
2346
-
2347
- if inner_w < display_width(help_line)
2348
- help_line = " ↑/↓: Mover | Enter: Ok | Esc: Salir"
2349
- end
2350
-
2351
- tl = border_cfg[:tl] || "#"
2352
- tr = border_cfg[:tr] || "#"
2353
- bl = border_cfg[:bl] || "#"
2354
- br = border_cfg[:br] || "#"
2355
- h_char = border_cfg[:h] || "─"
2356
- v_char = border_cfg[:v] || "│"
2357
-
2358
- top_str = if title && !title.empty?
2359
- t_clean = " #{title} "
2360
- t_len = display_width(t_clean)
2361
- if t_len > inner_w
2362
- t_clean = " #{title[0...[(inner_w - 6), 1].max]}... "
2363
- t_len = display_width(t_clean)
2364
- end
2365
- left_len = [(inner_w - t_len) / 2, 0].max
2366
- right_len = [inner_w - t_len - left_len, 0].max
2367
- h_char * left_len + t_clean + h_char * right_len
2368
- else
2369
- h_char * inner_w
2370
- end
2371
-
2372
- out = String.new(CURSOR_HOME)
2373
- out << HIDE_CURSOR
2374
- out << "#{brd_color}#{tl}#{top_str}#{tr}#{rst}#{CLEAR_TO_EOL}\r\n"
2375
-
2376
- if search
2377
- s_line = " Buscar: #{query}█"
2378
- out << "#{brd_color}#{v_char}#{rst}#{pad_to_width(s_line, inner_w)}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2379
- out << "#{brd_color}#{v_char}#{h_char * inner_w}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2380
- end
2381
-
2382
- hdr_cells = headers.each_with_index.map do |h, i|
2383
- w = col_w[i] || 10
2384
- sort_indicator = sort_col == i ? (sort_asc ? " ▲" : " ▼") : ""
2385
- h_str = "#{h}#{sort_indicator}"
2386
- max_c = [w - 2, 2].max
2387
- h_str = h_str[0...[max_c - 2, 1].max] + ".." if display_width(h_str) > max_c
2388
- pad_to_width(" #{h_str}", w)
2389
- end
2390
- hdr_row_str = " " + hdr_cells.join("│")
2391
- hdr_row_str = hdr_row_str[0...inner_w] if display_width(hdr_row_str) > inner_w
2392
- 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"
2393
- out << "#{brd_color}#{v_char}#{h_char * inner_w}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2394
-
2395
- max_visible = page_size || 8
2396
- total_rows = filtered_rows.length
2397
- if total_rows == 0
2398
- empty_msg = " (Sin registros que coincidan con '#{query}')"
2399
- out << "#{brd_color}#{v_char}#{rst}#{pad_to_width(empty_msg, inner_w)}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2400
- else
2401
- start_idx = [(selected_idx - max_visible / 2), 0].max
2402
- start_idx = [start_idx, [total_rows - max_visible, 0].max].min
2403
- end_idx = [start_idx + max_visible - 1, total_rows - 1].min
2404
-
2405
- if start_idx > 0
2406
- up_str = " ▲ (+#{start_idx} arriba)"
2407
- 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"
2408
- end
2409
-
2410
- (start_idx..end_idx).each do |r_i|
2411
- row = filtered_rows[r_i]
2412
- is_active = (r_i == selected_idx)
2413
- prefix = is_active ? "> " : " "
2414
-
2415
- row_cells = row.each_with_index.map do |cell, c_i|
2416
- w = col_w[c_i] || 10
2417
- c_str = cell.to_s
2418
- max_c = [w - 2, 2].max
2419
- c_str = c_str[0...[max_c - 2, 1].max] + ".." if display_width(c_str) > max_c
2420
- pad_to_width(" #{c_str}", w)
2421
- end
2422
- row_str = prefix + row_cells.join("│")[1..-1].to_s
2423
- row_str = row_str[0...inner_w] if display_width(row_str) > inner_w
2424
-
2425
- if is_active
2426
- 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"
2427
- else
2428
- out << "#{brd_color}#{v_char}#{rst}#{pad_to_width(row_str, inner_w)}#{brd_color}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2429
- end
2430
- end
2431
-
2432
- remaining_down = total_rows - 1 - end_idx
2433
- if remaining_down > 0
2434
- down_str = " ▼ (+#{remaining_down} abajo)"
2435
- 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"
2436
- end
2437
- end
2438
-
2439
- out << "#{brd_color}#{v_char}#{h_char * inner_w}#{v_char}#{rst}#{CLEAR_TO_EOL}\r\n"
2440
- 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"
2441
- out << "#{brd_color}#{bl}#{h_char * inner_w}#{br}#{rst}#{CLEAR_TO_EOL}\r\n"
2442
- out << CLEAR_TO_EOS
2443
- output_stream.print(out)
2444
- output_stream.flush
2445
- end
2446
-
2447
- loop_res = nil
2448
- reader = lambda do |stream|
2449
- loop do
2450
- draw_table.call(tick)
2451
- is_anim = color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma"
2452
- if is_anim
2453
- ready = false
2454
- if stream.respond_to?(:to_io) || stream.is_a?(IO)
2455
- begin
2456
- res = IO.select([stream], nil, nil, 0.035)
2457
- ready = true if res && res[0] && !res[0].empty?
2458
- rescue StandardError
2459
- ready = true
2460
- end
2461
- else
2462
- ready = true
2463
- end
2464
- unless ready
2465
- tick += 0.08
2466
- next
2467
- end
2468
- end
2469
-
2470
- key = read_key_raw(stream)
2471
- break if key.nil? || key == "\x03" || key == "\x04"
2472
-
2473
- if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
2474
- if filtered_rows.length > 0
2475
- selected_idx = (selected_idx - 1) % filtered_rows.length
2476
- end
2477
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
2478
- if filtered_rows.length > 0
2479
- selected_idx = (selected_idx + 1) % filtered_rows.length
2480
- end
2481
- elsif key == "\e[5~" || key == "\e[D"
2482
- if filtered_rows.length > 0
2483
- selected_idx = [(selected_idx - (page_size || 8)), 0].max
2484
- end
2485
- elsif key == "\e[6~" || key == "\e[C"
2486
- if filtered_rows.length > 0
2487
- selected_idx = [(selected_idx + (page_size || 8)), filtered_rows.length - 1].min
2488
- end
2489
- elsif key == "\r" || key == "\n"
2490
- if filtered_rows.length > 0
2491
- loop_res = filtered_rows[selected_idx]
2492
- end
2493
- break
2494
- elsif key == "\e"
2495
- if search && !query.empty?
2496
- query.clear
2497
- filtered_rows = raw_rows.dup
2498
- selected_idx = 0
2499
- else
2500
- loop_res = nil
2501
- break
2502
- end
2503
- elsif key == "\x7f" || key == "\b" || key == "\x08"
2504
- if search && !query.empty?
2505
- query.chop!
2506
- if query.empty?
2507
- filtered_rows = raw_rows.dup
2508
- else
2509
- filtered_rows = raw_rows.select { |r| r.any? { |c| c.downcase.include?(query.downcase) } }
2510
- end
2511
- selected_idx = 0
2512
- end
2513
- elsif key == "\x15"
2514
- if search
2515
- query.clear
2516
- filtered_rows = raw_rows.dup
2517
- selected_idx = 0
2518
- end
2519
- elsif (!search || query.empty?) && (key == "s" || key == "S")
2520
- if sort
2521
- sort_col = ((sort_col || -1) + 1) % [headers.length, 1].max
2522
- filtered_rows.sort_by! { |r| r[sort_col] || "" }
2523
- selected_idx = 0
2524
- end
2525
- elsif (!search || query.empty?) && (key == "q" || key == "Q")
2526
- loop_res = nil
2527
- break
2528
- elsif search && key =~ /^[[:print:]]$/
2529
- query << key
2530
- filtered_rows = raw_rows.select { |r| r.any? { |c| c.downcase.include?(query.downcase) } }
2531
- selected_idx = 0
2532
- end
2533
- end
2534
- end
2535
-
2536
- begin
2537
- output_stream.print("#{HIDE_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
2538
- if input_stream.respond_to?(:raw) && input_stream.respond_to?(:tty?) && input_stream.tty?
2539
- input_stream.raw { |s| reader.call(s) }
2540
- else
2541
- reader.call(input_stream)
2542
- end
2543
- ensure
2544
- output_stream.print("#{SHOW_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
2545
- end
2546
- loop_res
2547
- end
2548
-
2549
- def self.div(long = nil, color = "blue", level = 1, char = "─")
2550
- width = long || [terminal_width - 2, 64].min
2551
- if color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma"
2552
- Kernel.print("#{Color.rgb(char * width)}\r\n")
2553
- else
2554
- color_code = ansi_color(color, level)
2555
- reset_code = ansi_reset
2556
- Kernel.print("#{color_code}#{char * width}#{reset_code}\r\n")
2557
- end
2558
- end
2559
-
2560
- def self.help(section = :all)
2561
- path = find_data_file("help.txt")
2562
- return unless path && File.exist?(path)
2563
- content = File.read(path)
2564
- COLORS.each do |c_name, lvls|
2565
- if lvls.is_a?(Hash)
2566
- content.gsub!("{#{c_name}}", ansi_color(c_name, 1))
2567
- content.gsub!("{bright_#{c_name}}", ansi_color(c_name, 2))
2568
- end
2569
- end
2570
- content.gsub!("{reset}", ansi_reset)
2571
- Kernel.print("\r\n#{content}\r\n")
2572
- end
2573
-
2574
- def help
2575
- self.class.help
2576
- end
2577
-
2578
- def self.continue(text = "Presiona cualquier tecla para continuar...")
2579
- Kernel.print("#{Color.gray(text)} ")
2580
- if $stdin.respond_to?(:raw) && $stdin.respond_to?(:tty?) && $stdin.tty?
2581
- $stdin.raw(&:getch)
2582
- elsif $stdin.respond_to?(:getch)
2583
- $stdin.getch
2584
- else
2585
- $stdin.read(1)
2586
- end
2587
- Kernel.print("\r\n")
2588
- end
2589
-
2590
- def self.build_ascii_lines(text, max_cols = terminal_width, font_id = 1)
2591
- target_font = FONTS[font_id.to_i] || FONTS[1]
2592
- clean_chars = text.to_s.upcase.chars.select { |c| target_font.key?(c) }
2593
- return [] if clean_chars.empty?
2594
-
2595
- font_height = target_font.values.first.length
2596
-
2597
- [2, 1, 0].each do |spacing|
2598
- lines = Array.new(font_height, "")
2599
- clean_chars.each_with_index do |c, idx|
2600
- fig = target_font[c]
2601
- pad = (idx == clean_chars.length - 1) ? "" : (" " * spacing)
2602
- font_height.times { |i| lines[i] += fig[i] + pad }
2603
- end
2604
-
2605
- max_len = lines.map { |l| display_width(l) }.max
2606
- return lines if (max_len + 6) <= max_cols
2607
- end
2608
-
2609
- nil
2610
- end
2611
-
2612
- def self.banner(text, delay = 0, color: "magenta", level: 2, style: 3, font: 1)
2613
- cols = terminal_width
2614
- is_rgb = (color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma")
2615
- color_code = is_rgb ? "" : ansi_color(color, level)
2616
- reset_code = ansi_reset
2617
-
2618
- ascii_rows = build_ascii_lines(text, cols, font)
2619
- border_cfg = BORDERS[style] || BORDERS[3]
2620
- h_top = border_cfg[:ht] || border_cfg[:h]
2621
- h_bot = border_cfg[:hb] || border_cfg[:h]
2622
- v_l = border_cfg[:vl] || border_cfg[:v]
2623
- v_r = border_cfg[:vr] || border_cfg[:v]
2624
-
2625
- if ascii_rows
2626
- max_len = ascii_rows.map { |r| display_width(r) }.max
2627
- top_fill = (h_top * ((max_len + 4).to_f / h_top.length).ceil)[0...(max_len + 4)]
2628
- bot_fill = (h_bot * ((max_len + 4).to_f / h_bot.length).ceil)[0...(max_len + 4)]
2629
-
2630
- if is_rgb
2631
- Kernel.print("#{Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")}\r\n")
2632
- ascii_rows.each_with_index do |line, idx|
2633
- pad = " " * (max_len - display_width(line))
2634
- row_content = " #{line}#{pad} "
2635
- Kernel.print("#{Color.rgb(v_l)}#{Color.rgb(row_content, idx * 0.2)}#{Color.rgb(v_r)}\r\n")
2636
- sleep(delay) if delay > 0
2637
- end
2638
- Kernel.print("#{Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")}\r\n")
2639
- else
2640
- Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
2641
- ascii_rows.each do |line|
2642
- pad = " " * (max_len - display_width(line))
2643
- Kernel.print("#{color_code}#{v_l} #{line}#{pad} #{v_r}#{reset_code}\r\n")
2644
- sleep(delay) if delay > 0
2645
- end
2646
- Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
2647
- end
2648
- else
2649
- clean_t = text.to_s.strip
2650
- box_w = [display_width(clean_t) + 6, cols - 2].min
2651
- top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
2652
- bot_fill = (h_bot * ((box_w - 2).to_f / h_bot.length).ceil)[0...(box_w - 2)]
2653
-
2654
- pad_t = [box_w - 4 - display_width(clean_t), 0].max
2655
- l_p = " " * (pad_t / 2)
2656
- r_p = " " * (pad_t - (pad_t / 2))
2657
-
2658
- if is_rgb
2659
- Kernel.print("#{Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")}\r\n")
2660
- Kernel.print("#{Color.rgb(v_l)} #{Color.rgb(l_p + clean_t + r_p)} #{Color.rgb(v_r)}\r\n")
2661
- Kernel.print("#{Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")}\r\n")
2662
- else
2663
- Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
2664
- Kernel.print("#{color_code}#{v_l} #{l_p}#{clean_t}#{r_p} #{v_r}#{reset_code}\r\n")
2665
- Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
2666
- end
2667
- end
2668
- end
2669
-
2670
- class << self
2671
- alias_method :message, :banner
2672
- alias_method :logo, :banner
2673
-
2674
- def tabs(tabs_hash, *args, **kwargs)
2675
- new([], *args, tabs: tabs_hash, **kwargs)
2676
- end
2677
- end
2678
-
2679
- 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, desc_prefix: nil, prefix: nil, border: nil, border_color: nil, options_color: nil, focus_color: nil, title_color: nil, banner_color: nil, subtitle_color: nil, divider_color: nil, **keyword_arguments)
2680
- tabs_data = tabs || keyword_arguments[:tabs] || (functions.is_a?(Hash) ? functions : nil)
2681
- if tabs_data.is_a?(Hash) && !tabs_data.empty?
2682
- @tabs = tabs_data.keys.map(&:to_s)
2683
- @tab_contents = tabs_data.transform_keys(&:to_s)
2684
- @active_tab_idx = 0
2685
- @functions = @tab_contents[@tabs[@active_tab_idx]] || []
2686
- else
2687
- @tabs = nil
2688
- @tab_contents = nil
2689
- @active_tab_idx = nil
2690
- @functions = functions.is_a?(Array) ? functions : Array(functions)
2691
- end
2692
-
2693
- pos_title = positional_arguments[0]
2694
- pos_style = positional_arguments[1]
2695
-
2696
- @title = (title || pos_title || keyword_arguments[:title] || "").to_s
2697
- @banner = (banner || keyword_arguments[:banner] || "").to_s
2698
- @subtitle = (subtitle || description || keyword_arguments[:subtitle] || keyword_arguments[:description] || "").to_s
2699
- @divider = divider.nil? ? (!@banner.empty? || !@subtitle.empty?) : divider
2700
-
2701
- theme_menu_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, 'menu')) || {}
2702
- th_style = theme_menu_sec['style']&.to_i
2703
- th_bstyle = theme_menu_sec['banner_style']&.to_i
2704
-
2705
- @style = (style || pos_style || keyword_arguments[:style] || th_style || 19).to_i
2706
- @banner_style = (banner_style || keyword_arguments[:banner_style] || th_bstyle || 3).to_i
2707
- @center = center.nil? ? (theme_menu_sec.key?('center') ? (theme_menu_sec['center'].to_s != 'false') : true) : center
2708
- @page_size = (page_size || keyword_arguments[:page_size])&.to_i
2709
- @search = search || keyword_arguments[:search] || false
2710
- @columns = [(columns || keyword_arguments[:columns] || 1).to_i, 1].max
2711
- @image = image || keyword_arguments[:image]
2712
- @image_width = (image_width || keyword_arguments[:image_width])&.to_i
2713
- @animate = (keyword_arguments[:animate] || (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, 'menu', 'animate')) || false).to_s
2714
- m_val = mouse.nil? ? keyword_arguments[:mouse] : mouse
2715
- if m_val.nil?
2716
- @mouse = theme_menu_sec.key?('mouse') ? (theme_menu_sec['mouse'].to_s != 'false') : true
2717
- else
2718
- @mouse = (m_val == true || m_val.to_s == 'true')
2719
- end
2720
-
2721
- t_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "tabs")) || {}
2722
- @active_tab_color = (active_tab_color || keyword_arguments[:active_tab_color] || t_sec["active_tab"] || t_sec["active_tab_color"] || "yellow").to_s
2723
- @tab_color = (tab_color || keyword_arguments[:tab_color] || t_sec["tab_color"] || t_sec["inactive_tab"] || t_sec["color"] || "gray").to_s
2724
-
2725
- @query = String.new("")
2726
- @index = 0
2727
- @rgb_tick = 0.0
2728
-
2729
- @level = 0
2730
- @open_level = 0
2731
- @sub_index_1 = 0
2732
- @sub_index_2 = 0
2733
- @sub_1_hit_map = {}
2734
- @sub_2_hit_map = {}
2735
- @sub_1_col_rng = nil
2736
- @sub_2_col_rng = nil
2737
-
2738
- @active_panel = :main
2739
- @sub_index = 0
2740
- @submenu_open = false
2741
-
2742
- @row_hit_map = {}
2743
- @tab_ranges = {}
2744
- @tabs_row = nil
2745
- @up_arrow_row = nil
2746
- @down_arrow_row = nil
2747
- @sub_hit_map = {}
2748
-
2749
- @cached_image_lines = nil
2750
- @cached_image_cols = nil
2751
-
2752
- init_font = font || keyword_arguments[:font] || keyword_arguments[:font_style] || SetStyle.font || 1
2753
- raw_pfx = desc_prefix || prefix || keyword_arguments[:desc_prefix] || keyword_arguments[:prefix] || keyword_arguments[:description_prefix]
2754
- @desc_prefix = raw_pfx.to_s unless raw_pfx.nil?
2755
- 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]"
2756
-
2757
- @style_config = SetStyle.new(
2758
- border: SetStyle.border.dup,
2759
- options: SetStyle.options.dup,
2760
- focus: SetStyle.focus.dup,
2761
- title: SetStyle.title.dup,
2762
- banner: SetStyle.banner.dup,
2763
- subtitle: SetStyle.subtitle.dup,
2764
- divider: SetStyle.divider.dup,
2765
- font: init_font,
2766
- desc_prefix: init_pfx
2767
- )
2768
- @style_config.mark_explicit(:font) if font || keyword_arguments[:font] || keyword_arguments[:font_style]
2769
- @style_config.mark_explicit(:desc_prefix) unless raw_pfx.nil?
2770
-
2771
- if border || border_color || keyword_arguments[:border] || keyword_arguments[:border_color]
2772
- b_c, b_l = self.class.extract_color_and_level(border || border_color || keyword_arguments[:border] || keyword_arguments[:border_color], 1)
2773
- @style_config.border(b_c, b_l)
2774
- end
2775
- if options_color || keyword_arguments[:options_color] || keyword_arguments[:options]
2776
- o_c, o_l = self.class.extract_color_and_level(options_color || keyword_arguments[:options_color] || keyword_arguments[:options], 1)
2777
- @style_config.options(o_c, o_l)
2778
- end
2779
- if focus_color || keyword_arguments[:focus_color] || keyword_arguments[:focus]
2780
- f_c, f_l = self.class.extract_color_and_level(focus_color || keyword_arguments[:focus_color] || keyword_arguments[:focus], 2)
2781
- @style_config.focus(f_c, f_l)
2782
- end
2783
- if title_color || keyword_arguments[:title_color]
2784
- t_c, t_l = self.class.extract_color_and_level(title_color || keyword_arguments[:title_color], 2)
2785
- @style_config.title(t_c, t_l)
2786
- end
2787
- if banner_color || keyword_arguments[:banner_color]
2788
- bn_c, bn_l = self.class.extract_color_and_level(banner_color || keyword_arguments[:banner_color], 2)
2789
- @style_config.banner(bn_c, bn_l)
2790
- end
2791
- if subtitle_color || keyword_arguments[:subtitle_color]
2792
- s_c, s_l = self.class.extract_color_and_level(subtitle_color || keyword_arguments[:subtitle_color], 1)
2793
- @style_config.subtitle(s_c, s_l)
2794
- end
2795
- if divider_color || keyword_arguments[:divider_color]
2796
- d_c, d_l = self.class.extract_color_and_level(divider_color || keyword_arguments[:divider_color], 1)
2797
- @style_config.divider(d_c, d_l)
2798
- end
2799
- end
2800
-
2801
- def current_matching_indices
2802
- if @search && !@query.empty?
2803
- indices = []
2804
- @functions.each_with_index do |func, idx|
2805
- name = extract_name_from_action(func)
2806
- indices << idx if name.downcase.include?(@query.downcase)
2807
- end
2808
- indices
2809
- else
2810
- (0...@functions.length).to_a
2811
- end
2812
- end
2813
-
2814
- def move_up
2815
- matching = current_matching_indices
2816
- return @index if matching.empty?
2817
- cols = @columns
2818
- pos = matching.index(@index) || 0
2819
- if cols <= 1
2820
- new_pos = (pos - 1) % matching.length
2821
- else
2822
- new_pos = pos - cols
2823
- if new_pos < 0
2824
- new_pos = pos
2825
- while (new_pos + cols) < matching.length
2826
- new_pos += cols
2827
- end
2828
- end
2829
- end
2830
- @index = matching[new_pos]
2831
- end
2832
- alias_method :_up, :move_up
2833
-
2834
- def move_down
2835
- matching = current_matching_indices
2836
- return @index if matching.empty?
2837
- cols = @columns
2838
- pos = matching.index(@index) || 0
2839
- if cols <= 1
2840
- new_pos = (pos + 1) % matching.length
2841
- else
2842
- new_pos = pos + cols
2843
- if new_pos >= matching.length
2844
- new_pos = pos % cols
2845
- end
2846
- end
2847
- @index = matching[new_pos]
2848
- end
2849
- alias_method :_down, :move_down
2850
-
2851
- def move_left
2852
- matching = current_matching_indices
2853
- return @index if matching.empty?
2854
- cols = @columns
2855
- pos = matching.index(@index) || 0
2856
- if cols <= 1
2857
- new_pos = (pos - 1) % matching.length
2858
- else
2859
- if (pos % cols) == 0
2860
- new_pos = [pos + (cols - 1), matching.length - 1].min
2861
- else
2862
- new_pos = pos - 1
2863
- end
2864
- end
2865
- @index = matching[new_pos]
2866
- end
2867
-
2868
- def move_right
2869
- matching = current_matching_indices
2870
- return @index if matching.empty?
2871
- cols = @columns
2872
- pos = matching.index(@index) || 0
2873
- if cols <= 1
2874
- new_pos = (pos + 1) % matching.length
2875
- else
2876
- if (pos % cols) == (cols - 1) || pos == (matching.length - 1)
2877
- new_pos = pos - (pos % cols)
2878
- else
2879
- new_pos = pos + 1
2880
- end
2881
- end
2882
- @index = matching[new_pos]
2883
- end
2884
-
2885
- def colorize(text, color_config, phase_offset = 0.0)
2886
- return text.to_s if color_config.nil? || color_config.empty?
2887
-
2888
- color_name = (color_config[:color] || color_config["color"]).to_s.downcase.strip
2889
- brightness_level = (color_config[:level] || color_config["level"] || 1).to_i
2890
-
2891
- if color_name.include?(":")
2892
- parts = color_name.split(":")
2893
- color_name = parts[0].strip
2894
- brightness_level = parts[1].to_i if parts[1] && !parts[1].empty?
2895
- end
2896
-
2897
- is_neon_color = color_name.start_with?("neon")
2898
- is_anim_active = is_neon_color || (@animate && ["neon", "fade"].include?(@animate.to_s.downcase))
2899
- is_chroma = color_name == "rgb" || color_name == "rainbow" || color_name == "chroma" || (@animate && ["rgb", "rainbow", "chroma"].include?(@animate.to_s.downcase))
2900
-
2901
- if is_chroma
2902
- tick = @rgb_tick || 0.0
2903
- out = String.new("")
2904
- char_count = 0
2905
- in_escape = false
2906
- escape_buf = String.new("")
2907
-
2908
- text.to_s.each_char do |ch|
2909
- if ch == "\e"
2910
- in_escape = true
2911
- escape_buf << ch
2912
- next
2913
- end
2914
- if in_escape
2915
- escape_buf << ch
2916
- if ch =~ /[a-zA-Z]/
2917
- in_escape = false
2918
- out << escape_buf
2919
- escape_buf.clear
2920
- end
2921
- next
2922
- end
2923
-
2924
- if ch == " " || ch == "\t" || ch == "\r" || ch == "\n"
2925
- out << ch
2926
- else
2927
- c_code = self.class.rgb_color(tick, char_count * 0.12 + phase_offset)
2928
- out << "#{c_code}#{ch}"
2929
- char_count += 1
2930
- end
2931
- end
2932
- out << self.class.ansi_reset
2933
- return out
2934
- end
2935
-
2936
- if is_anim_active
2937
- tick = @rgb_tick || 0.0
2938
- base = if color_name =~ /\A#?([0-9a-f]{6})\z/i
2939
- h = $1
2940
- [h[0..1].to_i(16), h[2..3].to_i(16), h[4..5].to_i(16)]
2941
- elsif color_name =~ /\A#?([0-9a-f]{3})\z/i
2942
- h = $1
2943
- [(h[0] * 2).to_i(16), (h[1] * 2).to_i(16), (h[2] * 2).to_i(16)]
2944
- else
2945
- BASE_RGB[color_name] || [255, 255, 255]
2946
- end
2947
-
2948
- factor = (Math.sin(tick + phase_offset) + 1.0) / 2.0
2949
- f = 0.35 + 0.65 * factor
2950
- r = (base[0] * f).clamp(0, 255).to_i
2951
- g = (base[1] * f).clamp(0, 255).to_i
2952
- b = (base[2] * f).clamp(0, 255).to_i
2953
- glow = (factor > 0.85) ? ";1" : ""
2954
- return "\e[38;2;#{r};#{g};#{b}#{glow}m#{text}#{self.class.ansi_reset}"
2955
- end
2956
-
2957
- color_code = self.class.ansi_color(color_name, brightness_level)
2958
- return text.to_s unless color_code
2959
-
2960
- "#{color_code}#{text}#{self.class.ansi_reset}"
2961
- end
2962
- alias_method :_colorize, :colorize
2963
-
2964
- def build_horizontal_line(pattern, target_width)
2965
- return "" if target_width <= 0 || pattern.nil? || pattern.empty?
2966
-
2967
- pattern_length = pattern.length
2968
- repetitions_needed = (target_width.to_f / pattern_length).ceil + 1
2969
- (pattern * repetitions_needed)[0...target_width]
2970
- end
2971
- alias_method :_hline, :build_horizontal_line
2972
-
2973
- def render_banner_lines(term_cols)
2974
- return [[], 0] if @banner.nil? || @banner.empty?
2975
-
2976
- font_id = @style_config.font || 1
2977
- ascii_rows = self.class.build_ascii_lines(@banner, term_cols, font_id)
2978
- banner_border = BORDERS[@banner_style] || BORDERS[3]
2979
- banner_color_cfg = @style_config.banner
2980
-
2981
- h_top = banner_border[:ht] || banner_border[:h]
2982
- h_bot = banner_border[:hb] || banner_border[:h]
2983
- v_l = banner_border[:vl] || banner_border[:v]
2984
- v_r = banner_border[:vr] || banner_border[:v]
2985
-
2986
- lines = []
2987
- box_w = 0
2988
- if ascii_rows
2989
- content_w = ascii_rows.map { |r| GRmenu.display_width(r) }.max
2990
- box_w = content_w + 6
2991
- top_fill = build_horizontal_line(h_top, content_w + 4)
2992
- bot_fill = build_horizontal_line(h_bot, content_w + 4)
2993
-
2994
- lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg, 0.0)
2995
- ascii_rows.each_with_index do |row, r_i|
2996
- pad = " " * (content_w - GRmenu.display_width(row))
2997
- lines << colorize("#{v_l} #{row}#{pad} #{v_r}", banner_color_cfg, r_i * 0.2)
2998
- end
2999
- lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg, 1.2)
3000
- else
3001
- clean_b = @banner.strip
3002
- b_vis_w = GRmenu.display_width(clean_b)
3003
- box_w = [b_vis_w + 6, term_cols - 2].min
3004
- top_fill = build_horizontal_line(h_top, box_w - 2)
3005
- bot_fill = build_horizontal_line(h_bot, box_w - 2)
3006
-
3007
- pad_b = [box_w - 4 - b_vis_w, 0].max
3008
- l_p = " " * (pad_b / 2)
3009
- r_p = " " * (pad_b - (pad_b / 2))
3010
-
3011
- lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg, 0.0)
3012
- lines << colorize("#{v_l} #{l_p}#{clean_b}#{r_p} #{v_r}", banner_color_cfg, 0.4)
3013
- lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg, 0.8)
3014
- end
3015
- [lines, box_w]
3016
- end
3017
-
3018
- def render_image_lines(term_cols)
3019
- return @cached_image_lines if @cached_image_lines && @cached_image_cols == term_cols
3020
-
3021
- return [[], 0] unless @image && File.exist?(@image)
3022
-
3023
- raw_lines = self.class.load_and_render_image(@image, @image_width || 40, nil, term_cols)
3024
- return [[], 0] if raw_lines.empty?
3025
-
3026
- img_w = self.class.display_width(raw_lines.first)
3027
- box_w = img_w + 4
3028
- banner_border = BORDERS[@banner_style] || BORDERS[3]
3029
- banner_color_cfg = @style_config.banner
3030
-
3031
- h_top = banner_border[:ht] || banner_border[:h]
3032
- h_bot = banner_border[:hb] || banner_border[:h]
3033
- v_l = banner_border[:vl] || banner_border[:v]
3034
- v_r = banner_border[:vr] || banner_border[:v]
3035
-
3036
- top_fill = build_horizontal_line(h_top, img_w + 2)
3037
- bot_fill = build_horizontal_line(h_bot, img_w + 2)
3038
-
3039
- lines = []
3040
- lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg)
3041
- raw_lines.each do |r_line|
3042
- lines << "#{colorize(v_l, banner_color_cfg)} #{r_line} #{colorize(v_r, banner_color_cfg)}"
3043
- end
3044
- lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg)
3045
-
3046
- @cached_image_cols = term_cols
3047
- @cached_image_lines = [lines, box_w]
3048
- @cached_image_lines
3049
- end
3050
-
3051
- def is_submenu_item?(action)
3052
- if action.is_a?(Array)
3053
- target = action[1]
3054
- 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))
3055
- return true if target.is_a?(GRmenu)
3056
- elsif action.is_a?(Hash)
3057
- return true if action[:submenu] || action["submenu"]
3058
- end
3059
- false
3060
- end
3061
-
3062
- def get_submenu_actions(action)
3063
- if action.is_a?(Array)
3064
- action[1].is_a?(GRmenu) ? action[1].instance_variable_get(:@functions) : action[1]
3065
- elsif action.is_a?(Hash)
3066
- action[:submenu] || action["submenu"]
3067
- end
3068
- end
3069
-
3070
- def render_submenu_box(sub_actions, parent_title = "", is_active: false, selected_idx: 0)
3071
- sub_names = sub_actions.map { |a| extract_name_from_action(a) }
3072
- max_name_len = sub_names.empty? ? 10 : sub_names.map { |n| GRmenu.display_width(n) }.max
3073
- sub_title = parent_title.to_s
3074
- sub_w = [max_name_len + 8, GRmenu.display_width(sub_title) + 6, 20].max
3075
- sub_w = [sub_w, 36].min
3076
- inner_w = [sub_w - 2, 8].max
3077
-
3078
- s_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "submenu")) || {}
3079
- sub_style = (s_sec["style"] || @style || 3).to_i
3080
- sub_border = BORDERS[sub_style] || BORDERS[3]
3081
- h_top = sub_border[:ht] || sub_border[:h]
3082
- h_bot = sub_border[:hb] || sub_border[:h]
3083
- v_l = sub_border[:vl] || sub_border[:v]
3084
- v_r = sub_border[:vr] || sub_border[:v]
3085
-
3086
- s_brd_col = s_sec["border"] || s_sec["border_color"] || @style_config.border[:color] || "cyan"
3087
- s_foc_col = s_sec["focus"] || s_sec["focus_color"] || @style_config.focus[:color] || "yellow"
3088
- s_opt_col = s_sec["options"] || s_sec["options_color"] || @style_config.options[:color] || "white"
3089
-
3090
- s_brd_cfg = { color: s_brd_col, level: 1 }
3091
- s_foc_cfg = { color: s_foc_col, level: 2 }
3092
- s_opt_cfg = { color: s_opt_col, level: 1 }
3093
- s_held_cfg = { color: "yellow", level: 1 }
3094
-
3095
- lines = []
3096
- top_fill = build_horizontal_line(h_top, inner_w)
3097
- bot_fill = build_horizontal_line(h_bot, inner_w)
3098
-
3099
- top_border_line = sub_border[:tl] + top_fill + sub_border[:tr]
3100
- lines << colorize(top_border_line, s_brd_cfg, 0.0)
3101
-
3102
- unless sub_title.empty?
3103
- pad_t = [inner_w - 2 - GRmenu.display_width(sub_title), 0].max
3104
- t_padded = " " * (pad_t / 2) + sub_title + " " * (pad_t - (pad_t / 2))
3105
- c_title = colorize(t_padded, s_foc_cfg, 0.2)
3106
- v_l_col = colorize(v_l, s_brd_cfg, 0.2)
3107
- v_r_col = colorize(v_r, s_brd_cfg, 0.2)
3108
- lines << "#{v_l_col} #{c_title} #{v_r_col}"
3109
- mid_fill = build_horizontal_line(h_top, inner_w)
3110
- lines << colorize(v_l + mid_fill + v_r, s_brd_cfg, 0.4)
3111
- end
3112
-
3113
- parent_item_row = nil
3114
- sub_names.each_with_index do |s_name, s_idx|
3115
- is_sub = is_submenu_item?(sub_actions[s_idx])
3116
- arrow = is_sub ? "▶" : ""
3117
- is_selected = (s_idx == selected_idx)
3118
- parent_item_row = lines.length if is_selected
3119
-
3120
- prefix = is_selected ? "> " : " "
3121
- pad_s = [inner_w - 2 - prefix.length - GRmenu.display_width(s_name) - (is_sub ? 2 : 0), 0].max
3122
- raw_item = if is_sub
3123
- "#{prefix}#{s_name}#{' ' * pad_s} #{arrow}"
3124
- else
3125
- "#{prefix}#{s_name}#{' ' * pad_s}"
3126
- end
3127
-
3128
- item_cfg = if is_selected
3129
- is_active ? s_foc_cfg : s_held_cfg
3130
- else
3131
- s_opt_cfg
3132
- end
3133
-
3134
- colored_item = colorize(raw_item, item_cfg, s_idx * 0.2)
3135
- v_l_col = colorize(v_l, s_brd_cfg, 0.2)
3136
- v_r_col = colorize(v_r, s_brd_cfg, 0.8)
3137
- lines << "#{v_l_col} #{colored_item} #{v_r_col}"
3138
- end
3139
-
3140
- bot_border_line = sub_border[:bl] + bot_fill + sub_border[:br]
3141
- lines << colorize(bot_border_line, s_brd_cfg, 0.6)
3142
-
3143
- [lines, sub_w, parent_item_row]
3144
- end
3145
-
3146
- def render_lines(size_max = 20)
3147
- term_cols = self.class.terminal_width
3148
- term_rows = self.class.terminal_height
3149
- rendered_lines = []
3150
- @row_hit_map = {}
3151
- @tab_ranges = {}
3152
- @tabs_row = nil
3153
- @up_arrow_row = nil
3154
- @down_arrow_row = nil
3155
- @sub_hit_map = {}
3156
- @sub_1_hit_map = {}
3157
- @sub_2_hit_map = {}
3158
- @sub_1_col_rng = nil
3159
- @sub_2_col_rng = nil
3160
-
3161
- header_box_width = 0
3162
- header_lines_count = 0
3163
-
3164
- if @image && File.exist?(@image)
3165
- img_lines, img_box_w = render_image_lines(term_cols)
3166
- unless img_lines.empty?
3167
- rendered_lines.concat(img_lines)
3168
- rendered_lines << ""
3169
- header_lines_count += img_lines.length + 1
3170
- header_box_width = [header_box_width, img_box_w].max
3171
- end
3172
- end
3173
-
3174
- if @banner && !@banner.empty?
3175
- banner_lines, banner_box_w = render_banner_lines(term_cols)
3176
- unless banner_lines.empty?
3177
- b_pad = (@center && term_cols > banner_box_w) ? " " * ((term_cols - banner_box_w) / 2) : ""
3178
- rendered_lines.concat(banner_lines.map { |l| "#{b_pad}#{l}" })
3179
- rendered_lines << ""
3180
- header_lines_count += banner_lines.length + 1
3181
- header_box_width = [header_box_width, banner_box_w].max
3182
- end
3183
- end
3184
-
3185
- matching_indices = current_matching_indices
3186
- all_names = @functions.map { |func| extract_name_from_action(func) }
3187
- all_descriptions = @functions.map { |func| extract_description_from_action(func) }
3188
-
3189
- active_desc = all_descriptions[@index] || ""
3190
-
3191
- cols = @columns
3192
- max_item_len = all_names.empty? ? 10 : all_names.map { |n| GRmenu.display_width(n) }.max
3193
- grid_suggested_w = (max_item_len + 6) * cols + 4
3194
-
3195
- calculated_width = [size_max, GRmenu.display_width(@title) + 4, grid_suggested_w].max
3196
- calculated_width = ([calculated_width, GRmenu.display_width(active_desc) + 8].max) unless active_desc.empty?
3197
- calculated_width = ([calculated_width, GRmenu.display_width(@query) + 16].max) if @search
3198
- if @tabs && !@tabs.empty?
3199
- tabs_min_w = @tabs.map { |t| GRmenu.display_width(t) + 4 }.sum + (@tabs.length - 1) * 3
3200
- calculated_width = [calculated_width, tabs_min_w + 4].max
3201
- end
3202
- total_width = [calculated_width, term_cols - 2].min
3203
-
3204
- sub_1_preview_w = 0
3205
- sub_2_preview_w = 0
3206
- if @open_level >= 1 && is_submenu_item?(@functions[@index])
3207
- s1_acts = get_submenu_actions(@functions[@index])
3208
- if s1_acts && !s1_acts.empty?
3209
- s1_names = s1_acts.map { |a| extract_name_from_action(a) }
3210
- s1_max = s1_names.empty? ? 10 : s1_names.map { |n| GRmenu.display_width(n) }.max
3211
- s1_title = all_names[@index] || ""
3212
- sub_1_preview_w = [s1_max + 8, GRmenu.display_width(s1_title) + 6, 20].max
3213
- sub_1_preview_w = [sub_1_preview_w, 36].min
3214
-
3215
- if @open_level >= 2 && is_submenu_item?(s1_acts[@sub_index_1])
3216
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1])
3217
- if s2_acts && !s2_acts.empty?
3218
- s2_names = s2_acts.map { |a| extract_name_from_action(a) }
3219
- s2_max = s2_names.empty? ? 10 : s2_names.map { |n| GRmenu.display_width(n) }.max
3220
- s2_title = extract_name_from_action(s1_acts[@sub_index_1]) || ""
3221
- sub_2_preview_w = [s2_max + 8, GRmenu.display_width(s2_title) + 6, 20].max
3222
- sub_2_preview_w = [sub_2_preview_w, 36].min
3223
- end
3224
- end
3225
- end
3226
- end
3227
-
3228
- total_chain_w = total_width
3229
- total_chain_w += 2 + sub_1_preview_w if sub_1_preview_w > 0
3230
- total_chain_w += 2 + sub_2_preview_w if sub_2_preview_w > 0
3231
-
3232
- margin_left = if @center && term_cols > total_chain_w
3233
- " " * ((term_cols - total_chain_w) / 2)
3234
- elsif @center && term_cols > total_width
3235
- " " * ((term_cols - total_width) / 2)
3236
- else
3237
- ""
3238
- end
3239
-
3240
- subtitle_lines_count = 0
3241
- if @subtitle && !@subtitle.empty?
3242
- subtitle_lines = @subtitle.lines.map(&:chomp)
3243
- ref_sub_w = header_box_width > 0 ? header_box_width : total_chain_w
3244
- div_w = @divider.is_a?(Numeric) ? @divider.to_i : [ref_sub_w, term_cols - 2].min
3245
- sub_pad = (@center && term_cols > div_w) ? " " * ((term_cols - div_w) / 2) : ""
3246
-
3247
- if @divider
3248
- rendered_lines << "#{sub_pad}#{colorize("─" * div_w, @style_config.divider, 0.0)}"
3249
- subtitle_lines_count += 1
3250
- end
3251
-
3252
- subtitle_lines.each_with_index do |sub_line, s_i|
3253
- pad_sub = [div_w - GRmenu.display_width(sub_line), 0].max
3254
- formatted_sub = @center ? (" " * (pad_sub / 2) + sub_line + " " * (pad_sub - (pad_sub / 2))) : sub_line
3255
- rendered_lines << "#{sub_pad}#{colorize(formatted_sub, @style_config.subtitle, s_i * 0.3)}"
3256
- subtitle_lines_count += 1
3257
- end
3258
-
3259
- if @divider
3260
- rendered_lines << "#{sub_pad}#{colorize("─" * div_w, @style_config.divider, 0.6)}"
3261
- subtitle_lines_count += 1
3262
- end
3263
- rendered_lines << ""
3264
- subtitle_lines_count += 1
3265
- end
3266
-
3267
- border_color_cfg = @style_config.border
3268
- options_color_cfg = @style_config.options
3269
- focus_color_cfg = @style_config.focus
3270
- title_color_cfg = @style_config.title
3271
-
3272
- box_border = BORDERS[@style]
3273
-
3274
- overhead = header_lines_count + subtitle_lines_count + 6
3275
- overhead += 2 unless active_desc.empty?
3276
- overhead += 2 if @search
3277
- available_rows = [term_rows - overhead - 2, 2].max
3278
-
3279
- rows_data = matching_indices.each_slice(cols).to_a
3280
- total_rows = rows_data.length
3281
-
3282
- effective_page_rows = if @page_size && @page_size > 0
3283
- [@page_size, total_rows, available_rows].min
3284
- else
3285
- [total_rows, available_rows].min
3286
- end
3287
- effective_page_rows = [effective_page_rows, 1].max
3288
-
3289
- curr_matching_pos = matching_indices.index(@index) || 0
3290
- curr_row = total_rows > 0 ? (curr_matching_pos / cols) : 0
3291
-
3292
- start_row = 0
3293
- end_row = [total_rows - 1, 0].max
3294
- if total_rows > effective_page_rows
3295
- half_r = effective_page_rows / 2
3296
- start_row = [[curr_row - half_r, 0].max, total_rows - effective_page_rows].min
3297
- end_row = start_row + effective_page_rows - 1
3298
- end
3299
-
3300
- visible_rows_data = rows_data[start_row..end_row] || []
3301
- has_more_above = start_row > 0
3302
- has_more_below = end_row < (total_rows - 1)
3303
-
3304
- avail_w = [total_width - 4, 1].max
3305
- col_w = [(avail_w - (cols - 1) * 2) / cols, 1].max
3306
-
3307
- if @tabs && !@tabs.empty?
3308
- tab_strs = []
3309
- raw_tab_strs = []
3310
- @tab_ranges = {}
3311
- @tabs.each_with_index do |t_name, t_i|
3312
- is_active = (t_i == @active_tab_idx)
3313
- raw_t = is_active ? "[ #{t_name} ]" : " #{t_name} "
3314
- colored_t = if is_active
3315
- colorize(raw_t, { color: @active_tab_color, level: 2 }, 0.0)
3316
- else
3317
- colorize(raw_t, { color: @tab_color, level: 1 }, 0.0)
3318
- end
3319
- tab_strs << colored_t
3320
- raw_tab_strs << raw_t
3321
- end
3322
- raw_tabs_joined = raw_tab_strs.join(" ")
3323
- tabs_w = GRmenu.display_width(raw_tabs_joined)
3324
- l_tabs_pad = [(total_width - tabs_w) / 2, 0].max
3325
- formatted_tabs = (" " * l_tabs_pad) + tab_strs.join(" ")
3326
-
3327
- start_col = margin_left.length + l_tabs_pad
3328
- cur_x = start_col
3329
- @tabs.each_with_index do |_t_name, t_i|
3330
- t_len = GRmenu.display_width(raw_tab_strs[t_i])
3331
- @tab_ranges[t_i] = (cur_x + 1)..(cur_x + t_len)
3332
- cur_x += t_len + 3
3333
- end
3334
-
3335
- @tabs_row = rendered_lines.length + 1
3336
- rendered_lines << "#{margin_left}#{formatted_tabs}"
3337
- rendered_lines << ""
3338
- end
3339
-
3340
- if box_border
3341
- h_top = box_border[:ht] || box_border[:h]
3342
- h_bot = box_border[:hb] || box_border[:h]
3343
- v_l_raw = box_border[:vl] || box_border[:v]
3344
- v_r_raw = box_border[:vr] || box_border[:v]
3345
-
3346
- top_fill = build_horizontal_line(h_top, total_width - 2)
3347
- bot_fill = build_horizontal_line(h_bot, total_width - 2)
3348
- mid_fill = build_horizontal_line(h_top, total_width - 2)
3349
-
3350
- v_left = colorize(v_l_raw, border_color_cfg, 0.2)
3351
- v_right = colorize(v_r_raw, border_color_cfg, 0.8)
3352
-
3353
- main_box_start = rendered_lines.length
3354
- top_border_line = box_border[:tl] + top_fill + box_border[:tr]
3355
- rendered_lines << "#{margin_left}#{colorize(top_border_line, border_color_cfg, 0.0)}"
3356
-
3357
- unless @title.empty?
3358
- pad_t = [total_width - 4 - GRmenu.display_width(@title), 0].max
3359
- title_padded = " " * (pad_t / 2) + @title + " " * (pad_t - (pad_t / 2))
3360
- centered_title = colorize(title_padded, title_color_cfg, 0.4)
3361
- rendered_lines << "#{margin_left}#{v_left} #{centered_title} #{v_right}"
3362
-
3363
- separator_line = v_l_raw + mid_fill + v_r_raw
3364
- rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg, 0.6)}"
3365
- end
3366
-
3367
- if @search
3368
- search_prompt = "Buscar: #{@query}█"
3369
- pad_s = [avail_w - GRmenu.display_width(search_prompt), 0].max
3370
- search_padded = search_prompt + (" " * pad_s)
3371
- rendered_lines << "#{margin_left}#{v_left} #{Color.bright_yellow(search_padded)} #{v_right}"
3372
- separator_line = v_l_raw + mid_fill + v_r_raw
3373
- rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg, 0.8)}"
3374
- end
3375
-
3376
- if has_more_above
3377
- @up_arrow_row = rendered_lines.length + 1
3378
- up_text = "▲ (+#{start_row} #{cols > 1 ? 'filas' : 'arriba'})"
3379
- pad_up = [avail_w - GRmenu.display_width(up_text), 0].max
3380
- up_indicator = colorize(" " * (pad_up / 2) + up_text + " " * (pad_up - (pad_up / 2)), { color: "gray", level: 2 })
3381
- rendered_lines << "#{margin_left}#{v_left} #{up_indicator} #{v_right}"
3382
- end
3383
-
3384
- parent_row_idx = nil
3385
- if rows_data.empty?
3386
- no_res_txt = "(Sin resultados)"
3387
- pad_no = [avail_w - GRmenu.display_width(no_res_txt), 0].max
3388
- no_res = colorize(" " * (pad_no / 2) + no_res_txt + " " * (pad_no - (pad_no / 2)), { color: "gray", level: 1 })
3389
- rendered_lines << "#{margin_left}#{v_left} #{no_res} #{v_right}"
3390
- else
3391
- visible_rows_data.each_with_index do |row_indices, r_idx|
3392
- cells = []
3393
- cols.times do |c_idx|
3394
- item_idx = row_indices[c_idx]
3395
- if item_idx
3396
- op_name = all_names[item_idx]
3397
- is_sub = is_submenu_item?(@functions[item_idx])
3398
- arrow = is_sub ? "▶" : ""
3399
- if @index == item_idx
3400
- parent_row_idx = rendered_lines.length
3401
- cell_raw = if is_sub
3402
- pad_sub = [col_w - 2 - GRmenu.display_width(op_name) - 2, 0].max
3403
- "> #{op_name}#{' ' * pad_sub}#{arrow}"
3404
- else
3405
- "> #{op_name}"
3406
- end
3407
- pad_c = [col_w - GRmenu.display_width(cell_raw), 0].max
3408
- cells << colorize(cell_raw + (" " * pad_c), focus_color_cfg, r_idx * 0.3)
3409
- else
3410
- cell_raw = if is_sub
3411
- pad_sub = [col_w - 2 - GRmenu.display_width(op_name) - 2, 0].max
3412
- " #{op_name}#{' ' * pad_sub}#{arrow}"
3413
- else
3414
- " #{op_name}"
3415
- end
3416
- pad_c = [col_w - GRmenu.display_width(cell_raw), 0].max
3417
- cells << colorize(cell_raw + (" " * pad_c), options_color_cfg, r_idx * 0.2)
3418
- end
3419
- else
3420
- cells << (" " * col_w)
3421
- end
3422
- end
3423
- row_str = cells.join(" ")
3424
- pad_r = [avail_w - (col_w * cols + (cols - 1) * 2), 0].max
3425
- row_padded = row_str + (" " * pad_r)
3426
- cur_line_num = rendered_lines.length + 1
3427
- @row_hit_map[cur_line_num] = { row_indices: row_indices, cols: cols, col_w: col_w, margin_left: margin_left.length }
3428
- rendered_lines << "#{margin_left}#{v_left} #{row_padded} #{v_right}"
3429
- end
3430
- end
3431
-
3432
- if has_more_below
3433
- @down_arrow_row = rendered_lines.length + 1
3434
- remaining_below = total_rows - 1 - end_row
3435
- down_text = "▼ (+#{remaining_below} #{cols > 1 ? 'filas' : 'abajo'})"
3436
- pad_down = [avail_w - GRmenu.display_width(down_text), 0].max
3437
- down_indicator = colorize(" " * (pad_down / 2) + down_text + " " * (pad_down - (pad_down / 2)), { color: "gray", level: 2 })
3438
- rendered_lines << "#{margin_left}#{v_left} #{down_indicator} #{v_right}"
3439
- end
3440
-
3441
- unless active_desc.empty?
3442
- separator_line = v_l_raw + mid_fill + v_r_raw
3443
- rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg, 1.0)}"
3444
- pfx = (@style_config&.desc_prefix || @desc_prefix || SetStyle.desc_prefix || "[i]").to_s
3445
- pfx = "#{pfx} " unless pfx.end_with?(" ")
3446
- raw_desc = "#{pfx}#{active_desc}"
3447
- pad_d = [avail_w - GRmenu.display_width(raw_desc), 0].max
3448
- desc_text = colorize(raw_desc + (" " * pad_d), { color: "cyan", level: 1 })
3449
- rendered_lines << "#{margin_left}#{v_left} #{desc_text} #{v_right}"
3450
- end
3451
-
3452
- bottom_border_line = box_border[:bl] + bot_fill + box_border[:br]
3453
- rendered_lines << "#{margin_left}#{colorize(bottom_border_line, border_color_cfg, 1.4)}"
3454
- else
3455
- symbol_char = STYLES[@style] || "#"
3456
- solid_border = colorize(symbol_char, border_color_cfg)
3457
- solid_line = symbol_char * total_width
3458
-
3459
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3460
-
3461
- unless @title.empty?
3462
- pad_t = [total_width - 4 - GRmenu.display_width(@title), 0].max
3463
- title_padded = " " * (pad_t / 2) + @title + " " * (pad_t - (pad_t / 2))
3464
- centered_title = colorize(title_padded, title_color_cfg)
3465
- rendered_lines << "#{margin_left}#{solid_border} #{centered_title} #{solid_border}"
3466
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3467
- end
3468
-
3469
- if @search
3470
- search_prompt = "Buscar: #{@query}█"
3471
- pad_s = [avail_w - GRmenu.display_width(search_prompt), 0].max
3472
- search_padded = search_prompt + (" " * pad_s)
3473
- rendered_lines << "#{margin_left}#{solid_border} #{Color.bright_yellow(search_padded)} #{solid_border}"
3474
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3475
- end
3476
-
3477
- if has_more_above
3478
- @up_arrow_row = rendered_lines.length + 1
3479
- up_text = "▲ (+#{start_row} #{cols > 1 ? 'filas' : 'arriba'})"
3480
- pad_up = [avail_w - GRmenu.display_width(up_text), 0].max
3481
- up_indicator = colorize(" " * (pad_up / 2) + up_text + " " * (pad_up - (pad_up / 2)), { color: "gray", level: 2 })
3482
- rendered_lines << "#{margin_left}#{solid_border} #{up_indicator} #{solid_border}"
3483
- end
3484
-
3485
- parent_row_idx = nil
3486
- if rows_data.empty?
3487
- no_res_txt = "(Sin resultados)"
3488
- pad_no = [avail_w - GRmenu.display_width(no_res_txt), 0].max
3489
- no_res = colorize(" " * (pad_no / 2) + no_res_txt + " " * (pad_no - (pad_no / 2)), { color: "gray", level: 1 })
3490
- rendered_lines << "#{margin_left}#{solid_border} #{no_res} #{solid_border}"
3491
- else
3492
- visible_rows_data.each_with_index do |row_indices, r_idx|
3493
- cells = []
3494
- cols.times do |c_idx|
3495
- item_idx = row_indices[c_idx]
3496
- if item_idx
3497
- op_name = all_names[item_idx]
3498
- is_sub = is_submenu_item?(@functions[item_idx])
3499
- arrow = is_sub ? "▶" : ""
3500
- if @index == item_idx
3501
- parent_row_idx = rendered_lines.length
3502
- cell_raw = if is_sub
3503
- pad_sub = [col_w - 2 - GRmenu.display_width(op_name) - 2, 0].max
3504
- "> #{op_name}#{' ' * pad_sub}#{arrow}"
3505
- else
3506
- "> #{op_name}"
3507
- end
3508
- pad_c = [col_w - GRmenu.display_width(cell_raw), 0].max
3509
- cells << colorize(cell_raw + (" " * pad_c), focus_color_cfg, r_idx * 0.3)
3510
- else
3511
- cell_raw = if is_sub
3512
- pad_sub = [col_w - 2 - GRmenu.display_width(op_name) - 2, 0].max
3513
- " #{op_name}#{' ' * pad_sub}#{arrow}"
3514
- else
3515
- " #{op_name}"
3516
- end
3517
- pad_c = [col_w - GRmenu.display_width(cell_raw), 0].max
3518
- cells << colorize(cell_raw + (" " * pad_c), options_color_cfg, r_idx * 0.2)
3519
- end
3520
- else
3521
- cells << (" " * col_w)
3522
- end
3523
- end
3524
- row_str = cells.join(" ")
3525
- pad_r = [avail_w - (col_w * cols + (cols - 1) * 2), 0].max
3526
- row_padded = row_str + (" " * pad_r)
3527
- cur_line_num = rendered_lines.length + 1
3528
- @row_hit_map[cur_line_num] = { row_indices: row_indices, cols: cols, col_w: col_w, margin_left: margin_left.length }
3529
- rendered_lines << "#{margin_left}#{solid_border} #{row_padded} #{solid_border}"
3530
- end
3531
- end
3532
-
3533
- if has_more_below
3534
- @down_arrow_row = rendered_lines.length + 1
3535
- remaining_below = total_rows - 1 - end_row
3536
- down_text = "▼ (+#{remaining_below} #{cols > 1 ? 'filas' : 'abajo'})"
3537
- pad_down = [avail_w - GRmenu.display_width(down_text), 0].max
3538
- down_indicator = colorize(" " * (pad_down / 2) + down_text + " " * (pad_down - (pad_down / 2)), { color: "gray", level: 2 })
3539
- rendered_lines << "#{margin_left}#{solid_border} #{down_indicator} #{solid_border}"
3540
- end
3541
-
3542
- unless active_desc.empty?
3543
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3544
- pfx = (@style_config&.desc_prefix || @desc_prefix || SetStyle.desc_prefix || "[i]").to_s
3545
- pfx = "#{pfx} " unless pfx.end_with?(" ")
3546
- raw_desc = "#{pfx}#{active_desc}"
3547
- pad_d = [avail_w - GRmenu.display_width(raw_desc), 0].max
3548
- desc_text = colorize(raw_desc + (" " * pad_d), { color: "cyan", level: 1 })
3549
- rendered_lines << "#{margin_left}#{solid_border} #{desc_text} #{solid_border}"
3550
- end
3551
-
3552
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
3553
- end
3554
-
3555
- if @open_level >= 1 && is_submenu_item?(@functions[@index]) && parent_row_idx
3556
- sub_1_acts = get_submenu_actions(@functions[@index])
3557
- if sub_1_acts && !sub_1_acts.empty?
3558
- sub_1_title = all_names[@index] || "Submenu"
3559
- 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)
3560
-
3561
- sub_2_acts = nil
3562
- sub_2_lines = nil
3563
- sub_2_w = 0
3564
- p_row_2 = nil
3565
- if @open_level >= 2 && is_submenu_item?(sub_1_acts[@sub_index_1])
3566
- sub_2_acts = get_submenu_actions(sub_1_acts[@sub_index_1])
3567
- if sub_2_acts && !sub_2_acts.empty?
3568
- sub_2_title = extract_name_from_action(sub_1_acts[@sub_index_1]) || "Submenu"
3569
- 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)
3570
- end
3571
- end
3572
-
3573
- if sub_2_lines
3574
- total_3_w = margin_left.length + total_width + 2 + sub_1_w + 2 + sub_2_w
3575
- if total_3_w <= term_cols
3576
- sub_1_start = main_box_start
3577
- p1_abs_row = sub_1_start + (p_row_1 || 1)
3578
- sub_2_start = main_box_start
3579
- max_3_lines = [rendered_lines.length, sub_1_start + sub_1_lines.length, sub_2_start + sub_2_lines.length].max
3580
-
3581
- x1_start = margin_left.length + total_width + 2
3582
- x1_end = x1_start + sub_1_w
3583
- x2_start = x1_end + 2
3584
- x2_end = x2_start + sub_2_w
3585
- @sub_1_col_rng = (x1_start..x1_end)
3586
- @sub_2_col_rng = (x2_start..x2_end)
3587
-
3588
- sub_1_offset = sub_1_title.empty? ? 1 : 3
3589
- sub_2_offset = sub_2_title.empty? ? 1 : 3
3590
-
3591
- stitched_lines = []
3592
- max_3_lines.times do |i|
3593
- l0 = rendered_lines[i] || ("#{margin_left}#{' ' * total_width}")
3594
-
3595
- l1 = " " * sub_1_w
3596
- br1 = " "
3597
- if i >= sub_1_start && i < (sub_1_start + sub_1_lines.length)
3598
- s1_idx = i - sub_1_start
3599
- l1 = sub_1_lines[s1_idx]
3600
- br1 = (i == parent_row_idx) ? "──" : " "
3601
- s1_item = s1_idx - sub_1_offset
3602
- if s1_item >= 0 && s1_item < sub_1_acts.length
3603
- @sub_1_hit_map[i + 1] = s1_item
3604
- @sub_hit_map[i + 1] = s1_item
3605
- end
3606
- end
3607
-
3608
- l2 = ""
3609
- br2 = ""
3610
- if i >= sub_2_start && i < (sub_2_start + sub_2_lines.length)
3611
- s2_idx = i - sub_2_start
3612
- l2 = sub_2_lines[s2_idx]
3613
- br2 = (i == p1_abs_row) ? "──" : " "
3614
- s2_item = s2_idx - sub_2_offset
3615
- if s2_item >= 0 && s2_item < sub_2_acts.length
3616
- @sub_2_hit_map[i + 1] = s2_item
3617
- end
3618
- end
3619
-
3620
- stitched_lines << "#{l0}#{br1}#{l1}#{br2}#{l2}".rstrip
3621
- end
3622
- rendered_lines = stitched_lines
3623
- elsif (sub_1_w + 2 + sub_2_w) <= term_cols
3624
- p1_abs_row = (p_row_1 || 1)
3625
- sub_2_start = main_box_start
3626
- max_2_lines = [sub_1_lines.length, sub_2_start + sub_2_lines.length].max
3627
-
3628
- x1_start = margin_left.length
3629
- x1_end = x1_start + sub_1_w
3630
- x2_start = x1_end + 2
3631
- x2_end = x2_start + sub_2_w
3632
- @sub_1_col_rng = (x1_start..x1_end)
3633
- @sub_2_col_rng = (x2_start..x2_end)
3634
-
3635
- sub_1_offset = sub_1_title.empty? ? 1 : 3
3636
- sub_2_offset = sub_2_title.empty? ? 1 : 3
3637
-
3638
- stitched_lines = []
3639
- max_2_lines.times do |i|
3640
- l1 = sub_1_lines[i] || (" " * sub_1_w)
3641
- l2 = ""
3642
- br2 = ""
3643
- if i >= sub_2_start && i < (sub_2_start + sub_2_lines.length)
3644
- s2_idx = i - sub_2_start
3645
- l2 = sub_2_lines[s2_idx]
3646
- br2 = (i == p1_abs_row) ? "──" : " "
3647
- s2_item = s2_idx - sub_2_offset
3648
- @sub_2_hit_map[i + 1] = s2_item if s2_item >= 0 && s2_item < sub_2_acts.length
3649
- end
3650
- s1_item = i - sub_1_offset
3651
- if s1_item >= 0 && s1_item < sub_1_acts.length
3652
- @sub_1_hit_map[i + 1] = s1_item
3653
- @sub_hit_map[i + 1] = s1_item
3654
- end
3655
- stitched_lines << "#{margin_left}#{l1}#{br2}#{l2}".rstrip
3656
- end
3657
- rendered_lines = stitched_lines
3658
- else
3659
- active_box = (@level == 2) ? sub_2_lines : sub_1_lines
3660
- rendered_lines = active_box.map { |l| "#{margin_left}#{l}" }
3661
- end
3662
- else
3663
- total_2_w = margin_left.length + total_width + 2 + sub_1_w
3664
- if total_2_w <= term_cols
3665
- sub_start_line = main_box_start
3666
- max_total_lines = [rendered_lines.length, sub_start_line + sub_1_lines.length].max
3667
-
3668
- x1_start = margin_left.length + total_width + 2
3669
- x1_end = x1_start + sub_1_w
3670
- @sub_1_col_rng = (x1_start..x1_end)
3671
-
3672
- sub_item_offset = sub_1_title.empty? ? 1 : 3
3673
-
3674
- stitched_lines = []
3675
- max_total_lines.times do |i|
3676
- main_line = rendered_lines[i] || ("#{margin_left}#{' ' * total_width}")
3677
- if i >= sub_start_line && i < (sub_start_line + sub_1_lines.length)
3678
- sub_idx = i - sub_start_line
3679
- sub_l = sub_1_lines[sub_idx]
3680
- bridge = (i == parent_row_idx) ? "──" : " "
3681
- stitched_lines << "#{main_line}#{bridge}#{sub_l}"
3682
-
3683
- s_item_idx = sub_idx - sub_item_offset
3684
- if s_item_idx >= 0 && s_item_idx < sub_1_acts.length
3685
- @sub_1_hit_map[i + 1] = s_item_idx
3686
- @sub_hit_map[i + 1] = s_item_idx
3687
- end
3688
- else
3689
- stitched_lines << main_line
3690
- end
3691
- end
3692
- rendered_lines = stitched_lines
3693
- else
3694
- active_box = (@level == 1) ? sub_1_lines : rendered_lines
3695
- rendered_lines = active_box.map { |l| "#{margin_left}#{l}" }
3696
- end
3697
- end
3698
- end
3699
- end
3700
-
3701
- rendered_lines
3702
- end
3703
-
3704
- def has_rgb_animation?
3705
- configs = [
3706
- @style_config.border,
3707
- @style_config.options,
3708
- @style_config.focus,
3709
- @style_config.title,
3710
- @style_config.banner,
3711
- @style_config.subtitle,
3712
- @style_config.divider
3713
- ]
3714
- configs.any? do |c|
3715
- if c.is_a?(Hash)
3716
- val = (c[:color] || c["color"]).to_s.downcase
3717
- val == "rgb" || val == "rainbow" || val == "chroma"
3718
- else
3719
- false
3720
- end
3721
- end
3722
- end
3723
-
3724
- def has_active_animation?
3725
- return true if @animate && ["diagonal", "linear", "fade", "rgb", "rainbow", "chroma", "neon"].include?(@animate.to_s.downcase)
3726
- return true if has_rgb_animation?
3727
- return true if @active_tab_color && @active_tab_color.to_s.downcase.start_with?("neon")
3728
- configs = [
3729
- @style_config.border,
3730
- @style_config.options,
3731
- @style_config.focus,
3732
- @style_config.title,
3733
- @style_config.banner,
3734
- @style_config.subtitle,
3735
- @style_config.divider
3736
- ]
3737
- configs.any? do |c|
3738
- if c.is_a?(Hash)
3739
- val = (c[:color] || c["color"]).to_s.downcase.strip
3740
- val.start_with?("neon")
3741
- else
3742
- false
3743
- end
3744
- end
3745
- end
3746
-
3747
- def style(css_content = nil)
3748
- return @style if css_content.nil?
3749
- parsed = self.class.parse_config_text(css_content)
3750
- m = ((parsed[:sections] && parsed[:sections]["menu"]) || {}).merge(parsed[:global] || {})
3751
- if m["style"]
3752
- @style = m["style"].to_i
3753
- @border_config = BORDERS[@style] || BORDERS[3]
3754
- end
3755
- @banner_style = m["banner_style"].to_i if m["banner_style"]
3756
- @animate = m["animate"].to_s if m["animate"]
3757
- @center = (m["center"].to_s != "false") if m.key?("center")
3758
- if (m["border"] || m["border_color"]) && !@style_config.explicitly_set?(:border)
3759
- c, l = self.class.extract_color_and_level(m["border"] || m["border_color"], 1)
3760
- @style_config.border(c, l, from_css: true)
3761
- end
3762
- if (m["options"] || m["options_color"]) && !@style_config.explicitly_set?(:options)
3763
- c, l = self.class.extract_color_and_level(m["options"] || m["options_color"], 1)
3764
- @style_config.options(c, l, from_css: true)
3765
- end
3766
- if (m["focus"] || m["focus_color"]) && !@style_config.explicitly_set?(:focus)
3767
- c, l = self.class.extract_color_and_level(m["focus"] || m["focus_color"], 2)
3768
- @style_config.focus(c, l, from_css: true)
3769
- end
3770
- if (m["title"] || m["title_color"]) && !@style_config.explicitly_set?(:title)
3771
- c, l = self.class.extract_color_and_level(m["title"] || m["title_color"], 2)
3772
- @style_config.title(c, l, from_css: true)
3773
- end
3774
- if (m["banner"] || m["banner_color"]) && !@style_config.explicitly_set?(:banner)
3775
- c, l = self.class.extract_color_and_level(m["banner"] || m["banner_color"], 2)
3776
- @style_config.banner(c, l, from_css: true)
3777
- end
3778
- if (m["subtitle"] || m["subtitle_color"]) && !@style_config.explicitly_set?(:subtitle)
3779
- c, l = self.class.extract_color_and_level(m["subtitle"] || m["subtitle_color"], 1)
3780
- @style_config.subtitle(c, l, from_css: true)
3781
- end
3782
- if (m["divider"] || m["divider_color"]) && !@style_config.explicitly_set?(:divider)
3783
- c, l = self.class.extract_color_and_level(m["divider"] || m["divider_color"], 1)
3784
- @style_config.divider(c, l, from_css: true)
3785
- end
3786
- if (m["desc_prefix"] || m["description_prefix"] || m["prefix"]) && !@style_config.explicitly_set?(:desc_prefix)
3787
- @style_config.desc_prefix(m["desc_prefix"] || m["description_prefix"] || m["prefix"], from_css: true)
3788
- end
3789
- if m["font"] && !@style_config.explicitly_set?(:font)
3790
- @style_config.font(m["font"].to_i, from_css: true)
3791
- end
3792
- @mouse = (m["mouse"].to_s == "true") if m.key?("mouse")
3793
- if parsed[:sections] && parsed[:sections]["tabs"]
3794
- t_sec = parsed[:sections]["tabs"]
3795
- @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"])
3796
- @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"])
3797
- end
3798
- self
3799
- end
3800
-
3801
- def style=(val)
3802
- if val.is_a?(String) && (val.include?("::") || val.include?("<<") || val.include?("@theme"))
3803
- style(val)
3804
- else
3805
- @style = val.to_i
3806
- @border_config = BORDERS[@style] || BORDERS[3]
3807
- end
3808
- end
3809
-
3810
- def export_config(path = nil)
3811
- if path.nil?
3812
- caller_loc = caller_locations.find { |c| !c.path.include?(__FILE__) }
3813
- base = caller_loc ? caller_loc.path.sub(/\.rb$/, '') : "theme"
3814
- path = "#{base}.gr"
3815
- end
3816
- b_cfg = @style_config&.border || SetStyle.border
3817
- t_cfg = @style_config&.title || SetStyle.title
3818
- f_cfg = @style_config&.focus || SetStyle.focus
3819
- o_cfg = @style_config&.options || SetStyle.options
3820
- bn_cfg = @style_config&.banner || SetStyle.banner
3821
- s_cfg = @style_config&.subtitle || SetStyle.subtitle
3822
- d_cfg = @style_config&.divider || SetStyle.divider
3823
- dp_val = @style_config&.desc_prefix || SetStyle.desc_prefix
3824
-
3825
- lines = ["GRmenu::config<-1->", ""]
3826
- lines << "@theme:: \"#{File.basename(path, '.gr').capitalize}\""
3827
- lines << "@author:: \"grcode\""
3828
- lines << "@version:: \"1.0\""
3829
- lines << ""
3830
- lines << "<<menu"
3831
- lines << " style:: #{@style || 3}"
3832
- lines << " banner_style:: #{@banner_style || 3}"
3833
- lines << " font:: #{@style_config&.font || SetStyle.font}"
3834
- lines << " animate:: #{@animate || 'rgb'}"
3835
- lines << " center:: #{@center.nil? ? true : @center}"
3836
- lines << " desc_prefix:: #{dp_val}"
3837
- lines << " mouse:: #{@mouse}" if @mouse
3838
- lines << " border:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3839
- lines << " title:: #{t_cfg[:color]}:#{t_cfg[:level]}"
3840
- lines << " focus:: #{f_cfg[:color]}:#{f_cfg[:level]}"
3841
- lines << " options:: #{o_cfg[:color]}:#{o_cfg[:level]}"
3842
- lines << " banner:: #{bn_cfg[:color]}:#{bn_cfg[:level]}"
3843
- lines << " subtitle:: #{s_cfg[:color]}:#{s_cfg[:level]}"
3844
- lines << " divider:: #{d_cfg[:color]}:#{d_cfg[:level]}"
3845
- lines << ">>"
3846
- lines << ""
3847
- lines << "<<submenu"
3848
- lines << " style:: #{@style || 3}"
3849
- lines << " border:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3850
- lines << " focus:: #{f_cfg[:color]}:#{f_cfg[:level]}"
3851
- lines << " options:: #{o_cfg[:color]}:#{o_cfg[:level]}"
3852
- lines << ">>"
3853
- lines << ""
3854
- lines << "<<tabs"
3855
- lines << " active_tab:: #{@active_tab_color || 'yellow'}:2"
3856
- lines << " tab_color:: #{@tab_color || 'gray'}:1"
3857
- lines << ">>"
3858
- lines << ""
3859
- lines << "<<input"
3860
- lines << " style:: 3"
3861
- lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3862
- lines << " title_color:: #{t_cfg[:color]}:#{t_cfg[:level]}"
3863
- lines << " label_color:: white:1"
3864
- lines << ">>"
3865
- lines << ""
3866
- lines << "<<table"
3867
- lines << " style:: #{@style || 3}"
3868
- lines << " header_color:: yellow:2"
3869
- lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3870
- lines << " selected_row:: #{f_cfg[:color]}:#{f_cfg[:level]}"
3871
- lines << " row_color:: white:1"
3872
- lines << " zebra_striping:: true"
3873
- lines << ">>"
3874
- lines << ""
3875
- lines << "<<card"
3876
- lines << " style:: 7"
3877
- lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3878
- lines << " title_color:: #{t_cfg[:color]}:#{t_cfg[:level]}"
3879
- lines << " content_color:: white:1"
3880
- lines << ">>"
3881
- lines << ""
3882
- lines << "<<slider"
3883
- lines << " style:: #{@style || 3}"
3884
- lines << " color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3885
- lines << " fill_char:: █"
3886
- lines << " empty_char:: ░"
3887
- lines << ">>"
3888
- lines << ""
3889
- lines << "<<checkbox"
3890
- lines << " style:: #{@style || 3}"
3891
- lines << " color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
3892
- lines << " checked_mark:: [X]"
3893
- lines << " unchecked_mark:: [ ]"
3894
- lines << ">>"
3895
- lines << ""
3896
- File.write(path, lines.join("\n") + "\n")
3897
- path
3898
- end
3899
- alias_method :export_theme, :export_config
3900
-
3901
- def draw(size_max: 20, min_width: nil)
3902
- if ARGV.any? { |a| ["-theme", "--theme", "-ex", "--export-theme"].include?(a.to_s.downcase) }
3903
- out_idx = ARGV.index { |a| ["-o", "--out", "--output"].include?(a.to_s.downcase) }
3904
- target_file = out_idx ? ARGV[out_idx + 1] : "tema_exportado.gr"
3905
- export_config(target_file)
3906
- Kernel.puts Color.bright_green("[OK] Tema exportado exitosamente a: #{target_file}")
3907
- exit(0)
3908
- end
3909
-
3910
- target_width = min_width || size_max || 20
3911
- action_to_execute = nil
3912
-
3913
- self.class.enable_windows_vt
3914
- $stdout.sync = true
3915
-
3916
- is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
3917
-
3918
- begin
3919
- Kernel.print("#{HIDE_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
3920
- Kernel.print(ENABLE_MOUSE) if @mouse
3921
- $stdout.flush
3922
-
3923
- if @animate && !["false", "rgb", "", "nil"].include?(@animate.downcase)
3924
- intro_lines = render_lines(target_width)
3925
- self.class.animate_render(intro_lines, @animate)
3926
- end
3927
-
3928
- if is_tty
3929
- $stdin.raw do |raw_input_stream|
3930
- action_to_execute = run_interactive_loop(raw_input_stream, target_width)
3931
- end
3932
- else
3933
- action_to_execute = run_interactive_loop($stdin, target_width)
3934
- end
3935
- ensure
3936
- Kernel.print(DISABLE_MOUSE) if @mouse
3937
- Kernel.print(SHOW_CURSOR)
3938
- $stdout.flush
3939
- end
3940
-
3941
- if action_to_execute
3942
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
3943
- $stdout.flush
3944
- execute_action(action_to_execute)
3945
- else
3946
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
3947
- $stdout.flush
3948
- end
3949
- rescue Interrupt
3950
- Kernel.print(DISABLE_MOUSE) if @mouse
3951
- Kernel.print("#{SHOW_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
3952
- $stdout.flush
3953
- nil
3954
- end
3955
-
3956
- private
3957
-
3958
- def draw_frame(target_width)
3959
- lines = render_lines(target_width)
3960
- buffer = String.new(CURSOR_HOME)
3961
- lines.each_with_index do |line, idx|
3962
- buffer << line << CLEAR_TO_EOL
3963
- buffer << "\r\n" if idx < lines.length - 1
3964
- end
3965
- buffer << CLEAR_TO_EOS
3966
- Kernel.print(buffer)
3967
- $stdout.flush
3968
- end
3969
-
3970
- def run_interactive_loop(input_stream, target_width)
3971
- matching = current_matching_indices
3972
- @index = matching.first || 0 unless matching.include?(@index)
3973
- @rgb_tick = 0.0
3974
- draw_frame(target_width)
3975
-
3976
- animating = has_active_animation?
3977
-
3978
- while true
3979
- if animating
3980
- ready = false
3981
- if input_stream.respond_to?(:to_io) || input_stream.is_a?(IO)
3982
- begin
3983
- select_res = IO.select([input_stream], nil, nil, 0.035)
3984
- ready = true if select_res && select_res[0] && !select_res[0].empty?
3985
- rescue StandardError
3986
- ready = true
3987
- end
3988
- else
3989
- ready = true
3990
- end
3991
-
3992
- unless ready
3993
- @rgb_tick += 0.08
3994
- draw_frame(target_width)
3995
- next
3996
- end
3997
- end
3998
-
3999
- key = read_single_key(input_stream)
4000
- break if key.nil? || key == "\x03" || key == "\x04"
4001
-
4002
- if key =~ /\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/
4003
- btn = $1.to_i
4004
- col = $2.to_i
4005
- row = $3.to_i
4006
- act = $4
4007
-
4008
- if btn == 64
4009
- if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
4010
- s1_acts = get_submenu_actions(@functions[@index])
4011
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
4012
- @sub_index_2 = (@sub_index_2 - 1) % s2_acts.length if s2_acts && !s2_acts.empty?
4013
- @level = 2
4014
- elsif @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
4015
- s1_acts = get_submenu_actions(@functions[@index])
4016
- @sub_index_1 = (@sub_index_1 - 1) % s1_acts.length if s1_acts && !s1_acts.empty?
4017
- @sub_index_2 = 0
4018
- @level = 1
4019
- else
4020
- move_up
4021
- @sub_index_1 = 0
4022
- @sub_index_2 = 0
4023
- @level = 0
4024
- end
4025
- draw_frame(target_width)
4026
- next
4027
- elsif btn == 65
4028
- if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
4029
- s1_acts = get_submenu_actions(@functions[@index])
4030
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
4031
- @sub_index_2 = (@sub_index_2 + 1) % s2_acts.length if s2_acts && !s2_acts.empty?
4032
- @level = 2
4033
- elsif @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
4034
- s1_acts = get_submenu_actions(@functions[@index])
4035
- @sub_index_1 = (@sub_index_1 + 1) % s1_acts.length if s1_acts && !s1_acts.empty?
4036
- @sub_index_2 = 0
4037
- @level = 1
4038
- else
4039
- move_down
4040
- @sub_index_1 = 0
4041
- @sub_index_2 = 0
4042
- @level = 0
4043
- end
4044
- draw_frame(target_width)
4045
- next
4046
- elsif btn == 0 && act == "M"
4047
- if @tabs && !@tabs.empty? && @tabs_row && row == @tabs_row
4048
- clicked_tab = @tab_ranges.find { |_idx, rng| rng.cover?(col) }
4049
- if clicked_tab
4050
- @active_tab_idx = clicked_tab[0]
4051
- @functions = @tab_contents[@tabs[@active_tab_idx]] || []
4052
- @index = 0
4053
- @level = 0
4054
- @open_level = 0
4055
- @sub_index_1 = 0
4056
- @sub_index_2 = 0
4057
- @active_panel = :main
4058
- @submenu_open = false
4059
- draw_frame(target_width)
4060
- next
4061
- end
4062
- end
4063
-
4064
- if @up_arrow_row && row == @up_arrow_row
4065
- move_up
4066
- draw_frame(target_width)
4067
- next
4068
- end
4069
-
4070
- if @down_arrow_row && row == @down_arrow_row
4071
- move_down
4072
- draw_frame(target_width)
4073
- next
4074
- end
4075
-
4076
- if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && @sub_2_hit_map[row]
4077
- s2_clicked = @sub_2_hit_map[row]
4078
- s1_acts = get_submenu_actions(@functions[@index])
4079
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
4080
- if s2_acts && s2_clicked < s2_acts.length
4081
- @level = 2
4082
- @sub_index_2 = s2_clicked
4083
- return s2_acts[s2_clicked]
4084
- end
4085
- end
4086
-
4087
- if @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && @sub_1_hit_map[row]
4088
- s1_clicked = @sub_1_hit_map[row]
4089
- s1_acts = get_submenu_actions(@functions[@index])
4090
- if s1_acts && s1_clicked < s1_acts.length
4091
- t_act = s1_acts[s1_clicked]
4092
- @level = 1
4093
- @sub_index_1 = s1_clicked
4094
- if is_submenu_item?(t_act)
4095
- @open_level = 2
4096
- @level = 2
4097
- @sub_index_2 = 0
4098
- draw_frame(target_width)
4099
- next
4100
- else
4101
- return t_act
4102
- end
4103
- end
4104
- end
4105
-
4106
- if @row_hit_map && @row_hit_map[row]
4107
- hit = @row_hit_map[row]
4108
- x_in_box = col - hit[:margin_left]
4109
- if x_in_box > 0 && (hit[:total_w].nil? || x_in_box <= hit[:total_w])
4110
- c_idx = [[((x_in_box - 2) / ([hit[:col_w], 1].max + 2)).to_i, 0].max, hit[:cols] - 1].min
4111
- clicked_item = hit[:row_indices][c_idx]
4112
- if clicked_item
4113
- @index = clicked_item
4114
- if is_submenu_item?(@functions[clicked_item])
4115
- @open_level = 1
4116
- @level = 1
4117
- @sub_index_1 = 0
4118
- @sub_index_2 = 0
4119
- @active_panel = :sub
4120
- @submenu_open = true
4121
- draw_frame(target_width)
4122
- next
4123
- else
4124
- return @functions[clicked_item]
4125
- end
4126
- end
4127
- end
4128
- end
4129
- end
4130
- next
4131
- end
4132
-
4133
- if @tabs && !@tabs.empty?
4134
- if key == "\t"
4135
- @active_tab_idx = (@active_tab_idx + 1) % @tabs.length
4136
- @functions = @tab_contents[@tabs[@active_tab_idx]] || []
4137
- @index = 0
4138
- @level = 0
4139
- @open_level = 0
4140
- @sub_index_1 = 0
4141
- @sub_index_2 = 0
4142
- @active_panel = :main
4143
- @submenu_open = false
4144
- draw_frame(target_width)
4145
- next
4146
- elsif key == "\e[Z"
4147
- @active_tab_idx = (@active_tab_idx - 1) % @tabs.length
4148
- @functions = @tab_contents[@tabs[@active_tab_idx]] || []
4149
- @index = 0
4150
- @level = 0
4151
- @open_level = 0
4152
- @sub_index_1 = 0
4153
- @sub_index_2 = 0
4154
- @active_panel = :main
4155
- @submenu_open = false
4156
- draw_frame(target_width)
4157
- next
4158
- end
4159
- end
4160
-
4161
- if @level == 2 && @open_level >= 2 && is_submenu_item?(@functions[@index])
4162
- s1_acts = get_submenu_actions(@functions[@index])
4163
- s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
4164
- if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
4165
- @sub_index_2 = (@sub_index_2 - 1) % s2_acts.length if s2_acts && !s2_acts.empty?
4166
- draw_frame(target_width)
4167
- next
4168
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
4169
- @sub_index_2 = (@sub_index_2 + 1) % s2_acts.length if s2_acts && !s2_acts.empty?
4170
- draw_frame(target_width)
4171
- next
4172
- elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K" || key == "\e"
4173
- @open_level = 1
4174
- @level = 1
4175
- draw_frame(target_width)
4176
- next
4177
- elsif key == "\r" || key == "\n"
4178
- return s2_acts[@sub_index_2] if s2_acts && @sub_index_2 < s2_acts.length
4179
- end
4180
- end
4181
-
4182
- if @level == 1 && @open_level >= 1 && is_submenu_item?(@functions[@index])
4183
- s1_acts = get_submenu_actions(@functions[@index])
4184
- if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
4185
- @sub_index_1 = (@sub_index_1 - 1) % s1_acts.length if s1_acts && !s1_acts.empty?
4186
- @sub_index_2 = 0
4187
- draw_frame(target_width)
4188
- next
4189
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
4190
- @sub_index_1 = (@sub_index_1 + 1) % s1_acts.length if s1_acts && !s1_acts.empty?
4191
- @sub_index_2 = 0
4192
- draw_frame(target_width)
4193
- next
4194
- elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K" || key == "\e"
4195
- @open_level = 0
4196
- @level = 0
4197
- @active_panel = :main
4198
- @submenu_open = false
4199
- draw_frame(target_width)
4200
- next
4201
- elsif key == "\e[C" || key == "\eOC" || key == "\xe0M" || key == "\x00M"
4202
- cur_l1 = s1_acts[@sub_index_1] if s1_acts
4203
- if is_submenu_item?(cur_l1)
4204
- @open_level = 2
4205
- @level = 2
4206
- @sub_index_2 = 0
4207
- draw_frame(target_width)
4208
- next
4209
- end
4210
- elsif key == "\r" || key == "\n"
4211
- cur_l1 = s1_acts[@sub_index_1] if s1_acts
4212
- if is_submenu_item?(cur_l1)
4213
- @open_level = 2
4214
- @level = 2
4215
- @sub_index_2 = 0
4216
- draw_frame(target_width)
4217
- next
4218
- else
4219
- return cur_l1
4220
- end
4221
- end
4222
- end
4223
-
4224
- if !@search && (key == "q" || key == "Q")
4225
- break
4226
- end
4227
-
4228
- if key == "\e"
4229
- if @search && !@query.empty?
4230
- @query.clear
4231
- matching = current_matching_indices
4232
- @index = matching.first || 0
4233
- draw_frame(target_width)
4234
- else
4235
- break
4236
- end
4237
- elsif key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
4238
- move_up
4239
- @sub_index_1 = 0
4240
- @sub_index_2 = 0
4241
- draw_frame(target_width)
4242
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
4243
- move_down
4244
- @sub_index_1 = 0
4245
- @sub_index_2 = 0
4246
- draw_frame(target_width)
4247
- elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K"
4248
- if @open_level > 0
4249
- @open_level = 0
4250
- @level = 0
4251
- @active_panel = :main
4252
- @submenu_open = false
4253
- draw_frame(target_width)
4254
- else
4255
- move_left
4256
- draw_frame(target_width)
4257
- end
4258
- elsif key == "\e[C" || key == "\eOC" || key == "\xe0M" || key == "\x00M"
4259
- if is_submenu_item?(@functions[@index])
4260
- @open_level = 1
4261
- @level = 1
4262
- @sub_index_1 = 0
4263
- @sub_index_2 = 0
4264
- @active_panel = :sub
4265
- @submenu_open = true
4266
- draw_frame(target_width)
4267
- else
4268
- move_right
4269
- draw_frame(target_width)
4270
- end
4271
- elsif key == "\x7f" || key == "\b" || key == "\x08"
4272
- if @search && !@query.empty?
4273
- @query.chop!
4274
- matching = current_matching_indices
4275
- @index = matching.first || 0
4276
- draw_frame(target_width)
4277
- end
4278
- elsif key == "\x15"
4279
- if @search
4280
- @query.clear
4281
- matching = current_matching_indices
4282
- @index = matching.first || 0
4283
- draw_frame(target_width)
4284
- end
4285
- elsif key == "\r" || key == "\n"
4286
- matching = current_matching_indices
4287
- if matching.include?(@index)
4288
- if is_submenu_item?(@functions[@index])
4289
- @open_level = 1
4290
- @level = 1
4291
- @sub_index_1 = 0
4292
- @sub_index_2 = 0
4293
- @active_panel = :sub
4294
- @submenu_open = true
4295
- draw_frame(target_width)
4296
- else
4297
- return @functions[@index]
4298
- end
4299
- end
4300
- elsif @search && key =~ /^[[:print:]]$/
4301
- @query << key
4302
- matching = current_matching_indices
4303
- @index = matching.first || 0
4304
- draw_frame(target_width)
4305
- end
4306
- end
4307
-
4308
- nil
4309
- end
4310
-
4311
- def read_single_key(input_stream)
4312
- GRmenu.read_key_raw(input_stream)
4313
- end
4314
-
4315
- def format_auto_name(raw_name)
4316
- cleaned = raw_name.to_s.gsub(/[_-]+/, ' ').strip
4317
- cleaned.split(' ').map(&:capitalize).join(' ')
4318
- end
4319
-
4320
- def extract_name_from_action(action)
4321
- case action
4322
- when Array
4323
- action[0].to_s
4324
- when Hash
4325
- (action[:name] || action[:title] || action["name"] || action["title"] || "Opcion").to_s
4326
- when Method
4327
- format_auto_name(action.name)
4328
- when Symbol
4329
- format_auto_name(action)
4330
- when Proc
4331
- if action.respond_to?(:name) && action.name
4332
- format_auto_name(action.name)
4333
- else
4334
- "Opcion"
4335
- end
4336
- else
4337
- if action.respond_to?(:name)
4338
- format_auto_name(action.name)
4339
- elsif action.respond_to?(:title)
4340
- action.title.to_s
4341
- else
4342
- format_auto_name(action)
4343
- end
4344
- end
4345
- end
4346
-
4347
- def extract_description_from_action(action)
4348
- if action.is_a?(Array) && action.length >= 3
4349
- action[2].to_s
4350
- elsif action.is_a?(Hash)
4351
- (action[:desc] || action[:description] || action["desc"] || action["description"]).to_s
4352
- else
4353
- ""
4354
- end
4355
- end
4356
-
4357
- def execute_action(action)
4358
- case action
4359
- when Method, Proc
4360
- action.call
4361
- when Symbol
4362
- if Object.respond_to?(action, true)
4363
- Object.send(action)
4364
- elsif Kernel.respond_to?(action, true)
4365
- Kernel.send(action)
4366
- end
4367
- when Array
4368
- callable = action[1]
4369
- if callable.is_a?(Symbol)
4370
- if Object.respond_to?(callable, true)
4371
- Object.send(callable)
4372
- elsif Kernel.respond_to?(callable, true)
4373
- Kernel.send(callable)
4374
- end
4375
- elsif callable.respond_to?(:call)
4376
- callable.call
4377
- end
4378
- when Hash
4379
- callable = action[:action] || action[:call] || action["action"] || action["call"]
4380
- if callable.is_a?(Symbol)
4381
- if Object.respond_to?(callable, true)
4382
- Object.send(callable)
4383
- elsif Kernel.respond_to?(callable, true)
4384
- Kernel.send(callable)
4385
- end
4386
- elsif callable.respond_to?(:call)
4387
- callable.call
4388
- end
4389
- else
4390
- 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"]
4391
116
  end
4392
117
  end
118
+ BASE_RGB.freeze
4393
119
  end
4394
120
 
4395
- Color = GRmenu::Color unless defined?(Color)
4396
- Colors = GRmenu::Color unless defined?(Colors)
4397
- C = GRmenu::Color unless defined?(C)
4398
- 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)