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
data/grmenu/set_style.rb
ADDED
|
@@ -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
|
data/grmenu/terminal.rb
ADDED
|
@@ -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
|