Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 9 additions & 24 deletions pkg/cmd/devicecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"os"
"strings"

"github.com/kernel/hypeman-go"
"github.com/kernel/hypeman-go/option"
Expand Down Expand Up @@ -154,42 +153,37 @@ func showAvailableDevicesTable(data []byte) error {
return nil
}

fmt.Println("PCI ADDRESS VENDOR DEVICE IOMMU DRIVER")
fmt.Println(strings.Repeat("-", 80))
table := NewTableWriter(os.Stdout, "PCI ADDRESS", "VENDOR", "DEVICE", "IOMMU", "DRIVER")
table.TruncOrder = []int{2, 1} // DEVICE first, then VENDOR

devices.ForEach(func(key, value gjson.Result) bool {
pciAddr := value.Get("pci_address").String()
vendorID := value.Get("vendor_id").String()
deviceID := value.Get("device_id").String()
vendorName := value.Get("vendor_name").String()
deviceName := value.Get("device_name").String()
iommuGroup := value.Get("iommu_group").Int()
iommuGroup := fmt.Sprintf("%d", value.Get("iommu_group").Int())
driver := value.Get("current_driver").String()

// Format vendor info
vendor := vendorName
if vendor == "" {
vendor = vendorID
} else if len(vendor) > 18 {
vendor = vendor[:15] + "..."
}

// Format device info
device := deviceName
if device == "" {
device = deviceID
} else if len(device) > 18 {
device = device[:15] + "..."
}

if driver == "" {
driver = "-"
}

fmt.Printf("%-16s %-19s %-19s %-7d %s\n", pciAddr, vendor, device, iommuGroup, driver)
table.AddRow(pciAddr, vendor, device, iommuGroup, driver)
return true
})

table.Render()
return nil
}

Expand Down Expand Up @@ -274,20 +268,12 @@ func showDeviceListTable(data []byte) error {
return nil
}

fmt.Println("ID NAME TYPE PCI ADDRESS VFIO ATTACHED TO")
fmt.Println(strings.Repeat("-", 90))
table := NewTableWriter(os.Stdout, "ID", "NAME", "TYPE", "PCI ADDRESS", "VFIO", "ATTACHED TO")
table.TruncOrder = []int{0, 1, 5} // ID first, then NAME, ATTACHED TO

devices.ForEach(func(key, value gjson.Result) bool {
id := value.Get("id").String()
if len(id) > 20 {
id = id[:17] + "..."
}

name := value.Get("name").String()
if len(name) > 20 {
name = name[:17] + "..."
}

deviceType := value.Get("type").String()
pciAddr := value.Get("pci_address").String()

Expand All @@ -299,14 +285,13 @@ func showDeviceListTable(data []byte) error {
attachedTo := value.Get("attached_to").String()
if attachedTo == "" {
attachedTo = "-"
} else if len(attachedTo) > 15 {
attachedTo = attachedTo[:12] + "..."
}

fmt.Printf("%-21s %-19s %-6s %-16s %-6s %s\n", id, name, deviceType, pciAddr, vfio, attachedTo)
table.AddRow(id, name, deviceType, pciAddr, vfio, attachedTo)
return true
})

table.Render()
return nil
}

Expand Down
110 changes: 105 additions & 5 deletions pkg/cmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,31 @@ import (
"context"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"

"github.com/kernel/hypeman-go"
"golang.org/x/term"
)

// TableWriter provides simple table formatting for CLI output
// TableWriter provides simple table formatting for CLI output with
// terminal-width-aware column sizing.
type TableWriter struct {
w io.Writer
headers []string
widths []int
widths []int // natural widths (max of header and cell values)
rows [][]string

// TruncOrder specifies column indices in truncation priority order.
// The first index in the slice is truncated first when the table is
// too wide for the terminal. Columns not listed are never truncated.
TruncOrder []int
}

const columnGap = 2 // spaces between columns

// NewTableWriter creates a new table writer
func NewTableWriter(w io.Writer, headers ...string) *TableWriter {
widths := make([]int, len(headers))
Expand Down Expand Up @@ -46,23 +57,112 @@ func (t *TableWriter) AddRow(cells ...string) {
t.rows = append(t.rows, row)
}

// Render outputs the table
// getTerminalWidth returns the terminal width. It tries the stdout
// file descriptor first, then the COLUMNS env var, then defaults to 80.
func getTerminalWidth() int {
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
return w
}
if cols := os.Getenv("COLUMNS"); cols != "" {
if w, err := strconv.Atoi(cols); err == nil && w > 0 {
return w
}
}
return 80
}

// renderWidths computes the final column widths, shrinking columns in
// TruncOrder as needed to fit within the terminal width.
func (t *TableWriter) renderWidths() []int {
n := len(t.headers)
widths := make([]int, n)
copy(widths, t.widths)

termWidth := getTerminalWidth()

// Total space: column widths + gaps (no trailing gap on last column)
total := func() int {
s := 0
for _, w := range widths {
s += w
}
s += columnGap * (n - 1)
return s
}

if total() <= termWidth {
return widths
}

// Shrink columns in TruncOrder until the table fits
for _, col := range t.TruncOrder {
if col < 0 || col >= n {
continue
}
excess := total() - termWidth
if excess <= 0 {
break
}
// Minimum width: at least the header length, but no less than 5
minW := len(t.headers[col])
if minW < 5 {
minW = 5
}
canShrink := widths[col] - minW
if canShrink <= 0 {
continue
}
shrink := excess
if shrink > canShrink {
shrink = canShrink
}
widths[col] -= shrink
}

return widths
}

// Render outputs the table, dynamically fitting columns to the terminal width.
func (t *TableWriter) Render() {
widths := t.renderWidths()
last := len(t.headers) - 1

// Print headers
for i, h := range t.headers {
fmt.Fprintf(t.w, "%-*s", t.widths[i]+2, h)
cell := truncateCell(h, widths[i])
if i < last {
fmt.Fprintf(t.w, "%-*s", widths[i]+columnGap, cell)
} else {
fmt.Fprint(t.w, cell)
}
}
fmt.Fprintln(t.w)

// Print rows
for _, row := range t.rows {
for i, cell := range row {
fmt.Fprintf(t.w, "%-*s", t.widths[i]+2, cell)
cell = truncateCell(cell, widths[i])
if i < last {
fmt.Fprintf(t.w, "%-*s", widths[i]+columnGap, cell)
} else {
fmt.Fprint(t.w, cell)
}
}
fmt.Fprintln(t.w)
}
}

// truncateCell truncates s to fit within maxWidth, appending "..." if needed.
func truncateCell(s string, maxWidth int) string {
if len(s) <= maxWidth {
return s
}
if maxWidth <= 3 {
return s[:maxWidth]
}
return s[:maxWidth-3] + "..."
}

// FormatTimeAgo formats a time as "X ago" string
func FormatTimeAgo(t time.Time) string {
if t.IsZero() {
Expand Down
5 changes: 3 additions & 2 deletions pkg/cmd/ingresscmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ func handleIngressList(ctx context.Context, cmd *cli.Command) error {
}

table := NewTableWriter(os.Stdout, "ID", "NAME", "HOSTNAME", "TARGET", "TLS", "CREATED")
table.TruncOrder = []int{2, 3, 5, 1} // HOSTNAME first, then TARGET, CREATED, NAME
for _, ing := range *ingresses {
// Extract first rule's hostname and target for display
hostname := ""
Expand All @@ -200,8 +201,8 @@ func handleIngressList(ctx context.Context, cmd *cli.Command) error {

table.AddRow(
TruncateID(ing.ID),
TruncateString(ing.Name, 20),
TruncateString(hostname, 25),
ing.Name,
hostname,
target,
tlsEnabled,
FormatTimeAgo(ing.CreatedAt),
Expand Down
5 changes: 3 additions & 2 deletions pkg/cmd/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,12 @@ func handlePs(ctx context.Context, cmd *cli.Command) error {
}

table := NewTableWriter(os.Stdout, "INSTANCE ID", "NAME", "IMAGE", "STATE", "GPU", "HV", "CREATED")
table.TruncOrder = []int{2, 4, 6, 1} // IMAGE first, then GPU, CREATED, NAME
for _, inst := range filtered {
table.AddRow(
TruncateID(inst.ID),
TruncateString(inst.Name, 20),
TruncateString(inst.Image, 25),
inst.Name,
inst.Image,
string(inst.State),
formatGPU(inst.GPU),
formatHypervisor(inst.Hypervisor),
Expand Down
12 changes: 5 additions & 7 deletions pkg/cmd/resourcecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,20 @@ func showResourcesTable(data []byte) error {
if allocations.Exists() && allocations.IsArray() && len(allocations.Array()) > 0 {
fmt.Println()
fmt.Println("ALLOCATIONS:")
fmt.Println("INSTANCE CPU MEMORY DISK DISK I/O NET DOWN NET UP")
fmt.Println(strings.Repeat("-", 95))
table := NewTableWriter(os.Stdout, "INSTANCE", "CPU", "MEMORY", "DISK", "DISK I/O", "NET DOWN", "NET UP")
table.TruncOrder = []int{0} // Only truncate INSTANCE name if needed
allocations.ForEach(func(key, value gjson.Result) bool {
name := value.Get("instance_name").String()
if len(name) > 28 {
name = name[:25] + "..."
}
cpu := value.Get("cpu").Int()
cpu := fmt.Sprintf("%d", value.Get("cpu").Int())
mem := formatBytes(value.Get("memory_bytes").Int())
disk := formatBytes(value.Get("disk_bytes").Int())
diskIO := formatDiskBps(value.Get("disk_io_bps").Int())
netDown := formatBps(value.Get("network_download_bps").Int())
netUp := formatBps(value.Get("network_upload_bps").Int())
fmt.Printf("%-28s %3d %-9s %-9s %-10s %-10s %s\n", name, cpu, mem, disk, diskIO, netDown, netUp)
table.AddRow(name, cpu, mem, disk, diskIO, netDown, netUp)
return true
})
table.Render()
}

return nil
Expand Down