From 1fe51d817109aa51699ccd8245342c52a4c8d7a3 Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Wed, 11 Feb 2026 11:25:59 -0800 Subject: [PATCH] Respect terminal width --- pkg/cmd/devicecmd.go | 33 ++++--------- pkg/cmd/format.go | 110 +++++++++++++++++++++++++++++++++++++++-- pkg/cmd/ingresscmd.go | 5 +- pkg/cmd/ps.go | 5 +- pkg/cmd/resourcecmd.go | 12 ++--- 5 files changed, 125 insertions(+), 40 deletions(-) diff --git a/pkg/cmd/devicecmd.go b/pkg/cmd/devicecmd.go index ff02e6e..5b102de 100644 --- a/pkg/cmd/devicecmd.go +++ b/pkg/cmd/devicecmd.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "strings" "github.com/kernel/hypeman-go" "github.com/kernel/hypeman-go/option" @@ -154,8 +153,8 @@ 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() @@ -163,33 +162,28 @@ func showAvailableDevicesTable(data []byte) error { 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 } @@ -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() @@ -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 } diff --git a/pkg/cmd/format.go b/pkg/cmd/format.go index f8999c8..66aeca7 100644 --- a/pkg/cmd/format.go +++ b/pkg/cmd/format.go @@ -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)) @@ -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() { diff --git a/pkg/cmd/ingresscmd.go b/pkg/cmd/ingresscmd.go index 09d9401..2e83b87 100644 --- a/pkg/cmd/ingresscmd.go +++ b/pkg/cmd/ingresscmd.go @@ -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 := "" @@ -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), diff --git a/pkg/cmd/ps.go b/pkg/cmd/ps.go index 2a2c10c..832182b 100644 --- a/pkg/cmd/ps.go +++ b/pkg/cmd/ps.go @@ -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), diff --git a/pkg/cmd/resourcecmd.go b/pkg/cmd/resourcecmd.go index fe1495f..de2cce4 100644 --- a/pkg/cmd/resourcecmd.go +++ b/pkg/cmd/resourcecmd.go @@ -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