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,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class GRmenu
|
|
4
|
+
# Muestra un dialogo interactivo de confirmacion con botones [ Si ] y [ No ].
|
|
5
|
+
# Soporta navegacion con flechas izquierda/derecha, Tab, Enter, Espacio o letras (s/y/n).
|
|
6
|
+
# Redibuja limpiamente el marco usando secuencias ANSI sin parpadeo de pantalla.
|
|
7
|
+
def self.confirm(question_arg = nil, question: nil, default: true, color: "cyan", style: 3)
|
|
8
|
+
actual_question = question || question_arg || "¿Confirmar acción?"
|
|
9
|
+
# 0 para 'Si', 1 para 'No'
|
|
10
|
+
choice = default ? 0 : 1
|
|
11
|
+
term_w = terminal_width
|
|
12
|
+
q_w = display_width(actual_question)
|
|
13
|
+
box_w = [q_w + 8, term_w - 4, 38].max
|
|
14
|
+
box_w = [box_w, 64].min
|
|
15
|
+
inner_w = box_w - 4
|
|
16
|
+
|
|
17
|
+
border_cfg = BORDERS[style] || BORDERS[3]
|
|
18
|
+
h_top = border_cfg[:ht] || border_cfg[:h]
|
|
19
|
+
h_bot = border_cfg[:hb] || border_cfg[:h]
|
|
20
|
+
v_l = border_cfg[:vl] || border_cfg[:v]
|
|
21
|
+
v_r = border_cfg[:vr] || border_cfg[:v]
|
|
22
|
+
|
|
23
|
+
is_rgb = (color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma")
|
|
24
|
+
color_code = is_rgb ? "" : ansi_color(color, 2)
|
|
25
|
+
reset_code = ansi_reset
|
|
26
|
+
|
|
27
|
+
top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
|
|
28
|
+
bot_fill = (h_bot * ((box_w - 2).to_f / h_bot.length).ceil)[0...(box_w - 2)]
|
|
29
|
+
|
|
30
|
+
drawn_lines = 0
|
|
31
|
+
|
|
32
|
+
render_confirm = lambda do
|
|
33
|
+
btn_yes = (choice == 0) ? Color.bright_green("> [ Sí ] <") : Color.gray(" [ Sí ] ")
|
|
34
|
+
btn_no = (choice == 1) ? Color.bright_red("> [ No ] <") : Color.gray(" [ No ] ")
|
|
35
|
+
raw_btns = (choice == 0 ? "> [ Sí ] <" : " [ Sí ] ") + " " + (choice == 1 ? "> [ No ] <" : " [ No ] ")
|
|
36
|
+
btns_vis_w = display_width(raw_btns)
|
|
37
|
+
pad_total = [inner_w - btns_vis_w, 0].max
|
|
38
|
+
left_p = " " * (pad_total / 2)
|
|
39
|
+
right_p = " " * (pad_total - (pad_total / 2))
|
|
40
|
+
btn_formatted_line = "#{left_p}#{btn_yes} #{btn_no}#{right_p}"
|
|
41
|
+
|
|
42
|
+
q_clean = actual_question.to_s
|
|
43
|
+
if display_width(q_clean) > inner_w
|
|
44
|
+
q_clean = q_clean[0...[inner_w - 3, 1].max] + "..."
|
|
45
|
+
end
|
|
46
|
+
pad_q = [inner_w - display_width(q_clean), 0].max
|
|
47
|
+
q_left = " " * (pad_q / 2)
|
|
48
|
+
q_right = " " * (pad_q - (pad_q / 2))
|
|
49
|
+
|
|
50
|
+
lines = []
|
|
51
|
+
if is_rgb
|
|
52
|
+
v_r_col = Color.rgb(v_r, (inner_w + 1) * 0.12)
|
|
53
|
+
lines << Color.rgb("#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}")
|
|
54
|
+
lines << "#{Color.rgb(v_l)} #{q_left}#{q_clean}#{q_right} #{v_r_col}"
|
|
55
|
+
lines << "#{Color.rgb(v_l)} #{' ' * inner_w} #{v_r_col}"
|
|
56
|
+
lines << "#{Color.rgb(v_l)} #{btn_formatted_line} #{v_r_col}"
|
|
57
|
+
lines << Color.rgb("#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}")
|
|
58
|
+
else
|
|
59
|
+
lines << "#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}"
|
|
60
|
+
lines << "#{color_code}#{v_l}#{reset_code} #{q_left}#{q_clean}#{q_right} #{color_code}#{v_r}#{reset_code}"
|
|
61
|
+
lines << "#{color_code}#{v_l}#{reset_code} #{' ' * inner_w} #{color_code}#{v_r}#{reset_code}"
|
|
62
|
+
lines << "#{color_code}#{v_l}#{reset_code} #{btn_formatted_line} #{color_code}#{v_r}#{reset_code}"
|
|
63
|
+
lines << "#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
frame = lines.join("\r\n") + "\r\n"
|
|
67
|
+
# Sube el cursor y borra el frame previo para redibujar en el mismo lugar
|
|
68
|
+
Kernel.print("\e[#{drawn_lines}A\e[J") if drawn_lines > 0
|
|
69
|
+
Kernel.print(frame)
|
|
70
|
+
$stdout.flush
|
|
71
|
+
drawn_lines = lines.length
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
|
|
75
|
+
result = false
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
Kernel.print(HIDE_CURSOR)
|
|
79
|
+
render_confirm.call
|
|
80
|
+
|
|
81
|
+
reader = lambda do |stream|
|
|
82
|
+
while (key = GRmenu.read_key_raw(stream))
|
|
83
|
+
break if key == "q" || key == "Q" || key == "\x03" || key == "\e"
|
|
84
|
+
if key == "s" || key == "S" || key == "y" || key == "Y"
|
|
85
|
+
result = true
|
|
86
|
+
break
|
|
87
|
+
elsif key == "n" || key == "N"
|
|
88
|
+
result = false
|
|
89
|
+
break
|
|
90
|
+
# Flechas izquierda/derecha o Tab para alternar seleccion
|
|
91
|
+
elsif key == "\e[D" || key == "\eOD" || key == "\xe0K" || key == "\t" || key == "\e[C" || key == "\eOC" || key == "\xe0M"
|
|
92
|
+
choice = 1 - choice
|
|
93
|
+
render_confirm.call
|
|
94
|
+
elsif key == "\r" || key == "\n" || key == " "
|
|
95
|
+
result = (choice == 0)
|
|
96
|
+
break
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
if is_tty
|
|
102
|
+
$stdin.raw { |s| reader.call(s) }
|
|
103
|
+
else
|
|
104
|
+
reader.call($stdin)
|
|
105
|
+
end
|
|
106
|
+
ensure
|
|
107
|
+
Kernel.print(SHOW_CURSOR)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
result
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Muestra una tarjeta de alerta con estilo semantico (:success, :error, :warning, :info)
|
|
114
|
+
def self.alert(type, message, title: nil, style: 3, color: nil, border_color: nil, title_color: nil, pause: true)
|
|
115
|
+
type_sym = type.to_sym rescue :info
|
|
116
|
+
tag, def_col, def_title = case type_sym
|
|
117
|
+
when :success, :ok
|
|
118
|
+
["[✔ EXITO]", "green", "Operacion Exitosa"]
|
|
119
|
+
when :error, :fail, :danger
|
|
120
|
+
["[✖ ERROR]", "red", "Error en el Sistema"]
|
|
121
|
+
when :warning, :warn
|
|
122
|
+
["[⚠ AVISO]", "yellow", "Advertencia"]
|
|
123
|
+
else
|
|
124
|
+
["[ℹ INFO]", "cyan", "Informacion"]
|
|
125
|
+
end
|
|
126
|
+
card_col = border_color || color || def_col
|
|
127
|
+
card_title = title || "#{tag} #{def_title}"
|
|
128
|
+
card(title: card_title, content: message, style: style, color: card_col, title_color: title_color, pause: pause)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Muestra una tarjeta o panel informativo con ajuste de texto automatico (word-wrapping)
|
|
132
|
+
def self.card(title_or_content = nil, content_arg = nil, title: nil, content: nil, style: nil, color: nil, border_color: nil, title_color: nil, content_color: nil, width: nil, pause: false)
|
|
133
|
+
card_theme_section = (@@global_theme.is_a?(Hash) && @@global_theme.dig(:sections, "card")) || {}
|
|
134
|
+
actual_title = title || (content_arg ? title_or_content : nil)
|
|
135
|
+
actual_content = (content || (content_arg ? content_arg : title_or_content) || "").to_s
|
|
136
|
+
style_num = (style || card_theme_section["style"] || 7).to_i
|
|
137
|
+
border_cfg = BORDERS[style_num] || BORDERS[7]
|
|
138
|
+
card_color = (border_color || color || card_theme_section["border_color"] || card_theme_section["color"] || "cyan").to_s
|
|
139
|
+
actual_title_color = (title_color || card_theme_section["title_color"] || "yellow").to_s
|
|
140
|
+
actual_content_color = (content_color || card_theme_section["content_color"] || "white").to_s
|
|
141
|
+
is_rgb = card_color.downcase == "rgb" || card_color.downcase == "rainbow" || card_color.downcase == "chroma"
|
|
142
|
+
|
|
143
|
+
lines = actual_content.split("\n")
|
|
144
|
+
content_max = lines.map { |l| display_width(l) }.max || 0
|
|
145
|
+
box_w = width || [content_max + 6, actual_title ? display_width(actual_title) + 6 : 0, 46].max
|
|
146
|
+
box_w = [box_w, terminal_width - 2].min
|
|
147
|
+
inner_w = box_w - 2
|
|
148
|
+
|
|
149
|
+
# Ajuste de lineas por palabras para que no desborden el marco
|
|
150
|
+
wrapped_lines = []
|
|
151
|
+
lines.each do |raw_l|
|
|
152
|
+
if display_width(raw_l) <= (inner_w - 2)
|
|
153
|
+
wrapped_lines << raw_l
|
|
154
|
+
else
|
|
155
|
+
current_word_line = String.new("")
|
|
156
|
+
raw_l.split(" ").each do |word|
|
|
157
|
+
if current_word_line.empty?
|
|
158
|
+
current_word_line << word
|
|
159
|
+
elsif display_width("#{current_word_line} #{word}") <= (inner_w - 2)
|
|
160
|
+
current_word_line << " " << word
|
|
161
|
+
else
|
|
162
|
+
wrapped_lines << current_word_line
|
|
163
|
+
current_word_line = String.new(word)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
wrapped_lines << current_word_line unless current_word_line.empty?
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
tl = border_cfg[:tl] || "#"
|
|
171
|
+
tr = border_cfg[:tr] || "#"
|
|
172
|
+
bl = border_cfg[:bl] || "#"
|
|
173
|
+
br = border_cfg[:br] || "#"
|
|
174
|
+
h_char = border_cfg[:h] || "─"
|
|
175
|
+
v_char = border_cfg[:v] || "│"
|
|
176
|
+
|
|
177
|
+
brd_col = is_rgb ? Color.rgb("").sub(/\e\[0m$/, '') : ansi_color(card_color, 1)
|
|
178
|
+
rst = ansi_reset
|
|
179
|
+
|
|
180
|
+
# Inserta el titulo decorativo centrado en la linea superior del marco
|
|
181
|
+
top_str = if actual_title && !actual_title.empty?
|
|
182
|
+
t_clean = " #{actual_title} "
|
|
183
|
+
t_len = display_width(t_clean)
|
|
184
|
+
if t_len > inner_w
|
|
185
|
+
t_clean = " #{actual_title[0...[inner_w - 6, 1].max]}... "
|
|
186
|
+
t_len = display_width(t_clean)
|
|
187
|
+
end
|
|
188
|
+
l_len = [(inner_w - t_len) / 2, 0].max
|
|
189
|
+
r_len = [inner_w - t_len - l_len, 0].max
|
|
190
|
+
h_char * l_len + ansi_color(actual_title_color, 2) + t_clean + brd_col + h_char * r_len
|
|
191
|
+
else
|
|
192
|
+
h_char * inner_w
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
out = +""
|
|
196
|
+
out << "#{brd_col}#{tl}#{top_str}#{tr}#{rst}\r\n"
|
|
197
|
+
wrapped_lines.each do |line|
|
|
198
|
+
pad_line = " " + line
|
|
199
|
+
out << "#{brd_col}#{v_char}#{rst}#{ansi_color(actual_content_color, 1)}#{pad_to_width(pad_line, inner_w)}#{rst}#{brd_col}#{v_char}#{rst}\r\n"
|
|
200
|
+
end
|
|
201
|
+
out << "#{brd_col}#{bl}#{h_char * inner_w}#{br}#{rst}\r\n"
|
|
202
|
+
|
|
203
|
+
Kernel.print(out)
|
|
204
|
+
self.continue if pause
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Dibuja una linea divisoria horizontal con estilo y color en la consola
|
|
208
|
+
def self.div(long = nil, color = "blue", level = 1, char = "─")
|
|
209
|
+
width = long || [terminal_width - 2, 64].min
|
|
210
|
+
if color.to_s.downcase == "rgb" || color.to_s.downcase == "rainbow" || color.to_s.downcase == "chroma"
|
|
211
|
+
Kernel.print("#{Color.rgb(char * width)}\r\n")
|
|
212
|
+
else
|
|
213
|
+
color_code = ansi_color(color, level)
|
|
214
|
+
reset_code = ansi_reset
|
|
215
|
+
Kernel.print("#{color_code}#{char * width}#{reset_code}\r\n")
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Muestra el contenido del archivo de ayuda help.txt sustituyendo etiquetas de color por escapes ANSI
|
|
220
|
+
def self.help(section = :all)
|
|
221
|
+
path = find_data_file("help.txt")
|
|
222
|
+
return unless path && File.exist?(path)
|
|
223
|
+
content = File.read(path)
|
|
224
|
+
COLORS.each do |color_name, levels|
|
|
225
|
+
if levels.is_a?(Hash)
|
|
226
|
+
content.gsub!("{#{color_name}}", ansi_color(color_name, 1))
|
|
227
|
+
content.gsub!("{bright_#{color_name}}", ansi_color(color_name, 2))
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
content.gsub!("{reset}", ansi_reset)
|
|
231
|
+
Kernel.print("\r\n#{content}\r\n")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Delegador de instancia para invocar ayuda desde un objeto menu
|
|
235
|
+
def help
|
|
236
|
+
self.class.help
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Pausa la ejecucion hasta que el usuario presione una tecla
|
|
240
|
+
def self.continue(text = "Presiona cualquier tecla para continuar...")
|
|
241
|
+
Kernel.print("#{Color.gray(text)} ")
|
|
242
|
+
if $stdin.respond_to?(:raw) && $stdin.respond_to?(:tty?) && $stdin.tty?
|
|
243
|
+
$stdin.raw(&:getch)
|
|
244
|
+
elsif $stdin.respond_to?(:getch)
|
|
245
|
+
$stdin.getch
|
|
246
|
+
else
|
|
247
|
+
$stdin.read(1)
|
|
248
|
+
end
|
|
249
|
+
Kernel.print("\r\n")
|
|
250
|
+
end
|
|
251
|
+
end
|