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.
@@ -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
- @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
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,5 @@
1
+ class CursesMenu
2
+
3
+ VERSION = '0.0.5'
4
+
5
+ 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