curses_menu 0.0.1 → 0.0.5
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/CHANGELOG.md +28 -0
- data/LICENSE.md +31 -0
- data/README.md +78 -0
- data/examples/actions.rb +25 -25
- data/examples/automatic_key_presses.rb +49 -49
- data/examples/formatting.rb +148 -130
- data/examples/hello.rb +8 -8
- data/examples/refresh.rb +22 -22
- data/examples/scrolling.rb +9 -9
- data/examples/several_items.rb +18 -18
- data/examples/sub_menus.rb +20 -20
- data/examples/utf_8.rb +8 -0
- data/lib/curses_menu.rb +248 -241
- data/lib/curses_menu/curses_row.rb +151 -150
- data/lib/curses_menu/version.rb +5 -0
- data/spec/curses_menu_test.rb +131 -0
- data/spec/curses_menu_test/actions_spec.rb +221 -0
- data/spec/curses_menu_test/formatting_spec.rb +411 -0
- data/spec/curses_menu_test/rubocop_spec.rb +31 -0
- data/spec/curses_menu_test/scrolling_spec.rb +95 -0
- data/spec/curses_menu_test/simple_navigation_spec.rb +123 -0
- data/spec/spec_helper.rb +105 -0
- metadata +89 -10
@@ -1,150 +1,151 @@
|
|
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
|
-
|
50
|
-
@cells[cell_id].
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
cells.
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
# * *
|
73
|
-
# * *
|
74
|
-
# * *
|
75
|
-
# * *
|
76
|
-
# * *
|
77
|
-
# * *
|
78
|
-
# * *
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
to =
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
@cells[cell_id][:
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
+
|
50
|
+
@cells[cell_id].merge!(cell_info)
|
51
|
+
@cells[cell_id].delete(:cache_rendered_text)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get the size of the total string of such row.
|
56
|
+
#
|
57
|
+
# Parameters::
|
58
|
+
# * *cells* (Array<Symbol>): The list of cells to consider for the size [default: @cells.keys]
|
59
|
+
# Result::
|
60
|
+
# * Integer: Row size
|
61
|
+
def size(cells: @cells.keys)
|
62
|
+
result = @separator.size * (cells.size - 1)
|
63
|
+
cells.each do |cell_id|
|
64
|
+
result += cell_text(cell_id).size
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
# Print this row into a window
|
70
|
+
#
|
71
|
+
# Parameters::
|
72
|
+
# * *window* (Window): Curses window to print on
|
73
|
+
# * *from* (Integer): From index to be displayed [default: 0]
|
74
|
+
# * *to* (Integer): To index to be displayed [default: total size]
|
75
|
+
# * *default_color_pair* (Integer): Default color pair to use if no color information is provided [default: COLORS_LINE]
|
76
|
+
# * *force_color_pair* (Integer): Force color pair to use, or nil to not force [default: nil]
|
77
|
+
# * *pad* (String or nil): Pad the line to the row extent with the given string, or nil for no padding. [default: nil]
|
78
|
+
# * *add_nl* (Boolean): If true, then add a new line at the end [default: true]
|
79
|
+
# * *single_line* (Boolean): If true, then make sure the print does not exceed the line [default: false]
|
80
|
+
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)
|
81
|
+
text_size = size
|
82
|
+
from = text_size if from > text_size
|
83
|
+
to = text_size - 1 if to.nil?
|
84
|
+
to = window.maxx - window.curx + from - 2 if single_line && window.curx + to - from >= window.maxx - 1
|
85
|
+
current_idx = 0
|
86
|
+
@cells.each.with_index do |(cell_id, cell_info), cell_idx|
|
87
|
+
text = cell_text(cell_id)
|
88
|
+
full_substring_size = text.size + @separator.size
|
89
|
+
if from < current_idx + full_substring_size
|
90
|
+
# We have something to display from this substring
|
91
|
+
window.color_set(
|
92
|
+
if force_color_pair.nil?
|
93
|
+
cell_info[:color_pair] || default_color_pair
|
94
|
+
else
|
95
|
+
force_color_pair
|
96
|
+
end
|
97
|
+
)
|
98
|
+
window << "#{text}#{cell_idx == @cells.size - 1 ? '' : @separator}"[(from < current_idx ? 0 : from - current_idx)..to - current_idx]
|
99
|
+
end
|
100
|
+
current_idx += full_substring_size
|
101
|
+
break if current_idx > to
|
102
|
+
end
|
103
|
+
window.color_set(force_color_pair.nil? ? default_color_pair : force_color_pair)
|
104
|
+
if pad && window.curx < window.maxx
|
105
|
+
nbr_chars = window.maxx - window.curx - 1
|
106
|
+
window << (pad * nbr_chars)[0..nbr_chars - 1]
|
107
|
+
end
|
108
|
+
window << "\n" if add_nl
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Get a cell's text.
|
114
|
+
# Cache it to not compute it several times.
|
115
|
+
#
|
116
|
+
# Parameters::
|
117
|
+
# * *cell_id* (Symbol): Cell id to get text for
|
118
|
+
# Result::
|
119
|
+
# * String: The cell's text
|
120
|
+
def cell_text(cell_id)
|
121
|
+
unless @cells[cell_id].key?(:cache_rendered_text)
|
122
|
+
begin_str = "#{@cells[cell_id][:begin_with] || ''}#{@cells[cell_id][:text]}"
|
123
|
+
end_str = @cells[cell_id][:end_with] || ''
|
124
|
+
@cells[cell_id][:cache_rendered_text] =
|
125
|
+
if @cells[cell_id][:fixed_size]
|
126
|
+
text = "#{begin_str[0..@cells[cell_id][:fixed_size] - end_str.size - 1]}#{end_str}"
|
127
|
+
remaining_size = @cells[cell_id][:fixed_size] - text.size
|
128
|
+
if remaining_size.positive?
|
129
|
+
padding = ((@cells[cell_id][:pad] || ' ') * remaining_size)[0..remaining_size - 1]
|
130
|
+
justify = @cells[cell_id][:justify] || :left
|
131
|
+
case justify
|
132
|
+
when :left
|
133
|
+
"#{text}#{padding}"
|
134
|
+
when :right
|
135
|
+
"#{padding}#{text}"
|
136
|
+
else
|
137
|
+
raise "Unknown justify decorator: #{justify}"
|
138
|
+
end
|
139
|
+
else
|
140
|
+
text[0..@cells[cell_id][:fixed_size] - 1]
|
141
|
+
end
|
142
|
+
else
|
143
|
+
"#{begin_str}#{end_str}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
@cells[cell_id][:cache_rendered_text]
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'curses_menu'
|
2
|
+
|
3
|
+
module CursesMenuTest
|
4
|
+
|
5
|
+
# Monkey-patch the curses_menu_finalize method so that it captures the menu screen before finalizing
|
6
|
+
module CursesMenuPatch
|
7
|
+
|
8
|
+
# Last screenshot taken
|
9
|
+
# Array<String>: List of lines
|
10
|
+
attr_reader :screenshot
|
11
|
+
|
12
|
+
# Finalize the curses menu window
|
13
|
+
def curses_menu_finalize
|
14
|
+
@screenshot = capture_screenshot
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Get a screenshot of the menu
|
21
|
+
#
|
22
|
+
# Result::
|
23
|
+
# * Array<String>: List of lines
|
24
|
+
def capture_screenshot
|
25
|
+
# Curses is initialized
|
26
|
+
window = Curses.stdscr
|
27
|
+
old_x = window.curx
|
28
|
+
old_y = window.cury
|
29
|
+
chars = []
|
30
|
+
window.maxy.times do |idx_y|
|
31
|
+
window.maxx.times do |idx_x|
|
32
|
+
window.setpos idx_y, idx_x
|
33
|
+
chars << window.inch
|
34
|
+
end
|
35
|
+
end
|
36
|
+
window.setpos old_y, old_x
|
37
|
+
# Build the map of colors per color pair acutally registered
|
38
|
+
colors_left_shift = Curses::A_COLOR.to_s(2).match(/^1+(0+)$/)[1].size
|
39
|
+
color_pairs = CursesMenu.constants.select { |const| const.to_s.start_with?('COLORS_') }.map do |const|
|
40
|
+
color_pair = CursesMenu.const_get(const)
|
41
|
+
[
|
42
|
+
# On Windows using Curses.color_pair can result in bugs [BUG] Unnormalized Fixnum value when using/displaying the value.
|
43
|
+
# So for now we depend on the internal algorithm used by color_pair (which is a left shift of the 0 bits of A_COLOR mask)
|
44
|
+
# TODO: Uncomment the following when curses will be fixed on Windows
|
45
|
+
# Curses.color_pair(color_pair),
|
46
|
+
color_pair << colors_left_shift,
|
47
|
+
const
|
48
|
+
]
|
49
|
+
end.to_h
|
50
|
+
chars.
|
51
|
+
map do |chr|
|
52
|
+
{
|
53
|
+
char: begin
|
54
|
+
(chr & Curses::A_CHARTEXT).chr(Encoding::UTF_8)
|
55
|
+
rescue RangeError
|
56
|
+
# On Windows curses returns wrong encoded characters.
|
57
|
+
# In this case we just force an ASCII version of it.
|
58
|
+
(chr & 255).chr(Encoding::UTF_8)
|
59
|
+
end,
|
60
|
+
color: color_pairs[chr & Curses::A_COLOR] || chr & Curses::A_COLOR,
|
61
|
+
attributes: chr & Curses::A_ATTRIBUTES
|
62
|
+
}
|
63
|
+
end.
|
64
|
+
each_slice(window.maxx).
|
65
|
+
to_a
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
# Helpers for the tests
|
71
|
+
module Helpers
|
72
|
+
|
73
|
+
# Test a given menu, and prepare a screenshot to be analyzed
|
74
|
+
#
|
75
|
+
# Parameters::
|
76
|
+
# * *title* (String): The title [default: 'Menu title']
|
77
|
+
# * *keys* (Array<Object> or nil): Keys to automatically press [default: []]
|
78
|
+
# * *auto_exit* (Boolean): Do we automatically add the escape key to the key presses? [default: true]
|
79
|
+
# * Proc: The code called with the test menu to be populated
|
80
|
+
# * Parameters::
|
81
|
+
# * *menu* (CursesMenu): Curses menu to populate
|
82
|
+
# * *key_presses* (Array<Object>): Keys to possibly give to sub-menus
|
83
|
+
def test_menu(title: 'Menu title', keys: [], auto_exit: true)
|
84
|
+
# TODO: Find a way to not depend on the current terminal screen, and run the tests silently.
|
85
|
+
key_presses = auto_exit ? keys + [CursesMenu::KEY_ESCAPE] : keys
|
86
|
+
menu = CursesMenu.new(title, key_presses: key_presses) do |m|
|
87
|
+
yield m, key_presses
|
88
|
+
end
|
89
|
+
@screenshot = menu.screenshot
|
90
|
+
end
|
91
|
+
|
92
|
+
# Assert that a line of the screenshot starts with a given content
|
93
|
+
#
|
94
|
+
# Parameters::
|
95
|
+
# * *line_idx* (Integer): The line index of the screenshot
|
96
|
+
# * *expectation* (String or Regexp): The expected line
|
97
|
+
def assert_line(line_idx, expectation)
|
98
|
+
line = @screenshot[line_idx][0..(expectation.is_a?(Regexp) ? -1 : expectation.size)].map { |char_info| char_info[:char] }.join
|
99
|
+
if expectation.is_a?(Regexp)
|
100
|
+
expect(line).to match(expectation), "Screenshot line #{line_idx} differs:\n \"#{line}\" should be\n \"#{expectation} \""
|
101
|
+
else
|
102
|
+
# Add an ending space to make sure the line does not continue after what we test
|
103
|
+
expect(line).to eq("#{expectation} "), "Screenshot line #{line_idx} differs:\n \"#{line}\" should be\n \"#{expectation} \""
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Assert that a line of the screenshot starts with a given content, using colors information
|
108
|
+
#
|
109
|
+
# Parameters::
|
110
|
+
# * *line_idx* (Integer): The line index of the screenshot
|
111
|
+
# * *expectation* (String): The expected line
|
112
|
+
# * *color* (Symbol): The expected color pair name
|
113
|
+
def assert_colored_line(line_idx, expectation, color)
|
114
|
+
colored_line = @screenshot[line_idx][0..expectation.size - 1].map do |char_info|
|
115
|
+
[char_info[:char], char_info[:color]]
|
116
|
+
end
|
117
|
+
expected_colored_line = expectation.each_char.map do |chr|
|
118
|
+
[chr, color]
|
119
|
+
end
|
120
|
+
expect(colored_line).to eq(expected_colored_line), "Screenshot line #{line_idx} differs:\n \"#{colored_line}\" should be\n \"#{expected_colored_line}\""
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
class CursesMenu
|
128
|
+
|
129
|
+
prepend CursesMenuTest::CursesMenuPatch
|
130
|
+
|
131
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
describe CursesMenu do
|
2
|
+
|
3
|
+
it 'actions the default selection when pressed enter' do
|
4
|
+
actioned = false
|
5
|
+
test_menu(keys: [CursesMenu::KEY_ENTER]) do |menu|
|
6
|
+
menu.item 'Menu item' do
|
7
|
+
actioned = true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
expect(actioned).to eq(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'actions the default selection when pressed enter on the correct item' do
|
14
|
+
action = nil
|
15
|
+
test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_DOWN, CursesMenu::KEY_ENTER]) do |menu|
|
16
|
+
menu.item 'Menu item 1' do
|
17
|
+
action = 1
|
18
|
+
end
|
19
|
+
menu.item 'Menu item 2' do
|
20
|
+
action = 2
|
21
|
+
end
|
22
|
+
menu.item 'Menu item 3' do
|
23
|
+
action = 3
|
24
|
+
end
|
25
|
+
menu.item 'Menu item 4' do
|
26
|
+
action = 4
|
27
|
+
end
|
28
|
+
end
|
29
|
+
expect(action).to eq(3)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'actions other actions' do
|
33
|
+
action = nil
|
34
|
+
test_menu(keys: ['a']) do |menu|
|
35
|
+
menu.item 'Menu item', actions: {
|
36
|
+
'a' => {
|
37
|
+
name: 'Action A',
|
38
|
+
execute: proc { action = 'a' }
|
39
|
+
},
|
40
|
+
'b' => {
|
41
|
+
name: 'Action B',
|
42
|
+
execute: proc { action = 'b' }
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
expect(action).to eq('a')
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'actions several actions' do
|
50
|
+
actions = []
|
51
|
+
test_menu(keys: %w[a b a]) do |menu|
|
52
|
+
menu.item 'Menu item', actions: {
|
53
|
+
'a' => {
|
54
|
+
name: 'Action A',
|
55
|
+
execute: proc { actions << 'a' }
|
56
|
+
},
|
57
|
+
'b' => {
|
58
|
+
name: 'Action B',
|
59
|
+
execute: proc { actions << 'b' }
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
expect(actions).to eq(%w[a b a])
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'actions several actions including the default one' do
|
67
|
+
actions = []
|
68
|
+
test_menu(keys: ['a', 'b', CursesMenu::KEY_ENTER, 'a']) do |menu|
|
69
|
+
menu.item(
|
70
|
+
'Menu item',
|
71
|
+
actions: {
|
72
|
+
'a' => {
|
73
|
+
name: 'Action A',
|
74
|
+
execute: proc { actions << 'a' }
|
75
|
+
},
|
76
|
+
'b' => {
|
77
|
+
name: 'Action B',
|
78
|
+
execute: proc { actions << 'b' }
|
79
|
+
}
|
80
|
+
}
|
81
|
+
) do
|
82
|
+
actions << 'ENTER'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
expect(actions).to eq(%w[a b ENTER a])
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'actions nothing if action does not exist' do
|
89
|
+
actions = []
|
90
|
+
test_menu(keys: %w[a b c a]) do |menu|
|
91
|
+
menu.item(
|
92
|
+
'Menu item',
|
93
|
+
actions: {
|
94
|
+
'a' => {
|
95
|
+
name: 'Action A',
|
96
|
+
execute: proc { actions << 'a' }
|
97
|
+
},
|
98
|
+
'b' => {
|
99
|
+
name: 'Action B',
|
100
|
+
execute: proc { actions << 'b' }
|
101
|
+
}
|
102
|
+
}
|
103
|
+
) do
|
104
|
+
actions << 'ENTER'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
expect(actions).to eq(%w[a b a])
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'exits when action returns :menu_exit' do
|
111
|
+
quit = false
|
112
|
+
test_menu(keys: [CursesMenu::KEY_ENTER], auto_exit: false) do |menu|
|
113
|
+
menu.item 'Menu item quit' do
|
114
|
+
quit = true
|
115
|
+
:menu_exit
|
116
|
+
end
|
117
|
+
end
|
118
|
+
expect(quit).to eq(true)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'navigates in sub-menus' do
|
122
|
+
actions = []
|
123
|
+
test_menu(
|
124
|
+
keys: [
|
125
|
+
# Enter sub-menu 1
|
126
|
+
CursesMenu::KEY_ENTER,
|
127
|
+
# Action sub-menu second item
|
128
|
+
Curses::KEY_DOWN,
|
129
|
+
CursesMenu::KEY_ENTER,
|
130
|
+
# Back to first menu
|
131
|
+
CursesMenu::KEY_ESCAPE,
|
132
|
+
# Enter sub-menu 2
|
133
|
+
Curses::KEY_DOWN,
|
134
|
+
CursesMenu::KEY_ENTER,
|
135
|
+
# Action sub-menu item
|
136
|
+
CursesMenu::KEY_ENTER,
|
137
|
+
# Exit sub-menu
|
138
|
+
Curses::KEY_DOWN,
|
139
|
+
CursesMenu::KEY_ENTER
|
140
|
+
]
|
141
|
+
) do |menu, key_presses|
|
142
|
+
menu.item 'Sub-menu 1' do
|
143
|
+
described_class.new('Sub-menu 1 title', key_presses: key_presses) do |sub_menu|
|
144
|
+
sub_menu.item 'Sub-menu item 1'
|
145
|
+
sub_menu.item 'Sub-menu item 2' do
|
146
|
+
actions << 'a'
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
menu.item 'Sub-menu 2' do
|
151
|
+
described_class.new('Sub-menu 2 title', key_presses: key_presses) do |sub_menu|
|
152
|
+
sub_menu.item 'Sub-menu item 1' do
|
153
|
+
actions << 'b'
|
154
|
+
end
|
155
|
+
sub_menu.item 'Sub-menu item 2' do
|
156
|
+
:menu_exit
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
expect(actions).to eq(%w[a b])
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'exits only the sub-menu when action returns :menu_exit in a sub-menu' do
|
165
|
+
actions = []
|
166
|
+
test_menu(keys: [CursesMenu::KEY_ENTER, CursesMenu::KEY_ENTER, Curses::KEY_DOWN, CursesMenu::KEY_ENTER]) do |menu, key_presses|
|
167
|
+
menu.item 'Sub-menu' do
|
168
|
+
described_class.new('Sub-menu title', key_presses: key_presses) do |sub_menu|
|
169
|
+
sub_menu.item 'Sub-menu item quit' do
|
170
|
+
actions << 'a'
|
171
|
+
:menu_exit
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
menu.item 'Menu item 2' do
|
176
|
+
actions << 'b'
|
177
|
+
end
|
178
|
+
end
|
179
|
+
expect(actions).to eq(%w[a b])
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'exits only the sub-menu when Escape key is used' do
|
183
|
+
actions = []
|
184
|
+
test_menu(keys: [CursesMenu::KEY_ENTER, CursesMenu::KEY_ESCAPE, Curses::KEY_DOWN, CursesMenu::KEY_ENTER]) do |menu, key_presses|
|
185
|
+
menu.item 'Sub-menu' do
|
186
|
+
described_class.new('Sub-menu title', key_presses: key_presses) do |sub_menu|
|
187
|
+
sub_menu.item 'Sub-menu item quit' do
|
188
|
+
actions << 'a'
|
189
|
+
:menu_exit
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
menu.item 'Menu item 2' do
|
194
|
+
actions << 'b'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
expect(actions).to eq(%w[b])
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'does not refresh menu items normally' do
|
201
|
+
idx = 0
|
202
|
+
test_menu(keys: [CursesMenu::KEY_ENTER, CursesMenu::KEY_ENTER]) do |menu|
|
203
|
+
menu.item "Menu item #{idx}" do
|
204
|
+
idx += 1
|
205
|
+
end
|
206
|
+
end
|
207
|
+
assert_line 3, 'Menu item 0'
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'refreshes menu items when action returns :menu_refresh' do
|
211
|
+
idx = 0
|
212
|
+
test_menu(keys: [CursesMenu::KEY_ENTER, CursesMenu::KEY_ENTER]) do |menu|
|
213
|
+
menu.item "Menu item #{idx}" do
|
214
|
+
idx += 1
|
215
|
+
:menu_refresh
|
216
|
+
end
|
217
|
+
end
|
218
|
+
assert_line 3, 'Menu item 2'
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|