viewworkbook 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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