Skip to content

Commit 4aa0df2

Browse files
authored
Respect terminal width (#34)
1 parent 37b6307 commit 4aa0df2

5 files changed

Lines changed: 125 additions & 40 deletions

File tree

pkg/cmd/devicecmd.go

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"os"
7-
"strings"
87

98
"github.com/kernel/hypeman-go"
109
"github.com/kernel/hypeman-go/option"
@@ -154,42 +153,37 @@ func showAvailableDevicesTable(data []byte) error {
154153
return nil
155154
}
156155

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

160159
devices.ForEach(func(key, value gjson.Result) bool {
161160
pciAddr := value.Get("pci_address").String()
162161
vendorID := value.Get("vendor_id").String()
163162
deviceID := value.Get("device_id").String()
164163
vendorName := value.Get("vendor_name").String()
165164
deviceName := value.Get("device_name").String()
166-
iommuGroup := value.Get("iommu_group").Int()
165+
iommuGroup := fmt.Sprintf("%d", value.Get("iommu_group").Int())
167166
driver := value.Get("current_driver").String()
168167

169-
// Format vendor info
170168
vendor := vendorName
171169
if vendor == "" {
172170
vendor = vendorID
173-
} else if len(vendor) > 18 {
174-
vendor = vendor[:15] + "..."
175171
}
176172

177-
// Format device info
178173
device := deviceName
179174
if device == "" {
180175
device = deviceID
181-
} else if len(device) > 18 {
182-
device = device[:15] + "..."
183176
}
184177

185178
if driver == "" {
186179
driver = "-"
187180
}
188181

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

186+
table.Render()
193187
return nil
194188
}
195189

@@ -274,20 +268,12 @@ func showDeviceListTable(data []byte) error {
274268
return nil
275269
}
276270

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

280274
devices.ForEach(func(key, value gjson.Result) bool {
281275
id := value.Get("id").String()
282-
if len(id) > 20 {
283-
id = id[:17] + "..."
284-
}
285-
286276
name := value.Get("name").String()
287-
if len(name) > 20 {
288-
name = name[:17] + "..."
289-
}
290-
291277
deviceType := value.Get("type").String()
292278
pciAddr := value.Get("pci_address").String()
293279

@@ -299,14 +285,13 @@ func showDeviceListTable(data []byte) error {
299285
attachedTo := value.Get("attached_to").String()
300286
if attachedTo == "" {
301287
attachedTo = "-"
302-
} else if len(attachedTo) > 15 {
303-
attachedTo = attachedTo[:12] + "..."
304288
}
305289

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

294+
table.Render()
310295
return nil
311296
}
312297

pkg/cmd/format.go

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,31 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"os"
8+
"strconv"
79
"strings"
810
"time"
911

1012
"github.com/kernel/hypeman-go"
13+
"golang.org/x/term"
1114
)
1215

13-
// TableWriter provides simple table formatting for CLI output
16+
// TableWriter provides simple table formatting for CLI output with
17+
// terminal-width-aware column sizing.
1418
type TableWriter struct {
1519
w io.Writer
1620
headers []string
17-
widths []int
21+
widths []int // natural widths (max of header and cell values)
1822
rows [][]string
23+
24+
// TruncOrder specifies column indices in truncation priority order.
25+
// The first index in the slice is truncated first when the table is
26+
// too wide for the terminal. Columns not listed are never truncated.
27+
TruncOrder []int
1928
}
2029

30+
const columnGap = 2 // spaces between columns
31+
2132
// NewTableWriter creates a new table writer
2233
func NewTableWriter(w io.Writer, headers ...string) *TableWriter {
2334
widths := make([]int, len(headers))
@@ -46,23 +57,112 @@ func (t *TableWriter) AddRow(cells ...string) {
4657
t.rows = append(t.rows, row)
4758
}
4859

49-
// Render outputs the table
60+
// getTerminalWidth returns the terminal width. It tries the stdout
61+
// file descriptor first, then the COLUMNS env var, then defaults to 80.
62+
func getTerminalWidth() int {
63+
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
64+
return w
65+
}
66+
if cols := os.Getenv("COLUMNS"); cols != "" {
67+
if w, err := strconv.Atoi(cols); err == nil && w > 0 {
68+
return w
69+
}
70+
}
71+
return 80
72+
}
73+
74+
// renderWidths computes the final column widths, shrinking columns in
75+
// TruncOrder as needed to fit within the terminal width.
76+
func (t *TableWriter) renderWidths() []int {
77+
n := len(t.headers)
78+
widths := make([]int, n)
79+
copy(widths, t.widths)
80+
81+
termWidth := getTerminalWidth()
82+
83+
// Total space: column widths + gaps (no trailing gap on last column)
84+
total := func() int {
85+
s := 0
86+
for _, w := range widths {
87+
s += w
88+
}
89+
s += columnGap * (n - 1)
90+
return s
91+
}
92+
93+
if total() <= termWidth {
94+
return widths
95+
}
96+
97+
// Shrink columns in TruncOrder until the table fits
98+
for _, col := range t.TruncOrder {
99+
if col < 0 || col >= n {
100+
continue
101+
}
102+
excess := total() - termWidth
103+
if excess <= 0 {
104+
break
105+
}
106+
// Minimum width: at least the header length, but no less than 5
107+
minW := len(t.headers[col])
108+
if minW < 5 {
109+
minW = 5
110+
}
111+
canShrink := widths[col] - minW
112+
if canShrink <= 0 {
113+
continue
114+
}
115+
shrink := excess
116+
if shrink > canShrink {
117+
shrink = canShrink
118+
}
119+
widths[col] -= shrink
120+
}
121+
122+
return widths
123+
}
124+
125+
// Render outputs the table, dynamically fitting columns to the terminal width.
50126
func (t *TableWriter) Render() {
127+
widths := t.renderWidths()
128+
last := len(t.headers) - 1
129+
51130
// Print headers
52131
for i, h := range t.headers {
53-
fmt.Fprintf(t.w, "%-*s", t.widths[i]+2, h)
132+
cell := truncateCell(h, widths[i])
133+
if i < last {
134+
fmt.Fprintf(t.w, "%-*s", widths[i]+columnGap, cell)
135+
} else {
136+
fmt.Fprint(t.w, cell)
137+
}
54138
}
55139
fmt.Fprintln(t.w)
56140

57141
// Print rows
58142
for _, row := range t.rows {
59143
for i, cell := range row {
60-
fmt.Fprintf(t.w, "%-*s", t.widths[i]+2, cell)
144+
cell = truncateCell(cell, widths[i])
145+
if i < last {
146+
fmt.Fprintf(t.w, "%-*s", widths[i]+columnGap, cell)
147+
} else {
148+
fmt.Fprint(t.w, cell)
149+
}
61150
}
62151
fmt.Fprintln(t.w)
63152
}
64153
}
65154

155+
// truncateCell truncates s to fit within maxWidth, appending "..." if needed.
156+
func truncateCell(s string, maxWidth int) string {
157+
if len(s) <= maxWidth {
158+
return s
159+
}
160+
if maxWidth <= 3 {
161+
return s[:maxWidth]
162+
}
163+
return s[:maxWidth-3] + "..."
164+
}
165+
66166
// FormatTimeAgo formats a time as "X ago" string
67167
func FormatTimeAgo(t time.Time) string {
68168
if t.IsZero() {

pkg/cmd/ingresscmd.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func handleIngressList(ctx context.Context, cmd *cli.Command) error {
182182
}
183183

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

201202
table.AddRow(
202203
TruncateID(ing.ID),
203-
TruncateString(ing.Name, 20),
204-
TruncateString(hostname, 25),
204+
ing.Name,
205+
hostname,
205206
target,
206207
tlsEnabled,
207208
FormatTimeAgo(ing.CreatedAt),

pkg/cmd/ps.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,12 @@ func handlePs(ctx context.Context, cmd *cli.Command) error {
7373
}
7474

7575
table := NewTableWriter(os.Stdout, "INSTANCE ID", "NAME", "IMAGE", "STATE", "GPU", "HV", "CREATED")
76+
table.TruncOrder = []int{2, 4, 6, 1} // IMAGE first, then GPU, CREATED, NAME
7677
for _, inst := range filtered {
7778
table.AddRow(
7879
TruncateID(inst.ID),
79-
TruncateString(inst.Name, 20),
80-
TruncateString(inst.Image, 25),
80+
inst.Name,
81+
inst.Image,
8182
string(inst.State),
8283
formatGPU(inst.GPU),
8384
formatHypervisor(inst.Hypervisor),

pkg/cmd/resourcecmd.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,20 @@ func showResourcesTable(data []byte) error {
105105
if allocations.Exists() && allocations.IsArray() && len(allocations.Array()) > 0 {
106106
fmt.Println()
107107
fmt.Println("ALLOCATIONS:")
108-
fmt.Println("INSTANCE CPU MEMORY DISK DISK I/O NET DOWN NET UP")
109-
fmt.Println(strings.Repeat("-", 95))
108+
table := NewTableWriter(os.Stdout, "INSTANCE", "CPU", "MEMORY", "DISK", "DISK I/O", "NET DOWN", "NET UP")
109+
table.TruncOrder = []int{0} // Only truncate INSTANCE name if needed
110110
allocations.ForEach(func(key, value gjson.Result) bool {
111111
name := value.Get("instance_name").String()
112-
if len(name) > 28 {
113-
name = name[:25] + "..."
114-
}
115-
cpu := value.Get("cpu").Int()
112+
cpu := fmt.Sprintf("%d", value.Get("cpu").Int())
116113
mem := formatBytes(value.Get("memory_bytes").Int())
117114
disk := formatBytes(value.Get("disk_bytes").Int())
118115
diskIO := formatDiskBps(value.Get("disk_io_bps").Int())
119116
netDown := formatBps(value.Get("network_download_bps").Int())
120117
netUp := formatBps(value.Get("network_upload_bps").Int())
121-
fmt.Printf("%-28s %3d %-9s %-9s %-10s %-10s %s\n", name, cpu, mem, disk, diskIO, netDown, netUp)
118+
table.AddRow(name, cpu, mem, disk, diskIO, netDown, netUp)
122119
return true
123120
})
121+
table.Render()
124122
}
125123

126124
return nil

0 commit comments

Comments
 (0)