grmenu 0.1.6 → 2.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/GRmenu.rb +381 -91
  3. data/README.md +121 -77
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f46b9abdef35e533292871a7a45455814d6d5ad1251e344f18f0cf76361b02f
4
- data.tar.gz: 562005a4a30a236ff2dfa139deb44ad7402838d31623acfc9a5862f1fbf322c8
3
+ metadata.gz: 9321761865c19b47e92b7fb2ed1808a23184d65a65cfef033b6f2129537512f9
4
+ data.tar.gz: f6ff6e9057c2dee31a7af652b74b9dd88eac8b8b13b05638956516ecafdfb2af
5
5
  SHA512:
6
- metadata.gz: a597007a74ea9bb27e1f290c0ba01e0ba081783f8caa9656b1a084511ce5bc3aebc890e0d410e5a2afe78a751e550638064648a5022e6678d4bc51ff10ea0ab9
7
- data.tar.gz: 27337525d19288c49cd5c2061768338d07352c4e228525d9c696164b85039c1a527373e6c848ce5be3c103ee84e7e2768f89970cf9ec4e61bfdc35d409cdb879
6
+ metadata.gz: 27cbe736aba023bf6f5cda2063c5dae971127a485b0b441d6600aa211f2d6734a04917d638e9795bb4106e8a52acdf1f9a3f0f1eebea155949e1999f4ad06b2d
7
+ data.tar.gz: 86c3b3d6de6c6fff5dc4959743c87089173701d52edaaf6613de8381574c954c02ae6a8caab517d24a19f8e78e1882ba35189020b67cd32dca96eefa933ede60
data/GRmenu.rb CHANGED
@@ -39,55 +39,55 @@ class GRmenu
39
39
  "#{code}#{text}#{RESET}"
40
40
  end
41
41
 
42
- def red(s) = paint(s, :red, 1)
43
- def bright_red(s) = paint(s, :red, 2)
44
- def dark_red(s) = paint(s, :red, 1)
42
+ def red(s); paint(s, :red, 1); end
43
+ def bright_red(s); paint(s, :red, 2); end
44
+ def dark_red(s); paint(s, :red, 1); end
45
45
 
46
- def green(s) = paint(s, :green, 1)
47
- def bright_green(s) = paint(s, :green, 2)
48
- def dark_green(s) = paint(s, :green, 1)
46
+ def green(s); paint(s, :green, 1); end
47
+ def bright_green(s); paint(s, :green, 2); end
48
+ def dark_green(s); paint(s, :green, 1); end
49
49
 
50
- def yellow(s) = paint(s, :yellow, 1)
51
- def bright_yellow(s) = paint(s, :yellow, 2)
50
+ def yellow(s); paint(s, :yellow, 1); end
51
+ def bright_yellow(s); paint(s, :yellow, 2); end
52
52
 
53
- def blue(s) = paint(s, :blue, 1)
54
- def bright_blue(s) = paint(s, :blue, 2)
53
+ def blue(s); paint(s, :blue, 1); end
54
+ def bright_blue(s); paint(s, :blue, 2); end
55
55
 
56
- def magenta(s) = paint(s, :magenta, 1)
57
- def bright_magenta(s) = paint(s, :magenta, 2)
56
+ def magenta(s); paint(s, :magenta, 1); end
57
+ def bright_magenta(s); paint(s, :magenta, 2); end
58
58
 
59
- def purple(s) = paint(s, :purple, 1)
60
- def bright_purple(s) = paint(s, :purple, 2)
59
+ def purple(s); paint(s, :purple, 1); end
60
+ def bright_purple(s); paint(s, :purple, 2); end
61
61
 
62
- def pink(s) = paint(s, :pink, 1)
63
- def bright_pink(s) = paint(s, :pink, 2)
62
+ def pink(s); paint(s, :pink, 1); end
63
+ def bright_pink(s); paint(s, :pink, 2); end
64
64
 
65
- def cyan(s) = paint(s, :cyan, 1)
66
- def bright_cyan(s) = paint(s, :cyan, 2)
65
+ def cyan(s); paint(s, :cyan, 1); end
66
+ def bright_cyan(s); paint(s, :cyan, 2); end
67
67
 
68
- def aqua(s) = paint(s, :aqua, 1)
69
- def bright_aqua(s) = paint(s, :aqua, 2)
68
+ def aqua(s); paint(s, :aqua, 1); end
69
+ def bright_aqua(s); paint(s, :aqua, 2); end
70
70
 
71
- def orange(s) = paint(s, :orange, 1)
72
- def bright_orange(s) = paint(s, :orange, 2)
71
+ def orange(s); paint(s, :orange, 1); end
72
+ def bright_orange(s); paint(s, :orange, 2); end
73
73
 
74
- def white(s) = paint(s, :white, 1)
75
- def bright_white(s) = paint(s, :white, 2)
74
+ def white(s); paint(s, :white, 1); end
75
+ def bright_white(s); paint(s, :white, 2); end
76
76
 
77
- def black(s) = paint(s, :black, 1)
78
- def gray(s) = paint(s, :gray, 1)
79
- def bright_gray(s) = paint(s, :gray, 2)
80
- def grey(s) = gray(s)
77
+ def black(s); paint(s, :black, 1); end
78
+ def gray(s); paint(s, :gray, 1); end
79
+ def bright_gray(s); paint(s, :gray, 2); end
80
+ def grey(s); gray(s); end
81
81
 
82
- def r(s) = bright_red(s)
83
- def dr(s) = dark_red(s)
84
- def g(s) = bright_green(s)
85
- def y(s) = bright_yellow(s)
86
- def w(s) = bright_white(s)
87
- def gr(s) = gray(s)
88
- def cy(s) = bright_cyan(s)
89
- def mg(s) = bright_magenta(s)
90
- def bl(s) = bright_blue(s)
82
+ def r(s); bright_red(s); end
83
+ def dr(s); dark_red(s); end
84
+ def g(s); bright_green(s); end
85
+ def y(s); bright_yellow(s); end
86
+ def w(s); bright_white(s); end
87
+ def gr(s); gray(s); end
88
+ def cy(s); bright_cyan(s); end
89
+ def mg(s); bright_magenta(s); end
90
+ def bl(s); bright_blue(s); end
91
91
  end
92
92
  C = Color
93
93
 
@@ -162,6 +162,148 @@ class GRmenu
162
162
  FONTS.each { |id, data| const_set("FONT_#{id}", data) }
163
163
  FONT = FONTS[1] || {}.freeze
164
164
 
165
+ class ProgressBar
166
+ attr_reader :total, :current, :title, :status
167
+
168
+ def initialize(total = 100, title: nil, color: "cyan", level: 2, style: 3, width: nil)
169
+ @total = [total.to_i, 1].max
170
+ @current = 0
171
+ @title = title
172
+ @status = ""
173
+ @color = color.to_s.downcase
174
+ @level = level.to_i
175
+ @style = style.to_i
176
+ @width = width
177
+ @closed = false
178
+ @drawn_lines_count = 0
179
+ end
180
+
181
+ def advance(step = 1, status: nil)
182
+ return if @closed
183
+ @current = [(@current + step), @total].min
184
+ @status = status.to_s if status
185
+ render
186
+ end
187
+ alias_method :increment, :advance
188
+ alias_method :step, :advance
189
+
190
+ def set(value, status: nil)
191
+ return if @closed
192
+ @current = [[value.to_i, 0].max, @total].min
193
+ @status = status.to_s if status
194
+ render
195
+ end
196
+
197
+ def render
198
+ term_w = GRmenu.terminal_width
199
+ box_w = @width || [term_w - 4, 60].min
200
+ box_w = [box_w, 36].max
201
+
202
+ color_code = GRmenu::COLORS.dig(@color, @level) || "\e[1;96m"
203
+ reset_code = GRmenu::COLORS["reset"]
204
+ border_cfg = GRmenu::BORDERS[@style] || GRmenu::BORDERS[3]
205
+
206
+ v_l = border_cfg[:vl] || border_cfg[:v]
207
+ v_r = border_cfg[:vr] || border_cfg[:v]
208
+ h_t = border_cfg[:ht] || border_cfg[:h]
209
+ h_b = border_cfg[:hb] || border_cfg[:h]
210
+
211
+ top_fill = (h_t * ((box_w - 2).to_f / h_t.length).ceil)[0...(box_w - 2)]
212
+ bot_fill = (h_b * ((box_w - 2).to_f / h_b.length).ceil)[0...(box_w - 2)]
213
+
214
+ pct = ((@current.to_f / @total) * 100).round
215
+ pct_str = "#{pct}% (#{@current}/#{@total})"
216
+
217
+ inner_w = box_w - 4
218
+ bar_w = [inner_w - pct_str.length - 3, 10].max
219
+ filled_len = ((@current.to_f / @total) * bar_w).round
220
+ empty_len = bar_w - filled_len
221
+
222
+ bar_str = "[#{"█" * filled_len}#{"░" * empty_len}] #{pct_str}"
223
+ bar_line = bar_str.ljust(inner_w)[0...inner_w]
224
+
225
+ lines = []
226
+ lines << "#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}"
227
+ if @title && !@title.empty?
228
+ lines << "#{color_code}#{v_l}#{reset_code} #{@title.center(inner_w)} #{color_code}#{v_r}#{reset_code}"
229
+ lines << "#{color_code}#{v_l}#{top_fill}#{v_r}#{reset_code}"
230
+ end
231
+ lines << "#{color_code}#{v_l}#{reset_code} #{color_code}#{bar_line}#{reset_code} #{color_code}#{v_r}#{reset_code}"
232
+ if @status && !@status.empty?
233
+ stat_line = @status.ljust(inner_w)[0...inner_w]
234
+ lines << "#{color_code}#{v_l}#{reset_code} #{Color.gray(stat_line)} #{color_code}#{v_r}#{reset_code}"
235
+ end
236
+ lines << "#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}"
237
+
238
+ frame = lines.join("\r\n") + "\r\n"
239
+
240
+ if @drawn_lines_count && @drawn_lines_count > 0
241
+ Kernel.print("\e[#{@drawn_lines_count}A\e[J")
242
+ end
243
+ Kernel.print(frame)
244
+ $stdout.flush
245
+ @drawn_lines_count = lines.length
246
+ end
247
+
248
+ def finish(status: "¡Completado!")
249
+ return if @closed
250
+ set(@total, status: status)
251
+ @closed = true
252
+ Kernel.print(GRmenu::SHOW_CURSOR)
253
+ end
254
+ end
255
+
256
+ def self.spinner(message = "Cargando...", color: "cyan", level: 2, delay: 0.08, &block)
257
+ frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
258
+ color_name = color.to_s.downcase
259
+ color_code = COLORS.dig(color_name, level) || "\e[1;96m"
260
+ reset_code = COLORS["reset"]
261
+
262
+ stop_spinner = false
263
+ spinner_thread = Thread.new do
264
+ frame_idx = 0
265
+ while !stop_spinner
266
+ f = frames[frame_idx % frames.length]
267
+ Kernel.print("\r\e[K#{color_code}#{f}#{reset_code} #{message}")
268
+ $stdout.flush
269
+ frame_idx += 1
270
+ sleep(delay)
271
+ end
272
+ end
273
+
274
+ begin
275
+ Kernel.print(HIDE_CURSOR)
276
+ result = block ? block.call : nil
277
+ stop_spinner = true
278
+ spinner_thread.join
279
+ success_color = COLORS.dig("green", 2) || "\e[1;92m"
280
+ Kernel.print("\r\e[K#{success_color}✔#{reset_code} #{message} #{Color.gray("¡Listo!")}\r\n")
281
+ result
282
+ rescue Exception => e
283
+ stop_spinner = true
284
+ spinner_thread.join rescue nil
285
+ error_color = COLORS.dig("red", 2) || "\e[1;91m"
286
+ Kernel.print("\r\e[K#{error_color}✖#{reset_code} #{message} #{Color.bright_red("(Error: #{e.message})")}\r\n")
287
+ raise e
288
+ ensure
289
+ stop_spinner = true
290
+ Kernel.print(SHOW_CURSOR)
291
+ end
292
+ end
293
+
294
+ def self.progress(total = 100, title: nil, color: "cyan", level: 2, style: 3, width: nil, &block)
295
+ bar = ProgressBar.new(total, title: title, color: color, level: level, style: style, width: width)
296
+ Kernel.print(HIDE_CURSOR)
297
+ bar.render
298
+ begin
299
+ result = block ? block.call(bar) : bar
300
+ bar.finish
301
+ result
302
+ ensure
303
+ Kernel.print(SHOW_CURSOR)
304
+ end
305
+ end
306
+
165
307
  class SetStyle
166
308
  def initialize(
167
309
  border: { color: "cyan", level: 1 },
@@ -333,7 +475,7 @@ class GRmenu
333
475
  end
334
476
  end
335
477
 
336
- attr_accessor :functions, :title, :subtitle, :banner, :banner_style, :divider, :style, :index, :style_config, :center
478
+ attr_accessor :functions, :title, :subtitle, :banner, :banner_style, :divider, :style, :index, :style_config, :center, :page_size
337
479
 
338
480
  alias_method :options, :functions
339
481
  alias_method :options=, :functions=
@@ -344,11 +486,11 @@ class GRmenu
344
486
  alias_method :description, :subtitle
345
487
  alias_method :description=, :subtitle=
346
488
 
347
- def self.STYLES = STYLES
348
- def self.COLORS = COLORS
349
- def self.BORDERS = BORDERS
350
- def self.FONTS = FONTS
351
- def self.FONT = FONT_1
489
+ def self.STYLES; STYLES; end
490
+ def self.COLORS; COLORS; end
491
+ def self.BORDERS; BORDERS; end
492
+ def self.FONTS; FONTS; end
493
+ def self.FONT; FONT_1; end
352
494
 
353
495
  def self.terminal_width
354
496
  cols = $stdout.winsize[1] rescue nil
@@ -358,6 +500,14 @@ class GRmenu
358
500
  80
359
501
  end
360
502
 
503
+ def self.terminal_height
504
+ rows = $stdout.winsize[0] rescue nil
505
+ rows = $stdin.winsize[0] rescue nil if rows.nil? || rows <= 0
506
+ (rows && rows > 0) ? rows : (ENV['LINES'] ? ENV['LINES'].to_i : 24)
507
+ rescue StandardError
508
+ 24
509
+ end
510
+
361
511
  def self.clear_screen
362
512
  Kernel.print(CLEAR_SCREEN_SEQUENCE)
363
513
  end
@@ -371,20 +521,20 @@ class GRmenu
371
521
  Kernel.print("#{color_code}#{char * width}#{COLORS['reset']}\r\n")
372
522
  end
373
523
 
374
- def self.help
375
- w = [terminal_width - 4, 64].min
376
- w = [w, 42].max
524
+ def self.help(section = :all)
525
+ w = [terminal_width - 4, 70].min
526
+ w = [w, 46].max
377
527
  inner_w = w - 2
378
528
  h_line = "═" * inner_w
379
529
  s_line = "─" * w
380
530
 
381
531
  Kernel.print "\r\n"
382
532
  Kernel.print "#{Color.bright_cyan("╔" + h_line + "╗")}\r\n"
383
- Kernel.print "#{Color.bright_cyan("║")}#{Color.bright_yellow("GRmenu - Guia y Referencia Rapida".center(inner_w))}#{Color.bright_cyan("║")}\r\n"
384
- Kernel.print "#{Color.bright_cyan("║")}#{Color.gray("Navegacion interactiva en terminal TTY".center(inner_w))}#{Color.bright_cyan("║")}\r\n"
533
+ Kernel.print "#{Color.bright_cyan("║")}#{Color.bright_yellow("GRmenu - Guia y Referencia Completa (v2.0)".center(inner_w))}#{Color.bright_cyan("║")}\r\n"
534
+ Kernel.print "#{Color.bright_cyan("║")}#{Color.gray("Menus interactivos, Banners 3D, Barras de Progreso y TTY".center(inner_w))}#{Color.bright_cyan("║")}\r\n"
385
535
  Kernel.print "#{Color.bright_cyan("╚" + h_line + "╝")}\r\n\r\n"
386
536
 
387
- Kernel.print "#{Color.bright_magenta("[1] HELPERS NATIVOS EN MODO CRUDO")}\r\n"
537
+ Kernel.print "#{Color.bright_magenta("[1] HELPERS NATIVOS")}\r\n"
388
538
  Kernel.print "#{Color.bright_blue(s_line)}\r\n"
389
539
  Kernel.print " #{Color.bright_green("GRmenu.clear_screen")} #{Color.gray("(o GRmenu.clr)")}\r\n"
390
540
  Kernel.print " * Limpia la terminal al instante con secuencias ANSI.\r\n"
@@ -392,18 +542,55 @@ class GRmenu
392
542
  Kernel.print " * Pausa interactiva: espera una sola tecla en modo TTY crudo.\r\n"
393
543
  Kernel.print " #{Color.bright_green("GRmenu.banner(texto, delay, color:, level:, style:, font:)")}\r\n"
394
544
  Kernel.print " * Renderiza banner ASCII 3D con marco y animacion opcional.\r\n"
545
+ Kernel.print " #{Color.bright_green("GRmenu.spinner(mensaje, color:, level:, delay:, &bloque)")}\r\n"
546
+ Kernel.print " * Animacion giratoria fluida para tareas de tiempo desconocido.\r\n"
547
+ Kernel.print " * Ejemplo: #{Color.bright_white("GRmenu.spinner(\"Conectando...\") { conectar_db }")}\r\n"
548
+ Kernel.print " #{Color.bright_green("GRmenu.progress(total, title:, color:, level:, style:, width:, &bloque)")}\r\n"
549
+ Kernel.print " * Barra de progreso porcentual dentro de un recuadro estilizado.\r\n"
550
+ Kernel.print " * El bloque recibe 'bar'. Metodos disponibles:\r\n"
551
+ Kernel.print " - #{Color.cyan("bar.advance(n, status: \"...\")")} -> Avanza n pasos (alias: increment, step).\r\n"
552
+ Kernel.print " - #{Color.cyan("bar.set(valor, status: \"...\")")} -> Fija el valor exacto actual.\r\n"
553
+ Kernel.print " - #{Color.cyan("bar.finish(status: \"...\")")} -> Finaliza la barra al 100%.\r\n"
554
+ Kernel.print " * Ejemplo: #{Color.bright_white("GRmenu.progress(10, title: \"Copia\") { |b| 10.times { b.advance(1) } }")}\r\n"
395
555
  Kernel.print " #{Color.bright_green("GRmenu.div(longitud, color, level, char)")}\r\n"
396
- Kernel.print " * Dibuja linea divisoria horizontal adaptable a la consola.\r\n"
397
- Kernel.print " #{Color.bright_green("GRmenu.help")}\r\n"
398
- Kernel.print " * Imprime esta guia visual interactiva en consola.\r\n\r\n"
556
+ Kernel.print " * Dibuja linea divisoria horizontal adaptable a la consola.\r\n\r\n"
399
557
 
400
- Kernel.print "#{Color.bright_magenta("[2] MODULO DE COLORES (Color / C)")}\r\n"
558
+ Kernel.print "#{Color.bright_magenta("[2] FORMATOS DE OPCIONES Y TOOLTIPS DINAMICOS")}\r\n"
559
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
560
+ Kernel.print " #{Color.cyan("1. Metodo directo:")} #{Color.bright_white("method(:iniciar)")} #{Color.gray("(auto-capitaliza nombre)")}\r\n"
561
+ Kernel.print " #{Color.cyan("2. Simbolo:")} #{Color.bright_white(":iniciar")}\r\n"
562
+ Kernel.print " #{Color.cyan("3. Nombre propio:")} #{Color.bright_white("[\"Mi Accion\", method(:iniciar)]")}\r\n"
563
+ Kernel.print " #{Color.cyan("4. Con Tooltip/Info:")} #{Color.bright_white("[\"Mi Accion\", method(:iniciar), \"Descripcion que sale abajo\"]")}\r\n"
564
+ Kernel.print " #{Color.cyan("5. Lambda / Proc:")} #{Color.bright_white("[\"Test\", -> { puts \"Hola\" }, \"Tooltip opcional\"]")}\r\n"
565
+ Kernel.print " #{Color.cyan("6. Hash:")} #{Color.bright_white("{ name: \"Test\", action: method(:iniciar), desc: \"Info\" }")}\r\n\r\n"
566
+
567
+ Kernel.print "#{Color.bright_magenta("[3] PARAMETROS DE GRmenu.new(functions, ...)")}\r\n"
568
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
569
+ Kernel.print " #{Color.bright_green("functions:")} #{Color.white("Array")} -> Lista de opciones (metodos, simbolos, arreglos, lambdas).\r\n"
570
+ Kernel.print " #{Color.bright_green("banner:")} #{Color.white("String")} -> Texto grande a renderizar en arte ASCII 3D.\r\n"
571
+ Kernel.print " #{Color.bright_green("title:")} #{Color.white("String")} -> Titulo en el encabezado del recuadro.\r\n"
572
+ Kernel.print " #{Color.bright_green("subtitle:")} #{Color.white("String")} -> Subtitulo / descripcion (soporta \\n).\r\n"
573
+ Kernel.print " #{Color.bright_green("page_size:")} #{Color.white("Integer")} -> Limite visible para scroll y paginacion automatica.\r\n"
574
+ Kernel.print " #{Color.bright_green("font:")} #{Color.white("Integer")} -> Fuente ASCII del banner (1 al 10, default 1).\r\n"
575
+ Kernel.print " #{Color.bright_green("style:")} #{Color.white("Integer")} -> Estilo de marco de opciones (1 al 20, default 19).\r\n"
576
+ Kernel.print " #{Color.bright_green("banner_style:")} #{Color.white("Integer")} -> Estilo de marco del banner (1 al 20, default 3).\r\n"
577
+ Kernel.print " #{Color.bright_green("divider:")} #{Color.white("Boolean")} -> Divisores alineados al banner (true/false).\r\n"
578
+ Kernel.print " #{Color.bright_green("center:")} #{Color.white("Boolean")} -> Centrado simetrico de subtitulo y menu (default true).\r\n\r\n"
579
+
580
+ Kernel.print "#{Color.bright_magenta("[4] AUTO-PAGINACION Y SCROLL")}\r\n"
581
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
582
+ Kernel.print " * #{Color.white("100% Automatica:")} Si la lista tiene muchas opciones o la pantalla es pequena,\r\n"
583
+ Kernel.print " GRmenu calcula el espacio disponible y genera una ventana deslizante suave.\r\n"
584
+ Kernel.print " * Indicadores visuales: #{Color.bright_yellow("▲ (+N arriba)")} y #{Color.bright_yellow("▼ (+M abajo)")}.\r\n"
585
+ Kernel.print " * Opcional: fija el limite con #{Color.bright_white("page_size: 8")} al instanciar #{Color.bright_green("GRmenu.new")}.\r\n\r\n"
586
+
587
+ Kernel.print "#{Color.bright_magenta("[5] MODULO DE COLORES (Color / C)")}\r\n"
401
588
  Kernel.print "#{Color.bright_blue(s_line)}\r\n"
402
589
  Kernel.print " #{Color.cyan("Uso directo: ")}#{Color.bright_white("puts Color.green(\"Texto\")")} | #{Color.bright_white("puts Color.bright_cyan(\"Texto\")")}\r\n"
403
590
  Kernel.print " #{Color.cyan("Paleta: ")}#{Color.red("red")}, #{Color.green("green")}, #{Color.yellow("yellow")}, #{Color.blue("blue")}, #{Color.magenta("magenta")}, #{Color.purple("purple")}, #{Color.pink("pink")}, #{Color.cyan("cyan")}, #{Color.aqua("aqua")}, #{Color.orange("orange")}, #{Color.white("white")}, #{Color.gray("gray")}, #{Color.black("black")}.\r\n"
404
591
  Kernel.print " #{Color.cyan("Brillo: ")}#{Color.white("1")} = Normal, #{Color.bright_white("2")} = Brillante / Bold.\r\n\r\n"
405
592
 
406
- Kernel.print "#{Color.bright_magenta("[3] FUENTES ASCII 3D DEL BANNER (font: 1 al 10)")}\r\n"
593
+ Kernel.print "#{Color.bright_magenta("[6] FUENTES ASCII 3D DEL BANNER (font: 1 al 10)")}\r\n"
407
594
  Kernel.print "#{Color.bright_blue(s_line)}\r\n"
408
595
  Kernel.print " #{Color.yellow("1")} -> #{Color.bright_white("ANSI Shadow 3D (Default)")} #{Color.cyan("[██████╗ ██╗ ██╗]")}\r\n"
409
596
  Kernel.print " #{Color.yellow("2")} -> #{Color.bright_white("Slant 3D (FIGlet)")} #{Color.cyan("[ ____ __ __]")}\r\n"
@@ -416,28 +603,17 @@ class GRmenu
416
603
  Kernel.print " #{Color.yellow("9")} -> #{Color.bright_white("Solid Fat 3D Block")} #{Color.cyan("[ ██████▄ ██ ██]")}\r\n"
417
604
  Kernel.print " #{Color.yellow("10")}-> #{Color.bright_white("Arcade Stars Matrix")} #{Color.cyan("[ ★★★★ ★ ★]")}\r\n\r\n"
418
605
 
419
- Kernel.print "#{Color.bright_magenta("[4] ESTILOS DE MARCO (style / banner_style: 1 al 20)")}\r\n"
606
+ Kernel.print "#{Color.bright_magenta("[7] ESTILOS DE MARCO (style / banner_style: 1 al 20)")}\r\n"
420
607
  Kernel.print "#{Color.bright_blue(s_line)}\r\n"
421
608
  Kernel.print " #{Color.yellow("3")} -> #{Color.bright_white("Doble linea")} #{Color.cyan("╔═══╗ ║ ║ ╚═══╝")} (Default en Banner)\r\n"
422
609
  Kernel.print " #{Color.yellow("7")} -> #{Color.bright_white("Curvas redondeadas")} #{Color.cyan("╭───╮ │ │ ╰───╯")}\r\n"
423
610
  Kernel.print " #{Color.yellow("4")} -> #{Color.bright_white("Linea gruesa")} #{Color.cyan("┏━━━┓ ┃ ┃ ┗━━━┛")}\r\n"
424
611
  Kernel.print " #{Color.yellow("2")} -> #{Color.bright_white("Linea simple")} #{Color.cyan("┌───┐ │ │ └───┘")}\r\n"
425
- Kernel.print " #{Color.yellow("8")} -> #{Color.bright_white("Bloques outline")} #{Color.cyan("▛▀▀▀▜ ▌ ▙▄▄▄▟")}\r\n"
612
+ Kernel.print " #{Color.yellow("8")} -> #{Color.bright_white("Bloques outline")} #{Color.cyan("▛▀▀▀▜ ▌ ▙▄▄▄▟")}\r\n"
426
613
  Kernel.print " #{Color.yellow("19")} -> #{Color.bright_white("Circulos")} #{Color.cyan("●○○○● ● ● ●○○○●")} (Default en Opciones)\r\n"
427
614
  Kernel.print " #{Color.yellow("20")} -> #{Color.bright_white("Estrellas")} #{Color.cyan("★☆☆☆★ ★ ★ ★☆☆☆★")}\r\n\r\n"
428
615
 
429
- Kernel.print "#{Color.bright_magenta("[5] PARAMETROS DE GRmenu.new(functions, ...)")}\r\n"
430
- Kernel.print "#{Color.bright_blue(s_line)}\r\n"
431
- Kernel.print " #{Color.bright_green("banner:")} #{Color.white("String")} -> Texto grande a renderizar en arte ASCII 3D.\r\n"
432
- Kernel.print " #{Color.bright_green("title:")} #{Color.white("String")} -> Titulo en el encabezado del recuadro.\r\n"
433
- Kernel.print " #{Color.bright_green("subtitle:")} #{Color.white("String")} -> Subtitulo / descripcion (soporta \\n).\r\n"
434
- Kernel.print " #{Color.bright_green("font:")} #{Color.white("Integer")} -> Fuente ASCII del banner (1 al 10, default 1).\r\n"
435
- Kernel.print " #{Color.bright_green("style:")} #{Color.white("Integer")} -> Estilo de marco de opciones (1 al 20, default 19).\r\n"
436
- Kernel.print " #{Color.bright_green("banner_style:")} #{Color.white("Integer")} -> Estilo de marco del banner (1 al 20, default 3).\r\n"
437
- Kernel.print " #{Color.bright_green("divider:")} #{Color.white("Boolean")} -> Divisores alineados al banner (true/false).\r\n"
438
- Kernel.print " #{Color.bright_green("center:")} #{Color.white("Boolean")} -> Centrado simetrico de subtitulo y menu (default true).\r\n\r\n"
439
-
440
- Kernel.print "#{Color.bright_magenta("[6] METODOS DE CONFIGURACION (menu.set_style)")}\r\n"
616
+ Kernel.print "#{Color.bright_magenta("[8] METODOS DE CONFIGURACION (menu.set_style)")}\r\n"
441
617
  Kernel.print "#{Color.bright_blue(s_line)}\r\n"
442
618
  Kernel.print " #{Color.cyan("menu.set_style.font(id)")} -> Cambia fuente ASCII (1..10)\r\n"
443
619
  Kernel.print " #{Color.cyan("menu.set_style.banner(color, level)")} -> Color y brillo del banner ASCII\r\n"
@@ -448,7 +624,7 @@ class GRmenu
448
624
  Kernel.print " #{Color.cyan("menu.set_style.options(color, level)")} -> Color y brillo de opciones no activas\r\n"
449
625
  Kernel.print " #{Color.cyan("menu.set_style.focus(color, level)")} -> Color y brillo de la opcion resaltada\r\n\r\n"
450
626
 
451
- Kernel.print "#{Color.bright_magenta("[7] EJECUCION (menu.draw)")}\r\n"
627
+ Kernel.print "#{Color.bright_magenta("[9] EJECUCION (menu.draw)")}\r\n"
452
628
  Kernel.print "#{Color.bright_blue(s_line)}\r\n"
453
629
  Kernel.print " #{Color.bright_white("menu.draw(size_max: 38)")} -> Inicia el menu interactivo con ancho minimo.\r\n"
454
630
  Kernel.print "#{Color.bright_blue(s_line)}\r\n\r\n"
@@ -499,25 +675,32 @@ class GRmenu
499
675
 
500
676
  ascii_rows = build_ascii_lines(text, cols, font)
501
677
  border_cfg = BORDERS[style] || BORDERS[3]
678
+ h_top = border_cfg[:ht] || border_cfg[:h]
679
+ h_bot = border_cfg[:hb] || border_cfg[:h]
680
+ v_l = border_cfg[:vl] || border_cfg[:v]
681
+ v_r = border_cfg[:vr] || border_cfg[:v]
502
682
 
503
683
  if ascii_rows
504
684
  max_len = ascii_rows.map(&:length).max
505
- h_fill = (border_cfg[:h] * ((max_len + 4).to_f / border_cfg[:h].length).ceil)[0...(max_len + 4)]
506
-
507
- Kernel.print("#{color_code}#{border_cfg[:tl]}#{h_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
685
+ top_fill = (h_top * ((max_len + 4).to_f / h_top.length).ceil)[0...(max_len + 4)]
686
+ bot_fill = (h_bot * ((max_len + 4).to_f / h_bot.length).ceil)[0...(max_len + 4)]
687
+
688
+ Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
508
689
  ascii_rows.each do |line|
509
690
  pad = " " * (max_len - line.length)
510
- Kernel.print("#{color_code}#{border_cfg[:v]} #{line}#{pad} #{border_cfg[:v]}#{reset_code}\r\n")
691
+ Kernel.print("#{color_code}#{v_l} #{line}#{pad} #{v_r}#{reset_code}\r\n")
511
692
  sleep(delay) if delay > 0
512
693
  end
513
- Kernel.print("#{color_code}#{border_cfg[:bl]}#{h_fill}#{border_cfg[:br]}#{reset_code}\r\n")
694
+ Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
514
695
  else
515
696
  clean_t = text.to_s.strip
516
697
  box_w = [clean_t.length + 6, cols - 2].min
517
- h_fill = (border_cfg[:h] * ((box_w - 2).to_f / border_cfg[:h].length).ceil)[0...(box_w - 2)]
518
- Kernel.print("#{color_code}#{border_cfg[:tl]}#{h_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
519
- Kernel.print("#{color_code}#{border_cfg[:v]} #{clean_t.center(box_w - 4)} #{border_cfg[:v]}#{reset_code}\r\n")
520
- Kernel.print("#{color_code}#{border_cfg[:bl]}#{h_fill}#{border_cfg[:br]}#{reset_code}\r\n")
698
+ top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
699
+ bot_fill = (h_bot * ((box_w - 2).to_f / h_b.length).ceil)[0...(box_w - 2)]
700
+
701
+ Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
702
+ Kernel.print("#{color_code}#{v_l} #{clean_t.center(box_w - 4)} #{v_r}#{reset_code}\r\n")
703
+ Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
521
704
  end
522
705
  end
523
706
 
@@ -526,7 +709,7 @@ class GRmenu
526
709
  alias_method :logo, :banner
527
710
  end
528
711
 
529
- def initialize(functions, *positional_arguments, title: nil, banner: nil, subtitle: nil, description: nil, divider: nil, style: nil, banner_style: nil, center: true, font: nil, **keyword_arguments)
712
+ 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, **keyword_arguments)
530
713
  @functions = functions.is_a?(Array) ? functions : Array(functions)
531
714
 
532
715
  pos_title = positional_arguments[0]
@@ -539,8 +722,8 @@ class GRmenu
539
722
  @style = (style || pos_style || keyword_arguments[:style] || 19).to_i
540
723
  @banner_style = (banner_style || keyword_arguments[:banner_style] || 3).to_i
541
724
  @center = center.nil? ? true : center
725
+ @page_size = (page_size || keyword_arguments[:page_size])&.to_i
542
726
  @index = 0
543
- @clear_seq = CLEAR_SCREEN_SEQUENCE
544
727
 
545
728
  init_font = font || keyword_arguments[:font_style] || SetStyle.font || 1
546
729
 
@@ -598,13 +781,13 @@ class GRmenu
598
781
  banner_border = BORDERS[@banner_style] || BORDERS[3]
599
782
  banner_color_cfg = @style_config.banner
600
783
 
601
- lines = []
602
- box_w = 0
603
784
  h_top = banner_border[:ht] || banner_border[:h]
604
785
  h_bot = banner_border[:hb] || banner_border[:h]
605
786
  v_l = banner_border[:vl] || banner_border[:v]
606
787
  v_r = banner_border[:vr] || banner_border[:v]
607
788
 
789
+ lines = []
790
+ box_w = 0
608
791
  if ascii_rows
609
792
  content_w = ascii_rows.map(&:length).max
610
793
  box_w = content_w + 6
@@ -632,40 +815,53 @@ class GRmenu
632
815
 
633
816
  def render_lines(size_max = 20)
634
817
  term_cols = self.class.terminal_width
818
+ term_rows = self.class.terminal_height
635
819
  rendered_lines = []
636
820
 
637
821
  banner_box_width = 0
822
+ banner_lines_count = 0
638
823
  if @banner && !@banner.empty?
639
824
  banner_lines, banner_box_width = render_banner_lines(term_cols)
640
825
  rendered_lines.concat(banner_lines)
641
826
  rendered_lines << ""
827
+ banner_lines_count = banner_lines.length + 1
642
828
  end
643
829
 
644
- option_names = @functions.map { |func| extract_name_from_action(func) }
830
+ all_names = @functions.map { |func| extract_name_from_action(func) }
831
+ all_descriptions = @functions.map { |func| extract_description_from_action(func) }
832
+
833
+ active_desc = all_descriptions[@index] || ""
834
+
645
835
  calculated_width = [size_max, @title.length + 4].max
646
- calculated_width = ([calculated_width] + option_names.map { |name| name.length + 6 }).max
836
+ calculated_width = ([calculated_width] + all_names.map { |name| name.length + 6 }).max
837
+ calculated_width = ([calculated_width, active_desc.length + 8].max) unless active_desc.empty?
647
838
  total_width = [calculated_width, term_cols - 2].min
648
839
 
649
840
  reference_width = banner_box_width > 0 ? banner_box_width : total_width
650
841
  margin_left = (@center && reference_width > total_width) ? " " * ((reference_width - total_width) / 2) : ""
651
842
 
843
+ subtitle_lines_count = 0
652
844
  if @subtitle && !@subtitle.empty?
653
845
  subtitle_lines = @subtitle.lines.map(&:chomp)
654
846
  div_w = @divider.is_a?(Numeric) ? @divider.to_i : [reference_width, term_cols - 2].min
655
847
 
656
848
  if @divider
657
849
  rendered_lines << colorize("─" * div_w, @style_config.divider)
850
+ subtitle_lines_count += 1
658
851
  end
659
852
 
660
853
  subtitle_lines.each do |sub_line|
661
854
  formatted_sub = @center ? sub_line.center(div_w) : sub_line
662
855
  rendered_lines << colorize(formatted_sub, @style_config.subtitle)
856
+ subtitle_lines_count += 1
663
857
  end
664
858
 
665
859
  if @divider
666
860
  rendered_lines << colorize("─" * div_w, @style_config.divider)
861
+ subtitle_lines_count += 1
667
862
  end
668
863
  rendered_lines << ""
864
+ subtitle_lines_count += 1
669
865
  end
670
866
 
671
867
  border_color_cfg = @style_config.border
@@ -675,6 +871,33 @@ class GRmenu
675
871
 
676
872
  box_border = BORDERS[@style]
677
873
 
874
+ total_items = @functions.length
875
+ overhead = banner_lines_count + subtitle_lines_count + 6
876
+ overhead += 2 unless active_desc.empty?
877
+ available_rows = [term_rows - overhead - 2, 3].max
878
+
879
+ effective_page_size = if @page_size && @page_size > 0
880
+ [@page_size, total_items].min
881
+ elsif total_items > available_rows
882
+ available_rows
883
+ else
884
+ total_items
885
+ end
886
+
887
+ start_idx = 0
888
+ end_idx = total_items - 1
889
+ if total_items > effective_page_size
890
+ half = effective_page_size / 2
891
+ start_idx = [[@index - half, 0].max, total_items - effective_page_size].min
892
+ end_idx = start_idx + effective_page_size - 1
893
+ end
894
+
895
+ visible_indices = (start_idx..end_idx).to_a
896
+ has_more_above = start_idx > 0
897
+ has_more_below = end_idx < (total_items - 1)
898
+
899
+ avail_w = [total_width - 6, 1].max
900
+
678
901
  if box_border
679
902
  h_top = box_border[:ht] || box_border[:h]
680
903
  h_bot = box_border[:hb] || box_border[:h]
@@ -699,8 +922,13 @@ class GRmenu
699
922
  rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg)}"
700
923
  end
701
924
 
702
- option_names.each_with_index do |option_name, current_index|
703
- avail_w = [total_width - 6, 1].max
925
+ if has_more_above
926
+ up_indicator = colorize("▲ (+#{start_idx} arriba)".center(avail_w + 2), { color: "gray", level: 2 })
927
+ rendered_lines << "#{margin_left}#{v_left} #{up_indicator} #{v_right}"
928
+ end
929
+
930
+ visible_indices.each do |current_index|
931
+ option_name = all_names[current_index]
704
932
  if @index == current_index
705
933
  highlighted_text = colorize("> #{option_name.ljust(avail_w)}", focus_color_cfg)
706
934
  rendered_lines << "#{margin_left}#{v_left} #{highlighted_text} #{v_right}"
@@ -710,6 +938,19 @@ class GRmenu
710
938
  end
711
939
  end
712
940
 
941
+ if has_more_below
942
+ remaining_below = total_items - 1 - end_idx
943
+ down_indicator = colorize("▼ (+#{remaining_below} abajo)".center(avail_w + 2), { color: "gray", level: 2 })
944
+ rendered_lines << "#{margin_left}#{v_left} #{down_indicator} #{v_right}"
945
+ end
946
+
947
+ unless active_desc.empty?
948
+ separator_line = v_l_raw + mid_fill + v_r_raw
949
+ rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg)}"
950
+ desc_text = colorize("ℹ #{active_desc.ljust(avail_w)}", { color: "cyan", level: 1 })
951
+ rendered_lines << "#{margin_left}#{v_left} #{desc_text} #{v_right}"
952
+ end
953
+
713
954
  bottom_border_line = box_border[:bl] + bot_fill + box_border[:br]
714
955
  rendered_lines << "#{margin_left}#{colorize(bottom_border_line, border_color_cfg)}"
715
956
  else
@@ -725,8 +966,13 @@ class GRmenu
725
966
  rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
726
967
  end
727
968
 
728
- option_names.each_with_index do |option_name, current_index|
729
- avail_w = [total_width - 6, 1].max
969
+ if has_more_above
970
+ up_indicator = colorize("▲ (+#{start_idx} arriba)".center(avail_w + 2), { color: "gray", level: 2 })
971
+ rendered_lines << "#{margin_left}#{solid_border} #{up_indicator} #{solid_border}"
972
+ end
973
+
974
+ visible_indices.each do |current_index|
975
+ option_name = all_names[current_index]
730
976
  if @index == current_index
731
977
  highlighted_text = colorize("> #{option_name.ljust(avail_w)}", focus_color_cfg)
732
978
  rendered_lines << "#{margin_left}#{solid_border} #{highlighted_text} #{solid_border}"
@@ -736,6 +982,18 @@ class GRmenu
736
982
  end
737
983
  end
738
984
 
985
+ if has_more_below
986
+ remaining_below = total_items - 1 - end_idx
987
+ down_indicator = colorize("▼ (+#{remaining_below} abajo)".center(avail_w + 2), { color: "gray", level: 2 })
988
+ rendered_lines << "#{margin_left}#{solid_border} #{down_indicator} #{solid_border}"
989
+ end
990
+
991
+ unless active_desc.empty?
992
+ rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
993
+ desc_text = colorize("ℹ #{active_desc.ljust(avail_w)}", { color: "cyan", level: 1 })
994
+ rendered_lines << "#{margin_left}#{solid_border} #{desc_text} #{solid_border}"
995
+ end
996
+
739
997
  rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
740
998
  end
741
999
 
@@ -778,8 +1036,9 @@ class GRmenu
778
1036
  def draw_frame(target_width)
779
1037
  lines = render_lines(target_width)
780
1038
  buffer = String.new(CURSOR_HOME)
781
- lines.each do |line|
782
- buffer << line << CLEAR_TO_EOL << "\r\n"
1039
+ lines.each_with_index do |line, idx|
1040
+ buffer << line << CLEAR_TO_EOL
1041
+ buffer << "\r\n" if idx < lines.length - 1
783
1042
  end
784
1043
  buffer << CLEAR_TO_EOS
785
1044
  Kernel.print(buffer)
@@ -848,6 +1107,8 @@ class GRmenu
848
1107
  case action
849
1108
  when Array
850
1109
  action[0].to_s
1110
+ when Hash
1111
+ (action[:name] || action[:title] || action["name"] || action["title"] || "Opcion").to_s
851
1112
  when Method
852
1113
  format_auto_name(action.name)
853
1114
  when Symbol
@@ -869,6 +1130,16 @@ class GRmenu
869
1130
  end
870
1131
  end
871
1132
 
1133
+ def extract_description_from_action(action)
1134
+ if action.is_a?(Array) && action.length >= 3
1135
+ action[2].to_s
1136
+ elsif action.is_a?(Hash)
1137
+ (action[:desc] || action[:description] || action["desc"] || action["description"]).to_s
1138
+ else
1139
+ ""
1140
+ end
1141
+ end
1142
+
872
1143
  def execute_action(action)
873
1144
  case action
874
1145
  when Method, Proc
@@ -881,7 +1152,26 @@ class GRmenu
881
1152
  end
882
1153
  when Array
883
1154
  callable = action[1]
884
- callable.call if callable.respond_to?(:call)
1155
+ if callable.is_a?(Symbol)
1156
+ if Object.respond_to?(callable, true)
1157
+ Object.send(callable)
1158
+ elsif Kernel.respond_to?(callable, true)
1159
+ Kernel.send(callable)
1160
+ end
1161
+ elsif callable.respond_to?(:call)
1162
+ callable.call
1163
+ end
1164
+ when Hash
1165
+ callable = action[:action] || action[:call] || action["action"] || action["call"]
1166
+ if callable.is_a?(Symbol)
1167
+ if Object.respond_to?(callable, true)
1168
+ Object.send(callable)
1169
+ elsif Kernel.respond_to?(callable, true)
1170
+ Kernel.send(callable)
1171
+ end
1172
+ elsif callable.respond_to?(:call)
1173
+ callable.call
1174
+ end
885
1175
  else
886
1176
  action.call if action.respond_to?(:call)
887
1177
  end
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # GRmenu (Ruby)
4
4
 
5
- **Menús interactivos por teclado para terminal en modo TTY crudo, con soporte para Banners ASCII 3D, estilos y colores personalizados**
5
+ **Menús interactivos por teclado para terminal en modo TTY crudo, con soporte para Banners ASCII 3D, barras de progreso, spinners animados, tooltips dinámicos, estilos y colores personalizados**
6
6
 
7
7
  Flechas arriba/abajo para moverte · `Enter` para elegir · `q` para salir
8
8
 
@@ -18,12 +18,15 @@ Flechas arriba/abajo para moverte · `Enter` para elegir · `q` para salir
18
18
 
19
19
  - 🎮 **Navegación con flechas** — arriba/abajo para moverte, `Enter` para ejecutar, `q` para salir.
20
20
  - 🔤 **10 Fuentes ASCII 3D para Banners** — fuentes tridimensionales (ANSI Shadow, Slant, Doom, Graffiti, Modular, Wire, Block, Stars, etc.).
21
- - 🎨 **20 estilos de borde** — desde ASCII clásico hasta caracteres Unicode dobles, curvas redondeadas y bloques.
21
+ - 📜 **Auto-Paginación y Scroll Fluido** — calcula la altura de la terminal y crea una ventana deslizante con indicadores automáticos (`▲ (+N arriba)` / `▼ (+M abajo)`).
22
+ - 💡 **Descripciones y Tooltips Dinámicos** — muestra información explicativa en la parte inferior del recuadro al enfocar cada opción.
23
+ - ⏳ **Spinners y Barras de Progreso** — helpers nativos `GRmenu.spinner` y `GRmenu.progress` dentro de recuadros con la misma estética visual.
24
+ - 🎨 **20 estilos de borde** — desde ASCII clásico hasta caracteres Unicode dobles, curvas redondeadas y bloques outline.
22
25
  - 🌈 **Paleta de colores completa** — personalización individual de marco, título, banner, subtítulo, divisores, opciones y foco activo con 2 niveles de brillo.
23
26
  - 📐 **Centrado simétrico automático** — alinea y centra automáticamente subtítulos y menús de opciones respecto al ancho de banners grandes.
24
- - 🛠️ **Helpers nativos en modo crudo** — `clear_screen`, `continue`, `banner`, `div` y `help` sin subprocesos lentos del sistema.
27
+ - 🛠️ **Helpers nativos en modo crudo** — `clear_screen`, `continue`, `banner`, `spinner`, `progress`, `div` y `help`.
25
28
  - 💻 **100% Multiplataforma** — compatible con Linux, macOS y Windows (PowerShell, CMD, Windows Terminal, VS Code).
26
- - 📦 **Cero dependencias externas** — utiliza únicamente la librería estándar `io/console`.
29
+ - 📦 **Cero dependencias externas** — utiliza únicamente la librería estándar `io/console` y `json`.
27
30
 
28
31
  ---
29
32
 
@@ -36,7 +39,7 @@ gem install grmenu
36
39
  O si trabajás con el archivo directamente en tu proyecto:
37
40
 
38
41
  ```ruby
39
- require_relative "GRmenu"
42
+ require "GRmenu"
40
43
  ```
41
44
 
42
45
  <sub>Requiere Ruby ≥ 2.6.</sub>
@@ -45,7 +48,7 @@ require_relative "GRmenu"
45
48
 
46
49
  ## 💡 Cómo se pasan las opciones y todos los parámetros
47
50
 
48
- `GRmenu` permite pasar métodos directos, símbolos, arreglos con nombres personalizados, bloques lambda/procs y helpers. Además, acepta todos los parámetros de configuración visual en la instanciación:
51
+ `GRmenu` permite pasar métodos directos, símbolos, arreglos con nombres personalizados, bloques lambda/procs y **descripciones explicativas (tooltips)**:
49
52
 
50
53
  ```ruby
51
54
  menu = GRmenu.new(
@@ -53,7 +56,8 @@ menu = GRmenu.new(
53
56
  method(:iniciar_servidor), # 1. Method (auto-capitaliza: "Iniciar Servidor")
54
57
  :crear_respaldo, # 2. Symbol (auto-capitaliza: "Crear Respaldo")
55
58
  ["Métricas del Sistema", method(:ver_metricas)], # 3. Array ["Nombre Personalizado", acción]
56
- ["Ejecutar Lambda", -> { puts Color.pink("Lambda!"); GRmenu.continue }], # 4. Lambda/Proc
59
+ ["Crear Backup", method(:respaldo), "Genera dump SQL de la BD"], # 4. Con Tooltip descriptivo al pie del marco
60
+ ["Ejecutar Lambda", -> { puts Color.pink("Lambda!"); GRmenu.continue }, "Bloque anónimo Proc/Lambda"],
57
61
  ["Probar Banner Helper", method(:prueba_banner_rapido)], # 5. Helper GRmenu.banner
58
62
  ["Ver Ayuda y Referencia", method(:ver_ayuda_completa)], # 6. Helper GRmenu.help
59
63
  method(:salir) # 7. Salir
@@ -61,11 +65,12 @@ menu = GRmenu.new(
61
65
  banner: "DEV OPS", # Texto gigante en arte ASCII 3D
62
66
  title: "Panel de Control", # Título en el marco de opciones
63
67
  subtitle: "Consola de Administración\nUsa las flechas y Enter", # Subtítulo (soporta saltos de línea \n)
64
- font: 1, # Fuente del banner (1 al 10, por defecto 1: ANSI Shadow 3D)
68
+ font: 1, # Fuente del banner (1 al 10, default 1: ANSI Shadow 3D)
65
69
  style: 7, # Estilo de marco de opciones (1 al 20, ej: 7=redondeado, 3=doble)
66
70
  banner_style: 3, # Estilo de marco del banner (1 al 20, ej: 3=doble línea)
67
71
  divider: true, # Líneas divisorias a la par del banner (true, false o número)
68
- center: true # Centrado automático del menú y subtítulo respecto al banner
72
+ center: true, # Centrado automático del menú y subtítulo respecto al banner
73
+ page_size: 8 # (Opcional) Límite visible para auto-scroll y paginación
69
74
  )
70
75
  ```
71
76
 
@@ -73,14 +78,14 @@ menu = GRmenu.new(
73
78
 
74
79
  ## 🌟 Ejemplo Completo de Uso (`e.rb`)
75
80
 
76
- A continuación se muestra el archivo de ejemplo completo [`e.rb`](e.rb) con acciones, helpers, configuración de estilos, fuentes y colores:
81
+ A continuación se muestra el archivo de ejemplo completo [`e.rb`](e.rb) con acciones originales, helpers de carga (`spinner` / `progress`), paginación, configuración de estilos, fuentes y colores:
77
82
 
78
83
  ```ruby
79
84
  # frozen_string_literal: true
80
85
 
81
- require_relative "GRmenu"
86
+ require "GRmenu"
82
87
 
83
- # 1. Definición de acciones/métodos
88
+ # 1. Definición de acciones/métodos originales
84
89
  def iniciar_servidor
85
90
  GRmenu.clear_screen
86
91
  puts Color.bright_green("-> Servidor iniciado correctamente en el puerto 3000.")
@@ -101,7 +106,6 @@ end
101
106
 
102
107
  def prueba_banner_rapido
103
108
  GRmenu.clear_screen
104
- # Helper para mostrar un banner estático o animado en cualquier momento
105
109
  GRmenu.banner("OK", 0, color: "green", level: 2, style: 3, font: 1)
106
110
  GRmenu.div(40, "green", 1, "═")
107
111
  puts Color.green(" Prueba completada con éxito.")
@@ -111,7 +115,6 @@ end
111
115
 
112
116
  def ver_ayuda_completa
113
117
  GRmenu.clear_screen
114
- # Helper interactivo que imprime toda la guía y referencia de GRmenu
115
118
  GRmenu.help
116
119
  GRmenu.continue
117
120
  end
@@ -122,44 +125,88 @@ def salir
122
125
  exit(0)
123
126
  end
124
127
 
125
- # 2. Instanciación del menú con TODOS los parámetros disponibles
126
- menu = GRmenu.new(
127
- [
128
- method(:iniciar_servidor), # 1. Method (auto-capitaliza: "Iniciar Servidor")
129
- :crear_respaldo, # 2. Symbol (auto-capitaliza: "Crear Respaldo")
130
- ["Métricas del Sistema", method(:ver_metricas)], # 3. Array ["Nombre Personalizado", acción]
131
- ["Ejecutar Lambda", -> { puts Color.pink("Lambda!"); GRmenu.continue }], # 4. Lambda/Proc
132
- ["Probar Banner Helper", method(:prueba_banner_rapido)], # 5. Helper GRmenu.banner
133
- ["Ver Ayuda y Referencia", method(:ver_ayuda_completa)], # 6. Helper GRmenu.help
134
- method(:salir) # 7. Salir
135
- ],
136
- banner: "DEV OPS", # Texto gigante en arte ASCII 3D
137
- title: "Panel de Control", # Título en el marco de opciones
138
- subtitle: "Consola de Administración\nUsa las flechas y Enter", # Subtítulo (soporta saltos de línea \n)
139
- font: 1, # Fuente del banner (1 al 10, por defecto 1: ANSI Shadow 3D)
140
- style: 7, # Estilo de marco de opciones (1 al 20, ej: 7=redondeado, 3=doble)
141
- banner_style: 3, # Estilo de marco del banner (1 al 20, ej: 3=doble línea)
142
- divider: true, # Líneas divisorias a la par del banner (true, false o número)
143
- center: true # Centrado automático del menú y subtítulo respecto al banner
144
- )
128
+ # 2. Nuevos ejemplos: Barra de Progreso, Spinner y Paginación
129
+ def demo_barra_progreso
130
+ GRmenu.clear_screen
131
+ # Fácil y rápido: pasas el total y bar.advance(1) en tu bucle
132
+ GRmenu.progress(10, title: "Descargando Paquetes", color: "cyan", style: 3) do |bar|
133
+ 10.times do |i|
134
+ sleep 0.1
135
+ bar.advance(1, status: "Paso #{i + 1} de 10 completado")
136
+ end
137
+ end
138
+ puts Color.bright_green("\n-> ¡Barra completada al 100%!")
139
+ GRmenu.continue
140
+ end
145
141
 
146
- # 3. Configuración completa de colores y estilos (set_style / style_config)
147
- # Colores disponibles: "black", "gray", "red", "green", "yellow", "blue",
148
- # "magenta", "purple", "pink", "cyan", "aqua", "orange", "white"
149
- # Niveles de brillo: 1 = normal, 2 = brillante
150
-
151
- menu.set_style.font(1) # 1 = ANSI Shadow 3D, 2 = Slant 3D, 3 = Doom, etc.
152
- menu.set_style.banner("cyan", 2) # Color del banner ASCII
153
- menu.set_style.title("yellow", 2) # Color del título del recuadro
154
- menu.set_style.subtitle("white", 1)# Color del subtítulo/descripción
155
- menu.set_style.divider("blue", 1) # Color de las líneas divisorias
156
- menu.set_style.border("yellow", 1) # Color del borde del marco de opciones
157
- menu.set_style.options("white", 1) # Color de opciones no seleccionadas
158
- menu.set_style.focus("green", 2) # Color y brillo de la opción resaltada
159
-
160
- # 4. Dibujar y lanzar el menú interactivo
161
- # size_max / min_width define el ancho mínimo sugerido para el marco de opciones
162
- menu.draw(size_max: 38)
142
+ def demo_spinner
143
+ GRmenu.clear_screen
144
+ GRmenu.spinner("Conectando a la base de datos...", color: "green") do
145
+ sleep 1.2
146
+ end
147
+ puts Color.bright_green("\n-> Conexión establecida con éxito.")
148
+ GRmenu.continue
149
+ end
150
+
151
+ def demo_paginacion
152
+ GRmenu.clear_screen
153
+ opciones_largas = (1..20).map do |n|
154
+ ["Elemento ##{n}", -> {
155
+ GRmenu.clear_screen
156
+ puts Color.bright_cyan("-> Has seleccionado el Elemento ##{n}")
157
+ GRmenu.continue
158
+ }, "Descripción opcional del elemento ##{n}"]
159
+ end
160
+
161
+ sub = GRmenu.new(
162
+ opciones_largas,
163
+ title: "Submenú Paginado",
164
+ subtitle: "Usa ↑ / ↓ para ver el auto-scroll",
165
+ style: 7,
166
+ page_size: 6
167
+ )
168
+ sub.draw(size_max: 38)
169
+ end
170
+
171
+ # 3. Instanciación del menú con TODOS los parámetros disponibles
172
+ def main
173
+ menu = GRmenu.new(
174
+ [
175
+ method(:iniciar_servidor), # 1. Method (sin info)
176
+ :crear_respaldo, # 2. Symbol (sin info)
177
+ ["Métricas del Sistema", method(:ver_metricas)], # 3. Array (sin info)
178
+ ["Probar Barra de Progreso", method(:demo_barra_progreso), "Ejemplo fácil de GRmenu.progress al 100%"], # 4. CON Tooltip
179
+ ["Probar Spinner de Carga", method(:demo_spinner), "Animación en tiempo real para funciones pesadas"],
180
+ ["Probar Paginación (20 ítems)", method(:demo_paginacion), "Desplazamiento suave de opciones"],
181
+ ["Ejecutar Lambda", -> { puts Color.pink("¡Lambda!"); GRmenu.continue }], # 5. Lambda/Proc
182
+ ["Probar Banner Helper", method(:prueba_banner_rapido)], # 6. Helper GRmenu.banner
183
+ ["Ver Ayuda y Referencia", method(:ver_ayuda_completa), "Abre la guía de ayuda interactiva"], # 7. Helper GRmenu.help
184
+ method(:salir) # 8. Salir
185
+ ],
186
+ banner: " gr codE ",
187
+ title: "Panel de Control",
188
+ subtitle: "Consola de Administración TTY\nUsa ↑ / ↓ para navegar y Enter para seleccionar",
189
+ style: 3,
190
+ banner_style: 3,
191
+ divider: true,
192
+ center: true
193
+ )
194
+
195
+ # 4. Configuración completa de colores y estilos
196
+ menu.set_style.font(1) # 1 = ANSI Shadow 3D
197
+ menu.set_style.banner("cyan", 2) # Color del banner ASCII
198
+ menu.set_style.title("yellow", 2) # Color del título del recuadro
199
+ menu.set_style.subtitle("white", 1)# Color del subtítulo
200
+ menu.set_style.divider("blue", 1) # Color de las líneas divisorias
201
+ menu.set_style.border("yellow", 1) # Color del borde del marco de opciones
202
+ menu.set_style.options("white", 1) # Color de opciones no seleccionadas
203
+ menu.set_style.focus("green", 2) # Color y brillo de la opción resaltada
204
+
205
+ # 5. Dibujar y lanzar el menú interactivo
206
+ menu.draw(size_max: 38)
207
+ end
208
+
209
+ main
163
210
  ```
164
211
 
165
212
  ---
@@ -178,28 +225,6 @@ O desde una instancia:
178
225
  menu.help
179
226
  ```
180
227
 
181
- Salida limpia y estructurada en consola:
182
-
183
- ```text
184
- ╔══════════════════════════════════════════════════════════════╗
185
- ║ GRmenu - Guia y Referencia Rapida ║
186
- ║ Navegacion interactiva en terminal TTY ║
187
- ╚══════════════════════════════════════════════════════════════╝
188
-
189
- [1] HELPERS NATIVOS EN MODO CRUDO
190
- ────────────────────────────────────────────────────────────────
191
- GRmenu.clear_screen (o GRmenu.clr)
192
- * Limpia la terminal al instante con secuencias ANSI.
193
- GRmenu.continue(mensaje)
194
- * Pausa interactiva: espera una sola tecla en modo TTY crudo.
195
- GRmenu.banner(texto, delay, color:, level:, style:, font:)
196
- * Renderiza banner ASCII 3D con marco y animacion opcional.
197
- GRmenu.div(longitud, color, level, char)
198
- * Dibuja linea divisoria horizontal adaptable a la consola.
199
- GRmenu.help
200
- * Imprime esta guia visual interactiva en consola.
201
- ```
202
-
203
228
  ---
204
229
 
205
230
  ## 🕹️ Controles
@@ -273,13 +298,23 @@ GRmenu.clear_screen # o GRmenu.clr
273
298
  # 2. Imprime un banner o logo gigante responsivo
274
299
  GRmenu.banner("SECURE", 0, color: "magenta", style: 3, font: 1)
275
300
 
276
- # 3. Línea divisoria horizontal adaptable
301
+ # 3. Spinner animado en tiempo real mientras corre un bloque
302
+ GRmenu.spinner("Conectando al clúster...", color: "green") do
303
+ hacer_tarea_pesada()
304
+ end
305
+
306
+ # 4. Barra de progreso porcentual dentro de un recuadro estilizado
307
+ GRmenu.progress(10, title: "Exportando Datos", color: "cyan") do |bar|
308
+ 10.times { |i| bar.advance(1, status: "Paso #{i + 1}/10") }
309
+ end
310
+
311
+ # 5. Línea divisoria horizontal adaptable
277
312
  GRmenu.div(60, "blue")
278
313
 
279
- # 4. Pausa de consola que espera una sola tecla en modo TTY crudo
314
+ # 6. Pausa de consola que espera una sola tecla en modo TTY crudo
280
315
  GRmenu.continue("Presiona cualquier tecla para continuar...")
281
316
 
282
- # 5. Guía interactiva en consola
317
+ # 7. Guía interactiva en consola
283
318
  GRmenu.help
284
319
  ```
285
320
 
@@ -291,7 +326,7 @@ GRmenu.help
291
326
 
292
327
  | Parámetro | Tipo | Descripción |
293
328
  |------------------|------------|-------------|
294
- | `functions` | `Array` | Opciones a mostrar (`Method`, `Symbol`, `Array ["Nombre", acción]`, `Proc`/`lambda`). |
329
+ | `functions` | `Array` | Opciones a mostrar (`Method`, `Symbol`, `Array ["Nombre", acción, "Descripción"]`, `Proc`/`lambda`). |
295
330
  | `banner:` | `String` | Texto gigante a renderizar en arte ASCII 3D arriba del menú. |
296
331
  | `title:` | `String` | Título en la cabecera del marco de opciones. |
297
332
  | `subtitle:` | `String` | Subtítulo o descripción (soporta múltiples líneas con `\n`). |
@@ -300,6 +335,15 @@ GRmenu.help
300
335
  | `banner_style:` | `Integer` | Estilo de marco para el banner (1 al 20, default 3). |
301
336
  | `divider:` | `Boolean` | Dibuja líneas divisorias a la par del ancho del banner. |
302
337
  | `center:` | `Boolean` | Centra simétricamente el subtítulo y menú de opciones (default `true`). |
338
+ | `page_size:` | `Integer` | (Opcional) Número máximo de opciones visibles en pantalla para auto-paginación. |
339
+
340
+ ### `GRmenu.spinner(message, color: "cyan", &block)`
341
+
342
+ Ejecuta un bloque en segundo plano mientras dibuja una animación giratoria y muestra `✔ Mensaje ¡Listo!` al terminar.
343
+
344
+ ### `GRmenu.progress(total, title:, color:, style:, &block)`
345
+
346
+ Crea una barra de progreso porcentual dentro de un recuadro estilizado. El bloque recibe el objeto `bar` con los métodos `bar.advance(n, status:)` y `bar.set(valor, status:)`.
303
347
 
304
348
  ### `menu.set_style`
305
349
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grmenu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - grcode