grmenu 0.1.4
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 +7 -0
- data/GRmenu.rb +417 -0
- data/LICENSE +21 -0
- data/README.md +239 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: efb3daa87667ff2d6ba92320620cf4ad02f320eb519fa040318d24bc8e820159
|
|
4
|
+
data.tar.gz: 2824b0ae2fd4ba2137fff7c4d23552f76468b1d4cccca9daa5c6fe6b51110e47
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 064b4bb162edfdf14a0b5225515f780a8affba8bdaf3f2fc73f0fa7b51abfab71fccf01340dc5f9bda4f19bb6c9e21dbbb9ef380de83aac27e1ff0c1c866c62c
|
|
7
|
+
data.tar.gz: 9d2e29db0d8aad09e3846f07cabfbf3b366a891596ad27a20705487f3701be1c8cd9376811d8dbae366b23e1714fe35926bd4b9c8b562e9d787caf7df01a00ab
|
data/GRmenu.rb
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
class GRmenu
|
|
6
|
+
CLEAR_SCREEN_SEQUENCE = "\e[H\e[2J\e[3J"
|
|
7
|
+
HIDE_CURSOR = "\e[?25l"
|
|
8
|
+
SHOW_CURSOR = "\e[?25h"
|
|
9
|
+
CURSOR_HOME = "\e[H"
|
|
10
|
+
CLEAR_TO_EOL = "\e[K"
|
|
11
|
+
CLEAR_TO_EOS = "\e[J"
|
|
12
|
+
|
|
13
|
+
STYLES = {
|
|
14
|
+
1 => "#", 2 => "┌", 3 => "╔", 4 => "┏", 5 => "╒",
|
|
15
|
+
6 => "╓", 7 => "╭", 8 => "▛", 9 => "▓", 10 => "▒",
|
|
16
|
+
11 => "░", 12 => "█", 13 => "*", 14 => "+", 15 => "=",
|
|
17
|
+
16 => "~", 17 => "-", 18 => "◆", 19 => "●", 20 => "★"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
COLORS = {
|
|
21
|
+
"black" => { 1 => "\e[30m", 2 => "\e[90m" },
|
|
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
|
+
"cyan" => { 1 => "\e[36m", 2 => "\e[96m" },
|
|
28
|
+
"white" => { 1 => "\e[37m", 2 => "\e[97m" },
|
|
29
|
+
"reset" => "\e[0m"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
BORDERS = {
|
|
33
|
+
1 => { h: "=-", v: "|", tl: "#", tr: "#", bl: "#", br: "#" },
|
|
34
|
+
2 => { h: "─", v: "│", tl: "┌", tr: "┐", bl: "└", br: "┘" },
|
|
35
|
+
3 => { h: "═", v: "║", tl: "╔", tr: "╗", bl: "╚", br: "╝" },
|
|
36
|
+
4 => { h: "━", v: "┃", tl: "┏", tr: "┓", bl: "┗", br: "┛" },
|
|
37
|
+
5 => { h: "═", v: "│", tl: "╒", tr: "╕", bl: "╘", br: "╛" },
|
|
38
|
+
6 => { h: "─", v: "║", tl: "╓", tr: "╖", bl: "╙", br: "╜" },
|
|
39
|
+
7 => { h: "─", v: "│", tl: "╭", tr: "╮", bl: "╰", br: "╯" },
|
|
40
|
+
8 => { h: "▀", v: "▌", tl: "▛", tr: "▜", bl: "▙", br: "▟" },
|
|
41
|
+
19 => { h: "●○", v: "●", tl: "●", tr: "●", bl: "●", br: "●" },
|
|
42
|
+
20 => { h: "★☆", v: "★", tl: "★", tr: "★", bl: "★", br: "★" }
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
class SetStyle
|
|
46
|
+
def initialize(
|
|
47
|
+
border: { color: "cyan", level: 1 },
|
|
48
|
+
options: { color: "white", level: 1 },
|
|
49
|
+
focus: { color: "green", level: 2 }
|
|
50
|
+
)
|
|
51
|
+
@border = border.dup
|
|
52
|
+
@options = options.dup
|
|
53
|
+
@focus = focus.dup
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def border(color_name = nil, brightness_level = 1)
|
|
57
|
+
return @border if color_name.nil?
|
|
58
|
+
if color_name.is_a?(Hash)
|
|
59
|
+
@border = color_name
|
|
60
|
+
else
|
|
61
|
+
@border = { color: color_name.to_s, level: brightness_level.to_i }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
alias_method :Border, :border
|
|
65
|
+
alias_method :set_border, :border
|
|
66
|
+
alias_method :border=, :border
|
|
67
|
+
|
|
68
|
+
def options(color_name = nil, brightness_level = 1)
|
|
69
|
+
return @options if color_name.nil?
|
|
70
|
+
if color_name.is_a?(Hash)
|
|
71
|
+
@options = color_name
|
|
72
|
+
else
|
|
73
|
+
@options = { color: color_name.to_s, level: brightness_level.to_i }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
alias_method :Options, :options
|
|
77
|
+
alias_method :set_options, :options
|
|
78
|
+
alias_method :options=, :options
|
|
79
|
+
|
|
80
|
+
def focus(color_name = nil, brightness_level = 2)
|
|
81
|
+
return @focus if color_name.nil?
|
|
82
|
+
if color_name.is_a?(Hash)
|
|
83
|
+
@focus = color_name
|
|
84
|
+
else
|
|
85
|
+
@focus = { color: color_name.to_s, level: brightness_level.to_i }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
alias_method :Focus, :focus
|
|
89
|
+
alias_method :set_focus, :focus
|
|
90
|
+
alias_method :focus=, :focus
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
def border(color_name = nil, brightness_level = 1)
|
|
94
|
+
@default_border ||= { color: "cyan", level: 1 }
|
|
95
|
+
return @default_border if color_name.nil?
|
|
96
|
+
if color_name.is_a?(Hash)
|
|
97
|
+
@default_border = color_name
|
|
98
|
+
else
|
|
99
|
+
@default_border = { color: color_name.to_s, level: brightness_level.to_i }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
alias_method :Border, :border
|
|
103
|
+
alias_method :border=, :border
|
|
104
|
+
|
|
105
|
+
def options(color_name = nil, brightness_level = 1)
|
|
106
|
+
@default_options ||= { color: "white", level: 1 }
|
|
107
|
+
return @default_options if color_name.nil?
|
|
108
|
+
if color_name.is_a?(Hash)
|
|
109
|
+
@default_options = color_name
|
|
110
|
+
else
|
|
111
|
+
@default_options = { color: color_name.to_s, level: brightness_level.to_i }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
alias_method :Options, :options
|
|
115
|
+
alias_method :options=, :options
|
|
116
|
+
|
|
117
|
+
def focus(color_name = nil, brightness_level = 2)
|
|
118
|
+
@default_focus ||= { color: "green", level: 2 }
|
|
119
|
+
return @default_focus if color_name.nil?
|
|
120
|
+
if color_name.is_a?(Hash)
|
|
121
|
+
@default_focus = color_name
|
|
122
|
+
else
|
|
123
|
+
@default_focus = { color: color_name.to_s, level: brightness_level.to_i }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
alias_method :Focus, :focus
|
|
127
|
+
alias_method :focus=, :focus
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
module GRprint
|
|
132
|
+
module_function
|
|
133
|
+
|
|
134
|
+
def p(text = "", ending = "\r\n")
|
|
135
|
+
Kernel.print("#{text}#{ending}")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
attr_accessor :functions, :title, :style, :index, :style_config
|
|
140
|
+
|
|
141
|
+
alias_method :options, :functions
|
|
142
|
+
alias_method :options=, :functions=
|
|
143
|
+
alias_method :selected_index, :index
|
|
144
|
+
alias_method :selected_index=, :index=
|
|
145
|
+
alias_method :SetStyle, :style_config
|
|
146
|
+
alias_method :set_style, :style_config
|
|
147
|
+
|
|
148
|
+
def self.STYLES
|
|
149
|
+
STYLES
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.COLORS
|
|
153
|
+
COLORS
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.BORDERS
|
|
157
|
+
BORDERS
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def initialize(functions, *positional_arguments, title: nil, style: nil, **keyword_arguments)
|
|
161
|
+
@functions = functions.is_a?(Array) ? functions : Array(functions)
|
|
162
|
+
|
|
163
|
+
pos_title = positional_arguments[0]
|
|
164
|
+
pos_style = positional_arguments[1]
|
|
165
|
+
|
|
166
|
+
@title = (title || pos_title || keyword_arguments[:title] || "").to_s
|
|
167
|
+
@style = (style || pos_style || keyword_arguments[:style] || 19).to_i
|
|
168
|
+
@index = 0
|
|
169
|
+
@clear_seq = CLEAR_SCREEN_SEQUENCE
|
|
170
|
+
|
|
171
|
+
@style_config = SetStyle.new(
|
|
172
|
+
border: SetStyle.border.dup,
|
|
173
|
+
options: SetStyle.options.dup,
|
|
174
|
+
focus: SetStyle.focus.dup
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def move_up
|
|
179
|
+
return @index if @functions.empty?
|
|
180
|
+
@index = (@index - 1) % @functions.length
|
|
181
|
+
end
|
|
182
|
+
alias_method :_up, :move_up
|
|
183
|
+
|
|
184
|
+
def move_down
|
|
185
|
+
return @index if @functions.empty?
|
|
186
|
+
@index = (@index + 1) % @functions.length
|
|
187
|
+
end
|
|
188
|
+
alias_method :_down, :move_down
|
|
189
|
+
|
|
190
|
+
def colorize(text, color_config)
|
|
191
|
+
return text.to_s if color_config.nil? || color_config.empty?
|
|
192
|
+
|
|
193
|
+
color_name = (color_config[:color] || color_config["color"]).to_s.downcase
|
|
194
|
+
brightness_level = (color_config[:level] || color_config["level"] || 1).to_i
|
|
195
|
+
|
|
196
|
+
color_code = COLORS.dig(color_name, brightness_level)
|
|
197
|
+
return text.to_s unless color_code
|
|
198
|
+
|
|
199
|
+
"#{color_code}#{text}#{COLORS['reset']}"
|
|
200
|
+
end
|
|
201
|
+
alias_method :_colorize, :colorize
|
|
202
|
+
|
|
203
|
+
def build_horizontal_line(pattern, target_width)
|
|
204
|
+
return "" if target_width <= 0 || pattern.nil? || pattern.empty?
|
|
205
|
+
|
|
206
|
+
pattern_length = pattern.length
|
|
207
|
+
repetitions_needed = (target_width.to_f / pattern_length).ceil + 1
|
|
208
|
+
(pattern * repetitions_needed)[0...target_width]
|
|
209
|
+
end
|
|
210
|
+
alias_method :_hline, :build_horizontal_line
|
|
211
|
+
|
|
212
|
+
def menu
|
|
213
|
+
Kernel.print("Press any key to start ...\r\n")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def render_lines(size_max = 20)
|
|
217
|
+
option_names = @functions.map { |func| extract_name_from_action(func) }
|
|
218
|
+
|
|
219
|
+
total_width = [([size_max] + option_names.map { |name| name.length + 4 }).max, @title.length + 4].max unless @title.empty?
|
|
220
|
+
|
|
221
|
+
border_color_cfg = @style_config.border
|
|
222
|
+
options_color_cfg = @style_config.options
|
|
223
|
+
focus_color_cfg = @style_config.focus
|
|
224
|
+
|
|
225
|
+
box_border = BORDERS[@style]
|
|
226
|
+
rendered_lines = []
|
|
227
|
+
|
|
228
|
+
if box_border
|
|
229
|
+
horizontal_fill = build_horizontal_line(box_border[:h], total_width - 2)
|
|
230
|
+
vertical_char = colorize(box_border[:v], border_color_cfg)
|
|
231
|
+
|
|
232
|
+
top_border_line = box_border[:tl] + horizontal_fill + box_border[:tr]
|
|
233
|
+
rendered_lines << colorize(top_border_line, border_color_cfg)
|
|
234
|
+
|
|
235
|
+
unless @title.empty?
|
|
236
|
+
centered_title = @title.center(total_width - 4)
|
|
237
|
+
rendered_lines << "#{vertical_char} #{centered_title} #{vertical_char}"
|
|
238
|
+
|
|
239
|
+
separator_line = box_border[:v] + horizontal_fill + box_border[:v]
|
|
240
|
+
rendered_lines << colorize(separator_line, border_color_cfg)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
option_names.each_with_index do |option_name, current_index|
|
|
244
|
+
if @index == current_index
|
|
245
|
+
highlighted_text = colorize(">#{option_name.ljust(total_width - 6)}", focus_color_cfg)
|
|
246
|
+
rendered_lines << "#{vertical_char} #{highlighted_text} #{vertical_char}"
|
|
247
|
+
else
|
|
248
|
+
normal_text = colorize("> #{option_name.ljust(total_width - 6)}", options_color_cfg)
|
|
249
|
+
rendered_lines << "#{vertical_char} #{normal_text} #{vertical_char}"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
bottom_border_line = box_border[:bl] + horizontal_fill + box_border[:br]
|
|
254
|
+
rendered_lines << colorize(bottom_border_line, border_color_cfg)
|
|
255
|
+
else
|
|
256
|
+
symbol_char = STYLES[@style] || "#"
|
|
257
|
+
solid_border = colorize(symbol_char, border_color_cfg)
|
|
258
|
+
solid_line = symbol_char * total_width
|
|
259
|
+
|
|
260
|
+
rendered_lines << colorize(solid_line, border_color_cfg)
|
|
261
|
+
|
|
262
|
+
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)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
option_names.each_with_index do |option_name, current_index|
|
|
269
|
+
if @index == current_index
|
|
270
|
+
highlighted_text = colorize(option_name.ljust(total_width - 4), focus_color_cfg)
|
|
271
|
+
rendered_lines << "#{solid_border} #{highlighted_text} #{solid_border}"
|
|
272
|
+
else
|
|
273
|
+
normal_text = colorize(option_name.ljust(total_width - 4), options_color_cfg)
|
|
274
|
+
rendered_lines << "#{solid_border} #{normal_text} #{solid_border}"
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
rendered_lines << colorize(solid_line, border_color_cfg)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
rendered_lines
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def draw(size_max: 20, min_width: nil)
|
|
285
|
+
target_width = min_width || size_max || 20
|
|
286
|
+
action_to_execute = nil
|
|
287
|
+
|
|
288
|
+
is_tty = $stdin.respond_to?(:tty?) && $stdin.tty?
|
|
289
|
+
|
|
290
|
+
begin
|
|
291
|
+
Kernel.print("#{HIDE_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
|
|
292
|
+
|
|
293
|
+
if is_tty
|
|
294
|
+
$stdin.raw do |raw_input_stream|
|
|
295
|
+
action_to_execute = run_interactive_loop(raw_input_stream, target_width)
|
|
296
|
+
end
|
|
297
|
+
else
|
|
298
|
+
action_to_execute = run_interactive_loop($stdin, target_width)
|
|
299
|
+
end
|
|
300
|
+
ensure
|
|
301
|
+
Kernel.print(SHOW_CURSOR)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
if action_to_execute
|
|
305
|
+
Kernel.print(CLEAR_SCREEN_SEQUENCE)
|
|
306
|
+
execute_action(action_to_execute)
|
|
307
|
+
else
|
|
308
|
+
Kernel.print(CLEAR_SCREEN_SEQUENCE)
|
|
309
|
+
end
|
|
310
|
+
rescue Interrupt
|
|
311
|
+
Kernel.print("#{SHOW_CURSOR}#{CLEAR_SCREEN_SEQUENCE}")
|
|
312
|
+
nil
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
private
|
|
316
|
+
|
|
317
|
+
def draw_frame(target_width)
|
|
318
|
+
lines = render_lines(target_width)
|
|
319
|
+
buffer = String.new(CURSOR_HOME)
|
|
320
|
+
lines.each do |line|
|
|
321
|
+
buffer << line << CLEAR_TO_EOL << "\r\n"
|
|
322
|
+
end
|
|
323
|
+
buffer << CLEAR_TO_EOS
|
|
324
|
+
Kernel.print(buffer)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def run_interactive_loop(input_stream, target_width)
|
|
328
|
+
draw_frame(target_width)
|
|
329
|
+
|
|
330
|
+
while (key = read_single_key(input_stream))
|
|
331
|
+
break if key == "q" || key == "Q" || key == "\x03" || key == "\x04"
|
|
332
|
+
|
|
333
|
+
if key == "\e[A" || key == "\eOA"
|
|
334
|
+
move_up
|
|
335
|
+
draw_frame(target_width)
|
|
336
|
+
elsif key == "\e[B" || key == "\eOB"
|
|
337
|
+
move_down
|
|
338
|
+
draw_frame(target_width)
|
|
339
|
+
elsif key == "\r" || key == "\n"
|
|
340
|
+
return @functions[@index]
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
nil
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def read_single_key(input_stream)
|
|
348
|
+
unless input_stream.respond_to?(:tty?) && input_stream.tty?
|
|
349
|
+
begin
|
|
350
|
+
return input_stream.sysread(3) if input_stream.respond_to?(:sysread)
|
|
351
|
+
return input_stream.read(1)
|
|
352
|
+
rescue EOFError, Errno::EPIPE
|
|
353
|
+
return nil
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
first_char = input_stream.getch
|
|
358
|
+
return nil if first_char.nil?
|
|
359
|
+
|
|
360
|
+
if first_char == "\e"
|
|
361
|
+
begin
|
|
362
|
+
extra_chars = input_stream.read_nonblock(2)
|
|
363
|
+
first_char << extra_chars
|
|
364
|
+
rescue IO::WaitReadable, IO::EAGAINWaitReadable, EOFError
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
first_char
|
|
369
|
+
rescue EOFError, Errno::EPIPE, Errno::ENOTTY
|
|
370
|
+
nil
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def extract_name_from_action(action)
|
|
374
|
+
case action
|
|
375
|
+
when Method
|
|
376
|
+
action.name.to_s
|
|
377
|
+
when Symbol
|
|
378
|
+
action.to_s
|
|
379
|
+
when Array
|
|
380
|
+
action[0].to_s
|
|
381
|
+
when Proc
|
|
382
|
+
if action.respond_to?(:name) && action.name
|
|
383
|
+
action.name.to_s
|
|
384
|
+
else
|
|
385
|
+
"opcion"
|
|
386
|
+
end
|
|
387
|
+
else
|
|
388
|
+
if action.respond_to?(:name)
|
|
389
|
+
action.name.to_s
|
|
390
|
+
elsif action.respond_to?(:title)
|
|
391
|
+
action.title.to_s
|
|
392
|
+
else
|
|
393
|
+
action.to_s
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def execute_action(action)
|
|
399
|
+
case action
|
|
400
|
+
when Method, Proc
|
|
401
|
+
action.call
|
|
402
|
+
when Symbol
|
|
403
|
+
if Object.respond_to?(action, true)
|
|
404
|
+
Object.send(action)
|
|
405
|
+
elsif Kernel.respond_to?(action, true)
|
|
406
|
+
Kernel.send(action)
|
|
407
|
+
end
|
|
408
|
+
when Array
|
|
409
|
+
callable = action[1]
|
|
410
|
+
callable.call if callable.respond_to?(:call)
|
|
411
|
+
else
|
|
412
|
+
action.call if action.respond_to?(:call)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
Grmenu = GRmenu unless defined?(Grmenu)
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 grcode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# GRmenu
|
|
4
|
+
|
|
5
|
+
**Menús de navegación por teclado para terminal, en modo TTY crudo**
|
|
6
|
+
|
|
7
|
+
Flechas arriba/abajo para moverte · `Enter` para elegir · `q` para salir
|
|
8
|
+
|
|
9
|
+
[](https://badge.fury.io/rb/grmenu)
|
|
10
|
+
[](https://pypi.org/project/grmenu/)
|
|
11
|
+
[](https://www.ruby-lang.org)
|
|
12
|
+
[](https://pypi.org/project/grmenu/)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
[](https://github.com/JoseEduardoGR/GRmenu/commits/main)
|
|
15
|
+
[](https://github.com/JoseEduardoGR/GRmenu/stargazers)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ✨ Características
|
|
22
|
+
|
|
23
|
+
- 🎮 **Navegación con flechas** — arriba/abajo para moverte, `Enter` para ejecutar, `q` para salir
|
|
24
|
+
- 🎨 **20 estilos de borde** — desde ASCII clásico hasta caracteres Unicode y bloques
|
|
25
|
+
- 🌈 **Colores personalizables** — borde, opciones y foco por separado, con 8 colores en 2 tonos cada uno
|
|
26
|
+
- 📦 **Cero dependencias externas** — solo la librería estándar (`io/console` en Ruby, `termios`/`tty` en Python)
|
|
27
|
+
- 💎 **Ruby & Python** — implementaciones disponibles tanto en RubyGems (`grmenu`) como en PyPI (`grmenu`)
|
|
28
|
+
- 🐧 **Linux / macOS** — funciona en cualquier terminal POSIX
|
|
29
|
+
|
|
30
|
+
> ⚠️ Requiere una terminal real (TTY) en Linux o macOS. En Ruby utiliza `io/console` en modo crudo (`raw`) y en Python `termios`/`tty` para capturar pulsaciones de teclas en tiempo real.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📦 Instalación
|
|
35
|
+
|
|
36
|
+
### Ruby (RubyGems)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
gem install grmenu
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
O en tu `Gemfile`:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
gem 'grmenu'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Python (PyPI)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install grmenu
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🚀 Uso rápido (Ruby)
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
require 'GRmenu'
|
|
60
|
+
|
|
61
|
+
def opcion_uno
|
|
62
|
+
puts "elegiste uno"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def opcion_dos
|
|
66
|
+
puts "elegiste dos"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
menu = GRmenu.new(
|
|
70
|
+
[method(:opcion_uno), method(:opcion_dos)],
|
|
71
|
+
title: "Mi menu",
|
|
72
|
+
style: 19
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
menu.set_style.border("yellow")
|
|
76
|
+
menu.set_style.options("green")
|
|
77
|
+
menu.draw
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Cada elemento de la lista puede ser un `Method` (`method(:mi_metodo)`), un `Proc` / `lambda` (`-> { ... }`), un arreglo `["Nombre", callable]` o un `Symbol` (`:mi_metodo`). El nombre se toma automáticamente de la opción y al presionar `Enter` sobre ella, esa acción se ejecuta.
|
|
81
|
+
|
|
82
|
+
<details>
|
|
83
|
+
<summary>🐍 <b>Ver ejemplo en Python</b></summary>
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from GRmenu import GRmenu
|
|
87
|
+
|
|
88
|
+
def opcion_uno():
|
|
89
|
+
print("elegiste uno")
|
|
90
|
+
|
|
91
|
+
def opcion_dos():
|
|
92
|
+
print("elegiste dos")
|
|
93
|
+
|
|
94
|
+
menu = GRmenu([opcion_uno, opcion_dos], title="Mi menu", style=19)
|
|
95
|
+
menu.SetStyle.Border("yellow")
|
|
96
|
+
menu.SetStyle.Options("green")
|
|
97
|
+
menu.draw()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
</details>
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 🕹️ Controles
|
|
105
|
+
|
|
106
|
+
| Tecla | Acción |
|
|
107
|
+
|-------------|---------------------------|
|
|
108
|
+
| `↑` | Mover selección arriba |
|
|
109
|
+
| `↓` | Mover selección abajo |
|
|
110
|
+
| `Enter` | Ejecutar opción seleccionada |
|
|
111
|
+
| `q` | Salir del menú |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🎨 Personalización
|
|
116
|
+
|
|
117
|
+
### Colores (`set_style` / `SetStyle`)
|
|
118
|
+
|
|
119
|
+
Cada zona del menú se personaliza por separado. `level: 1` es el tono normal y `level: 2` el brillante.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# En Ruby:
|
|
123
|
+
menu.set_style.border("cyan") # color del borde
|
|
124
|
+
menu.set_style.options("white") # color de las opciones no seleccionadas
|
|
125
|
+
menu.set_style.focus("green", 2) # color de la opción resaltada (brillante)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Colores disponibles: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`.
|
|
129
|
+
|
|
130
|
+
### Estilos de borde (`style`)
|
|
131
|
+
|
|
132
|
+
El parámetro `style` acepta un número (1 al 20) que define cómo se dibuja el marco del menú:
|
|
133
|
+
|
|
134
|
+
| `style` | Vista previa | `style` | Vista previa |
|
|
135
|
+
|:---:|:---|:---:|:---|
|
|
136
|
+
| 1 | `#===#` | 11 | `░░░░░` |
|
|
137
|
+
| 2 | `┌───┐` | 12 | `█████` |
|
|
138
|
+
| 3 | `╔═══╗` | 13 | `*****` |
|
|
139
|
+
| 4 | `┏━━━┓` | 14 | `+++++` |
|
|
140
|
+
| 5 | `╒═══╕` | 15 | `=====` |
|
|
141
|
+
| 6 | `╓───╖` | 16 | `~~~~~` |
|
|
142
|
+
| 7 | `╭───╮` | 17 | `-----` |
|
|
143
|
+
| 8 | `▛▀▀▀▜` | 18 | `◆◆◆◆◆` |
|
|
144
|
+
| 9 | `▓▓▓▓▓` | 19 | `●●●●●` *(default)* |
|
|
145
|
+
| 10 | `▒▒▒▒▒` | 20 | `★★★★★` |
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
menu = GRmenu.new([method(:opcion_uno), method(:opcion_dos)], title: "Mi menu", style: 7) # bordes redondeados
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Ancho del menú
|
|
152
|
+
|
|
153
|
+
`draw()` acepta `size_max` (o `min_width`), el ancho mínimo en caracteres del cuadro (se expande automáticamente si el título o las opciones son más largos):
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
menu.draw(size_max: 30)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 📖 Referencia de la API (Ruby)
|
|
162
|
+
|
|
163
|
+
### `GRmenu.new(functions, title: "", style: 19)`
|
|
164
|
+
|
|
165
|
+
| Parámetro | Tipo | Descripción |
|
|
166
|
+
|-------------|---------------------------------------|---------------------------------------------------|
|
|
167
|
+
| `functions` | `Array<Method, Proc, Symbol, Array>` | Métodos, procs o lambdas a mostrar como opciones. |
|
|
168
|
+
| `title` | `String` (opcional) | Título mostrado en la cabecera del menú. |
|
|
169
|
+
| `style` | `Integer` (opcional) | Número de estilo de borde (ver tabla arriba). |
|
|
170
|
+
|
|
171
|
+
### `menu.draw(size_max: 20)`
|
|
172
|
+
|
|
173
|
+
Dibuja el menú y bloquea el hilo hasta que el usuario elige una opción (`Enter`) o sale (`q`).
|
|
174
|
+
|
|
175
|
+
### `menu.set_style`
|
|
176
|
+
|
|
177
|
+
| Método | Descripción |
|
|
178
|
+
|-------------------------------------|-------------------------------------------|
|
|
179
|
+
| `set_style.border(color, level=1)` | Color del marco del menú. |
|
|
180
|
+
| `set_style.options(color, level=1)` | Color de las opciones sin seleccionar. |
|
|
181
|
+
| `set_style.focus(color, level=2)` | Color de la opción resaltada. |
|
|
182
|
+
|
|
183
|
+
*(Nota: también se soportan los métodos con alias `menu.SetStyle.Border(...)`, `menu.SetStyle.Options(...)`, `menu.SetStyle.Focus(...)` para compatibilidad de interfaz con la versión de Python).*
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 🧩 Ejemplo completo (Ruby)
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
require 'GRmenu'
|
|
191
|
+
|
|
192
|
+
def saludar
|
|
193
|
+
puts "¡Hola desde Ruby!"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def salir_app
|
|
197
|
+
puts "Hasta luego 👋"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def acerca_de
|
|
201
|
+
puts "GRmenu v0.1.4 — menú TTY para terminal"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
menu = GRmenu.new(
|
|
205
|
+
[
|
|
206
|
+
method(:saludar),
|
|
207
|
+
method(:acerca_de),
|
|
208
|
+
["Personalizado", -> { puts "Opción con bloque lambda" }],
|
|
209
|
+
method(:salir_app)
|
|
210
|
+
],
|
|
211
|
+
title: "GRmenu Demo",
|
|
212
|
+
style: 7
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
menu.set_style.border("magenta")
|
|
216
|
+
menu.set_style.options("white")
|
|
217
|
+
menu.set_style.focus("cyan", 2)
|
|
218
|
+
menu.draw(size_max: 28)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🤝 Contribuir
|
|
224
|
+
|
|
225
|
+
Las contribuciones son bienvenidas. Podés abrir un [issue](https://github.com/JoseEduardoGR/GRmenu/issues) o enviar un pull request.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 📄 Licencia
|
|
230
|
+
|
|
231
|
+
Distribuido bajo licencia [MIT](LICENSE).
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
<div align="center">
|
|
236
|
+
|
|
237
|
+
Hecho por [grcode](https://github.com/JoseEduardoGR)
|
|
238
|
+
|
|
239
|
+
</div>
|
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: grmenu
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- grcode
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: GRmenu es una gema ligera para crear menús interactivos de navegación
|
|
13
|
+
en terminal POSIX (Linux/macOS) usando flechas arriba/abajo y Enter, sin dependencias
|
|
14
|
+
externas.
|
|
15
|
+
email:
|
|
16
|
+
- garabatoangelopolis@gmail.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- GRmenu.rb
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
homepage: https://github.com/JoseEduardoGR/GRmenu
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata:
|
|
28
|
+
source_code_uri: https://github.com/JoseEduardoGR/GRmenu
|
|
29
|
+
bug_tracker_uri: https://github.com/JoseEduardoGR/GRmenu/issues
|
|
30
|
+
changelog_uri: https://github.com/JoseEduardoGR/GRmenu/commits/main
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- "."
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 2.6.0
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.6.7
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Menu de navegacion por teclado para terminal en modo TTY crudo (flechas +
|
|
48
|
+
Enter)
|
|
49
|
+
test_files: []
|