class SheetInterface

TODO: provide conversion for column designators to int vv, also for AA to ZZ !

Public Class Methods

new(wb, num) click to toggle source
# File lib/sheetinterface.rb, line 61
def initialize(wb, num)
  init_logger(STDOUT, Logger::INFO)
  @workbook = wb.sheet(num)
  @menu = nil
  @status = Status.new
  construct
end

Private Instance Methods

ask_cell_coord() click to toggle source
# File lib/sheetinterface.rb, line 154
def ask_cell_coord()
  coord = nil
  until coord
    print 'Which cell (col:row)? '
    begin
      coord = STDIN.gets().chomp!
    rescue Interrupt
      return nil
    end
    if('' == coord || !coord.include?(':') || "\e" == coord)
      return nil
    else
      col, row = coord.split(':')
      row = row.to_i
      col = col.strip
    end
    return [col, row]
  end
end
ask_number(question) { |num| ... } click to toggle source

Asks the user to enter a number, then verifies ageinst eventual constraints.

# File lib/sheetinterface.rb, line 80
def ask_number(question, &constraint)
  num = nil
  3.times do
    print  question.dup << " "
    begin
      num = STDIN.gets() 
    rescue Interrupt 
      return nil
    end
    num = num.to_i
    if (constraint && !yield(num))
      num = nil
    end
    return num if num
  end
  if !num
    @status.message = "Inacceptable value, doing nothing."
    return nil
  end
end
ask_string(question) { |str| ... } click to toggle source

Shortcutting from “user inputs bull” to “program interrupts” without the need for the program to actually run havoc, first.

# File lib/sheetinterface.rb, line 103
def ask_string(question, &constraint)
  str = nil
  until str
    print  question.dup << " "
    begin
      str = STDIN.gets().chomp!
    rescue Interrupt
      return nil
    end
    if("" == str || "\e" == str)
      return nil
    end
    if(constraint && !yield(str))
      str = nil
    end
  end
  if !str
    @status.message = "Inacceptable value, doing nothing."
  end
  return str
end
construct(on_screen = true) click to toggle source

The function which is called to recreate the view. It prepares the table-generation by transforming the Roo-instance to objects of the classes Cell, Row and Column, then calls draw(), above and adds the status- and menu-lines. The menu waits for user input (blocking) before construct() returns, no other thread or “event-loop” involved there.

# File lib/sheetinterface.rb, line 406
def construct(on_screen = true)
  puts "... reading spreadsheet data (%s), PSE wait" %[@workbook.default_sheet]
  bi = BusyIndicator.new()
  if(!@rows || !@columns)

    @rows = Array.new if !@rows
    @columns = Array.new if !@columns
    # create for each column
    num_columns = @workbook.last_column
    if(num_columns && num_columns > 0)
      num_columns.times do |column|
        # ... a column, then
        @columns[column] = Column.new(column) if !@columns[column]
        cur_col = @columns[column]

        # ... create for each row in each column
        (@workbook.last_row).times do |row|
          # ... a row then
          @rows[row] = Row.new(row) if !@rows[row]
          cur_row = @rows[row]
          # ... create for each cell in each row
          cur_cell = cur_row.cells.detect {|cell| column == cell.col }
          if !cur_cell
            # ... a cell
            cell = @workbook.cell(row + 1, column + 1)
            cur_cell = Cell.new(cur_row, cur_col, cell.to_s)
            cur_row << cur_cell
            cur_col << cur_cell
          end
        end
      end
    end
  end

  bi.stop("\n")
  if(on_screen)
    @table_view = draw()
    if @table_view
      @table_view.extend(Scrollable)
      @table_view.fixed_rows = 4
      @table_view.fixed_cols = 4
      refresh()
    end
  end
end
define_actions(menu) click to toggle source

Adds concrete actions to the menu. Note the calls to ask_number and ask_string with constraints.

# File lib/sheetinterface.rb, line 209
def define_actions(menu)
  sm_Menu = Menu.new(:name => 'sheet', :key => 's')
  sm_Menu.add_action(Action.new(:name => 'sheet number', :key => '#') {open_sheet(:number)})
  sm_Menu.add_action(Action.new(:name => 'sheet name', :key => 'n') {open_sheet(:name)})

  save_Menu = Menu.new(:name => 'save to file', :key => 'f')
  save_Menu.add_action(Action.new(:name => 'current sheet', :key => 'c') do 
    file = new_file()
    if(file)
      File::open(file, 'w+') {|out| draw(out) }
      @status.message = green('... done')
    end
    construct
  end)        

  save_Menu.add_action(Action.new(:name => 'all sheets', :key => 'a') do 
    current_sheet = @workbook.default_sheet
    @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"
    file = new_file()
    if(file)
      bi = BusyIndicator.new(true)
      begin
        File::open(file, 'w+') do |out| 
          @workbook.sheets.each do |s|
            @log.debug('writing sheet ' << s)
            set_sheet(s)
            construct(false)
            draw(out) 
          end
        end
      rescue Interrupt
        puts bold(red("Interrupted by user. Bye!"))
        exit true
      end
      bi.stop('... done')
    end
    set_sheet(current_sheet)
    construct
  end)        

  col_Menu = Menu.new(:name=>'column', :key => 'c')
  col_Menu.add_action(Action.new(:name => 'column width', :key => 'w') do |num| 
    num = ask_number('column width (min. 4) ?'){|w| w > 3}
    Column::col_width = num if num
    construct 
  end)

  menu.add_action(Action.new(:name => 'value', :key => 'v') do 
    cell_coord = ask_cell_coord
    if(cell_coord && cell_coord.length == 2)
      row = cell_coord[1] - 1
      col = cell_coord[0].upcase.bytes[0] - 65
      cell = nil
      if(@rows[row] && @columns[col])
        cell = @rows[row].cells.detect {|c| c.col == col} 
        if(cell)
          @status.message = "#{cell_coord[0].upcase}:#{cell_coord[1]} is \"" << cell.value.to_s << "\" (%d)" %cell.value.to_s.length
        else
          @status.message = red('invalid cell')
        end
      else
        @status.message = red(cell_coord.join(':')  << ': out of range')
      end
    elsif(cell_coord && cell_coord.length > 0)
      @status.message = red('invalid input')
    end
    construct
  end)

  menu.add_menu(save_Menu)
  menu.add_menu(sm_Menu)
  menu.add_menu(col_Menu)
  menu.add_action(Action.new(:name => "⇧", :key => "i") {@table_view.up; refresh()})
  menu.add_action(Action.new(:name => "⇩", :key => "k") {@table_view.down; refresh()})
  menu.add_action(Action.new(:name => "⇦", :key => "j") {@table_view.left; refresh()})
  menu.add_action(Action.new(:name => "⇨", :key => "l") {@table_view.right; refresh()})
  menu.add_action(Action.new(:global => true, :hidden => true, :name => '', :key => "\e") {refresh})
  menu.add_action(Action.new(:global => true, :name => 'quit', :key => 'q') {puts 'Bye ';exit 0})
end
draw(out = STDOUT) click to toggle source

This is hard to describe. The function basically draws a table on screen.

# File lib/sheetinterface.rb, line 290
  def draw(out = STDOUT)
    if(@rows && !@rows.empty?)
      # Welcome to hell.
      on_screen = STDOUT == out
      col_w = Column::col_width
      num_c = @rows[0].cells.length()
      num_r = @rows.length
      ch = nil
      hline = nil
      #-----
      table_view = ''
      #-----

      table_view << @workbook.default_sheet << "\n"
      #out.puts @workbook.default_sheet
      table_view << ('┌' << '─' * num_r.to_s.length << '┬' << (('─' * col_w) << '┬' ) * (num_c -1) ) << (('─' * col_w) << '┐' ) << "\n"
      # out.puts ('┌' << '─' * num_r.to_s.length << '┬' << (('─' * col_w) << '┬' ) * (num_c -1) ) << (('─' * col_w) << '┐' )
      lh = 0
      @rows.each do |row|

        if(row == @rows.first)
          ch = 'A'
          table_view << "│%#{num_r.to_s.length}s" %" " 
          #out.print "│%#{num_r.to_s.length}s" %" "
          table_view << '│'
          # out.print '│'
          row.each do |c|
            head = "%#{col_w}s" %ch 
            head = bold(head) if(on_screen)
            # out.print(head << "|")
            table_view << (head << "│")
            ch = ch.next
          end
          # out.puts
          table_view << "\n"
          hline = ('├' << ('─' * (num_r.to_s.length) ) <<'┼' << (('─' * col_w) << '┼' ) * (num_c - 1) ) <<  (('─' * col_w) << '┤')if !hline
          table_view << hline << "\n"
          # out.puts hline
        end
        lh += 1
        table_view << "│" 
        # out.print "│"
        line_head = "%#{num_r.to_s.length}s" %lh.to_s
        line_head = bold(line_head) if on_screen 
        table_view << line_head
        # out.print line_head

        #      out.print "│" << bold("%#{num_r.to_s.length}s" %line_head)

        row.height.times do |li|
          table_view << "│%#{num_r.to_s.length}s" %" " if li > 0
          # out.print "│%#{num_r.to_s.length}s" %" " if li > 0
          table_view << '│'
          # out.print '│'
          row.each do |cell|
            line = cell.line(li)
            content = "%#{col_w}s│" %line 
            if(on_screen && line && line.end_with?('...'))
              content = red(content)
            end
            table_view << content
            # out.print content
          end
          table_view << "\n"
          # out.puts
        end
        hlline = ('│' << (('─' * col_w) << '│' ) * num_c  ) if !hline

        table_view << hline << "\n"
        # out.puts hline

        # Some of the following code may go to a function as it repeats the
        # procedure from the beginning of the table.  I am currently unwilling to
        # do it, because all the rest must stay so verbose that another
        # function-call at this position cannot contribute to a better
        # understanding of this ... table-generating MESS!!
        if(row == @rows.last)
          ch = 'A'
          table_view << "│%#{num_r.to_s.length}s" %" " 
          # out.print "│%#{num_r.to_s.length}s" %" "
          table_view << '│'
          # out.print '│'
          row.each do |c|
            head = "%#{col_w}s" %ch
            head = bold(head) if on_screen
            table_view << ( head << '│')
            # out.print( head << '│')
            ch = ch.next
          end
          table_view << "\n"
          # out.puts
          hline = ('└' << ('─' * (num_r.to_s.length) ) << '┴' << (('─' * col_w) << '┴' ) * (num_c - 1) ) << (('─' * col_w) << '┘' )

          table_view << hline << "\n"
          # out.puts hline
        end

      end
=begin
    table_width = hline.length 
    tw = terminal_size[0]
    if(on_screen && table_width > tw)
      @status.message = red("ATTN! Terminal is to narrow to display the whole table (#{tw} characters for #{table_width}). Diminish column-width!")
    end
=end
      return table_view
    else
      return nil
    end
  end
new_file() click to toggle source

Write content to a new file

# File lib/sheetinterface.rb, line 126
def new_file()
  file = nil
  until file
    print 'file path:' << ' '
    begin
      file = STDIN.gets().chomp!
    rescue Interrupt
      return nil
    end
    if(file && File::exist?(file) )
      if(File::writable?(file) )
        print "File #{file} exists! Do you want to overwrite its content, now (j/N)? "
        answer = "%c" %wait_for_user  
        if('j' != answer.downcase)
          file = nil
        else
          puts
        end
      else
        puts "File #{file} is not writable! Please name a different file path"
        file = nil

      end
    end
  end
  return file
end
open_sheet(designator = :number) click to toggle source
# File lib/sheetinterface.rb, line 187
def open_sheet(designator = :number)
  sn = nil
  if designator == :number
    sn = ask_number("Sheet number (max #{@workbook.sheets.size() - 1})?") do |num|
      num < @workbook.sheets.size
    end 
  else
    sn = ask_string("Sheet name (#{@workbook.sheets.join(', ')})?") do |name|
      @workbook.sheets.include?(name)
    end
  end

  if sn && !sn.to_s.empty?
    @log.debug('calling workbook.sheet with ' << sn.to_s)
    @log.debug('@workbook is of type ' << @workbook.class.name)
    set_sheet(sn)
  end
  construct
end
refresh() click to toggle source
# File lib/sheetinterface.rb, line 452
def refresh()
  @table_view.width=terminal_size[0] - 1
  @table_view.height=terminal_size[1] 
  # @table_view.box = true
  @table_view.show
  @status.show
  if !@menu
    @menu = Menu.new(:name => 'main')
    define_actions(@menu)
  end
  @menu.call
end
set_sheet(sheet) click to toggle source
# File lib/sheetinterface.rb, line 174
def set_sheet(sheet)
  current_sheet = @workbook.default_sheet
  @workbook = @workbook.sheet(sheet)
  columns = @workbook.last_column
  if columns && columns > 0
    @rows = nil
    @columns = nil
  else
    @workbook = @workbook.sheet(current_sheet)
    @status.message = red("Sheet #{sheet} contains no data!")
  end
end
terminal_size() click to toggle source

returns [width, height] in columns and lines of the current terminal-window.

# File lib/sheetinterface.rb, line 74
def terminal_size
  size = `stty size`.split.map { |x| x.to_i }.reverse
  return size
end