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.
- checksums.yaml +4 -4
- data/README.md +186 -49
- data/data/colors.json +224 -44
- data/data/help.txt +223 -90
- data/data/themes/chromatic_red.gr +74 -0
- data/data/themes/cyberpunk.gr +18 -2
- data/data/themes/dracula.gr +18 -2
- data/data/themes/matrix.gr +18 -2
- data/data/themes/monokai.gr +18 -2
- data/data/themes/nord.gr +18 -2
- data/data/themes/sunset.gr +18 -2
- data/grmenu/animation.rb +139 -0
- data/grmenu/banner.rb +99 -0
- data/grmenu/color.rb +265 -0
- data/grmenu/image.rb +339 -0
- data/grmenu/interactive.rb +1002 -0
- data/grmenu/renderer.rb +865 -0
- data/grmenu/set_style.rb +312 -0
- data/grmenu/terminal.rb +189 -0
- data/grmenu/theme.rb +378 -0
- data/grmenu/version.rb +6 -0
- data/grmenu/widgets/checkbox.rb +291 -0
- data/grmenu/widgets/dialogs.rb +251 -0
- data/grmenu/widgets/form.rb +533 -0
- data/grmenu/widgets/input.rb +171 -0
- data/grmenu/widgets/modal.rb +209 -0
- data/grmenu/widgets/progress.rb +210 -0
- data/grmenu/widgets/slider.rb +178 -0
- data/grmenu/widgets/table.rb +291 -0
- data/grmenu/window.rb +187 -0
- data/grmenu.rb +88 -4339
- metadata +23 -3
- data/data/themes/neon_red.gr +0 -51
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class GRmenu
|
|
4
|
+
# Atributos de configuración y estado del menú interactivo
|
|
5
|
+
attr_accessor :functions, :title, :subtitle, :banner, :banner_style, :divider,
|
|
6
|
+
:index, :style_config, :center, :page_size, :search, :columns,
|
|
7
|
+
:query, :image, :image_width
|
|
8
|
+
|
|
9
|
+
# Alias de compatibilidad hacia atrás
|
|
10
|
+
alias_method :options, :functions
|
|
11
|
+
alias_method :options=, :functions=
|
|
12
|
+
alias_method :selected_index, :index
|
|
13
|
+
alias_method :selected_index=, :index=
|
|
14
|
+
alias_method :SetStyle, :style_config
|
|
15
|
+
alias_method :set_style, :style_config
|
|
16
|
+
alias_method :description, :subtitle
|
|
17
|
+
alias_method :description=, :subtitle=
|
|
18
|
+
|
|
19
|
+
# Retorna el prefijo configurado para las descripciones de opciones (por defecto '[i]')
|
|
20
|
+
def desc_prefix
|
|
21
|
+
@desc_prefix || @style_config&.desc_prefix || SetStyle.desc_prefix
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Modifica dinámicamente el prefijo de las descripciones
|
|
25
|
+
def desc_prefix=(val)
|
|
26
|
+
@desc_prefix = val.to_s
|
|
27
|
+
@style_config&.desc_prefix(val)
|
|
28
|
+
end
|
|
29
|
+
alias_method :prefix, :desc_prefix
|
|
30
|
+
alias_method :prefix=, :desc_prefix=
|
|
31
|
+
alias_method :description_prefix, :desc_prefix
|
|
32
|
+
alias_method :description_prefix=, :desc_prefix=
|
|
33
|
+
|
|
34
|
+
# Accesores directos a los catálogos y datasets cargados desde JSON
|
|
35
|
+
def self.STYLES; STYLES; end
|
|
36
|
+
def self.COLORS; COLORS; end
|
|
37
|
+
def self.BORDERS; BORDERS; end
|
|
38
|
+
def self.FONTS; FONTS; end
|
|
39
|
+
def self.FONT; FONT_1; end
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Constructor conveniente para instanciar un menú organizado por pestañas.
|
|
43
|
+
#
|
|
44
|
+
# @param tabs_hash [Hash] Diccionario donde las claves son los nombres de pestañas y los valores son listas de acciones.
|
|
45
|
+
# @return [GRmenu] Instancia del menú configurada con pestañas.
|
|
46
|
+
def tabs(tabs_hash, *args, **kwargs)
|
|
47
|
+
new([], *args, tabs: tabs_hash, **kwargs)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Inicializa una nueva instancia de menú interactivo.
|
|
52
|
+
#
|
|
53
|
+
# Puede configurarse mediante argumentos nominales o posicionales, así como
|
|
54
|
+
# heredar estilos globales o valores predeterminados de SetStyle.
|
|
55
|
+
#
|
|
56
|
+
# @param functions [Array, Hash] Lista de opciones ejecutables o Hash de pestañas.
|
|
57
|
+
# @param positional_arguments [Array] Argumentos posicionales opcionales: [title, style].
|
|
58
|
+
# @param title [String, nil] Título del menú mostrado en la parte superior.
|
|
59
|
+
# @param banner [String, nil] Texto para banner o logo en ASCII 3D.
|
|
60
|
+
# @param subtitle [String, nil] Subtítulo o texto descriptivo general.
|
|
61
|
+
# @param description [String, nil] Alias de subtítulo.
|
|
62
|
+
# @param divider [Boolean, Integer, nil] Si se dibuja línea divisoria tras el banner/subtítulo.
|
|
63
|
+
# @param style [Integer, nil] Estilo de borde del menú (1 al 19).
|
|
64
|
+
# @param banner_style [Integer, nil] Estilo de borde del banner (1 al 12).
|
|
65
|
+
# @param center [Boolean] Centra horizontalmente el menú en la ventana de la terminal.
|
|
66
|
+
# @param font [Integer, nil] Fuente tipográfica ASCII para el banner (1 a 11).
|
|
67
|
+
# @param page_size [Integer, nil] Número máximo de opciones visibles en pantalla.
|
|
68
|
+
# @param search [Boolean] Habilita búsqueda y filtrado en tiempo real.
|
|
69
|
+
# @param columns [Integer] Número de columnas de opciones para mostrar en cuadrícula.
|
|
70
|
+
# @param image [String, nil] Ruta a un archivo PNG para renderizarlo con caracteres de bloques.
|
|
71
|
+
# @param image_width [Integer, nil] Ancho deseado de la imagen renderizada.
|
|
72
|
+
# @param mouse [Boolean, nil] Habilita captura de eventos de clic y rueda del ratón.
|
|
73
|
+
# @param tabs [Hash, nil] Diccionario de pestañas si no se pasó en functions.
|
|
74
|
+
# @param desc_prefix [String, nil] Prefijo para la descripción inferior (ej: "[i]").
|
|
75
|
+
# @param animate [String, Symbol, Boolean, nil] Animación de entrada o continua (:rainbow, :chroma, :rgb, :fade, :diagonal, :linear, :chromatic).
|
|
76
|
+
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, search: false, columns: 1, image: nil, image_width: nil, mouse: nil, tabs: nil, active_tab_color: nil, tab_color: nil, desc_prefix: nil, prefix: nil, border: nil, border_color: nil, options_color: nil, focus_color: nil, title_color: nil, banner_color: nil, subtitle_color: nil, divider_color: nil, animate: nil, **keyword_arguments)
|
|
77
|
+
# Detectamos si se pasaron pestañas en el argumento tabs o functions
|
|
78
|
+
tabs_data = tabs || keyword_arguments[:tabs] || (functions.is_a?(Hash) ? functions : nil)
|
|
79
|
+
if tabs_data.is_a?(Hash) && !tabs_data.empty?
|
|
80
|
+
@tabs = tabs_data.keys.map(&:to_s)
|
|
81
|
+
@tab_contents = tabs_data.transform_keys(&:to_s)
|
|
82
|
+
@active_tab_idx = 0
|
|
83
|
+
@functions = @tab_contents[@tabs[@active_tab_idx]] || []
|
|
84
|
+
else
|
|
85
|
+
@tabs = nil
|
|
86
|
+
@tab_contents = nil
|
|
87
|
+
@active_tab_idx = nil
|
|
88
|
+
@functions = functions.is_a?(Array) ? functions : Array(functions)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
pos_title = positional_arguments[0]
|
|
92
|
+
pos_style = positional_arguments[1]
|
|
93
|
+
|
|
94
|
+
@title = (title || pos_title || keyword_arguments[:title] || "").to_s
|
|
95
|
+
@banner = (banner || keyword_arguments[:banner] || "").to_s
|
|
96
|
+
@subtitle = (subtitle || description || keyword_arguments[:subtitle] || keyword_arguments[:description] || "").to_s
|
|
97
|
+
@divider = divider.nil? ? (!@banner.empty? || !@subtitle.empty?) : divider
|
|
98
|
+
|
|
99
|
+
theme_menu_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, 'menu')) || {}
|
|
100
|
+
th_style = theme_menu_sec['style']&.to_i
|
|
101
|
+
th_bstyle = theme_menu_sec['banner_style']&.to_i
|
|
102
|
+
|
|
103
|
+
@style = (style || pos_style || keyword_arguments[:style] || th_style || 19).to_i
|
|
104
|
+
@banner_style = (banner_style || keyword_arguments[:banner_style] || th_bstyle || 3).to_i
|
|
105
|
+
@center = center.nil? ? (theme_menu_sec.key?('center') ? (theme_menu_sec['center'].to_s != 'false') : true) : center
|
|
106
|
+
@page_size = (page_size || keyword_arguments[:page_size])&.to_i
|
|
107
|
+
@search = search || keyword_arguments[:search] || false
|
|
108
|
+
@columns = [(columns || keyword_arguments[:columns] || 1).to_i, 1].max
|
|
109
|
+
@image = image || keyword_arguments[:image]
|
|
110
|
+
@image_width = (image_width || keyword_arguments[:image_width])&.to_i
|
|
111
|
+
@animate = (animate || keyword_arguments[:animate] || (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, 'menu', 'animate')) || false).to_s
|
|
112
|
+
|
|
113
|
+
# Configuración de soporte para ratón
|
|
114
|
+
m_val = mouse.nil? ? keyword_arguments[:mouse] : mouse
|
|
115
|
+
if m_val.nil?
|
|
116
|
+
@mouse = theme_menu_sec.key?('mouse') ? (theme_menu_sec['mouse'].to_s != 'false') : true
|
|
117
|
+
else
|
|
118
|
+
@mouse = (m_val == true || m_val.to_s == 'true')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
tabs_theme_sec = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "tabs")) || {}
|
|
122
|
+
@active_tab_color = (active_tab_color || keyword_arguments[:active_tab_color] || tabs_theme_sec["active_tab"] || tabs_theme_sec["active_tab_color"] || "yellow").to_s
|
|
123
|
+
@tab_color = (tab_color || keyword_arguments[:tab_color] || tabs_theme_sec["tab_color"] || tabs_theme_sec["inactive_tab"] || tabs_theme_sec["color"] || "gray").to_s
|
|
124
|
+
|
|
125
|
+
@query = String.new("")
|
|
126
|
+
@index = 0
|
|
127
|
+
@rgb_tick = 0.0
|
|
128
|
+
|
|
129
|
+
# Estado de navegación en submenús anidados
|
|
130
|
+
@level = 0
|
|
131
|
+
@open_level = 0
|
|
132
|
+
@sub_index_1 = 0
|
|
133
|
+
@sub_index_2 = 0
|
|
134
|
+
@sub_1_hit_map = {}
|
|
135
|
+
@sub_2_hit_map = {}
|
|
136
|
+
@sub_1_col_rng = nil
|
|
137
|
+
@sub_2_col_rng = nil
|
|
138
|
+
|
|
139
|
+
@active_panel = :main
|
|
140
|
+
@sub_index = 0
|
|
141
|
+
@submenu_open = false
|
|
142
|
+
|
|
143
|
+
# Registro de áreas activas para eventos de ratón
|
|
144
|
+
@row_hit_map = {}
|
|
145
|
+
@tab_ranges = {}
|
|
146
|
+
@tabs_row = nil
|
|
147
|
+
@up_arrow_row = nil
|
|
148
|
+
@down_arrow_row = nil
|
|
149
|
+
@sub_hit_map = {}
|
|
150
|
+
|
|
151
|
+
@cached_image_lines = nil
|
|
152
|
+
@cached_image_cols = nil
|
|
153
|
+
|
|
154
|
+
# Resolución del prefijo y fuente con prioridad estricta: SetStyle explícito > argumento directo > tema > por defecto
|
|
155
|
+
init_font = font || keyword_arguments[:font] || keyword_arguments[:font_style] || SetStyle.font || 1
|
|
156
|
+
raw_pfx = desc_prefix || prefix || keyword_arguments[:desc_prefix] || keyword_arguments[:prefix] || keyword_arguments[:description_prefix]
|
|
157
|
+
@desc_prefix = raw_pfx.to_s unless raw_pfx.nil?
|
|
158
|
+
init_pfx = @desc_prefix || (@@global_theme.is_a?(Hash) && (@@global_theme.dig(:sections, 'menu', 'desc_prefix') || @@global_theme.dig(:sections, 'menu', 'prefix'))) || SetStyle.desc_prefix || "[i]"
|
|
159
|
+
|
|
160
|
+
@style_config = SetStyle.new(
|
|
161
|
+
border: SetStyle.border.dup,
|
|
162
|
+
options: SetStyle.options.dup,
|
|
163
|
+
focus: SetStyle.focus.dup,
|
|
164
|
+
title: SetStyle.title.dup,
|
|
165
|
+
banner: SetStyle.banner.dup,
|
|
166
|
+
subtitle: SetStyle.subtitle.dup,
|
|
167
|
+
divider: SetStyle.divider.dup,
|
|
168
|
+
font: init_font,
|
|
169
|
+
desc_prefix: init_pfx
|
|
170
|
+
)
|
|
171
|
+
@style_config.mark_explicit(:font) if font || keyword_arguments[:font] || keyword_arguments[:font_style]
|
|
172
|
+
@style_config.mark_explicit(:desc_prefix) unless raw_pfx.nil?
|
|
173
|
+
|
|
174
|
+
# Aplicamos estilos directos si fueron provistos en la llamada new()
|
|
175
|
+
if border || border_color || keyword_arguments[:border] || keyword_arguments[:border_color]
|
|
176
|
+
b_c, b_l = self.class.extract_color_and_level(border || border_color || keyword_arguments[:border] || keyword_arguments[:border_color], 1)
|
|
177
|
+
@style_config.border(b_c, b_l)
|
|
178
|
+
end
|
|
179
|
+
if options_color || keyword_arguments[:options_color] || keyword_arguments[:options]
|
|
180
|
+
o_c, o_l = self.class.extract_color_and_level(options_color || keyword_arguments[:options_color] || keyword_arguments[:options], 1)
|
|
181
|
+
@style_config.options(o_c, o_l)
|
|
182
|
+
end
|
|
183
|
+
if focus_color || keyword_arguments[:focus_color] || keyword_arguments[:focus]
|
|
184
|
+
f_c, f_l = self.class.extract_color_and_level(focus_color || keyword_arguments[:focus_color] || keyword_arguments[:focus], 2)
|
|
185
|
+
@style_config.focus(f_c, f_l)
|
|
186
|
+
end
|
|
187
|
+
if title_color || keyword_arguments[:title_color]
|
|
188
|
+
t_c, t_l = self.class.extract_color_and_level(title_color || keyword_arguments[:title_color], 2)
|
|
189
|
+
@style_config.title(t_c, t_l)
|
|
190
|
+
end
|
|
191
|
+
if banner_color || keyword_arguments[:banner_color]
|
|
192
|
+
bn_c, bn_l = self.class.extract_color_and_level(banner_color || keyword_arguments[:banner_color], 2)
|
|
193
|
+
@style_config.banner(bn_c, bn_l)
|
|
194
|
+
end
|
|
195
|
+
if subtitle_color || keyword_arguments[:subtitle_color]
|
|
196
|
+
s_c, s_l = self.class.extract_color_and_level(subtitle_color || keyword_arguments[:subtitle_color], 1)
|
|
197
|
+
@style_config.subtitle(s_c, s_l)
|
|
198
|
+
end
|
|
199
|
+
if divider_color || keyword_arguments[:divider_color]
|
|
200
|
+
d_c, d_l = self.class.extract_color_and_level(divider_color || keyword_arguments[:divider_color], 1)
|
|
201
|
+
@style_config.divider(d_c, d_l)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Devuelve la lista de índices de @functions que coinciden con el término de búsqueda actual.
|
|
206
|
+
#
|
|
207
|
+
# @return [Array<Integer>]
|
|
208
|
+
def current_matching_indices
|
|
209
|
+
if @search && !@query.empty?
|
|
210
|
+
indices = []
|
|
211
|
+
@functions.each_with_index do |func, idx|
|
|
212
|
+
name = extract_name_from_action(func)
|
|
213
|
+
indices << idx if name.downcase.include?(@query.downcase)
|
|
214
|
+
end
|
|
215
|
+
indices
|
|
216
|
+
else
|
|
217
|
+
(0...@functions.length).to_a
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Desplaza el cursor de selección una fila hacia arriba.
|
|
222
|
+
# En menús multi-columna salta exactamente el ancho de la columna hacia arriba.
|
|
223
|
+
def move_up
|
|
224
|
+
matching = current_matching_indices
|
|
225
|
+
return @index if matching.empty?
|
|
226
|
+
cols = @columns
|
|
227
|
+
pos = matching.index(@index) || 0
|
|
228
|
+
if cols <= 1
|
|
229
|
+
new_pos = (pos - 1) % matching.length
|
|
230
|
+
else
|
|
231
|
+
new_pos = pos - cols
|
|
232
|
+
if new_pos < 0
|
|
233
|
+
new_pos = pos
|
|
234
|
+
while (new_pos + cols) < matching.length
|
|
235
|
+
new_pos += cols
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
@index = matching[new_pos]
|
|
240
|
+
end
|
|
241
|
+
alias_method :_up, :move_up
|
|
242
|
+
|
|
243
|
+
# Desplaza el cursor de selección una fila hacia abajo.
|
|
244
|
+
def move_down
|
|
245
|
+
matching = current_matching_indices
|
|
246
|
+
return @index if matching.empty?
|
|
247
|
+
cols = @columns
|
|
248
|
+
pos = matching.index(@index) || 0
|
|
249
|
+
if cols <= 1
|
|
250
|
+
new_pos = (pos + 1) % matching.length
|
|
251
|
+
else
|
|
252
|
+
new_pos = pos + cols
|
|
253
|
+
if new_pos >= matching.length
|
|
254
|
+
new_pos = pos % cols
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
@index = matching[new_pos]
|
|
258
|
+
end
|
|
259
|
+
alias_method :_down, :move_down
|
|
260
|
+
|
|
261
|
+
# Desplaza el cursor de selección una columna hacia la izquierda.
|
|
262
|
+
def move_left
|
|
263
|
+
matching = current_matching_indices
|
|
264
|
+
return @index if matching.empty?
|
|
265
|
+
cols = @columns
|
|
266
|
+
pos = matching.index(@index) || 0
|
|
267
|
+
if cols <= 1
|
|
268
|
+
new_pos = (pos - 1) % matching.length
|
|
269
|
+
else
|
|
270
|
+
if (pos % cols) == 0
|
|
271
|
+
new_pos = [pos + (cols - 1), matching.length - 1].min
|
|
272
|
+
else
|
|
273
|
+
new_pos = pos - 1
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
@index = matching[new_pos]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Desplaza el cursor de selección una columna hacia la derecha.
|
|
280
|
+
def move_right
|
|
281
|
+
matching = current_matching_indices
|
|
282
|
+
return @index if matching.empty?
|
|
283
|
+
cols = @columns
|
|
284
|
+
pos = matching.index(@index) || 0
|
|
285
|
+
if cols <= 1
|
|
286
|
+
new_pos = (pos + 1) % matching.length
|
|
287
|
+
else
|
|
288
|
+
if (pos % cols) == (cols - 1) || pos == (matching.length - 1)
|
|
289
|
+
new_pos = pos - (pos % cols)
|
|
290
|
+
else
|
|
291
|
+
new_pos = pos + 1
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
@index = matching[new_pos]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Configura estilos a partir de una cadena con sintaxis de hoja de estilos GR.
|
|
298
|
+
#
|
|
299
|
+
# Respeta las propiedades que hayan sido configuradas explícitamente mediante SetStyle,
|
|
300
|
+
# garantizando que la configuración programática siempre tiene la máxima prioridad.
|
|
301
|
+
#
|
|
302
|
+
# @param css_content [String, nil] Bloque de texto con directivas <<menu ... >>
|
|
303
|
+
# @return [self, Integer] La instancia para encadenar llamadas, o el estilo actual si no se pasan argumentos.
|
|
304
|
+
def style(css_content = nil)
|
|
305
|
+
return @style if css_content.nil?
|
|
306
|
+
parsed = self.class.parse_config_text(css_content)
|
|
307
|
+
menu_cfg = ((parsed[:sections] && parsed[:sections]["menu"]) || {}).merge(parsed[:global] || {})
|
|
308
|
+
|
|
309
|
+
if menu_cfg["style"]
|
|
310
|
+
@style = menu_cfg["style"].to_i
|
|
311
|
+
@border_config = BORDERS[@style] || BORDERS[3]
|
|
312
|
+
end
|
|
313
|
+
@banner_style = menu_cfg["banner_style"].to_i if menu_cfg["banner_style"]
|
|
314
|
+
@animate = menu_cfg["animate"].to_s if menu_cfg["animate"]
|
|
315
|
+
@center = (menu_cfg["center"].to_s != "false") if menu_cfg.key?("center")
|
|
316
|
+
|
|
317
|
+
# Los colores de CSS solo se aplican si la propiedad no fue definida explícitamente en SetStyle
|
|
318
|
+
if (menu_cfg["border"] || menu_cfg["border_color"]) && !@style_config.explicitly_set?(:border)
|
|
319
|
+
c, l = self.class.extract_color_and_level(menu_cfg["border"] || menu_cfg["border_color"], 1)
|
|
320
|
+
@style_config.border(c, l, from_css: true)
|
|
321
|
+
end
|
|
322
|
+
if (menu_cfg["options"] || menu_cfg["options_color"]) && !@style_config.explicitly_set?(:options)
|
|
323
|
+
c, l = self.class.extract_color_and_level(menu_cfg["options"] || menu_cfg["options_color"], 1)
|
|
324
|
+
@style_config.options(c, l, from_css: true)
|
|
325
|
+
end
|
|
326
|
+
if (menu_cfg["focus"] || menu_cfg["focus_color"]) && !@style_config.explicitly_set?(:focus)
|
|
327
|
+
c, l = self.class.extract_color_and_level(menu_cfg["focus"] || menu_cfg["focus_color"], 2)
|
|
328
|
+
@style_config.focus(c, l, from_css: true)
|
|
329
|
+
end
|
|
330
|
+
if (menu_cfg["title"] || menu_cfg["title_color"]) && !@style_config.explicitly_set?(:title)
|
|
331
|
+
c, l = self.class.extract_color_and_level(menu_cfg["title"] || menu_cfg["title_color"], 2)
|
|
332
|
+
@style_config.title(c, l, from_css: true)
|
|
333
|
+
end
|
|
334
|
+
if (menu_cfg["banner"] || menu_cfg["banner_color"]) && !@style_config.explicitly_set?(:banner)
|
|
335
|
+
c, l = self.class.extract_color_and_level(menu_cfg["banner"] || menu_cfg["banner_color"], 2)
|
|
336
|
+
@style_config.banner(c, l, from_css: true)
|
|
337
|
+
end
|
|
338
|
+
if (menu_cfg["subtitle"] || menu_cfg["subtitle_color"]) && !@style_config.explicitly_set?(:subtitle)
|
|
339
|
+
c, l = self.class.extract_color_and_level(menu_cfg["subtitle"] || menu_cfg["subtitle_color"], 1)
|
|
340
|
+
@style_config.subtitle(c, l, from_css: true)
|
|
341
|
+
end
|
|
342
|
+
if (menu_cfg["divider"] || menu_cfg["divider_color"]) && !@style_config.explicitly_set?(:divider)
|
|
343
|
+
c, l = self.class.extract_color_and_level(menu_cfg["divider"] || menu_cfg["divider_color"], 1)
|
|
344
|
+
@style_config.divider(c, l, from_css: true)
|
|
345
|
+
end
|
|
346
|
+
if (menu_cfg["desc_prefix"] || menu_cfg["description_prefix"] || menu_cfg["prefix"]) && !@style_config.explicitly_set?(:desc_prefix)
|
|
347
|
+
@style_config.desc_prefix(menu_cfg["desc_prefix"] || menu_cfg["description_prefix"] || menu_cfg["prefix"], from_css: true)
|
|
348
|
+
end
|
|
349
|
+
if menu_cfg["font"] && !@style_config.explicitly_set?(:font)
|
|
350
|
+
@style_config.font(menu_cfg["font"].to_i, from_css: true)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
@mouse = (menu_cfg["mouse"].to_s == "true") if menu_cfg.key?("mouse")
|
|
354
|
+
|
|
355
|
+
if parsed[:sections] && parsed[:sections]["tabs"]
|
|
356
|
+
tabs_sec = parsed[:sections]["tabs"]
|
|
357
|
+
@active_tab_color = tabs_sec["active_tab"] || tabs_sec["active_tab_color"] || @active_tab_color if (tabs_sec["active_tab"] || tabs_sec["active_tab_color"])
|
|
358
|
+
@tab_color = tabs_sec["tab_color"] || tabs_sec["inactive_tab"] || tabs_sec["color"] || @tab_color if (tabs_sec["tab_color"] || tabs_sec["inactive_tab"] || tabs_sec["color"])
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
self
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Setter para style que acepta tanto un entero de estilo como una hoja de estilos completa.
|
|
365
|
+
def style=(val)
|
|
366
|
+
if val.is_a?(String) && (val.include?("::") || val.include?("<<") || val.include?("@theme"))
|
|
367
|
+
style(val)
|
|
368
|
+
else
|
|
369
|
+
@style = val.to_i
|
|
370
|
+
@border_config = BORDERS[@style] || BORDERS[3]
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Exporta la configuración visual y colores actuales del menú a un archivo .gr.
|
|
375
|
+
#
|
|
376
|
+
# @param path [String, nil] Ruta de salida. Si es nil, infiere el nombre del archivo llamador.
|
|
377
|
+
# @return [String] Ruta final del archivo generado.
|
|
378
|
+
def export_config(path = nil)
|
|
379
|
+
if path.nil?
|
|
380
|
+
caller_loc = caller_locations.find { |c| !c.path.include?(__FILE__) }
|
|
381
|
+
base = caller_loc ? caller_loc.path.sub(/\.rb$/, '') : "theme"
|
|
382
|
+
path = "#{base}.gr"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
b_cfg = @style_config&.border || SetStyle.border
|
|
386
|
+
t_cfg = @style_config&.title || SetStyle.title
|
|
387
|
+
f_cfg = @style_config&.focus || SetStyle.focus
|
|
388
|
+
o_cfg = @style_config&.options || SetStyle.options
|
|
389
|
+
bn_cfg = @style_config&.banner || SetStyle.banner
|
|
390
|
+
s_cfg = @style_config&.subtitle || SetStyle.subtitle
|
|
391
|
+
d_cfg = @style_config&.divider || SetStyle.divider
|
|
392
|
+
dp_val = @style_config&.desc_prefix || SetStyle.desc_prefix
|
|
393
|
+
|
|
394
|
+
lines = ["GRmenu::config<-1->", ""]
|
|
395
|
+
lines << "@theme:: \"#{File.basename(path, '.gr').capitalize}\""
|
|
396
|
+
lines << "@author:: \"grcode\""
|
|
397
|
+
lines << "@version:: \"1.0\""
|
|
398
|
+
lines << ""
|
|
399
|
+
lines << "<<menu"
|
|
400
|
+
lines << " style:: #{@style || 3}"
|
|
401
|
+
lines << " banner_style:: #{@banner_style || 3}"
|
|
402
|
+
lines << " font:: #{@style_config&.font || SetStyle.font}"
|
|
403
|
+
lines << " animate:: #{@animate || 'rgb'}"
|
|
404
|
+
lines << " center:: #{@center.nil? ? true : @center}"
|
|
405
|
+
lines << " desc_prefix:: #{dp_val}"
|
|
406
|
+
lines << " mouse:: #{@mouse}" if @mouse
|
|
407
|
+
lines << " border:: #{b_cfg[:color]}:#{b_cfg[:level]}"
|
|
408
|
+
lines << " title:: #{t_cfg[:color]}:#{t_cfg[:level]}"
|
|
409
|
+
lines << " focus:: #{f_cfg[:color]}:#{f_cfg[:level]}"
|
|
410
|
+
lines << " options:: #{o_cfg[:color]}:#{o_cfg[:level]}"
|
|
411
|
+
lines << " banner:: #{bn_cfg[:color]}:#{bn_cfg[:level]}"
|
|
412
|
+
lines << " subtitle:: #{s_cfg[:color]}:#{s_cfg[:level]}"
|
|
413
|
+
lines << " divider:: #{d_cfg[:color]}:#{d_cfg[:level]}"
|
|
414
|
+
lines << ">>"
|
|
415
|
+
lines << ""
|
|
416
|
+
lines << "<<submenu"
|
|
417
|
+
lines << " style:: #{@style || 3}"
|
|
418
|
+
lines << " border:: #{b_cfg[:color]}:#{b_cfg[:level]}"
|
|
419
|
+
lines << " focus:: #{f_cfg[:color]}:#{f_cfg[:level]}"
|
|
420
|
+
lines << " options:: #{o_cfg[:color]}:#{o_cfg[:level]}"
|
|
421
|
+
lines << ">>"
|
|
422
|
+
lines << ""
|
|
423
|
+
lines << "<<tabs"
|
|
424
|
+
lines << " active_tab:: #{@active_tab_color || 'yellow'}:2"
|
|
425
|
+
lines << " tab_color:: #{@tab_color || 'gray'}:1"
|
|
426
|
+
lines << ">>"
|
|
427
|
+
lines << ""
|
|
428
|
+
lines << "<<input"
|
|
429
|
+
lines << " style:: 3"
|
|
430
|
+
lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
|
|
431
|
+
lines << " title_color:: #{t_cfg[:color]}:#{t_cfg[:level]}"
|
|
432
|
+
lines << " label_color:: white:1"
|
|
433
|
+
lines << ">>"
|
|
434
|
+
lines << ""
|
|
435
|
+
lines << "<<table"
|
|
436
|
+
lines << " style:: #{@style || 3}"
|
|
437
|
+
lines << " header_color:: yellow:2"
|
|
438
|
+
lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
|
|
439
|
+
lines << " selected_row:: #{f_cfg[:color]}:#{f_cfg[:level]}"
|
|
440
|
+
lines << " row_color:: white:1"
|
|
441
|
+
lines << " zebra_striping:: true"
|
|
442
|
+
lines << ">>"
|
|
443
|
+
lines << ""
|
|
444
|
+
lines << "<<card"
|
|
445
|
+
lines << " style:: 7"
|
|
446
|
+
lines << " border_color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
|
|
447
|
+
lines << " title_color:: #{t_cfg[:color]}:#{t_cfg[:level]}"
|
|
448
|
+
lines << " content_color:: white:1"
|
|
449
|
+
lines << ">>"
|
|
450
|
+
lines << ""
|
|
451
|
+
lines << "<<slider"
|
|
452
|
+
lines << " style:: #{@style || 3}"
|
|
453
|
+
lines << " color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
|
|
454
|
+
lines << " fill_char:: █"
|
|
455
|
+
lines << " empty_char:: ░"
|
|
456
|
+
lines << ">>"
|
|
457
|
+
lines << ""
|
|
458
|
+
lines << "<<checkbox"
|
|
459
|
+
lines << " style:: #{@style || 3}"
|
|
460
|
+
lines << " color:: #{b_cfg[:color]}:#{b_cfg[:level]}"
|
|
461
|
+
lines << " checked_mark:: [X]"
|
|
462
|
+
lines << " unchecked_mark:: [ ]"
|
|
463
|
+
lines << ">>"
|
|
464
|
+
lines << ""
|
|
465
|
+
File.write(path, lines.join("\n") + "\n")
|
|
466
|
+
path
|
|
467
|
+
end
|
|
468
|
+
alias_method :export_theme, :export_config
|
|
469
|
+
|
|
470
|
+
# Inicia el ciclo interactivo del menú en la terminal.
|
|
471
|
+
#
|
|
472
|
+
# Muestra las opciones, procesa pulsaciones de teclado y clics del ratón,
|
|
473
|
+
# redibuja animaciones si están activas y ejecuta la acción elegida.
|
|
474
|
+
#
|
|
475
|
+
# @param size_max [Integer] Ancho mínimo o sugerido de la caja.
|
|
476
|
+
# @param min_width [Integer, nil] Alias de ancho mínimo.
|
|
477
|
+
# @return [Object, nil] El resultado de la acción ejecutada o nil si se canceló.
|
|
478
|
+
def draw(size_max: 20, min_width: nil)
|
|
479
|
+
# Soporte para exportación rápida vía argumento en línea de comandos (-theme o -ex)
|
|
480
|
+
if ARGV.any? { |a| ["-theme", "--theme", "-ex", "--export-theme"].include?(a.to_s.downcase) }
|
|
481
|
+
out_idx = ARGV.index { |a| ["-o", "--out", "--output"].include?(a.to_s.downcase) }
|
|
482
|
+
target_file = out_idx ? ARGV[out_idx + 1] : "tema_exportado.gr"
|
|
483
|
+
export_config(target_file)
|
|
484
|
+
Kernel.puts Color.bright_green("[OK] Tema exportado exitosamente a: #{target_file}")
|
|
485
|
+
exit(0)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
target_width = min_width || size_max || 20
|
|
489
|
+
action_to_execute = nil
|
|
490
|
+
|
|
491
|
+
self.class.enable_windows_vt
|
|
492
|
+
$stdout.sync = true
|
|
493
|
+
|
|
494
|
+
is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
|
|
495
|
+
|
|
496
|
+
begin
|
|
497
|
+
Kernel.print("#{HIDE_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
|
|
498
|
+
Kernel.print(ENABLE_MOUSE) if @mouse
|
|
499
|
+
$stdout.flush
|
|
500
|
+
|
|
501
|
+
# Si hay una transición de entrada configurada (:diagonal, :linear, :fade, :rainbow, :chroma, :rgb, :chromatic, :cromatico)
|
|
502
|
+
intro_anim = @animate.to_s.downcase
|
|
503
|
+
if ["diagonal", "linear", "fade", "rainbow", "chroma", "rgb", "chromatic", "cromatico"].include?(intro_anim)
|
|
504
|
+
intro_lines = render_lines(target_width)
|
|
505
|
+
self.class.animate_render(intro_lines, @animate)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
if is_tty
|
|
509
|
+
$stdin.raw do |raw_input_stream|
|
|
510
|
+
action_to_execute = run_interactive_loop(raw_input_stream, target_width)
|
|
511
|
+
end
|
|
512
|
+
else
|
|
513
|
+
action_to_execute = run_interactive_loop($stdin, target_width)
|
|
514
|
+
end
|
|
515
|
+
ensure
|
|
516
|
+
# Siempre restaurar el ratón y el cursor para no dejar la terminal corrupta
|
|
517
|
+
Kernel.print(DISABLE_MOUSE) if @mouse
|
|
518
|
+
Kernel.print(SHOW_CURSOR)
|
|
519
|
+
$stdout.flush
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
if action_to_execute
|
|
523
|
+
Kernel.print(CLEAR_SCREEN_SEQUENCE)
|
|
524
|
+
$stdout.flush
|
|
525
|
+
execute_action(action_to_execute)
|
|
526
|
+
else
|
|
527
|
+
Kernel.print(CLEAR_SCREEN_SEQUENCE)
|
|
528
|
+
$stdout.flush
|
|
529
|
+
end
|
|
530
|
+
rescue Interrupt
|
|
531
|
+
# Manejo limpio al interrumpir con Ctrl+C
|
|
532
|
+
Kernel.print(DISABLE_MOUSE) if @mouse
|
|
533
|
+
Kernel.print("#{SHOW_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
|
|
534
|
+
$stdout.flush
|
|
535
|
+
nil
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
private
|
|
539
|
+
|
|
540
|
+
# Dibuja un solo cuadro del menú en la posición de inicio del cursor (CURSOR_HOME)
|
|
541
|
+
def draw_frame(target_width)
|
|
542
|
+
lines = render_lines(target_width)
|
|
543
|
+
buffer = String.new(CURSOR_HOME)
|
|
544
|
+
lines.each_with_index do |line, idx|
|
|
545
|
+
buffer << line << CLEAR_TO_EOL
|
|
546
|
+
buffer << "\r\n" if idx < lines.length - 1
|
|
547
|
+
end
|
|
548
|
+
buffer << CLEAR_TO_EOS
|
|
549
|
+
Kernel.print(buffer)
|
|
550
|
+
$stdout.flush
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Bucle interactivo principal: escucha teclas, eventos de ratón y actualiza animación
|
|
554
|
+
def run_interactive_loop(input_stream, target_width)
|
|
555
|
+
matching = current_matching_indices
|
|
556
|
+
@index = matching.first || 0 unless matching.include?(@index)
|
|
557
|
+
@rgb_tick = 0.0
|
|
558
|
+
draw_frame(target_width)
|
|
559
|
+
|
|
560
|
+
animating = has_active_animation?
|
|
561
|
+
|
|
562
|
+
while true
|
|
563
|
+
# Si hay animación continua (RGB, Chroma, Cromático), esperamos con select no bloqueante
|
|
564
|
+
if animating
|
|
565
|
+
ready = false
|
|
566
|
+
if input_stream.respond_to?(:to_io) || input_stream.is_a?(IO)
|
|
567
|
+
begin
|
|
568
|
+
select_res = IO.select([input_stream], nil, nil, 0.035)
|
|
569
|
+
ready = true if select_res && select_res[0] && !select_res[0].empty?
|
|
570
|
+
rescue StandardError
|
|
571
|
+
ready = true
|
|
572
|
+
end
|
|
573
|
+
else
|
|
574
|
+
ready = true
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
unless ready
|
|
578
|
+
@rgb_tick += 0.08
|
|
579
|
+
draw_frame(target_width)
|
|
580
|
+
next
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
key = read_single_key(input_stream)
|
|
585
|
+
break if key.nil? || key == "\x03" || key == "\x04"
|
|
586
|
+
|
|
587
|
+
# Evento de ratón en formato SGR: \e[<Btn;Col;Row(M|m)
|
|
588
|
+
if key =~ /\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/
|
|
589
|
+
btn = $1.to_i
|
|
590
|
+
col = $2.to_i
|
|
591
|
+
row = $3.to_i
|
|
592
|
+
act = $4
|
|
593
|
+
|
|
594
|
+
# Rueda del ratón hacia arriba (btn 64)
|
|
595
|
+
if btn == 64
|
|
596
|
+
if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
|
|
597
|
+
s1_acts = get_submenu_actions(@functions[@index])
|
|
598
|
+
s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
|
|
599
|
+
@sub_index_2 = (@sub_index_2 - 1) % s2_acts.length if s2_acts && !s2_acts.empty?
|
|
600
|
+
@level = 2
|
|
601
|
+
elsif @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
|
|
602
|
+
s1_acts = get_submenu_actions(@functions[@index])
|
|
603
|
+
@sub_index_1 = (@sub_index_1 - 1) % s1_acts.length if s1_acts && !s1_acts.empty?
|
|
604
|
+
@sub_index_2 = 0
|
|
605
|
+
@level = 1
|
|
606
|
+
else
|
|
607
|
+
move_up
|
|
608
|
+
@sub_index_1 = 0
|
|
609
|
+
@sub_index_2 = 0
|
|
610
|
+
@level = 0
|
|
611
|
+
end
|
|
612
|
+
draw_frame(target_width)
|
|
613
|
+
next
|
|
614
|
+
# Rueda del ratón hacia abajo (btn 65)
|
|
615
|
+
elsif btn == 65
|
|
616
|
+
if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
|
|
617
|
+
s1_acts = get_submenu_actions(@functions[@index])
|
|
618
|
+
s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
|
|
619
|
+
@sub_index_2 = (@sub_index_2 + 1) % s2_acts.length if s2_acts && !s2_acts.empty?
|
|
620
|
+
@level = 2
|
|
621
|
+
elsif @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && is_submenu_item?(@functions[@index])
|
|
622
|
+
s1_acts = get_submenu_actions(@functions[@index])
|
|
623
|
+
@sub_index_1 = (@sub_index_1 + 1) % s1_acts.length if s1_acts && !s1_acts.empty?
|
|
624
|
+
@sub_index_2 = 0
|
|
625
|
+
@level = 1
|
|
626
|
+
else
|
|
627
|
+
move_down
|
|
628
|
+
@sub_index_1 = 0
|
|
629
|
+
@sub_index_2 = 0
|
|
630
|
+
@level = 0
|
|
631
|
+
end
|
|
632
|
+
draw_frame(target_width)
|
|
633
|
+
next
|
|
634
|
+
# Clic izquierdo del ratón (btn 0, act 'M')
|
|
635
|
+
elsif btn == 0 && act == "M"
|
|
636
|
+
# Clic sobre la barra de pestañas
|
|
637
|
+
if @tabs && !@tabs.empty? && @tabs_row && row == @tabs_row
|
|
638
|
+
clicked_tab = @tab_ranges.find { |_idx, rng| rng.cover?(col) }
|
|
639
|
+
if clicked_tab
|
|
640
|
+
@active_tab_idx = clicked_tab[0]
|
|
641
|
+
@functions = @tab_contents[@tabs[@active_tab_idx]] || []
|
|
642
|
+
@index = 0
|
|
643
|
+
@level = 0
|
|
644
|
+
@open_level = 0
|
|
645
|
+
@sub_index_1 = 0
|
|
646
|
+
@sub_index_2 = 0
|
|
647
|
+
@active_panel = :main
|
|
648
|
+
@submenu_open = false
|
|
649
|
+
draw_frame(target_width)
|
|
650
|
+
next
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Clic en flecha superior de scroll
|
|
655
|
+
if @up_arrow_row && row == @up_arrow_row
|
|
656
|
+
move_up
|
|
657
|
+
draw_frame(target_width)
|
|
658
|
+
next
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Clic en flecha inferior de scroll
|
|
662
|
+
if @down_arrow_row && row == @down_arrow_row
|
|
663
|
+
move_down
|
|
664
|
+
draw_frame(target_width)
|
|
665
|
+
next
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Clic dentro de submenú nivel 2
|
|
669
|
+
if @open_level >= 2 && @sub_2_col_rng && @sub_2_col_rng.cover?(col) && @sub_2_hit_map[row]
|
|
670
|
+
s2_clicked = @sub_2_hit_map[row]
|
|
671
|
+
s1_acts = get_submenu_actions(@functions[@index])
|
|
672
|
+
s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
|
|
673
|
+
if s2_acts && s2_clicked < s2_acts.length
|
|
674
|
+
@level = 2
|
|
675
|
+
@sub_index_2 = s2_clicked
|
|
676
|
+
return s2_acts[s2_clicked]
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# Clic dentro de submenú nivel 1
|
|
681
|
+
if @open_level >= 1 && @sub_1_col_rng && @sub_1_col_rng.cover?(col) && @sub_1_hit_map[row]
|
|
682
|
+
s1_clicked = @sub_1_hit_map[row]
|
|
683
|
+
s1_acts = get_submenu_actions(@functions[@index])
|
|
684
|
+
if s1_acts && s1_clicked < s1_acts.length
|
|
685
|
+
target_action = s1_acts[s1_clicked]
|
|
686
|
+
@level = 1
|
|
687
|
+
@sub_index_1 = s1_clicked
|
|
688
|
+
if is_submenu_item?(target_action)
|
|
689
|
+
@open_level = 2
|
|
690
|
+
@level = 2
|
|
691
|
+
@sub_index_2 = 0
|
|
692
|
+
draw_frame(target_width)
|
|
693
|
+
next
|
|
694
|
+
else
|
|
695
|
+
return target_action
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# Clic sobre una opción de la lista principal
|
|
701
|
+
if @row_hit_map && @row_hit_map[row]
|
|
702
|
+
hit = @row_hit_map[row]
|
|
703
|
+
x_in_box = col - hit[:margin_left]
|
|
704
|
+
if x_in_box > 0 && (hit[:total_w].nil? || x_in_box <= hit[:total_w])
|
|
705
|
+
c_idx = [[((x_in_box - 2) / ([hit[:col_w], 1].max + 2)).to_i, 0].max, hit[:cols] - 1].min
|
|
706
|
+
clicked_item = hit[:row_indices][c_idx]
|
|
707
|
+
if clicked_item
|
|
708
|
+
@index = clicked_item
|
|
709
|
+
if is_submenu_item?(@functions[clicked_item])
|
|
710
|
+
@open_level = 1
|
|
711
|
+
@level = 1
|
|
712
|
+
@sub_index_1 = 0
|
|
713
|
+
@sub_index_2 = 0
|
|
714
|
+
@active_panel = :sub
|
|
715
|
+
@submenu_open = true
|
|
716
|
+
draw_frame(target_width)
|
|
717
|
+
next
|
|
718
|
+
else
|
|
719
|
+
return @functions[clicked_item]
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
next
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Navegación entre pestañas con tecla Tab y Shift+Tab (\e[Z)
|
|
729
|
+
if @tabs && !@tabs.empty?
|
|
730
|
+
if key == "\t"
|
|
731
|
+
@active_tab_idx = (@active_tab_idx + 1) % @tabs.length
|
|
732
|
+
@functions = @tab_contents[@tabs[@active_tab_idx]] || []
|
|
733
|
+
@index = 0
|
|
734
|
+
@level = 0
|
|
735
|
+
@open_level = 0
|
|
736
|
+
@sub_index_1 = 0
|
|
737
|
+
@sub_index_2 = 0
|
|
738
|
+
@active_panel = :main
|
|
739
|
+
@submenu_open = false
|
|
740
|
+
draw_frame(target_width)
|
|
741
|
+
next
|
|
742
|
+
elsif key == "\e[Z"
|
|
743
|
+
@active_tab_idx = (@active_tab_idx - 1) % @tabs.length
|
|
744
|
+
@functions = @tab_contents[@tabs[@active_tab_idx]] || []
|
|
745
|
+
@index = 0
|
|
746
|
+
@level = 0
|
|
747
|
+
@open_level = 0
|
|
748
|
+
@sub_index_1 = 0
|
|
749
|
+
@sub_index_2 = 0
|
|
750
|
+
@active_panel = :main
|
|
751
|
+
@submenu_open = false
|
|
752
|
+
draw_frame(target_width)
|
|
753
|
+
next
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
# Control de teclado enfocado en submenú nivel 2
|
|
758
|
+
if @level == 2 && @open_level >= 2 && is_submenu_item?(@functions[@index])
|
|
759
|
+
s1_acts = get_submenu_actions(@functions[@index])
|
|
760
|
+
s2_acts = get_submenu_actions(s1_acts[@sub_index_1]) if s1_acts
|
|
761
|
+
if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
|
|
762
|
+
@sub_index_2 = (@sub_index_2 - 1) % s2_acts.length if s2_acts && !s2_acts.empty?
|
|
763
|
+
draw_frame(target_width)
|
|
764
|
+
next
|
|
765
|
+
elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
|
|
766
|
+
@sub_index_2 = (@sub_index_2 + 1) % s2_acts.length if s2_acts && !s2_acts.empty?
|
|
767
|
+
draw_frame(target_width)
|
|
768
|
+
next
|
|
769
|
+
elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K" || key == "\e"
|
|
770
|
+
@open_level = 1
|
|
771
|
+
@level = 1
|
|
772
|
+
draw_frame(target_width)
|
|
773
|
+
next
|
|
774
|
+
elsif key == "\r" || key == "\n"
|
|
775
|
+
return s2_acts[@sub_index_2] if s2_acts && @sub_index_2 < s2_acts.length
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# Control de teclado enfocado en submenú nivel 1
|
|
780
|
+
if @level == 1 && @open_level >= 1 && is_submenu_item?(@functions[@index])
|
|
781
|
+
s1_acts = get_submenu_actions(@functions[@index])
|
|
782
|
+
if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
|
|
783
|
+
@sub_index_1 = (@sub_index_1 - 1) % s1_acts.length if s1_acts && !s1_acts.empty?
|
|
784
|
+
@sub_index_2 = 0
|
|
785
|
+
draw_frame(target_width)
|
|
786
|
+
next
|
|
787
|
+
elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
|
|
788
|
+
@sub_index_1 = (@sub_index_1 + 1) % s1_acts.length if s1_acts && !s1_acts.empty?
|
|
789
|
+
@sub_index_2 = 0
|
|
790
|
+
draw_frame(target_width)
|
|
791
|
+
next
|
|
792
|
+
elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K" || key == "\e"
|
|
793
|
+
@open_level = 0
|
|
794
|
+
@level = 0
|
|
795
|
+
@active_panel = :main
|
|
796
|
+
@submenu_open = false
|
|
797
|
+
draw_frame(target_width)
|
|
798
|
+
next
|
|
799
|
+
elsif key == "\e[C" || key == "\eOC" || key == "\xe0M" || key == "\x00M"
|
|
800
|
+
cur_l1 = s1_acts[@sub_index_1] if s1_acts
|
|
801
|
+
if is_submenu_item?(cur_l1)
|
|
802
|
+
@open_level = 2
|
|
803
|
+
@level = 2
|
|
804
|
+
@sub_index_2 = 0
|
|
805
|
+
draw_frame(target_width)
|
|
806
|
+
next
|
|
807
|
+
end
|
|
808
|
+
elsif key == "\r" || key == "\n"
|
|
809
|
+
cur_l1 = s1_acts[@sub_index_1] if s1_acts
|
|
810
|
+
if is_submenu_item?(cur_l1)
|
|
811
|
+
@open_level = 2
|
|
812
|
+
@level = 2
|
|
813
|
+
@sub_index_2 = 0
|
|
814
|
+
draw_frame(target_width)
|
|
815
|
+
next
|
|
816
|
+
else
|
|
817
|
+
return cur_l1
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# Salir con 'q' si no estamos escribiendo en la barra de búsqueda
|
|
823
|
+
if !@search && (key == "q" || key == "Q")
|
|
824
|
+
break
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# Tecla Escape: limpia búsqueda si hay texto, de lo contrario cierra el menú
|
|
828
|
+
if key == "\e"
|
|
829
|
+
if @search && !@query.empty?
|
|
830
|
+
@query.clear
|
|
831
|
+
matching = current_matching_indices
|
|
832
|
+
@index = matching.first || 0
|
|
833
|
+
draw_frame(target_width)
|
|
834
|
+
else
|
|
835
|
+
break
|
|
836
|
+
end
|
|
837
|
+
elsif key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
|
|
838
|
+
move_up
|
|
839
|
+
@sub_index_1 = 0
|
|
840
|
+
@sub_index_2 = 0
|
|
841
|
+
draw_frame(target_width)
|
|
842
|
+
elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
|
|
843
|
+
move_down
|
|
844
|
+
@sub_index_1 = 0
|
|
845
|
+
@sub_index_2 = 0
|
|
846
|
+
draw_frame(target_width)
|
|
847
|
+
elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\x00K"
|
|
848
|
+
if @open_level > 0
|
|
849
|
+
@open_level = 0
|
|
850
|
+
@level = 0
|
|
851
|
+
@active_panel = :main
|
|
852
|
+
@submenu_open = false
|
|
853
|
+
draw_frame(target_width)
|
|
854
|
+
else
|
|
855
|
+
move_left
|
|
856
|
+
draw_frame(target_width)
|
|
857
|
+
end
|
|
858
|
+
elsif key == "\e[C" || key == "\eOC" || key == "\xe0M" || key == "\x00M"
|
|
859
|
+
if is_submenu_item?(@functions[@index])
|
|
860
|
+
@open_level = 1
|
|
861
|
+
@level = 1
|
|
862
|
+
@sub_index_1 = 0
|
|
863
|
+
@sub_index_2 = 0
|
|
864
|
+
@active_panel = :sub
|
|
865
|
+
@submenu_open = true
|
|
866
|
+
draw_frame(target_width)
|
|
867
|
+
else
|
|
868
|
+
move_right
|
|
869
|
+
draw_frame(target_width)
|
|
870
|
+
end
|
|
871
|
+
elsif key == "\x7f" || key == "\b" || key == "\x08"
|
|
872
|
+
# Backspace en el campo de búsqueda
|
|
873
|
+
if @search && !@query.empty?
|
|
874
|
+
@query.chop!
|
|
875
|
+
matching = current_matching_indices
|
|
876
|
+
@index = matching.first || 0
|
|
877
|
+
draw_frame(target_width)
|
|
878
|
+
end
|
|
879
|
+
elsif key == "\x15"
|
|
880
|
+
# Ctrl+U: limpiar búsqueda
|
|
881
|
+
if @search
|
|
882
|
+
@query.clear
|
|
883
|
+
matching = current_matching_indices
|
|
884
|
+
@index = matching.first || 0
|
|
885
|
+
draw_frame(target_width)
|
|
886
|
+
end
|
|
887
|
+
elsif key == "\r" || key == "\n"
|
|
888
|
+
# Enter: abrir submenú o ejecutar la opción actual
|
|
889
|
+
matching = current_matching_indices
|
|
890
|
+
if matching.include?(@index)
|
|
891
|
+
if is_submenu_item?(@functions[@index])
|
|
892
|
+
@open_level = 1
|
|
893
|
+
@level = 1
|
|
894
|
+
@sub_index_1 = 0
|
|
895
|
+
@sub_index_2 = 0
|
|
896
|
+
@active_panel = :sub
|
|
897
|
+
@submenu_open = true
|
|
898
|
+
draw_frame(target_width)
|
|
899
|
+
else
|
|
900
|
+
return @functions[@index]
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
elsif @search && key =~ /^[[:print:]]$/
|
|
904
|
+
# Agregar letra a la búsqueda y filtrar opciones
|
|
905
|
+
@query << key
|
|
906
|
+
matching = current_matching_indices
|
|
907
|
+
@index = matching.first || 0
|
|
908
|
+
draw_frame(target_width)
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
nil
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# Lee una tecla individual o secuencia de escape desde el stream
|
|
916
|
+
def read_single_key(input_stream)
|
|
917
|
+
GRmenu.read_key_raw(input_stream)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# Transforma nombres en snake_case o kebab-case a palabras capitalizadas legibles
|
|
921
|
+
def format_auto_name(raw_name)
|
|
922
|
+
cleaned = raw_name.to_s.gsub(/[_-]+/, ' ').strip
|
|
923
|
+
cleaned.split(' ').map(&:capitalize).join(' ')
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# Extrae la etiqueta visible para un elemento del menú según su tipo
|
|
927
|
+
def extract_name_from_action(action)
|
|
928
|
+
case action
|
|
929
|
+
when Array
|
|
930
|
+
action[0].to_s
|
|
931
|
+
when Hash
|
|
932
|
+
(action[:name] || action[:title] || action["name"] || action["title"] || "Opcion").to_s
|
|
933
|
+
when Method
|
|
934
|
+
format_auto_name(action.name)
|
|
935
|
+
when Symbol
|
|
936
|
+
format_auto_name(action)
|
|
937
|
+
when Proc
|
|
938
|
+
if action.respond_to?(:name) && action.name
|
|
939
|
+
format_auto_name(action.name)
|
|
940
|
+
else
|
|
941
|
+
"Opcion"
|
|
942
|
+
end
|
|
943
|
+
else
|
|
944
|
+
if action.respond_to?(:name)
|
|
945
|
+
format_auto_name(action.name)
|
|
946
|
+
elsif action.respond_to?(:title)
|
|
947
|
+
action.title.to_s
|
|
948
|
+
else
|
|
949
|
+
format_auto_name(action)
|
|
950
|
+
end
|
|
951
|
+
end
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
# Extrae la descripción secundaria asociada a una opción
|
|
955
|
+
def extract_description_from_action(action)
|
|
956
|
+
if action.is_a?(Array) && action.length >= 3
|
|
957
|
+
action[2].to_s
|
|
958
|
+
elsif action.is_a?(Hash)
|
|
959
|
+
(action[:desc] || action[:description] || action["desc"] || action["description"]).to_s
|
|
960
|
+
else
|
|
961
|
+
""
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# Ejecuta la acción seleccionada por el usuario (Proc, Method, Symbol o bloque invocable)
|
|
966
|
+
def execute_action(action)
|
|
967
|
+
case action
|
|
968
|
+
when Method, Proc
|
|
969
|
+
action.call
|
|
970
|
+
when Symbol
|
|
971
|
+
if Object.respond_to?(action, true)
|
|
972
|
+
Object.send(action)
|
|
973
|
+
elsif Kernel.respond_to?(action, true)
|
|
974
|
+
Kernel.send(action)
|
|
975
|
+
end
|
|
976
|
+
when Array
|
|
977
|
+
callable = action[1]
|
|
978
|
+
if callable.is_a?(Symbol)
|
|
979
|
+
if Object.respond_to?(callable, true)
|
|
980
|
+
Object.send(callable)
|
|
981
|
+
elsif Kernel.respond_to?(callable, true)
|
|
982
|
+
Kernel.send(callable)
|
|
983
|
+
end
|
|
984
|
+
elsif callable.respond_to?(:call)
|
|
985
|
+
callable.call
|
|
986
|
+
end
|
|
987
|
+
when Hash
|
|
988
|
+
callable = action[:action] || action[:call] || action["action"] || action["call"]
|
|
989
|
+
if callable.is_a?(Symbol)
|
|
990
|
+
if Object.respond_to?(callable, true)
|
|
991
|
+
Object.send(callable)
|
|
992
|
+
elsif Kernel.respond_to?(callable, true)
|
|
993
|
+
Kernel.send(callable)
|
|
994
|
+
end
|
|
995
|
+
elsif callable.respond_to?(:call)
|
|
996
|
+
callable.call
|
|
997
|
+
end
|
|
998
|
+
else
|
|
999
|
+
action.call if action.respond_to?(:call)
|
|
1000
|
+
end
|
|
1001
|
+
end
|
|
1002
|
+
end
|