typr 1.1.4 → 1.3.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 CHANGED
@@ -57,7 +57,7 @@ module Visuals
57
57
  end
58
58
  end
59
59
 
60
- def show( str )
60
+ def draw( str )
61
61
  return unless str
62
62
  if get_background
63
63
  GO.draw_color = get_background
@@ -81,17 +81,14 @@ module Visuals
81
81
  e = Event.poll until e.is_a? Event::KeyDown
82
82
  return e.sym.between?(32, 126) ? e.sym.chr : e.sym
83
83
  when :line
84
- x, y = Visuals.row, Visuals.column
85
- listen = Thread.new{
86
- loop do
87
- move x, y
88
- show line
89
- sleep 0.1
90
- end
91
- }
92
- line = Readline.readline
93
- listen.terminate
94
- return line
84
+ str = ''
85
+ loop do
86
+ e = Event.poll
87
+ next unless e.is_a? Event::KeyDown
88
+ return nil if e.sym == KEY_ESCAPE
89
+ return str if e.sym == KEY_RETURN
90
+ str << e.sym.chr if e.sym.between? 32, 126
91
+ end
95
92
  end
96
93
  end
97
94
 
@@ -117,7 +114,7 @@ if __FILE__ == $0
117
114
  loop do
118
115
  clear
119
116
  COLORS.keys.each_with_index{ |c,i|
120
- foreground c; move 0,i; show characters }
117
+ foreground c; move 0,i; draw characters }
121
118
  refresh
122
119
  exit if read(:key) == KEY_ESCAPE
123
120
  end
data/lib/grid.rb CHANGED
@@ -9,16 +9,16 @@ module Typr
9
9
  # input: [ ["Alice", 30], ["Bob", 25] ],
10
10
  # header: [:name, :age],
11
11
  # format: [:max, :right]
12
- # )
13
- # grid.draw
14
- #
12
+ # )
13
+ # grid.show
14
+ #
15
15
  # Column format tokens: +Integer+ (fixed width), +:min+ (auto), +:max+ (fill remaining).
16
16
  #
17
17
  # Built-in procs: +:filetree+, +:datetime+, +:magnitudes+, +:convert+.
18
18
 
19
19
  class Grid < Stack # Table
20
20
  attr_accessor :format, :procs, :filters, :data, :map
21
- attr_accessor :align, :rawsort, :rawfilter
21
+ attr_accessor :align, :rawsort, :rawfilter, :reverse
22
22
  attr_writer :sequence
23
23
 
24
24
  ##
@@ -177,10 +177,11 @@ module Typr
177
177
  end
178
178
 
179
179
  ##
180
- # Add a column filter (substring or regex).
180
+ # Add a column filter (substring or regex). +:all+ matches any column.
181
181
  #
182
182
  # grid.add_filter(0, "Ali") # name contains "Ali"
183
183
  # grid.add_filter(1, /^\d{2}$/) # age matches regex
184
+ # grid.add_filter(:all, "ip") # any column contains "ip"
184
185
  #
185
186
  # @param column [Integer, Symbol]
186
187
  # @param query [String, Regexp]
@@ -202,8 +203,10 @@ module Typr
202
203
 
203
204
  def check row
204
205
  @filters.reject{ |column,query|
205
- ( (@procs[column] and not @rawfilter[column]) ?
206
- @layer : @data )[row][column].to_s[query] }.empty?
206
+ cols = column == :all ? @data[row].each_index.to_a : [column]
207
+ cols.any?{ |col|
208
+ ( (@procs[col] and not @rawfilter[col]) ?
209
+ @layer : @data )[row][col].to_s[query] } }.empty?
207
210
  end
208
211
 
209
212
  ##
@@ -237,6 +240,57 @@ module Typr
237
240
 
238
241
  def sorted_by name=false; (name and @sort) ? @header[@sort] : @sort end
239
242
 
243
+ ##
244
+ # Pick a row with live-filtering.
245
+ #
246
+ # When column is given, `Grid` overrides `Stack#pick` to add interactive
247
+ # text filtering of rows by the given column. Filter text is shown at the
248
+ # bottom of the grid. Passes through to `super` for all other types or
249
+ # when no column is given.
250
+ #
251
+ # grid.pick :row, 0, column: :name
252
+ # grid.pick :row, 0, column: :all # match any column
253
+ #
254
+ # @param type [Symbol] type is ignored (always :row); passed to super otherwise
255
+ # @param column [Symbol, String] column name to filter on, or :all (default nil)
256
+
257
+ def pick type = :row, row=0, column: nil
258
+ return super(type, row) unless column
259
+
260
+ saved = [@filters.dup, @map, @start]
261
+ base = @map || ids
262
+ @filters = [[column, '']]
263
+ @map = base.select{ |id| check id }
264
+ @start = 0
265
+ show
266
+ draw_hints
267
+ result = Typr.read_line '/', left: left, top: self.bottom + 1,
268
+ &->(key, query, _) {
269
+ if key.is_a?(String) and idx = @hints[0..height-@hints_start-1].index(key)
270
+ return page.to_a[idx + @hints_start]
271
+ end
272
+ if key.is_a?(String) and !key.each_char.all?{ |c| c.ord.between?(32, 126) } and
273
+ @keymap.values.include?(key) and ![KEY_BACKSPACE, "\b"].include?(key)
274
+ send key
275
+ else
276
+ @filters = [[column, query]]
277
+ @map = base.select{ |id| check id }
278
+ @start = 0
279
+ end
280
+ show
281
+ draw_hints
282
+ nil
283
+ }
284
+ case result
285
+ when nil; @filters, @map, @start = saved
286
+ when Integer; return result
287
+ else
288
+ @filters = result.empty? ? saved[0] : saved[0] + [[column, result]]
289
+ filter :all
290
+ end
291
+ return
292
+ end
293
+
240
294
  ##
241
295
  # Sort by column (toggle direction on repeat).
242
296
  #
@@ -260,8 +314,9 @@ module Typr
260
314
  def sort
261
315
  return false unless @sort
262
316
  data = (@procs[@sort] and not @rawsort[@sort]) ? @layer : @data
263
- @map = ids.sort{ |a,b| (data[a][@sort] <=> data[b][@sort]) || 0 }
317
+ @map = (0...@data.size).to_a.sort{ |a,b| (data[a][@sort] <=> data[b][@sort]) || 0 }
264
318
  @map.reverse! if @reverse
319
+ @map = @map.select{ |id| check id } unless @filters.empty?
265
320
  return
266
321
  end
267
322
 
@@ -293,11 +348,11 @@ module Typr
293
348
  ##
294
349
  # Compute column widths from visible data and render the grid.
295
350
  #
296
- # grid.draw
351
+ # grid.show
297
352
  #
298
353
  # @return [void]
299
354
 
300
- def draw
355
+ def show
301
356
  if not @data or @data.empty? or ( @map and @map.empty? )
302
357
  @widths = [ width/columns ] * columns if @widths.count < columns
303
358
  @rest = 0
@@ -349,20 +404,20 @@ module Typr
349
404
  def print row=nil
350
405
  return super unless row
351
406
  header = ( row == :header )
352
- return show @header[0..width-1].ljust(width) if
407
+ return draw @header[0..width-1].ljust(width) if
353
408
  header and @header.is_a? String
354
409
  fields = (header ? @header : @data[row] ).dup
355
410
  @layer[row].each{ |col,value| fields[col] = value } if
356
411
  @layer[row] if @layer unless header
357
412
  for col,id in sequence.each_with_index
358
- show @separator unless id == 0
413
+ draw @separator unless id == 0
359
414
  select = ( @selected[:columns].include? col or
360
415
  @selected[:fields].include? [row,col] ) unless
361
416
  @selected[:rows].include?(row) or header
362
417
  color( @colors[:fields][[row,col]] ||
363
418
  @colors[:columns][col] || @colors[:default] ) unless header
364
419
  background @colors[:selected] if select
365
- show prepare( fields[col].to_s, width_for( id ), @align[col] )
420
+ draw prepare( fields[col].to_s, width_for( id ), @align[col] )
366
421
  background @colors[:default][1] if select
367
422
  end
368
423
  end
@@ -406,7 +461,7 @@ module Typr
406
461
  @colors = { columns: [], fields: {} }.merge ( @colors )
407
462
  @selected = { fields:[], columns:[] }.merge @selected
408
463
  @functions = {
409
- filetree: Proc.new{ |f| f.gsub /.*\/[^$]/, ' ' },
464
+ filetree: Proc.new{ |f| ' ' * [(d = f.count('/') - 1), 0].max + '└' + File.basename(f) },
410
465
  datetime: Proc.new{|sec|Time.at(sec).strftime"%y-%m-%d %H:%M" rescue ??},
411
466
  magnitudes: Proc.new{ |size| mag = (size.to_s.length-1) / 3
412
467
  mag>0 ? (size.to_s.insert -(mag*3+1), ?.)[0..4] + %w[B K M G T][mag] :
data/lib/line.rb CHANGED
@@ -7,36 +7,36 @@ module Typr
7
7
  # == Example
8
8
  #
9
9
  # line = Line.new( top: 5, left: 10, width: 40 )
10
- # line.draw "Hello world"
10
+ # line.show "Hello world"
11
11
  # line.append " more"
12
- # line.ask([ ["Name?", :string], ["Age?", :integer] ])
12
+ # line.ask([ ["Name?", :line], ["Age?", :line] ])
13
13
 
14
14
  class Line < Space
15
- attr_accessor :default, :prompt, :bindings, :data
15
+ attr_accessor :default, :prompt, :bindings, :data, :align, :trim
16
16
 
17
17
  # Render +msg+ (String, Proc, or Space) on this line.
18
18
  #
19
- # line.draw "status: ok"
20
- # line.draw Proc.new { show_time }
19
+ # line.show "status: ok"
20
+ # line.show Proc.new { show_time }
21
21
 
22
- def draw msg=@data
22
+ def show msg=@data, align=@align
23
23
  @data = msg
24
24
  move left, top
25
25
  color @colors[:default]
26
26
  case msg
27
27
  when Proc; msg.call
28
- when Space; msg.draw
29
- when String; show( prepare msg, width )
28
+ when Space; msg.show
29
+ when String; draw( prepare msg, width, align, @trim )
30
30
  end
31
31
  end
32
32
 
33
33
  # Append +str+ to the current content and redraw.
34
34
  #
35
- # line.draw "hello"
35
+ # line.show "hello"
36
36
  # line.append " world" #=> "hello world"
37
37
  # line << "!" #=> "hello world!"
38
38
 
39
- def append(str); draw @data+str end
39
+ def append(str); show @data+str end
40
40
  alias :<< :append
41
41
 
42
42
  # Display keybinding help in a floating grid.
@@ -56,38 +56,68 @@ class Line < Space
56
56
  #
57
57
  # line.reset
58
58
 
59
- def reset; draw( @default || ( @bindings || [@prompt] ).join(' ') ) end
59
+ def reset; show( @default || ( @bindings || [@prompt] ).join(' ') ) end
60
60
 
61
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.
62
+ # +sentence+ is an array of [question, type] pairs (or a hash of them).
63
+ # Returns a single answer, or an array when there are multiple questions.
64
64
  #
65
- # line.ask([ ["Name?", :string] ]) #=> "Alice"
66
- # line.ask([ ["Name?", :string], ["Ok?", ["y","n"]] ]) #=> ["Alice", "y"]
65
+ # The +type+ selects how the answer is collected:
66
+ # Array - interactive pick via <widget>.pick(<type>); an optional third
67
+ # element renders the chosen row's field into the line
68
+ # :key - a single keypress via Typr.read_key
69
+ # :line - interactive line editor via Typr.read_line at the cursor
70
+ #
71
+ # Pressing the exit key (@keymap[:exit], Escape by default) aborts the
72
+ # remaining questions and returns nil.
73
+ #
74
+ # line.ask(name: :line) #=> "Alice"
75
+ # line.ask(keys: :key) #=> "a"
76
+ # line.ask(open: [grid, :row, :name]) #=> 2 (picked row id)
77
+ # line.ask([ ["Name?", :line], ["Ok?", :key] ]) #=> ["Alice", "a"]
67
78
 
68
79
  def ask sentence
69
80
  answer = []
70
81
  move left, top
71
82
  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 )
83
+ saved = @trim
84
+ @trim = :left
85
+ begin
86
+ @data = @prompt.to_s
87
+ colored = color_code( @colors[:default] ).to_s + @data
88
+ move left, top
89
+ draw prepare( colored, width, @align, @trim )
90
+ for question, object in sentence
91
+ text = " " + question.to_s + " "
92
+ @data += text
93
+ colored += color_code( @colors[:question] ).to_s + text
94
+ move left, top
95
+ draw prepare( colored, width, @align, @trim )
96
+ color( @colors[:answer] )
97
+ case object
98
+ when Array then answer << object.first.pick( object[1] )
99
+ when :key then answer << Typr.read_key
100
+ when :line then answer << Typr.read_line( colored + color_code( @colors[:answer] ),
101
+ left: left, top: top )
102
+ end
103
+ return if !answer.last or answer.last == @keymap[:exit]
104
+ text = ( object.is_a?(Array) and object[2] ) ?
105
+ object.first.data[answer.last][object[2]].to_s : answer.last.to_s
106
+ @data += text
107
+ colored += color_code( @colors[:answer] ).to_s + text
108
+ move left, top
109
+ draw prepare( colored, width, @align, @trim )
81
110
  end
82
- return if !answer.last or answer.last == @keymap[:exit]
83
- move column, top
84
- append answer.last.to_s
111
+ ensure
112
+ @trim = saved
85
113
  end
86
114
  return ( answer.one? ? answer.first : answer )
87
115
  end
88
116
 
89
117
  def initialize args
90
118
  @right= -1
119
+ @align = :left
120
+ @trim = :right
91
121
  @prompt = ?>
92
122
  super
93
123
  @bottom = @top
data/lib/space.rb CHANGED
@@ -10,7 +10,7 @@ module Typr
10
10
  #
11
11
  # @example
12
12
  # box = Space.new(left: 0, top: 2, right: 0.5, bottom: -4, border: :round)
13
- # box.draw
13
+ # box.show
14
14
 
15
15
  class Space
16
16
  include Typr
@@ -29,21 +29,36 @@ include Typr
29
29
  STR
30
30
  eval( method % ([name]*11) ) }
31
31
 
32
+ # Deep-convert String keys to Symbols so string-keyed config (e.g. YAML) is honored.
33
+ def symbolize obj
34
+ case obj
35
+ when Hash
36
+ obj.each_with_object({}) { |(k, v), h|
37
+ h[k.is_a?(String) ? k.to_sym : k] = symbolize(v) }
38
+ else obj end
39
+ end
40
+
32
41
  attr_writer :left, :top, :right, :bottom
33
42
  attr_accessor :margin, :colors, :interval, :borders
34
43
 
44
+ # Viewport width in cells (right - left + 1).
35
45
  def width; right - left + 1 end
46
+ # Viewport height in cells (bottom - top + 1).
36
47
  def height; bottom - top + 1 end
37
48
 
38
- def start; @updater = Thread.new{ loop{draw; sleep @interval }} end
49
+ # Redraw every +interval+ seconds from a background thread.
50
+ def start; @updater = Thread.new{ loop{show; sleep @interval }} end
51
+ # Stop the background refresh thread.
39
52
  def stop; @updater.terminate end
40
53
 
54
+ # Draw a border char at screen (x, y); skips nil and off-screen positions.
41
55
  def draw_border x,y,char
42
- show move_code(x, y) + char unless not char or
56
+ draw move_code(x, y) + char unless not char or
43
57
  x < 0 or x > Typr.width or y < 0 or y > Typr.height
44
58
  end
45
59
 
46
- def draw
60
+ # Draw the widget's border box (no-op when no borders are set).
61
+ def show
47
62
  return if @borders.empty?
48
63
  color @colors[:border]
49
64
  draw_border(left - 1, top - 1, @borders[:top_left])
@@ -58,6 +73,8 @@ eval( method % ([name]*11) ) }
58
73
  draw_border(right + 1, bottom + 1, @borders[:bottom_right])
59
74
  end
60
75
 
76
+ # Set border style: :light/:heavy/:double/:round, a char repeated across
77
+ # the box, or nil for none.
61
78
  def border=(border)
62
79
  chars = case border
63
80
  when Symbol
@@ -70,11 +87,14 @@ eval( method % ([name]*11) ) }
70
87
  bottom_right ].map.with_index{ |part,id| [part.to_sym, chars[id]] }.to_h if chars
71
88
  end
72
89
 
90
+ # Build a widget from boundary (left/top/right/bottom), border, margin,
91
+ # colors, interval, and keymap.
73
92
  def initialize args={}
74
- @max, @keymap, @colors, @margin = 0, {}, {}, ' '
93
+ @max, @keymap, @colors, @margin = 0, {}, {}, ' '
75
94
  @left, @top, @interval, @borders = 0, 0, 1, {}
76
95
  args.each{ |name, value|
77
96
  instance_variable_set ?@ + name.to_s, value }
97
+ @colors = symbolize( @colors ) if @colors.is_a? Hash
78
98
  @keymap = { exit: KEY_ESCAPE }.merge @keymap
79
99
  @colors = { default: [:white, :black], border: [:white, :black] }.merge(@colors)
80
100
  borders = @borders.dup
data/lib/stack.rb CHANGED
@@ -38,6 +38,20 @@ module Typr
38
38
 
39
39
  def headspace; @header ? 1 : 0 end
40
40
 
41
+ ##
42
+ # Map a mouse click at 1-based terminal (row, col) to a data row id on
43
+ # the current page, or nil when the click lands outside the widget (border,
44
+ # header, margins). Row ids match what hint presses return from `pick`.
45
+ #
46
+ # stack.hit 3, 5 # => 0 (row 3, col 5 hits data row 0)
47
+
48
+ def hit row, col
49
+ return unless row and col
50
+ rel = row - 1 - top - headspace
51
+ return unless rel.between?(0, height - 1) and col.between?(left, right)
52
+ page.to_a[rel]
53
+ end
54
+
41
55
  ##
42
56
  # The usable height after subtracting any header space.
43
57
  #
@@ -50,7 +64,7 @@ module Typr
50
64
  #
51
65
  # stack.print 42 # renders a blank row
52
66
 
53
- def print id; show " " * (width-@margin.size) end
67
+ def print id; draw " " * (width-@margin.size) end
54
68
 
55
69
  ##
56
70
  # Resets widget state by one or more categories.
@@ -80,16 +94,16 @@ module Typr
80
94
  if type.to_s[ /column/ ]
81
95
  positions( row_id )[ @hints_start..-1 ].each_with_index{ |pos, idx|
82
96
  move( left + pos, top + row_id )
83
- show @hints[idx] }
97
+ draw @hints[idx] }
84
98
  else
85
99
  bottom = top + headspace + [height, rows].min - 1
86
100
  (top + headspace..bottom).each{ |pos|
87
- move( left, pos ); show ' ' }
101
+ move( left, pos ); draw ' ' }
88
102
  @hints.chars.each_with_index{ |char, idx|
89
103
  pos = idx + @hints_start + headspace
90
104
  break if top + pos > bottom
91
105
  move( left, top + pos )
92
- show char }
106
+ draw char }
93
107
  end
94
108
  end
95
109
 
@@ -100,11 +114,11 @@ module Typr
100
114
  # This method is called by your application's main loop each frame; it does not handle
101
115
  # events itself (see {#send}).
102
116
 
103
- def draw
117
+ def show
104
118
  if @header
105
119
  color @colors[ :header ]
106
120
  move left,top
107
- show @margin
121
+ draw @margin
108
122
  print :header
109
123
  end
110
124
  list = page.to_a
@@ -116,7 +130,7 @@ module Typr
116
130
  dark=!dark if @alternate
117
131
  background ( select ? @colors[:selected] :
118
132
  (@alternate and dark) ? @colors[:alternate] : @colors[:default][1] )
119
- show @margin
133
+ draw @margin
120
134
  print id
121
135
  background if select
122
136
  end
@@ -193,6 +207,8 @@ module Typr
193
207
  # stack.pick # single row picker
194
208
  # stack.pick :column, 3 # pick a column in row 3
195
209
  # stack.pick "rows" # multi-select rows
210
+ #
211
+ # Mouse: left-click a row to pick it; wheel up/down scrolls the page.
196
212
 
197
213
  def pick type = :row, row=0 #, key=nil
198
214
  type = type.to_s
@@ -203,11 +219,19 @@ module Typr
203
219
  column = pick( :column, page.to_a.index( row + headspace)) or return
204
220
  value = [ row , column ]
205
221
  else
206
- draw
222
+ show
207
223
  draw_hints type, row unless type['none']
208
224
  limit = type['row'] ? height : positions(row).count
209
- key = Typr.read :key
210
- if key.is_a?(String) and value =
225
+ key = Typr.read_key
226
+ if key.is_a?(Typr::Mouse)
227
+ if key.wheel? and key.press?
228
+ send( key.wheel_up? ? @keymap[:up] : @keymap[:down] )
229
+ next
230
+ elsif key.press? and key.left? and type['row'] and
231
+ value = hit( key.y, key.x )
232
+ relative = value if type['relative_']
233
+ end
234
+ elsif key.is_a?(String) and value =
211
235
  @hints[0..limit-@hints_start-1].index(key)
212
236
  value += @hints_start
213
237
  relative = value + @start if type['relative_']