grmenu 2.0.0 → 4.0.1

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 DELETED
@@ -1,1184 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'io/console'
4
- require 'json'
5
-
6
- class GRmenu
7
- CLEAR_SCREEN_SEQUENCE = "\e[H\e[2J\e[3J"
8
- HIDE_CURSOR = "\e[?25l"
9
- SHOW_CURSOR = "\e[?25h"
10
- CURSOR_HOME = "\e[H"
11
- CLEAR_TO_EOL = "\e[K"
12
- CLEAR_TO_EOS = "\e[J"
13
-
14
- module Color
15
- RESET = "\e[0m"
16
- BOLD = "\e[1m"
17
-
18
- CODES = {
19
- black: { 1 => "\e[30m", 2 => "\e[90m" },
20
- gray: { 1 => "\e[90m", 2 => "\e[38;5;245m" },
21
- grey: { 1 => "\e[90m", 2 => "\e[38;5;245m" },
22
- red: { 1 => "\e[31m", 2 => "\e[91m" },
23
- green: { 1 => "\e[32m", 2 => "\e[92m" },
24
- yellow: { 1 => "\e[33m", 2 => "\e[93m" },
25
- blue: { 1 => "\e[34m", 2 => "\e[94m" },
26
- magenta: { 1 => "\e[35m", 2 => "\e[95m" },
27
- purple: { 1 => "\e[38;5;129m", 2 => "\e[38;5;141m" },
28
- pink: { 1 => "\e[38;5;205m", 2 => "\e[38;5;218m" },
29
- cyan: { 1 => "\e[36m", 2 => "\e[96m" },
30
- aqua: { 1 => "\e[38;5;45m", 2 => "\e[38;5;51m" },
31
- orange: { 1 => "\e[38;5;208m", 2 => "\e[38;5;214m" },
32
- white: { 1 => "\e[37m", 2 => "\e[97m" }
33
- }.freeze
34
-
35
- module_function
36
-
37
- def paint(text, color_name, level = 1)
38
- code = CODES.dig(color_name.to_sym, level) || "\e[37m"
39
- "#{code}#{text}#{RESET}"
40
- end
41
-
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
-
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
-
50
- def yellow(s); paint(s, :yellow, 1); end
51
- def bright_yellow(s); paint(s, :yellow, 2); end
52
-
53
- def blue(s); paint(s, :blue, 1); end
54
- def bright_blue(s); paint(s, :blue, 2); end
55
-
56
- def magenta(s); paint(s, :magenta, 1); end
57
- def bright_magenta(s); paint(s, :magenta, 2); end
58
-
59
- def purple(s); paint(s, :purple, 1); end
60
- def bright_purple(s); paint(s, :purple, 2); end
61
-
62
- def pink(s); paint(s, :pink, 1); end
63
- def bright_pink(s); paint(s, :pink, 2); end
64
-
65
- def cyan(s); paint(s, :cyan, 1); end
66
- def bright_cyan(s); paint(s, :cyan, 2); end
67
-
68
- def aqua(s); paint(s, :aqua, 1); end
69
- def bright_aqua(s); paint(s, :aqua, 2); end
70
-
71
- def orange(s); paint(s, :orange, 1); end
72
- def bright_orange(s); paint(s, :orange, 2); end
73
-
74
- def white(s); paint(s, :white, 1); end
75
- def bright_white(s); paint(s, :white, 2); end
76
-
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
-
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
- end
92
- C = Color
93
-
94
- STYLES = {
95
- 1 => "#", 2 => "┌", 3 => "╔", 4 => "┏", 5 => "╒",
96
- 6 => "╓", 7 => "╭", 8 => "▛", 9 => "▓", 10 => "▒",
97
- 11 => "░", 12 => "█", 13 => "*", 14 => "+", 15 => "=",
98
- 16 => "~", 17 => "-", 18 => "◆", 19 => "●", 20 => "★"
99
- }.freeze
100
-
101
- COLORS = {
102
- "black" => { 1 => "\e[30m", 2 => "\e[90m" },
103
- "gray" => { 1 => "\e[90m", 2 => "\e[38;5;245m" },
104
- "grey" => { 1 => "\e[90m", 2 => "\e[38;5;245m" },
105
- "red" => { 1 => "\e[31m", 2 => "\e[91m" },
106
- "green" => { 1 => "\e[32m", 2 => "\e[92m" },
107
- "yellow" => { 1 => "\e[33m", 2 => "\e[93m" },
108
- "blue" => { 1 => "\e[34m", 2 => "\e[94m" },
109
- "magenta" => { 1 => "\e[35m", 2 => "\e[95m" },
110
- "purple" => { 1 => "\e[38;5;129m", 2 => "\e[38;5;141m" },
111
- "pink" => { 1 => "\e[38;5;205m", 2 => "\e[38;5;218m" },
112
- "cyan" => { 1 => "\e[36m", 2 => "\e[96m" },
113
- "aqua" => { 1 => "\e[38;5;45m", 2 => "\e[38;5;51m" },
114
- "orange" => { 1 => "\e[38;5;208m", 2 => "\e[38;5;214m" },
115
- "white" => { 1 => "\e[37m", 2 => "\e[97m" },
116
- "reset" => "\e[0m"
117
- }.freeze
118
-
119
- BORDERS = {
120
- 1 => { h: "=-", v: "|", tl: "#", tr: "#", bl: "#", br: "#" },
121
- 2 => { h: "─", v: "│", tl: "┌", tr: "┐", bl: "└", br: "┘" },
122
- 3 => { h: "═", v: "║", tl: "╔", tr: "╗", bl: "╚", br: "╝" },
123
- 4 => { h: "━", v: "┃", tl: "┏", tr: "┓", bl: "┗", br: "┛" },
124
- 5 => { h: "═", v: "│", tl: "╒", tr: "╕", bl: "╘", br: "╛" },
125
- 6 => { h: "─", v: "║", tl: "╓", tr: "╖", bl: "╙", br: "╜" },
126
- 7 => { h: "─", v: "│", tl: "╭", tr: "╮", bl: "╰", br: "╯" },
127
- 8 => { h: "▀", hb: "▄", v: "▌", vl: "▌", vr: "▐", tl: "▛", tr: "▜", bl: "▙", br: "▟" },
128
- 19 => { h: "●○", v: "●", tl: "●", tr: "●", bl: "●", br: "●" },
129
- 20 => { h: "★☆", v: "★", tl: "★", tr: "★", bl: "★", br: "★" }
130
- }.freeze
131
-
132
- def self._normalize_font(f)
133
- normalized = {}
134
- f.each do |key, lines|
135
- max_w = lines.map(&:length).max
136
- normalized[key] = lines.map { |line| line.ljust(max_w) }.freeze
137
- end
138
- normalized.freeze
139
- end
140
-
141
- def self._load_fonts
142
- possible_paths = [
143
- File.expand_path("data/fonts.json", __dir__),
144
- File.expand_path("../data/fonts.json", __dir__),
145
- File.expand_path("fonts.json", __dir__)
146
- ]
147
- path = possible_paths.find { |p| File.file?(p) }
148
- return {}.freeze unless path
149
-
150
- raw_fonts = JSON.parse(File.read(path))
151
- loaded = {}
152
- raw_fonts.each do |font_key, chars|
153
- font_id = font_key.to_i
154
- loaded[font_id] = _normalize_font(chars)
155
- end
156
- loaded.freeze
157
- rescue StandardError
158
- {}.freeze
159
- end
160
-
161
- FONTS = _load_fonts
162
- FONTS.each { |id, data| const_set("FONT_#{id}", data) }
163
- FONT = FONTS[1] || {}.freeze
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
-
307
- class SetStyle
308
- def initialize(
309
- border: { color: "cyan", level: 1 },
310
- options: { color: "white", level: 1 },
311
- focus: { color: "green", level: 2 },
312
- title: { color: "yellow", level: 2 },
313
- banner: { color: "magenta", level: 2 },
314
- subtitle: { color: "cyan", level: 2 },
315
- divider: { color: "blue", level: 1 },
316
- font: 1
317
- )
318
- @border = border.dup
319
- @options = options.dup
320
- @focus = focus.dup
321
- @title = title.dup
322
- @banner = banner.dup
323
- @subtitle = subtitle.dup
324
- @divider = divider.dup
325
- @font = font.to_i
326
- end
327
-
328
- def border(color_name = nil, brightness_level = 1)
329
- return @border if color_name.nil?
330
- @border = parse_color(color_name, brightness_level)
331
- end
332
- alias_method :Border, :border
333
- alias_method :set_border, :border
334
- alias_method :border=, :border
335
-
336
- def options(color_name = nil, brightness_level = 1)
337
- return @options if color_name.nil?
338
- @options = parse_color(color_name, brightness_level)
339
- end
340
- alias_method :Options, :options
341
- alias_method :set_options, :options
342
- alias_method :options=, :options
343
-
344
- def focus(color_name = nil, brightness_level = 2)
345
- return @focus if color_name.nil?
346
- @focus = parse_color(color_name, brightness_level)
347
- end
348
- alias_method :Focus, :focus
349
- alias_method :set_focus, :focus
350
- alias_method :focus=, :focus
351
-
352
- def title(color_name = nil, brightness_level = 2)
353
- return @title if color_name.nil?
354
- @title = parse_color(color_name, brightness_level)
355
- end
356
- alias_method :Title, :title
357
- alias_method :set_title, :title
358
- alias_method :title=, :title
359
-
360
- def banner(color_name = nil, brightness_level = 2)
361
- return @banner if color_name.nil?
362
- @banner = parse_color(color_name, brightness_level)
363
- end
364
- alias_method :Banner, :banner
365
- alias_method :set_banner, :banner
366
- alias_method :banner=, :banner
367
-
368
- def subtitle(color_name = nil, brightness_level = 2)
369
- return @subtitle if color_name.nil?
370
- @subtitle = parse_color(color_name, brightness_level)
371
- end
372
- alias_method :Subtitle, :subtitle
373
- alias_method :set_subtitle, :subtitle
374
- alias_method :subtitle=, :subtitle
375
-
376
- def divider(color_name = nil, brightness_level = 1)
377
- return @divider if color_name.nil?
378
- @divider = parse_color(color_name, brightness_level)
379
- end
380
- alias_method :Divider, :divider
381
- alias_method :set_divider, :divider
382
- alias_method :divider=, :divider
383
-
384
- def font(font_id = nil)
385
- return @font if font_id.nil?
386
- @font = font_id.to_i
387
- end
388
- alias_method :Font, :font
389
- alias_method :set_font, :font
390
- alias_method :font=, :font
391
-
392
- private
393
-
394
- def parse_color(color_val, default_level = 1)
395
- if color_val.is_a?(Hash)
396
- { color: (color_val[:color] || color_val["color"]).to_s, level: (color_val[:level] || color_val["level"] || default_level).to_i }
397
- else
398
- { color: color_val.to_s, level: default_level.to_i }
399
- end
400
- end
401
-
402
- class << self
403
- def border(color_name = nil, brightness_level = 1)
404
- @default_border ||= { color: "cyan", level: 1 }
405
- return @default_border if color_name.nil?
406
- @default_border = { color: color_name.to_s, level: brightness_level.to_i }
407
- end
408
- alias_method :Border, :border
409
- alias_method :border=, :border
410
-
411
- def options(color_name = nil, brightness_level = 1)
412
- @default_options ||= { color: "white", level: 1 }
413
- return @default_options if color_name.nil?
414
- @default_options = { color: color_name.to_s, level: brightness_level.to_i }
415
- end
416
- alias_method :Options, :options
417
- alias_method :options=, :options
418
-
419
- def focus(color_name = nil, brightness_level = 2)
420
- @default_focus ||= { color: "green", level: 2 }
421
- return @default_focus if color_name.nil?
422
- @default_focus = { color: color_name.to_s, level: brightness_level.to_i }
423
- end
424
- alias_method :Focus, :focus
425
- alias_method :focus=, :focus
426
-
427
- def title(color_name = nil, brightness_level = 2)
428
- @default_title ||= { color: "yellow", level: 2 }
429
- return @default_title if color_name.nil?
430
- @default_title = { color: color_name.to_s, level: brightness_level.to_i }
431
- end
432
- alias_method :Title, :title
433
- alias_method :title=, :title
434
-
435
- def banner(color_name = nil, brightness_level = 2)
436
- @default_banner ||= { color: "magenta", level: 2 }
437
- return @default_banner if color_name.nil?
438
- @default_banner = { color: color_name.to_s, level: brightness_level.to_i }
439
- end
440
- alias_method :Banner, :banner
441
- alias_method :banner=, :banner
442
-
443
- def subtitle(color_name = nil, brightness_level = 2)
444
- @default_subtitle ||= { color: "cyan", level: 2 }
445
- return @default_subtitle if color_name.nil?
446
- @default_subtitle = { color: color_name.to_s, level: brightness_level.to_i }
447
- end
448
- alias_method :Subtitle, :subtitle
449
- alias_method :subtitle=, :subtitle
450
-
451
- def divider(color_name = nil, brightness_level = 1)
452
- @default_divider ||= { color: "blue", level: 1 }
453
- return @default_divider if color_name.nil?
454
- @default_divider = { color: color_name.to_s, level: brightness_level.to_i }
455
- end
456
- alias_method :Divider, :divider
457
- alias_method :divider=, :divider
458
-
459
- def font(font_id = nil)
460
- @default_font ||= 1
461
- return @default_font if font_id.nil?
462
- @default_font = font_id.to_i
463
- end
464
- alias_method :Font, :font
465
- alias_method :set_font, :font
466
- alias_method :font=, :font
467
- end
468
- end
469
-
470
- module GRprint
471
- module_function
472
-
473
- def p(text = "", ending = "\r\n")
474
- Kernel.print("#{text}#{ending}")
475
- end
476
- end
477
-
478
- attr_accessor :functions, :title, :subtitle, :banner, :banner_style, :divider, :style, :index, :style_config, :center, :page_size
479
-
480
- alias_method :options, :functions
481
- alias_method :options=, :functions=
482
- alias_method :selected_index, :index
483
- alias_method :selected_index=, :index=
484
- alias_method :SetStyle, :style_config
485
- alias_method :set_style, :style_config
486
- alias_method :description, :subtitle
487
- alias_method :description=, :subtitle=
488
-
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
494
-
495
- def self.terminal_width
496
- cols = $stdout.winsize[1] rescue nil
497
- cols = $stdin.winsize[1] rescue nil if cols.nil? || cols <= 0
498
- (cols && cols > 0) ? cols : (ENV['COLUMNS'] ? ENV['COLUMNS'].to_i : 80)
499
- rescue StandardError
500
- 80
501
- end
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
-
511
- def self.clear_screen
512
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
513
- end
514
- class << self
515
- alias_method :clr, :clear_screen
516
- end
517
-
518
- def self.div(long = nil, color = "blue", level = 1, char = "─")
519
- width = long || [terminal_width - 2, 64].min
520
- color_code = COLORS.dig(color.to_s.downcase, level) || "\e[34m"
521
- Kernel.print("#{color_code}#{char * width}#{COLORS['reset']}\r\n")
522
- end
523
-
524
- def self.help(section = :all)
525
- w = [terminal_width - 4, 70].min
526
- w = [w, 46].max
527
- inner_w = w - 2
528
- h_line = "═" * inner_w
529
- s_line = "─" * w
530
-
531
- Kernel.print "\r\n"
532
- Kernel.print "#{Color.bright_cyan("╔" + h_line + "╗")}\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"
535
- Kernel.print "#{Color.bright_cyan("╚" + h_line + "╝")}\r\n\r\n"
536
-
537
- Kernel.print "#{Color.bright_magenta("[1] HELPERS NATIVOS")}\r\n"
538
- Kernel.print "#{Color.bright_blue(s_line)}\r\n"
539
- Kernel.print " #{Color.bright_green("GRmenu.clear_screen")} #{Color.gray("(o GRmenu.clr)")}\r\n"
540
- Kernel.print " * Limpia la terminal al instante con secuencias ANSI.\r\n"
541
- Kernel.print " #{Color.bright_green("GRmenu.continue(mensaje)")}\r\n"
542
- Kernel.print " * Pausa interactiva: espera una sola tecla en modo TTY crudo.\r\n"
543
- Kernel.print " #{Color.bright_green("GRmenu.banner(texto, delay, color:, level:, style:, font:)")}\r\n"
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"
555
- Kernel.print " #{Color.bright_green("GRmenu.div(longitud, color, level, char)")}\r\n"
556
- Kernel.print " * Dibuja linea divisoria horizontal adaptable a la consola.\r\n\r\n"
557
-
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"
588
- Kernel.print "#{Color.bright_blue(s_line)}\r\n"
589
- Kernel.print " #{Color.cyan("Uso directo: ")}#{Color.bright_white("puts Color.green(\"Texto\")")} | #{Color.bright_white("puts Color.bright_cyan(\"Texto\")")}\r\n"
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"
591
- Kernel.print " #{Color.cyan("Brillo: ")}#{Color.white("1")} = Normal, #{Color.bright_white("2")} = Brillante / Bold.\r\n\r\n"
592
-
593
- Kernel.print "#{Color.bright_magenta("[6] FUENTES ASCII 3D DEL BANNER (font: 1 al 10)")}\r\n"
594
- Kernel.print "#{Color.bright_blue(s_line)}\r\n"
595
- Kernel.print " #{Color.yellow("1")} -> #{Color.bright_white("ANSI Shadow 3D (Default)")} #{Color.cyan("[██████╗ ██╗ ██╗]")}\r\n"
596
- Kernel.print " #{Color.yellow("2")} -> #{Color.bright_white("Slant 3D (FIGlet)")} #{Color.cyan("[ ____ __ __]")}\r\n"
597
- Kernel.print " #{Color.yellow("3")} -> #{Color.bright_white("Doom / Standard 3D")} #{Color.cyan("[ ____ _ _]")}\r\n"
598
- Kernel.print " #{Color.yellow("4")} -> #{Color.bright_white("Graffiti Shadow 3D")} #{Color.cyan("[ ,---. ,--. ,--.]")}\r\n"
599
- Kernel.print " #{Color.yellow("5")} -> #{Color.bright_white("Small Slant / Mini 3D")} #{Color.cyan("[ ___ _ _]")}\r\n"
600
- Kernel.print " #{Color.yellow("6")} -> #{Color.bright_white("Modular Pipe 3D")} #{Color.cyan("[ _____ _____]")}\r\n"
601
- Kernel.print " #{Color.yellow("7")} -> #{Color.bright_white("Bubble / Round Gothic")} #{Color.cyan("[ ____ _ _]")}\r\n"
602
- Kernel.print " #{Color.yellow("8")} -> #{Color.bright_white("Double-Line Wire 3D")} #{Color.cyan("[ ╔═════╗ ║ ║]")}\r\n"
603
- Kernel.print " #{Color.yellow("9")} -> #{Color.bright_white("Solid Fat 3D Block")} #{Color.cyan("[ ██████▄ ██ ██]")}\r\n"
604
- Kernel.print " #{Color.yellow("10")}-> #{Color.bright_white("Arcade Stars Matrix")} #{Color.cyan("[ ★★★★ ★ ★]")}\r\n\r\n"
605
-
606
- Kernel.print "#{Color.bright_magenta("[7] ESTILOS DE MARCO (style / banner_style: 1 al 20)")}\r\n"
607
- Kernel.print "#{Color.bright_blue(s_line)}\r\n"
608
- Kernel.print " #{Color.yellow("3")} -> #{Color.bright_white("Doble linea")} #{Color.cyan("╔═══╗ ║ ║ ╚═══╝")} (Default en Banner)\r\n"
609
- Kernel.print " #{Color.yellow("7")} -> #{Color.bright_white("Curvas redondeadas")} #{Color.cyan("╭───╮ │ │ ╰───╯")}\r\n"
610
- Kernel.print " #{Color.yellow("4")} -> #{Color.bright_white("Linea gruesa")} #{Color.cyan("┏━━━┓ ┃ ┃ ┗━━━┛")}\r\n"
611
- Kernel.print " #{Color.yellow("2")} -> #{Color.bright_white("Linea simple")} #{Color.cyan("┌───┐ │ │ └───┘")}\r\n"
612
- Kernel.print " #{Color.yellow("8")} -> #{Color.bright_white("Bloques outline")} #{Color.cyan("▛▀▀▀▜ ▌ ▐ ▙▄▄▄▟")}\r\n"
613
- Kernel.print " #{Color.yellow("19")} -> #{Color.bright_white("Circulos")} #{Color.cyan("●○○○● ● ● ●○○○●")} (Default en Opciones)\r\n"
614
- Kernel.print " #{Color.yellow("20")} -> #{Color.bright_white("Estrellas")} #{Color.cyan("★☆☆☆★ ★ ★ ★☆☆☆★")}\r\n\r\n"
615
-
616
- Kernel.print "#{Color.bright_magenta("[8] METODOS DE CONFIGURACION (menu.set_style)")}\r\n"
617
- Kernel.print "#{Color.bright_blue(s_line)}\r\n"
618
- Kernel.print " #{Color.cyan("menu.set_style.font(id)")} -> Cambia fuente ASCII (1..10)\r\n"
619
- Kernel.print " #{Color.cyan("menu.set_style.banner(color, level)")} -> Color y brillo del banner ASCII\r\n"
620
- Kernel.print " #{Color.cyan("menu.set_style.title(color, level)")} -> Color y brillo del titulo\r\n"
621
- Kernel.print " #{Color.cyan("menu.set_style.subtitle(color, level)")} -> Color y brillo del subtitulo\r\n"
622
- Kernel.print " #{Color.cyan("menu.set_style.divider(color, level)")} -> Color y brillo de las lineas divisorias\r\n"
623
- Kernel.print " #{Color.cyan("menu.set_style.border(color, level)")} -> Color y brillo del marco de opciones\r\n"
624
- Kernel.print " #{Color.cyan("menu.set_style.options(color, level)")} -> Color y brillo de opciones no activas\r\n"
625
- Kernel.print " #{Color.cyan("menu.set_style.focus(color, level)")} -> Color y brillo de la opcion resaltada\r\n\r\n"
626
-
627
- Kernel.print "#{Color.bright_magenta("[9] EJECUCION (menu.draw)")}\r\n"
628
- Kernel.print "#{Color.bright_blue(s_line)}\r\n"
629
- Kernel.print " #{Color.bright_white("menu.draw(size_max: 38)")} -> Inicia el menu interactivo con ancho minimo.\r\n"
630
- Kernel.print "#{Color.bright_blue(s_line)}\r\n\r\n"
631
- end
632
-
633
- def help
634
- self.class.help
635
- end
636
-
637
- def self.continue(text = "Presiona cualquier tecla para continuar...")
638
- Kernel.print("#{Color.gray(text)} ")
639
- if $stdin.respond_to?(:raw) && $stdin.respond_to?(:tty?) && $stdin.tty?
640
- $stdin.raw(&:getch)
641
- elsif $stdin.respond_to?(:getch)
642
- $stdin.getch
643
- else
644
- $stdin.read(1)
645
- end
646
- Kernel.print("\r\n")
647
- end
648
-
649
- def self.build_ascii_lines(text, max_cols = terminal_width, font_id = 1)
650
- target_font = FONTS[font_id.to_i] || FONTS[1]
651
- clean_chars = text.to_s.upcase.chars.select { |c| target_font.key?(c) }
652
- return [] if clean_chars.empty?
653
-
654
- font_height = target_font.values.first.length
655
-
656
- [2, 1, 0].each do |spacing|
657
- lines = Array.new(font_height, "")
658
- clean_chars.each_with_index do |c, idx|
659
- fig = target_font[c]
660
- pad = (idx == clean_chars.length - 1) ? "" : (" " * spacing)
661
- font_height.times { |i| lines[i] += fig[i] + pad }
662
- end
663
-
664
- max_len = lines.map(&:length).max
665
- return lines if (max_len + 6) <= max_cols
666
- end
667
-
668
- nil
669
- end
670
-
671
- def self.banner(text, delay = 0, color: "magenta", level: 2, style: 3, font: 1)
672
- cols = terminal_width
673
- color_code = COLORS.dig(color.to_s.downcase, level) || "\e[1;95m"
674
- reset_code = COLORS["reset"]
675
-
676
- ascii_rows = build_ascii_lines(text, cols, font)
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]
682
-
683
- if ascii_rows
684
- max_len = ascii_rows.map(&:length).max
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")
689
- ascii_rows.each do |line|
690
- pad = " " * (max_len - line.length)
691
- Kernel.print("#{color_code}#{v_l} #{line}#{pad} #{v_r}#{reset_code}\r\n")
692
- sleep(delay) if delay > 0
693
- end
694
- Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
695
- else
696
- clean_t = text.to_s.strip
697
- box_w = [clean_t.length + 6, cols - 2].min
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")
704
- end
705
- end
706
-
707
- class << self
708
- alias_method :message, :banner
709
- alias_method :logo, :banner
710
- end
711
-
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)
713
- @functions = functions.is_a?(Array) ? functions : Array(functions)
714
-
715
- pos_title = positional_arguments[0]
716
- pos_style = positional_arguments[1]
717
-
718
- @title = (title || pos_title || keyword_arguments[:title] || "").to_s
719
- @banner = (banner || keyword_arguments[:banner] || "").to_s
720
- @subtitle = (subtitle || description || keyword_arguments[:subtitle] || keyword_arguments[:description] || "").to_s
721
- @divider = divider.nil? ? (!@banner.empty? || !@subtitle.empty?) : divider
722
- @style = (style || pos_style || keyword_arguments[:style] || 19).to_i
723
- @banner_style = (banner_style || keyword_arguments[:banner_style] || 3).to_i
724
- @center = center.nil? ? true : center
725
- @page_size = (page_size || keyword_arguments[:page_size])&.to_i
726
- @index = 0
727
-
728
- init_font = font || keyword_arguments[:font_style] || SetStyle.font || 1
729
-
730
- @style_config = SetStyle.new(
731
- border: SetStyle.border.dup,
732
- options: SetStyle.options.dup,
733
- focus: SetStyle.focus.dup,
734
- title: SetStyle.title.dup,
735
- banner: SetStyle.banner.dup,
736
- subtitle: SetStyle.subtitle.dup,
737
- divider: SetStyle.divider.dup,
738
- font: init_font
739
- )
740
- end
741
-
742
- def move_up
743
- return @index if @functions.empty?
744
- @index = (@index - 1) % @functions.length
745
- end
746
- alias_method :_up, :move_up
747
-
748
- def move_down
749
- return @index if @functions.empty?
750
- @index = (@index + 1) % @functions.length
751
- end
752
- alias_method :_down, :move_down
753
-
754
- def colorize(text, color_config)
755
- return text.to_s if color_config.nil? || color_config.empty?
756
-
757
- color_name = (color_config[:color] || color_config["color"]).to_s.downcase
758
- brightness_level = (color_config[:level] || color_config["level"] || 1).to_i
759
-
760
- color_code = COLORS.dig(color_name, brightness_level)
761
- return text.to_s unless color_code
762
-
763
- "#{color_code}#{text}#{COLORS['reset']}"
764
- end
765
- alias_method :_colorize, :colorize
766
-
767
- def build_horizontal_line(pattern, target_width)
768
- return "" if target_width <= 0 || pattern.nil? || pattern.empty?
769
-
770
- pattern_length = pattern.length
771
- repetitions_needed = (target_width.to_f / pattern_length).ceil + 1
772
- (pattern * repetitions_needed)[0...target_width]
773
- end
774
- alias_method :_hline, :build_horizontal_line
775
-
776
- def render_banner_lines(term_cols)
777
- return [[], 0] if @banner.nil? || @banner.empty?
778
-
779
- font_id = @style_config.font || 1
780
- ascii_rows = self.class.build_ascii_lines(@banner, term_cols, font_id)
781
- banner_border = BORDERS[@banner_style] || BORDERS[3]
782
- banner_color_cfg = @style_config.banner
783
-
784
- h_top = banner_border[:ht] || banner_border[:h]
785
- h_bot = banner_border[:hb] || banner_border[:h]
786
- v_l = banner_border[:vl] || banner_border[:v]
787
- v_r = banner_border[:vr] || banner_border[:v]
788
-
789
- lines = []
790
- box_w = 0
791
- if ascii_rows
792
- content_w = ascii_rows.map(&:length).max
793
- box_w = content_w + 6
794
- top_fill = build_horizontal_line(h_top, content_w + 4)
795
- bot_fill = build_horizontal_line(h_bot, content_w + 4)
796
-
797
- lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg)
798
- ascii_rows.each do |row|
799
- pad = " " * (content_w - row.length)
800
- lines << colorize("#{v_l} #{row}#{pad} #{v_r}", banner_color_cfg)
801
- end
802
- lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg)
803
- else
804
- clean_b = @banner.strip
805
- box_w = [clean_b.length + 6, term_cols - 2].min
806
- top_fill = build_horizontal_line(h_top, box_w - 2)
807
- bot_fill = build_horizontal_line(h_bot, box_w - 2)
808
-
809
- lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg)
810
- lines << colorize("#{v_l} #{clean_b.center(box_w - 4)} #{v_r}", banner_color_cfg)
811
- lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg)
812
- end
813
- [lines, box_w]
814
- end
815
-
816
- def render_lines(size_max = 20)
817
- term_cols = self.class.terminal_width
818
- term_rows = self.class.terminal_height
819
- rendered_lines = []
820
-
821
- banner_box_width = 0
822
- banner_lines_count = 0
823
- if @banner && !@banner.empty?
824
- banner_lines, banner_box_width = render_banner_lines(term_cols)
825
- rendered_lines.concat(banner_lines)
826
- rendered_lines << ""
827
- banner_lines_count = banner_lines.length + 1
828
- end
829
-
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
-
835
- calculated_width = [size_max, @title.length + 4].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?
838
- total_width = [calculated_width, term_cols - 2].min
839
-
840
- reference_width = banner_box_width > 0 ? banner_box_width : total_width
841
- margin_left = (@center && reference_width > total_width) ? " " * ((reference_width - total_width) / 2) : ""
842
-
843
- subtitle_lines_count = 0
844
- if @subtitle && !@subtitle.empty?
845
- subtitle_lines = @subtitle.lines.map(&:chomp)
846
- div_w = @divider.is_a?(Numeric) ? @divider.to_i : [reference_width, term_cols - 2].min
847
-
848
- if @divider
849
- rendered_lines << colorize("─" * div_w, @style_config.divider)
850
- subtitle_lines_count += 1
851
- end
852
-
853
- subtitle_lines.each do |sub_line|
854
- formatted_sub = @center ? sub_line.center(div_w) : sub_line
855
- rendered_lines << colorize(formatted_sub, @style_config.subtitle)
856
- subtitle_lines_count += 1
857
- end
858
-
859
- if @divider
860
- rendered_lines << colorize("─" * div_w, @style_config.divider)
861
- subtitle_lines_count += 1
862
- end
863
- rendered_lines << ""
864
- subtitle_lines_count += 1
865
- end
866
-
867
- border_color_cfg = @style_config.border
868
- options_color_cfg = @style_config.options
869
- focus_color_cfg = @style_config.focus
870
- title_color_cfg = @style_config.title
871
-
872
- box_border = BORDERS[@style]
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
-
901
- if box_border
902
- h_top = box_border[:ht] || box_border[:h]
903
- h_bot = box_border[:hb] || box_border[:h]
904
- v_l_raw = box_border[:vl] || box_border[:v]
905
- v_r_raw = box_border[:vr] || box_border[:v]
906
-
907
- top_fill = build_horizontal_line(h_top, total_width - 2)
908
- bot_fill = build_horizontal_line(h_bot, total_width - 2)
909
- mid_fill = build_horizontal_line(h_top, total_width - 2)
910
-
911
- v_left = colorize(v_l_raw, border_color_cfg)
912
- v_right = colorize(v_r_raw, border_color_cfg)
913
-
914
- top_border_line = box_border[:tl] + top_fill + box_border[:tr]
915
- rendered_lines << "#{margin_left}#{colorize(top_border_line, border_color_cfg)}"
916
-
917
- unless @title.empty?
918
- centered_title = colorize(@title.center(total_width - 4), title_color_cfg)
919
- rendered_lines << "#{margin_left}#{v_left} #{centered_title} #{v_right}"
920
-
921
- separator_line = v_l_raw + mid_fill + v_r_raw
922
- rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg)}"
923
- end
924
-
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]
932
- if @index == current_index
933
- highlighted_text = colorize("> #{option_name.ljust(avail_w)}", focus_color_cfg)
934
- rendered_lines << "#{margin_left}#{v_left} #{highlighted_text} #{v_right}"
935
- else
936
- normal_text = colorize(" #{option_name.ljust(avail_w)}", options_color_cfg)
937
- rendered_lines << "#{margin_left}#{v_left} #{normal_text} #{v_right}"
938
- end
939
- end
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
-
954
- bottom_border_line = box_border[:bl] + bot_fill + box_border[:br]
955
- rendered_lines << "#{margin_left}#{colorize(bottom_border_line, border_color_cfg)}"
956
- else
957
- symbol_char = STYLES[@style] || "#"
958
- solid_border = colorize(symbol_char, border_color_cfg)
959
- solid_line = symbol_char * total_width
960
-
961
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
962
-
963
- unless @title.empty?
964
- centered_title = colorize(@title.center(total_width - 4), title_color_cfg)
965
- rendered_lines << "#{margin_left}#{solid_border} #{centered_title} #{solid_border}"
966
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
967
- end
968
-
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]
976
- if @index == current_index
977
- highlighted_text = colorize("> #{option_name.ljust(avail_w)}", focus_color_cfg)
978
- rendered_lines << "#{margin_left}#{solid_border} #{highlighted_text} #{solid_border}"
979
- else
980
- normal_text = colorize(" #{option_name.ljust(avail_w)}", options_color_cfg)
981
- rendered_lines << "#{margin_left}#{solid_border} #{normal_text} #{solid_border}"
982
- end
983
- end
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
-
997
- rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
998
- end
999
-
1000
- rendered_lines
1001
- end
1002
-
1003
- def draw(size_max: 20, min_width: nil)
1004
- target_width = min_width || size_max || 20
1005
- action_to_execute = nil
1006
-
1007
- is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
1008
-
1009
- begin
1010
- Kernel.print("#{HIDE_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
1011
-
1012
- if is_tty
1013
- $stdin.raw do |raw_input_stream|
1014
- action_to_execute = run_interactive_loop(raw_input_stream, target_width)
1015
- end
1016
- else
1017
- action_to_execute = run_interactive_loop($stdin, target_width)
1018
- end
1019
- ensure
1020
- Kernel.print(SHOW_CURSOR)
1021
- end
1022
-
1023
- if action_to_execute
1024
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
1025
- execute_action(action_to_execute)
1026
- else
1027
- Kernel.print(CLEAR_SCREEN_SEQUENCE)
1028
- end
1029
- rescue Interrupt
1030
- Kernel.print("#{SHOW_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
1031
- nil
1032
- end
1033
-
1034
- private
1035
-
1036
- def draw_frame(target_width)
1037
- lines = render_lines(target_width)
1038
- buffer = String.new(CURSOR_HOME)
1039
- lines.each_with_index do |line, idx|
1040
- buffer << line << CLEAR_TO_EOL
1041
- buffer << "\r\n" if idx < lines.length - 1
1042
- end
1043
- buffer << CLEAR_TO_EOS
1044
- Kernel.print(buffer)
1045
- end
1046
-
1047
- def run_interactive_loop(input_stream, target_width)
1048
- draw_frame(target_width)
1049
-
1050
- while (key = read_single_key(input_stream))
1051
- break if key == "q" || key == "Q" || key == "\x03" || key == "\x04"
1052
-
1053
- if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
1054
- move_up
1055
- draw_frame(target_width)
1056
- elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
1057
- move_down
1058
- draw_frame(target_width)
1059
- elsif key == "\r" || key == "\n"
1060
- return @functions[@index]
1061
- end
1062
- end
1063
-
1064
- nil
1065
- end
1066
-
1067
- def read_single_key(input_stream)
1068
- unless input_stream.respond_to?(:tty?) && input_stream.tty?
1069
- begin
1070
- return input_stream.sysread(3) if input_stream.respond_to?(:sysread)
1071
- return input_stream.read(1)
1072
- rescue EOFError, Errno::EPIPE
1073
- return nil
1074
- end
1075
- end
1076
-
1077
- first_char = input_stream.getch
1078
- return nil if first_char.nil?
1079
-
1080
- if first_char == "\e"
1081
- begin
1082
- extra_chars = input_stream.read_nonblock(2)
1083
- first_char << extra_chars
1084
- rescue IO::WaitReadable, IO::EAGAINWaitReadable, EOFError
1085
- end
1086
- elsif first_char == "\x00" || first_char == "\xe0"
1087
- begin
1088
- second_char = input_stream.read_nonblock(1)
1089
- first_char << second_char
1090
- rescue IO::WaitReadable, IO::EAGAINWaitReadable, EOFError
1091
- second_char = input_stream.getch rescue nil
1092
- first_char << second_char if second_char
1093
- end
1094
- end
1095
-
1096
- first_char
1097
- rescue EOFError, Errno::EPIPE, Errno::ENOTTY
1098
- nil
1099
- end
1100
-
1101
- def format_auto_name(raw_name)
1102
- cleaned = raw_name.to_s.gsub(/[_-]+/, ' ').strip
1103
- cleaned.split(' ').map(&:capitalize).join(' ')
1104
- end
1105
-
1106
- def extract_name_from_action(action)
1107
- case action
1108
- when Array
1109
- action[0].to_s
1110
- when Hash
1111
- (action[:name] || action[:title] || action["name"] || action["title"] || "Opcion").to_s
1112
- when Method
1113
- format_auto_name(action.name)
1114
- when Symbol
1115
- format_auto_name(action)
1116
- when Proc
1117
- if action.respond_to?(:name) && action.name
1118
- format_auto_name(action.name)
1119
- else
1120
- "Opcion"
1121
- end
1122
- else
1123
- if action.respond_to?(:name)
1124
- format_auto_name(action.name)
1125
- elsif action.respond_to?(:title)
1126
- action.title.to_s
1127
- else
1128
- format_auto_name(action)
1129
- end
1130
- end
1131
- end
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
-
1143
- def execute_action(action)
1144
- case action
1145
- when Method, Proc
1146
- action.call
1147
- when Symbol
1148
- if Object.respond_to?(action, true)
1149
- Object.send(action)
1150
- elsif Kernel.respond_to?(action, true)
1151
- Kernel.send(action)
1152
- end
1153
- when Array
1154
- callable = action[1]
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
1175
- else
1176
- action.call if action.respond_to?(:call)
1177
- end
1178
- end
1179
- end
1180
-
1181
- Color = GRmenu::Color unless defined?(Color)
1182
- Colors = GRmenu::Color unless defined?(Colors)
1183
- C = GRmenu::Color unless defined?(C)
1184
- Grmenu = GRmenu unless defined?(Grmenu)