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.
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GRmenu
4
+ # Gestiona los colores, bordes y estilos visuales del menu.
5
+ # Soporta encadenamiento de metodos (method chaining) y priorizacion estricta:
6
+ # Cualquier valor configurado explicitamente por codigo tendra prioridad
7
+ # absoluta sobre hojas de estilo CSS .gr o bloques <<-GR.
8
+ class SetStyle
9
+ # Inicializa la configuracion de estilo con valores predeterminados
10
+ def initialize(
11
+ border: { color: "cyan", level: 1 },
12
+ options: { color: "white", level: 1 },
13
+ focus: { color: "green", level: 2 },
14
+ title: { color: "yellow", level: 2 },
15
+ banner: { color: "magenta", level: 2 },
16
+ subtitle: { color: "cyan", level: 2 },
17
+ divider: { color: "blue", level: 1 },
18
+ font: 1,
19
+ desc_prefix: "[i]"
20
+ )
21
+ @border = border.dup
22
+ @options = options.dup
23
+ @focus = focus.dup
24
+ @title = title.dup
25
+ @banner = banner.dup
26
+ @subtitle = subtitle.dup
27
+ @divider = divider.dup
28
+ @font = font.to_i
29
+ @desc_prefix = desc_prefix.to_s
30
+
31
+ # Hash para registrar que atributos fueron fijados por el desarrollador en codigo.
32
+ # Evita que temas CSS posteriores sobreescriban decisiones explicitas del programador.
33
+ @explicitly_set = {}
34
+ end
35
+
36
+ # Comprueba si un atributo fue fijado explicitamente a nivel de instancia o de clase
37
+ def explicitly_set?(key)
38
+ @explicitly_set ||= {}
39
+ @explicitly_set[key.to_sym] == true || self.class.explicitly_set?(key)
40
+ end
41
+
42
+ # Marca un atributo como fijado explicitamente por el usuario
43
+ def mark_explicit(key)
44
+ @explicitly_set ||= {}
45
+ @explicitly_set[key.to_sym] = true
46
+ end
47
+
48
+ # Configura o consulta el prefijo usado en las descripciones de opciones (ej. "[i]")
49
+ def desc_prefix(prefix_str = nil, from_css: false)
50
+ return @desc_prefix if prefix_str.nil?
51
+ return self if from_css && explicitly_set?(:desc_prefix)
52
+ @explicitly_set ||= {}
53
+ @explicitly_set[:desc_prefix] = true unless from_css
54
+ @desc_prefix = prefix_str.to_s
55
+ self
56
+ end
57
+ alias_method :description_prefix, :desc_prefix
58
+ alias_method :desc_prefix=, :desc_prefix
59
+ alias_method :description_prefix=, :desc_prefix
60
+
61
+ # Configura o consulta el color del borde del menu principal
62
+ def border(color_name = nil, brightness_level = 1, from_css: false)
63
+ return @border if color_name.nil?
64
+ return self if from_css && explicitly_set?(:border)
65
+ @explicitly_set ||= {}
66
+ @explicitly_set[:border] = true unless from_css
67
+ @border = parse_color(color_name, brightness_level)
68
+ self
69
+ end
70
+ alias_method :Border, :border
71
+ alias_method :set_border, :border
72
+ alias_method :border=, :border
73
+
74
+ # Configura o consulta el color de las opciones inactivas
75
+ def options(color_name = nil, brightness_level = 1, from_css: false)
76
+ return @options if color_name.nil?
77
+ return self if from_css && explicitly_set?(:options)
78
+ @explicitly_set ||= {}
79
+ @explicitly_set[:options] = true unless from_css
80
+ @options = parse_color(color_name, brightness_level)
81
+ self
82
+ end
83
+ alias_method :Options, :options
84
+ alias_method :set_options, :options
85
+ alias_method :options=, :options
86
+
87
+ # Configura o consulta el color de la opcion seleccionada (foco)
88
+ def focus(color_name = nil, brightness_level = 2, from_css: false)
89
+ return @focus if color_name.nil?
90
+ return self if from_css && explicitly_set?(:focus)
91
+ @explicitly_set ||= {}
92
+ @explicitly_set[:focus] = true unless from_css
93
+ @focus = parse_color(color_name, brightness_level)
94
+ self
95
+ end
96
+ alias_method :Focus, :focus
97
+ alias_method :set_focus, :focus
98
+ alias_method :focus=, :focus
99
+
100
+ # Configura o consulta el color del titulo del menu
101
+ def title(color_name = nil, brightness_level = 2, from_css: false)
102
+ return @title if color_name.nil?
103
+ return self if from_css && explicitly_set?(:title)
104
+ @explicitly_set ||= {}
105
+ @explicitly_set[:title] = true unless from_css
106
+ @title = parse_color(color_name, brightness_level)
107
+ self
108
+ end
109
+ alias_method :Title, :title
110
+ alias_method :set_title, :title
111
+ alias_method :title=, :title
112
+
113
+ # Configura o consulta el color del banner ASCII superior
114
+ def banner(color_name = nil, brightness_level = 2, from_css: false)
115
+ return @banner if color_name.nil?
116
+ return self if from_css && explicitly_set?(:banner)
117
+ @explicitly_set ||= {}
118
+ @explicitly_set[:banner] = true unless from_css
119
+ @banner = parse_color(color_name, brightness_level)
120
+ self
121
+ end
122
+ alias_method :Banner, :banner
123
+ alias_method :set_banner, :banner
124
+ alias_method :banner=, :banner
125
+
126
+ # Configura o consulta el color del subtitulo o descripcion superior
127
+ def subtitle(color_name = nil, brightness_level = 2, from_css: false)
128
+ return @subtitle if color_name.nil?
129
+ return self if from_css && explicitly_set?(:subtitle)
130
+ @explicitly_set ||= {}
131
+ @explicitly_set[:subtitle] = true unless from_css
132
+ @subtitle = parse_color(color_name, brightness_level)
133
+ self
134
+ end
135
+ alias_method :Subtitle, :subtitle
136
+ alias_method :set_subtitle, :subtitle
137
+ alias_method :subtitle=, :subtitle
138
+
139
+ # Configura o consulta el color de las lineas divisorias
140
+ def divider(color_name = nil, brightness_level = 1, from_css: false)
141
+ return @divider if color_name.nil?
142
+ return self if from_css && explicitly_set?(:divider)
143
+ @explicitly_set ||= {}
144
+ @explicitly_set[:divider] = true unless from_css
145
+ @divider = parse_color(color_name, brightness_level)
146
+ self
147
+ end
148
+ alias_method :Divider, :divider
149
+ alias_method :set_divider, :divider
150
+ alias_method :divider=, :divider
151
+
152
+ # Configura o consulta la fuente numerica para el banner ASCII (1..10)
153
+ def font(font_id = nil, from_css: false)
154
+ return @font if font_id.nil?
155
+ return self if from_css && explicitly_set?(:font)
156
+ @explicitly_set ||= {}
157
+ @explicitly_set[:font] = true unless from_css
158
+ @font = font_id.to_i
159
+ self
160
+ end
161
+ alias_method :Font, :font
162
+ alias_method :set_font, :font
163
+ alias_method :font=, :font
164
+
165
+ private
166
+
167
+ # Convierte un valor de color (string o hash) a la estructura interna { color: ..., level: ... }
168
+ def parse_color(color_val, default_level = 1)
169
+ if color_val.is_a?(Hash)
170
+ {
171
+ color: (color_val[:color] || color_val["color"]).to_s,
172
+ level: (color_val[:level] || color_val["level"] || default_level).to_i
173
+ }
174
+ else
175
+ { color: color_val.to_s, level: default_level.to_i }
176
+ end
177
+ end
178
+
179
+ class << self
180
+ # Comprueba si un atributo fue marcado explicitamente a nivel global de clase
181
+ def explicitly_set?(key)
182
+ @explicitly_set ||= {}
183
+ @explicitly_set[key.to_sym] == true
184
+ end
185
+
186
+ # Marca un atributo global como fijado explicitamente
187
+ def mark_explicit(key)
188
+ @explicitly_set ||= {}
189
+ @explicitly_set[key.to_sym] = true
190
+ end
191
+
192
+ # Color de borde predeterminado para nuevas instancias
193
+ def border(color_name = nil, brightness_level = 1, from_css: false)
194
+ @default_border ||= { color: "cyan", level: 1 }
195
+ return @default_border if color_name.nil?
196
+ return @default_border if from_css && explicitly_set?(:border)
197
+ @explicitly_set ||= {}
198
+ @explicitly_set[:border] = true unless from_css
199
+ @default_border = { color: color_name.to_s, level: brightness_level.to_i }
200
+ end
201
+ alias_method :Border, :border
202
+ alias_method :border=, :border
203
+
204
+ # Color de opciones predeterminado para nuevas instancias
205
+ def options(color_name = nil, brightness_level = 1, from_css: false)
206
+ @default_options ||= { color: "white", level: 1 }
207
+ return @default_options if color_name.nil?
208
+ return @default_options if from_css && explicitly_set?(:options)
209
+ @explicitly_set ||= {}
210
+ @explicitly_set[:options] = true unless from_css
211
+ @default_options = { color: color_name.to_s, level: brightness_level.to_i }
212
+ end
213
+ alias_method :Options, :options
214
+ alias_method :options=, :options
215
+
216
+ # Color de foco predeterminado para nuevas instancias
217
+ def focus(color_name = nil, brightness_level = 2, from_css: false)
218
+ @default_focus ||= { color: "green", level: 2 }
219
+ return @default_focus if color_name.nil?
220
+ return @default_focus if from_css && explicitly_set?(:focus)
221
+ @explicitly_set ||= {}
222
+ @explicitly_set[:focus] = true unless from_css
223
+ @default_focus = { color: color_name.to_s, level: brightness_level.to_i }
224
+ end
225
+ alias_method :Focus, :focus
226
+ alias_method :focus=, :focus
227
+
228
+ # Color de titulo predeterminado para nuevas instancias
229
+ def title(color_name = nil, brightness_level = 2, from_css: false)
230
+ @default_title ||= { color: "yellow", level: 2 }
231
+ return @default_title if color_name.nil?
232
+ return @default_title if from_css && explicitly_set?(:title)
233
+ @explicitly_set ||= {}
234
+ @explicitly_set[:title] = true unless from_css
235
+ @default_title = { color: color_name.to_s, level: brightness_level.to_i }
236
+ end
237
+ alias_method :Title, :title
238
+ alias_method :title=, :title
239
+
240
+ # Color de banner predeterminado para nuevas instancias
241
+ def banner(color_name = nil, brightness_level = 2, from_css: false)
242
+ @default_banner ||= { color: "magenta", level: 2 }
243
+ return @default_banner if color_name.nil?
244
+ return @default_banner if from_css && explicitly_set?(:banner)
245
+ @explicitly_set ||= {}
246
+ @explicitly_set[:banner] = true unless from_css
247
+ @default_banner = { color: color_name.to_s, level: brightness_level.to_i }
248
+ end
249
+ alias_method :Banner, :banner
250
+ alias_method :banner=, :banner
251
+
252
+ # Color de subtitulo predeterminado para nuevas instancias
253
+ def subtitle(color_name = nil, brightness_level = 2, from_css: false)
254
+ @default_subtitle ||= { color: "cyan", level: 2 }
255
+ return @default_subtitle if color_name.nil?
256
+ return @default_subtitle if from_css && explicitly_set?(:subtitle)
257
+ @explicitly_set ||= {}
258
+ @explicitly_set[:subtitle] = true unless from_css
259
+ @default_subtitle = { color: color_name.to_s, level: brightness_level.to_i }
260
+ end
261
+ alias_method :Subtitle, :subtitle
262
+ alias_method :subtitle=, :subtitle
263
+
264
+ # Color de divisor predeterminado para nuevas instancias
265
+ def divider(color_name = nil, brightness_level = 1, from_css: false)
266
+ @default_divider ||= { color: "blue", level: 1 }
267
+ return @default_divider if color_name.nil?
268
+ return @default_divider if from_css && explicitly_set?(:divider)
269
+ @explicitly_set ||= {}
270
+ @explicitly_set[:divider] = true unless from_css
271
+ @default_divider = { color: color_name.to_s, level: brightness_level.to_i }
272
+ end
273
+ alias_method :Divider, :divider
274
+ alias_method :divider=, :divider
275
+
276
+ # Fuente tipografica predeterminada para nuevas instancias
277
+ def font(font_id = nil, from_css: false)
278
+ @default_font ||= 1
279
+ return @default_font if font_id.nil?
280
+ return @default_font if from_css && explicitly_set?(:font)
281
+ @explicitly_set ||= {}
282
+ @explicitly_set[:font] = true unless from_css
283
+ @default_font = font_id.to_i
284
+ end
285
+ alias_method :Font, :font
286
+ alias_method :set_font, :font
287
+ alias_method :font=, :font
288
+
289
+ # Prefijo de descripcion predeterminado para nuevas instancias
290
+ def desc_prefix(prefix_str = nil, from_css: false)
291
+ @default_desc_prefix ||= "[i]"
292
+ return @default_desc_prefix if prefix_str.nil?
293
+ return @default_desc_prefix if from_css && explicitly_set?(:desc_prefix)
294
+ @explicitly_set ||= {}
295
+ @explicitly_set[:desc_prefix] = true unless from_css
296
+ @default_desc_prefix = prefix_str.to_s
297
+ end
298
+ alias_method :description_prefix, :desc_prefix
299
+ alias_method :desc_prefix=, :desc_prefix
300
+ alias_method :description_prefix=, :desc_prefix
301
+ end
302
+ end
303
+
304
+ # Utilidad de impresion directa con retorno de carro para terminales crudas
305
+ module GRprint
306
+ module_function
307
+
308
+ def p(text = "", ending = "\r\n")
309
+ Kernel.print("#{text}#{ending}")
310
+ end
311
+ end
312
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GRmenu
4
+ # Secuencias de control ANSI para la terminal
5
+ CLEAR_SCREEN_SEQUENCE = "\e[H\e[2J\e[3J"
6
+ HIDE_CURSOR = "\e[?25l"
7
+ SHOW_CURSOR = "\e[?25h"
8
+ CURSOR_HOME = "\e[H"
9
+ CLEAR_TO_EOL = "\e[K"
10
+ CLEAR_TO_EOS = "\e[J"
11
+
12
+ # Protocolo de raton ANSI:
13
+ # 1000h: Reporta clics del raton (MouseDown / MouseUp)
14
+ # 1002h: Reporta eventos de arrastre o movimiento con boton presionado
15
+ # 1006h: Formato extendido SGR (permite coordenadas mas alla de la columna 223)
16
+ ENABLE_MOUSE = "\e[?1000h\e[?1002h\e[?1006h"
17
+ DISABLE_MOUSE = "\e[?1006l\e[?1002l\e[?1000l"
18
+
19
+ # Habilita el modo de procesamiento virtual (VT) en la consola de Windows.
20
+ # En Windows clasico (cmd.exe antiguo), las secuencias ANSI no se interpretan por defecto;
21
+ # usamos Fiddle para interactuar directamente con kernel32 y activar los flags correspondientes.
22
+ def self.enable_windows_vt
23
+ return unless Gem.win_platform? || RUBY_PLATFORM =~ /mswin|mingw|cygwin/
24
+ require 'fiddle'
25
+
26
+ kernel32 = Fiddle.dlopen('kernel32')
27
+ get_std_handle = Fiddle::Function.new(kernel32['GetStdHandle'], [Fiddle::TYPE_INT], Fiddle::TYPE_VOIDP)
28
+ get_console_mode = Fiddle::Function.new(kernel32['GetConsoleMode'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
29
+ set_console_mode = Fiddle::Function.new(kernel32['SetConsoleMode'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
30
+
31
+ # STD_OUTPUT_HANDLE = -11 -> Activa ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x0004)
32
+ handle_out = get_std_handle.call(-11)
33
+ out_mode_buffer = ' ' * 4
34
+ if get_console_mode.call(handle_out, out_mode_buffer) != 0
35
+ current_mode = out_mode_buffer.unpack1('L')
36
+ set_console_mode.call(handle_out, current_mode | 0x0004)
37
+ end
38
+
39
+ # STD_INPUT_HANDLE = -10 -> Activa ENABLE_VIRTUAL_TERMINAL_INPUT (0x0200)
40
+ handle_in = get_std_handle.call(-10)
41
+ in_mode_buffer = ' ' * 4
42
+ if get_console_mode.call(handle_in, in_mode_buffer) != 0
43
+ current_mode = in_mode_buffer.unpack1('L')
44
+ set_console_mode.call(handle_in, current_mode | 0x0200)
45
+ end
46
+ rescue StandardError
47
+ # Si falla por permisos o entorno no estandar, dejamos que continue normalmente
48
+ end
49
+
50
+ # Devuelve el ancho visual de un unico caracter en terminal (0, 1 o 2 columnas).
51
+ # Toma en cuenta caracteres de ancho cero (selectores de variacion emoji, marcas diacriticas)
52
+ # y caracteres de doble ancho (ideogramas CJK, hangul y la mayoria de emojis modernos).
53
+ def self.char_width(char)
54
+ code = char.ord
55
+
56
+ # Caracteres nulos, selectores de variacion de emoji y diacriticos combinables (ancho 0)
57
+ return 0 if code == 0 || code == 0xFE0F || code == 0xFE0E || (code >= 0x0300 && code <= 0x036F) || (code >= 0x200B && code <= 0x200F)
58
+ return 0 if code < 32 || (code >= 0x7F && code < 0xA0)
59
+
60
+ # Simbolos especificos que en fuentes modernas ocupan 1 celda
61
+ return 1 if code == 0x1F5BC || code == 0x1F5B4 || code == 0x1F5B5 || code == 0x1F5C2
62
+
63
+ # Rangos CJK y emojis que ocupan 2 columnas en terminales compatibles con Unicode
64
+ if (code >= 0x1100 && code <= 0x115F) ||
65
+ (code >= 0x2329 && code <= 0x232A) ||
66
+ (code >= 0x2E80 && code <= 0xA4CF && code != 0x303F) ||
67
+ (code >= 0xAC00 && code <= 0xD7A3) ||
68
+ (code >= 0xF900 && code <= 0xFAFF) ||
69
+ (code >= 0xFE10 && code <= 0xFE19) ||
70
+ (code >= 0xFE30 && code <= 0xFE6F) ||
71
+ (code >= 0xFF01 && code <= 0xFF60) ||
72
+ (code >= 0xFFE0 && code <= 0xFFE6) ||
73
+ (code >= 0x1F300 && code <= 0x1F6FF) ||
74
+ (code >= 0x1F900 && code <= 0x1FAFF)
75
+ 2
76
+ else
77
+ 1
78
+ end
79
+ end
80
+
81
+ # Calcula el ancho visual de una cadena ignorando los codigos de escape ANSI.
82
+ def self.display_width(str)
83
+ # Limpiamos secuencias de escape ANSI tipo \e[...m antes de contar
84
+ clean = str.to_s.gsub(/\e\[[0-9;]*[a-zA-Z]/, '')
85
+ clean.chars.map { |c| char_width(c) }.sum
86
+ end
87
+
88
+ # Ajusta el texto al ancho objetivo con espacios segun la alineacion deseada.
89
+ def self.pad_to_width(str, target_width, align = :left)
90
+ current_w = display_width(str)
91
+ pad_needed = [target_width - current_w, 0].max
92
+ case align
93
+ when :right
94
+ (" " * pad_needed) + str.to_s
95
+ when :center
96
+ left_pad = " " * (pad_needed / 2)
97
+ right_pad = " " * (pad_needed - (pad_needed / 2))
98
+ left_pad + str.to_s + right_pad
99
+ else
100
+ str.to_s + (" " * pad_needed)
101
+ end
102
+ end
103
+
104
+ # Detecta el numero de columnas disponibles en la terminal actual.
105
+ def self.terminal_width
106
+ cols = $stdout.winsize[1] rescue nil
107
+ cols = $stdin.winsize[1] rescue nil if cols.nil? || cols <= 0
108
+ (cols && cols > 0) ? cols : (ENV['COLUMNS'] ? ENV['COLUMNS'].to_i : 80)
109
+ rescue StandardError
110
+ 80
111
+ end
112
+
113
+ # Detecta el numero de filas disponibles en la terminal actual.
114
+ def self.terminal_height
115
+ rows = $stdout.winsize[0] rescue nil
116
+ rows = $stdin.winsize[0] rescue nil if rows.nil? || rows <= 0
117
+ (rows && rows > 0) ? rows : (ENV['LINES'] ? ENV['LINES'].to_i : 24)
118
+ rescue StandardError
119
+ 24
120
+ end
121
+
122
+ # Limpia la pantalla completa de la terminal y posiciona el cursor al inicio.
123
+ def self.clear_screen
124
+ Kernel.print(CLEAR_SCREEN_SEQUENCE)
125
+ end
126
+
127
+ class << self
128
+ alias_method :clr, :clear_screen
129
+ end
130
+
131
+ # Lee una tecla o secuencia de escape de la terminal en modo crudo.
132
+ # Soporta teclas comunes, flechas, teclas de funcion, eventos de raton SGR y teclas extendidas de Windows.
133
+ def self.read_key_raw(input_stream)
134
+ is_tty = input_stream.respond_to?(:tty?) && input_stream.tty?
135
+ first_char = nil
136
+ begin
137
+ first_char = is_tty ? input_stream.getch : input_stream.read(1)
138
+ rescue EOFError, Errno::EPIPE
139
+ return nil
140
+ end
141
+ return nil if first_char.nil?
142
+
143
+ if first_char == "\e"
144
+ seq = first_char.dup
145
+ # Acumula caracteres adicionales si forman parte de una secuencia de escape
146
+ if is_tty
147
+ while (ch = (input_stream.getch(min: 0, time: 0.1) rescue nil))
148
+ seq << ch
149
+ break if seq =~ /[a-zA-Z~]\z/
150
+ end
151
+ else
152
+ while (IO.select([input_stream], nil, nil, 0.05) rescue nil)
153
+ ch = (input_stream.read(1) rescue nil)
154
+ break if ch.nil?
155
+ seq << ch
156
+ break if seq =~ /[a-zA-Z~]\z/
157
+ end
158
+ end
159
+
160
+ # Si es el protocolo clasico X10 (\e[M + 3 bytes), lo convertimos al estandar SGR 1006
161
+ if seq == "\e[M"
162
+ cb = is_tty ? (input_stream.getch(min: 0, time: 0.1) rescue nil) : (input_stream.read(1) rescue nil)
163
+ cx = is_tty ? (input_stream.getch(min: 0, time: 0.1) rescue nil) : (input_stream.read(1) rescue nil)
164
+ cy = is_tty ? (input_stream.getch(min: 0, time: 0.1) rescue nil) : (input_stream.read(1) rescue nil)
165
+ if cb && cx && cy
166
+ btn_c = cb.ord - 32
167
+ col_c = cx.ord - 32
168
+ row_c = cy.ord - 32
169
+ return "\e[<#{btn_c};#{col_c};#{row_c}M"
170
+ end
171
+ end
172
+
173
+ return seq
174
+ elsif first_char == "\x00" || first_char == "\xe0"
175
+ # Teclas de dos bytes en consolas Windows clásicas
176
+ second_char = is_tty ? (input_stream.getch(min: 0, time: 0.1) rescue nil) : (input_stream.read(1) rescue nil)
177
+ return first_char + second_char if second_char
178
+ end
179
+
180
+ first_char
181
+ rescue EOFError, Errno::EPIPE, Errno::ENOTTY
182
+ nil
183
+ end
184
+
185
+ # Delegador de instancia para lectura de una sola tecla
186
+ def read_single_key(input_stream)
187
+ self.class.read_key_raw(input_stream)
188
+ end
189
+ end