grmenu 0.1.4 → 0.1.6

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 (7) hide show
  1. checksums.yaml +4 -4
  2. data/GRmenu.rb +581 -104
  3. data/README.md +223 -125
  4. data/data/borders.json +85 -0
  5. data/data/colors.json +16 -0
  6. data/data/fonts.json +3005 -0
  7. metadata +9 -6
data/GRmenu.rb CHANGED
@@ -1,14 +1,95 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'io/console'
4
+ require 'json'
4
5
 
5
6
  class GRmenu
6
7
  CLEAR_SCREEN_SEQUENCE = "\e[H\e[2J\e[3J"
7
- HIDE_CURSOR = "\e[?25l"
8
- SHOW_CURSOR = "\e[?25h"
9
- CURSOR_HOME = "\e[H"
10
- CLEAR_TO_EOL = "\e[K"
11
- CLEAR_TO_EOS = "\e[J"
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)
43
+ def bright_red(s) = paint(s, :red, 2)
44
+ def dark_red(s) = paint(s, :red, 1)
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)
49
+
50
+ def yellow(s) = paint(s, :yellow, 1)
51
+ def bright_yellow(s) = paint(s, :yellow, 2)
52
+
53
+ def blue(s) = paint(s, :blue, 1)
54
+ def bright_blue(s) = paint(s, :blue, 2)
55
+
56
+ def magenta(s) = paint(s, :magenta, 1)
57
+ def bright_magenta(s) = paint(s, :magenta, 2)
58
+
59
+ def purple(s) = paint(s, :purple, 1)
60
+ def bright_purple(s) = paint(s, :purple, 2)
61
+
62
+ def pink(s) = paint(s, :pink, 1)
63
+ def bright_pink(s) = paint(s, :pink, 2)
64
+
65
+ def cyan(s) = paint(s, :cyan, 1)
66
+ def bright_cyan(s) = paint(s, :cyan, 2)
67
+
68
+ def aqua(s) = paint(s, :aqua, 1)
69
+ def bright_aqua(s) = paint(s, :aqua, 2)
70
+
71
+ def orange(s) = paint(s, :orange, 1)
72
+ def bright_orange(s) = paint(s, :orange, 2)
73
+
74
+ def white(s) = paint(s, :white, 1)
75
+ def bright_white(s) = paint(s, :white, 2)
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)
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)
91
+ end
92
+ C = Color
12
93
 
13
94
  STYLES = {
14
95
  1 => "#", 2 => "┌", 3 => "╔", 4 => "┏", 5 => "╒",
@@ -18,15 +99,21 @@ class GRmenu
18
99
  }.freeze
19
100
 
20
101
  COLORS = {
21
- "black" => { 1 => "\e[30m", 2 => "\e[90m" },
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
- "cyan" => { 1 => "\e[36m", 2 => "\e[96m" },
28
- "white" => { 1 => "\e[37m", 2 => "\e[97m" },
29
- "reset" => "\e[0m"
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"
30
117
  }.freeze
31
118
 
32
119
  BORDERS = {
@@ -37,29 +124,68 @@ class GRmenu
37
124
  5 => { h: "═", v: "│", tl: "╒", tr: "╕", bl: "╘", br: "╛" },
38
125
  6 => { h: "─", v: "║", tl: "╓", tr: "╖", bl: "╙", br: "╜" },
39
126
  7 => { h: "─", v: "│", tl: "╭", tr: "╮", bl: "╰", br: "╯" },
40
- 8 => { h: "▀", v: "▌", tl: "▛", tr: "▜", bl: "▙", br: "▟" },
127
+ 8 => { h: "▀", hb: "▄", v: "▌", vl: "▌", vr: "▐", tl: "▛", tr: "▜", bl: "▙", br: "▟" },
41
128
  19 => { h: "●○", v: "●", tl: "●", tr: "●", bl: "●", br: "●" },
42
129
  20 => { h: "★☆", v: "★", tl: "★", tr: "★", bl: "★", br: "★" }
43
130
  }.freeze
44
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
+
45
165
  class SetStyle
46
166
  def initialize(
47
- border: { color: "cyan", level: 1 },
48
- options: { color: "white", level: 1 },
49
- focus: { color: "green", level: 2 }
167
+ border: { color: "cyan", level: 1 },
168
+ options: { color: "white", level: 1 },
169
+ focus: { color: "green", level: 2 },
170
+ title: { color: "yellow", level: 2 },
171
+ banner: { color: "magenta", level: 2 },
172
+ subtitle: { color: "cyan", level: 2 },
173
+ divider: { color: "blue", level: 1 },
174
+ font: 1
50
175
  )
51
- @border = border.dup
52
- @options = options.dup
53
- @focus = focus.dup
176
+ @border = border.dup
177
+ @options = options.dup
178
+ @focus = focus.dup
179
+ @title = title.dup
180
+ @banner = banner.dup
181
+ @subtitle = subtitle.dup
182
+ @divider = divider.dup
183
+ @font = font.to_i
54
184
  end
55
185
 
56
186
  def border(color_name = nil, brightness_level = 1)
57
187
  return @border if color_name.nil?
58
- if color_name.is_a?(Hash)
59
- @border = color_name
60
- else
61
- @border = { color: color_name.to_s, level: brightness_level.to_i }
62
- end
188
+ @border = parse_color(color_name, brightness_level)
63
189
  end
64
190
  alias_method :Border, :border
65
191
  alias_method :set_border, :border
@@ -67,11 +193,7 @@ class GRmenu
67
193
 
68
194
  def options(color_name = nil, brightness_level = 1)
69
195
  return @options if color_name.nil?
70
- if color_name.is_a?(Hash)
71
- @options = color_name
72
- else
73
- @options = { color: color_name.to_s, level: brightness_level.to_i }
74
- end
196
+ @options = parse_color(color_name, brightness_level)
75
197
  end
76
198
  alias_method :Options, :options
77
199
  alias_method :set_options, :options
@@ -79,25 +201,67 @@ class GRmenu
79
201
 
80
202
  def focus(color_name = nil, brightness_level = 2)
81
203
  return @focus if color_name.nil?
82
- if color_name.is_a?(Hash)
83
- @focus = color_name
84
- else
85
- @focus = { color: color_name.to_s, level: brightness_level.to_i }
86
- end
204
+ @focus = parse_color(color_name, brightness_level)
87
205
  end
88
206
  alias_method :Focus, :focus
89
207
  alias_method :set_focus, :focus
90
208
  alias_method :focus=, :focus
91
209
 
210
+ def title(color_name = nil, brightness_level = 2)
211
+ return @title if color_name.nil?
212
+ @title = parse_color(color_name, brightness_level)
213
+ end
214
+ alias_method :Title, :title
215
+ alias_method :set_title, :title
216
+ alias_method :title=, :title
217
+
218
+ def banner(color_name = nil, brightness_level = 2)
219
+ return @banner if color_name.nil?
220
+ @banner = parse_color(color_name, brightness_level)
221
+ end
222
+ alias_method :Banner, :banner
223
+ alias_method :set_banner, :banner
224
+ alias_method :banner=, :banner
225
+
226
+ def subtitle(color_name = nil, brightness_level = 2)
227
+ return @subtitle if color_name.nil?
228
+ @subtitle = parse_color(color_name, brightness_level)
229
+ end
230
+ alias_method :Subtitle, :subtitle
231
+ alias_method :set_subtitle, :subtitle
232
+ alias_method :subtitle=, :subtitle
233
+
234
+ def divider(color_name = nil, brightness_level = 1)
235
+ return @divider if color_name.nil?
236
+ @divider = parse_color(color_name, brightness_level)
237
+ end
238
+ alias_method :Divider, :divider
239
+ alias_method :set_divider, :divider
240
+ alias_method :divider=, :divider
241
+
242
+ def font(font_id = nil)
243
+ return @font if font_id.nil?
244
+ @font = font_id.to_i
245
+ end
246
+ alias_method :Font, :font
247
+ alias_method :set_font, :font
248
+ alias_method :font=, :font
249
+
250
+ private
251
+
252
+ def parse_color(color_val, default_level = 1)
253
+ if color_val.is_a?(Hash)
254
+ { color: (color_val[:color] || color_val["color"]).to_s, level: (color_val[:level] || color_val["level"] || default_level).to_i }
255
+ else
256
+ { color: color_val.to_s, level: default_level.to_i }
257
+ end
258
+ end
259
+
92
260
  class << self
93
261
  def border(color_name = nil, brightness_level = 1)
94
262
  @default_border ||= { color: "cyan", level: 1 }
95
263
  return @default_border if color_name.nil?
96
- if color_name.is_a?(Hash)
97
- @default_border = color_name
98
- else
99
- @default_border = { color: color_name.to_s, level: brightness_level.to_i }
100
- end
264
+ @default_border = { color: color_name.to_s, level: brightness_level.to_i }
101
265
  end
102
266
  alias_method :Border, :border
103
267
  alias_method :border=, :border
@@ -105,11 +269,7 @@ class GRmenu
105
269
  def options(color_name = nil, brightness_level = 1)
106
270
  @default_options ||= { color: "white", level: 1 }
107
271
  return @default_options if color_name.nil?
108
- if color_name.is_a?(Hash)
109
- @default_options = color_name
110
- else
111
- @default_options = { color: color_name.to_s, level: brightness_level.to_i }
112
- end
272
+ @default_options = { color: color_name.to_s, level: brightness_level.to_i }
113
273
  end
114
274
  alias_method :Options, :options
115
275
  alias_method :options=, :options
@@ -117,14 +277,51 @@ class GRmenu
117
277
  def focus(color_name = nil, brightness_level = 2)
118
278
  @default_focus ||= { color: "green", level: 2 }
119
279
  return @default_focus if color_name.nil?
120
- if color_name.is_a?(Hash)
121
- @default_focus = color_name
122
- else
123
- @default_focus = { color: color_name.to_s, level: brightness_level.to_i }
124
- end
280
+ @default_focus = { color: color_name.to_s, level: brightness_level.to_i }
125
281
  end
126
282
  alias_method :Focus, :focus
127
283
  alias_method :focus=, :focus
284
+
285
+ def title(color_name = nil, brightness_level = 2)
286
+ @default_title ||= { color: "yellow", level: 2 }
287
+ return @default_title if color_name.nil?
288
+ @default_title = { color: color_name.to_s, level: brightness_level.to_i }
289
+ end
290
+ alias_method :Title, :title
291
+ alias_method :title=, :title
292
+
293
+ def banner(color_name = nil, brightness_level = 2)
294
+ @default_banner ||= { color: "magenta", level: 2 }
295
+ return @default_banner if color_name.nil?
296
+ @default_banner = { color: color_name.to_s, level: brightness_level.to_i }
297
+ end
298
+ alias_method :Banner, :banner
299
+ alias_method :banner=, :banner
300
+
301
+ def subtitle(color_name = nil, brightness_level = 2)
302
+ @default_subtitle ||= { color: "cyan", level: 2 }
303
+ return @default_subtitle if color_name.nil?
304
+ @default_subtitle = { color: color_name.to_s, level: brightness_level.to_i }
305
+ end
306
+ alias_method :Subtitle, :subtitle
307
+ alias_method :subtitle=, :subtitle
308
+
309
+ def divider(color_name = nil, brightness_level = 1)
310
+ @default_divider ||= { color: "blue", level: 1 }
311
+ return @default_divider if color_name.nil?
312
+ @default_divider = { color: color_name.to_s, level: brightness_level.to_i }
313
+ end
314
+ alias_method :Divider, :divider
315
+ alias_method :divider=, :divider
316
+
317
+ def font(font_id = nil)
318
+ @default_font ||= 1
319
+ return @default_font if font_id.nil?
320
+ @default_font = font_id.to_i
321
+ end
322
+ alias_method :Font, :font
323
+ alias_method :set_font, :font
324
+ alias_method :font=, :font
128
325
  end
129
326
  end
130
327
 
@@ -136,7 +333,7 @@ class GRmenu
136
333
  end
137
334
  end
138
335
 
139
- attr_accessor :functions, :title, :style, :index, :style_config
336
+ attr_accessor :functions, :title, :subtitle, :banner, :banner_style, :divider, :style, :index, :style_config, :center
140
337
 
141
338
  alias_method :options, :functions
142
339
  alias_method :options=, :functions=
@@ -144,37 +341,221 @@ class GRmenu
144
341
  alias_method :selected_index=, :index=
145
342
  alias_method :SetStyle, :style_config
146
343
  alias_method :set_style, :style_config
344
+ alias_method :description, :subtitle
345
+ alias_method :description=, :subtitle=
346
+
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
352
+
353
+ def self.terminal_width
354
+ cols = $stdout.winsize[1] rescue nil
355
+ cols = $stdin.winsize[1] rescue nil if cols.nil? || cols <= 0
356
+ (cols && cols > 0) ? cols : (ENV['COLUMNS'] ? ENV['COLUMNS'].to_i : 80)
357
+ rescue StandardError
358
+ 80
359
+ end
360
+
361
+ def self.clear_screen
362
+ Kernel.print(CLEAR_SCREEN_SEQUENCE)
363
+ end
364
+ class << self
365
+ alias_method :clr, :clear_screen
366
+ end
147
367
 
148
- def self.STYLES
149
- STYLES
368
+ def self.div(long = nil, color = "blue", level = 1, char = "─")
369
+ width = long || [terminal_width - 2, 64].min
370
+ color_code = COLORS.dig(color.to_s.downcase, level) || "\e[34m"
371
+ Kernel.print("#{color_code}#{char * width}#{COLORS['reset']}\r\n")
150
372
  end
151
373
 
152
- def self.COLORS
153
- COLORS
374
+ def self.help
375
+ w = [terminal_width - 4, 64].min
376
+ w = [w, 42].max
377
+ inner_w = w - 2
378
+ h_line = "═" * inner_w
379
+ s_line = "─" * w
380
+
381
+ Kernel.print "\r\n"
382
+ 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"
385
+ Kernel.print "#{Color.bright_cyan("╚" + h_line + "╝")}\r\n\r\n"
386
+
387
+ Kernel.print "#{Color.bright_magenta("[1] HELPERS NATIVOS EN MODO CRUDO")}\r\n"
388
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
389
+ Kernel.print " #{Color.bright_green("GRmenu.clear_screen")} #{Color.gray("(o GRmenu.clr)")}\r\n"
390
+ Kernel.print " * Limpia la terminal al instante con secuencias ANSI.\r\n"
391
+ Kernel.print " #{Color.bright_green("GRmenu.continue(mensaje)")}\r\n"
392
+ Kernel.print " * Pausa interactiva: espera una sola tecla en modo TTY crudo.\r\n"
393
+ Kernel.print " #{Color.bright_green("GRmenu.banner(texto, delay, color:, level:, style:, font:)")}\r\n"
394
+ Kernel.print " * Renderiza banner ASCII 3D con marco y animacion opcional.\r\n"
395
+ 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"
399
+
400
+ Kernel.print "#{Color.bright_magenta("[2] MODULO DE COLORES (Color / C)")}\r\n"
401
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
402
+ Kernel.print " #{Color.cyan("Uso directo: ")}#{Color.bright_white("puts Color.green(\"Texto\")")} | #{Color.bright_white("puts Color.bright_cyan(\"Texto\")")}\r\n"
403
+ 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
+ Kernel.print " #{Color.cyan("Brillo: ")}#{Color.white("1")} = Normal, #{Color.bright_white("2")} = Brillante / Bold.\r\n\r\n"
405
+
406
+ Kernel.print "#{Color.bright_magenta("[3] FUENTES ASCII 3D DEL BANNER (font: 1 al 10)")}\r\n"
407
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
408
+ Kernel.print " #{Color.yellow("1")} -> #{Color.bright_white("ANSI Shadow 3D (Default)")} #{Color.cyan("[██████╗ ██╗ ██╗]")}\r\n"
409
+ Kernel.print " #{Color.yellow("2")} -> #{Color.bright_white("Slant 3D (FIGlet)")} #{Color.cyan("[ ____ __ __]")}\r\n"
410
+ Kernel.print " #{Color.yellow("3")} -> #{Color.bright_white("Doom / Standard 3D")} #{Color.cyan("[ ____ _ _]")}\r\n"
411
+ Kernel.print " #{Color.yellow("4")} -> #{Color.bright_white("Graffiti Shadow 3D")} #{Color.cyan("[ ,---. ,--. ,--.]")}\r\n"
412
+ Kernel.print " #{Color.yellow("5")} -> #{Color.bright_white("Small Slant / Mini 3D")} #{Color.cyan("[ ___ _ _]")}\r\n"
413
+ Kernel.print " #{Color.yellow("6")} -> #{Color.bright_white("Modular Pipe 3D")} #{Color.cyan("[ _____ _____]")}\r\n"
414
+ Kernel.print " #{Color.yellow("7")} -> #{Color.bright_white("Bubble / Round Gothic")} #{Color.cyan("[ ____ _ _]")}\r\n"
415
+ Kernel.print " #{Color.yellow("8")} -> #{Color.bright_white("Double-Line Wire 3D")} #{Color.cyan("[ ╔═════╗ ║ ║]")}\r\n"
416
+ Kernel.print " #{Color.yellow("9")} -> #{Color.bright_white("Solid Fat 3D Block")} #{Color.cyan("[ ██████▄ ██ ██]")}\r\n"
417
+ Kernel.print " #{Color.yellow("10")}-> #{Color.bright_white("Arcade Stars Matrix")} #{Color.cyan("[ ★★★★ ★ ★]")}\r\n\r\n"
418
+
419
+ Kernel.print "#{Color.bright_magenta("[4] ESTILOS DE MARCO (style / banner_style: 1 al 20)")}\r\n"
420
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
421
+ Kernel.print " #{Color.yellow("3")} -> #{Color.bright_white("Doble linea")} #{Color.cyan("╔═══╗ ║ ║ ╚═══╝")} (Default en Banner)\r\n"
422
+ Kernel.print " #{Color.yellow("7")} -> #{Color.bright_white("Curvas redondeadas")} #{Color.cyan("╭───╮ │ │ ╰───╯")}\r\n"
423
+ Kernel.print " #{Color.yellow("4")} -> #{Color.bright_white("Linea gruesa")} #{Color.cyan("┏━━━┓ ┃ ┃ ┗━━━┛")}\r\n"
424
+ 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"
426
+ Kernel.print " #{Color.yellow("19")} -> #{Color.bright_white("Circulos")} #{Color.cyan("●○○○● ● ● ●○○○●")} (Default en Opciones)\r\n"
427
+ Kernel.print " #{Color.yellow("20")} -> #{Color.bright_white("Estrellas")} #{Color.cyan("★☆☆☆★ ★ ★ ★☆☆☆★")}\r\n\r\n"
428
+
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"
441
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
442
+ Kernel.print " #{Color.cyan("menu.set_style.font(id)")} -> Cambia fuente ASCII (1..10)\r\n"
443
+ Kernel.print " #{Color.cyan("menu.set_style.banner(color, level)")} -> Color y brillo del banner ASCII\r\n"
444
+ Kernel.print " #{Color.cyan("menu.set_style.title(color, level)")} -> Color y brillo del titulo\r\n"
445
+ Kernel.print " #{Color.cyan("menu.set_style.subtitle(color, level)")} -> Color y brillo del subtitulo\r\n"
446
+ Kernel.print " #{Color.cyan("menu.set_style.divider(color, level)")} -> Color y brillo de las lineas divisorias\r\n"
447
+ Kernel.print " #{Color.cyan("menu.set_style.border(color, level)")} -> Color y brillo del marco de opciones\r\n"
448
+ Kernel.print " #{Color.cyan("menu.set_style.options(color, level)")} -> Color y brillo de opciones no activas\r\n"
449
+ Kernel.print " #{Color.cyan("menu.set_style.focus(color, level)")} -> Color y brillo de la opcion resaltada\r\n\r\n"
450
+
451
+ Kernel.print "#{Color.bright_magenta("[7] EJECUCION (menu.draw)")}\r\n"
452
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n"
453
+ Kernel.print " #{Color.bright_white("menu.draw(size_max: 38)")} -> Inicia el menu interactivo con ancho minimo.\r\n"
454
+ Kernel.print "#{Color.bright_blue(s_line)}\r\n\r\n"
154
455
  end
155
456
 
156
- def self.BORDERS
157
- BORDERS
457
+ def help
458
+ self.class.help
158
459
  end
159
460
 
160
- def initialize(functions, *positional_arguments, title: nil, style: nil, **keyword_arguments)
461
+ def self.continue(text = "Presiona cualquier tecla para continuar...")
462
+ Kernel.print("#{Color.gray(text)} ")
463
+ if $stdin.respond_to?(:raw) && $stdin.respond_to?(:tty?) && $stdin.tty?
464
+ $stdin.raw(&:getch)
465
+ elsif $stdin.respond_to?(:getch)
466
+ $stdin.getch
467
+ else
468
+ $stdin.read(1)
469
+ end
470
+ Kernel.print("\r\n")
471
+ end
472
+
473
+ def self.build_ascii_lines(text, max_cols = terminal_width, font_id = 1)
474
+ target_font = FONTS[font_id.to_i] || FONTS[1]
475
+ clean_chars = text.to_s.upcase.chars.select { |c| target_font.key?(c) }
476
+ return [] if clean_chars.empty?
477
+
478
+ font_height = target_font.values.first.length
479
+
480
+ [2, 1, 0].each do |spacing|
481
+ lines = Array.new(font_height, "")
482
+ clean_chars.each_with_index do |c, idx|
483
+ fig = target_font[c]
484
+ pad = (idx == clean_chars.length - 1) ? "" : (" " * spacing)
485
+ font_height.times { |i| lines[i] += fig[i] + pad }
486
+ end
487
+
488
+ max_len = lines.map(&:length).max
489
+ return lines if (max_len + 6) <= max_cols
490
+ end
491
+
492
+ nil
493
+ end
494
+
495
+ def self.banner(text, delay = 0, color: "magenta", level: 2, style: 3, font: 1)
496
+ cols = terminal_width
497
+ color_code = COLORS.dig(color.to_s.downcase, level) || "\e[1;95m"
498
+ reset_code = COLORS["reset"]
499
+
500
+ ascii_rows = build_ascii_lines(text, cols, font)
501
+ border_cfg = BORDERS[style] || BORDERS[3]
502
+
503
+ if ascii_rows
504
+ 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")
508
+ ascii_rows.each do |line|
509
+ pad = " " * (max_len - line.length)
510
+ Kernel.print("#{color_code}#{border_cfg[:v]} #{line}#{pad} #{border_cfg[:v]}#{reset_code}\r\n")
511
+ sleep(delay) if delay > 0
512
+ end
513
+ Kernel.print("#{color_code}#{border_cfg[:bl]}#{h_fill}#{border_cfg[:br]}#{reset_code}\r\n")
514
+ else
515
+ clean_t = text.to_s.strip
516
+ 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")
521
+ end
522
+ end
523
+
524
+ class << self
525
+ alias_method :message, :banner
526
+ alias_method :logo, :banner
527
+ end
528
+
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)
161
530
  @functions = functions.is_a?(Array) ? functions : Array(functions)
162
531
 
163
532
  pos_title = positional_arguments[0]
164
533
  pos_style = positional_arguments[1]
165
534
 
166
- @title = (title || pos_title || keyword_arguments[:title] || "").to_s
167
- @style = (style || pos_style || keyword_arguments[:style] || 19).to_i
168
- @index = 0
169
- @clear_seq = CLEAR_SCREEN_SEQUENCE
535
+ @title = (title || pos_title || keyword_arguments[:title] || "").to_s
536
+ @banner = (banner || keyword_arguments[:banner] || "").to_s
537
+ @subtitle = (subtitle || description || keyword_arguments[:subtitle] || keyword_arguments[:description] || "").to_s
538
+ @divider = divider.nil? ? (!@banner.empty? || !@subtitle.empty?) : divider
539
+ @style = (style || pos_style || keyword_arguments[:style] || 19).to_i
540
+ @banner_style = (banner_style || keyword_arguments[:banner_style] || 3).to_i
541
+ @center = center.nil? ? true : center
542
+ @index = 0
543
+ @clear_seq = CLEAR_SCREEN_SEQUENCE
544
+
545
+ init_font = font || keyword_arguments[:font_style] || SetStyle.font || 1
170
546
 
171
547
  @style_config = SetStyle.new(
172
- border: SetStyle.border.dup,
173
- options: SetStyle.options.dup,
174
- focus: SetStyle.focus.dup
548
+ border: SetStyle.border.dup,
549
+ options: SetStyle.options.dup,
550
+ focus: SetStyle.focus.dup,
551
+ title: SetStyle.title.dup,
552
+ banner: SetStyle.banner.dup,
553
+ subtitle: SetStyle.subtitle.dup,
554
+ divider: SetStyle.divider.dup,
555
+ font: init_font
175
556
  )
176
557
  end
177
-
558
+
178
559
  def move_up
179
560
  return @index if @functions.empty?
180
561
  @index = (@index - 1) % @functions.length
@@ -209,73 +590,153 @@ class GRmenu
209
590
  end
210
591
  alias_method :_hline, :build_horizontal_line
211
592
 
212
- def menu
213
- Kernel.print("Press any key to start ...\r\n")
593
+ def render_banner_lines(term_cols)
594
+ return [[], 0] if @banner.nil? || @banner.empty?
595
+
596
+ font_id = @style_config.font || 1
597
+ ascii_rows = self.class.build_ascii_lines(@banner, term_cols, font_id)
598
+ banner_border = BORDERS[@banner_style] || BORDERS[3]
599
+ banner_color_cfg = @style_config.banner
600
+
601
+ lines = []
602
+ box_w = 0
603
+ h_top = banner_border[:ht] || banner_border[:h]
604
+ h_bot = banner_border[:hb] || banner_border[:h]
605
+ v_l = banner_border[:vl] || banner_border[:v]
606
+ v_r = banner_border[:vr] || banner_border[:v]
607
+
608
+ if ascii_rows
609
+ content_w = ascii_rows.map(&:length).max
610
+ box_w = content_w + 6
611
+ top_fill = build_horizontal_line(h_top, content_w + 4)
612
+ bot_fill = build_horizontal_line(h_bot, content_w + 4)
613
+
614
+ lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg)
615
+ ascii_rows.each do |row|
616
+ pad = " " * (content_w - row.length)
617
+ lines << colorize("#{v_l} #{row}#{pad} #{v_r}", banner_color_cfg)
618
+ end
619
+ lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg)
620
+ else
621
+ clean_b = @banner.strip
622
+ box_w = [clean_b.length + 6, term_cols - 2].min
623
+ top_fill = build_horizontal_line(h_top, box_w - 2)
624
+ bot_fill = build_horizontal_line(h_bot, box_w - 2)
625
+
626
+ lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg)
627
+ lines << colorize("#{v_l} #{clean_b.center(box_w - 4)} #{v_r}", banner_color_cfg)
628
+ lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg)
629
+ end
630
+ [lines, box_w]
214
631
  end
215
632
 
216
633
  def render_lines(size_max = 20)
634
+ term_cols = self.class.terminal_width
635
+ rendered_lines = []
636
+
637
+ banner_box_width = 0
638
+ if @banner && !@banner.empty?
639
+ banner_lines, banner_box_width = render_banner_lines(term_cols)
640
+ rendered_lines.concat(banner_lines)
641
+ rendered_lines << ""
642
+ end
643
+
217
644
  option_names = @functions.map { |func| extract_name_from_action(func) }
645
+ calculated_width = [size_max, @title.length + 4].max
646
+ calculated_width = ([calculated_width] + option_names.map { |name| name.length + 6 }).max
647
+ total_width = [calculated_width, term_cols - 2].min
648
+
649
+ reference_width = banner_box_width > 0 ? banner_box_width : total_width
650
+ margin_left = (@center && reference_width > total_width) ? " " * ((reference_width - total_width) / 2) : ""
651
+
652
+ if @subtitle && !@subtitle.empty?
653
+ subtitle_lines = @subtitle.lines.map(&:chomp)
654
+ div_w = @divider.is_a?(Numeric) ? @divider.to_i : [reference_width, term_cols - 2].min
655
+
656
+ if @divider
657
+ rendered_lines << colorize("─" * div_w, @style_config.divider)
658
+ end
659
+
660
+ subtitle_lines.each do |sub_line|
661
+ formatted_sub = @center ? sub_line.center(div_w) : sub_line
662
+ rendered_lines << colorize(formatted_sub, @style_config.subtitle)
663
+ end
218
664
 
219
- total_width = [([size_max] + option_names.map { |name| name.length + 4 }).max, @title.length + 4].max unless @title.empty?
665
+ if @divider
666
+ rendered_lines << colorize("─" * div_w, @style_config.divider)
667
+ end
668
+ rendered_lines << ""
669
+ end
220
670
 
221
671
  border_color_cfg = @style_config.border
222
672
  options_color_cfg = @style_config.options
223
673
  focus_color_cfg = @style_config.focus
674
+ title_color_cfg = @style_config.title
224
675
 
225
676
  box_border = BORDERS[@style]
226
- rendered_lines = []
227
677
 
228
678
  if box_border
229
- horizontal_fill = build_horizontal_line(box_border[:h], total_width - 2)
230
- vertical_char = colorize(box_border[:v], border_color_cfg)
679
+ h_top = box_border[:ht] || box_border[:h]
680
+ h_bot = box_border[:hb] || box_border[:h]
681
+ v_l_raw = box_border[:vl] || box_border[:v]
682
+ v_r_raw = box_border[:vr] || box_border[:v]
683
+
684
+ top_fill = build_horizontal_line(h_top, total_width - 2)
685
+ bot_fill = build_horizontal_line(h_bot, total_width - 2)
686
+ mid_fill = build_horizontal_line(h_top, total_width - 2)
231
687
 
232
- top_border_line = box_border[:tl] + horizontal_fill + box_border[:tr]
233
- rendered_lines << colorize(top_border_line, border_color_cfg)
688
+ v_left = colorize(v_l_raw, border_color_cfg)
689
+ v_right = colorize(v_r_raw, border_color_cfg)
690
+
691
+ top_border_line = box_border[:tl] + top_fill + box_border[:tr]
692
+ rendered_lines << "#{margin_left}#{colorize(top_border_line, border_color_cfg)}"
234
693
 
235
694
  unless @title.empty?
236
- centered_title = @title.center(total_width - 4)
237
- rendered_lines << "#{vertical_char} #{centered_title} #{vertical_char}"
695
+ centered_title = colorize(@title.center(total_width - 4), title_color_cfg)
696
+ rendered_lines << "#{margin_left}#{v_left} #{centered_title} #{v_right}"
238
697
 
239
- separator_line = box_border[:v] + horizontal_fill + box_border[:v]
240
- rendered_lines << colorize(separator_line, border_color_cfg)
698
+ separator_line = v_l_raw + mid_fill + v_r_raw
699
+ rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg)}"
241
700
  end
242
701
 
243
702
  option_names.each_with_index do |option_name, current_index|
703
+ avail_w = [total_width - 6, 1].max
244
704
  if @index == current_index
245
- highlighted_text = colorize(">#{option_name.ljust(total_width - 6)}", focus_color_cfg)
246
- rendered_lines << "#{vertical_char} #{highlighted_text} #{vertical_char}"
705
+ highlighted_text = colorize("> #{option_name.ljust(avail_w)}", focus_color_cfg)
706
+ rendered_lines << "#{margin_left}#{v_left} #{highlighted_text} #{v_right}"
247
707
  else
248
- normal_text = colorize("> #{option_name.ljust(total_width - 6)}", options_color_cfg)
249
- rendered_lines << "#{vertical_char} #{normal_text} #{vertical_char}"
708
+ normal_text = colorize(" #{option_name.ljust(avail_w)}", options_color_cfg)
709
+ rendered_lines << "#{margin_left}#{v_left} #{normal_text} #{v_right}"
250
710
  end
251
711
  end
252
712
 
253
- bottom_border_line = box_border[:bl] + horizontal_fill + box_border[:br]
254
- rendered_lines << colorize(bottom_border_line, border_color_cfg)
713
+ bottom_border_line = box_border[:bl] + bot_fill + box_border[:br]
714
+ rendered_lines << "#{margin_left}#{colorize(bottom_border_line, border_color_cfg)}"
255
715
  else
256
716
  symbol_char = STYLES[@style] || "#"
257
717
  solid_border = colorize(symbol_char, border_color_cfg)
258
718
  solid_line = symbol_char * total_width
259
719
 
260
- rendered_lines << colorize(solid_line, border_color_cfg)
720
+ rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
261
721
 
262
722
  unless @title.empty?
263
- centered_title = @title.center(total_width - 4)
264
- rendered_lines << "#{solid_border} #{centered_title} #{solid_border}"
265
- rendered_lines << colorize(solid_line, border_color_cfg)
723
+ centered_title = colorize(@title.center(total_width - 4), title_color_cfg)
724
+ rendered_lines << "#{margin_left}#{solid_border} #{centered_title} #{solid_border}"
725
+ rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
266
726
  end
267
727
 
268
728
  option_names.each_with_index do |option_name, current_index|
729
+ avail_w = [total_width - 6, 1].max
269
730
  if @index == current_index
270
- highlighted_text = colorize(option_name.ljust(total_width - 4), focus_color_cfg)
271
- rendered_lines << "#{solid_border} #{highlighted_text} #{solid_border}"
731
+ highlighted_text = colorize("> #{option_name.ljust(avail_w)}", focus_color_cfg)
732
+ rendered_lines << "#{margin_left}#{solid_border} #{highlighted_text} #{solid_border}"
272
733
  else
273
- normal_text = colorize(option_name.ljust(total_width - 4), options_color_cfg)
274
- rendered_lines << "#{solid_border} #{normal_text} #{solid_border}"
734
+ normal_text = colorize(" #{option_name.ljust(avail_w)}", options_color_cfg)
735
+ rendered_lines << "#{margin_left}#{solid_border} #{normal_text} #{solid_border}"
275
736
  end
276
737
  end
277
738
 
278
- rendered_lines << colorize(solid_line, border_color_cfg)
739
+ rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
279
740
  end
280
741
 
281
742
  rendered_lines
@@ -330,10 +791,10 @@ class GRmenu
330
791
  while (key = read_single_key(input_stream))
331
792
  break if key == "q" || key == "Q" || key == "\x03" || key == "\x04"
332
793
 
333
- if key == "\e[A" || key == "\eOA"
794
+ if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
334
795
  move_up
335
796
  draw_frame(target_width)
336
- elsif key == "\e[B" || key == "\eOB"
797
+ elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
337
798
  move_down
338
799
  draw_frame(target_width)
339
800
  elsif key == "\r" || key == "\n"
@@ -363,6 +824,14 @@ class GRmenu
363
824
  first_char << extra_chars
364
825
  rescue IO::WaitReadable, IO::EAGAINWaitReadable, EOFError
365
826
  end
827
+ elsif first_char == "\x00" || first_char == "\xe0"
828
+ begin
829
+ second_char = input_stream.read_nonblock(1)
830
+ first_char << second_char
831
+ rescue IO::WaitReadable, IO::EAGAINWaitReadable, EOFError
832
+ second_char = input_stream.getch rescue nil
833
+ first_char << second_char if second_char
834
+ end
366
835
  end
367
836
 
368
837
  first_char
@@ -370,27 +839,32 @@ class GRmenu
370
839
  nil
371
840
  end
372
841
 
842
+ def format_auto_name(raw_name)
843
+ cleaned = raw_name.to_s.gsub(/[_-]+/, ' ').strip
844
+ cleaned.split(' ').map(&:capitalize).join(' ')
845
+ end
846
+
373
847
  def extract_name_from_action(action)
374
848
  case action
375
- when Method
376
- action.name.to_s
377
- when Symbol
378
- action.to_s
379
849
  when Array
380
850
  action[0].to_s
851
+ when Method
852
+ format_auto_name(action.name)
853
+ when Symbol
854
+ format_auto_name(action)
381
855
  when Proc
382
856
  if action.respond_to?(:name) && action.name
383
- action.name.to_s
857
+ format_auto_name(action.name)
384
858
  else
385
- "opcion"
859
+ "Opcion"
386
860
  end
387
861
  else
388
862
  if action.respond_to?(:name)
389
- action.name.to_s
863
+ format_auto_name(action.name)
390
864
  elsif action.respond_to?(:title)
391
865
  action.title.to_s
392
866
  else
393
- action.to_s
867
+ format_auto_name(action)
394
868
  end
395
869
  end
396
870
  end
@@ -414,4 +888,7 @@ class GRmenu
414
888
  end
415
889
  end
416
890
 
891
+ Color = GRmenu::Color unless defined?(Color)
892
+ Colors = GRmenu::Color unless defined?(Colors)
893
+ C = GRmenu::Color unless defined?(C)
417
894
  Grmenu = GRmenu unless defined?(Grmenu)