Class: Pvectl::Formatters::Table

Inherits:
Base
  • Object
show all
Defined in:
lib/pvectl/formatters/table.rb,
sig/pvectl/formatters/table.rbs

Overview

Formats data as a table using tty-table gem.

For collections: renders standard horizontal table with headers. For single resources (describe): renders vertical key-value layout.

Examples:

Table output for collection

NAME    STATUS   NODE
vm-100  running  pve1
vm-101  stopped  pve2

Vertical output for single resource (describe)

Name:     vm-100
Status:   running
Node:     pve1

See Also:

Instance Method Summary collapse

Methods inherited from Base

#collection?, #normalize_nil

Instance Method Details

#colorize_row(row, headers, pastel) ⇒ Array

Colorizes status columns in a row.



80
81
82
83
84
85
86
87
88
89
# File 'lib/pvectl/formatters/table.rb', line 80

def colorize_row(row, headers, pastel)
  row.each_with_index.map do |value, idx|
    header = headers[idx].to_s.downcase
    if header == "status"
      ColorSupport.colorize_status(value, pastel)
    else
      normalize_nil(value)
    end
  end
end

#format(data, presenter, color_enabled: true, describe: false, **context) ⇒ String

Formats data as table output.



33
34
35
36
37
38
39
40
41
# File 'lib/pvectl/formatters/table.rb', line 33

def format(data, presenter, color_enabled: true, describe: false, **context)
  pastel = ColorSupport.pastel(explicit_flag: color_enabled)

  if describe || !collection?(data)
    format_describe(data, presenter, pastel)
  else
    format_table(data, presenter, pastel, **context)
  end
end

#format_describe(model, presenter, pastel) ⇒ String

Formats single resource as vertical key-value layout. Supports nested Hashes (sections) and Arrays of Hashes (tables).



69
70
71
72
# File 'lib/pvectl/formatters/table.rb', line 69

def format_describe(model, presenter, pastel)
  hash = presenter.to_description(model)
  format_describe_hash(hash, pastel, indent: 0)
end

#format_describe_hash(hash, pastel, indent: 0) ⇒ String

Recursively formats a hash for describe output.

At indent 0 (top level), once a section (Hash/Array) has been rendered, all subsequent entries get a blank line separator for consistent spacing.



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/pvectl/formatters/table.rb', line 100

def format_describe_hash(hash, pastel, indent: 0)
  return "-" if hash.nil?

  lines = []
  prefix = "  " * indent

  # Calculate max key length for alignment (only for non-nested values)
  simple_keys = hash.select { |_, v| !v.is_a?(Hash) && !(v.is_a?(Array) && v.first.is_a?(Hash)) && !v.to_s.include?("\n") }
  max_key_length = simple_keys.keys.map { |k| k.to_s.length }.max || 0

  # Track whether we've passed a nested section (Hash/Array).
  # Once a section appears, all subsequent simple entries get blank-line separators.
  prev_was_section = false

  hash.each do |key, value|
    human_key = humanize_key(key)

    if value.is_a?(Hash)
      # Nested section
      lines << ""
      lines << "#{prefix}#{human_key}:"
      lines << format_describe_hash(value, pastel, indent: indent + 1)
      prev_was_section = true
    elsif value.is_a?(Array) && !value.empty? && value.first.is_a?(Hash)
      # Array of hashes -> inline table
      lines << ""
      lines << "#{prefix}#{human_key}:"
      lines << format_describe_table(value, indent: indent + 1)
      prev_was_section = true
    elsif value.is_a?(Array) && value.empty?
      # Empty array -> show as "-"
      lines << "" if prev_was_section
      formatted_key = "#{human_key}:".ljust(max_key_length + 2)
      lines << "#{prefix}#{formatted_key}-"
    else
      formatted_value = format_describe_value(value, key.to_s, pastel)
      if formatted_value.include?("\n")
        # Multi-line value: render as block section
        lines << ""
        lines << "#{prefix}#{human_key}:"
        formatted_value.each_line do |line|
          lines << "#{prefix}  #{line.chomp}"
        end
        prev_was_section = true
      else
        # Simple key-value — add separator after sections at any indent level
        lines << "" if prev_was_section
        formatted_key = "#{human_key}:".ljust(max_key_length + 2)
        lines << "#{prefix}#{formatted_key}#{formatted_value}"
      end
    end
  end

  lines.join("\n").sub(/\A\n+/, "").rstrip
end

#format_describe_table(array, indent: 0) ⇒ String

Formats array of hashes as inline table.



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/pvectl/formatters/table.rb', line 161

def format_describe_table(array, indent: 0)
  return "  " * indent + "-" if array.empty?

  prefix = "  " * indent
  headers = array.first.keys.map { |k| k.to_s.upcase }
  rows = array.map { |item| item.values.map(&:to_s) }

  # Calculate column widths
  widths = headers.each_with_index.map do |h, i|
    [h.length, *rows.map { |r| r[i]&.length || 0 }].max
  end

  lines = []
  # Header
  header_line = headers.each_with_index.map { |h, i| h.ljust(widths[i]) }.join("  ")
  lines << "#{prefix}#{header_line}"

  # Rows
  rows.each do |row|
    row_line = row.each_with_index.map { |v, i| (v || "-").ljust(widths[i]) }.join("  ")
    lines << "#{prefix}#{row_line}"
  end

  lines.join("\n")
end

#format_describe_value(value, key, pastel) ⇒ String

Formats a value for describe output.



193
194
195
196
197
198
199
200
201
# File 'lib/pvectl/formatters/table.rb', line 193

def format_describe_value(value, key, pastel)
  return "-" if value.nil?

  if key.downcase == "status"
    ColorSupport.colorize_status(value, pastel)
  else
    value.to_s
  end
end

#format_table(data, presenter, pastel, **context) ⇒ String

Formats collection as horizontal table.



52
53
54
55
56
57
58
59
60
# File 'lib/pvectl/formatters/table.rb', line 52

def format_table(data, presenter, pastel, **context)
  headers = presenter.columns
  rows = data.map do |model|
    row = presenter.to_row(model, **context)
    colorize_row(row, headers, pastel)
  end

  render_table(headers, rows)
end

#humanize_key(key) ⇒ String

Converts snake_case or kebab-case key to Title Case. Preserves already-formatted keys (e.g., "CPU", "DNS", "Cloud-Init").



208
209
210
211
212
213
214
215
# File 'lib/pvectl/formatters/table.rb', line 208

def humanize_key(key)
  str = key.to_s
  # If the key is already formatted (contains spaces, is all caps,
  # or has uppercase letters indicating intentional formatting), return as-is
  return str if str.include?(" ") || str == str.upcase || str.match?(/[A-Z]/)

  str.split(/[_-]/).map(&:capitalize).join(" ")
end

#render_table(headers, rows) ⇒ String

Renders table using tty-table.

Uses :basic renderer for clean kubectl-like output (no borders). Handles non-TTY environments gracefully by rescuing ioctl errors.



225
226
227
228
229
230
231
232
# File 'lib/pvectl/formatters/table.rb', line 225

def render_table(headers, rows)
  table = TTY::Table.new(header: headers, rows: rows)
  table.render(:basic, padding: [0, 2]) || headers.join("\t")
rescue NoMethodError
  # TTY::Screen may call ioctl on non-TTY streams (e.g., StringIO in tests)
  # Fall back to simple tab-separated output
  ([headers] + rows).map { |row| row.join("\t") }.join("\n")
end