viewworkbook 0.1.3

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,96 @@
1
+ #encoding: UTF-8
2
+
3
+ =begin
4
+ /*****************************************************************************
5
+ * Copyright © 2016-2016, Michael Uplawski <michael.uplawski@uplawski.eu> *
6
+ * *
7
+ * This program is free software; you can redistribute it and/or modify *
8
+ * it under the terms of the GNU General Public License as published by *
9
+ * the Free Software Foundation; either version 3 of the License, or *
10
+ * (at your option) any later version. *
11
+ * *
12
+ * This program is distributed in the hope that it will be useful, *
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15
+ * GNU General Public License for more details. *
16
+ * *
17
+ * You should have received a copy of the GNU General Public License *
18
+ * along with this program; if not, write to the *
19
+ * Free Software Foundation, Inc., *
20
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
21
+ *****************************************************************************/
22
+ =end
23
+
24
+ require 'roo'
25
+ require 'roo-xls'
26
+ require 'filemagic'
27
+
28
+ require_relative 'logging'
29
+
30
+ # Transforms a file into a Roo spreadsheet instance.
31
+
32
+ ODS_Magic = "OpenDocument Spreadsheet"
33
+ XLS_Magic = "Composite Document File V2 Document"
34
+ XLSX_Magic = "Microsoft OOXML"
35
+
36
+ ODS_MIME = "application/vnd.oasis.opendocument.spreadsheet"
37
+ XLS_MIME = "application/vnd.ms-excel"
38
+ XLSX_MIME = "application/octet-stream"
39
+ CSV_MIME = "text/plain"
40
+
41
+ class SheetData
42
+ self::extend(Logging)
43
+ @@log = init_logger(STDOUT, Logger::INFO)
44
+
45
+ @@file = nil
46
+ @@workbook = nil
47
+
48
+ def self::workbook(file)
49
+ @@file = file
50
+ if !@@workbook
51
+ begin
52
+ magic, mime = file_type
53
+ @@workbook = case mime
54
+ when ODS_MIME
55
+ if magic.match ODS_Magic
56
+ @@log.debug('ODS')
57
+ Roo::Spreadsheet::open(@@file, extension: :ods )
58
+ end
59
+ when XLS_MIME
60
+ if magic.match XLS_Magic
61
+ @@log.debug('XLS')
62
+ Roo::Spreadsheet::open(@@file, extension: :xls )
63
+ end
64
+ when XLSX_MIME
65
+ if magic.match XLSX_Magic
66
+ @@log.debug('XLSX')
67
+ Roo::Spreadsheet::open(@@file, extension: :xlsx )
68
+ end
69
+ when CSV_MIME
70
+ raise IOError.new('CSV is not yet supported, sorry')
71
+ else
72
+ raise IOError.new('Mime-Type is ' << mime << ' and Magic sais: ' << magic << ". Is this supposed to be a spreadsheet?")
73
+ end
74
+ rescue Exception => ex
75
+ msg = 'ERROR! File %s is not supported: %s' %[@@file, ex.message]
76
+ puts msg
77
+ @log.error yellow(msg)
78
+ exit false
79
+ end
80
+
81
+ return @@workbook
82
+ end
83
+ end
84
+
85
+ private
86
+ # derive the mime-type from the file
87
+ def self::file_type
88
+ fm = FileMagic.fm
89
+ file_magic = fm.file(@@file)
90
+ fm.flags = [:mime_type]
91
+ file_mime = fm.file(@@file)
92
+ @log.debug('File type is ' << file_magic << ', Mime-type is ' << file_mime)
93
+ return file_magic, file_mime
94
+ end
95
+ end
96
+
@@ -0,0 +1,464 @@
1
+ #encoding: UTF-8
2
+
3
+ =begin
4
+ /***************************************************************************
5
+ * Copyright © 2014 - 2017, Michael Uplawski <michael.uplawski@uplawski.eu> *
6
+ * *
7
+ * This program is free software; you can redistribute it and/or modify *
8
+ * it under the terms of the GNU General Public License as published by *
9
+ * the Free Software Foundation; either version 3 of the License, or *
10
+ * (at your option) any later version. *
11
+ * *
12
+ * This program is distributed in the hope that it will be useful, *
13
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
14
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15
+ * GNU General Public License for more details. *
16
+ * *
17
+ * You should have received a copy of the GNU General Public License *
18
+ * along with this program; if not, write to the *
19
+ * Free Software Foundation, Inc., *
20
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
21
+ ***************************************************************************/
22
+ =end
23
+
24
+ require 'roo'
25
+ require 'io/wait'
26
+ require 'io/console'
27
+
28
+ require_relative 'logging'
29
+ require_relative 'scrollable'
30
+ require_relative 'row'
31
+ require_relative 'cell'
32
+ require_relative 'column'
33
+ require_relative 'color_output'
34
+ require_relative 'busy_indicator/busy_indicator'
35
+ require_relative 'user_input'
36
+ require_relative 'menu'
37
+ require_relative 'action'
38
+ require_relative 'translating'
39
+
40
+ # So this draws a table complete with the content of the cells from the
41
+ # currently selected spreadsheet.
42
+ # The really ingenuous stuff was provided by a helpful user on a newsgroup.
43
+ # Really... Usenet! Like in ... 2014!
44
+
45
+ #TODO: provide conversion for column designators to int vv, also for AA to ZZ !
46
+
47
+ class SheetInterface
48
+ include Logging
49
+ include Translating
50
+
51
+ class Status
52
+ def show
53
+ if(@message)
54
+ puts @message
55
+ @message.clear
56
+ end
57
+ end
58
+ attr_writer :message
59
+ end
60
+
61
+ def initialize(wb, num)
62
+ init_logger(STDOUT, Logger::INFO)
63
+ @workbook = wb.sheet(num)
64
+ @menu = nil
65
+ @status = Status.new
66
+ construct
67
+ end
68
+
69
+ private
70
+ =begin
71
+ returns [width, height] in columns and lines of the current
72
+ terminal-window.
73
+ =end
74
+ def terminal_size
75
+ size = `stty size`.split.map { |x| x.to_i }.reverse
76
+ return size
77
+ end
78
+ # Asks the user to enter a number, then verifies ageinst eventual
79
+ # constraints.
80
+ def ask_number(question, &constraint)
81
+ num = nil
82
+ 3.times do
83
+ print question.dup << " "
84
+ begin
85
+ num = STDIN.gets()
86
+ rescue Interrupt
87
+ return nil
88
+ end
89
+ num = num.to_i
90
+ if (constraint && !yield(num))
91
+ num = nil
92
+ end
93
+ return num if num
94
+ end
95
+ if !num
96
+ @status.message = "Inacceptable value, doing nothing."
97
+ return nil
98
+ end
99
+ end
100
+
101
+ # Shortcutting from “user inputs bull” to “program interrupts”
102
+ # without the need for the program to actually run havoc, first.
103
+ def ask_string(question, &constraint)
104
+ str = nil
105
+ until str
106
+ print question.dup << " "
107
+ begin
108
+ str = STDIN.gets().chomp!
109
+ rescue Interrupt
110
+ return nil
111
+ end
112
+ if("" == str || "\e" == str)
113
+ return nil
114
+ end
115
+ if(constraint && !yield(str))
116
+ str = nil
117
+ end
118
+ end
119
+ if !str
120
+ @status.message = "Inacceptable value, doing nothing."
121
+ end
122
+ return str
123
+ end
124
+
125
+ # Write content to a new file
126
+ def new_file()
127
+ file = nil
128
+ until file
129
+ print 'file path:' << ' '
130
+ begin
131
+ file = STDIN.gets().chomp!
132
+ rescue Interrupt
133
+ return nil
134
+ end
135
+ if(file && File::exist?(file) )
136
+ if(File::writable?(file) )
137
+ print "File #{file} exists! Do you want to overwrite its content, now (j/N)? "
138
+ answer = "%c" %wait_for_user
139
+ if('j' != answer.downcase)
140
+ file = nil
141
+ else
142
+ puts
143
+ end
144
+ else
145
+ puts "File #{file} is not writable! Please name a different file path"
146
+ file = nil
147
+
148
+ end
149
+ end
150
+ end
151
+ return file
152
+ end
153
+
154
+ def ask_cell_coord()
155
+ coord = nil
156
+ until coord
157
+ print 'Which cell (col:row)? '
158
+ begin
159
+ coord = STDIN.gets().chomp!
160
+ rescue Interrupt
161
+ return nil
162
+ end
163
+ if('' == coord || !coord.include?(':') || "\e" == coord)
164
+ return nil
165
+ else
166
+ col, row = coord.split(':')
167
+ row = row.to_i
168
+ col = col.strip
169
+ end
170
+ return [col, row]
171
+ end
172
+ end
173
+
174
+ def set_sheet(sheet)
175
+ current_sheet = @workbook.default_sheet
176
+ @workbook = @workbook.sheet(sheet)
177
+ columns = @workbook.last_column
178
+ if columns && columns > 0
179
+ @rows = nil
180
+ @columns = nil
181
+ else
182
+ @workbook = @workbook.sheet(current_sheet)
183
+ @status.message = red("Sheet #{sheet} contains no data!")
184
+ end
185
+ end
186
+
187
+ def open_sheet(designator = :number)
188
+ sn = nil
189
+ if designator == :number
190
+ sn = ask_number("Sheet number (max #{@workbook.sheets.size() - 1})?") do |num|
191
+ num < @workbook.sheets.size
192
+ end
193
+ else
194
+ sn = ask_string("Sheet name (#{@workbook.sheets.join(', ')})?") do |name|
195
+ @workbook.sheets.include?(name)
196
+ end
197
+ end
198
+
199
+ if sn && !sn.to_s.empty?
200
+ @log.debug('calling workbook.sheet with ' << sn.to_s)
201
+ @log.debug('@workbook is of type ' << @workbook.class.name)
202
+ set_sheet(sn)
203
+ end
204
+ construct
205
+ end
206
+
207
+ # Adds concrete actions to the menu.
208
+ # Note the calls to ask_number and ask_string with constraints.
209
+ def define_actions(menu)
210
+ sm_Menu = Menu.new(:name => 'sheet', :key => 's')
211
+ sm_Menu.add_action(Action.new(:name => 'sheet number', :key => '#') {open_sheet(:number)})
212
+ sm_Menu.add_action(Action.new(:name => 'sheet name', :key => 'n') {open_sheet(:name)})
213
+
214
+ save_Menu = Menu.new(:name => 'save to file', :key => 'f')
215
+ save_Menu.add_action(Action.new(:name => 'current sheet', :key => 'c') do
216
+ file = new_file()
217
+ if(file)
218
+ File::open(file, 'w+') {|out| draw(out) }
219
+ @status.message = green('... done')
220
+ end
221
+ construct
222
+ end)
223
+
224
+ save_Menu.add_action(Action.new(:name => 'all sheets', :key => 'a') do
225
+ current_sheet = @workbook.default_sheet
226
+ @status.message = "\nATTN! If saving takes a long time, your spreadsheet may contain useless data or many empty cells. Check the visible width and height of your tables.\n"
227
+ file = new_file()
228
+ if(file)
229
+ bi = BusyIndicator.new(true)
230
+ begin
231
+ File::open(file, 'w+') do |out|
232
+ @workbook.sheets.each do |s|
233
+ @log.debug('writing sheet ' << s)
234
+ set_sheet(s)
235
+ construct(false)
236
+ draw(out)
237
+ end
238
+ end
239
+ rescue Interrupt
240
+ puts bold(red("Interrupted by user. Bye!"))
241
+ exit true
242
+ end
243
+ bi.stop('... done')
244
+ end
245
+ set_sheet(current_sheet)
246
+ construct
247
+ end)
248
+
249
+ col_Menu = Menu.new(:name=>'column', :key => 'c')
250
+ col_Menu.add_action(Action.new(:name => 'column width', :key => 'w') do |num|
251
+ num = ask_number('column width (min. 4) ?'){|w| w > 3}
252
+ Column::col_width = num if num
253
+ construct
254
+ end)
255
+
256
+ menu.add_action(Action.new(:name => 'value', :key => 'v') do
257
+ cell_coord = ask_cell_coord
258
+ if(cell_coord && cell_coord.length == 2)
259
+ row = cell_coord[1] - 1
260
+ col = cell_coord[0].upcase.bytes[0] - 65
261
+ cell = nil
262
+ if(@rows[row] && @columns[col])
263
+ cell = @rows[row].cells.detect {|c| c.col == col}
264
+ if(cell)
265
+ @status.message = "#{cell_coord[0].upcase}:#{cell_coord[1]} is \"" << cell.value.to_s << "\" (%d)" %cell.value.to_s.length
266
+ else
267
+ @status.message = red('invalid cell')
268
+ end
269
+ else
270
+ @status.message = red(cell_coord.join(':') << ': out of range')
271
+ end
272
+ elsif(cell_coord && cell_coord.length > 0)
273
+ @status.message = red('invalid input')
274
+ end
275
+ construct
276
+ end)
277
+
278
+ menu.add_menu(save_Menu)
279
+ menu.add_menu(sm_Menu)
280
+ menu.add_menu(col_Menu)
281
+ menu.add_action(Action.new(:name => "⇧", :key => "i") {@table_view.up; refresh()})
282
+ menu.add_action(Action.new(:name => "⇩", :key => "k") {@table_view.down; refresh()})
283
+ menu.add_action(Action.new(:name => "⇦", :key => "j") {@table_view.left; refresh()})
284
+ menu.add_action(Action.new(:name => "⇨", :key => "l") {@table_view.right; refresh()})
285
+ menu.add_action(Action.new(:global => true, :hidden => true, :name => '', :key => "\e") {refresh})
286
+ menu.add_action(Action.new(:global => true, :name => 'quit', :key => 'q') {puts 'Bye ';exit 0})
287
+ end
288
+ # This is hard to describe.
289
+ # The function basically draws a table on screen.
290
+ def draw(out = STDOUT)
291
+ if(@rows && !@rows.empty?)
292
+ # Welcome to hell.
293
+ on_screen = STDOUT == out
294
+ col_w = Column::col_width
295
+ num_c = @rows[0].cells.length()
296
+ num_r = @rows.length
297
+ ch = nil
298
+ hline = nil
299
+ #-----
300
+ table_view = ''
301
+ #-----
302
+
303
+ table_view << @workbook.default_sheet << "\n"
304
+ #out.puts @workbook.default_sheet
305
+ table_view << ('┌' << '─' * num_r.to_s.length << '┬' << (('─' * col_w) << '┬' ) * (num_c -1) ) << (('─' * col_w) << '┐' ) << "\n"
306
+ # out.puts ('┌' << '─' * num_r.to_s.length << '┬' << (('─' * col_w) << '┬' ) * (num_c -1) ) << (('─' * col_w) << '┐' )
307
+ lh = 0
308
+ @rows.each do |row|
309
+
310
+ if(row == @rows.first)
311
+ ch = 'A'
312
+ table_view << "│%#{num_r.to_s.length}s" %" "
313
+ #out.print "│%#{num_r.to_s.length}s" %" "
314
+ table_view << '│'
315
+ # out.print '│'
316
+ row.each do |c|
317
+ head = "%#{col_w}s" %ch
318
+ head = bold(head) if(on_screen)
319
+ # out.print(head << "|")
320
+ table_view << (head << "│")
321
+ ch = ch.next
322
+ end
323
+ # out.puts
324
+ table_view << "\n"
325
+ hline = ('├' << ('─' * (num_r.to_s.length) ) <<'┼' << (('─' * col_w) << '┼' ) * (num_c - 1) ) << (('─' * col_w) << '┤')if !hline
326
+ table_view << hline << "\n"
327
+ # out.puts hline
328
+ end
329
+ lh += 1
330
+ table_view << "│"
331
+ # out.print "│"
332
+ line_head = "%#{num_r.to_s.length}s" %lh.to_s
333
+ line_head = bold(line_head) if on_screen
334
+ table_view << line_head
335
+ # out.print line_head
336
+
337
+ # out.print "│" << bold("%#{num_r.to_s.length}s" %line_head)
338
+
339
+ row.height.times do |li|
340
+ table_view << "│%#{num_r.to_s.length}s" %" " if li > 0
341
+ # out.print "│%#{num_r.to_s.length}s" %" " if li > 0
342
+ table_view << '│'
343
+ # out.print '│'
344
+ row.each do |cell|
345
+ line = cell.line(li)
346
+ content = "%#{col_w}s│" %line
347
+ if(on_screen && line && line.end_with?('...'))
348
+ content = red(content)
349
+ end
350
+ table_view << content
351
+ # out.print content
352
+ end
353
+ table_view << "\n"
354
+ # out.puts
355
+ end
356
+ hlline = ('│' << (('─' * col_w) << '│' ) * num_c ) if !hline
357
+
358
+ table_view << hline << "\n"
359
+ # out.puts hline
360
+
361
+ # Some of the following code may go to a function as it repeats the
362
+ # procedure from the beginning of the table. I am currently unwilling to
363
+ # do it, because all the rest must stay so verbose that another
364
+ # function-call at this position cannot contribute to a better
365
+ # understanding of this ... table-generating MESS!!
366
+ if(row == @rows.last)
367
+ ch = 'A'
368
+ table_view << "│%#{num_r.to_s.length}s" %" "
369
+ # out.print "│%#{num_r.to_s.length}s" %" "
370
+ table_view << '│'
371
+ # out.print '│'
372
+ row.each do |c|
373
+ head = "%#{col_w}s" %ch
374
+ head = bold(head) if on_screen
375
+ table_view << ( head << '│')
376
+ # out.print( head << '│')
377
+ ch = ch.next
378
+ end
379
+ table_view << "\n"
380
+ # out.puts
381
+ hline = ('└' << ('─' * (num_r.to_s.length) ) << '┴' << (('─' * col_w) << '┴' ) * (num_c - 1) ) << (('─' * col_w) << '┘' )
382
+
383
+ table_view << hline << "\n"
384
+ # out.puts hline
385
+ end
386
+
387
+ end
388
+ =begin
389
+ table_width = hline.length
390
+ tw = terminal_size[0]
391
+ if(on_screen && table_width > tw)
392
+ @status.message = red("ATTN! Terminal is to narrow to display the whole table (#{tw} characters for #{table_width}). Diminish column-width!")
393
+ end
394
+ =end
395
+ return table_view
396
+ else
397
+ return nil
398
+ end
399
+ end
400
+
401
+ # The function which is called to recreate the view.
402
+ # It prepares the table-generation by transforming the Roo-instance to
403
+ # objects of the classes Cell, Row and Column, then calls draw(), above and
404
+ # adds the status- and menu-lines. The menu waits for user input (blocking)
405
+ # before construct() returns, no other thread or “event-loop” involved there.
406
+ def construct(on_screen = true)
407
+ puts "... reading spreadsheet data (%s), PSE wait" %[@workbook.default_sheet]
408
+ bi = BusyIndicator.new()
409
+ if(!@rows || !@columns)
410
+
411
+ @rows = Array.new if !@rows
412
+ @columns = Array.new if !@columns
413
+ # create for each column
414
+ num_columns = @workbook.last_column
415
+ if(num_columns && num_columns > 0)
416
+ num_columns.times do |column|
417
+ # ... a column, then
418
+ @columns[column] = Column.new(column) if !@columns[column]
419
+ cur_col = @columns[column]
420
+
421
+ # ... create for each row in each column
422
+ (@workbook.last_row).times do |row|
423
+ # ... a row then
424
+ @rows[row] = Row.new(row) if !@rows[row]
425
+ cur_row = @rows[row]
426
+ # ... create for each cell in each row
427
+ cur_cell = cur_row.cells.detect {|cell| column == cell.col }
428
+ if !cur_cell
429
+ # ... a cell
430
+ cell = @workbook.cell(row + 1, column + 1)
431
+ cur_cell = Cell.new(cur_row, cur_col, cell.to_s)
432
+ cur_row << cur_cell
433
+ cur_col << cur_cell
434
+ end
435
+ end
436
+ end
437
+ end
438
+ end
439
+
440
+ bi.stop("\n")
441
+ if(on_screen)
442
+ @table_view = draw()
443
+ if @table_view
444
+ @table_view.extend(Scrollable)
445
+ @table_view.fixed_rows = 4
446
+ @table_view.fixed_cols = 4
447
+ refresh()
448
+ end
449
+ end
450
+ end
451
+
452
+ def refresh()
453
+ @table_view.width=terminal_size[0] - 1
454
+ @table_view.height=terminal_size[1]
455
+ # @table_view.box = true
456
+ @table_view.show
457
+ @status.show
458
+ if !@menu
459
+ @menu = Menu.new(:name => 'main')
460
+ define_actions(@menu)
461
+ end
462
+ @menu.call
463
+ end
464
+ end