typr 1.1.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.
data/lib/graphical.rb ADDED
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/ruby
2
+ require 'sdl2'
3
+ #require 'io/console'
4
+ #require 'readline'
5
+ require_relative 'frontend'
6
+
7
+ module Visuals
8
+ include SDL2
9
+
10
+ public
11
+ i=-1
12
+ # SDL2.init(SDL2::INIT_EVERYTHING)
13
+ SDL2.init(SDL2::INIT_VIDEO)
14
+ TTF.init
15
+ WINDOW = Window.create("visuals",
16
+ Window::POS_CENTERED, #POS_UNDEFINED,
17
+ Window::POS_CENTERED, #POS_UNDEFINED,
18
+ 1324, 768, 0)
19
+ GO = WINDOW.create_renderer(-1,
20
+ Renderer::Flags::ACCELERATED|Renderer::Flags::TARGETTEXTURE)
21
+ Key.constants.each{ |c| c = c.to_s
22
+ eval 'KEY_'+c.upcase+'=Key::'+c }
23
+
24
+ def self.width; WINDOW.size[0] / $charsize[1] end
25
+ def self.height; WINDOW.size[1] / $charsize[0] end
26
+ def self.row; $row end
27
+ def self.column; $col end
28
+ def self.x; $x end
29
+ def self.y; $y end
30
+
31
+
32
+ def color_for c
33
+ return unless c
34
+ c = COLORS[c] if c.is_a?( Symbol )
35
+ c.map{ |c| (c/1000.0*256).to_i }
36
+ end
37
+
38
+ def get_background; $color[1] end
39
+ def get_foreground; $color[0] end
40
+ def background color; return unless color
41
+ $color[1] = color_for( color ) end
42
+ def foreground color;
43
+ $color[0] = (color ? color_for( color ) : $default[0]) end
44
+
45
+ def color c
46
+ c = [c] unless c.is_a? Array and c.count == 2 #.flatten
47
+ foreground c[0]
48
+ background c[1]
49
+ # $letters[c] = (32.upto 126).map{ |n| n = n.chr
50
+ # [n, $font.render_blended( n, c )] }.to_h unless $letters[c]
51
+ end
52
+
53
+ def move col, row
54
+ $x = ($col = col) * $charsize[1] #width
55
+ $y = ($row = row) * $charsize[0] #$height
56
+ end
57
+
58
+ def clear type = :screen #bg=
59
+ GO.draw_color = $default[1] #bg
60
+ case type
61
+ # when Array; GO.fill_rect Rect.new()
62
+ when :screen; GO.fill_rect Rect.new(0, 0, *WINDOW.size)
63
+ when :line; GO.fill_rect Rect.new(
64
+ Visuals.x, Visuals.y, WINDOW.size[0], Visuals.y+$charsize[0])
65
+ when :down; GO.fill_rect Rect.new(
66
+ 0, Visuals.y, *WINDOW.size)
67
+ end
68
+ end
69
+
70
+ def show( str ) #solid_blended_shaded(
71
+ return unless str
72
+ # chars = str.chars.map{ |char| $letters[foreground][char] }
73
+ # width = chars.map( &:w ).sum
74
+ # if $color[1] != $default[1] #get_background
75
+ if get_background
76
+ GO.draw_color = get_background
77
+ GO.fill_rect Rect.new($x, $y, str.size*$charsize[1], $charsize[0])
78
+ end
79
+ # GO.fill_rect Rect.new($x, $y, width, $charsize[0])
80
+ # text = Surface.new( width, $charsize[0], 32, 0, 0, 0, 0 )
81
+ # x=0
82
+ # for char in chars #.each{|c|
83
+ # Surface.blit char, nil, text, Rect.new( x, 0, char.w, char.h )
84
+ # x += char.w
85
+ # end
86
+ # GO.copy GO.create_texture_from(text),nil,Rect.new($x, $y, width, $charsize[0])
87
+ text = str.rstrip
88
+ unless text.empty?
89
+ image = $font.render_blended( text, get_foreground )
90
+ GO.copy GO.create_texture_from( image ), nil,
91
+ Rect.new($x, $y, image.w, $charsize[0])
92
+ end
93
+ move $col+str.size, $row
94
+ end
95
+
96
+ def refresh; GO.present end
97
+
98
+ def read input
99
+ case input
100
+ when :event then e = Event.poll until e
101
+ when :key then
102
+ e = Event.poll until e.is_a? Event::KeyDown
103
+ return e.sym.between?(32, 126) ? e.sym.chr : e.sym
104
+ when :line#; line = ''
105
+ # File.mkfifo( 'input' ) unless File.exist? 'input'
106
+ # Readline.output=input=open('input')
107
+ # Readline.output=input=StringIO.new
108
+ # fork{ Readline.readline }
109
+ x, y = Visuals.row, Visuals.column
110
+ listen = Thread.new{
111
+ # until (char = input.read) == $/
112
+ # until (line = Readline.line_buffer)[-1] == $/
113
+ loop do
114
+ move x, y
115
+ show line
116
+ sleep 0.1
117
+ end
118
+ }
119
+ line = Readline.readline
120
+ listen.terminate
121
+ # until input.string[-1]==$/
122
+ return line
123
+ # until (key = read :key) == KEY_RETURN
124
+ # str << key if key.is_a? String
125
+ # end
126
+ #
127
+ # text = ""
128
+ # TextInput.start
129
+ # loop do
130
+ ## ev = Event.poll
131
+ # case ev = read( :event )
132
+ # when Event::KeyDown
133
+ # if ev.sym == KEY_RETURN
134
+ # TextInput.stop
135
+ # return text
136
+ # end
137
+ # when Event::TextInput
138
+ # text += ev.text
139
+ # show ev.text
140
+ # p ev.text
141
+ # end
142
+ # end
143
+ end
144
+ end
145
+
146
+ def self.exit
147
+ end
148
+
149
+ def self.init args={}
150
+ $default = [
151
+ color_for( args[:foreground] || :grey80 ),
152
+ color_for( args[:background] || :grey10 ) ]
153
+ $color = $default.dup
154
+ $charsize = args[:charsize] || [18,11]
155
+ $font = TTF.open( args[:font] || "/usr/share/fonts/X11/TTF/VeraMono.ttf", $charsize[0] )
156
+ move 0, 0
157
+ end
158
+
159
+ end
160
+
161
+ if __FILE__ == $0
162
+ # require_relative 'base'
163
+ include Visuals
164
+ Visuals.init font:ARGV[0] #[[200,200,200],[20,20,20]], [80,50]
165
+ characters = (32..126).map(&:chr).join
166
+ loop do
167
+ clear
168
+ COLORS.keys.each_with_index{ |c,i|
169
+ foreground c; move 0,i; show characters }
170
+ refresh
171
+ # GO.present
172
+ exit if read(:key) == KEY_ESCAPE
173
+ end
174
+ end
data/lib/grid.rb ADDED
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'space.rb'
6
+
7
+ module Typr
8
+
9
+ ##
10
+ # A sortable, filterable, multi-column table widget.
11
+ #
12
+ # grid = Typr::Grid.new(
13
+ # input: [ ["Alice", 30], ["Bob", 25] ],
14
+ # header: [:name, :age],
15
+ # format: [:max, :right]
16
+ # )
17
+ # grid.draw
18
+ #
19
+ # Column format tokens: +Integer+ (fixed width), +:min+ (auto), +:max+ (fill remaining).
20
+ #
21
+ # Built-in procs: +:filetree+, +:datetime+, +:magnitudes+, +:convert+.
22
+
23
+ class Grid < Stack # Table
24
+ attr_accessor :format, :procs, :filters, :data, :map
25
+ attr_accessor :align, :rawsort, :rawfilter
26
+ attr_writer :sequence
27
+
28
+ ##
29
+ # Number of rows currently visible (filtered count, or total).
30
+ #
31
+ # grid.rows #=> 2
32
+ #
33
+ # @return [Integer]
34
+
35
+ def rows; (@map || @data || [] ).count end
36
+
37
+ ##
38
+ # Row identifiers: Hash keys or integer range.
39
+ #
40
+ # grid.ids #=> [0, 1]
41
+ #
42
+ # @return [Array<Object>]
43
+
44
+ def ids; @map || ( is_hash? ?
45
+ @data.keys : (@data||[]).count.times.to_a ) end
46
+
47
+ ##
48
+ # Row ids visible on the current page after filtering.
49
+ #
50
+ # grid.page #=> [0, 1]
51
+ #
52
+ # @return [Array<Object>]
53
+
54
+ def page; ids[ super ] end
55
+
56
+ ##
57
+ # Column render order. Defaults to 0..n-1; override with +sequence=+.
58
+ #
59
+ # grid.sequence #=> [0, 1, 2]
60
+ #
61
+ # @return [Array<Integer>]
62
+
63
+ def sequence; @sequence || @columns.times.to_a end
64
+
65
+ ##
66
+ # Total number of data columns.
67
+ #
68
+ # grid.columns #=> 3
69
+ #
70
+ # @return [Integer]
71
+
72
+ def columns; @sequence ? @sequence.count : @columns end
73
+
74
+ ##
75
+ # True if data is a Hash.
76
+ #
77
+ # grid.is_hash? #=> false
78
+ #
79
+ # @return [Boolean]
80
+
81
+ def is_hash?; @data.is_a? Hash end
82
+
83
+ ##
84
+ # Retrieve a row by id.
85
+ #
86
+ # grid[0] #=> ["Alice", 30, "engineer"]
87
+ #
88
+ # @return [Array, nil]
89
+
90
+ def []( id ); @data[id] end
91
+
92
+ ##
93
+ # Set a row by id. Auto-detects numeric types and alignment.
94
+ #
95
+ # grid[2] = ["Charlie", 35, "manager"]
96
+ # grid[:charlie] = [35, "manager"] # Hash-backed
97
+ #
98
+ # @param id [Integer, Object] row key
99
+ # @param row [Object, Array] field values
100
+
101
+ def []=( id,row )
102
+ @data ||= id.is_a?(Integer) ? [] : {}
103
+ row = [row] unless row.is_a? Array
104
+ row = [id] + row if is_hash?
105
+ row.flatten! if is_hash?
106
+ @data[id] = row.map.with_index{ |field, column|
107
+ if column == @columns
108
+ @colors[:columns][column] ||= [:grey90,:grey60][column%2]
109
+ @format[column] ||= :min
110
+ @columns += 1
111
+ end
112
+ field = @functions[ :convert ].call field unless @ignore[column]
113
+ @align[column] ||= [Integer,Float].include?(field.class) ?
114
+ :right : :left
115
+ field
116
+ }
117
+ process id
118
+ filter id
119
+ end
120
+
121
+ ##
122
+ # Set column headers. Symbols are auto-defined as +UPCASE_I+ constants.
123
+ #
124
+ # grid.header = [:name, :age]
125
+ # # defines NAME_0 = "name", AGE_1 = "age"
126
+ #
127
+ # @param h [Array<String, Symbol>]
128
+
129
+ def header=( h ); super
130
+ h.each_with_index{ |h,i| Typr.module_eval h.upcase+?=+i.to_s } if
131
+ h.is_a? Array
132
+ end
133
+
134
+ ##
135
+ # Reset grid state.
136
+ #
137
+ # grid.reset :all # everything
138
+ # grid.reset :display # filters, sort, map
139
+ # grid.reset :format # widths, maxed
140
+ #
141
+ # @param type [:all, :display, :format, Array<Symbol>]
142
+
143
+ def reset type=:all
144
+ case type
145
+ when :display; @map = nil; @sort = nil; @filters.clear
146
+ when :format; @widths.clear; @maxed = false
147
+ when :all; reset [:display, :format]
148
+ end
149
+ super
150
+ end
151
+
152
+ ##
153
+ # Clear all data, columns, and transforms.
154
+ #
155
+ # grid.clear
156
+ #
157
+ # @return [void]
158
+
159
+ def clear
160
+ @columns = 0; @layer = @map = @data = nil
161
+ reset [:format, :positions, :selection]
162
+ end
163
+
164
+ ##
165
+ # Apply column processors to a row's cells, storing results in +@layer+.
166
+ #
167
+ # grid.process(0)
168
+ #
169
+ # @param row [Integer] row id
170
+
171
+ def process row #=:all
172
+ @layer ||= @data.class.new
173
+ return if @procs.empty?
174
+ @layer[row] = @procs.map{ |col,proc|
175
+ proc = @functions[proc] if proc.is_a? Symbol
176
+ cell = @data[row][col]
177
+ next unless cell
178
+ [ col, proc.lambda? ? proc.call( cell ) : proc.call( cell, row, col ) ] unless
179
+ cell.is_a?(Proc) }.to_h
180
+ end
181
+
182
+ ##
183
+ # Add a column filter (substring or regex).
184
+ #
185
+ # grid.add_filter(0, "Ali") # name contains "Ali"
186
+ # grid.add_filter(1, /^\d{2}$/) # age matches regex
187
+ #
188
+ # @param column [Integer, Symbol]
189
+ # @param query [String, Regexp]
190
+
191
+ def add_filter column=nil, query=nil
192
+ return unless column and query
193
+ @filters << [column, query]
194
+ filter :all
195
+ return
196
+ end
197
+
198
+ ##
199
+ # Test whether a row passes all active filters.
200
+ #
201
+ # grid.check(0) #=> true
202
+ #
203
+ # @param row [Integer]
204
+ # @return [Boolean]
205
+
206
+ def check row
207
+ @filters.reject{ |column,query|
208
+ ( (@procs[column] and not @rawfilter[column]) ?
209
+ @layer : @data )[row][column].to_s[query] }.empty?
210
+ end
211
+
212
+ ##
213
+ # Apply or revise filters. +:all+ recomputes the full map; a row id adds/removes it.
214
+ #
215
+ # grid.filter :all
216
+ # grid.filter 0
217
+ #
218
+ # @param row [Integer, Symbol]
219
+
220
+ def filter row=:all
221
+ return if @filters.empty?
222
+ if row == :all
223
+ @map = ids.select{ |id| check id }
224
+ @start = 0
225
+ elsif check row then
226
+ @map ||= []
227
+ @map << row unless @map.include? row
228
+ else @map.delete(row) if @map end
229
+ end
230
+
231
+ ##
232
+ # Current sort column name (when called with arg) or index (without).
233
+ #
234
+ # grid.sort_by :age
235
+ # grid.sorted_by #=> "age"
236
+ # grid.sorted_by true #=> "age"
237
+ #
238
+ # @param name [Boolean] pass truthy to get display name
239
+ # @return [String, Integer, false, nil]
240
+
241
+ def sorted_by name=false; (name and @sort) ? @header[@sort] : @sort end
242
+
243
+ ##
244
+ # Sort by column (toggle direction on repeat).
245
+ #
246
+ # grid.sort_by :age
247
+ # grid.sort_by :age # reverses
248
+ #
249
+ # @param column [Integer, Symbol]
250
+
251
+ def sort_by column
252
+ column = @header.index( column.to_s ) if column.is_a? Symbol
253
+ if @sort and (column == @sort) then @reverse = !@reverse
254
+ else @sort = column end
255
+ sort
256
+ end
257
+
258
+ ##
259
+ # Execute the sort and update +@map+.
260
+ #
261
+ # @return [false, void]
262
+
263
+ def sort
264
+ return false unless @sort
265
+ data = (@procs[@sort] and not @rawsort[@sort]) ? @layer : @data
266
+ @map = ids.sort{ |a,b| (data[a][@sort] <=> data[b][@sort]) || 0 }
267
+ @map.reverse! if @reverse
268
+ return
269
+ end
270
+
271
+ ##
272
+ # Pixel width for a single visual column (includes +:max+ bonus).
273
+ #
274
+ # grid.width_for(0) #=> 12
275
+ #
276
+ # @param col [Integer] visual column index
277
+ # @return [Integer]
278
+
279
+ def width_for col
280
+ @widths[col] + ( @format[sequence[col]] == :max ? @rest : 0 )
281
+ end
282
+
283
+ ##
284
+ # X-offset for each visual column on a row.
285
+ #
286
+ # grid.positions #=> [0, 13, 25]
287
+ #
288
+ # @param row [Integer]
289
+ # @return [Array<Integer>]
290
+
291
+ def positions row=0; x=0
292
+ [0] + (sequence.count-1).times.map{ |id|
293
+ x += width_for(id) + @separator.size }
294
+ end
295
+
296
+ ##
297
+ # Compute column widths from visible data and render the grid.
298
+ #
299
+ # grid.draw
300
+ #
301
+ # @return [void]
302
+
303
+ def draw
304
+ if not @data or @data.empty? or ( @map and @map.empty? )
305
+ @widths = [ width/columns ] * columns if @widths.count < columns
306
+ @rest = 0
307
+ return super
308
+ end
309
+ for row in page.compact
310
+ fields = @data[row].dup
311
+ fields = [ fields ] unless fields.is_a?(Array)
312
+ for col,id in sequence.each_with_index
313
+ field = fields[col]
314
+ if field.is_a? Proc
315
+ field = field.call
316
+ field = @procs[col].call field,row,col if @procs[col]
317
+ @layer[row] ||= {}
318
+ @layer[row][col] = field
319
+ end
320
+ field = @layer[row][col] if @procs[col]
321
+ if @format[col].is_a? Integer
322
+ @widths[id] ||= @format[col]
323
+ else
324
+ @widths[id] ||= 0
325
+ size = real_size( field.to_s )
326
+ @widths[id] = size if size > @widths[id]
327
+ end
328
+ end
329
+ end
330
+ gaps = @margin.size + @separator.size * (columns - 1)
331
+ sum = @widths.sum
332
+ @max = sum + gaps - 1
333
+ space = width - gaps
334
+ if (extra = sum - space ) > 0
335
+ extra.times{ |i| @widths[ @widths.index(@widths.max) ] -= 1 }
336
+ @rest = 0
337
+ else
338
+ format = @format.values_at( *sequence )
339
+ @rest = space - sum / format.count(:max) if format.include? :max
340
+ end
341
+ super
342
+ end
343
+
344
+ ##
345
+ # Render a single row or the header bar to the terminal.
346
+ #
347
+ # grid.print :header
348
+ # grid.print 0
349
+ #
350
+ # @param row [Integer, Symbol] row id, or +:header+
351
+
352
+ def print row=nil
353
+ return super unless row
354
+ header = ( row == :header )
355
+ return show @header[0..width-1].ljust(width) if
356
+ header and @header.is_a? String
357
+ fields = (header ? @header : @data[row] ).dup
358
+ @layer[row].each{ |col,value| fields[col] = value } if
359
+ @layer[row] if @layer unless header
360
+ for col,id in sequence.each_with_index
361
+ show @separator unless id == 0
362
+ select = ( @selected[:columns].include? col or
363
+ @selected[:fields].include? [row,col] ) unless
364
+ @selected[:rows].include?(row) or header
365
+ color( @colors[:fields][[row,col]] ||
366
+ @colors[:columns][col] || @colors[:default] ) unless header
367
+ background @colors[:selected] if select
368
+ show prepare( fields[col].to_s, width_for( id ), @align[col] )
369
+ background @colors[:default][1] if select
370
+ end
371
+ end
372
+
373
+ ##
374
+ # Append multiple rows at once. Returns +self+ for chaining.
375
+ #
376
+ # grid << [["Dave", 28], ["Eve", 32]]
377
+ # grid << { alice: [30, "eng"] }
378
+ #
379
+ # @param data [Enumerable<Array>, Enumerable<Hash>]
380
+ # @return [self]
381
+
382
+ def << (data)
383
+ @data ||= data.class.new
384
+ skip = @data.size
385
+ data.each_with_index{ |row, id|
386
+ self[ is_hash? ? row.shift : id + skip ] = row }
387
+ sort
388
+ end
389
+
390
+ ##
391
+ # Construct a Grid.
392
+ #
393
+ # grid = Typr::Grid.new(
394
+ # input: [ ["Alice", 30], ["Bob", 25] ],
395
+ # header: [:name, :age],
396
+ # format: [:max, :min],
397
+ # procs: { 1 => :magnitudes },
398
+ # colors: { columns: [:cyan, nil] }
399
+ # )
400
+ #
401
+ # @param args [Hash] +input+, +header+, +format+, +procs+, +colors+, +align+, +functions+
402
+
403
+ def initialize args={}
404
+ @columns,@functions = 0,{}
405
+ @ignore,@rawfilter,@rawsort,@reverse = [],[],[],false
406
+ @filters,@procs = [],{}
407
+ @align,@format,@widths,@totals = [],[],[],[]
408
+ super #args
409
+ @colors = { columns: [], fields: {} }.merge ( @colors )
410
+ @selected = { fields:[], columns:[] }.merge @selected
411
+ @functions = {
412
+ filetree: Proc.new{ |f| f.gsub /.*\/[^$]/, ' ' },
413
+ datetime: Proc.new{|sec|Time.at(sec).strftime"%y-%m-%d %H:%M" rescue ??},
414
+ magnitudes: Proc.new{ |size| mag = (size.to_s.length-1) / 3
415
+ mag>0 ? (size.to_s.insert -(mag*3+1), ?.)[0..4] + %w[B K M G T][mag] :
416
+ size rescue size },
417
+ convert: Proc.new{ |value|
418
+ case value.to_s
419
+ when /^[\d]+$/ then value.to_s.to_i
420
+ when /^\d*[\.\,]\d+$/ then value.to_s.to_f
421
+ else value
422
+ end } }
423
+
424
+ if @input
425
+ self << @input
426
+ @input = nil
427
+ end
428
+ end
429
+ end
430
+
431
+ end
data/lib/line.rb ADDED
@@ -0,0 +1,99 @@
1
+ require_relative 'space'
2
+
3
+ module Typr
4
+
5
+ # A single-line UI element for displaying text and collecting input.
6
+ #
7
+ # == Example
8
+ #
9
+ # line = Line.new( top: 5, left: 10, width: 40 )
10
+ # line.draw "Hello world"
11
+ # line.append " more"
12
+ # line.ask([ ["Name?", :string], ["Age?", :integer] ])
13
+
14
+ class Line < Space
15
+ attr_accessor :default, :prompt, :bindings, :data
16
+
17
+ # Render +msg+ (String, Proc, or Space) on this line.
18
+ #
19
+ # line.draw "status: ok"
20
+ # line.draw Proc.new { show_time }
21
+
22
+ def draw msg=@data
23
+ @data = msg
24
+ move left, top
25
+ color @colors[:default]
26
+ case msg
27
+ when Proc; msg.call
28
+ when Space; msg.draw
29
+ when String; show( prepare msg, width )
30
+ end
31
+ end
32
+
33
+ # Append +str+ to the current content and redraw.
34
+ #
35
+ # line.draw "hello"
36
+ # line.append " world" #=> "hello world"
37
+ # line << "!" #=> "hello world!"
38
+
39
+ def append(str); draw @data+str end
40
+ alias :<< :append
41
+
42
+ # Display keybinding help in a floating grid.
43
+ #
44
+ # line.bindings = ["Ctrl-S: Save", "Ctrl-Q: Quit"]
45
+ # line.help
46
+
47
+ def help conf={}
48
+ Grid.new( { input: ['']+@bindings+[''], top: 3, left:0.4,
49
+ header: "KEYBINDINGS",
50
+ colors: { header: [:white,:black], border:[:white,:grey10] },
51
+ alternate: false, border: ' ' }.merge conf ).pick :none
52
+ return
53
+ end
54
+
55
+ # Reset the line to its default text or prompt.
56
+ #
57
+ # line.reset
58
+
59
+ def reset; draw( @default || ( @bindings || [@prompt] ).join(' ') ) end
60
+
61
+ # Prompt the user with a sequence of questions.
62
+ # +sentence+ is an array of [question, type] pairs.
63
+ # Returns a single answer or an array when multiple questions.
64
+ #
65
+ # line.ask([ ["Name?", :string] ]) #=> "Alice"
66
+ # line.ask([ ["Name?", :string], ["Ok?", ["y","n"]] ]) #=> ["Alice", "y"]
67
+
68
+ def ask sentence
69
+ answer = []
70
+ move left, top
71
+ Typr.clear :line
72
+ draw @prompt
73
+ for question, object in sentence
74
+ color( @colors[:question] )
75
+ append " " + question.to_s + " "
76
+ column = Typr.column
77
+ color( @colors[:answer] )
78
+ case object
79
+ when Array then answer << object.first.pick( object.last )
80
+ else answer << Typr.read( object, @data )
81
+ end
82
+ return if !answer.last or answer.last == @keymap[:exit]
83
+ move column, top
84
+ append answer.last.to_s
85
+ end
86
+ return ( answer.one? ? answer.first : answer )
87
+ end
88
+
89
+ def initialize args
90
+ @right= -1
91
+ @prompt = ?>
92
+ super
93
+ @bottom = @top
94
+ @colors = { question: :green, answer: :yellow }.merge @colors
95
+ reset
96
+ end
97
+ end
98
+
99
+ end