# 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