cless 0.3.20

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.
@@ -0,0 +1,639 @@
1
+ class Array
2
+ def max_update(a)
3
+ a.each_with_index { |x, i|
4
+ self[i] = x if !(v = self[i]) || v < x
5
+ }
6
+ end
7
+ end
8
+
9
+ class Attr
10
+ NAME2COLORS = {
11
+ "none" => -1,
12
+ "black" => Ncurses::COLOR_BLACK,
13
+ "red" => Ncurses::COLOR_RED,
14
+ "green" => Ncurses::COLOR_GREEN,
15
+ "yellow" => Ncurses::COLOR_YELLOW,
16
+ "blue" => Ncurses::COLOR_BLUE,
17
+ "magenta" => Ncurses::COLOR_MAGENTA,
18
+ "white" => Ncurses::COLOR_WHITE,
19
+ }
20
+ COLORS = NAME2COLORS.values
21
+ NAME2ATTR = {
22
+ "normal" => Ncurses::A_NORMAL,
23
+ "standout" => Ncurses::A_STANDOUT,
24
+ "underline" => Ncurses::A_UNDERLINE,
25
+ "dim" => Ncurses::A_DIM,
26
+ "bold" => Ncurses::A_BOLD,
27
+ }
28
+ ATTRS = NAME2ATTR.values
29
+
30
+ DEFAULTS = {
31
+ :background => NAME2COLORS["none"],
32
+ :foreground => NAME2COLORS["none"],
33
+ :attribute => NAME2ATTR["bold"],
34
+ }
35
+
36
+ def initialize(args = {}) # background, foreground, attribute
37
+ # Sanitize
38
+ DEFAULTS.each { |k, v|
39
+ instance_variable_set("@#{k}", args[k].nil? ? v : args[k])
40
+ }
41
+ @background = check_color(@background)
42
+ @foreground = check_color(@foreground)
43
+ @attribute = check_attribute(@attribute)
44
+ update_pair
45
+ end
46
+
47
+ def next_background; @background = inc(@background, COLORS); update_pair; end
48
+ def next_foreground; @foreground = inc(@foreground, COLORS); update_pair; end
49
+ def next_attribute; @attribute = inc(@attribute, ATTRS); end
50
+
51
+ def set; Ncurses.attrset(@attribute | @pair); end
52
+ def reset; Ncurses.attrset(Ncurses::A_NORMAL); end
53
+
54
+ def on; Ncurses.attron(@attribute | @pair); end
55
+ def off; Ncurses.attroff(@attribute | @pair); end
56
+
57
+ def names
58
+ r = {}
59
+ r[:foreground] = (NAME2COLORS.find { |n, v| v == @foreground } || ["black"])[0]
60
+ r[:background] = (NAME2COLORS.find { |n, v| v == @background } || ["white"])[0]
61
+ r[:attribute] = (NAME2ATTR.find { |n, v| v == @attribute } || ["normal"])[0]
62
+ r
63
+ end
64
+
65
+ private
66
+ def check(c, hash, ary)
67
+ case c
68
+ when Integer
69
+ ary.include?(c) ? c : ary.first
70
+ when String
71
+ (v = hash[c.downcase.strip]) ? v : ary.first
72
+ else
73
+ ary.first
74
+ end
75
+ end
76
+ def check_color(c); check(c, NAME2COLORS, COLORS); end
77
+ def check_attribute(a); check(a, NAME2ATTR, ATTRS); end
78
+
79
+ def inc(c, ary); ary[((ary.index(c) || 0)+1) % ary.size]; end
80
+
81
+ def update_pair
82
+ Ncurses.use_default_colors
83
+ Ncurses.init_pair(1, @foreground, @background)
84
+ @pair = Ncurses.COLOR_PAIR(1)
85
+ end
86
+ end
87
+
88
+ class Curses
89
+ ((?a.ord)..(?z.ord)).each { |c|
90
+ const_set("CTRL_#{c.chr.upcase}", c - ?a.ord + 1)
91
+ }
92
+ ESC = ?\e.ord
93
+ SPACE = " "[0].ord # [0] useless for 1.9 but necessary for 1.8
94
+
95
+ def initialize(args = {})
96
+ Ncurses.initscr
97
+ @started = true
98
+ begin
99
+ Ncurses.start_color
100
+ Ncurses.cbreak
101
+ Ncurses.noecho
102
+ Ncurses.nonl
103
+ Ncurses.stdscr.intrflush(false)
104
+ Ncurses.stdscr.immedok(false)
105
+ Ncurses.keypad(Ncurses.stdscr, true)
106
+
107
+
108
+ yield self
109
+ ensure
110
+ @started && Ncurses.endwin
111
+ end
112
+ end
113
+ end
114
+
115
+ class LineDisplay
116
+ ISNUM = /^[+-]?[\d,]*\.?\d*(?:[eE][+-]?\d+)?$/
117
+ DEFAULTS = {
118
+ :line_highlight => true, # Wether to hilight every other line
119
+ :line_highlight_period => 2,
120
+ :line_highlight_shift => 0,
121
+ :col_highlight => true, # Wether to hilight every other column
122
+ :col_highlight_period => 2,
123
+ :col_highlight_shift => 1,
124
+ :column => false, # Wether to display column number
125
+ :col_start => 1, # 1-based column numbering by default
126
+ :col_width => 50, # Maximum column width
127
+ :line => false, # Wether to display line number
128
+ :line_offset => false, # Display line offset instead of number
129
+ :col_names => false, # Wether to display column names
130
+ :col_space => 1, # Width of separator between columns
131
+ :separator => " ", # Separator caracter
132
+ :padding => " ", # Padding caracter
133
+ :right_align_re => ISNUM, # Matching columns are right align by default
134
+ :hide_ignore => false # Hide lines that are ignored
135
+ }
136
+ attr_accessor *DEFAULTS.keys
137
+ attr_accessor :col_offsets, :widths
138
+ attr_reader :prompt_line, :sizes
139
+
140
+
141
+ def separator=(s)
142
+ @separator = (!s || s.empty?) ? " " : s
143
+ end
144
+
145
+ def padding=(s)
146
+ @padding = (!s || s.empty?) ? " " : s
147
+ end
148
+
149
+ attr_accessor :col_headers, :hide_ignored
150
+ def initialize(data, args = {})
151
+ DEFAULTS.each { |k, v|
152
+ self.send("#{k}=", args[k].nil? ? v : args[k])
153
+ }
154
+ @data = data
155
+ @col_hide = []
156
+ @align = [] # column alignment: nil (i.e. auto), :left, :right, :center
157
+ @widths = [] # max column widths
158
+ @col_offsets = [] # offsets for large columns
159
+ @col_headers = nil # Actual names
160
+ @col_off = 0
161
+ @st_col = 0
162
+ @args = args
163
+ end
164
+
165
+ def initialize_curses
166
+ @attr = Attr.new(@args)
167
+ @col_names &= @col_headers # Disable col_names if no headers
168
+ @args = nil
169
+ end
170
+
171
+ def nb_lines
172
+ Ncurses.stdscr.getmaxy - 1 - (@column ? 1 : 0) - (@col_names ? 1 : 0)
173
+ end
174
+
175
+ def next_foreground; @attr.next_foreground; end
176
+ def next_background; @attr.next_background; end
177
+ def next_attribute; @attr.next_attribute; end
178
+ def attr_names; @attr.names; end
179
+
180
+ # @col_hide always store the 0 based indices of the
181
+ # columns to show or hide. @col_start is taken into account when
182
+ # setting and getting the @col_hide variables, and for display.
183
+ def col_hide_clear; @col_hide = []; end
184
+ def col_hide(*args)
185
+ args = args.collect { |x| x - @col_start }
186
+ @col_hide.push(*args)
187
+ @col_hide.uniq!
188
+ @col_hide.sort!
189
+ end
190
+
191
+ def col_hidden(with_start = true)
192
+ if with_start
193
+ @col_hide.collect { |x| x + @col_start }
194
+ else
195
+ @col_hide
196
+ end
197
+ end
198
+
199
+ def col_show(*args)
200
+ @col_hide -= args.collect { |x| x - @col_start }
201
+ end
202
+
203
+ def col_align(align, cols)
204
+ cols.each { |x|
205
+ x -= @col_start
206
+ next if x < 0
207
+ @align[x] = align
208
+ }
209
+ end
210
+
211
+ def st_col; @st_col; end
212
+
213
+ def st_col=(n)
214
+ n = 0 if n < 0
215
+ n = @data.sizes.size if n > @data.sizes.size
216
+ return @st_col if n == @st_col
217
+
218
+ range, sign = (n > @st_col) ? [@st_col...n, 1] : [n...@st_col, -1]
219
+ @col_off += sign * @data.sizes[range].inject(0) { |acc, x|
220
+ acc + x + @sep.size
221
+ }
222
+ @col_off = [@col_off, 0].max
223
+ @col_off = 0 if n == 0
224
+ @st_col = n
225
+ end
226
+
227
+ def col_space=(n)
228
+ @col_space = [1, n.to_i].max
229
+ end
230
+
231
+ def refresh
232
+ Ncurses.move(0, 0)
233
+ Ncurses.attrset(Ncurses.COLOR_PAIR(0))
234
+ lines = refresh_prepare
235
+
236
+ i = 0
237
+ line_i = @data.line + 1
238
+
239
+ sline = refresh_column_headers
240
+
241
+ @data.lines(lines) { |l|
242
+ highlighted = @line_highlight && ((line_i - @line_highlight_shift) %
243
+ @line_highlight_period == 0)
244
+ highlighted ||= l.highlight?
245
+ highlighted ? @attr.set : @attr.reset
246
+ display_line(l, line_i, sline, highlighted)
247
+ i += 1
248
+ line_i += 1
249
+ sline += 1
250
+ }
251
+ Ncurses.clrtobot
252
+ ensure
253
+ Ncurses.refresh
254
+ end
255
+
256
+ def display_line(l, line_i, sline, highlighted, sift = true, force_center = false)
257
+ if @line
258
+ Ncurses.attron(Ncurses::A_REVERSE) if l.has_match
259
+ Ncurses.attron(Ncurses::A_UNDERLINE) if IgnoredLine === l
260
+ s = @line_offset ? l.off : line_i
261
+ Ncurses.mvaddstr(sline, 0, @col_fmt % [@linec, s])
262
+ Ncurses.attroff(Ncurses::A_REVERSE) if l.has_match
263
+ Ncurses.attroff(Ncurses::A_UNDERLINE) if IgnoredLine === l
264
+ end
265
+
266
+ if Line === l
267
+ a = sift ? l.values_at(*@col_show) : l.values_at(0..-1)
268
+ # Now always display one field at a time
269
+ ms = sift ? l.matches_at(*@col_show) : l.matches_at(0..-1)
270
+ clen = @len
271
+ @sizes.zip(ms).each_with_index { |sm, i|
272
+ chilighted = !highlighted && @col_highlight
273
+ chilighted &&= ((@st_col - @col_highlight_shift + i) % @col_highlight_period == 0)
274
+ @attr.on if chilighted
275
+
276
+ s, m = *sm
277
+
278
+ align = force_center ? :center : @align[i]
279
+ align = (@right_align_re && a[i] =~ @right_align_re) ? :right : :left if align.nil?
280
+
281
+ # Handle max column width
282
+ cwidth = [s, clen, @widths_show[i]].min # Actual width of column
283
+ lcwidth = cwidth # lcwdith is width left in column
284
+
285
+ if m
286
+ string_length = m.string.length
287
+ large = string_length > cwidth
288
+ if !large
289
+ if align == :right
290
+ Ncurses.addstr(str = (" " * (cwidth - string_length)))
291
+ lcwidth -= str.length
292
+ elsif align == :center
293
+ Ncurses.addstr(str = (" " * ((cwidth - string_length) / 2)))
294
+ lcwidth -= str.length
295
+ end
296
+ Ncurses.addstr(str = m.pre_match[0, lcwidth])
297
+ lcwidth -= str.length
298
+ Ncurses.attron(Ncurses::A_REVERSE)
299
+ Ncurses.addstr(str = m[0][0, lcwidth])
300
+ Ncurses.attroff(Ncurses::A_REVERSE)
301
+ lcwidth -= str.length
302
+ Ncurses.addstr(str = m.post_match[0, lcwidth])
303
+ lcwidth -= str.length
304
+ if align == :left
305
+ Ncurses.addstr(str = (" " * (cwidth - string_length)))
306
+ lcwidth -= str.length
307
+ elsif align == :center
308
+ space = cwidth - string_length
309
+ Ncurses.addstr(str = (" " * (space / 2 + space % 2)))
310
+ lcwidth -= str.length
311
+ end
312
+ else # large
313
+ loff = @offsets_show[i] || 0 # Offset left to skip
314
+ cap_str = proc { |x|
315
+ if x.length > loff
316
+ Ncurses.addstr(str = x[loff..-1][0, lcwidth])
317
+ lcwidth -= str.length
318
+ loff = 0
319
+ else
320
+ loff -= x.length
321
+ end
322
+ }
323
+ cap_str[m.pre_match]
324
+ Ncurses.attron(Ncurses::A_REVERSE)
325
+ cap_str[m[0]]
326
+ Ncurses.attroff(Ncurses::A_REVERSE)
327
+ cap_str[m.post_match]
328
+ Ncurses.addstr(" " * lcwidth) if lcwidth > 0
329
+ end
330
+ else # No match
331
+ col_string = a[i] || ""
332
+ if col_string.length <= cwidth
333
+ case align
334
+ when :left
335
+ str = col_string.ljust(cwidth)
336
+ when :right
337
+ str = col_string.rjust(cwidth)
338
+ when :center
339
+ str = col_string.center(cwidth)
340
+ end
341
+ else
342
+ str = (col_string[@offsets_show[i], cwidth] || "").ljust(cwidth)
343
+ end
344
+ Ncurses.addstr(str[0, cwidth])
345
+ end
346
+ clen -= cwidth; break if clen <= 0
347
+ @attr.off if chilighted
348
+ Ncurses.addstr(str = @sep[0, clen])
349
+ clen -= str.length; break if clen <= 0
350
+ }
351
+ @attr.reset if @col_highlight
352
+ Ncurses.addstr(" " * clen) if clen > 0
353
+ # else
354
+ # # No match, display all at once
355
+ # str = (@format % @sizes.zip(a).flatten).ljust(@len)[0, @len]
356
+ # Ncurses.addstr(str)
357
+ # end
358
+ else # l is an ignored line
359
+ off = @col_off
360
+ clen = @len
361
+ if @hide_ignored
362
+ Ncurses.addstr(" " * clen) if clen > 0
363
+ elsif l.has_match
364
+ m = l.matches
365
+ s = m.pre_match
366
+ if s.length > off && clen > 0
367
+ Ncurses.addstr(str = s[off, clen])
368
+ clen -= str.length
369
+ off = 0
370
+ else
371
+ off -= s.length
372
+ end
373
+ s = m[0]
374
+ if s.length > off && clen > 0
375
+ Ncurses.attron(Ncurses::A_REVERSE)
376
+ Ncurses.addstr(str = s[off, clen])
377
+ Ncurses.attroff(Ncurses::A_REVERSE)
378
+ clen -= str.length
379
+ off = 0
380
+ else
381
+ off -= s.length
382
+ end
383
+ s = m.post_match
384
+ if s.length > off && clen > 0
385
+ Ncurses.addstr(str = s[off, clen])
386
+ clen -= str.length
387
+ end
388
+ Ncurses.addstr(" " * clen) if clen > 0
389
+ else # l.has_match
390
+ s = l.str
391
+ if s.length > off && clen > 0
392
+ Ncurses.addstr(str = s[off, @len].ljust(clen)[0, clen])
393
+ clen -= str.length
394
+ end
395
+ Ncurses.addstr(" " * clen) if clen > 0
396
+ end
397
+ end
398
+ end
399
+
400
+ # Modifies @sizes
401
+ def refresh_column_headers
402
+ @col_names &= @col_headers # Disable col_names if no headers
403
+ if @column
404
+ cnumber = @col_show.map { |x| (x + @col_start).to_s }
405
+ cnumber.each_with_index { |s, i| s.replace("< #{s} >") if @sizes[i] > @widths_show[i] }
406
+ @sizes.max_update(cnumber.collect { |x| x.size })
407
+ end
408
+ if @col_names
409
+ column_headers = @col_headers.values_at(*@col_show).map { |x| x || "" }
410
+ hs = col_headers.map { |s| s.size }
411
+ @sizes.max_update(hs)
412
+ end
413
+
414
+ sline = 0
415
+ if @column
416
+ Ncurses.attron(Ncurses::A_UNDERLINE) if !@col_names
417
+ display_line(Line.new(cnumber), "", sline, false, false, true)
418
+ Ncurses.attroff(Ncurses::A_UNDERLINE) if !@col_names
419
+ sline += 1
420
+ end
421
+ if @col_names
422
+ Ncurses.attron(Ncurses::A_UNDERLINE)
423
+ display_line(Line.new(column_headers), "", sline, false, false)
424
+ Ncurses.attroff(Ncurses::A_UNDERLINE)
425
+ sline += 1
426
+ end
427
+ sline
428
+ end
429
+
430
+ def refresh_prepare
431
+ lines = nb_lines
432
+ @len = Ncurses.stdscr.getmaxx
433
+ @col_show = (@st_col..(@st_col + @col_hide.size + @len / 2)).to_a
434
+ @col_show -= @col_hide
435
+ @sizes = @data.sizes.values_at(*@col_show).map { |x| x || 0 }
436
+ @widths_show = @widths.values_at(*@col_show).map { |x| x || @col_width }
437
+ @offsets_show = @col_offsets.values_at(*@col_show).map { |x| x || 0 }
438
+ if @line
439
+ @linec = @line_offset ? @data.max_offset : (@data.line + lines)
440
+ @linec = @linec.to_s.size
441
+ end
442
+ @len -= @linec + @col_space if @line
443
+ @sep = @separator.center(@col_space, @padding)
444
+ @col_fmt = "%*s#{@sep}"
445
+ @format = @col_fmt * @col_show.size
446
+ return lines
447
+ end
448
+
449
+ def wait_status(status, wprompt)
450
+ len = Ncurses.stdscr.getmaxx
451
+ aprompt = wprompt[0, len - 1]
452
+ Ncurses.attrset(Ncurses::A_NORMAL)
453
+ Ncurses.mvaddstr(Ncurses.stdscr.getmaxy-1, 0, aprompt)
454
+
455
+ nlen = len - aprompt.length
456
+ Ncurses.attrset(Ncurses::A_BOLD)
457
+ Ncurses.addstr(status.rjust(nlen)[0, nlen])
458
+ Ncurses.attrset(Ncurses::A_NORMAL)
459
+ end
460
+
461
+ def start_active_status(status)
462
+ len = Ncurses.stdscr.getmaxx
463
+ astatus = status[0, len - 2]
464
+ Ncurses.attrset(Ncurses::A_NORMAL)
465
+ Ncurses.mvaddstr(Ncurses.stdscr.getmaxy-1, 0, status[0, len - 2])
466
+
467
+ nlen = len - astatus.length
468
+ Ncurses.addstr('|'.rjust(nlen)[0, nlen])
469
+ Ncurses.refresh
470
+
471
+ @active_th = Thread.new {
472
+ loop {
473
+ ['/', '-', '\\', '|'].each { |c|
474
+ sleep(0.5)
475
+ Ncurses.mvaddstr(Ncurses.stdscr.getmaxy-1, len - 1, c)
476
+ Ncurses.refresh
477
+ }
478
+ }
479
+ }
480
+ end
481
+
482
+ def end_active_status
483
+ @active_th.kill if @active_th
484
+ @active_th = nil
485
+ end
486
+
487
+ def flush
488
+ Ncurses.refresh
489
+ end
490
+
491
+ def prompt(ps, opts = {})
492
+ stdscr = Ncurses.stdscr
493
+ len = stdscr.getmaxx
494
+ Ncurses.attrset(Ncurses.COLOR_PAIR(0))
495
+ Ncurses.mvaddstr(stdscr.getmaxy-1, 0, ps.ljust(len)[0, len])
496
+ s, pos, key = read_line(stdscr.getmaxy-1, ps.length, opts)
497
+ Ncurses.mvaddstr(stdscr.getmaxy-1, 0, " " * len)
498
+ return (key == ?\e.ord) ? nil : s
499
+ rescue KeyboardInterrupt
500
+ return nil
501
+ end
502
+
503
+ # read_line returns an array
504
+ # [string, last_cursor_position_in_string, keycode_of_terminating_enter_key].
505
+ # options recognize:
506
+ # :window What window to work with
507
+ # :max_len Width of window
508
+ # :string Initial value
509
+ # :cursor_pos Initial cursor position
510
+ # :history Array containing the history to use with up and down arrows
511
+ def read_line(y, x, opts = {})
512
+ window = opts[:window] || Ncurses.stdscr
513
+ max_len = opts[:max_len] || (window.getmaxx - x - 1)
514
+ @prompt_line = opts[:init] || ""
515
+ cursor_pos = opts[:cursor_pos] || @prompt_line.size
516
+ other = opts[:other]
517
+ extra = opts[:extra]
518
+ history = opts[:history] || []
519
+ history_pos = 0
520
+ save_line = ""
521
+
522
+ loop do
523
+ window.mvaddstr(y,x,@prompt_line)
524
+ window.move(y,x+cursor_pos)
525
+
526
+ extra.call if extra
527
+
528
+ ch = window.getch
529
+ case ch
530
+ when Ncurses::KEY_LEFT, Curses::CTRL_B
531
+ cursor_pos = [0, cursor_pos-1].max
532
+ when Ncurses::KEY_RIGHT, Curses::CTRL_F
533
+ cursor_pos = [@prompt_line.length, cursor_pos+1].min
534
+ when Ncurses::KEY_ENTER, ?\n.ord, ?\r.ord
535
+ return @prompt_line, cursor_pos, ch # Which return key has been used?
536
+ when Ncurses::KEY_HOME, Curses::CTRL_A
537
+ cursor_pos = 0
538
+ when Ncurses::KEY_END, Curses::CTRL_E
539
+ cursor_pos = [max_len, @prompt_line.length].min
540
+ when Ncurses::KEY_DC, Curses::CTRL_D
541
+ @prompt_line.slice!(cursor_pos)
542
+ window.mvaddstr(y, x+@prompt_line.length, " ")
543
+ when Curses::CTRL_K
544
+ window.mvaddstr(y, x+cursor_pos, " " * (@prompt_line.length - cursor_pos))
545
+ @prompt_line = @prompt_line[0, cursor_pos]
546
+ when Ncurses::KEY_BACKSPACE, ?\b.ord
547
+ if cursor_pos > 0
548
+ cursor_pos -= 1
549
+ @prompt_line.slice!(cursor_pos)
550
+ window.mvaddstr(y, x+@prompt_line.length, " ")
551
+ else
552
+ return "", 0, ?\e.ord
553
+ end
554
+ when ?\e.ord # ESCAPE
555
+ return "", 0, ch
556
+ when Ncurses::KEY_UP
557
+ if history_pos < history.size
558
+ save_line = @prompt_line if history_pos == 0
559
+ window.mvaddstr(y, x, " " * @prompt_line.length)
560
+ @prompt_line = history[history_pos].dup
561
+ history_pos += 1
562
+ cursor_pos = @prompt_line.size
563
+ end
564
+ when Ncurses::KEY_DOWN
565
+ if history_pos > 0
566
+ window.mvaddstr(y, x, " " * @prompt_line.length)
567
+ history_pos -= 1
568
+ @prompt_line = history_pos > 0 ? history[history_pos - 1].dup : save_line
569
+ cursor_pos = @prompt_line.size
570
+ end
571
+ when C::SPACE..255 # remaining printables
572
+ if (cursor_pos < max_len)
573
+ @prompt_line[cursor_pos,0] = ch.chr
574
+ cursor_pos += 1
575
+ else
576
+ Ncurses.beep
577
+ end
578
+ end
579
+ other[ch] if other
580
+ end
581
+ end
582
+ end
583
+
584
+ class CommandSubWindow
585
+ def initialize(width = 0, height = 15, bordery = 5, borderx = 10)
586
+ maxy, maxx = Ncurses.stdscr.getmaxy, Ncurses.stdscr.getmaxx
587
+ @nlines = [height + 2, maxy - 2 * bordery].min
588
+ @ncols = [width + 2, maxx - 2 * borderx].min
589
+ @win = Ncurses.stdscr.subwin(@nlines, @ncols, bordery, borderx)
590
+ new_list([])
591
+ end
592
+
593
+ def destroy
594
+ @win.delwin if @win
595
+ end
596
+
597
+ def new_list(list)
598
+ @list = list
599
+ @top_item = 0
600
+ @cur_item = 0
601
+ display_list
602
+ end
603
+
604
+ def display_list
605
+ return unless @win
606
+ @win.box(0, 0)
607
+ len = @ncols - 2
608
+ height = @nlines - 2
609
+ str = " Commands "
610
+ @win.mvaddstr(0, (len - str.size) / 2, str) if len > str.size
611
+ i = 1
612
+ @list[@top_item..-1].each { |s|
613
+ break if i > height
614
+ @win.attron(Ncurses::A_REVERSE) if @cur_item + 1 == i + @top_item
615
+ @win.mvaddstr(i, 1, s.ljust(len)[0, len])
616
+ @win.attroff(Ncurses::A_REVERSE) if @cur_item + 1 == i + @top_item
617
+ i += 1
618
+ }
619
+ empty = " " * len
620
+ i.upto(height) { |j|
621
+ @win.mvaddstr(j, 1, empty)
622
+ }
623
+ @win.wsyncup
624
+ end
625
+
626
+ def next_item
627
+ @cur_item += 1 if @cur_item < @list.size - 1
628
+ @top_item += 1 if (@cur_item - @top_item).abs >= @nlines - 2
629
+ display_list
630
+ end
631
+
632
+ def previous_item
633
+ @cur_item -= 1 if @cur_item > 0
634
+ @top_item -= 1 if @cur_item < @top_item
635
+ display_list
636
+ end
637
+
638
+ def item; @list[@cur_item]; end
639
+ end