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,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