ncumbra 0.1.0 → 0.1.1

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,260 @@
1
+ # ----------------------------------------------------------------------------- #
2
+ # File: table.rb
3
+ # Description: widget for tabular data
4
+ # Author: j kepler http://github.com/mare-imbrium/umbra/
5
+ # Date: 2018-05-06 - 09:56
6
+ # License: MIT
7
+ # Last update: 2018-05-22 14:46
8
+ # ----------------------------------------------------------------------------- #
9
+ # table.rb Copyright (C) 2018 j kepler
10
+
11
+ ##--------- Todo section ---------------
12
+ ## DONE w - next column, b - previous column
13
+ ## TODO paint lines as column separators. issue is panning.
14
+ ## TODO starting visual column (required when scrolling)
15
+ ## DONE change a value value_at(x,y, value) ; << ; delete_at
16
+ ## TODO change column width interactively, hide column , move column
17
+ ## TODO maybe even column_color(n, color_pair, attr)
18
+ ## TODO sort on column/s.
19
+ ## TODO selection will have to be added. maybe we should have extended listbox after all. Or made multiline selectable.
20
+ ## DONE how to format the header
21
+ ## DONE formatting rows
22
+ ## DONE if we want to color specific columns based on values then I think we have to format (render) the row at the last
23
+ ## moment in print_row and not in advance
24
+ ## NOTE: we are setting the data in tabular, not list. So calling list() will give nil until a render has happened.
25
+ ## callers will have to use data() instead of list() which is not consistent.
26
+ ## NOTE: current_index in this object refers to index including header and separator. It is not the offset in the data array.
27
+ ## For that we need to adjust with @data_offset.
28
+ #
29
+ require 'forwardable'
30
+ require 'umbra/tabular'
31
+ require 'umbra/multiline'
32
+
33
+ module Umbra
34
+ ##
35
+ ## A table of columnar data.
36
+ ## This uses Tabular as a table model and extends Multiline.
37
+ #
38
+ class Table < Multiline
39
+
40
+ extend Forwardable
41
+
42
+
43
+ ## tabular is the data model for Table.
44
+ ## It may be passed in in the constructor, or else is created when columns and data are passed in.
45
+ attr_accessor :tabular
46
+
47
+ ## color pair and attribute for header row
48
+ attr_accessor :header_color_pair, :header_attr
49
+
50
+ attr_accessor :rendered ## boolean, if data has changed, we need to re-render
51
+
52
+
53
+ ## Create a Table object passing either a Tabular object or columns and list
54
+ ## e.g. Table.new tabular: tabular
55
+ ## Table.new columns: cols, list: mylist
56
+ ##
57
+ def initialize config={}, &block
58
+ if config.key? :tabular
59
+ @tabular = config.delete(:tabular)
60
+ else
61
+ cols = config.delete(:columns)
62
+ data = config.delete(:list)
63
+ @tabular = Tabular.new cols
64
+ if data
65
+ @tabular.data = data
66
+ end
67
+ end
68
+ @rendered = nil
69
+ super
70
+
71
+ bind_key(?w, "next column") { self.next_column }
72
+ bind_key(?b, "prev column") { self.prev_column }
73
+ bind_key(KEY_RETURN, :fire_action_event)
74
+ ## NOTE: a tabular object should be existing at this point.
75
+ end
76
+
77
+ ## returns the raw data as array of arrays in tabular
78
+ def data
79
+ @tabular.list
80
+ end
81
+
82
+
83
+ def data=(list)
84
+ @rendered = false
85
+ @tabular.data = list
86
+ @repaint_required = true
87
+ self.focusable = true
88
+ @pstart = @current_index = 0
89
+ @pcol = 0
90
+ #$log.debug " before table data= CHANGED "
91
+ #fire_handler(:CHANGED, self) ## added 2018-05-08 -
92
+ end
93
+
94
+ ## render the two-dimensional array of data as an array of Strings.
95
+ ## Calculates data_offset which is the row offset from which data starts.
96
+ def render
97
+ @data_offset = 0
98
+ @data_offset +=1 if @tabular.use_separator
99
+ @data_offset +=1 if @tabular.columns
100
+ self.list = @tabular.render
101
+ end
102
+
103
+ ## paint the table
104
+ def repaint
105
+ render if !@rendered
106
+ super
107
+
108
+ @rendered = true
109
+ end
110
+
111
+ ## Specify how to print the header and separator.
112
+ ## index can be 0 or 1
113
+ ## returns an array of color_pair and attribute
114
+ def color_of_header_row index, state
115
+ arr = [ @header_color_pair || CP_MAGENTA, @header_attr || REVERSE ]
116
+ return arr if index == 0
117
+ [ arr[0], NORMAL ]
118
+ end
119
+
120
+ ## Specify how the data rows are to be coloured.
121
+ ## Override this to have customised row coloring.
122
+ ## @return array of color_pair and attrib.
123
+ def color_of_data_row index, state, data_index
124
+ color_of_row(index, state) ## calling superclass here
125
+ end
126
+
127
+ ## Print the row which could be header or data
128
+ ## @param index [Integer] - index of list, starting with header and separator
129
+ def print_row(win, row, col, str, index, state)
130
+ if index <= @data_offset - 1
131
+ _print_headings(win, row, col, str, index, state)
132
+ else
133
+ data_index = index - @data_offset ## index into actual data object
134
+ _print_data(win, row, col, str, index, state, data_index)
135
+ end
136
+ end
137
+
138
+ ## Print the header row
139
+ ## index [Integer] - should be 0 or 1 (1 for optional separator)
140
+ def _print_headings(win, row, col, str, index, state)
141
+ arr = color_of_header_row(index, state)
142
+ win.printstring(row, col, str, arr[0], arr[1])
143
+ end
144
+
145
+
146
+
147
+ ## Print the data.
148
+ ## index is index into visual row, starting 0 for headings, and 1 for separator
149
+ ## data_index is index into actual data object. Use this if checking actual data array
150
+ def _print_data(win, row, col, str, index, state, data_index)
151
+ data_index = index - @data_offset ## index into actual data object
152
+ arr = color_of_data_row(index, state, data_index)
153
+
154
+ win.printstring(row, col, str, arr[0], arr[1])
155
+ end
156
+ def color_of_column ix, value, defaultcolor
157
+ raise "unused yet"
158
+ end
159
+
160
+
161
+
162
+ def row_count
163
+ @tabular.list.size
164
+ end
165
+
166
+
167
+ ## return rowid (assumed to be first column)
168
+ def current_id
169
+ data = current_row_as_array()
170
+ return nil unless data
171
+ data.first
172
+ end
173
+ # How do I deal with separators and headers here - return nil
174
+ ## This returns all columns including hidden so rowid can be accessed
175
+ def current_row_as_array
176
+ data_index = @current_index - @data_offset ## index into actual data object
177
+ return nil if data_index < 0 ## separator and heading
178
+ data()[data_index]
179
+ end
180
+
181
+ ## returns the current row as a hash with column name as key.
182
+ def current_row_as_hash
183
+ data = current_row_as_array
184
+ return nil unless data
185
+ columns = @tabular.columns
186
+ hash = columns.zip(data).to_h
187
+ end
188
+
189
+ ## Move cursor to next column
190
+ def next_column
191
+ @coffsets = @tabular._calculate_column_offsets unless @coffsets
192
+ #c = @column_pointer.next
193
+ current_column = current_column_offset() +1
194
+ if current_column > @tabular.column_count-1
195
+ current_column = 0
196
+ end
197
+ cp = @coffsets[current_column]
198
+ @curpos = cp if cp
199
+ $log.debug " next_column #{@coffsets} :::: #{cp}, curpos=#{@curpos} "
200
+ set_col_offset @curpos
201
+ #down() if c < @column_pointer.last_index
202
+ #fire_column_event :ENTER_COLUMN
203
+ end
204
+
205
+ ## Move cursor to previous column
206
+ def prev_column
207
+ @coffsets = @tabular._calculate_column_offsets unless @coffsets
208
+ #c = @column_pointer.next
209
+ current_column = current_column_offset() -1
210
+ if current_column < 0 #
211
+ current_column = @tabular.column_count-1
212
+ end
213
+ cp = @coffsets[current_column]
214
+ @curpos = cp if cp
215
+ $log.debug " next_column #{@coffsets} :::: #{cp}, curpos=#{@curpos} "
216
+ set_col_offset @curpos
217
+ #down() if c < @column_pointer.last_index
218
+ #fire_column_event :ENTER_COLUMN
219
+ end
220
+
221
+ # Convert current cursor position to a table column
222
+ # calculate column based on curpos since user may not have
223
+ # used w and b keys (:next_column)
224
+ # @return [Integer] column index base 0
225
+ def current_column_offset
226
+ _calculate_column_offsets unless @coffsets
227
+ x = 0
228
+ @coffsets.each_with_index { |i, ix|
229
+ if @curpos < i
230
+ break
231
+ else
232
+ x += 1
233
+ end
234
+ }
235
+ x -= 1 # since we start offsets with 0, so first auto becoming 1
236
+ return x
237
+ end
238
+
239
+ def header_row?
240
+ @current_index == 0 and @data_offset > 0
241
+ end
242
+
243
+ ## Handle case where ENTER/RETURN pressed on header row (so sorting can be done).
244
+ def fire_action_event
245
+ if header_row?
246
+ # TODO sorting here
247
+ $log.debug " PRESSED ENTER on header row, TODO sorting here"
248
+ end
249
+ super
250
+ end
251
+
252
+ ## delegate calls to the tabular object
253
+ def_delegators :@tabular, :headings=, :columns= , :add, :add_row, :<< , :column_width, :column_align, :column_hide, :convert_value_to_text, :separator, :to_string, :x=, :y=, :column_unhide
254
+ def_delegators :@tabular, :columns , :numbering
255
+ def_delegators :@tabular, :column_hidden, :delete_at, :value_at
256
+
257
+ end # class
258
+ end # module
259
+
260
+ # vim: comments=sr\:##,mb\:##,el\:#/,\:## :
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env ruby -w
2
+ =begin
3
+ * Name : A Quick take on tabular data. Readonly.
4
+ * Description : To show tabular data inside a control, rather than going by the huge
5
+ Table object, I want to create a simple, minimal table data generator.
6
+ This will be thrown into a TextView for the user to navigate, select
7
+ etc.
8
+ I would use this applications where the tabular data is fairly fixed
9
+ not where i want the user to select columns, move them, expand etc.
10
+ * :
11
+ * Author : jkepler
12
+ * Date :
13
+ * Last Update : 2018-05-20 14:24
14
+ * License : MIT
15
+ =end
16
+
17
+ ## Todo Section --------------
18
+ ## What if user wishes to supply formatstring and override ours
19
+ ## ---------------------------
20
+
21
+
22
+ # A simple tabular data generator. Given table data in arrays and a column heading row in arrays, it
23
+ # quickely generates tabular data. It only takes left and right alignment of columns into account.
24
+ # You may specify individual column widths. Else it will take the widths of the column names you supply
25
+ # in the startup array. You are encouraged to supply column widths.
26
+ # If no columns are specified, and no widths are given, it take the widths of the first row
27
+ # as a model to determine column widths.
28
+ #
29
+ module Umbra
30
+
31
+ class Tabular
32
+ GUESSCOLUMNS = 30
33
+
34
+ def yield_or_eval &block
35
+ return unless block
36
+ if block.arity > 0
37
+ yield self
38
+ else
39
+ self.instance_eval(&block)
40
+ end
41
+ end
42
+
43
+
44
+ ## stores column info internally: name, width and alignment
45
+ class ColumnInfo < Struct.new(:name, :width, :index, :offset, :align, :hidden)
46
+ end
47
+
48
+
49
+
50
+
51
+ ## an array of column titles
52
+ attr_reader :columns
53
+
54
+ ## data which is array of arrays: rows and columns
55
+ attr_reader :list
56
+
57
+ # boolean, does user want lines numbered
58
+ attr_accessor :numbering
59
+
60
+ attr_accessor :use_separator ## boolean. Use a separator line after heading or not.
61
+
62
+ # x is the + character used a field delim in separators
63
+ # y is the field delim used in data rows, default is pipe or bar
64
+ attr_accessor :x, :y
65
+
66
+ # takes first optional argument as array of column names
67
+ # second optional argument as array of data arrays
68
+ # @yield self
69
+ #
70
+ def initialize cols=nil, *args, &block
71
+ @chash = [] # hash of column info, not used
72
+ @_skip_columns = {} # internal, which columns not to calc width of since user has specified
73
+ @separ = @columns = @numbering = nil
74
+ @y = '|'
75
+ @x = '+'
76
+ @use_separator = false
77
+ @_hidden_columns_flag = false
78
+ self.columns = cols if cols
79
+ if !args.empty?
80
+ self.data = args
81
+ end
82
+ yield_or_eval(&block) if block_given?
83
+ end
84
+ #
85
+ # set columns names .
86
+ ## NOTE that we are not clearing chash here. In case, someone changes table and columns.
87
+ # @param [Array<String>] column names, preferably padded out to width for column
88
+ def columns=(array)
89
+ #$log.debug "tabular got columns #{array.count} #{array.inspect} " if $log
90
+ @columns = array
91
+ @columns.each_with_index { |e,i|
92
+ #@chash[i] = ColumnInfo.new(c, c.to_s.length)
93
+ c = get_column(i)
94
+ c.name = e
95
+ c.width = e.to_s.length
96
+ #@chash[i] = c
97
+ #@cw[i] ||= c.to_s.length
98
+ #@calign[i] ||= :left # 2011-09-27 prevent setting later on
99
+ }
100
+ end
101
+ alias :headings= :columns=
102
+ #
103
+ # set data as an array of arrays
104
+ # @param [Array<Array>] data as array of arrays
105
+ def data=(list)
106
+ #puts "got data: #{list.size} " if !$log
107
+ #puts list if !$log
108
+ @list = list
109
+ end
110
+
111
+ # add a row of data
112
+ # @param [Array] an array containing entries for each column
113
+ def add array
114
+ #$log.debug "tabular got add #{array.count} #{array.inspect} " if $log
115
+ @list ||= []
116
+ @list << array
117
+ end
118
+ alias :<< :add
119
+ alias :add_row :add
120
+
121
+ # retrieve the column info structure for the given offset. The offset
122
+ # pertains to the visible offset not actual offset in data model.
123
+ # These two differ when we move a column.
124
+ # @return ColumnInfo object containing width align color bgcolor attrib hidden
125
+ def get_column index
126
+ return @chash[index] if @chash[index]
127
+ # create a new entry since none present
128
+ c = ColumnInfo.new
129
+ c.index = index
130
+ @chash[index] = c
131
+ return c
132
+ end
133
+ # set width of a given column, any data beyond this will be truncated at display time.
134
+ # @param [Number] column offset, starting 0
135
+ # @param [Number] width
136
+ def column_width colindex, width=:NONE
137
+ if width == :NONE
138
+ #return @cw[colindex]
139
+ return get_column(colindex).width
140
+ end
141
+ @_skip_columns[colindex] = true ## don't calculate col width for this.
142
+ get_column(colindex).width = width
143
+ self
144
+ end
145
+
146
+ def column_hidden colindex, flag=:NONE
147
+ if flag == :NONE
148
+ return get_column(colindex).hidden
149
+ #return @chide[colindex]
150
+ end
151
+ @_hidden_columns_flag = true if flag
152
+ #@chide[colindex] = flag
153
+ get_column(colindex).hidden = flag
154
+ self
155
+ end
156
+
157
+ # set alignment of given column offset
158
+ # @param [Number] column offset, starting 0
159
+ # @param [Symbol] :left, :right
160
+ def column_align colindex, lrc=:NONE
161
+ if lrc == :NONE
162
+ return get_column(colindex).align
163
+ #return @calign[colindex]
164
+ end
165
+ raise ArgumentError, "wrong alignment value sent" if ![:right, :left, :center].include? lrc
166
+ get_column(colindex).align = lrc
167
+ self
168
+ end
169
+
170
+ ## return an array of visible columns names
171
+ def visible_column_names
172
+ visible = []
173
+ @chash.each_with_index do |c, ix|
174
+ if !c.hidden
175
+ if block_given?
176
+ yield c.name, ix
177
+ else
178
+ visible << c.name
179
+ end
180
+ end
181
+ end
182
+ return visible unless block_given?
183
+ end
184
+
185
+ ## returns the count of visible columns based on column names.
186
+ ## NOTE: what if no column names gives ???
187
+ def column_count
188
+ visible_column_names().count
189
+ end
190
+
191
+ # yields non-hidden columns (ColumnInfo) and the offset/index
192
+ # This is the order in which columns are to be printed
193
+ def each_column
194
+ @chash.each_with_index { |c, i|
195
+ next if c.hidden
196
+ yield c,i if block_given?
197
+ }
198
+ end
199
+
200
+ ## for the given row, return visible columns as an array
201
+ ## @yield column and index
202
+ def visible_columns(row)
203
+ visible = []
204
+ row.each_with_index do |e, ix|
205
+ hid = @chash[ix].hidden
206
+ if !hid
207
+ if block_given?
208
+ yield e, ix
209
+ else
210
+ visible << e
211
+ end
212
+ end
213
+ end
214
+ return visible if !block_given?
215
+ end
216
+
217
+ #
218
+ # Now returns an array with formatted data
219
+ # @return [Array<String>] array of formatted data
220
+ def render
221
+ raise "tabular:: list is nil " unless @list
222
+ $log.debug " render list:: #{@list.size} "
223
+ #$log.debug " render list:1: #{@list} "
224
+ raise "tabular:: columns is nil " unless @columns
225
+ buffer = []
226
+ @separ = nil
227
+ _guess_col_widths
228
+ rows = @list.size.to_s.length
229
+ #@rows = rows
230
+ fmstr = _prepare_format
231
+ $log.debug "tabular: fmstr:: #{fmstr}"
232
+ $log.debug "tabular: cols: #{@columns}"
233
+ #$log.debug "tabular: data: #{@list}"
234
+
235
+ str = ""
236
+ if @numbering
237
+ str = " "*(rows+1)+@y
238
+ end
239
+ #str << fmstr % visible_column_names()
240
+ str << convert_heading_to_text(visible_column_names(), fmstr)
241
+ buffer << str
242
+ #puts "-" * str.length
243
+ buffer << separator if @use_separator
244
+ if @list ## XXX why wasn't this done in _prepare_format ???? FIXME
245
+ if @numbering
246
+ fmstr = "%#{rows}d "+ @y + fmstr
247
+ end
248
+ #@list.each { |e| puts e.join(@y) }
249
+ count = 0
250
+ @list.each_with_index { |r,i|
251
+ if r == :separator
252
+ buffer << separator
253
+ next
254
+ end
255
+ if @_hidden_columns_flag
256
+ r = visible_columns(r)
257
+ end
258
+ if @numbering
259
+ r.insert 0, count+1
260
+ end
261
+ #value = convert_value_to_text r, count
262
+ value = convert_value_to_text r, fmstr, i
263
+ buffer << value
264
+ count += 1
265
+ }
266
+ end
267
+ buffer
268
+ end
269
+
270
+ ## render_row
271
+ def convert_value_to_text r, fmstr, index
272
+ return fmstr % r;
273
+ end
274
+ def convert_heading_to_text r, fmstr
275
+ return fmstr % r;
276
+ end
277
+ # use this for printing out on terminal
278
+ # NOTE: Do not name this to_s as it will print the entire content in many places in debug statements
279
+ # @example
280
+ # puts t.to_s
281
+ def to_string
282
+ render().join "\n"
283
+ end
284
+
285
+ def value_at x,y, value=:NONE
286
+ if value == :NONE
287
+ return @list[x, y]
288
+ end
289
+ @list[x, y] = value
290
+ end
291
+ def delete_at ix
292
+ return unless @list
293
+ raise ArgumentError, "Argument must be within 0 and #{@list.length}" if ix < 0 or ix >= @list.length
294
+ #fire_dimension_changed
295
+ #@list.delete_at(ix + @_header_adjustment)
296
+ @list.delete_at(ix)
297
+ end
298
+
299
+
300
+
301
+
302
+ def add_separator
303
+ @list << :separator
304
+ end
305
+
306
+ ## This refers to a separator line after the heading and not a field separator.
307
+ ## Badly named !
308
+ def separator
309
+ return @separ if @separ
310
+ str = ""
311
+ if @numbering
312
+ str = "-"*(rows+1)+@x
313
+ end
314
+ each_column { | c, ix|
315
+ v = c.width
316
+ next if v == 0 ## hidden column
317
+ str << "-" * (v+1) + @x
318
+ }
319
+ @separ = str.chop
320
+ end
321
+
322
+ ## This calculates and stores the offset at which each column starts.
323
+ ## Used when going to next column or doing a find for a string in the table.
324
+ def _calculate_column_offsets
325
+ total = 0
326
+ coffsets = []
327
+ ctr = 0
328
+ ## ix will have gaps in between for hidden fields
329
+ each_column { | c, ix|
330
+ v = c.width
331
+ coffsets[ctr] = total
332
+ ctr += 1
333
+ total += v + 2 ## blank space plus separator
334
+ }
335
+ return coffsets
336
+ end
337
+
338
+
339
+ private
340
+ def _guess_col_widths #:nodoc:
341
+ @list.each_with_index { |r, i|
342
+ break if i > GUESSCOLUMNS
343
+ next if r == :separator
344
+ r.each_with_index { |c, j|
345
+ ## we need to skip those columns which user has specified
346
+ next if @_skip_columns[j] == true
347
+ #next if @chide[j]
348
+ next if @chash[j].hidden
349
+ x = c.to_s.length
350
+ w = @chash[j].width
351
+ if !w
352
+ @chash[j].width = x
353
+ else
354
+ @chash[j].width = x if x > w
355
+ end
356
+ }
357
+ }
358
+ end
359
+
360
+ ## prepare formatstring.
361
+ ## NOTE: this is not the final value.
362
+ ## render adds numbering to this, if user has set numbering option.!!!!
363
+ def _prepare_format #:nodoc:
364
+ fmstr = nil
365
+ fmt = []
366
+ each_column { |c, i|
367
+ ## trying a zero for hidden columns
368
+ ## worked but an extra space is added below and the sep
369
+ w = c.width
370
+ case c.align
371
+ when :right
372
+ #fmt << "%.#{w}s "
373
+ fmt << "%#{w}.#{w}s "
374
+ else
375
+ fmt << "%-#{w}.#{w}s "
376
+ end
377
+ }
378
+ ## the next line will put a separator after hidden columns also
379
+ fmstr = fmt.join(@y)
380
+ #puts "format: #{fmstr} " # 2011-12-09 23:09:57
381
+ return fmstr
382
+ end
383
+ end
384
+ end
385
+
386
+ if __FILE__ == $PROGRAM_NAME
387
+ include Umbra
388
+ $log = nil
389
+ t = Tabular.new(['a', 'b'], [1, 2], [3, 4])
390
+ puts t.to_string
391
+ puts
392
+ t = Tabular.new([" Name ", " Number ", " Email "])
393
+ t.add %w{ rahul 32 r@ruby.org }
394
+ t << %w{ _why 133 j@gnu.org }
395
+ t << %w{ Jane 1331 jane@gnu.org }
396
+ t.column_width 1, 10
397
+ t.column_align 1, :right
398
+ puts t.to_string
399
+ puts
400
+
401
+ s = Tabular.new do |b|
402
+ b.columns = %w{ country continent text }
403
+ b << ["india","asia","a warm country" ]
404
+ b << ["japan","asia","a cool country" ]
405
+ b << ["russia","europe","a hot country" ]
406
+ b.column_width 2, 30
407
+ end
408
+ puts s.to_string
409
+ puts
410
+ puts "::::"
411
+ puts
412
+ s = Tabular.new do |b|
413
+ b.columns = %w{ place continent text }
414
+ b << ["india","asia","a warm country" ]
415
+ b << ["japan","asia","a cool country" ]
416
+ b << ["russia","europe","a hot country" ]
417
+ b << ["sydney","australia","a dry country" ]
418
+ b << ["canberra","australia","a dry country" ]
419
+ b << ["ross island","antarctica","a dry country" ]
420
+ b << ["mount terror","antarctica","a windy country" ]
421
+ b << ["mt erebus","antarctica","a cold place" ]
422
+ b << ["siberia","russia","an icy city" ]
423
+ b << ["new york","USA","a fun place" ]
424
+ b.column_width 0, 12
425
+ b.column_width 1, 12
426
+ b.numbering = true
427
+ end
428
+ puts s.to_string
429
+ end
430
+
431
+ # vim: comments=sr\:##,mb\:##,el\:#/,\:## :