Skip to content

Commit 44e88b5

Browse files
authored
Truncate long columns instead of line wrapping (#26)
Detecting terminal size, use smart column truncation. Seek to avoid truncating shorter columns. <img width="3420" height="378" alt="image" src="https://github.com/user-attachments/assets/1e62dbf3-2d64-4aea-9e0d-1941e4a6300a" /> --- <!-- mesa-description-start --> ## TL;DR Replaced line wrapping with smart column truncation for CLI tables, which now adapt to the terminal's width to improve readability. ## Why we made these changes Previously, wide tables would wrap lines, creating a messy and difficult-to-read layout. This change ensures tables maintain a clean, grid-like structure by truncating overflowing content, making the output much easier to scan. ## What changed? - **`cmd/table.go`**: Implemented intelligent table truncation logic that detects terminal width to prevent line wrapping. A new algorithm proportionally truncates the longest columns first while correctly handling Unicode characters and ANSI color codes. - **`cmd/proxies/get.go`**: Added color-coded status and a 'Last Checked' timestamp to the proxy details output. - **`cmd/app.go`**: Removed truncation for environment variable and action names to ensure their full values are always displayed. - **`cmd/proxies/list_test.go`**: Updated tests to reflect the new 'Status' column in the proxy list output. ## Validation - [ ] Verified tables render correctly on different terminal widths. - [ ] Confirmed that only necessary columns are truncated. - [ ] Tested with tables that have no long columns to ensure no change in behavior. <sup>_Description generated by Mesa. [Update settings](https://app.mesa.dev/onkernel/settings/pull-requests)_</sup> <!-- mesa-description-end -->
1 parent c9601be commit 44e88b5

File tree

10 files changed

+323
-153
lines changed

10 files changed

+323
-153
lines changed

cmd/app.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111
)
1212

1313
var appCmd = &cobra.Command{
14-
Use: "app",
15-
Short: "Manage deployed applications",
16-
Long: "Commands for managing deployed Kernel applications",
14+
Use: "app",
15+
Aliases: []string{"apps"},
16+
Short: "Manage deployed applications",
17+
Long: "Commands for managing deployed Kernel applications",
1718
}
1819

1920
// --- app list subcommand
@@ -80,19 +81,13 @@ func runAppList(cmd *cobra.Command, args []string) error {
8081
envVarsStr := "-"
8182
if len(app.EnvVars) > 0 {
8283
envVarsStr = strings.Join(lo.Keys(app.EnvVars), ", ")
83-
if len(envVarsStr) > 50 {
84-
envVarsStr = envVarsStr[:47] + "..."
85-
}
8684
}
8785

8886
actionsStr := "-"
8987
if len(app.Actions) > 0 {
9088
actionsStr = strings.Join(lo.Map(app.Actions, func(a kernel.AppAction, _ int) string {
9189
return a.Name
9290
}), ", ")
93-
if len(actionsStr) > 50 {
94-
actionsStr = actionsStr[:47] + "..."
95-
}
9691
}
9792

9893
tableData = append(tableData, []string{
@@ -105,7 +100,7 @@ func runAppList(cmd *cobra.Command, args []string) error {
105100
})
106101
}
107102

108-
printTableNoPad(tableData, true)
103+
PrintTableNoPad(tableData, true)
109104
return nil
110105
}
111106

@@ -156,6 +151,6 @@ func runAppHistory(cmd *cobra.Command, args []string) error {
156151
}
157152
}
158153

159-
printTableNoPad(tableData, true)
154+
PrintTableNoPad(tableData, true)
160155
return nil
161156
}

cmd/browsers.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func (b BrowsersCmd) List(ctx context.Context) error {
144144
})
145145
}
146146

147-
printTableNoPad(tableData, true)
147+
PrintTableNoPad(tableData, true)
148148
return nil
149149
}
150150

@@ -208,7 +208,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
208208
tableData = append(tableData, []string{"Profile", profVal})
209209
}
210210

211-
printTableNoPad(tableData, true)
211+
PrintTableNoPad(tableData, true)
212212
return nil
213213
}
214214

@@ -415,7 +415,7 @@ func (b BrowsersCmd) ReplaysList(ctx context.Context, in BrowsersReplaysListInpu
415415
for _, r := range *items {
416416
rows = append(rows, []string{r.ReplayID, util.FormatLocal(r.StartedAt), util.FormatLocal(r.FinishedAt), truncateURL(r.ReplayViewURL, 60)})
417417
}
418-
printTableNoPad(rows, true)
418+
PrintTableNoPad(rows, true)
419419
return nil
420420
}
421421

@@ -440,7 +440,7 @@ func (b BrowsersCmd) ReplaysStart(ctx context.Context, in BrowsersReplaysStartIn
440440
return util.CleanedUpSdkError{Err: err}
441441
}
442442
rows := pterm.TableData{{"Property", "Value"}, {"Replay ID", res.ReplayID}, {"View URL", res.ReplayViewURL}, {"Started At", util.FormatLocal(res.StartedAt)}}
443-
printTableNoPad(rows, true)
443+
PrintTableNoPad(rows, true)
444444
return nil
445445
}
446446

@@ -563,7 +563,7 @@ func (b BrowsersCmd) ProcessExec(ctx context.Context, in BrowsersProcessExecInpu
563563
return util.CleanedUpSdkError{Err: err}
564564
}
565565
rows := pterm.TableData{{"Property", "Value"}, {"Exit Code", fmt.Sprintf("%d", res.ExitCode)}, {"Duration (ms)", fmt.Sprintf("%d", res.DurationMs)}}
566-
printTableNoPad(rows, true)
566+
PrintTableNoPad(rows, true)
567567
if res.StdoutB64 != "" {
568568
data, err := base64.StdEncoding.DecodeString(res.StdoutB64)
569569
if err != nil {
@@ -625,7 +625,7 @@ func (b BrowsersCmd) ProcessSpawn(ctx context.Context, in BrowsersProcessSpawnIn
625625
return util.CleanedUpSdkError{Err: err}
626626
}
627627
rows := pterm.TableData{{"Property", "Value"}, {"Process ID", res.ProcessID}, {"PID", fmt.Sprintf("%d", res.Pid)}, {"Started At", util.FormatLocal(res.StartedAt)}}
628-
printTableNoPad(rows, true)
628+
PrintTableNoPad(rows, true)
629629
return nil
630630
}
631631

@@ -669,7 +669,7 @@ func (b BrowsersCmd) ProcessStatus(ctx context.Context, in BrowsersProcessStatus
669669
return util.CleanedUpSdkError{Err: err}
670670
}
671671
rows := pterm.TableData{{"Property", "Value"}, {"State", string(res.State)}, {"CPU %", fmt.Sprintf("%.2f", res.CPUPct)}, {"Mem Bytes", fmt.Sprintf("%d", res.MemBytes)}, {"Exit Code", fmt.Sprintf("%d", res.ExitCode)}}
672-
printTableNoPad(rows, true)
672+
PrintTableNoPad(rows, true)
673673
return nil
674674
}
675675

@@ -928,7 +928,7 @@ func (b BrowsersCmd) FSFileInfo(ctx context.Context, in BrowsersFSFileInfoInput)
928928
return util.CleanedUpSdkError{Err: err}
929929
}
930930
rows := pterm.TableData{{"Property", "Value"}, {"Path", res.Path}, {"Name", res.Name}, {"Mode", res.Mode}, {"IsDir", fmt.Sprintf("%t", res.IsDir)}, {"SizeBytes", fmt.Sprintf("%d", res.SizeBytes)}, {"ModTime", util.FormatLocal(res.ModTime)}}
931-
printTableNoPad(rows, true)
931+
PrintTableNoPad(rows, true)
932932
return nil
933933
}
934934

@@ -957,7 +957,7 @@ func (b BrowsersCmd) FSListFiles(ctx context.Context, in BrowsersFSListFilesInpu
957957
for _, f := range *res {
958958
rows = append(rows, []string{f.Mode, fmt.Sprintf("%d", f.SizeBytes), util.FormatLocal(f.ModTime), f.Name, f.Path})
959959
}
960-
printTableNoPad(rows, true)
960+
PrintTableNoPad(rows, true)
961961
return nil
962962
}
963963

cmd/profiles.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (p ProfilesCmd) List(ctx context.Context) error {
7373
util.FormatLocal(prof.LastUsedAt),
7474
})
7575
}
76-
printTableNoPad(rows, true)
76+
PrintTableNoPad(rows, true)
7777
return nil
7878
}
7979

@@ -96,7 +96,7 @@ func (p ProfilesCmd) Get(ctx context.Context, in ProfilesGetInput) error {
9696
rows = append(rows, []string{"Created At", util.FormatLocal(item.CreatedAt)})
9797
rows = append(rows, []string{"Updated At", util.FormatLocal(item.UpdatedAt)})
9898
rows = append(rows, []string{"Last Used At", util.FormatLocal(item.LastUsedAt)})
99-
printTableNoPad(rows, true)
99+
PrintTableNoPad(rows, true)
100100
return nil
101101
}
102102

@@ -118,7 +118,7 @@ func (p ProfilesCmd) Create(ctx context.Context, in ProfilesCreateInput) error {
118118
rows = append(rows, []string{"Name", name})
119119
rows = append(rows, []string{"Created At", util.FormatLocal(item.CreatedAt)})
120120
rows = append(rows, []string{"Last Used At", util.FormatLocal(item.LastUsedAt)})
121-
printTableNoPad(rows, true)
121+
PrintTableNoPad(rows, true)
122122
return nil
123123
}
124124

cmd/proxies/create.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/onkernel/cli/pkg/table"
78
"github.com/onkernel/cli/pkg/util"
89
"github.com/onkernel/kernel-go-sdk"
910
"github.com/pterm/pterm"
@@ -188,7 +189,7 @@ func (p ProxyCmd) Create(ctx context.Context, in ProxyCreateInput) error {
188189
}
189190
rows = append(rows, []string{"Protocol", protocol})
190191

191-
PrintTableNoPad(rows, true)
192+
table.PrintTableNoPad(rows, true)
192193
return nil
193194
}
194195

cmd/proxies/get.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/onkernel/cli/pkg/table"
78
"github.com/onkernel/cli/pkg/util"
89
"github.com/onkernel/kernel-go-sdk"
910
"github.com/pterm/pterm"
@@ -38,7 +39,22 @@ func (p ProxyCmd) Get(ctx context.Context, in ProxyGetInput) error {
3839
// Display type-specific config details
3940
rows = append(rows, getProxyConfigRows(item)...)
4041

41-
PrintTableNoPad(rows, true)
42+
// Display status with color
43+
status := string(item.Status)
44+
if status == "" {
45+
status = "-"
46+
} else if status == "available" {
47+
status = pterm.Green(status)
48+
} else if status == "unavailable" {
49+
status = pterm.Red(status)
50+
}
51+
rows = append(rows, []string{"Status", status})
52+
53+
// Display last checked timestamp
54+
lastChecked := util.FormatLocal(item.LastChecked)
55+
rows = append(rows, []string{"Last Checked", lastChecked})
56+
57+
table.PrintTableNoPad(rows, true)
4258
return nil
4359
}
4460

cmd/proxies/helpers.go

Lines changed: 0 additions & 14 deletions
This file was deleted.

cmd/proxies/list.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strings"
77

8+
"github.com/onkernel/cli/pkg/table"
89
"github.com/onkernel/cli/pkg/util"
910
"github.com/onkernel/kernel-go-sdk"
1011
"github.com/pterm/pterm"
@@ -68,7 +69,7 @@ func (p ProxyCmd) List(ctx context.Context) error {
6869
})
6970
}
7071

71-
PrintTableNoPad(tableData, true)
72+
table.PrintTableNoPad(tableData, true)
7273
return nil
7374
}
7475

cmd/proxies/list_test.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,33 +64,31 @@ func TestProxyList_WithProxies(t *testing.T) {
6464
assert.NoError(t, err)
6565
output := buf.String()
6666

67-
// Check table headers
67+
// Check table headers (Config may be truncated in narrow terminals)
6868
assert.Contains(t, output, "ID")
6969
assert.Contains(t, output, "Name")
7070
assert.Contains(t, output, "Type")
7171
assert.Contains(t, output, "Protocol")
72-
assert.Contains(t, output, "Config")
72+
assert.Contains(t, output, "Status")
7373

74-
// Check proxy data
74+
// Check proxy data - verify IDs and short columns are fully visible
7575
assert.Contains(t, output, "dc-1")
7676
assert.Contains(t, output, "https") // Protocol is shown
77-
assert.Contains(t, output, "Country")
77+
assert.Contains(t, output, "datacenter")
7878

7979
assert.Contains(t, output, "res-1")
80+
assert.Contains(t, output, "residential")
8081

8182
assert.Contains(t, output, "custom-1")
8283
assert.Contains(t, output, "My Proxy")
8384
assert.Contains(t, output, "custom")
84-
assert.Contains(t, output, "proxy") // Part of proxy.example.com, will be truncated
8585

8686
assert.Contains(t, output, "mobile-1")
8787
assert.Contains(t, output, "mobile")
88-
assert.Contains(t, output, "Carrier: verizon")
8988

9089
assert.Contains(t, output, "isp-1")
9190
assert.Contains(t, output, "-") // Empty name shows as "-"
9291
assert.Contains(t, output, "isp")
93-
assert.Contains(t, output, "Country: EU")
9492
}
9593

9694
func TestProxyList_Error(t *testing.T) {

cmd/table.go

Lines changed: 4 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,11 @@
11
package cmd
22

33
import (
4-
"strings"
5-
"unicode/utf8"
6-
4+
"github.com/onkernel/cli/pkg/table"
75
"github.com/pterm/pterm"
86
)
97

10-
// printTableNoPad renders a table similar to pterm.DefaultTable, but it avoids
11-
// adding trailing padding spaces after the last column and does not add blank
12-
// padded lines to match multi-line cells in other columns. The last column may
13-
// contain multi-line content which will be printed as-is on following lines.
14-
func printTableNoPad(data pterm.TableData, hasHeader bool) {
15-
if len(data) == 0 {
16-
return
17-
}
18-
19-
// Determine number of columns from the first row
20-
numCols := len(data[0])
21-
if numCols == 0 {
22-
return
23-
}
24-
25-
// Pre-compute max width per column for all but the last column
26-
maxColWidths := make([]int, numCols)
27-
for _, row := range data {
28-
for colIdx := 0; colIdx < numCols && colIdx < len(row); colIdx++ {
29-
if colIdx == numCols-1 {
30-
continue
31-
}
32-
for _, line := range strings.Split(row[colIdx], "\n") {
33-
if w := utf8.RuneCountInString(line); w > maxColWidths[colIdx] {
34-
maxColWidths[colIdx] = w
35-
}
36-
}
37-
}
38-
}
39-
40-
var b strings.Builder
41-
sep := pterm.DefaultTable.Separator
42-
sepStyled := pterm.ThemeDefault.TableSeparatorStyle.Sprint(sep)
43-
44-
renderRow := func(row []string, styleHeader bool) {
45-
// Build first-line-only for non-last columns; last column is full string
46-
firstLineParts := make([]string, 0, numCols)
47-
for colIdx := 0; colIdx < numCols; colIdx++ {
48-
var cell string
49-
if colIdx < len(row) {
50-
cell = row[colIdx]
51-
}
52-
53-
if colIdx < numCols-1 {
54-
// Only the first line for non-last columns
55-
lines := strings.Split(cell, "\n")
56-
first := ""
57-
if len(lines) > 0 {
58-
first = lines[0]
59-
}
60-
padCount := maxColWidths[colIdx] - utf8.RuneCountInString(first)
61-
if padCount < 0 {
62-
padCount = 0
63-
}
64-
firstLineParts = append(firstLineParts, first+strings.Repeat(" ", padCount))
65-
} else {
66-
// Last column: render the first line now; remaining lines after
67-
lines := strings.Split(cell, "\n")
68-
if len(lines) > 0 {
69-
firstLineParts = append(firstLineParts, lines[0])
70-
} else {
71-
firstLineParts = append(firstLineParts, "")
72-
}
73-
}
74-
}
75-
76-
line := strings.Join(firstLineParts[:numCols-1], sepStyled)
77-
if numCols > 1 {
78-
if line != "" {
79-
line += sepStyled
80-
}
81-
line += firstLineParts[numCols-1]
82-
}
83-
84-
if styleHeader {
85-
b.WriteString(pterm.ThemeDefault.TableHeaderStyle.Sprint(line))
86-
} else {
87-
b.WriteString(line)
88-
}
89-
b.WriteString("\n")
90-
91-
// Print remaining lines from the last column without alignment padding
92-
if numCols > 0 {
93-
var lastCell string
94-
if len(row) >= numCols {
95-
lastCell = row[numCols-1]
96-
}
97-
lines := strings.Split(lastCell, "\n")
98-
if len(lines) > 1 {
99-
rest := strings.Join(lines[1:], "\n")
100-
if rest != "" {
101-
b.WriteString(rest)
102-
b.WriteString("\n")
103-
}
104-
}
105-
}
106-
}
107-
108-
for idx, row := range data {
109-
renderRow(row, hasHeader && idx == 0)
110-
}
111-
112-
pterm.Print(b.String())
8+
// PrintTableNoPad is a wrapper around pkg/table.PrintTableNoPad for backwards compatibility
9+
func PrintTableNoPad(data pterm.TableData, hasHeader bool) {
10+
table.PrintTableNoPad(data, hasHeader)
11311
}

0 commit comments

Comments
 (0)