# This file is part of term_utils.
#
# term_utils is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# term_utils is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with term_utils. If not, see .
module TermUtils
module Tab
# Represents a table.
class Table
# @return [Symbol]
attr_accessor :id
# @return [Array]
attr_accessor :columns
# @param opts [Hash]
# @option opts [Symbol] :id
def initialize(opts = {})
@id = opts.fetch(:id, nil)
@columns = []
end
# Defines a column.
# @param id [Symbol]
# @param opts [Hash]
# @option opts [Integer] :width
# @return [Tab::Column]
def define_column(id, opts = {}, &block)
col = @columns.find { |c| c.id == id }
if col
block.call(col) if block
col.validate
else
opts[:id] = id
opts[:index] = @columns.length
col = Column.new(opts)
block.call(col) if block
col.validate
@columns << col
end
col
end
# Finds a column.
# @param id [Symbol]
# @return [Tab::Column, nil]
def find_column(id)
@columns.find { |c| c.id == id }
end
# Creates a new table printer.
# @param io [IO]
# @param values [Array, Hash]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [Tab::Printer]
def printer(io, opts = {}, &block)
ptr = Printer.new(self, io, opts)
block.call(ptr) if block
ptr
end
# Prints a header row.
# @param io [IO]
# @param values [Array, Hash]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
def print_header(io, values = nil, opts = {})
vals = values
if values.nil?
vals = @columns.map { |col| col.id.to_s }
elsif values.is_a? Hash
vals = []
@columns.each do |col|
vals << values[col.id]
end
end
raise "wrong values (not array)" unless vals.is_a? Array
offset = opts.fetch(:offset, 0)
column_separator_width = opts.fetch(:column_separator_width, 2)
sb = StringIO.new
sb << " " * offset if offset > 0
@columns.each do |col|
sb << " " * column_separator_width if col.index > 0
sb << col.render_header(vals[col.index])
end
io.puts sb.string
end
# Prints a data row.
# @param io [IO]
# @param values [Array, Hash]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
def print_data(io, values, opts = {})
vals = values
if values.is_a? Hash
vals = []
@columns.each do |col|
vals << values[col.id]
end
end
raise "wrong values (not array)" unless vals.is_a? Array
offset = opts.fetch(:offset, 0)
column_separator_width = opts.fetch(:column_separator_width, 2)
sb = StringIO.new
sb << " " * offset if offset > 0
@columns.each do |col|
sb << " " * column_separator_width if col.index > 0
sb << col.render_data(vals[col.index])
end
io.puts sb.string
end
# Prints a separator row.
# @param io [IO]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
def print_separator(io, opts = {})
offset = opts.fetch(:offset, 0)
column_separator_width = opts.fetch(:column_separator_width, 2)
sb = StringIO.new
sb << " " * offset if offset > 0
@columns.each do |col|
sb << " " * column_separator_width if col.index > 0
sb << "-" * col.width
end
io.puts sb.string
end
# Returns column titles.
# @return [Hash]
def titles
h = {}
@columns.each do |col|
h[col.id] = col.id.to_s
end
h
end
end
# Represents a table column.
class Column
# @return [Symbol]
attr_accessor :id
# @return [Integer]
attr_accessor :index
# @return [Integer]
attr_accessor :width
# @return [Symbol] `:left`, `:right`.
attr_accessor :align
# @return [Boolean]
attr_accessor :fixed
# @return [String]
attr_accessor :ellipsis
# @return [Proc, String, nil]
attr_accessor :format
# @param opts [Hash]
# @option opts [Symbol] :id
# @option opts [Integer] :index
# @option opts [Integer] :width
# @option opts [Symbol] :align
# @option opts [Boolean] :fixed
# @option opts [String] :ellipsis
# @option opts [Proc, String, nil] :format
def initialize(opts = {})
@id = opts.fetch(:id)
@index = opts.fetch(:index)
@width = opts.fetch(:width, 8)
@align = opts.fetch(:align, :left)
@fixed = opts.fetch(:fixed, false)
@ellipsis = opts.fetch(:ellipsis, "?")
@format = opts.fetch(:format, nil)
end
# Validates the column represented by this one.
# @return [nil]
def validate
raise "missing column id (nil)" if @id.nil?
raise "missing column index (nil)" if @index.nil?
raise "wrong column index (not integer)" unless @index.is_a? Integer
raise "wrong column index (not >= 0)" if @index < 0
raise "missing column width (nil)" if @width.nil?
raise "wrong column width (not integer)" unless @width.is_a? Integer
raise "wrong column width (not > 0)" if @width <= 0
raise "wrong column align (not :left or :right)" unless %i{left right}.index @align
end
# Aligns and cuts a given string.
# @param str [String]
# @return [String]
def align_cut(str)
if @align == :left
# Align left
if @fixed and (str.length > @width)
str = "#{str[0..(@width - (@ellipsis.length + 1))]}#{@ellipsis}"
else
str = "%-*s" % [@width, str]
end
else
# Align right
if @fixed and (str.length > @width)
str = "#{@ellipsis}#{str[(str.length - @width + @ellipsis.length)..(str.length - 1)]}"
else
str = "%*s" % [@width, str]
end
end
str
end
# Renders a given header.
# @param v [Object]
# return [String]
def render_header(v)
str = v
str = str.to_s unless str.is_a? String
align_cut str
end
# Renders a given value.
# @param v [Object]
# return [String]
def render_data(v)
str = v
if v
if @format.is_a? Proc
str = @format.call(v)
elsif @format.is_a? String
str = @format % v
end
end
str = str.to_s unless str.is_a? String
align_cut str
end
end
# Represents a table printer.
class Printer
# @return [Tab::Table]
attr_accessor :table
# @return [IO]
attr_accessor :io
# @return [Hash]
attr_accessor :options
# @param table [Tab::Table]
# @param io [IO]
# @param options [Hash]
def initialize(table, io, options)
@table = table
@io = io
@options = options
end
def line
@io.puts ""
end
def header(values = nil, opts = {})
@table.print_header(@io, values, @options.merge(opts))
end
def data(values, opts = {})
@table.print_data(@io, values, @options.merge(opts))
end
# Prints a separator.
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [nil]
def separator(opts = {})
@table.print_separator(@io, @options.merge(opts))
end
end
# Represents a holder of tables.
class Holder
# @return [Hash]
attr_accessor :tables
def initialize(opts = {})
@tables = {}
end
# Defines a table.
# @param id [Symbol]
# @param opts [Hash]
# @return [Tab::Table]
def define_table(id, opts = {}, &block)
if @tables.has_key? id
block.call(@tables[id]) if block
else
opts[:id] = id
new_tab = Table.new(opts)
block.call(new_tab) if block
@tables[id] = new_tab
end
@tables[id]
end
# Finds a table.
# @param id [Symbol]
# @return [Tab::Table, nil]
def find_table(id)
@tables[id]
end
# Creates a new table printer.
# @param id [Symbol]
# @param io [IO]
# @param values [Array, Hash]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [Tab::Printer]
def printer(id, io, opts = {}, &block)
find_table(id).printer(io, opts, &block)
end
end
@@default_holder = Holder.new
# Defines a table.
# @param id [Symbol]
# @param opts [Hash]
# @return [Tab::Table]
def self.define_table(id, opts = {}, &block)
@@default_holder.define_table(id, opts = {}, &block)
end
# Creates a new table printer.
# @param id [Symbol]
# @param io [IO]
# @param values [Array, Hash]
# @param opts [Hash]
# @option opts [Integer] :offset
# @option opts [Integer] :column_separator_width
# @return [Tab::Printer]
def self.printer(id, io, opts = {}, &block)
@@default_holder.find_table(id).printer(io, opts, &block)
end
end
end