# frozen_string_literal: true
# Copyright (C) 2023 Thomas Baron
#
# 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 .
require 'stringio'
module TermUtils
# The tab module provides a way to print formatted tables.
module Tab
# Represents a table error.
class TableError < StandardError
end
# Creates initial column properties.
# @return [Hash] `:offset`, `:column_separator_width`.
def self.init_table_props
{ offset: 0, column_separator_width: 2 }
end
# Creates initial column properties.
# @return [Hash] `:width`, `:align`, `:fixed`, `:ellipsis`, `:format`.
def self.init_column_props
{ width: 8, align: :left, fixed: false, ellipsis: '?', format: nil }
end
# Assigns table properties.
# @param target [Hash]
# @param source [Hash] `:offset`, `:column_separator_width`.
# @return [Hash]
def self.assign_table_props(target, source)
if (source.key? :offset) && (source[:offset].is_a? Integer) && (source[:offset] >= 0)
target[:offset] = source[:offset]
end
if (source.key? :column_separator_width) && (source[:column_separator_width].is_a? Integer) && source[:column_separator_width].positive?
target[:column_separator_width] = source[:column_separator_width]
end
target
end
# Assigns column properties.
# @param target [Hash]
# @param source [Hash] `:width`, `:align`, `:fixed`, `:ellipsis`, `:format`.
# @return [Hash]
def self.assign_column_props(target, source)
if (source.key? :width) && (source[:width].is_a? Integer) && source[:width].positive?
target[:width] = source[:width]
end
if (source.key? :align) && %i[left right].index(source[:align])
target[:align] = source[:align]
end
if (source.key? :fixed) && (!!source[:fixed] == source[:fixed])
target[:fixed] = source[:fixed]
end
if (source.key? :ellipsis) && (source[:ellipsis].is_a? String)
target[:ellipsis] = source[:ellipsis]
end
if (source.key? :format) && (source[:ellipsis].nil? || (source[:ellipsis].is_a? Proc) || (source[:ellipsis].is_a? String))
target[:format] = source[:format]
end
target
end
# Aligns and cuts a given string.
# @param src [String]
# @param align [Symbol] `:left`, `:right`.
# @param fixed [Boolean] Whether the column width is fixed.
# @param width [Integer] The column width.
# @param ellipsis [String] The ellipsis when not fixed.
# @return [String]
def self.align_cut(src, align, fixed, width, ellipsis)
if align == :left
# Align left
if fixed && (src.length > width)
if ellipsis.length >= width
ellipsis[0..(width - 1)]
else
format '%s%s', value: src[0..(width - (ellipsis.length + 1))], ellipsis: ellipsis
end
else
src.ljust(width)
end
elsif align == :right
# Align right
if fixed && (src.length > width)
if ellipsis.length >= width
ellipsis[0..(width - 1)]
else
format '%s%s', ellipsis: ellipsis, value: src[(src.length - width + ellipsis.length)..(src.length - 1)]
end
else
src.rjust(width)
end
end
end
# Represents a table.
class Table
# @return [Symbol]
attr_accessor :id
# @return [Integer]
attr_accessor :offset
# @return [Integer]
attr_accessor :column_separator_width
# @return [Hash] `:width`, `:align`, `:fixed`, `:ellipsis`, `:format`.
attr_accessor :column_defaults
# @return [Array]
attr_accessor :columns
# @param opts [Hash]
# @option opts [Symbol] :id
# @option opts [Integer] :offset
# @option opts [Hash] :column_defaults
def initialize(opts = {})
opts = TermUtils::Tab.init_table_props.merge(opts)
@id = opts.fetch(:id, nil)
@offset = opts.fetch(:offset)
@column_separator_width = opts.fetch(:column_separator_width)
@column_defaults = opts.key?(:column_defaults) ? opts[:column_defaults].dup : TermUtils::Tab.default_column_props
@columns = []
end
# Returns the properties of this one.
# @return [Hash]
def props
{ offset: @offset, column_separator_width: @column_separator_width }
end
# Sets column default properties.
# @param opts [Hash]
# @option opts [Integer] :width
# @option opts [Symbol] :align
# @option opts [Boolean] :fixed
# @option opts [String] :ellipsis
# @option opts [Proc, String, nil] :format
def set_column_defaults(opts = {})
TermUtils::Tab.assign_column_props(@column_defaults, opts)
end
# Defines a column.
# @param id [Symbol]
# @param opts [Hash]
# @option opts [Integer] :width
# @option opts [Symbol] :align
# @option opts [Boolean] :fixed
# @option opts [String] :ellipsis
# @option opts [Proc, String, nil] :format
# @return [Tab::Column]
def define_column(id, opts = {}, &block)
col = @columns.find { |c| c.id == id }
if col
block&.call(col)
col.validate
else
opts[:id] = id
opts[:index] = @columns.length
col = Column.new(@column_defaults.merge(opts))
block&.call(col)
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 [#puts]
# @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, props.merge(opts))
block&.call(ptr)
ptr
end
# Prints a header row.
# @param io [#puts]
# @param values [Array