grmenu 0.1.4 → 2.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/GRmenu.rb +877 -110
- data/README.md +268 -126
- data/data/borders.json +85 -0
- data/data/colors.json +16 -0
- data/data/fonts.json +3005 -0
- metadata +9 -6
data/GRmenu.rb
CHANGED
|
@@ -1,14 +1,95 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'io/console'
|
|
4
|
+
require 'json'
|
|
4
5
|
|
|
5
6
|
class GRmenu
|
|
6
7
|
CLEAR_SCREEN_SEQUENCE = "\e[H\e[2J\e[3J"
|
|
7
|
-
HIDE_CURSOR
|
|
8
|
-
SHOW_CURSOR
|
|
9
|
-
CURSOR_HOME
|
|
10
|
-
CLEAR_TO_EOL
|
|
11
|
-
CLEAR_TO_EOS
|
|
8
|
+
HIDE_CURSOR = "\e[?25l"
|
|
9
|
+
SHOW_CURSOR = "\e[?25h"
|
|
10
|
+
CURSOR_HOME = "\e[H"
|
|
11
|
+
CLEAR_TO_EOL = "\e[K"
|
|
12
|
+
CLEAR_TO_EOS = "\e[J"
|
|
13
|
+
|
|
14
|
+
module Color
|
|
15
|
+
RESET = "\e[0m"
|
|
16
|
+
BOLD = "\e[1m"
|
|
17
|
+
|
|
18
|
+
CODES = {
|
|
19
|
+
black: { 1 => "\e[30m", 2 => "\e[90m" },
|
|
20
|
+
gray: { 1 => "\e[90m", 2 => "\e[38;5;245m" },
|
|
21
|
+
grey: { 1 => "\e[90m", 2 => "\e[38;5;245m" },
|
|
22
|
+
red: { 1 => "\e[31m", 2 => "\e[91m" },
|
|
23
|
+
green: { 1 => "\e[32m", 2 => "\e[92m" },
|
|
24
|
+
yellow: { 1 => "\e[33m", 2 => "\e[93m" },
|
|
25
|
+
blue: { 1 => "\e[34m", 2 => "\e[94m" },
|
|
26
|
+
magenta: { 1 => "\e[35m", 2 => "\e[95m" },
|
|
27
|
+
purple: { 1 => "\e[38;5;129m", 2 => "\e[38;5;141m" },
|
|
28
|
+
pink: { 1 => "\e[38;5;205m", 2 => "\e[38;5;218m" },
|
|
29
|
+
cyan: { 1 => "\e[36m", 2 => "\e[96m" },
|
|
30
|
+
aqua: { 1 => "\e[38;5;45m", 2 => "\e[38;5;51m" },
|
|
31
|
+
orange: { 1 => "\e[38;5;208m", 2 => "\e[38;5;214m" },
|
|
32
|
+
white: { 1 => "\e[37m", 2 => "\e[97m" }
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def paint(text, color_name, level = 1)
|
|
38
|
+
code = CODES.dig(color_name.to_sym, level) || "\e[37m"
|
|
39
|
+
"#{code}#{text}#{RESET}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def red(s); paint(s, :red, 1); end
|
|
43
|
+
def bright_red(s); paint(s, :red, 2); end
|
|
44
|
+
def dark_red(s); paint(s, :red, 1); end
|
|
45
|
+
|
|
46
|
+
def green(s); paint(s, :green, 1); end
|
|
47
|
+
def bright_green(s); paint(s, :green, 2); end
|
|
48
|
+
def dark_green(s); paint(s, :green, 1); end
|
|
49
|
+
|
|
50
|
+
def yellow(s); paint(s, :yellow, 1); end
|
|
51
|
+
def bright_yellow(s); paint(s, :yellow, 2); end
|
|
52
|
+
|
|
53
|
+
def blue(s); paint(s, :blue, 1); end
|
|
54
|
+
def bright_blue(s); paint(s, :blue, 2); end
|
|
55
|
+
|
|
56
|
+
def magenta(s); paint(s, :magenta, 1); end
|
|
57
|
+
def bright_magenta(s); paint(s, :magenta, 2); end
|
|
58
|
+
|
|
59
|
+
def purple(s); paint(s, :purple, 1); end
|
|
60
|
+
def bright_purple(s); paint(s, :purple, 2); end
|
|
61
|
+
|
|
62
|
+
def pink(s); paint(s, :pink, 1); end
|
|
63
|
+
def bright_pink(s); paint(s, :pink, 2); end
|
|
64
|
+
|
|
65
|
+
def cyan(s); paint(s, :cyan, 1); end
|
|
66
|
+
def bright_cyan(s); paint(s, :cyan, 2); end
|
|
67
|
+
|
|
68
|
+
def aqua(s); paint(s, :aqua, 1); end
|
|
69
|
+
def bright_aqua(s); paint(s, :aqua, 2); end
|
|
70
|
+
|
|
71
|
+
def orange(s); paint(s, :orange, 1); end
|
|
72
|
+
def bright_orange(s); paint(s, :orange, 2); end
|
|
73
|
+
|
|
74
|
+
def white(s); paint(s, :white, 1); end
|
|
75
|
+
def bright_white(s); paint(s, :white, 2); end
|
|
76
|
+
|
|
77
|
+
def black(s); paint(s, :black, 1); end
|
|
78
|
+
def gray(s); paint(s, :gray, 1); end
|
|
79
|
+
def bright_gray(s); paint(s, :gray, 2); end
|
|
80
|
+
def grey(s); gray(s); end
|
|
81
|
+
|
|
82
|
+
def r(s); bright_red(s); end
|
|
83
|
+
def dr(s); dark_red(s); end
|
|
84
|
+
def g(s); bright_green(s); end
|
|
85
|
+
def y(s); bright_yellow(s); end
|
|
86
|
+
def w(s); bright_white(s); end
|
|
87
|
+
def gr(s); gray(s); end
|
|
88
|
+
def cy(s); bright_cyan(s); end
|
|
89
|
+
def mg(s); bright_magenta(s); end
|
|
90
|
+
def bl(s); bright_blue(s); end
|
|
91
|
+
end
|
|
92
|
+
C = Color
|
|
12
93
|
|
|
13
94
|
STYLES = {
|
|
14
95
|
1 => "#", 2 => "┌", 3 => "╔", 4 => "┏", 5 => "╒",
|
|
@@ -18,15 +99,21 @@ class GRmenu
|
|
|
18
99
|
}.freeze
|
|
19
100
|
|
|
20
101
|
COLORS = {
|
|
21
|
-
"black"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
102
|
+
"black" => { 1 => "\e[30m", 2 => "\e[90m" },
|
|
103
|
+
"gray" => { 1 => "\e[90m", 2 => "\e[38;5;245m" },
|
|
104
|
+
"grey" => { 1 => "\e[90m", 2 => "\e[38;5;245m" },
|
|
105
|
+
"red" => { 1 => "\e[31m", 2 => "\e[91m" },
|
|
106
|
+
"green" => { 1 => "\e[32m", 2 => "\e[92m" },
|
|
107
|
+
"yellow" => { 1 => "\e[33m", 2 => "\e[93m" },
|
|
108
|
+
"blue" => { 1 => "\e[34m", 2 => "\e[94m" },
|
|
109
|
+
"magenta" => { 1 => "\e[35m", 2 => "\e[95m" },
|
|
110
|
+
"purple" => { 1 => "\e[38;5;129m", 2 => "\e[38;5;141m" },
|
|
111
|
+
"pink" => { 1 => "\e[38;5;205m", 2 => "\e[38;5;218m" },
|
|
112
|
+
"cyan" => { 1 => "\e[36m", 2 => "\e[96m" },
|
|
113
|
+
"aqua" => { 1 => "\e[38;5;45m", 2 => "\e[38;5;51m" },
|
|
114
|
+
"orange" => { 1 => "\e[38;5;208m", 2 => "\e[38;5;214m" },
|
|
115
|
+
"white" => { 1 => "\e[37m", 2 => "\e[97m" },
|
|
116
|
+
"reset" => "\e[0m"
|
|
30
117
|
}.freeze
|
|
31
118
|
|
|
32
119
|
BORDERS = {
|
|
@@ -37,29 +124,210 @@ class GRmenu
|
|
|
37
124
|
5 => { h: "═", v: "│", tl: "╒", tr: "╕", bl: "╘", br: "╛" },
|
|
38
125
|
6 => { h: "─", v: "║", tl: "╓", tr: "╖", bl: "╙", br: "╜" },
|
|
39
126
|
7 => { h: "─", v: "│", tl: "╭", tr: "╮", bl: "╰", br: "╯" },
|
|
40
|
-
8 => { h: "▀",
|
|
127
|
+
8 => { h: "▀", hb: "▄", v: "▌", vl: "▌", vr: "▐", tl: "▛", tr: "▜", bl: "▙", br: "▟" },
|
|
41
128
|
19 => { h: "●○", v: "●", tl: "●", tr: "●", bl: "●", br: "●" },
|
|
42
129
|
20 => { h: "★☆", v: "★", tl: "★", tr: "★", bl: "★", br: "★" }
|
|
43
130
|
}.freeze
|
|
44
131
|
|
|
132
|
+
def self._normalize_font(f)
|
|
133
|
+
normalized = {}
|
|
134
|
+
f.each do |key, lines|
|
|
135
|
+
max_w = lines.map(&:length).max
|
|
136
|
+
normalized[key] = lines.map { |line| line.ljust(max_w) }.freeze
|
|
137
|
+
end
|
|
138
|
+
normalized.freeze
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self._load_fonts
|
|
142
|
+
possible_paths = [
|
|
143
|
+
File.expand_path("data/fonts.json", __dir__),
|
|
144
|
+
File.expand_path("../data/fonts.json", __dir__),
|
|
145
|
+
File.expand_path("fonts.json", __dir__)
|
|
146
|
+
]
|
|
147
|
+
path = possible_paths.find { |p| File.file?(p) }
|
|
148
|
+
return {}.freeze unless path
|
|
149
|
+
|
|
150
|
+
raw_fonts = JSON.parse(File.read(path))
|
|
151
|
+
loaded = {}
|
|
152
|
+
raw_fonts.each do |font_key, chars|
|
|
153
|
+
font_id = font_key.to_i
|
|
154
|
+
loaded[font_id] = _normalize_font(chars)
|
|
155
|
+
end
|
|
156
|
+
loaded.freeze
|
|
157
|
+
rescue StandardError
|
|
158
|
+
{}.freeze
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
FONTS = _load_fonts
|
|
162
|
+
FONTS.each { |id, data| const_set("FONT_#{id}", data) }
|
|
163
|
+
FONT = FONTS[1] || {}.freeze
|
|
164
|
+
|
|
165
|
+
class ProgressBar
|
|
166
|
+
attr_reader :total, :current, :title, :status
|
|
167
|
+
|
|
168
|
+
def initialize(total = 100, title: nil, color: "cyan", level: 2, style: 3, width: nil)
|
|
169
|
+
@total = [total.to_i, 1].max
|
|
170
|
+
@current = 0
|
|
171
|
+
@title = title
|
|
172
|
+
@status = ""
|
|
173
|
+
@color = color.to_s.downcase
|
|
174
|
+
@level = level.to_i
|
|
175
|
+
@style = style.to_i
|
|
176
|
+
@width = width
|
|
177
|
+
@closed = false
|
|
178
|
+
@drawn_lines_count = 0
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def advance(step = 1, status: nil)
|
|
182
|
+
return if @closed
|
|
183
|
+
@current = [(@current + step), @total].min
|
|
184
|
+
@status = status.to_s if status
|
|
185
|
+
render
|
|
186
|
+
end
|
|
187
|
+
alias_method :increment, :advance
|
|
188
|
+
alias_method :step, :advance
|
|
189
|
+
|
|
190
|
+
def set(value, status: nil)
|
|
191
|
+
return if @closed
|
|
192
|
+
@current = [[value.to_i, 0].max, @total].min
|
|
193
|
+
@status = status.to_s if status
|
|
194
|
+
render
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def render
|
|
198
|
+
term_w = GRmenu.terminal_width
|
|
199
|
+
box_w = @width || [term_w - 4, 60].min
|
|
200
|
+
box_w = [box_w, 36].max
|
|
201
|
+
|
|
202
|
+
color_code = GRmenu::COLORS.dig(@color, @level) || "\e[1;96m"
|
|
203
|
+
reset_code = GRmenu::COLORS["reset"]
|
|
204
|
+
border_cfg = GRmenu::BORDERS[@style] || GRmenu::BORDERS[3]
|
|
205
|
+
|
|
206
|
+
v_l = border_cfg[:vl] || border_cfg[:v]
|
|
207
|
+
v_r = border_cfg[:vr] || border_cfg[:v]
|
|
208
|
+
h_t = border_cfg[:ht] || border_cfg[:h]
|
|
209
|
+
h_b = border_cfg[:hb] || border_cfg[:h]
|
|
210
|
+
|
|
211
|
+
top_fill = (h_t * ((box_w - 2).to_f / h_t.length).ceil)[0...(box_w - 2)]
|
|
212
|
+
bot_fill = (h_b * ((box_w - 2).to_f / h_b.length).ceil)[0...(box_w - 2)]
|
|
213
|
+
|
|
214
|
+
pct = ((@current.to_f / @total) * 100).round
|
|
215
|
+
pct_str = "#{pct}% (#{@current}/#{@total})"
|
|
216
|
+
|
|
217
|
+
inner_w = box_w - 4
|
|
218
|
+
bar_w = [inner_w - pct_str.length - 3, 10].max
|
|
219
|
+
filled_len = ((@current.to_f / @total) * bar_w).round
|
|
220
|
+
empty_len = bar_w - filled_len
|
|
221
|
+
|
|
222
|
+
bar_str = "[#{"█" * filled_len}#{"░" * empty_len}] #{pct_str}"
|
|
223
|
+
bar_line = bar_str.ljust(inner_w)[0...inner_w]
|
|
224
|
+
|
|
225
|
+
lines = []
|
|
226
|
+
lines << "#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}"
|
|
227
|
+
if @title && !@title.empty?
|
|
228
|
+
lines << "#{color_code}#{v_l}#{reset_code} #{@title.center(inner_w)} #{color_code}#{v_r}#{reset_code}"
|
|
229
|
+
lines << "#{color_code}#{v_l}#{top_fill}#{v_r}#{reset_code}"
|
|
230
|
+
end
|
|
231
|
+
lines << "#{color_code}#{v_l}#{reset_code} #{color_code}#{bar_line}#{reset_code} #{color_code}#{v_r}#{reset_code}"
|
|
232
|
+
if @status && !@status.empty?
|
|
233
|
+
stat_line = @status.ljust(inner_w)[0...inner_w]
|
|
234
|
+
lines << "#{color_code}#{v_l}#{reset_code} #{Color.gray(stat_line)} #{color_code}#{v_r}#{reset_code}"
|
|
235
|
+
end
|
|
236
|
+
lines << "#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}"
|
|
237
|
+
|
|
238
|
+
frame = lines.join("\r\n") + "\r\n"
|
|
239
|
+
|
|
240
|
+
if @drawn_lines_count && @drawn_lines_count > 0
|
|
241
|
+
Kernel.print("\e[#{@drawn_lines_count}A\e[J")
|
|
242
|
+
end
|
|
243
|
+
Kernel.print(frame)
|
|
244
|
+
$stdout.flush
|
|
245
|
+
@drawn_lines_count = lines.length
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def finish(status: "¡Completado!")
|
|
249
|
+
return if @closed
|
|
250
|
+
set(@total, status: status)
|
|
251
|
+
@closed = true
|
|
252
|
+
Kernel.print(GRmenu::SHOW_CURSOR)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def self.spinner(message = "Cargando...", color: "cyan", level: 2, delay: 0.08, &block)
|
|
257
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
258
|
+
color_name = color.to_s.downcase
|
|
259
|
+
color_code = COLORS.dig(color_name, level) || "\e[1;96m"
|
|
260
|
+
reset_code = COLORS["reset"]
|
|
261
|
+
|
|
262
|
+
stop_spinner = false
|
|
263
|
+
spinner_thread = Thread.new do
|
|
264
|
+
frame_idx = 0
|
|
265
|
+
while !stop_spinner
|
|
266
|
+
f = frames[frame_idx % frames.length]
|
|
267
|
+
Kernel.print("\r\e[K#{color_code}#{f}#{reset_code} #{message}")
|
|
268
|
+
$stdout.flush
|
|
269
|
+
frame_idx += 1
|
|
270
|
+
sleep(delay)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
begin
|
|
275
|
+
Kernel.print(HIDE_CURSOR)
|
|
276
|
+
result = block ? block.call : nil
|
|
277
|
+
stop_spinner = true
|
|
278
|
+
spinner_thread.join
|
|
279
|
+
success_color = COLORS.dig("green", 2) || "\e[1;92m"
|
|
280
|
+
Kernel.print("\r\e[K#{success_color}✔#{reset_code} #{message} #{Color.gray("¡Listo!")}\r\n")
|
|
281
|
+
result
|
|
282
|
+
rescue Exception => e
|
|
283
|
+
stop_spinner = true
|
|
284
|
+
spinner_thread.join rescue nil
|
|
285
|
+
error_color = COLORS.dig("red", 2) || "\e[1;91m"
|
|
286
|
+
Kernel.print("\r\e[K#{error_color}✖#{reset_code} #{message} #{Color.bright_red("(Error: #{e.message})")}\r\n")
|
|
287
|
+
raise e
|
|
288
|
+
ensure
|
|
289
|
+
stop_spinner = true
|
|
290
|
+
Kernel.print(SHOW_CURSOR)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def self.progress(total = 100, title: nil, color: "cyan", level: 2, style: 3, width: nil, &block)
|
|
295
|
+
bar = ProgressBar.new(total, title: title, color: color, level: level, style: style, width: width)
|
|
296
|
+
Kernel.print(HIDE_CURSOR)
|
|
297
|
+
bar.render
|
|
298
|
+
begin
|
|
299
|
+
result = block ? block.call(bar) : bar
|
|
300
|
+
bar.finish
|
|
301
|
+
result
|
|
302
|
+
ensure
|
|
303
|
+
Kernel.print(SHOW_CURSOR)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
45
307
|
class SetStyle
|
|
46
308
|
def initialize(
|
|
47
|
-
border:
|
|
48
|
-
options:
|
|
49
|
-
focus:
|
|
309
|
+
border: { color: "cyan", level: 1 },
|
|
310
|
+
options: { color: "white", level: 1 },
|
|
311
|
+
focus: { color: "green", level: 2 },
|
|
312
|
+
title: { color: "yellow", level: 2 },
|
|
313
|
+
banner: { color: "magenta", level: 2 },
|
|
314
|
+
subtitle: { color: "cyan", level: 2 },
|
|
315
|
+
divider: { color: "blue", level: 1 },
|
|
316
|
+
font: 1
|
|
50
317
|
)
|
|
51
|
-
@border
|
|
52
|
-
@options
|
|
53
|
-
@focus
|
|
318
|
+
@border = border.dup
|
|
319
|
+
@options = options.dup
|
|
320
|
+
@focus = focus.dup
|
|
321
|
+
@title = title.dup
|
|
322
|
+
@banner = banner.dup
|
|
323
|
+
@subtitle = subtitle.dup
|
|
324
|
+
@divider = divider.dup
|
|
325
|
+
@font = font.to_i
|
|
54
326
|
end
|
|
55
327
|
|
|
56
328
|
def border(color_name = nil, brightness_level = 1)
|
|
57
329
|
return @border if color_name.nil?
|
|
58
|
-
|
|
59
|
-
@border = color_name
|
|
60
|
-
else
|
|
61
|
-
@border = { color: color_name.to_s, level: brightness_level.to_i }
|
|
62
|
-
end
|
|
330
|
+
@border = parse_color(color_name, brightness_level)
|
|
63
331
|
end
|
|
64
332
|
alias_method :Border, :border
|
|
65
333
|
alias_method :set_border, :border
|
|
@@ -67,11 +335,7 @@ class GRmenu
|
|
|
67
335
|
|
|
68
336
|
def options(color_name = nil, brightness_level = 1)
|
|
69
337
|
return @options if color_name.nil?
|
|
70
|
-
|
|
71
|
-
@options = color_name
|
|
72
|
-
else
|
|
73
|
-
@options = { color: color_name.to_s, level: brightness_level.to_i }
|
|
74
|
-
end
|
|
338
|
+
@options = parse_color(color_name, brightness_level)
|
|
75
339
|
end
|
|
76
340
|
alias_method :Options, :options
|
|
77
341
|
alias_method :set_options, :options
|
|
@@ -79,25 +343,67 @@ class GRmenu
|
|
|
79
343
|
|
|
80
344
|
def focus(color_name = nil, brightness_level = 2)
|
|
81
345
|
return @focus if color_name.nil?
|
|
82
|
-
|
|
83
|
-
@focus = color_name
|
|
84
|
-
else
|
|
85
|
-
@focus = { color: color_name.to_s, level: brightness_level.to_i }
|
|
86
|
-
end
|
|
346
|
+
@focus = parse_color(color_name, brightness_level)
|
|
87
347
|
end
|
|
88
348
|
alias_method :Focus, :focus
|
|
89
349
|
alias_method :set_focus, :focus
|
|
90
350
|
alias_method :focus=, :focus
|
|
91
351
|
|
|
352
|
+
def title(color_name = nil, brightness_level = 2)
|
|
353
|
+
return @title if color_name.nil?
|
|
354
|
+
@title = parse_color(color_name, brightness_level)
|
|
355
|
+
end
|
|
356
|
+
alias_method :Title, :title
|
|
357
|
+
alias_method :set_title, :title
|
|
358
|
+
alias_method :title=, :title
|
|
359
|
+
|
|
360
|
+
def banner(color_name = nil, brightness_level = 2)
|
|
361
|
+
return @banner if color_name.nil?
|
|
362
|
+
@banner = parse_color(color_name, brightness_level)
|
|
363
|
+
end
|
|
364
|
+
alias_method :Banner, :banner
|
|
365
|
+
alias_method :set_banner, :banner
|
|
366
|
+
alias_method :banner=, :banner
|
|
367
|
+
|
|
368
|
+
def subtitle(color_name = nil, brightness_level = 2)
|
|
369
|
+
return @subtitle if color_name.nil?
|
|
370
|
+
@subtitle = parse_color(color_name, brightness_level)
|
|
371
|
+
end
|
|
372
|
+
alias_method :Subtitle, :subtitle
|
|
373
|
+
alias_method :set_subtitle, :subtitle
|
|
374
|
+
alias_method :subtitle=, :subtitle
|
|
375
|
+
|
|
376
|
+
def divider(color_name = nil, brightness_level = 1)
|
|
377
|
+
return @divider if color_name.nil?
|
|
378
|
+
@divider = parse_color(color_name, brightness_level)
|
|
379
|
+
end
|
|
380
|
+
alias_method :Divider, :divider
|
|
381
|
+
alias_method :set_divider, :divider
|
|
382
|
+
alias_method :divider=, :divider
|
|
383
|
+
|
|
384
|
+
def font(font_id = nil)
|
|
385
|
+
return @font if font_id.nil?
|
|
386
|
+
@font = font_id.to_i
|
|
387
|
+
end
|
|
388
|
+
alias_method :Font, :font
|
|
389
|
+
alias_method :set_font, :font
|
|
390
|
+
alias_method :font=, :font
|
|
391
|
+
|
|
392
|
+
private
|
|
393
|
+
|
|
394
|
+
def parse_color(color_val, default_level = 1)
|
|
395
|
+
if color_val.is_a?(Hash)
|
|
396
|
+
{ color: (color_val[:color] || color_val["color"]).to_s, level: (color_val[:level] || color_val["level"] || default_level).to_i }
|
|
397
|
+
else
|
|
398
|
+
{ color: color_val.to_s, level: default_level.to_i }
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
92
402
|
class << self
|
|
93
403
|
def border(color_name = nil, brightness_level = 1)
|
|
94
404
|
@default_border ||= { color: "cyan", level: 1 }
|
|
95
405
|
return @default_border if color_name.nil?
|
|
96
|
-
|
|
97
|
-
@default_border = color_name
|
|
98
|
-
else
|
|
99
|
-
@default_border = { color: color_name.to_s, level: brightness_level.to_i }
|
|
100
|
-
end
|
|
406
|
+
@default_border = { color: color_name.to_s, level: brightness_level.to_i }
|
|
101
407
|
end
|
|
102
408
|
alias_method :Border, :border
|
|
103
409
|
alias_method :border=, :border
|
|
@@ -105,11 +411,7 @@ class GRmenu
|
|
|
105
411
|
def options(color_name = nil, brightness_level = 1)
|
|
106
412
|
@default_options ||= { color: "white", level: 1 }
|
|
107
413
|
return @default_options if color_name.nil?
|
|
108
|
-
|
|
109
|
-
@default_options = color_name
|
|
110
|
-
else
|
|
111
|
-
@default_options = { color: color_name.to_s, level: brightness_level.to_i }
|
|
112
|
-
end
|
|
414
|
+
@default_options = { color: color_name.to_s, level: brightness_level.to_i }
|
|
113
415
|
end
|
|
114
416
|
alias_method :Options, :options
|
|
115
417
|
alias_method :options=, :options
|
|
@@ -117,14 +419,51 @@ class GRmenu
|
|
|
117
419
|
def focus(color_name = nil, brightness_level = 2)
|
|
118
420
|
@default_focus ||= { color: "green", level: 2 }
|
|
119
421
|
return @default_focus if color_name.nil?
|
|
120
|
-
|
|
121
|
-
@default_focus = color_name
|
|
122
|
-
else
|
|
123
|
-
@default_focus = { color: color_name.to_s, level: brightness_level.to_i }
|
|
124
|
-
end
|
|
422
|
+
@default_focus = { color: color_name.to_s, level: brightness_level.to_i }
|
|
125
423
|
end
|
|
126
424
|
alias_method :Focus, :focus
|
|
127
425
|
alias_method :focus=, :focus
|
|
426
|
+
|
|
427
|
+
def title(color_name = nil, brightness_level = 2)
|
|
428
|
+
@default_title ||= { color: "yellow", level: 2 }
|
|
429
|
+
return @default_title if color_name.nil?
|
|
430
|
+
@default_title = { color: color_name.to_s, level: brightness_level.to_i }
|
|
431
|
+
end
|
|
432
|
+
alias_method :Title, :title
|
|
433
|
+
alias_method :title=, :title
|
|
434
|
+
|
|
435
|
+
def banner(color_name = nil, brightness_level = 2)
|
|
436
|
+
@default_banner ||= { color: "magenta", level: 2 }
|
|
437
|
+
return @default_banner if color_name.nil?
|
|
438
|
+
@default_banner = { color: color_name.to_s, level: brightness_level.to_i }
|
|
439
|
+
end
|
|
440
|
+
alias_method :Banner, :banner
|
|
441
|
+
alias_method :banner=, :banner
|
|
442
|
+
|
|
443
|
+
def subtitle(color_name = nil, brightness_level = 2)
|
|
444
|
+
@default_subtitle ||= { color: "cyan", level: 2 }
|
|
445
|
+
return @default_subtitle if color_name.nil?
|
|
446
|
+
@default_subtitle = { color: color_name.to_s, level: brightness_level.to_i }
|
|
447
|
+
end
|
|
448
|
+
alias_method :Subtitle, :subtitle
|
|
449
|
+
alias_method :subtitle=, :subtitle
|
|
450
|
+
|
|
451
|
+
def divider(color_name = nil, brightness_level = 1)
|
|
452
|
+
@default_divider ||= { color: "blue", level: 1 }
|
|
453
|
+
return @default_divider if color_name.nil?
|
|
454
|
+
@default_divider = { color: color_name.to_s, level: brightness_level.to_i }
|
|
455
|
+
end
|
|
456
|
+
alias_method :Divider, :divider
|
|
457
|
+
alias_method :divider=, :divider
|
|
458
|
+
|
|
459
|
+
def font(font_id = nil)
|
|
460
|
+
@default_font ||= 1
|
|
461
|
+
return @default_font if font_id.nil?
|
|
462
|
+
@default_font = font_id.to_i
|
|
463
|
+
end
|
|
464
|
+
alias_method :Font, :font
|
|
465
|
+
alias_method :set_font, :font
|
|
466
|
+
alias_method :font=, :font
|
|
128
467
|
end
|
|
129
468
|
end
|
|
130
469
|
|
|
@@ -136,7 +475,7 @@ class GRmenu
|
|
|
136
475
|
end
|
|
137
476
|
end
|
|
138
477
|
|
|
139
|
-
attr_accessor :functions, :title, :style, :index, :style_config
|
|
478
|
+
attr_accessor :functions, :title, :subtitle, :banner, :banner_style, :divider, :style, :index, :style_config, :center, :page_size
|
|
140
479
|
|
|
141
480
|
alias_method :options, :functions
|
|
142
481
|
alias_method :options=, :functions=
|
|
@@ -144,37 +483,262 @@ class GRmenu
|
|
|
144
483
|
alias_method :selected_index=, :index=
|
|
145
484
|
alias_method :SetStyle, :style_config
|
|
146
485
|
alias_method :set_style, :style_config
|
|
486
|
+
alias_method :description, :subtitle
|
|
487
|
+
alias_method :description=, :subtitle=
|
|
488
|
+
|
|
489
|
+
def self.STYLES; STYLES; end
|
|
490
|
+
def self.COLORS; COLORS; end
|
|
491
|
+
def self.BORDERS; BORDERS; end
|
|
492
|
+
def self.FONTS; FONTS; end
|
|
493
|
+
def self.FONT; FONT_1; end
|
|
494
|
+
|
|
495
|
+
def self.terminal_width
|
|
496
|
+
cols = $stdout.winsize[1] rescue nil
|
|
497
|
+
cols = $stdin.winsize[1] rescue nil if cols.nil? || cols <= 0
|
|
498
|
+
(cols && cols > 0) ? cols : (ENV['COLUMNS'] ? ENV['COLUMNS'].to_i : 80)
|
|
499
|
+
rescue StandardError
|
|
500
|
+
80
|
|
501
|
+
end
|
|
147
502
|
|
|
148
|
-
def self.
|
|
149
|
-
|
|
503
|
+
def self.terminal_height
|
|
504
|
+
rows = $stdout.winsize[0] rescue nil
|
|
505
|
+
rows = $stdin.winsize[0] rescue nil if rows.nil? || rows <= 0
|
|
506
|
+
(rows && rows > 0) ? rows : (ENV['LINES'] ? ENV['LINES'].to_i : 24)
|
|
507
|
+
rescue StandardError
|
|
508
|
+
24
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def self.clear_screen
|
|
512
|
+
Kernel.print(CLEAR_SCREEN_SEQUENCE)
|
|
513
|
+
end
|
|
514
|
+
class << self
|
|
515
|
+
alias_method :clr, :clear_screen
|
|
150
516
|
end
|
|
151
517
|
|
|
152
|
-
def self.
|
|
153
|
-
|
|
518
|
+
def self.div(long = nil, color = "blue", level = 1, char = "─")
|
|
519
|
+
width = long || [terminal_width - 2, 64].min
|
|
520
|
+
color_code = COLORS.dig(color.to_s.downcase, level) || "\e[34m"
|
|
521
|
+
Kernel.print("#{color_code}#{char * width}#{COLORS['reset']}\r\n")
|
|
154
522
|
end
|
|
155
523
|
|
|
156
|
-
def self.
|
|
157
|
-
|
|
524
|
+
def self.help(section = :all)
|
|
525
|
+
w = [terminal_width - 4, 70].min
|
|
526
|
+
w = [w, 46].max
|
|
527
|
+
inner_w = w - 2
|
|
528
|
+
h_line = "═" * inner_w
|
|
529
|
+
s_line = "─" * w
|
|
530
|
+
|
|
531
|
+
Kernel.print "\r\n"
|
|
532
|
+
Kernel.print "#{Color.bright_cyan("╔" + h_line + "╗")}\r\n"
|
|
533
|
+
Kernel.print "#{Color.bright_cyan("║")}#{Color.bright_yellow("GRmenu - Guia y Referencia Completa (v2.0)".center(inner_w))}#{Color.bright_cyan("║")}\r\n"
|
|
534
|
+
Kernel.print "#{Color.bright_cyan("║")}#{Color.gray("Menus interactivos, Banners 3D, Barras de Progreso y TTY".center(inner_w))}#{Color.bright_cyan("║")}\r\n"
|
|
535
|
+
Kernel.print "#{Color.bright_cyan("╚" + h_line + "╝")}\r\n\r\n"
|
|
536
|
+
|
|
537
|
+
Kernel.print "#{Color.bright_magenta("[1] HELPERS NATIVOS")}\r\n"
|
|
538
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
539
|
+
Kernel.print " #{Color.bright_green("GRmenu.clear_screen")} #{Color.gray("(o GRmenu.clr)")}\r\n"
|
|
540
|
+
Kernel.print " * Limpia la terminal al instante con secuencias ANSI.\r\n"
|
|
541
|
+
Kernel.print " #{Color.bright_green("GRmenu.continue(mensaje)")}\r\n"
|
|
542
|
+
Kernel.print " * Pausa interactiva: espera una sola tecla en modo TTY crudo.\r\n"
|
|
543
|
+
Kernel.print " #{Color.bright_green("GRmenu.banner(texto, delay, color:, level:, style:, font:)")}\r\n"
|
|
544
|
+
Kernel.print " * Renderiza banner ASCII 3D con marco y animacion opcional.\r\n"
|
|
545
|
+
Kernel.print " #{Color.bright_green("GRmenu.spinner(mensaje, color:, level:, delay:, &bloque)")}\r\n"
|
|
546
|
+
Kernel.print " * Animacion giratoria fluida para tareas de tiempo desconocido.\r\n"
|
|
547
|
+
Kernel.print " * Ejemplo: #{Color.bright_white("GRmenu.spinner(\"Conectando...\") { conectar_db }")}\r\n"
|
|
548
|
+
Kernel.print " #{Color.bright_green("GRmenu.progress(total, title:, color:, level:, style:, width:, &bloque)")}\r\n"
|
|
549
|
+
Kernel.print " * Barra de progreso porcentual dentro de un recuadro estilizado.\r\n"
|
|
550
|
+
Kernel.print " * El bloque recibe 'bar'. Metodos disponibles:\r\n"
|
|
551
|
+
Kernel.print " - #{Color.cyan("bar.advance(n, status: \"...\")")} -> Avanza n pasos (alias: increment, step).\r\n"
|
|
552
|
+
Kernel.print " - #{Color.cyan("bar.set(valor, status: \"...\")")} -> Fija el valor exacto actual.\r\n"
|
|
553
|
+
Kernel.print " - #{Color.cyan("bar.finish(status: \"...\")")} -> Finaliza la barra al 100%.\r\n"
|
|
554
|
+
Kernel.print " * Ejemplo: #{Color.bright_white("GRmenu.progress(10, title: \"Copia\") { |b| 10.times { b.advance(1) } }")}\r\n"
|
|
555
|
+
Kernel.print " #{Color.bright_green("GRmenu.div(longitud, color, level, char)")}\r\n"
|
|
556
|
+
Kernel.print " * Dibuja linea divisoria horizontal adaptable a la consola.\r\n\r\n"
|
|
557
|
+
|
|
558
|
+
Kernel.print "#{Color.bright_magenta("[2] FORMATOS DE OPCIONES Y TOOLTIPS DINAMICOS")}\r\n"
|
|
559
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
560
|
+
Kernel.print " #{Color.cyan("1. Metodo directo:")} #{Color.bright_white("method(:iniciar)")} #{Color.gray("(auto-capitaliza nombre)")}\r\n"
|
|
561
|
+
Kernel.print " #{Color.cyan("2. Simbolo:")} #{Color.bright_white(":iniciar")}\r\n"
|
|
562
|
+
Kernel.print " #{Color.cyan("3. Nombre propio:")} #{Color.bright_white("[\"Mi Accion\", method(:iniciar)]")}\r\n"
|
|
563
|
+
Kernel.print " #{Color.cyan("4. Con Tooltip/Info:")} #{Color.bright_white("[\"Mi Accion\", method(:iniciar), \"Descripcion que sale abajo\"]")}\r\n"
|
|
564
|
+
Kernel.print " #{Color.cyan("5. Lambda / Proc:")} #{Color.bright_white("[\"Test\", -> { puts \"Hola\" }, \"Tooltip opcional\"]")}\r\n"
|
|
565
|
+
Kernel.print " #{Color.cyan("6. Hash:")} #{Color.bright_white("{ name: \"Test\", action: method(:iniciar), desc: \"Info\" }")}\r\n\r\n"
|
|
566
|
+
|
|
567
|
+
Kernel.print "#{Color.bright_magenta("[3] PARAMETROS DE GRmenu.new(functions, ...)")}\r\n"
|
|
568
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
569
|
+
Kernel.print " #{Color.bright_green("functions:")} #{Color.white("Array")} -> Lista de opciones (metodos, simbolos, arreglos, lambdas).\r\n"
|
|
570
|
+
Kernel.print " #{Color.bright_green("banner:")} #{Color.white("String")} -> Texto grande a renderizar en arte ASCII 3D.\r\n"
|
|
571
|
+
Kernel.print " #{Color.bright_green("title:")} #{Color.white("String")} -> Titulo en el encabezado del recuadro.\r\n"
|
|
572
|
+
Kernel.print " #{Color.bright_green("subtitle:")} #{Color.white("String")} -> Subtitulo / descripcion (soporta \\n).\r\n"
|
|
573
|
+
Kernel.print " #{Color.bright_green("page_size:")} #{Color.white("Integer")} -> Limite visible para scroll y paginacion automatica.\r\n"
|
|
574
|
+
Kernel.print " #{Color.bright_green("font:")} #{Color.white("Integer")} -> Fuente ASCII del banner (1 al 10, default 1).\r\n"
|
|
575
|
+
Kernel.print " #{Color.bright_green("style:")} #{Color.white("Integer")} -> Estilo de marco de opciones (1 al 20, default 19).\r\n"
|
|
576
|
+
Kernel.print " #{Color.bright_green("banner_style:")} #{Color.white("Integer")} -> Estilo de marco del banner (1 al 20, default 3).\r\n"
|
|
577
|
+
Kernel.print " #{Color.bright_green("divider:")} #{Color.white("Boolean")} -> Divisores alineados al banner (true/false).\r\n"
|
|
578
|
+
Kernel.print " #{Color.bright_green("center:")} #{Color.white("Boolean")} -> Centrado simetrico de subtitulo y menu (default true).\r\n\r\n"
|
|
579
|
+
|
|
580
|
+
Kernel.print "#{Color.bright_magenta("[4] AUTO-PAGINACION Y SCROLL")}\r\n"
|
|
581
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
582
|
+
Kernel.print " * #{Color.white("100% Automatica:")} Si la lista tiene muchas opciones o la pantalla es pequena,\r\n"
|
|
583
|
+
Kernel.print " GRmenu calcula el espacio disponible y genera una ventana deslizante suave.\r\n"
|
|
584
|
+
Kernel.print " * Indicadores visuales: #{Color.bright_yellow("▲ (+N arriba)")} y #{Color.bright_yellow("▼ (+M abajo)")}.\r\n"
|
|
585
|
+
Kernel.print " * Opcional: fija el limite con #{Color.bright_white("page_size: 8")} al instanciar #{Color.bright_green("GRmenu.new")}.\r\n\r\n"
|
|
586
|
+
|
|
587
|
+
Kernel.print "#{Color.bright_magenta("[5] MODULO DE COLORES (Color / C)")}\r\n"
|
|
588
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
589
|
+
Kernel.print " #{Color.cyan("Uso directo: ")}#{Color.bright_white("puts Color.green(\"Texto\")")} | #{Color.bright_white("puts Color.bright_cyan(\"Texto\")")}\r\n"
|
|
590
|
+
Kernel.print " #{Color.cyan("Paleta: ")}#{Color.red("red")}, #{Color.green("green")}, #{Color.yellow("yellow")}, #{Color.blue("blue")}, #{Color.magenta("magenta")}, #{Color.purple("purple")}, #{Color.pink("pink")}, #{Color.cyan("cyan")}, #{Color.aqua("aqua")}, #{Color.orange("orange")}, #{Color.white("white")}, #{Color.gray("gray")}, #{Color.black("black")}.\r\n"
|
|
591
|
+
Kernel.print " #{Color.cyan("Brillo: ")}#{Color.white("1")} = Normal, #{Color.bright_white("2")} = Brillante / Bold.\r\n\r\n"
|
|
592
|
+
|
|
593
|
+
Kernel.print "#{Color.bright_magenta("[6] FUENTES ASCII 3D DEL BANNER (font: 1 al 10)")}\r\n"
|
|
594
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
595
|
+
Kernel.print " #{Color.yellow("1")} -> #{Color.bright_white("ANSI Shadow 3D (Default)")} #{Color.cyan("[██████╗ ██╗ ██╗]")}\r\n"
|
|
596
|
+
Kernel.print " #{Color.yellow("2")} -> #{Color.bright_white("Slant 3D (FIGlet)")} #{Color.cyan("[ ____ __ __]")}\r\n"
|
|
597
|
+
Kernel.print " #{Color.yellow("3")} -> #{Color.bright_white("Doom / Standard 3D")} #{Color.cyan("[ ____ _ _]")}\r\n"
|
|
598
|
+
Kernel.print " #{Color.yellow("4")} -> #{Color.bright_white("Graffiti Shadow 3D")} #{Color.cyan("[ ,---. ,--. ,--.]")}\r\n"
|
|
599
|
+
Kernel.print " #{Color.yellow("5")} -> #{Color.bright_white("Small Slant / Mini 3D")} #{Color.cyan("[ ___ _ _]")}\r\n"
|
|
600
|
+
Kernel.print " #{Color.yellow("6")} -> #{Color.bright_white("Modular Pipe 3D")} #{Color.cyan("[ _____ _____]")}\r\n"
|
|
601
|
+
Kernel.print " #{Color.yellow("7")} -> #{Color.bright_white("Bubble / Round Gothic")} #{Color.cyan("[ ____ _ _]")}\r\n"
|
|
602
|
+
Kernel.print " #{Color.yellow("8")} -> #{Color.bright_white("Double-Line Wire 3D")} #{Color.cyan("[ ╔═════╗ ║ ║]")}\r\n"
|
|
603
|
+
Kernel.print " #{Color.yellow("9")} -> #{Color.bright_white("Solid Fat 3D Block")} #{Color.cyan("[ ██████▄ ██ ██]")}\r\n"
|
|
604
|
+
Kernel.print " #{Color.yellow("10")}-> #{Color.bright_white("Arcade Stars Matrix")} #{Color.cyan("[ ★★★★ ★ ★]")}\r\n\r\n"
|
|
605
|
+
|
|
606
|
+
Kernel.print "#{Color.bright_magenta("[7] ESTILOS DE MARCO (style / banner_style: 1 al 20)")}\r\n"
|
|
607
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
608
|
+
Kernel.print " #{Color.yellow("3")} -> #{Color.bright_white("Doble linea")} #{Color.cyan("╔═══╗ ║ ║ ╚═══╝")} (Default en Banner)\r\n"
|
|
609
|
+
Kernel.print " #{Color.yellow("7")} -> #{Color.bright_white("Curvas redondeadas")} #{Color.cyan("╭───╮ │ │ ╰───╯")}\r\n"
|
|
610
|
+
Kernel.print " #{Color.yellow("4")} -> #{Color.bright_white("Linea gruesa")} #{Color.cyan("┏━━━┓ ┃ ┃ ┗━━━┛")}\r\n"
|
|
611
|
+
Kernel.print " #{Color.yellow("2")} -> #{Color.bright_white("Linea simple")} #{Color.cyan("┌───┐ │ │ └───┘")}\r\n"
|
|
612
|
+
Kernel.print " #{Color.yellow("8")} -> #{Color.bright_white("Bloques outline")} #{Color.cyan("▛▀▀▀▜ ▌ ▐ ▙▄▄▄▟")}\r\n"
|
|
613
|
+
Kernel.print " #{Color.yellow("19")} -> #{Color.bright_white("Circulos")} #{Color.cyan("●○○○● ● ● ●○○○●")} (Default en Opciones)\r\n"
|
|
614
|
+
Kernel.print " #{Color.yellow("20")} -> #{Color.bright_white("Estrellas")} #{Color.cyan("★☆☆☆★ ★ ★ ★☆☆☆★")}\r\n\r\n"
|
|
615
|
+
|
|
616
|
+
Kernel.print "#{Color.bright_magenta("[8] METODOS DE CONFIGURACION (menu.set_style)")}\r\n"
|
|
617
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
618
|
+
Kernel.print " #{Color.cyan("menu.set_style.font(id)")} -> Cambia fuente ASCII (1..10)\r\n"
|
|
619
|
+
Kernel.print " #{Color.cyan("menu.set_style.banner(color, level)")} -> Color y brillo del banner ASCII\r\n"
|
|
620
|
+
Kernel.print " #{Color.cyan("menu.set_style.title(color, level)")} -> Color y brillo del titulo\r\n"
|
|
621
|
+
Kernel.print " #{Color.cyan("menu.set_style.subtitle(color, level)")} -> Color y brillo del subtitulo\r\n"
|
|
622
|
+
Kernel.print " #{Color.cyan("menu.set_style.divider(color, level)")} -> Color y brillo de las lineas divisorias\r\n"
|
|
623
|
+
Kernel.print " #{Color.cyan("menu.set_style.border(color, level)")} -> Color y brillo del marco de opciones\r\n"
|
|
624
|
+
Kernel.print " #{Color.cyan("menu.set_style.options(color, level)")} -> Color y brillo de opciones no activas\r\n"
|
|
625
|
+
Kernel.print " #{Color.cyan("menu.set_style.focus(color, level)")} -> Color y brillo de la opcion resaltada\r\n\r\n"
|
|
626
|
+
|
|
627
|
+
Kernel.print "#{Color.bright_magenta("[9] EJECUCION (menu.draw)")}\r\n"
|
|
628
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n"
|
|
629
|
+
Kernel.print " #{Color.bright_white("menu.draw(size_max: 38)")} -> Inicia el menu interactivo con ancho minimo.\r\n"
|
|
630
|
+
Kernel.print "#{Color.bright_blue(s_line)}\r\n\r\n"
|
|
158
631
|
end
|
|
159
632
|
|
|
160
|
-
def
|
|
633
|
+
def help
|
|
634
|
+
self.class.help
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def self.continue(text = "Presiona cualquier tecla para continuar...")
|
|
638
|
+
Kernel.print("#{Color.gray(text)} ")
|
|
639
|
+
if $stdin.respond_to?(:raw) && $stdin.respond_to?(:tty?) && $stdin.tty?
|
|
640
|
+
$stdin.raw(&:getch)
|
|
641
|
+
elsif $stdin.respond_to?(:getch)
|
|
642
|
+
$stdin.getch
|
|
643
|
+
else
|
|
644
|
+
$stdin.read(1)
|
|
645
|
+
end
|
|
646
|
+
Kernel.print("\r\n")
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def self.build_ascii_lines(text, max_cols = terminal_width, font_id = 1)
|
|
650
|
+
target_font = FONTS[font_id.to_i] || FONTS[1]
|
|
651
|
+
clean_chars = text.to_s.upcase.chars.select { |c| target_font.key?(c) }
|
|
652
|
+
return [] if clean_chars.empty?
|
|
653
|
+
|
|
654
|
+
font_height = target_font.values.first.length
|
|
655
|
+
|
|
656
|
+
[2, 1, 0].each do |spacing|
|
|
657
|
+
lines = Array.new(font_height, "")
|
|
658
|
+
clean_chars.each_with_index do |c, idx|
|
|
659
|
+
fig = target_font[c]
|
|
660
|
+
pad = (idx == clean_chars.length - 1) ? "" : (" " * spacing)
|
|
661
|
+
font_height.times { |i| lines[i] += fig[i] + pad }
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
max_len = lines.map(&:length).max
|
|
665
|
+
return lines if (max_len + 6) <= max_cols
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
nil
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def self.banner(text, delay = 0, color: "magenta", level: 2, style: 3, font: 1)
|
|
672
|
+
cols = terminal_width
|
|
673
|
+
color_code = COLORS.dig(color.to_s.downcase, level) || "\e[1;95m"
|
|
674
|
+
reset_code = COLORS["reset"]
|
|
675
|
+
|
|
676
|
+
ascii_rows = build_ascii_lines(text, cols, font)
|
|
677
|
+
border_cfg = BORDERS[style] || BORDERS[3]
|
|
678
|
+
h_top = border_cfg[:ht] || border_cfg[:h]
|
|
679
|
+
h_bot = border_cfg[:hb] || border_cfg[:h]
|
|
680
|
+
v_l = border_cfg[:vl] || border_cfg[:v]
|
|
681
|
+
v_r = border_cfg[:vr] || border_cfg[:v]
|
|
682
|
+
|
|
683
|
+
if ascii_rows
|
|
684
|
+
max_len = ascii_rows.map(&:length).max
|
|
685
|
+
top_fill = (h_top * ((max_len + 4).to_f / h_top.length).ceil)[0...(max_len + 4)]
|
|
686
|
+
bot_fill = (h_bot * ((max_len + 4).to_f / h_bot.length).ceil)[0...(max_len + 4)]
|
|
687
|
+
|
|
688
|
+
Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
|
|
689
|
+
ascii_rows.each do |line|
|
|
690
|
+
pad = " " * (max_len - line.length)
|
|
691
|
+
Kernel.print("#{color_code}#{v_l} #{line}#{pad} #{v_r}#{reset_code}\r\n")
|
|
692
|
+
sleep(delay) if delay > 0
|
|
693
|
+
end
|
|
694
|
+
Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
|
|
695
|
+
else
|
|
696
|
+
clean_t = text.to_s.strip
|
|
697
|
+
box_w = [clean_t.length + 6, cols - 2].min
|
|
698
|
+
top_fill = (h_top * ((box_w - 2).to_f / h_top.length).ceil)[0...(box_w - 2)]
|
|
699
|
+
bot_fill = (h_bot * ((box_w - 2).to_f / h_b.length).ceil)[0...(box_w - 2)]
|
|
700
|
+
|
|
701
|
+
Kernel.print("#{color_code}#{border_cfg[:tl]}#{top_fill}#{border_cfg[:tr]}#{reset_code}\r\n")
|
|
702
|
+
Kernel.print("#{color_code}#{v_l} #{clean_t.center(box_w - 4)} #{v_r}#{reset_code}\r\n")
|
|
703
|
+
Kernel.print("#{color_code}#{border_cfg[:bl]}#{bot_fill}#{border_cfg[:br]}#{reset_code}\r\n")
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
class << self
|
|
708
|
+
alias_method :message, :banner
|
|
709
|
+
alias_method :logo, :banner
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
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, **keyword_arguments)
|
|
161
713
|
@functions = functions.is_a?(Array) ? functions : Array(functions)
|
|
162
714
|
|
|
163
715
|
pos_title = positional_arguments[0]
|
|
164
716
|
pos_style = positional_arguments[1]
|
|
165
717
|
|
|
166
|
-
@title
|
|
167
|
-
@
|
|
168
|
-
@
|
|
169
|
-
@
|
|
718
|
+
@title = (title || pos_title || keyword_arguments[:title] || "").to_s
|
|
719
|
+
@banner = (banner || keyword_arguments[:banner] || "").to_s
|
|
720
|
+
@subtitle = (subtitle || description || keyword_arguments[:subtitle] || keyword_arguments[:description] || "").to_s
|
|
721
|
+
@divider = divider.nil? ? (!@banner.empty? || !@subtitle.empty?) : divider
|
|
722
|
+
@style = (style || pos_style || keyword_arguments[:style] || 19).to_i
|
|
723
|
+
@banner_style = (banner_style || keyword_arguments[:banner_style] || 3).to_i
|
|
724
|
+
@center = center.nil? ? true : center
|
|
725
|
+
@page_size = (page_size || keyword_arguments[:page_size])&.to_i
|
|
726
|
+
@index = 0
|
|
727
|
+
|
|
728
|
+
init_font = font || keyword_arguments[:font_style] || SetStyle.font || 1
|
|
170
729
|
|
|
171
730
|
@style_config = SetStyle.new(
|
|
172
|
-
border:
|
|
173
|
-
options:
|
|
174
|
-
focus:
|
|
731
|
+
border: SetStyle.border.dup,
|
|
732
|
+
options: SetStyle.options.dup,
|
|
733
|
+
focus: SetStyle.focus.dup,
|
|
734
|
+
title: SetStyle.title.dup,
|
|
735
|
+
banner: SetStyle.banner.dup,
|
|
736
|
+
subtitle: SetStyle.subtitle.dup,
|
|
737
|
+
divider: SetStyle.divider.dup,
|
|
738
|
+
font: init_font
|
|
175
739
|
)
|
|
176
740
|
end
|
|
177
|
-
|
|
741
|
+
|
|
178
742
|
def move_up
|
|
179
743
|
return @index if @functions.empty?
|
|
180
744
|
@index = (@index - 1) % @functions.length
|
|
@@ -209,73 +773,228 @@ class GRmenu
|
|
|
209
773
|
end
|
|
210
774
|
alias_method :_hline, :build_horizontal_line
|
|
211
775
|
|
|
212
|
-
def
|
|
213
|
-
|
|
776
|
+
def render_banner_lines(term_cols)
|
|
777
|
+
return [[], 0] if @banner.nil? || @banner.empty?
|
|
778
|
+
|
|
779
|
+
font_id = @style_config.font || 1
|
|
780
|
+
ascii_rows = self.class.build_ascii_lines(@banner, term_cols, font_id)
|
|
781
|
+
banner_border = BORDERS[@banner_style] || BORDERS[3]
|
|
782
|
+
banner_color_cfg = @style_config.banner
|
|
783
|
+
|
|
784
|
+
h_top = banner_border[:ht] || banner_border[:h]
|
|
785
|
+
h_bot = banner_border[:hb] || banner_border[:h]
|
|
786
|
+
v_l = banner_border[:vl] || banner_border[:v]
|
|
787
|
+
v_r = banner_border[:vr] || banner_border[:v]
|
|
788
|
+
|
|
789
|
+
lines = []
|
|
790
|
+
box_w = 0
|
|
791
|
+
if ascii_rows
|
|
792
|
+
content_w = ascii_rows.map(&:length).max
|
|
793
|
+
box_w = content_w + 6
|
|
794
|
+
top_fill = build_horizontal_line(h_top, content_w + 4)
|
|
795
|
+
bot_fill = build_horizontal_line(h_bot, content_w + 4)
|
|
796
|
+
|
|
797
|
+
lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg)
|
|
798
|
+
ascii_rows.each do |row|
|
|
799
|
+
pad = " " * (content_w - row.length)
|
|
800
|
+
lines << colorize("#{v_l} #{row}#{pad} #{v_r}", banner_color_cfg)
|
|
801
|
+
end
|
|
802
|
+
lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg)
|
|
803
|
+
else
|
|
804
|
+
clean_b = @banner.strip
|
|
805
|
+
box_w = [clean_b.length + 6, term_cols - 2].min
|
|
806
|
+
top_fill = build_horizontal_line(h_top, box_w - 2)
|
|
807
|
+
bot_fill = build_horizontal_line(h_bot, box_w - 2)
|
|
808
|
+
|
|
809
|
+
lines << colorize("#{banner_border[:tl]}#{top_fill}#{banner_border[:tr]}", banner_color_cfg)
|
|
810
|
+
lines << colorize("#{v_l} #{clean_b.center(box_w - 4)} #{v_r}", banner_color_cfg)
|
|
811
|
+
lines << colorize("#{banner_border[:bl]}#{bot_fill}#{banner_border[:br]}", banner_color_cfg)
|
|
812
|
+
end
|
|
813
|
+
[lines, box_w]
|
|
214
814
|
end
|
|
215
815
|
|
|
216
816
|
def render_lines(size_max = 20)
|
|
217
|
-
|
|
817
|
+
term_cols = self.class.terminal_width
|
|
818
|
+
term_rows = self.class.terminal_height
|
|
819
|
+
rendered_lines = []
|
|
820
|
+
|
|
821
|
+
banner_box_width = 0
|
|
822
|
+
banner_lines_count = 0
|
|
823
|
+
if @banner && !@banner.empty?
|
|
824
|
+
banner_lines, banner_box_width = render_banner_lines(term_cols)
|
|
825
|
+
rendered_lines.concat(banner_lines)
|
|
826
|
+
rendered_lines << ""
|
|
827
|
+
banner_lines_count = banner_lines.length + 1
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
all_names = @functions.map { |func| extract_name_from_action(func) }
|
|
831
|
+
all_descriptions = @functions.map { |func| extract_description_from_action(func) }
|
|
832
|
+
|
|
833
|
+
active_desc = all_descriptions[@index] || ""
|
|
834
|
+
|
|
835
|
+
calculated_width = [size_max, @title.length + 4].max
|
|
836
|
+
calculated_width = ([calculated_width] + all_names.map { |name| name.length + 6 }).max
|
|
837
|
+
calculated_width = ([calculated_width, active_desc.length + 8].max) unless active_desc.empty?
|
|
838
|
+
total_width = [calculated_width, term_cols - 2].min
|
|
218
839
|
|
|
219
|
-
|
|
840
|
+
reference_width = banner_box_width > 0 ? banner_box_width : total_width
|
|
841
|
+
margin_left = (@center && reference_width > total_width) ? " " * ((reference_width - total_width) / 2) : ""
|
|
842
|
+
|
|
843
|
+
subtitle_lines_count = 0
|
|
844
|
+
if @subtitle && !@subtitle.empty?
|
|
845
|
+
subtitle_lines = @subtitle.lines.map(&:chomp)
|
|
846
|
+
div_w = @divider.is_a?(Numeric) ? @divider.to_i : [reference_width, term_cols - 2].min
|
|
847
|
+
|
|
848
|
+
if @divider
|
|
849
|
+
rendered_lines << colorize("─" * div_w, @style_config.divider)
|
|
850
|
+
subtitle_lines_count += 1
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
subtitle_lines.each do |sub_line|
|
|
854
|
+
formatted_sub = @center ? sub_line.center(div_w) : sub_line
|
|
855
|
+
rendered_lines << colorize(formatted_sub, @style_config.subtitle)
|
|
856
|
+
subtitle_lines_count += 1
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
if @divider
|
|
860
|
+
rendered_lines << colorize("─" * div_w, @style_config.divider)
|
|
861
|
+
subtitle_lines_count += 1
|
|
862
|
+
end
|
|
863
|
+
rendered_lines << ""
|
|
864
|
+
subtitle_lines_count += 1
|
|
865
|
+
end
|
|
220
866
|
|
|
221
867
|
border_color_cfg = @style_config.border
|
|
222
868
|
options_color_cfg = @style_config.options
|
|
223
869
|
focus_color_cfg = @style_config.focus
|
|
870
|
+
title_color_cfg = @style_config.title
|
|
224
871
|
|
|
225
872
|
box_border = BORDERS[@style]
|
|
226
|
-
|
|
873
|
+
|
|
874
|
+
total_items = @functions.length
|
|
875
|
+
overhead = banner_lines_count + subtitle_lines_count + 6
|
|
876
|
+
overhead += 2 unless active_desc.empty?
|
|
877
|
+
available_rows = [term_rows - overhead - 2, 3].max
|
|
878
|
+
|
|
879
|
+
effective_page_size = if @page_size && @page_size > 0
|
|
880
|
+
[@page_size, total_items].min
|
|
881
|
+
elsif total_items > available_rows
|
|
882
|
+
available_rows
|
|
883
|
+
else
|
|
884
|
+
total_items
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
start_idx = 0
|
|
888
|
+
end_idx = total_items - 1
|
|
889
|
+
if total_items > effective_page_size
|
|
890
|
+
half = effective_page_size / 2
|
|
891
|
+
start_idx = [[@index - half, 0].max, total_items - effective_page_size].min
|
|
892
|
+
end_idx = start_idx + effective_page_size - 1
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
visible_indices = (start_idx..end_idx).to_a
|
|
896
|
+
has_more_above = start_idx > 0
|
|
897
|
+
has_more_below = end_idx < (total_items - 1)
|
|
898
|
+
|
|
899
|
+
avail_w = [total_width - 6, 1].max
|
|
227
900
|
|
|
228
901
|
if box_border
|
|
229
|
-
|
|
230
|
-
|
|
902
|
+
h_top = box_border[:ht] || box_border[:h]
|
|
903
|
+
h_bot = box_border[:hb] || box_border[:h]
|
|
904
|
+
v_l_raw = box_border[:vl] || box_border[:v]
|
|
905
|
+
v_r_raw = box_border[:vr] || box_border[:v]
|
|
906
|
+
|
|
907
|
+
top_fill = build_horizontal_line(h_top, total_width - 2)
|
|
908
|
+
bot_fill = build_horizontal_line(h_bot, total_width - 2)
|
|
909
|
+
mid_fill = build_horizontal_line(h_top, total_width - 2)
|
|
231
910
|
|
|
232
|
-
|
|
233
|
-
|
|
911
|
+
v_left = colorize(v_l_raw, border_color_cfg)
|
|
912
|
+
v_right = colorize(v_r_raw, border_color_cfg)
|
|
913
|
+
|
|
914
|
+
top_border_line = box_border[:tl] + top_fill + box_border[:tr]
|
|
915
|
+
rendered_lines << "#{margin_left}#{colorize(top_border_line, border_color_cfg)}"
|
|
234
916
|
|
|
235
917
|
unless @title.empty?
|
|
236
|
-
centered_title = @title.center(total_width - 4)
|
|
237
|
-
rendered_lines << "#{
|
|
918
|
+
centered_title = colorize(@title.center(total_width - 4), title_color_cfg)
|
|
919
|
+
rendered_lines << "#{margin_left}#{v_left} #{centered_title} #{v_right}"
|
|
920
|
+
|
|
921
|
+
separator_line = v_l_raw + mid_fill + v_r_raw
|
|
922
|
+
rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg)}"
|
|
923
|
+
end
|
|
238
924
|
|
|
239
|
-
|
|
240
|
-
|
|
925
|
+
if has_more_above
|
|
926
|
+
up_indicator = colorize("▲ (+#{start_idx} arriba)".center(avail_w + 2), { color: "gray", level: 2 })
|
|
927
|
+
rendered_lines << "#{margin_left}#{v_left} #{up_indicator} #{v_right}"
|
|
241
928
|
end
|
|
242
929
|
|
|
243
|
-
|
|
930
|
+
visible_indices.each do |current_index|
|
|
931
|
+
option_name = all_names[current_index]
|
|
244
932
|
if @index == current_index
|
|
245
|
-
highlighted_text = colorize("
|
|
246
|
-
rendered_lines << "#{
|
|
933
|
+
highlighted_text = colorize("> #{option_name.ljust(avail_w)}", focus_color_cfg)
|
|
934
|
+
rendered_lines << "#{margin_left}#{v_left} #{highlighted_text} #{v_right}"
|
|
247
935
|
else
|
|
248
|
-
normal_text = colorize("
|
|
249
|
-
rendered_lines << "#{
|
|
936
|
+
normal_text = colorize(" #{option_name.ljust(avail_w)}", options_color_cfg)
|
|
937
|
+
rendered_lines << "#{margin_left}#{v_left} #{normal_text} #{v_right}"
|
|
250
938
|
end
|
|
251
939
|
end
|
|
252
940
|
|
|
253
|
-
|
|
254
|
-
|
|
941
|
+
if has_more_below
|
|
942
|
+
remaining_below = total_items - 1 - end_idx
|
|
943
|
+
down_indicator = colorize("▼ (+#{remaining_below} abajo)".center(avail_w + 2), { color: "gray", level: 2 })
|
|
944
|
+
rendered_lines << "#{margin_left}#{v_left} #{down_indicator} #{v_right}"
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
unless active_desc.empty?
|
|
948
|
+
separator_line = v_l_raw + mid_fill + v_r_raw
|
|
949
|
+
rendered_lines << "#{margin_left}#{colorize(separator_line, border_color_cfg)}"
|
|
950
|
+
desc_text = colorize("ℹ #{active_desc.ljust(avail_w)}", { color: "cyan", level: 1 })
|
|
951
|
+
rendered_lines << "#{margin_left}#{v_left} #{desc_text} #{v_right}"
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
bottom_border_line = box_border[:bl] + bot_fill + box_border[:br]
|
|
955
|
+
rendered_lines << "#{margin_left}#{colorize(bottom_border_line, border_color_cfg)}"
|
|
255
956
|
else
|
|
256
957
|
symbol_char = STYLES[@style] || "#"
|
|
257
958
|
solid_border = colorize(symbol_char, border_color_cfg)
|
|
258
959
|
solid_line = symbol_char * total_width
|
|
259
960
|
|
|
260
|
-
rendered_lines << colorize(solid_line, border_color_cfg)
|
|
961
|
+
rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
|
|
261
962
|
|
|
262
963
|
unless @title.empty?
|
|
263
|
-
centered_title = @title.center(total_width - 4)
|
|
264
|
-
rendered_lines << "#{solid_border} #{centered_title} #{solid_border}"
|
|
265
|
-
rendered_lines << colorize(solid_line, border_color_cfg)
|
|
964
|
+
centered_title = colorize(@title.center(total_width - 4), title_color_cfg)
|
|
965
|
+
rendered_lines << "#{margin_left}#{solid_border} #{centered_title} #{solid_border}"
|
|
966
|
+
rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
if has_more_above
|
|
970
|
+
up_indicator = colorize("▲ (+#{start_idx} arriba)".center(avail_w + 2), { color: "gray", level: 2 })
|
|
971
|
+
rendered_lines << "#{margin_left}#{solid_border} #{up_indicator} #{solid_border}"
|
|
266
972
|
end
|
|
267
973
|
|
|
268
|
-
|
|
974
|
+
visible_indices.each do |current_index|
|
|
975
|
+
option_name = all_names[current_index]
|
|
269
976
|
if @index == current_index
|
|
270
|
-
highlighted_text = colorize(option_name.ljust(
|
|
271
|
-
rendered_lines << "#{solid_border} #{highlighted_text} #{solid_border}"
|
|
977
|
+
highlighted_text = colorize("> #{option_name.ljust(avail_w)}", focus_color_cfg)
|
|
978
|
+
rendered_lines << "#{margin_left}#{solid_border} #{highlighted_text} #{solid_border}"
|
|
272
979
|
else
|
|
273
|
-
normal_text = colorize(option_name.ljust(
|
|
274
|
-
rendered_lines << "#{solid_border} #{normal_text} #{solid_border}"
|
|
980
|
+
normal_text = colorize(" #{option_name.ljust(avail_w)}", options_color_cfg)
|
|
981
|
+
rendered_lines << "#{margin_left}#{solid_border} #{normal_text} #{solid_border}"
|
|
275
982
|
end
|
|
276
983
|
end
|
|
277
984
|
|
|
278
|
-
|
|
985
|
+
if has_more_below
|
|
986
|
+
remaining_below = total_items - 1 - end_idx
|
|
987
|
+
down_indicator = colorize("▼ (+#{remaining_below} abajo)".center(avail_w + 2), { color: "gray", level: 2 })
|
|
988
|
+
rendered_lines << "#{margin_left}#{solid_border} #{down_indicator} #{solid_border}"
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
unless active_desc.empty?
|
|
992
|
+
rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
|
|
993
|
+
desc_text = colorize("ℹ #{active_desc.ljust(avail_w)}", { color: "cyan", level: 1 })
|
|
994
|
+
rendered_lines << "#{margin_left}#{solid_border} #{desc_text} #{solid_border}"
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
rendered_lines << "#{margin_left}#{colorize(solid_line, border_color_cfg)}"
|
|
279
998
|
end
|
|
280
999
|
|
|
281
1000
|
rendered_lines
|
|
@@ -317,8 +1036,9 @@ class GRmenu
|
|
|
317
1036
|
def draw_frame(target_width)
|
|
318
1037
|
lines = render_lines(target_width)
|
|
319
1038
|
buffer = String.new(CURSOR_HOME)
|
|
320
|
-
lines.
|
|
321
|
-
buffer << line << CLEAR_TO_EOL
|
|
1039
|
+
lines.each_with_index do |line, idx|
|
|
1040
|
+
buffer << line << CLEAR_TO_EOL
|
|
1041
|
+
buffer << "\r\n" if idx < lines.length - 1
|
|
322
1042
|
end
|
|
323
1043
|
buffer << CLEAR_TO_EOS
|
|
324
1044
|
Kernel.print(buffer)
|
|
@@ -330,10 +1050,10 @@ class GRmenu
|
|
|
330
1050
|
while (key = read_single_key(input_stream))
|
|
331
1051
|
break if key == "q" || key == "Q" || key == "\x03" || key == "\x04"
|
|
332
1052
|
|
|
333
|
-
if key == "\e[A" || key == "\eOA"
|
|
1053
|
+
if key == "\e[A" || key == "\eOA" || key == "\xe0H" || key == "\x00H"
|
|
334
1054
|
move_up
|
|
335
1055
|
draw_frame(target_width)
|
|
336
|
-
elsif key == "\e[B" || key == "\eOB"
|
|
1056
|
+
elsif key == "\e[B" || key == "\eOB" || key == "\xe0P" || key == "\x00P"
|
|
337
1057
|
move_down
|
|
338
1058
|
draw_frame(target_width)
|
|
339
1059
|
elsif key == "\r" || key == "\n"
|
|
@@ -363,6 +1083,14 @@ class GRmenu
|
|
|
363
1083
|
first_char << extra_chars
|
|
364
1084
|
rescue IO::WaitReadable, IO::EAGAINWaitReadable, EOFError
|
|
365
1085
|
end
|
|
1086
|
+
elsif first_char == "\x00" || first_char == "\xe0"
|
|
1087
|
+
begin
|
|
1088
|
+
second_char = input_stream.read_nonblock(1)
|
|
1089
|
+
first_char << second_char
|
|
1090
|
+
rescue IO::WaitReadable, IO::EAGAINWaitReadable, EOFError
|
|
1091
|
+
second_char = input_stream.getch rescue nil
|
|
1092
|
+
first_char << second_char if second_char
|
|
1093
|
+
end
|
|
366
1094
|
end
|
|
367
1095
|
|
|
368
1096
|
first_char
|
|
@@ -370,31 +1098,48 @@ class GRmenu
|
|
|
370
1098
|
nil
|
|
371
1099
|
end
|
|
372
1100
|
|
|
1101
|
+
def format_auto_name(raw_name)
|
|
1102
|
+
cleaned = raw_name.to_s.gsub(/[_-]+/, ' ').strip
|
|
1103
|
+
cleaned.split(' ').map(&:capitalize).join(' ')
|
|
1104
|
+
end
|
|
1105
|
+
|
|
373
1106
|
def extract_name_from_action(action)
|
|
374
1107
|
case action
|
|
375
|
-
when Method
|
|
376
|
-
action.name.to_s
|
|
377
|
-
when Symbol
|
|
378
|
-
action.to_s
|
|
379
1108
|
when Array
|
|
380
1109
|
action[0].to_s
|
|
1110
|
+
when Hash
|
|
1111
|
+
(action[:name] || action[:title] || action["name"] || action["title"] || "Opcion").to_s
|
|
1112
|
+
when Method
|
|
1113
|
+
format_auto_name(action.name)
|
|
1114
|
+
when Symbol
|
|
1115
|
+
format_auto_name(action)
|
|
381
1116
|
when Proc
|
|
382
1117
|
if action.respond_to?(:name) && action.name
|
|
383
|
-
action.name
|
|
1118
|
+
format_auto_name(action.name)
|
|
384
1119
|
else
|
|
385
|
-
"
|
|
1120
|
+
"Opcion"
|
|
386
1121
|
end
|
|
387
1122
|
else
|
|
388
1123
|
if action.respond_to?(:name)
|
|
389
|
-
action.name
|
|
1124
|
+
format_auto_name(action.name)
|
|
390
1125
|
elsif action.respond_to?(:title)
|
|
391
1126
|
action.title.to_s
|
|
392
1127
|
else
|
|
393
|
-
action
|
|
1128
|
+
format_auto_name(action)
|
|
394
1129
|
end
|
|
395
1130
|
end
|
|
396
1131
|
end
|
|
397
1132
|
|
|
1133
|
+
def extract_description_from_action(action)
|
|
1134
|
+
if action.is_a?(Array) && action.length >= 3
|
|
1135
|
+
action[2].to_s
|
|
1136
|
+
elsif action.is_a?(Hash)
|
|
1137
|
+
(action[:desc] || action[:description] || action["desc"] || action["description"]).to_s
|
|
1138
|
+
else
|
|
1139
|
+
""
|
|
1140
|
+
end
|
|
1141
|
+
end
|
|
1142
|
+
|
|
398
1143
|
def execute_action(action)
|
|
399
1144
|
case action
|
|
400
1145
|
when Method, Proc
|
|
@@ -407,11 +1152,33 @@ class GRmenu
|
|
|
407
1152
|
end
|
|
408
1153
|
when Array
|
|
409
1154
|
callable = action[1]
|
|
410
|
-
|
|
1155
|
+
if callable.is_a?(Symbol)
|
|
1156
|
+
if Object.respond_to?(callable, true)
|
|
1157
|
+
Object.send(callable)
|
|
1158
|
+
elsif Kernel.respond_to?(callable, true)
|
|
1159
|
+
Kernel.send(callable)
|
|
1160
|
+
end
|
|
1161
|
+
elsif callable.respond_to?(:call)
|
|
1162
|
+
callable.call
|
|
1163
|
+
end
|
|
1164
|
+
when Hash
|
|
1165
|
+
callable = action[:action] || action[:call] || action["action"] || action["call"]
|
|
1166
|
+
if callable.is_a?(Symbol)
|
|
1167
|
+
if Object.respond_to?(callable, true)
|
|
1168
|
+
Object.send(callable)
|
|
1169
|
+
elsif Kernel.respond_to?(callable, true)
|
|
1170
|
+
Kernel.send(callable)
|
|
1171
|
+
end
|
|
1172
|
+
elsif callable.respond_to?(:call)
|
|
1173
|
+
callable.call
|
|
1174
|
+
end
|
|
411
1175
|
else
|
|
412
1176
|
action.call if action.respond_to?(:call)
|
|
413
1177
|
end
|
|
414
1178
|
end
|
|
415
1179
|
end
|
|
416
1180
|
|
|
1181
|
+
Color = GRmenu::Color unless defined?(Color)
|
|
1182
|
+
Colors = GRmenu::Color unless defined?(Colors)
|
|
1183
|
+
C = GRmenu::Color unless defined?(C)
|
|
417
1184
|
Grmenu = GRmenu unless defined?(Grmenu)
|