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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/GRmenu.rb +417 -0
  3. data/LICENSE +21 -0
  4. data/README.md +239 -0
  5. 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
+ [![Gem Version](https://badge.fury.io/rb/grmenu.svg)](https://badge.fury.io/rb/grmenu)
10
+ [![PyPI version](https://img.shields.io/pypi/v/grmenu?color=blue&label=PyPI)](https://pypi.org/project/grmenu/)
11
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.6-red.svg)](https://www.ruby-lang.org)
12
+ [![Python versions](https://img.shields.io/pypi/pyversions/grmenu)](https://pypi.org/project/grmenu/)
13
+ [![License: MIT](https://img.shields.io/github/license/JoseEduardoGR/GRmenu)](LICENSE)
14
+ [![GitHub last commit](https://img.shields.io/github/last-commit/JoseEduardoGR/GRmenu)](https://github.com/JoseEduardoGR/GRmenu/commits/main)
15
+ [![GitHub stars](https://img.shields.io/github/stars/JoseEduardoGR/GRmenu?style=social)](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: []