curses_menu 0.0.1
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/examples/actions.rb +25 -0
- data/examples/automatic_key_presses.rb +49 -0
- data/examples/formatting.rb +130 -0
- data/examples/hello.rb +8 -0
- data/examples/refresh.rb +22 -0
- data/examples/scrolling.rb +9 -0
- data/examples/several_items.rb +18 -0
- data/examples/sub_menus.rb +20 -0
- data/lib/curses_menu.rb +241 -0
- data/lib/curses_menu/curses_row.rb +150 -0
- metadata +69 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c2226bebb142878eb4467c0471c3978f683746634acc51e6852f3b9298fbd3cf
|
4
|
+
data.tar.gz: 23f6626cf28f160149106907a307cdc49ce64741f62ffb6053e16ad276267223
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 31d298f6320962b6061936eff73ba7a517f10c0ec0b8b681455ae71df372b8ef5e0ee674d598fe094c76de18531a1d4a3a07b58ae2d405a6112a03b00bfd5275
|
7
|
+
data.tar.gz: 6f49adde82fac8dfd6e7f513184735b3f9e9f0024857b00604b9180bd926659a1a9ddece7ef112ad671f1921d60c236204359c8d6fe361a491c9fe393a31cd4f
|
data/examples/actions.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'curses_menu'
|
2
|
+
|
3
|
+
nbr = 0
|
4
|
+
CursesMenu.new 'Items can have several actions. Look at the footer!' do |menu|
|
5
|
+
menu.item "Current number is #{nbr} - Use a or d", actions: {
|
6
|
+
'd' => {
|
7
|
+
name: 'Increase',
|
8
|
+
execute: proc do
|
9
|
+
nbr += 1
|
10
|
+
:menu_refresh
|
11
|
+
end
|
12
|
+
},
|
13
|
+
'a' => {
|
14
|
+
name: 'Decrease',
|
15
|
+
execute: proc do
|
16
|
+
nbr -= 1
|
17
|
+
:menu_refresh
|
18
|
+
end
|
19
|
+
}
|
20
|
+
}
|
21
|
+
menu.item 'Quit' do
|
22
|
+
puts 'Quitting...'
|
23
|
+
:menu_exit
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'curses_menu'
|
2
|
+
|
3
|
+
keys = [
|
4
|
+
# Select first
|
5
|
+
CursesMenu::KEY_ENTER,
|
6
|
+
# Select second
|
7
|
+
Curses::KEY_DOWN,
|
8
|
+
'a',
|
9
|
+
'b',
|
10
|
+
# Select third (sub-menu)
|
11
|
+
Curses::KEY_DOWN,
|
12
|
+
CursesMenu::KEY_ENTER,
|
13
|
+
# Select sub-menu first
|
14
|
+
CursesMenu::KEY_ENTER,
|
15
|
+
# Exit sub-menu
|
16
|
+
CursesMenu::KEY_ESCAPE,
|
17
|
+
# Navigate a bit
|
18
|
+
Curses::KEY_NPAGE,
|
19
|
+
Curses::KEY_HOME,
|
20
|
+
# Select last
|
21
|
+
Curses::KEY_END,
|
22
|
+
CursesMenu::KEY_ENTER
|
23
|
+
]
|
24
|
+
CursesMenu.new('Menu being used automatically', key_presses: keys) do |menu|
|
25
|
+
menu.item 'Simple item' do
|
26
|
+
puts 'Selected a simple item'
|
27
|
+
end
|
28
|
+
menu.item 'Several actions on this item', actions: {
|
29
|
+
'a' => {
|
30
|
+
name: 'Action A',
|
31
|
+
execute: proc { puts 'Selected action A' }
|
32
|
+
},
|
33
|
+
'b' => {
|
34
|
+
name: 'Action B',
|
35
|
+
execute: proc { puts 'Selected action B' }
|
36
|
+
}
|
37
|
+
}
|
38
|
+
menu.item 'Sub-menu' do
|
39
|
+
CursesMenu.new('Sub-menu!', key_presses: keys) do |sub_menu|
|
40
|
+
sub_menu.item 'Simple sub-menu item' do
|
41
|
+
puts 'Selected item from sub-menu'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
menu.item 'Quit' do
|
46
|
+
puts 'Quitting...'
|
47
|
+
:menu_exit
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'curses_menu'
|
2
|
+
|
3
|
+
dynamic_row_1 = CursesMenu::CursesRow.new(
|
4
|
+
first_cell: { text: 'Select to' },
|
5
|
+
second_cell: {
|
6
|
+
text: 'change the',
|
7
|
+
color_pair: CursesMenu::COLORS_GREEN
|
8
|
+
},
|
9
|
+
third_cell: {
|
10
|
+
text: 'cells order',
|
11
|
+
color_pair: CursesMenu::COLORS_RED
|
12
|
+
}
|
13
|
+
)
|
14
|
+
dynamic_row_2 = CursesMenu::CursesRow.new(
|
15
|
+
first_cell: { text: 'Select to change' },
|
16
|
+
second_cell: {
|
17
|
+
text: 'the cells properties',
|
18
|
+
color_pair: CursesMenu::COLORS_GREEN,
|
19
|
+
fixed_size: 40
|
20
|
+
}
|
21
|
+
)
|
22
|
+
CursesMenu.new 'Extended formatting available too!' do |menu|
|
23
|
+
menu.item CursesMenu::CursesRow.new(
|
24
|
+
default_cell: {
|
25
|
+
text: 'Simple color change - GREEN!',
|
26
|
+
color_pair: CursesMenu::COLORS_GREEN
|
27
|
+
}
|
28
|
+
)
|
29
|
+
menu.item CursesMenu::CursesRow.new(
|
30
|
+
green_cell: {
|
31
|
+
text: 'Several cells ',
|
32
|
+
color_pair: CursesMenu::COLORS_GREEN
|
33
|
+
},
|
34
|
+
red_cell: {
|
35
|
+
text: 'with different ',
|
36
|
+
color_pair: CursesMenu::COLORS_RED
|
37
|
+
},
|
38
|
+
blue_cell: {
|
39
|
+
text: 'formatting',
|
40
|
+
color_pair: CursesMenu::COLORS_BLUE
|
41
|
+
}
|
42
|
+
)
|
43
|
+
menu.item CursesMenu::CursesRow.new(
|
44
|
+
default_cell: {
|
45
|
+
text: 'Use prefixes and suffixes',
|
46
|
+
begin_with: '[ ',
|
47
|
+
end_with: ' ]'
|
48
|
+
}
|
49
|
+
)
|
50
|
+
menu.item CursesMenu::CursesRow.new(
|
51
|
+
first_cell: {
|
52
|
+
text: 'This will have a fixed size!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!',
|
53
|
+
begin_with: '[ ',
|
54
|
+
end_with: ' ]',
|
55
|
+
fixed_size: 40
|
56
|
+
},
|
57
|
+
second_cell: {
|
58
|
+
text: 'And other cells will be aligned',
|
59
|
+
color_pair: CursesMenu::COLORS_GREEN
|
60
|
+
}
|
61
|
+
)
|
62
|
+
menu.item CursesMenu::CursesRow.new(
|
63
|
+
first_cell: {
|
64
|
+
text: 'Pretty nice',
|
65
|
+
fixed_size: 40
|
66
|
+
},
|
67
|
+
second_cell: {
|
68
|
+
text: 'for alignment',
|
69
|
+
color_pair: CursesMenu::COLORS_GREEN
|
70
|
+
}
|
71
|
+
)
|
72
|
+
menu.item CursesMenu::CursesRow.new(
|
73
|
+
first_cell: {
|
74
|
+
text: 'And you can justify',
|
75
|
+
justify: :right,
|
76
|
+
fixed_size: 40
|
77
|
+
},
|
78
|
+
second_cell: {
|
79
|
+
text: 'your text when size is fixed!',
|
80
|
+
justify: :left,
|
81
|
+
color_pair: CursesMenu::COLORS_GREEN
|
82
|
+
}
|
83
|
+
)
|
84
|
+
menu.item CursesMenu::CursesRow.new(
|
85
|
+
first_cell: {
|
86
|
+
text: 'You can even',
|
87
|
+
justify: :right,
|
88
|
+
fixed_size: 40,
|
89
|
+
pad: '_-'
|
90
|
+
},
|
91
|
+
second_cell: {
|
92
|
+
text: 'pad it!',
|
93
|
+
justify: :left,
|
94
|
+
color_pair: CursesMenu::COLORS_GREEN,
|
95
|
+
fixed_size: 40,
|
96
|
+
pad: '*'
|
97
|
+
}
|
98
|
+
)
|
99
|
+
menu.item CursesMenu::CursesRow.new(
|
100
|
+
{
|
101
|
+
first_cell: { text: 'Use a' },
|
102
|
+
second_cell: {
|
103
|
+
text: 'different separator',
|
104
|
+
color_pair: CursesMenu::COLORS_GREEN
|
105
|
+
},
|
106
|
+
third_cell: { text: 'between cells' }
|
107
|
+
},
|
108
|
+
separator: '|'
|
109
|
+
)
|
110
|
+
menu.item dynamic_row_1 do
|
111
|
+
dynamic_row_1.cells_order([:first_cell, :second_cell, :third_cell].sort_by { rand })
|
112
|
+
:menu_refresh
|
113
|
+
end
|
114
|
+
menu.item dynamic_row_2 do
|
115
|
+
dynamic_row_2.change_cells(
|
116
|
+
first_cell: {
|
117
|
+
color_pair: [CursesMenu::COLORS_GREEN, CursesMenu::COLORS_RED, CursesMenu::COLORS_BLUE].sample
|
118
|
+
},
|
119
|
+
second_cell: {
|
120
|
+
color_pair: [CursesMenu::COLORS_GREEN, CursesMenu::COLORS_RED, CursesMenu::COLORS_BLUE].sample,
|
121
|
+
pad: ['*', ' ', '|', '='].sample
|
122
|
+
}
|
123
|
+
)
|
124
|
+
:menu_refresh
|
125
|
+
end
|
126
|
+
menu.item 'Quit' do
|
127
|
+
puts 'Quitting...'
|
128
|
+
:menu_exit
|
129
|
+
end
|
130
|
+
end
|
data/examples/hello.rb
ADDED
data/examples/refresh.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'curses_menu'
|
2
|
+
|
3
|
+
nbr = 0
|
4
|
+
switch = false
|
5
|
+
CursesMenu.new 'Menu being refreshed when selecting things' do |menu|
|
6
|
+
menu.item "Current number is #{nbr} - Select me for +1" do
|
7
|
+
nbr += 1
|
8
|
+
:menu_refresh
|
9
|
+
end
|
10
|
+
menu.item "Current number is #{nbr} - Select me for -1" do
|
11
|
+
nbr -= 1
|
12
|
+
:menu_refresh
|
13
|
+
end
|
14
|
+
menu.item "[#{switch ? '*' : ' '}] Switch me!" do
|
15
|
+
switch = !switch
|
16
|
+
:menu_refresh
|
17
|
+
end
|
18
|
+
menu.item 'Quit' do
|
19
|
+
puts 'Quitting...'
|
20
|
+
:menu_exit
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'curses_menu'
|
2
|
+
|
3
|
+
CursesMenu.new 'We have several items, some of them have no action' do |menu|
|
4
|
+
menu.item 'Nothing to do with me'
|
5
|
+
menu.item 'Select me - I\'m option A!' do
|
6
|
+
puts 'You have selected A. Press enter to continue.'
|
7
|
+
$stdin.gets
|
8
|
+
end
|
9
|
+
menu.item 'Or select me - Option B!' do
|
10
|
+
puts 'You have selected B. Press enter to continue.'
|
11
|
+
$stdin.gets
|
12
|
+
end
|
13
|
+
menu.item '---- Separator'
|
14
|
+
menu.item 'Quit' do
|
15
|
+
puts 'Quitting...'
|
16
|
+
:menu_exit
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'curses_menu'
|
2
|
+
|
3
|
+
CursesMenu.new 'Top menu' do |menu|
|
4
|
+
menu.item 'Enter menu 1' do
|
5
|
+
CursesMenu.new 'Sub-menu 1' do |sub_menu|
|
6
|
+
sub_menu.item 'We are in sub-menu 1'
|
7
|
+
sub_menu.item('Back') { :menu_exit }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
menu.item 'Enter menu 2' do
|
11
|
+
CursesMenu.new 'Sub-menu 2' do |sub_menu|
|
12
|
+
sub_menu.item 'We are in sub-menu 2'
|
13
|
+
sub_menu.item('Back') { :menu_exit }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
menu.item 'Quit' do
|
17
|
+
puts 'Quitting...'
|
18
|
+
:menu_exit
|
19
|
+
end
|
20
|
+
end
|
data/lib/curses_menu.rb
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'curses'
|
2
|
+
require 'curses_menu/curses_row'
|
3
|
+
|
4
|
+
class CursesMenu
|
5
|
+
|
6
|
+
# Define some color pairs names.
|
7
|
+
# The integer value is meaningless in itself but they all have to be different.
|
8
|
+
COLORS_TITLE = 1
|
9
|
+
COLORS_LINE = 2
|
10
|
+
COLORS_MENU_ITEM = 3
|
11
|
+
COLORS_MENU_ITEM_SELECTED = 4
|
12
|
+
COLORS_INPUT = 5
|
13
|
+
COLORS_GREEN = 6
|
14
|
+
COLORS_RED = 7
|
15
|
+
COLORS_YELLOW = 8
|
16
|
+
COLORS_BLUE = 9
|
17
|
+
|
18
|
+
# curses keys that are not defined by Curses, but that are returned by getch
|
19
|
+
KEY_ENTER = 10
|
20
|
+
KEY_ESCAPE = 27
|
21
|
+
|
22
|
+
# Constructor.
|
23
|
+
# Display a list of choices, ask for user input and execute the choice made.
|
24
|
+
# Repeat the operation unless one of the code returns the :menu_exit symbol.
|
25
|
+
#
|
26
|
+
# Parameters::
|
27
|
+
# * *title* (String): Title of those choices
|
28
|
+
# * *key_presses* (Array<Object>): List of key presses to automatically apply [default: []]
|
29
|
+
# Can be characters or ascii values, as returned by curses' getch.
|
30
|
+
# The list is modified in place along with its consumption, so that it can be reused in sub-menus if needed.
|
31
|
+
# * *&menu_items_def* (Proc): Code to be called to get the list of choices. This code can call the following methods to design the menu:
|
32
|
+
# * Parameters::
|
33
|
+
# * *menu* (CursesMenu): The CursesMenu instance
|
34
|
+
def initialize(title, key_presses: [], &menu_items_def)
|
35
|
+
@current_menu_items = nil
|
36
|
+
@curses_initialized = false
|
37
|
+
current_items = gather_menu_items(&menu_items_def)
|
38
|
+
selected_idx = 0
|
39
|
+
raise "Menu #{title} has no items to select" if selected_idx.nil?
|
40
|
+
window = curses_menu_initialize
|
41
|
+
begin
|
42
|
+
max_displayed_items = window.maxy - 5
|
43
|
+
display_first_idx = 0
|
44
|
+
display_first_char_idx = 0
|
45
|
+
loop do
|
46
|
+
# TODO: Don't redraw fixed items for performance
|
47
|
+
# Display the title
|
48
|
+
window.setpos(0, 0)
|
49
|
+
print(window, '', default_color_pair: COLORS_TITLE, pad: '=')
|
50
|
+
print(window, "= #{title}", default_color_pair: COLORS_TITLE, pad: ' ', single_line: true)
|
51
|
+
print(window, '', default_color_pair: COLORS_TITLE, pad: '-')
|
52
|
+
# Display the menu
|
53
|
+
current_items[display_first_idx..display_first_idx + max_displayed_items - 1].each.with_index do |item_info, idx|
|
54
|
+
selected = display_first_idx + idx == selected_idx
|
55
|
+
print(
|
56
|
+
window,
|
57
|
+
item_info[:title],
|
58
|
+
from: display_first_char_idx,
|
59
|
+
default_color_pair: item_info.key?(:actions) ? COLORS_MENU_ITEM : COLORS_LINE,
|
60
|
+
force_color_pair: selected ? COLORS_MENU_ITEM_SELECTED : nil,
|
61
|
+
pad: selected ? ' ' : nil,
|
62
|
+
single_line: true
|
63
|
+
)
|
64
|
+
end
|
65
|
+
# Display the footer
|
66
|
+
window.setpos(window.maxy - 2, 0)
|
67
|
+
print(window, '', default_color_pair: COLORS_TITLE, pad: '=')
|
68
|
+
display_actions = {
|
69
|
+
'Arrows/Home/End' => 'Navigate',
|
70
|
+
'Esc' => 'Exit'
|
71
|
+
}
|
72
|
+
if current_items[selected_idx][:actions]
|
73
|
+
display_actions.merge!(Hash[current_items[selected_idx][:actions].map do |action_shortcut, action_info|
|
74
|
+
[
|
75
|
+
case action_shortcut
|
76
|
+
when KEY_ENTER
|
77
|
+
'Enter'
|
78
|
+
else
|
79
|
+
action_shortcut
|
80
|
+
end,
|
81
|
+
action_info[:name]
|
82
|
+
]
|
83
|
+
end])
|
84
|
+
end
|
85
|
+
print(
|
86
|
+
window,
|
87
|
+
"= #{display_actions.sort.map { |(shortcut, name)| "#{shortcut}: #{name}" }.join(' | ')}",
|
88
|
+
from: display_first_char_idx,
|
89
|
+
default_color_pair: COLORS_TITLE,
|
90
|
+
pad: ' ',
|
91
|
+
add_nl: false,
|
92
|
+
single_line: true
|
93
|
+
)
|
94
|
+
window.refresh
|
95
|
+
user_choice = nil
|
96
|
+
loop do
|
97
|
+
user_choice = key_presses.empty? ? window.getch : key_presses.shift
|
98
|
+
break unless user_choice.nil?
|
99
|
+
sleep 0.01
|
100
|
+
end
|
101
|
+
case user_choice
|
102
|
+
when Curses::KEY_RIGHT
|
103
|
+
display_first_char_idx += 1
|
104
|
+
when Curses::KEY_LEFT
|
105
|
+
display_first_char_idx -= 1
|
106
|
+
when Curses::KEY_UP
|
107
|
+
selected_idx -= 1
|
108
|
+
when Curses::KEY_PPAGE
|
109
|
+
selected_idx -= max_displayed_items - 1
|
110
|
+
when Curses::KEY_DOWN
|
111
|
+
selected_idx += 1
|
112
|
+
when Curses::KEY_NPAGE
|
113
|
+
selected_idx += max_displayed_items - 1
|
114
|
+
when Curses::KEY_HOME
|
115
|
+
selected_idx = 0
|
116
|
+
when Curses::KEY_END
|
117
|
+
selected_idx = current_items.size - 1
|
118
|
+
when KEY_ESCAPE
|
119
|
+
break
|
120
|
+
else
|
121
|
+
# Check actions
|
122
|
+
if current_items[selected_idx][:actions]&.key?(user_choice)
|
123
|
+
curses_menu_finalize
|
124
|
+
result = current_items[selected_idx][:actions][user_choice][:execute].call
|
125
|
+
if result.is_a?(Symbol)
|
126
|
+
case result
|
127
|
+
when :menu_exit
|
128
|
+
break
|
129
|
+
when :menu_refresh
|
130
|
+
current_items = gather_menu_items(&menu_items_def)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
window = curses_menu_initialize
|
134
|
+
window.clear
|
135
|
+
end
|
136
|
+
end
|
137
|
+
# Stay in bounds
|
138
|
+
display_first_char_idx = 0 if display_first_char_idx < 0
|
139
|
+
selected_idx = current_items.size - 1 if selected_idx >= current_items.size
|
140
|
+
selected_idx = 0 if selected_idx < 0
|
141
|
+
if selected_idx < display_first_idx
|
142
|
+
display_first_idx = selected_idx
|
143
|
+
elsif selected_idx >= display_first_idx + max_displayed_items
|
144
|
+
display_first_idx = selected_idx - max_displayed_items + 1
|
145
|
+
end
|
146
|
+
end
|
147
|
+
ensure
|
148
|
+
curses_menu_finalize
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Register a new menu item.
|
153
|
+
# This method is meant to be called from a choose_from call.
|
154
|
+
#
|
155
|
+
# Parameters::
|
156
|
+
# * *title* (String or CursesRow): Text to be displayed for this item
|
157
|
+
# * *actions* (Hash<Object, Hash<Symbol,Object> >): Associated actions to this item, per shortcut [default: {}]
|
158
|
+
# * *name* (String): Name of this action (displayed at the bottom of the menu)
|
159
|
+
# * *execute* (Proc): Code called when this action is selected
|
160
|
+
# * *&action* (Proc): Code called if the item is selected (action for the enter key) [optional].
|
161
|
+
# * Result::
|
162
|
+
# * Symbol or Object: If the code returns a symbol, the menu will behave in a specific way:
|
163
|
+
# * *menu_exit*: the menu selection exits.
|
164
|
+
# * *menu_refresh*: The menu will compute again its items.
|
165
|
+
def item(title, actions: {}, &action)
|
166
|
+
menu_item_def = { title: title }
|
167
|
+
all_actions = action.nil? ? actions : actions.merge(KEY_ENTER => { name: 'Select', execute: action })
|
168
|
+
menu_item_def[:actions] = all_actions unless all_actions.empty?
|
169
|
+
@current_menu_items << menu_item_def
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
# Display a given curses string information.
|
175
|
+
#
|
176
|
+
# Parameters::
|
177
|
+
# * *window* (Window): The curses window in which we display.
|
178
|
+
# * *string* (String or CursesRow): The curses row, or as a single String.
|
179
|
+
# * See CursesRow#print_on for all the other parameters description
|
180
|
+
def print(window, string, from: 0, to: nil, default_color_pair: COLORS_LINE, force_color_pair: nil, pad: nil, add_nl: true, single_line: false)
|
181
|
+
string = CursesRow.new(default: { text: string }) if string.is_a?(String)
|
182
|
+
string.print_on(
|
183
|
+
window,
|
184
|
+
from: from,
|
185
|
+
to: to,
|
186
|
+
default_color_pair: default_color_pair,
|
187
|
+
force_color_pair: force_color_pair,
|
188
|
+
pad: pad,
|
189
|
+
add_nl: add_nl,
|
190
|
+
single_line: single_line
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Initialize and get the curses menu window
|
195
|
+
#
|
196
|
+
# Result::
|
197
|
+
# * Window: The curses menu window
|
198
|
+
def curses_menu_initialize
|
199
|
+
Curses.init_screen
|
200
|
+
# Use non-blocking key read, otherwise using Popen3 later blocks
|
201
|
+
Curses.timeout = 0
|
202
|
+
Curses.start_color
|
203
|
+
Curses.init_pair(COLORS_TITLE, Curses::COLOR_BLACK, Curses::COLOR_CYAN)
|
204
|
+
Curses.init_pair(COLORS_LINE, Curses::COLOR_WHITE, Curses::COLOR_BLACK)
|
205
|
+
Curses.init_pair(COLORS_MENU_ITEM, Curses::COLOR_WHITE, Curses::COLOR_BLACK)
|
206
|
+
Curses.init_pair(COLORS_MENU_ITEM_SELECTED, Curses::COLOR_BLACK, Curses::COLOR_WHITE)
|
207
|
+
Curses.init_pair(COLORS_INPUT, Curses::COLOR_WHITE, Curses::COLOR_BLUE)
|
208
|
+
Curses.init_pair(COLORS_GREEN, Curses::COLOR_GREEN, Curses::COLOR_BLACK)
|
209
|
+
Curses.init_pair(COLORS_RED, Curses::COLOR_RED, Curses::COLOR_BLACK)
|
210
|
+
Curses.init_pair(COLORS_YELLOW, Curses::COLOR_YELLOW, Curses::COLOR_BLACK)
|
211
|
+
Curses.init_pair(COLORS_BLUE, Curses::COLOR_BLUE, Curses::COLOR_BLACK)
|
212
|
+
window = Curses.stdscr
|
213
|
+
window.keypad = true
|
214
|
+
@curses_initialized = true
|
215
|
+
window
|
216
|
+
end
|
217
|
+
|
218
|
+
# Finalize the curses menu window
|
219
|
+
def curses_menu_finalize
|
220
|
+
Curses.close_screen if @curses_initialized
|
221
|
+
@curses_initialized = false
|
222
|
+
end
|
223
|
+
|
224
|
+
# Get menu items.
|
225
|
+
#
|
226
|
+
# Parameters::
|
227
|
+
# * Proc: Code defining the menu items
|
228
|
+
# * *menu* (CursesMenu): The menu for which we gather items.
|
229
|
+
# Result::
|
230
|
+
# * Array< Hash<Symbol,Object> >: List of items to be displayed
|
231
|
+
# * *title* (String): Item title to display
|
232
|
+
# * *actions* (Hash<Object, Hash<Symbol,Object> >): Associated actions to this item, per shortcut [optional]
|
233
|
+
# * *name* (String): Name of this action (displayed at the bottom of the menu)
|
234
|
+
# * *execute* (Proc): Code called when this action is selected
|
235
|
+
def gather_menu_items
|
236
|
+
@current_menu_items = []
|
237
|
+
yield self
|
238
|
+
@current_menu_items
|
239
|
+
end
|
240
|
+
|
241
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
class CursesMenu
|
2
|
+
|
3
|
+
# Definition of a row that stores for each cell the string and color information to be displayed
|
4
|
+
class CursesRow
|
5
|
+
|
6
|
+
# Constructor
|
7
|
+
#
|
8
|
+
# Parameters::
|
9
|
+
# * *cells* (Hash< Symbol, Hash<Symbol,Object> >): For each cell id (ordered), the cell info:
|
10
|
+
# * *text* (String): Text associated to this cell
|
11
|
+
# * *color_pair* (Integer): Associated color pair [optional]
|
12
|
+
# * *begin_with* (String): String to prepend to the text [default: '']
|
13
|
+
# * *end_with* (String): String to append to the text [default: '']
|
14
|
+
# * *fixed_size* (Integer): Number of characters this cell will take, or nil if no limit. [default: nil]
|
15
|
+
# * *justify* (Symbol): Text justification (only used when fixed_size is not nil). Values can be: [default: :left]
|
16
|
+
# * *left*: Left justified
|
17
|
+
# * *right*: Right justified
|
18
|
+
# * *pad* (String): Text to be used to pad the cell content (only used when fixed_size is not nil) [default: ' ']
|
19
|
+
# * *separator* (String): Separator used between cells [default: ' ']
|
20
|
+
def initialize(cells, separator: ' ')
|
21
|
+
@cells = cells
|
22
|
+
@separator = separator
|
23
|
+
end
|
24
|
+
|
25
|
+
# Change the cells order
|
26
|
+
#
|
27
|
+
# Parameters::
|
28
|
+
# * *cells* (Array<Symbol>): The ordered list of cells to filter
|
29
|
+
# * *unknown_cells* (String or Hash<Symbol,Object>): Content to put in unknown cells (as a String or properties like in #initialize), or nil to not add them. [default: nil]
|
30
|
+
def cells_order(cells, unknown_cells: nil)
|
31
|
+
new_cells = {}
|
32
|
+
cells.each do |cell_id|
|
33
|
+
if @cells.key?(cell_id)
|
34
|
+
new_cells[cell_id] = @cells[cell_id]
|
35
|
+
elsif !unknown_cells.nil?
|
36
|
+
new_cells[cell_id] = unknown_cells.is_a?(String) ? { text: unknown_cells } : unknown_cells
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@cells = new_cells
|
40
|
+
end
|
41
|
+
|
42
|
+
# Change properties of a set of cells
|
43
|
+
#
|
44
|
+
# Parameters::
|
45
|
+
# * *cells* (Hash<Symbol, Hash<Symbol,Object> >): The cells properties to change, per cell id. Possible properties are the ones given in the #initialize method.
|
46
|
+
def change_cells(cells)
|
47
|
+
cells.each do |cell_id, cell_info|
|
48
|
+
raise "Unknown cell #{cell_id}" unless @cells.key?(cell_id)
|
49
|
+
@cells[cell_id].merge!(cell_info)
|
50
|
+
@cells[cell_id].delete(:cache_rendered_text)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Get the size of the total string of such row.
|
55
|
+
#
|
56
|
+
# Parameters::
|
57
|
+
# * *cells* (Array<Symbol>): The list of cells to consider for the size [default: @cells.keys]
|
58
|
+
# Result::
|
59
|
+
# * Integer: Row size
|
60
|
+
def size(cells: @cells.keys)
|
61
|
+
result = @separator.size * (cells.size - 1)
|
62
|
+
cells.each do |cell_id|
|
63
|
+
result += cell_text(cell_id).size
|
64
|
+
end
|
65
|
+
result
|
66
|
+
end
|
67
|
+
|
68
|
+
# Print this row into a window
|
69
|
+
#
|
70
|
+
# Parameters::
|
71
|
+
# * *window* (Window): Curses window to print on
|
72
|
+
# * *from* (Integer): From index to be displayed [default: 0]
|
73
|
+
# * *to* (Integer): To index to be displayed [default: total size]
|
74
|
+
# * *default_color_pair* (Integer): Default color pair to use if no color information is provided [default: COLORS_LINE]
|
75
|
+
# * *force_color_pair* (Integer): Force color pair to use, or nil to not force [default: nil]
|
76
|
+
# * *pad* (String or nil): Pad the line to the row extent with the given string, or nil for no padding. [default: nil]
|
77
|
+
# * *add_nl* (Boolean): If true, then add a new line at the end [default: true]
|
78
|
+
# * *single_line* (Boolean): If true, then make sure the print does not exceed the line [default: false]
|
79
|
+
def print_on(window, from: 0, to: nil, default_color_pair: COLORS_LINE, force_color_pair: nil, pad: nil, add_nl: true, single_line: false)
|
80
|
+
text_size = size
|
81
|
+
from = text_size if from > text_size
|
82
|
+
to = text_size - 1 if to.nil?
|
83
|
+
to = window.maxx - window.curx + from - 2 if single_line && window.curx + to - from >= window.maxx - 1
|
84
|
+
current_idx = 0
|
85
|
+
@cells.each.with_index do |(cell_id, cell_info), cell_idx|
|
86
|
+
text = cell_text(cell_id)
|
87
|
+
full_substring_size = text.size + @separator.size
|
88
|
+
if from < current_idx + full_substring_size
|
89
|
+
# We have something to display from this substring
|
90
|
+
window.color_set(
|
91
|
+
if force_color_pair.nil?
|
92
|
+
cell_info[:color_pair] ? cell_info[:color_pair] : default_color_pair
|
93
|
+
else
|
94
|
+
force_color_pair
|
95
|
+
end
|
96
|
+
)
|
97
|
+
window << "#{text}#{cell_idx == @cells.size - 1 ? '' : @separator}"[(from < current_idx ? 0 : from - current_idx)..to - current_idx]
|
98
|
+
end
|
99
|
+
current_idx += full_substring_size
|
100
|
+
break if current_idx > to
|
101
|
+
end
|
102
|
+
window.color_set(force_color_pair.nil? ? default_color_pair : force_color_pair)
|
103
|
+
if pad && window.curx < window.maxx
|
104
|
+
nbr_chars = window.maxx - window.curx - 1
|
105
|
+
window << (pad * nbr_chars)[0..nbr_chars - 1]
|
106
|
+
end
|
107
|
+
window << "\n" if add_nl
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
# Get a cell's text.
|
113
|
+
# Cache it to not compute it several times.
|
114
|
+
#
|
115
|
+
# Parameters::
|
116
|
+
# * *cell_id* (Symbol): Cell id to get text for
|
117
|
+
# Result::
|
118
|
+
# * String: The cell's text
|
119
|
+
def cell_text(cell_id, cell_decorator: nil)
|
120
|
+
unless @cells[cell_id].key?(:cache_rendered_text)
|
121
|
+
begin_str = "#{@cells[cell_id][:begin_with] || ''}#{@cells[cell_id][:text]}"
|
122
|
+
end_str = @cells[cell_id][:end_with] || ''
|
123
|
+
@cells[cell_id][:cache_rendered_text] =
|
124
|
+
if @cells[cell_id][:fixed_size]
|
125
|
+
text = "#{begin_str[0..@cells[cell_id][:fixed_size] - end_str.size - 1]}#{end_str}"
|
126
|
+
remaining_size = @cells[cell_id][:fixed_size] - text.size
|
127
|
+
if remaining_size > 0
|
128
|
+
padding = ((@cells[cell_id][:pad] || ' ') * remaining_size)[0..remaining_size - 1]
|
129
|
+
justify = @cells[cell_id][:justify] || :left
|
130
|
+
case justify
|
131
|
+
when :left
|
132
|
+
"#{text}#{padding}"
|
133
|
+
when :right
|
134
|
+
"#{padding}#{text}"
|
135
|
+
else
|
136
|
+
raise "Unknown justify decorator: #{justify}"
|
137
|
+
end
|
138
|
+
else
|
139
|
+
text[0..@cells[cell_id][:fixed_size] - 1]
|
140
|
+
end
|
141
|
+
else
|
142
|
+
"#{begin_str}#{end_str}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
@cells[cell_id][:cache_rendered_text]
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: curses_menu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Muriel Salvan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: curses
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- muriel@x-aeon.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- examples/actions.rb
|
35
|
+
- examples/automatic_key_presses.rb
|
36
|
+
- examples/formatting.rb
|
37
|
+
- examples/hello.rb
|
38
|
+
- examples/refresh.rb
|
39
|
+
- examples/scrolling.rb
|
40
|
+
- examples/several_items.rb
|
41
|
+
- examples/sub_menus.rb
|
42
|
+
- lib/curses_menu.rb
|
43
|
+
- lib/curses_menu/curses_row.rb
|
44
|
+
homepage: http://x-aeon.com
|
45
|
+
licenses:
|
46
|
+
- BSD-3-Clause
|
47
|
+
metadata:
|
48
|
+
homepage_uri: http://x-aeon.com
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 2.7.6
|
66
|
+
signing_key:
|
67
|
+
specification_version: 4
|
68
|
+
summary: Simple menu offering choices with navigation keys using curses
|
69
|
+
test_files: []
|