curses_menu 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8dc4a9d14a6f315f6e2fba464d40d58d7a17922ee9afe1aabc1c045a4731a561
4
- data.tar.gz: fde73b88bf3972e2b4353911dea6278cd606a2a8e673feeab8dff41a87a0f3e8
3
+ metadata.gz: '093b7bafe2adec885ebb1cd49607556b54a1925f416597d1ab220519ec6009f8'
4
+ data.tar.gz: f0a6110f5e1ae34026c3d6982a5f2e8c8f402849bd0a7805ed43f90a8b398cad
5
5
  SHA512:
6
- metadata.gz: 6f9e782b2d1264e4b8b2e3b5f721574ba62abb20935a6d61fe6a70647d73386da9fc6c429689f35de3463f2d6aa6ce5d799fe8d17793b05a5621a27128bb3456
7
- data.tar.gz: c39295fce31482e5f3dd91f2a0de36658912c4fd0d6da3d7a18d0ae55671b74dd5d13e6b74037c96b57993716776697f3b5cb01e5da484b660dc459cd2fe9d1a
6
+ metadata.gz: 6b8d1c93130d98de349183ffd888f21de3ea0023e0cd7372817dc7efa0606974c442b0c1bd1d10f946395deb62cac089a332a0fc7f35c13a662c6452147b736e
7
+ data.tar.gz: 15f764995f0baa99dca4da1e73a67c2079c4956f237f3975c3086f6f9678c212f10e6c40e8ae0d3d90ab4375463114c917708fd44ed1a058fe71239a61390254
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # [v0.2.0](https://github.com/Muriel-Salvan/curses_menu/compare/v0.1.0...v0.2.0) (2022-11-01 21:07:07)
2
+
3
+ ### Features
4
+
5
+ * [[feature] [#6] Add lazy evaluation for actions](https://github.com/Muriel-Salvan/curses_menu/commit/0a5a96ae84fecdf982f763b833e8e863231dde73)
6
+
1
7
  # [v0.1.0](https://github.com/Muriel-Salvan/curses_menu/compare/v0.0.6...v0.1.0) (2022-10-12 15:58:25)
2
8
 
3
9
  ### Features
@@ -3,10 +3,21 @@ require 'curses_menu'
3
3
  nbr_visible_items = Curses.stdscr.maxy - 5
4
4
  CursesMenu.new 'Menu items using lazy rendering' do |menu|
5
5
  (nbr_visible_items * 2).times do
6
- menu.item "I am a normal item, rendered at #{Time.now}"
6
+ menu.item "[Rendered at #{Time.now}] - I am a normal item"
7
7
  menu.item(proc do
8
- "I am a lazy item, rendered at #{Time.now}"
8
+ "[Rendered at #{Time.now}] - I am a lazy item"
9
9
  end)
10
+ menu.item(
11
+ "[Rendered at #{Time.now}] - I am a normal item with lazy rendered action",
12
+ actions: proc do
13
+ {
14
+ 'a' => {
15
+ name: "Action rendered at #{Time.now}",
16
+ execute: proc {}
17
+ }
18
+ }
19
+ end
20
+ )
10
21
  end
11
22
  menu.item 'Refresh menu' do
12
23
  :menu_refresh
@@ -1,5 +1,5 @@
1
1
  class CursesMenu
2
2
 
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
 
5
5
  end
data/lib/curses_menu.rb CHANGED
@@ -74,9 +74,11 @@ class CursesMenu
74
74
  'Arrows/Home/End' => 'Navigate',
75
75
  'Esc' => 'Exit'
76
76
  }
77
- if current_items[selected_idx][:actions]
77
+ # Keep a cache of actions as they can be loaded in a lazy way for performance
78
+ current_items[selected_idx][:actions_cached] = current_items[selected_idx][:actions].is_a?(Proc) ? current_items[selected_idx][:actions].call : current_items[selected_idx][:actions] unless current_items[selected_idx].key?(:actions_cached)
79
+ if current_items[selected_idx][:actions_cached]
78
80
  display_actions.merge!(
79
- current_items[selected_idx][:actions].to_h do |action_shortcut, action_info|
81
+ current_items[selected_idx][:actions_cached].to_h do |action_shortcut, action_info|
80
82
  [
81
83
  case action_shortcut
82
84
  when KEY_ENTER
@@ -127,9 +129,11 @@ class CursesMenu
127
129
  break
128
130
  else
129
131
  # Check actions
130
- if current_items[selected_idx][:actions]&.key?(user_choice)
132
+ # Keep a cache of actions as they can be loaded in a lazy way for performance
133
+ current_items[selected_idx][:actions_cached] = current_items[selected_idx][:actions].is_a?(Proc) ? current_items[selected_idx][:actions].call : current_items[selected_idx][:actions] unless current_items[selected_idx].key?(:actions_cached)
134
+ if current_items[selected_idx][:actions_cached]&.key?(user_choice)
131
135
  curses_menu_finalize
132
- result = current_items[selected_idx][:actions][user_choice][:execute].call
136
+ result = current_items[selected_idx][:actions_cached][user_choice][:execute].call
133
137
  if result.is_a?(Symbol)
134
138
  case result
135
139
  when :menu_exit
@@ -162,9 +166,10 @@ class CursesMenu
162
166
  #
163
167
  # Parameters::
164
168
  # * *title* (String, CursesRow or Proc): Text to be displayed for this item, or Proc returning this text when needed (lazy loading)
165
- # * *actions* (Hash<Object, Hash<Symbol,Object> >): Associated actions to this item, per shortcut [default: {}]
169
+ # * *actions* (Hash<Object, Hash<Symbol,Object> > or Proc): Associated actions to this item, per shortcut, or Proc returning those actions when needed (lazy loading) [default: {}]
166
170
  # * *name* (String): Name of this action (displayed at the bottom of the menu)
167
171
  # * *execute* (Proc): Code called when this action is selected
172
+ # In case of lazy loading (with a Proc), the title Proc will always be called first.
168
173
  # * *&action* (Proc): Code called if the item is selected (action for the enter key) [optional].
169
174
  # * Result::
170
175
  # * Symbol or Object: If the code returns a symbol, the menu will behave in a specific way:
@@ -172,8 +177,21 @@ class CursesMenu
172
177
  # * *menu_refresh*: The menu will compute again its items.
173
178
  def item(title, actions: {}, &action)
174
179
  menu_item_def = { title: title }
175
- all_actions = action.nil? ? actions : actions.merge(KEY_ENTER => { name: 'Select', execute: action })
176
- menu_item_def[:actions] = all_actions unless all_actions.empty?
180
+ all_actions =
181
+ if action.nil?
182
+ actions
183
+ else
184
+ mapped_default_action = { KEY_ENTER => { name: 'Select', execute: action } }
185
+ if actions.is_a?(Proc)
186
+ # Make sure we keep the lazyness
187
+ proc do
188
+ actions.call.merge(mapped_default_action)
189
+ end
190
+ else
191
+ actions.merge(mapped_default_action)
192
+ end
193
+ end
194
+ menu_item_def[:actions] = all_actions if all_actions.is_a?(Proc) || !all_actions.empty?
177
195
  @current_menu_items << menu_item_def
178
196
  end
179
197
 
@@ -2,7 +2,7 @@ describe CursesMenu do
2
2
 
3
3
  it 'displays a menu with 1 item with lazy rendering' do
4
4
  render_called = false
5
- test_menu(title: 'Menu title') do |menu|
5
+ test_menu do |menu|
6
6
  menu.item(proc do
7
7
  render_called = true
8
8
  'Menu item lazy'
@@ -14,7 +14,7 @@ describe CursesMenu do
14
14
 
15
15
  it 'displays a menu with 1 item in a CursesRow with lazy rendering' do
16
16
  render_called = false
17
- test_menu(title: 'Menu title') do |menu|
17
+ test_menu do |menu|
18
18
  menu.item(proc do
19
19
  render_called = true
20
20
  CursesMenu::CursesRow.new({ cell: { text: 'Menu item lazy' } })
@@ -24,10 +24,61 @@ describe CursesMenu do
24
24
  assert_line 3, 'Menu item lazy'
25
25
  end
26
26
 
27
+ it 'displays menu actions with lazy evaluation' do
28
+ render_called = false
29
+ action_executed = false
30
+ test_menu(keys: ['a']) do |menu|
31
+ menu.item('Menu item lazy', actions: proc do
32
+ render_called = true
33
+ {
34
+ 'a' => {
35
+ name: 'Lazy action',
36
+ execute: proc do
37
+ action_executed = true
38
+ end
39
+ }
40
+ }
41
+ end)
42
+ end
43
+ assert_line 3, 'Menu item lazy'
44
+ assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: Lazy action')
45
+ expect(render_called).to be true
46
+ expect(action_executed).to be true
47
+ end
48
+
49
+ it 'displays menu actions with lazy evaluation and a default action' do
50
+ render_called = false
51
+ action_executed = false
52
+ default_executed = false
53
+ test_menu(keys: ['a', CursesMenu::KEY_ENTER]) do |menu|
54
+ menu.item(
55
+ 'Menu item lazy',
56
+ actions: proc do
57
+ render_called = true
58
+ {
59
+ 'a' => {
60
+ name: 'Lazy action',
61
+ execute: proc do
62
+ action_executed = true
63
+ end
64
+ }
65
+ }
66
+ end
67
+ ) do
68
+ default_executed = true
69
+ end
70
+ end
71
+ assert_line 3, 'Menu item lazy'
72
+ assert_line(-1, '= Arrows/Home/End: Navigate | Enter: Select | Esc: Exit | a: Lazy action')
73
+ expect(render_called).to be true
74
+ expect(action_executed).to be true
75
+ expect(default_executed).to be true
76
+ end
77
+
27
78
  it 'doesn\'t lazy render when the item is not displayed' do
28
79
  nbr_visible_items = Curses.stdscr.maxy - 5
29
80
  render_called = false
30
- test_menu(title: 'Menu title') do |menu|
81
+ test_menu do |menu|
31
82
  (nbr_visible_items * 2).times do |idx|
32
83
  menu.item "Menu item #{idx}"
33
84
  end
@@ -39,6 +90,60 @@ describe CursesMenu do
39
90
  expect(render_called).to be false
40
91
  end
41
92
 
93
+ it 'doesn\'t lazy evaluate actions when the item is not displayed' do
94
+ nbr_visible_items = Curses.stdscr.maxy - 5
95
+ render_called = false
96
+ test_menu do |menu|
97
+ (nbr_visible_items * 2).times do |idx|
98
+ menu.item "Menu item #{idx}"
99
+ end
100
+ menu.item('Menu item lazy', actions: proc do
101
+ render_called = true
102
+ {
103
+ 'a' => {
104
+ name: 'Lazy action',
105
+ execute: proc {}
106
+ }
107
+ }
108
+ end)
109
+ end
110
+ expect(render_called).to be false
111
+ end
112
+
113
+ it 'doesn\'t lazy evaluate actions when the item is not selected' do
114
+ render_called = false
115
+ test_menu do |menu|
116
+ menu.item 'Menu item'
117
+ menu.item('Menu item lazy', actions: proc do
118
+ render_called = true
119
+ {
120
+ 'a' => {
121
+ name: 'Lazy action',
122
+ execute: proc {}
123
+ }
124
+ }
125
+ end)
126
+ end
127
+ expect(render_called).to be false
128
+ end
129
+
130
+ it 'lazy evaluates actions as soon as the item is selected' do
131
+ render_called = false
132
+ test_menu(keys: [Curses::KEY_DOWN]) do |menu|
133
+ menu.item 'Menu item'
134
+ menu.item('Menu item lazy', actions: proc do
135
+ render_called = true
136
+ {
137
+ 'a' => {
138
+ name: 'Lazy action',
139
+ execute: proc {}
140
+ }
141
+ }
142
+ end)
143
+ end
144
+ expect(render_called).to be true
145
+ end
146
+
42
147
  it 'keeps lazy rendered titles in a cache while navigating' do
43
148
  nbr_renders = 0
44
149
  test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_DOWN, Curses::KEY_DOWN]) do |menu|
@@ -54,6 +159,26 @@ describe CursesMenu do
54
159
  assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: Special action')
55
160
  end
56
161
 
162
+ it 'keeps lazy evaluated actions in a cache while navigating' do
163
+ nbr_renders = 0
164
+ test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_DOWN, Curses::KEY_DOWN]) do |menu|
165
+ menu.item 'Menu item 1'
166
+ menu.item('Menu item 2 lazy', actions: proc do
167
+ nbr_renders += 1
168
+ {
169
+ 'a' => {
170
+ name: 'Lazy action',
171
+ execute: proc {}
172
+ }
173
+ }
174
+ end)
175
+ menu.item 'Menu item 3'
176
+ menu.item 'Menu item 4', actions: { 'a' => { name: 'Special action', execute: proc {} } }
177
+ end
178
+ expect(nbr_renders).to eq 1
179
+ assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: Special action')
180
+ end
181
+
57
182
  it 'keeps lazy rendered titles in a cache while navigating across pages' do
58
183
  nbr_renders = 0
59
184
  nbr_visible_items = Curses.stdscr.maxy - 5
@@ -70,6 +195,27 @@ describe CursesMenu do
70
195
  assert_line(-3, 'Menu item Lazy')
71
196
  end
72
197
 
198
+ it 'keeps lazy evaluated actions in a cache while navigating across pages' do
199
+ nbr_renders = 0
200
+ nbr_visible_items = Curses.stdscr.maxy - 5
201
+ test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_DOWN, Curses::KEY_END, Curses::KEY_HOME, Curses::KEY_END, Curses::KEY_HOME, Curses::KEY_END]) do |menu|
202
+ (nbr_visible_items * 2).times do |idx|
203
+ menu.item "Menu item #{idx}"
204
+ end
205
+ menu.item('Menu item lazy', actions: proc do
206
+ nbr_renders += 1
207
+ {
208
+ 'a' => {
209
+ name: 'Lazy action',
210
+ execute: proc {}
211
+ }
212
+ }
213
+ end)
214
+ end
215
+ expect(nbr_renders).to eq 1
216
+ assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit | a: Lazy action')
217
+ end
218
+
73
219
  it 'refreshes lazy rendered titles between menu refreshes' do
74
220
  nbr_renders = 0
75
221
  test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_UP, CursesMenu::KEY_ENTER, Curses::KEY_DOWN, Curses::KEY_UP]) do |menu|
@@ -84,6 +230,25 @@ describe CursesMenu do
84
230
  expect(nbr_renders).to eq 2
85
231
  end
86
232
 
233
+ it 'refreshes lazy evaluated actions between menu refreshes' do
234
+ nbr_renders = 0
235
+ test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_UP, CursesMenu::KEY_ENTER, Curses::KEY_DOWN, Curses::KEY_UP]) do |menu|
236
+ menu.item 'Menu item Refresh' do
237
+ :menu_refresh
238
+ end
239
+ menu.item('Menu item lazy', actions: proc do
240
+ nbr_renders += 1
241
+ {
242
+ 'a' => {
243
+ name: 'Lazy action',
244
+ execute: proc {}
245
+ }
246
+ }
247
+ end)
248
+ end
249
+ expect(nbr_renders).to eq 2
250
+ end
251
+
87
252
  it 'does not refresh lazy rendered titles when executing actions' do
88
253
  nbr_renders = 0
89
254
  test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_UP, CursesMenu::KEY_ENTER, Curses::KEY_DOWN, Curses::KEY_UP]) do |menu|
@@ -98,6 +263,25 @@ describe CursesMenu do
98
263
  expect(nbr_renders).to eq 1
99
264
  end
100
265
 
266
+ it 'does not refresh lazy evaluated actions when executing actions' do
267
+ nbr_renders = 0
268
+ test_menu(keys: [Curses::KEY_DOWN, Curses::KEY_UP, CursesMenu::KEY_ENTER, Curses::KEY_DOWN, Curses::KEY_UP]) do |menu|
269
+ menu.item 'Menu item' do
270
+ # Do nothing
271
+ end
272
+ menu.item('Menu item lazy', actions: proc do
273
+ nbr_renders += 1
274
+ {
275
+ 'a' => {
276
+ name: 'Lazy action',
277
+ execute: proc {}
278
+ }
279
+ }
280
+ end)
281
+ end
282
+ expect(nbr_renders).to eq 1
283
+ end
284
+
101
285
  it 'does not refresh lazy rendered titles when getting into sub-menus' do
102
286
  nbr_renders = 0
103
287
  test_menu(
@@ -124,4 +308,76 @@ describe CursesMenu do
124
308
  expect(nbr_renders).to eq 1
125
309
  end
126
310
 
311
+ it 'does not refresh lazy evaluated actions when getting into sub-menus' do
312
+ nbr_renders = 0
313
+ test_menu(
314
+ keys: [
315
+ # Enter sub-menu
316
+ CursesMenu::KEY_ENTER,
317
+ Curses::KEY_DOWN,
318
+ # Back to first menu
319
+ CursesMenu::KEY_ESCAPE,
320
+ Curses::KEY_DOWN
321
+ ]
322
+ ) do |menu, key_presses|
323
+ menu.item 'Sub-menu' do
324
+ described_class.new('Sub-menu title', key_presses: key_presses) do |sub_menu|
325
+ sub_menu.item 'Sub-menu item 1'
326
+ sub_menu.item 'Sub-menu item 2'
327
+ end
328
+ end
329
+ menu.item('Menu item lazy', actions: proc do
330
+ nbr_renders += 1
331
+ {
332
+ 'a' => {
333
+ name: 'Lazy action',
334
+ execute: proc {}
335
+ }
336
+ }
337
+ end)
338
+ end
339
+ expect(nbr_renders).to eq 1
340
+ end
341
+
342
+ it 'always lazy renders titles before lazy evaluating actions' do
343
+ lazy_renders = []
344
+ nbr_visible_items = Curses.stdscr.maxy - 5
345
+ test_menu(keys: [Curses::KEY_END, Curses::KEY_HOME, Curses::KEY_DOWN]) do |menu|
346
+ menu.item(
347
+ proc do
348
+ lazy_renders << :item_1_title
349
+ 'Menu item 1 lazy'
350
+ end,
351
+ actions: proc do
352
+ lazy_renders << :item_1_action
353
+ { 'a' => { name: 'Lazy action 1', execute: proc {} } }
354
+ end
355
+ )
356
+ menu.item(
357
+ proc do
358
+ lazy_renders << :item_2_title
359
+ 'Menu item 2 lazy'
360
+ end,
361
+ actions: proc do
362
+ lazy_renders << :item_2_action
363
+ { 'a' => { name: 'Lazy action 2', execute: proc {} } }
364
+ end
365
+ )
366
+ (nbr_visible_items * 2).times do |idx|
367
+ menu.item "Menu item #{idx}"
368
+ end
369
+ menu.item(
370
+ proc do
371
+ lazy_renders << :item_3_title
372
+ 'Menu item 3 lazy'
373
+ end,
374
+ actions: proc do
375
+ lazy_renders << :item_3_action
376
+ { 'a' => { name: 'Lazy action 3', execute: proc {} } }
377
+ end
378
+ )
379
+ end
380
+ expect(lazy_renders).to eq %i[item_1_title item_2_title item_1_action item_3_title item_3_action item_2_action]
381
+ end
382
+
127
383
  end
@@ -9,6 +9,16 @@ describe CursesMenu do
9
9
  assert_line(-1, '= Arrows/Home/End: Navigate | Esc: Exit')
10
10
  end
11
11
 
12
+ it 'displays a menu with 1 item having a default action' do
13
+ test_menu(title: 'Menu title') do |menu|
14
+ menu.item 'Menu item' do
15
+ # Do nothing
16
+ end
17
+ end
18
+ assert_line 3, 'Menu item'
19
+ assert_line(-1, '= Arrows/Home/End: Navigate | Enter: Select | Esc: Exit')
20
+ end
21
+
12
22
  it 'displays a menu with several items' do
13
23
  test_menu do |menu|
14
24
  menu.item 'Menu item 1'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curses_menu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-12 00:00:00.000000000 Z
11
+ date: 2022-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: curses