grmenu 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,9 +52,25 @@ GRmenu::config<-1->
52
52
  unchecked_mark:: [ ]
53
53
  >>
54
54
 
55
+ <<input
56
+ style:: 4
57
+ border_color:: yellow:2
58
+ title_color:: green:2
59
+ label_color:: orange:2
60
+ >>
61
+
55
62
  <<modal
56
63
  style:: 4
57
64
  border_color:: yellow:2
58
- prompt_color:: magenta:2
59
- mask_char:: *
65
+ title_color:: green:2
66
+ content_color:: white:1
67
+ shadow:: true
68
+ >>
69
+
70
+ <<form
71
+ style:: 4
72
+ border_color:: yellow:2
73
+ title_color:: green:2
74
+ focus_color:: magenta:2
75
+ shadow:: true
60
76
  >>
data/data/themes/nord.gr CHANGED
@@ -52,9 +52,25 @@ GRmenu::config<-1->
52
52
  unchecked_mark:: [ ]
53
53
  >>
54
54
 
55
+ <<input
56
+ style:: 6
57
+ border_color:: blue:2
58
+ title_color:: cyan:2
59
+ label_color:: aqua:2
60
+ >>
61
+
55
62
  <<modal
56
63
  style:: 6
57
64
  border_color:: blue:2
58
- prompt_color:: cyan:2
59
- mask_char:: *
65
+ title_color:: cyan:2
66
+ content_color:: white:1
67
+ shadow:: true
68
+ >>
69
+
70
+ <<form
71
+ style:: 6
72
+ border_color:: blue:2
73
+ title_color:: cyan:2
74
+ focus_color:: aqua:2
75
+ shadow:: true
60
76
  >>
@@ -52,9 +52,25 @@ GRmenu::config<-1->
52
52
  unchecked_mark:: [ ]
53
53
  >>
54
54
 
55
+ <<input
56
+ style:: 19
57
+ border_color:: orange:2
58
+ title_color:: yellow:2
59
+ label_color:: pink:2
60
+ >>
61
+
55
62
  <<modal
56
63
  style:: 19
57
64
  border_color:: orange:2
58
- prompt_color:: yellow:2
59
- mask_char:: *
65
+ title_color:: yellow:2
66
+ content_color:: white:1
67
+ shadow:: true
68
+ >>
69
+
70
+ <<form
71
+ style:: 19
72
+ border_color:: orange:2
73
+ title_color:: yellow:2
74
+ focus_color:: yellow:2
75
+ shadow:: true
60
76
  >>
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GRmenu
4
+ # Divide una cadena con secuencias ANSI en un arreglo de caracteres individuales,
5
+ # asociando a cada caracter el código de estilo ANSI que tenía activo.
6
+ # Esto permite manipular o animar la matriz de texto celda por celda sin romper escapes.
7
+ def self.split_ansi_chars(str)
8
+ segments = []
9
+ current_style = String.new("")
10
+ in_escape = false
11
+ escape_buffer = String.new("")
12
+
13
+ str.to_s.each_char do |ch|
14
+ if ch == "\e"
15
+ in_escape = true
16
+ escape_buffer << ch
17
+ next
18
+ end
19
+ if in_escape
20
+ escape_buffer << ch
21
+ # Las secuencias de formato de estilo terminan en una letra
22
+ if ch =~ /[a-zA-Z]/
23
+ in_escape = false
24
+ current_style = escape_buffer.dup
25
+ escape_buffer.clear
26
+ end
27
+ next
28
+ end
29
+ segments << { char: ch, style: current_style.dup }
30
+ end
31
+ segments
32
+ end
33
+
34
+ # Ejecuta una animación de transición al presentar el menú en pantalla.
35
+ # Tipos soportados:
36
+ # - :diagonal -> Ola diagonal progresiva que revela el menú desde la esquina superior izquierda.
37
+ # - :linear -> Aparición línea por línea de arriba hacia abajo.
38
+ # - :fade -> Efecto de aparición gradual aumentando el brillo de los caracteres.
39
+ # - :rainbow -> Barrido espectral multicolor continuo de arcoíris.
40
+ # - :chroma -> Alias de rainbow.
41
+ def self.animate_render(lines, type = :diagonal, delay = 0.012)
42
+ type_str = type.to_s.downcase
43
+ return if lines.nil? || lines.empty?
44
+ reset_seq = ansi_reset
45
+
46
+ case type_str
47
+ when "diagonal"
48
+ # Tokeniza cada fila conservando sus estilos ANSI
49
+ parsed_rows = lines.map { |l| split_ansi_chars(l) }
50
+ max_len = parsed_rows.map(&:length).max || 0
51
+ total_steps = max_len + (parsed_rows.length * 2)
52
+ step = 0
53
+
54
+ while step <= total_steps
55
+ buffer = String.new(CURSOR_HOME)
56
+ parsed_rows.each_with_index do |row_segments, y|
57
+ rendered_row = String.new("")
58
+ row_segments.each_with_index do |segment, x|
59
+ # Ecuación de frente de onda diagonal: los puntos donde (x + 2y) <= step ya se revelaron
60
+ if (x + y * 2) <= step
61
+ rendered_row << segment[:style] << segment[:char] << reset_seq
62
+ else
63
+ rendered_row << " "
64
+ end
65
+ end
66
+ buffer << rendered_row << CLEAR_TO_EOL << "\r\n"
67
+ end
68
+ buffer << CLEAR_TO_EOS
69
+ Kernel.print(buffer)
70
+ $stdout.flush
71
+ sleep(delay)
72
+ step += 4 # Incremento de paso para mantener fluidez a 30-60 FPS
73
+ end
74
+ when "linear"
75
+ buffer = String.new(CURSOR_HOME)
76
+ lines.each do |line|
77
+ Kernel.print("#{line}#{CLEAR_TO_EOL}\r\n")
78
+ $stdout.flush
79
+ sleep(delay * 3)
80
+ end
81
+ when "fade"
82
+ [1, 2].each do |lvl|
83
+ buffer = String.new(CURSOR_HOME)
84
+ lines.each do |line|
85
+ clean_line = line.gsub(/\e\[[0-9;]*m/, '')
86
+ buffer << ansi_color("white", lvl) << clean_line << reset_seq << CLEAR_TO_EOL << "\r\n"
87
+ end
88
+ buffer << CLEAR_TO_EOS
89
+ Kernel.print(buffer)
90
+ $stdout.flush
91
+ sleep(delay * 8)
92
+ end
93
+ when "rainbow", "chroma", "rgb"
94
+ # Barrido de espectro cromático que revela la matriz con una ola multicolor viva
95
+ parsed_rows = lines.map { |l| split_ansi_chars(l) }
96
+ max_len = parsed_rows.map(&:length).max || 0
97
+ total_steps = max_len + (parsed_rows.length * 2)
98
+ step = 0
99
+
100
+ while step <= total_steps
101
+ buffer = String.new(CURSOR_HOME)
102
+ parsed_rows.each_with_index do |row_segments, y|
103
+ rendered_row = String.new("")
104
+ row_segments.each_with_index do |segment, x|
105
+ if (x + y * 2) <= step
106
+ # Al revelarse, genera la secuencia espectral de color arcoíris
107
+ wave_color = rgb_color(step * 0.12, (x + y * 2) * 0.08)
108
+ rendered_row << wave_color << segment[:char] << reset_seq
109
+ else
110
+ rendered_row << " "
111
+ end
112
+ end
113
+ buffer << rendered_row << CLEAR_TO_EOL << "\r\n"
114
+ end
115
+ buffer << CLEAR_TO_EOS
116
+ Kernel.print(buffer)
117
+ $stdout.flush
118
+ sleep(delay)
119
+ step += 4
120
+ end
121
+ when "chromatic", "cromatico"
122
+ # Transición de aparición con brillo cromático gradual
123
+ [0.3, 0.6, 1.0].each do |factor|
124
+ buffer = String.new(CURSOR_HOME)
125
+ lines.each do |line|
126
+ clean_line = line.gsub(/\e\[[0-9;]*m/, '')
127
+ r = (60 * factor).to_i
128
+ g = (255 * factor).to_i
129
+ b = (255 * factor).to_i
130
+ buffer << "\e[38;2;#{r};#{g};#{b}m#{clean_line}#{reset_seq}" << CLEAR_TO_EOL << "\r\n"
131
+ end
132
+ buffer << CLEAR_TO_EOS
133
+ Kernel.print(buffer)
134
+ $stdout.flush
135
+ sleep(delay * 6)
136
+ end
137
+ end
138
+ end
139
+ end
data/grmenu/banner.rb ADDED
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GRmenu
4
+ # Construye las filas de arte ASCII 3D para un texto dado utilizando una de las 10 fuentes.
5
+ # Intenta ajustar el espaciado entre letras (2, 1 o 0 espacios) para que el banner quepa
6
+ # comodamente dentro del ancho de la terminal sin desbordar.
7
+ def self.build_ascii_lines(text, max_cols = terminal_width, font_id = 1)
8
+ target_font = FONTS[font_id.to_i] || FONTS[1]
9
+ clean_chars = text.to_s.upcase.chars.select { |char| target_font.key?(char) }
10
+ return [] if clean_chars.empty?
11
+
12
+ font_height = target_font.values.first.length
13
+
14
+ # Prueba sucesivamente con espaciado amplio (2), medio (1) y compacto (0)
15
+ [2, 1, 0].each do |spacing|
16
+ lines = Array.new(font_height, "")
17
+ clean_chars.each_with_index do |char, idx|
18
+ glyph_rows = target_font[char]
19
+ padding = (idx == clean_chars.length - 1) ? "" : (" " * spacing)
20
+ font_height.times { |row_i| lines[row_i] += glyph_rows[row_i] + padding }
21
+ end
22
+
23
+ max_line_len = lines.map { |line| display_width(line) }.max
24
+ # Si cabe con holgura en las columnas de la terminal, lo retornamos
25
+ return lines if (max_line_len + 6) <= max_cols
26
+ end
27
+
28
+ # Si el texto es demasiado largo para cualquier espaciado ASCII, retornamos nil para fallback a texto plano
29
+ nil
30
+ end
31
+
32
+ # Renderiza un banner ASCII 3D enmarcado directamente en la terminal.
33
+ # Soporta colores ANSI calibrados o degradado continuo RGB Chroma Wave.
34
+ def self.banner(text, delay = 0, color: "magenta", level: 2, style: 3, font: 1)
35
+ cols = terminal_width
36
+ is_rgb = (color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma")
37
+ color_code = is_rgb ? "" : ansi_color(color, level)
38
+ reset_code = ansi_reset
39
+
40
+ ascii_rows = build_ascii_lines(text, cols, font)
41
+ border_cfg = BORDERS[style] || BORDERS[3]
42
+ h_top = border_cfg[:ht] || border_cfg[:h]
43
+ h_bot = border_cfg[:hb] || border_cfg[:h]
44
+ v_l = border_cfg[:vl] || border_cfg[:v]
45
+ v_r = border_cfg[:vr] || border_cfg[:v]
46
+
47
+ # Caso A: El texto cupo como arte ASCII 3D
48
+ if ascii_rows
49
+ max_len = ascii_rows.map { |r| display_width(r) }.max
50
+ # Calcula la linea de relleno superior e inferior repitiendo el caracter de borde
51
+ top_fill = (h_top * ((max_len + 4).to_f / h_top.length).ceil)[0...(max_len + 4)]
52
+ bot_fill = (h_bot * ((max_len + 4).to_f / h_bot.length).ceil)[0...(max_len + 4)]
53
+
54
+ if is_rgb
55
+ Kernel.print("#{Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")}\r\n")
56
+ ascii_rows.each_with_index do |line, idx|
57
+ pad = " " * (max_len - display_width(line))
58
+ row_content = " #{line}#{pad} "
59
+ Kernel.print("#{Color.rgb(v_l)}#{Color.rgb(row_content, idx * 0.2)}#{Color.rgb(v_r)}\r\n")
60
+ sleep(delay) if delay > 0
61
+ end
62
+ Kernel.print("#{Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")}\r\n")
63
+ else
64
+ Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
65
+ ascii_rows.each do |line|
66
+ pad = " " * (max_len - display_width(line))
67
+ Kernel.print("#{color_code}#{v_l} #{line}#{pad} #{v_r}#{reset_code}\r\n")
68
+ sleep(delay) if delay > 0
69
+ end
70
+ Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
71
+ end
72
+ # Caso B: Fallback a texto normal centrado dentro de una caja
73
+ else
74
+ clean_t = text.to_s.strip
75
+ box_w = [display_width(clean_t) + 6, cols - 2].min
76
+ top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
77
+ bot_fill = (h_bot * ((box_w - 2).to_f / h_bot.length).ceil)[0...(box_w - 2)]
78
+
79
+ pad_t = [box_w - 4 - display_width(clean_t), 0].max
80
+ l_p = " " * (pad_t / 2)
81
+ r_p = " " * (pad_t - (pad_t / 2))
82
+
83
+ if is_rgb
84
+ Kernel.print("#{Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")}\r\n")
85
+ Kernel.print("#{Color.rgb(v_l)} #{Color.rgb(l_p + clean_t + r_p)} #{Color.rgb(v_r)}\r\n")
86
+ Kernel.print("#{Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")}\r\n")
87
+ else
88
+ Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
89
+ Kernel.print("#{color_code}#{v_l} #{l_p}#{clean_t}#{r_p} #{v_r}#{reset_code}\r\n")
90
+ Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
91
+ end
92
+ end
93
+ end
94
+
95
+ class << self
96
+ alias_method :message, :banner
97
+ alias_method :logo, :banner
98
+ end
99
+ end
data/grmenu/color.rb ADDED
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GRmenu
4
+ # Genera un color TrueColor (24 bits) basado en una onda senoidal cromática.
5
+ # Los tres canales (R, G, B) están desfasados en 120 grados (2*pi/3 = 2.0943951 rad)
6
+ # para producir una transición de arcoíris fluida a lo largo del tiempo.
7
+ def self.rgb_color(tick, offset = 0.0)
8
+ time = tick.to_f + offset.to_f
9
+ red = (Math.sin(time) * 127 + 128).clamp(0, 255).to_i
10
+ green = (Math.sin(time + 2.0943951) * 127 + 128).clamp(0, 255).to_i
11
+ blue = (Math.sin(time + 4.1887902) * 127 + 128).clamp(0, 255).to_i
12
+ "\e[38;2;#{red};#{green};#{blue}m"
13
+ end
14
+
15
+ # Resuelve cualquier especificación de color a su secuencia de escape ANSI correspondiente.
16
+ # Admite:
17
+ # - Nombres integrados con nivel ("cyan:2", "red:1", etc.)
18
+ # - Palabras clave dinámicas ("rgb", "chroma", "rainbow")
19
+ # - Prefijos cromáticos ("chromatic_cyan", "cromatico_red")
20
+ # - Hexadecimal de 6 dígitos ("#FF0055" o "FF0055")
21
+ # - Hexadecimal shorthand de 3 dígitos ("#F05" o "F05")
22
+ def self.ansi_color(color_name, level = 1)
23
+ clean_name = color_name.to_s.downcase.strip
24
+ if clean_name.include?(":")
25
+ parts = clean_name.split(":")
26
+ clean_name = parts[0].strip
27
+ level = parts[1].to_i if parts[1] && !parts[1].empty?
28
+ end
29
+
30
+ return rgb_color(0.0) if clean_name == "rgb" || clean_name == "rainbow" || clean_name == "chroma"
31
+
32
+ # Hexadecimal de 6 dígitos (#RRGGBB)
33
+ if clean_name =~ /\A#?([0-9a-f]{6})\z/i
34
+ hex_code = $1
35
+ r = hex_code[0..1].to_i(16)
36
+ g = hex_code[2..3].to_i(16)
37
+ b = hex_code[4..5].to_i(16)
38
+ return "\e[38;2;#{r};#{g};#{b}m"
39
+ # Hexadecimal shorthand de 3 dígitos (#RGB) duplicado a (#RRGGBB)
40
+ elsif clean_name =~ /\A#?([0-9a-f]{3})\z/i
41
+ hex_code = $1
42
+ r = (hex_code[0] * 2).to_i(16)
43
+ g = (hex_code[1] * 2).to_i(16)
44
+ b = (hex_code[2] * 2).to_i(16)
45
+ return "\e[38;2;#{r};#{g};#{b}m"
46
+ end
47
+
48
+ # Mapeo transparente para nombres cromáticos contra colors.json
49
+ lookup_name = clean_name
50
+ unless COLORS.key?(lookup_name)
51
+ if lookup_name.start_with?("cromatico_")
52
+ lookup_name = lookup_name.sub(/\Acromatico_/, "chromatic_")
53
+ elsif lookup_name.start_with?("chromatic_")
54
+ lookup_name = lookup_name.sub(/\Achromatic_/, "cromatico_")
55
+ end
56
+ end
57
+
58
+ level_str = level.to_s
59
+ code_raw = COLORS.dig(lookup_name, level_str) || COLORS.dig(lookup_name, level.to_i) || COLORS[lookup_name]
60
+ return "\e[#{code_raw}" if code_raw
61
+ "\e[37m"
62
+ end
63
+
64
+ # Secuencia de reseteo ANSI a colores predeterminados de la terminal.
65
+ def self.ansi_reset
66
+ "\e[0m"
67
+ end
68
+
69
+ # Módulo utilitario para colorear texto directamente en consola o scripts de Ruby.
70
+ module Color
71
+ RESET = "\e[0m"
72
+ BOLD = "\e[1m"
73
+ module_function
74
+
75
+ # Pinta un texto con el color indicado y asegura el reseteo al final.
76
+ def paint(text, color_name, level = 1)
77
+ color_str = color_name.to_s.downcase
78
+ if color_str == "rgb" || color_str == "rainbow" || color_str == "chroma"
79
+ return rgb(text)
80
+ end
81
+ code = GRmenu.ansi_color(color_name, level) || "\e[37m"
82
+ "#{code}#{text}#{RESET}"
83
+ end
84
+
85
+ # Aplica un degradado RGB dinámico caracter por caracter respetando secuencias previas.
86
+ def rgb(text, offset = 0.0)
87
+ output_str = String.new("")
88
+ char_index = 0
89
+ in_escape = false
90
+ escape_buffer = String.new("")
91
+
92
+ text.to_s.each_char do |ch|
93
+ # Si encontramos el inicio de una secuencia ANSI existente, no la rompemos
94
+ if ch == "\e"
95
+ in_escape = true
96
+ escape_buffer << ch
97
+ next
98
+ end
99
+ if in_escape
100
+ escape_buffer << ch
101
+ # Las secuencias ANSI terminan en una letra (ej. m, H, J, etc.)
102
+ if ch =~ /[a-zA-Z]/
103
+ in_escape = false
104
+ output_str << escape_buffer
105
+ escape_buffer.clear
106
+ end
107
+ next
108
+ end
109
+
110
+ # Espacios y saltos de línea avanzan la posición de columna visual para mantener la fase continua
111
+ w = (GRmenu.char_width(ch) rescue 1)
112
+ if ch == " " || ch == "\n" || ch == "\r" || ch == "\t"
113
+ output_str << ch
114
+ char_index += w
115
+ else
116
+ # Calcula el desfase de color individual para dar efecto de ola (chroma wave)
117
+ t = char_index * 0.12 + offset
118
+ r = (Math.sin(t) * 127 + 128).clamp(0, 255).to_i
119
+ g = (Math.sin(t + 2.0943951) * 127 + 128).clamp(0, 255).to_i
120
+ b = (Math.sin(t + 4.1887902) * 127 + 128).clamp(0, 255).to_i
121
+ output_str << "\e[38;2;#{r};#{g};#{b}m#{ch}"
122
+ char_index += w
123
+ end
124
+ end
125
+ output_str << RESET
126
+ output_str
127
+ end
128
+
129
+ # Métodos directos para el espectro cromático continuo (RGB / Rainbow / Chroma)
130
+ def rainbow(text, offset = 0.0)
131
+ rgb(text, offset)
132
+ end
133
+
134
+ def chroma(text, offset = 0.0)
135
+ rgb(text, offset)
136
+ end
137
+
138
+ def chromatic(text, offset = 0.0)
139
+ rgb(text, offset)
140
+ end
141
+
142
+ def cromatico(text, offset = 0.0)
143
+ rgb(text, offset)
144
+ end
145
+
146
+ # Métodos directos para colores clásicos
147
+ def red(s); paint(s, :red, 1); end
148
+ def bright_red(s); paint(s, :red, 2); end
149
+ def dark_red(s); paint(s, :red, 1); end
150
+
151
+ def green(s); paint(s, :green, 1); end
152
+ def bright_green(s); paint(s, :green, 2); end
153
+ def dark_green(s); paint(s, :green, 1); end
154
+
155
+ def yellow(s); paint(s, :yellow, 1); end
156
+ def bright_yellow(s); paint(s, :yellow, 2); end
157
+
158
+ def blue(s); paint(s, :blue, 1); end
159
+ def bright_blue(s); paint(s, :blue, 2); end
160
+
161
+ def magenta(s); paint(s, :magenta, 1); end
162
+ def bright_magenta(s); paint(s, :magenta, 2); end
163
+
164
+ def purple(s); paint(s, :purple, 1); end
165
+ def bright_purple(s); paint(s, :purple, 2); end
166
+
167
+ def pink(s); paint(s, :pink, 1); end
168
+ def bright_pink(s); paint(s, :pink, 2); end
169
+
170
+ def cyan(s); paint(s, :cyan, 1); end
171
+ def bright_cyan(s); paint(s, :cyan, 2); end
172
+
173
+ def aqua(s); paint(s, :aqua, 1); end
174
+ def bright_aqua(s); paint(s, :aqua, 2); end
175
+
176
+ def orange(s); paint(s, :orange, 1); end
177
+ def bright_orange(s); paint(s, :orange, 2); end
178
+
179
+ def white(s); paint(s, :white, 1); end
180
+ def bright_white(s); paint(s, :white, 2); end
181
+
182
+ def black(s); paint(s, :black, 1); end
183
+ def gray(s); paint(s, :gray, 1); end
184
+ def bright_gray(s); paint(s, :gray, 2); end
185
+ def grey(s); gray(s); end
186
+
187
+ # Métodos cromáticos vivos (anteriormente llamados neón)
188
+ def chromatic_red(s); paint(s, :chromatic_red, 2); end
189
+ def chromatic_green(s); paint(s, :chromatic_green, 2); end
190
+ def chromatic_cyan(s); paint(s, :chromatic_cyan, 2); end
191
+ def chromatic_blue(s); paint(s, :chromatic_blue, 2); end
192
+ def chromatic_pink(s); paint(s, :chromatic_pink, 2); end
193
+ def chromatic_yellow(s); paint(s, :chromatic_yellow, 2); end
194
+ def chromatic_orange(s); paint(s, :chromatic_orange, 2); end
195
+ def chromatic_purple(s); paint(s, :chromatic_purple, 2); end
196
+ def chromatic_magenta(s); paint(s, :chromatic_magenta, 2); end
197
+ def chromatic_aqua(s); paint(s, :chromatic_aqua, 2); end
198
+ def chromatic_lime(s); paint(s, :chromatic_lime, 2); end
199
+ def chromatic_white(s); paint(s, :chromatic_white, 2); end
200
+
201
+ # Variantes en español
202
+ def cromatico_red(s); paint(s, :cromatico_red, 2); end
203
+ def cromatico_green(s); paint(s, :cromatico_green, 2); end
204
+ def cromatico_cyan(s); paint(s, :cromatico_cyan, 2); end
205
+ def cromatico_blue(s); paint(s, :cromatico_blue, 2); end
206
+ def cromatico_pink(s); paint(s, :cromatico_pink, 2); end
207
+ def cromatico_yellow(s); paint(s, :cromatico_yellow, 2); end
208
+ def cromatico_orange(s); paint(s, :cromatico_orange, 2); end
209
+ def cromatico_purple(s); paint(s, :cromatico_purple, 2); end
210
+ def cromatico_magenta(s); paint(s, :cromatico_magenta, 2); end
211
+ def cromatico_aqua(s); paint(s, :cromatico_aqua, 2); end
212
+ def cromatico_lime(s); paint(s, :cromatico_lime, 2); end
213
+ def cromatico_white(s); paint(s, :cromatico_white, 2); end
214
+
215
+ # Abreviaturas comunes para prototipado veloz
216
+ def r(s); bright_red(s); end
217
+ def dr(s); dark_red(s); end
218
+ def g(s); bright_green(s); end
219
+ def y(s); bright_yellow(s); end
220
+ def w(s); bright_white(s); end
221
+ def gr(s); gray(s); end
222
+ def cy(s); bright_cyan(s); end
223
+ def mg(s); bright_magenta(s); end
224
+ def bl(s); bright_blue(s); end
225
+
226
+ # Pinta texto en negrita (bold)
227
+ def bold(s)
228
+ "#{BOLD}#{s}#{RESET}"
229
+ end
230
+
231
+ # Pinta texto con color hexadecimal directo
232
+ def hex(code, text)
233
+ c = GRmenu.ansi_color(code.to_s)
234
+ "#{c}#{text}#{RESET}"
235
+ end
236
+
237
+ def respond_to_missing?(method_name, include_private = false)
238
+ m = method_name.to_s
239
+ GRmenu::COLORS.key?(m) || GRmenu::COLORS.key?(m.sub(/\Acromatico_/, 'chromatic_')) || super
240
+ end
241
+
242
+ # Permite invocar dinámicamente cualquier color cargado en colors.json o alias cromáticos
243
+ def method_missing(method_name, *args, &block)
244
+ m_str = method_name.to_s
245
+ lookup = m_str
246
+ unless GRmenu::COLORS.key?(lookup)
247
+ if lookup.start_with?("cromatico_")
248
+ lookup = lookup.sub(/\Acromatico_/, 'chromatic_')
249
+ elsif lookup.start_with?("chromatic_")
250
+ lookup = lookup.sub(/\Achromatic_/, 'cromatico_')
251
+ end
252
+ end
253
+ if GRmenu::COLORS.key?(lookup)
254
+ text = args[0].to_s
255
+ lvl = args[1] || 2
256
+ paint(text, m_str, lvl)
257
+ else
258
+ super
259
+ end
260
+ end
261
+ end
262
+
263
+ # Alias rápido para el módulo Color
264
+ C = Color
265
+ end