rubytext 0.1.21 → 0.1.25

Sign up to get free protection for your applications and to get access to all the features.
data/lib/menu.rb CHANGED
@@ -1,22 +1,333 @@
1
+ #### FIXME LATER
2
+
3
+ # The top-level module
4
+
1
5
  module RubyText
2
6
 
7
+ # Wrapper for a curses window
8
+
3
9
  class Window
4
- def topmenu(items:, curr: 0,
5
- title: nil, fg: Green, bg: Black)
6
- r, c = 0, 0
7
- border = false
8
- high = 1
9
10
 
11
+ class Menu2D
12
+
13
+ class Vertical
14
+ attr_reader :widest, :height, :header, :hash
15
+ def initialize(vlist)
16
+ @header = vlist[0]
17
+ @hash = vlist[1]
18
+ @widest = @header.length
19
+ @hash.each_pair {|k,v| puts "k = #{k.inspect}"; getch; @widest = [@widest, k.length].max }
20
+ @height = @hash.size
21
+ end
22
+ end
23
+
24
+ def initialize(win:, r: :center, c: :center, items:, colrow: [0, 0],
25
+ border: true, title: nil, fg: Green, bg: Black)
26
+ @win = win
27
+ @list = []
28
+ @header = @list.map {|x| x.header }
29
+ items.each {|vlist| @list << Vertical.new(vlist) }
30
+ @highest = @list.map {|x| x.height }.max
31
+ @full_width = @list.inject(0) {|sum, vlist| sum += vlist.widest + 2 }
32
+ @nlists = items.size
33
+ @grid = Array.new(@nlists) # column major order
34
+ @grid.map! {|x| [" "] * @highest }
35
+ @list.each.with_index do |vlist, i|
36
+ vlist.hash.each_pair.with_index do |kv, j|
37
+ k, v = kv
38
+ @grid[i][j] = [k, v]
39
+ end
40
+ end
41
+ RubyText.hide_cursor
42
+ @high = @highest
43
+ @wide = @full_width
44
+ @high += 2 if border
45
+ @wide += 2 if border
46
+
47
+ tlen = title.length + 8 rescue 0
48
+ # wide = [wide, tlen].max
49
+ row, col = @win.coords(r, c)
50
+ row = row - @high/2 if r == :center
51
+ col = col - @wide/2 if c == :center
52
+ r, c = row, col
53
+ @win.saveback(@high+1, @wide, r, c)
54
+ mr, mc = r+@win.r0, c+@win.c0
55
+ title = nil unless border
56
+
57
+ @mwin = RubyText.window(@high+1, @wide, r: mr, c: mc, border: true,
58
+ fg: fg, bg: bg, title: title)
59
+ @header.each {|head| printf "%-#{maxw}s", head }
60
+ puts # after header
61
+ Curses.stdscr.keypad(true)
62
+ maxcol = items.size - 1
63
+ sizes = items.map {|x| x.size }
64
+ max = sizes.max
65
+ # mwin.go(r, c)
66
+ r += 1 # account for header
67
+ @selc, @selr = colrow
68
+ end
69
+
70
+ def show(r, c, colrow: [0, 0])
71
+ @selc, @selr = colrow
72
+ @grid.each.with_index do |column, cix|
73
+ column.each.with_index do |pairs, rix| # {Jan: ..., Feb: ..., Mar: ..., ...}
74
+ # STDSCR.puts "go: #{r}+#{rix}, #{c}+#{cix}*#{maxw}"
75
+ @mwin.go(rix, cix) # FIXME wrong?
76
+ style = ([@selc, @selr] == [cix, rix]) ? :reverse : :normal
77
+ key, val = pairs
78
+ label = key.to_s
79
+ @mwin.print label # fx(label, style)
80
+ end
81
+ end
82
+ end
83
+
84
+ def handle(r, c)
85
+ loop do
86
+ show(r, c)
87
+ ch = getch
88
+ case ch
89
+ when RubyText::Window::Up
90
+ @selr -= 1 if @selr > 0
91
+ when RubyText::Window::Down
92
+ # puts "PAUSE r,c = #@selr #@selc highest=#@highest"; getch
93
+ @selr += 1 if @selr < @highest - 1
94
+ when RubyText::Window::Left
95
+ @selc -= 1 if @selc > 0
96
+ when RubyText::Window::Right
97
+ @selc += 1 if @selc < @full_width
98
+ when RubyText::Window::Esc
99
+ @win.restback(@high+1, @wide, r-1, c)
100
+ RubyText.show_cursor
101
+ return [nil, nil, nil]
102
+ when RubyText::Window::Enter
103
+ @win.restback(@high+1, @wide, r-1, c)
104
+ RubyText.show_cursor
105
+ choice = @grid[@selc][@selr][1]
106
+ case choice
107
+ when String;
108
+ puts "Returning #{[@selc, @selr, choice].inspect}"; getch
109
+ return [@selc, @selr, choice]
110
+ when NilClass; return [nil, nil, nil]
111
+ end
112
+ result = choice.call # should be a Proc
113
+ return [nil, nil, nil] if result.nil? || result.empty?
114
+ return result
115
+ else Curses.beep
116
+ end
117
+ end
118
+ RubyText.show_cursor
119
+ end
120
+ end
121
+
122
+
123
+ def rectmenu(r: :center, c: :center, items:, colrow: [0, 0],
124
+ border: true,
125
+ title: nil, fg: Green, bg: Black)
10
126
  RubyText.hide_cursor
127
+ maxh, maxw = _rectmenu_maxes(items)
128
+ header, stuff = _rect_hash2array(items, maxh, maxw)
129
+ wide = items.size * maxw
130
+ high = maxh
131
+ high += 2 if border
132
+ wide += 2 if border
133
+
134
+ tlen = title.length + 8 rescue 0
135
+ # wide = [wide, tlen].max
136
+ row, col = @win.coords(r, c)
137
+ row = row - high/2 if r == :center
138
+ col = col - wide/2 if c == :center
139
+ r, c = row, col
140
+ @win.saveback(high+1, wide, r, c)
141
+ mr, mc = r+@win.r0, c+@win.c0
142
+ title = nil unless border
143
+
144
+ mwin = RubyText.window(high+1, wide, r: mr, c: mc, border: true,
145
+ fg: fg, bg: bg, title: title)
146
+ header.each {|head| printf "%-#{maxw}s", head }
147
+ puts # after header
148
+ Curses.stdscr.keypad(true)
149
+ maxcol = items.size - 1
150
+ sizes = items.map {|x| x.size }
151
+ max = sizes.max
152
+ # mwin.go(r, c)
153
+ r += 1 # account for header
154
+ selc, selr = colrow
155
+
156
+ loop do
157
+ RubyText.hide_cursor # FIXME should be unnecessary
158
+ stuff.each.with_index do |column, cix|
159
+ column.each.with_index do |pairs, rix| # {Jan: ..., Feb: ..., Mar: ..., ...}
160
+ STDSCR.puts "go: #{r}+#{rix}, #{c}+#{cix}*#{maxw}"
161
+ mwin.go(rix+1, cix*maxw)
162
+ style = ([selc, selr] == [cix, rix]) ? :reverse : :normal
163
+ key, val = pairs
164
+ label = key.to_s
165
+ # mwin.print fx(label, style)
166
+ mwin.print label # fx(label, style)
167
+ end
168
+ end
169
+ ch = getch
170
+ case ch
171
+ when Up
172
+ selr -= 1 if selr > 0
173
+ when Down
174
+ selr += 1 if selr < maxh - 1
175
+ when Left
176
+ selc -= 1 if selc > 0
177
+ when Right
178
+ selc += 1 if selc < maxcol
179
+ when Esc
180
+ self.restback(high+1, wide, r-1, c)
181
+ RubyText.show_cursor
182
+ return [nil, nil, nil]
183
+ when Enter
184
+ self.restback(high+1, wide, r-1, c)
185
+ RubyText.show_cursor
186
+ choice = stuff[selc][selr][1]
187
+ case choice
188
+ when String; return [selc, selr, choice]
189
+ when NilClass; return [nil, nil, nil]
190
+ end
191
+ result = choice.call # should be a Proc
192
+ return [nil, nil, nil] if result.nil? || result.empty?
193
+ return result
194
+ else Curses.beep
195
+ end
196
+ RubyText.show_cursor
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ module RubyText
203
+
204
+ # Two-paned widget with menu on left, informtional area on right
205
+
206
+ def self.selector(win: STDSCR, r: 0, c: 0, rows: 10, cols: 20,
207
+ items:, fg: White, bg: Blue,
208
+ win2:, callback:, enter: nil, quit: "q")
209
+ high = rows
210
+ wide = cols
211
+ mwin = RubyText.window(high, wide, r: r, c: c, fg: fg, bg: bg)
212
+ handler = callback
213
+ Curses.stdscr.keypad(true)
214
+ RubyText.hide_cursor
215
+ sel = 0
216
+ max = items.size - 1
217
+ handler.call(sel, items[sel], win2)
218
+ loop do
219
+ mwin.home
220
+ items.each.with_index do |item, row|
221
+ mwin.crlf
222
+ style = (sel == row) ? :reverse : :normal
223
+ mwin.print fx(" #{item}", style)
224
+ end
225
+ ch = getch
226
+ case ch
227
+ when Up
228
+ if sel > 0
229
+ sel -= 1
230
+ handler.call(sel, items[sel], win2)
231
+ end
232
+ when Down
233
+ if sel < max
234
+ sel += 1
235
+ handler.call(sel, items[sel], win2)
236
+ end
237
+ when Enter
238
+ if enter
239
+ del = enter.call(sel, items[sel], win2)
240
+ if del
241
+ items -= [items[sel]]
242
+ raise
243
+ end
244
+ end
245
+ when Tab
246
+ Curses.flash
247
+ when quit # parameter
248
+ exit
249
+ else Curses.beep # all else is trash
250
+ end
251
+ end
252
+ rescue
253
+ retry
254
+ end
255
+
256
+ # "Menu" for checklists
257
+
258
+ def checklist(r: :center, c: :center,
259
+ items:, curr: 0, selected: [],
260
+ title: nil, sel_fg: Yellow, fg: White, bg: Blue)
261
+ RubyText.hide_cursor
262
+ high = items.size + 2
263
+ wide = items.map(&:length).max + 8
264
+ tlen = title.length + 8 rescue 0
265
+ wide = [wide, tlen].max
266
+ row, col = self.coords(r, c)
267
+ row = row - high/2 if r == :center
268
+ col = col - wide/2 if c == :center
269
+ r, c = row, col
270
+ self.saveback(high, wide, r, c)
271
+ mr, mc = r+self.r0, c+self.c0
272
+ mwin = RubyText.window(high, wide, r: mr, c: mc,
273
+ fg: fg, bg: bg, title: title)
274
+ Curses.stdscr.keypad(true)
275
+ sel = curr
276
+ max = items.size - 1
277
+ loop do
278
+ RubyText.hide_cursor # FIXME should be unnecessary
279
+ items.each.with_index do |item, row|
280
+ mwin.go row, 0
281
+ style = (sel == row) ? :reverse : :normal
282
+ color = selected.find {|x| x[0] == row } ? sel_fg : fg
283
+ label = "[ ]" + item
284
+ mwin.print fx(label, color, style)
285
+ end
286
+ ch = getch
287
+ case ch
288
+ when Up
289
+ sel -= 1 if sel > 0
290
+ when Down
291
+ sel += 1 if sel < max
292
+ when Esc
293
+ self.restback(high, wide, r, c)
294
+ RubyText.show_cursor
295
+ return []
296
+ when Enter
297
+ self.restback(high, wide, r, c)
298
+ RubyText.show_cursor
299
+ return selected.map {|i| items[i] }
300
+ when " "
301
+ selected << [sel, items[sel]]
302
+ sel += 1 if sel < max
303
+ else Curses.beep
304
+ end
305
+ RubyText.show_cursor
306
+ end
307
+ end
308
+
309
+ end
310
+
311
+ # The top-level module
312
+
313
+ module RubyText
314
+
315
+ # Wrapper for a curses window
316
+
317
+ class Window
318
+
319
+ # One-line menu at top of window
320
+
321
+ def topmenu(items:, curr: 0, fg: Green, bg: Black)
322
+ r, c, high = 0, 0, 1
323
+ RubyText.hide_cursor
324
+ hash_flag = false
325
+ results = items
11
326
  if items.is_a?(Hash)
12
- results = items.values
13
- items = items.keys
327
+ results, items = items.values, items.keys
14
328
  hash_flag = true
15
- else
16
- results = items
17
329
  end
18
330
 
19
- tlen = title.length + 8 rescue 0
20
331
  width = 0 # total width
21
332
  cols = [] # start-column of each item
22
333
  items.each do |item|
@@ -26,13 +337,9 @@ module RubyText
26
337
  end
27
338
 
28
339
  r, c = self.coords(r, c)
29
- # puts "topmenu saved"
30
- # sleep 2
31
340
  self.saveback(high, width, r, c)
32
341
  mr, mc = r+self.r0, c+self.c0
33
- title = nil
34
- mwin = RubyText.window(high, width, r: mr, c: mc, border: border,
35
- fg: fg, bg: bg, title: title)
342
+ mwin = RubyText.window(high, width, r: mr, c: mc, fg: fg, bg: bg, border: false, title: nil)
36
343
  Curses.stdscr.keypad(true)
37
344
  sel = curr
38
345
  max = items.size - 1
@@ -45,34 +352,37 @@ module RubyText
45
352
  end
46
353
  ch = getch
47
354
  case ch
48
- when Curses::KEY_LEFT
355
+ when Left
49
356
  sel -= 1 if sel > 0
50
- when Curses::KEY_RIGHT
357
+ when Right
51
358
  sel += 1 if sel < max
52
- when 27
359
+ when Esc, " " # spacebar also quits
53
360
  self.restback(high, width, r, c)
54
361
  RubyText.show_cursor
362
+ STDSCR.go r, c
55
363
  return [nil, nil]
56
- when 10
364
+ when Down, Enter
57
365
  self.restback(high, width, r, c)
58
- # puts "topmenu restored"
59
- # sleep 2
60
366
  RubyText.show_cursor
367
+ STDSCR.go r, c
61
368
  choice = results[sel]
62
369
  return [sel, choice] if choice.is_a? String
63
370
  result = choice.call
64
- next if result.nil?
65
- next if result.empty?
66
- return result
67
- else Curses.beep
371
+ return [nil, nil, nil] if result.nil? || result.empty?
372
+ # next if result.nil?
373
+ # next if result.empty?
374
+ # return result
375
+ else Curses.beep
68
376
  end
69
377
  RubyText.show_cursor
70
378
  end
71
379
  end
72
380
 
381
+ # Simple menu with rows of strings (or Procs)
382
+
73
383
  def menu(r: :center, c: :center, items:, curr: 0,
74
- border: true,
75
- title: nil, fg: Green, bg: Black)
384
+ border: true, sticky: false,
385
+ title: nil, fg: Green, bg: Black, wrap: false)
76
386
  RubyText.hide_cursor
77
387
  if items.is_a?(Hash)
78
388
  results = items.values
@@ -93,8 +403,6 @@ module RubyText
93
403
  row = row - high/2 if r == :center
94
404
  col = col - wide/2 if c == :center
95
405
  r, c = row, col
96
- # puts "menu2 saved"
97
- # sleep 2
98
406
  self.saveback(high, wide, r, c)
99
407
  mr, mc = r+self.r0, c+self.c0
100
408
  title = nil unless border
@@ -113,16 +421,24 @@ module RubyText
113
421
  end
114
422
  ch = getch
115
423
  case ch
116
- when Curses::KEY_UP
117
- sel -= 1 if sel > 0
118
- when Curses::KEY_DOWN
119
- sel += 1 if sel < max
120
- when 27
424
+ when Up
425
+ if sel > 0
426
+ sel -= 1
427
+ else
428
+ sel = max if wrap # asteroids mode :)
429
+ end
430
+ when Down, " " # let space mean down?
431
+ if sel < max
432
+ sel += 1
433
+ else
434
+ sel = 0 if wrap # asteroids mode :)
435
+ end
436
+ when Esc
121
437
  self.restback(high, wide, r, c)
122
438
  RubyText.show_cursor
123
439
  return [nil, nil]
124
- when 10
125
- self.restback(high, wide, r, c)
440
+ when Enter
441
+ self.restback(high, wide, r, c) unless sticky
126
442
  RubyText.show_cursor
127
443
  choice = results[sel]
128
444
  return [sel, choice] if choice.is_a? String
@@ -135,6 +451,8 @@ module RubyText
135
451
  end
136
452
  end
137
453
 
454
+ # Menu for multiple selections (buggy/unused?)
455
+
138
456
  def multimenu(r: :center, c: :center,
139
457
  items:, curr: 0, selected: [],
140
458
  title: nil, sel_fg: Yellow, fg: White, bg: Blue)
@@ -165,15 +483,15 @@ module RubyText
165
483
  end
166
484
  ch = getch
167
485
  case ch
168
- when Curses::KEY_UP
486
+ when Up
169
487
  sel -= 1 if sel > 0
170
- when Curses::KEY_DOWN
488
+ when Down
171
489
  sel += 1 if sel < max
172
- when 27
490
+ when Esc
173
491
  self.restback(high, wide, r, c)
174
492
  RubyText.show_cursor
175
493
  return []
176
- when 10
494
+ when Enter
177
495
  self.restback(high, wide, r, c)
178
496
  RubyText.show_cursor
179
497
  return selected.map {|i| items[i] }
@@ -186,13 +504,91 @@ module RubyText
186
504
  end
187
505
  end
188
506
 
507
+ # Simple yes/no decision
508
+
189
509
  def yesno
190
510
  # TODO: Accept YyNn
191
511
  r, c = STDSCR.rc
192
512
  num, str = STDSCR.menu(r: r, c: c+6, items: ["yes", "no"])
193
513
  num == 0
194
514
  end
515
+
516
+ # Menu to choose a single setting and retain it
517
+
518
+ def radio_menu(r: :center, c: :center, items:, curr: 0,
519
+ # Handle current value better?
520
+ border: true,
521
+ title: nil, fg: Green, bg: Black)
522
+ RubyText.hide_cursor
523
+ if items.is_a?(Hash)
524
+ results = items.values
525
+ items = items.keys
526
+ hash_flag = true
527
+ else
528
+ results = items
529
+ end
530
+
531
+ high = items.size
532
+ wide = items.map(&:length).max + 3
533
+ high += 2 if border
534
+ wide += 2 if border
535
+
536
+ tlen = title.length + 8 rescue 0
537
+ wide = [wide, tlen].max
538
+ row, col = self.coords(r, c)
539
+ row = row - high/2 if r == :center
540
+ col = col - wide/2 if c == :center
541
+ r, c = row, col
542
+ self.saveback(high, wide, r, c)
543
+ mr, mc = r+self.r0, c+self.c0
544
+ title = nil unless border
545
+ mwin = RubyText.window(high, wide, r: mr, c: mc, border: border,
546
+ fg: fg, bg: bg, title: title)
547
+ Curses.stdscr.keypad(true)
548
+ sel = curr
549
+ max = items.size - 1
550
+ loop do
551
+ RubyText.hide_cursor # FIXME should be unnecessary
552
+ items.each.with_index do |item, row|
553
+ mark = row == curr ? ">" : " "
554
+ mwin.go row, 0
555
+ style = (sel == row) ? :reverse : :normal
556
+ label = "#{mark} #{item}"
557
+ mwin.print fx(label, style)
558
+ end
559
+ ch = getch
560
+ case ch
561
+ when Up
562
+ sel -= 1 if sel > 0
563
+ when Down
564
+ sel += 1 if sel < max
565
+ when Esc
566
+ self.restback(high, wide, r, c)
567
+ RubyText.show_cursor
568
+ return [nil, nil]
569
+ when " "
570
+ mwin[curr, 0] = " "
571
+ mwin[sel, 0] = ">"
572
+ curr = sel
573
+ when Enter
574
+ self.restback(high, wide, r, c)
575
+ RubyText.show_cursor
576
+ choice = results[sel]
577
+ return [sel, choice] if choice.is_a? String
578
+ result = choice.call
579
+ return [nil, nil] if result.nil? || result.empty?
580
+ return result
581
+ else Curses.beep
582
+ end
583
+ RubyText.show_cursor
584
+ end
585
+ end
195
586
  end
587
+ end
588
+
589
+ module RubyText
590
+
591
+ # Two-paned widget with menu on left, informtional area on right
196
592
 
197
593
  def self.selector(win: STDSCR, r: 0, c: 0, rows: 10, cols: 20,
198
594
  items:, fg: White, bg: Blue,
@@ -215,17 +611,17 @@ module RubyText
215
611
  end
216
612
  ch = getch
217
613
  case ch
218
- when Curses::KEY_UP
614
+ when Up
219
615
  if sel > 0
220
616
  sel -= 1
221
617
  handler.call(sel, items[sel], win2)
222
618
  end
223
- when Curses::KEY_DOWN
619
+ when Down
224
620
  if sel < max
225
621
  sel += 1
226
622
  handler.call(sel, items[sel], win2)
227
623
  end
228
- when 10 # Enter
624
+ when Enter
229
625
  if enter
230
626
  del = enter.call(sel, items[sel], win2)
231
627
  if del
@@ -233,7 +629,7 @@ module RubyText
233
629
  raise
234
630
  end
235
631
  end
236
- when 9 # tab
632
+ when Tab
237
633
  Curses.flash
238
634
  when quit # parameter
239
635
  exit
@@ -243,5 +639,60 @@ module RubyText
243
639
  rescue
244
640
  retry
245
641
  end
642
+
643
+ # "Menu" for checklists
644
+
645
+ def checklist(r: :center, c: :center,
646
+ items:, curr: 0, selected: [],
647
+ title: nil, sel_fg: Yellow, fg: White, bg: Blue)
648
+ RubyText.hide_cursor
649
+ high = items.size + 2
650
+ wide = items.map(&:length).max + 8
651
+ tlen = title.length + 8 rescue 0
652
+ wide = [wide, tlen].max
653
+ row, col = self.coords(r, c)
654
+ row = row - high/2 if r == :center
655
+ col = col - wide/2 if c == :center
656
+ r, c = row, col
657
+ self.saveback(high, wide, r, c)
658
+ mr, mc = r+self.r0, c+self.c0
659
+ mwin = RubyText.window(high, wide, r: mr, c: mc,
660
+ fg: fg, bg: bg, title: title)
661
+ Curses.stdscr.keypad(true)
662
+ sel = curr
663
+ max = items.size - 1
664
+ loop do
665
+ RubyText.hide_cursor # FIXME should be unnecessary
666
+ items.each.with_index do |item, row|
667
+ mwin.go row, 0
668
+ style = (sel == row) ? :reverse : :normal
669
+ color = selected.find {|x| x[0] == row } ? sel_fg : fg
670
+ label = "[ ]" + item
671
+ mwin.print fx(label, color, style)
672
+ end
673
+ ch = getch
674
+ case ch
675
+ when Up
676
+ sel -= 1 if sel > 0
677
+ when Down
678
+ sel += 1 if sel < max
679
+ when Esc
680
+ self.restback(high, wide, r, c)
681
+ RubyText.show_cursor
682
+ return []
683
+ when Enter
684
+ self.restback(high, wide, r, c)
685
+ RubyText.show_cursor
686
+ return selected.map {|i| items[i] }
687
+ when " "
688
+ selected << [sel, items[sel]]
689
+ sel += 1 if sel < max
690
+ else Curses.beep
691
+ end
692
+ RubyText.show_cursor
693
+ end
694
+ end
695
+
246
696
  end
247
697
 
698
+