rubytext 0.1.21 → 0.1.25

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.
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
+