From 273d68a3eaa563eb2c962d9f11d090e7b6df4d59 Mon Sep 17 00:00:00 2001 From: Akshay Singla Date: Fri, 29 May 2026 17:34:32 +0000 Subject: [PATCH 1/2] lakebox: measure NAME column widths in terminal cells, not bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anwell flagged on the bug-bash form that `lakebox list` mis-aligns rows whose `--name` contains emoji or CJK glyphs: 🚀 is 4 bytes / 1 rune but renders as 2 terminal cells, so the row's STATUS/AUTOSTOP/DEFAULT columns shift left when the padding math counts bytes (len) instead of display width. Switch every column-padding computation in `list` (and the symmetric NAME column in `ssh-key list`) to runewidth.StringWidth, which knows about East Asian Width and emoji widths. Names round-trip and store fine — only the display math was wrong. Promotes `github.com/mattn/go-runewidth` from indirect to direct require with the MIT SPDX comment and adds the corresponding NOTICE entry. The package was already pulled in transitively by the charmbracelet/x family. Co-authored-by: Isaac --- NOTICE | 4 ++++ cmd/lakebox/list.go | 21 ++++++++++++++------- cmd/lakebox/sshkey.go | 9 ++++++--- go.mod | 2 +- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/NOTICE b/NOTICE index 01f9df98246..fb80040b33a 100644 --- a/NOTICE +++ b/NOTICE @@ -151,6 +151,10 @@ mattn/go-isatty - https://github.com/mattn/go-isatty Copyright (c) Yasuhiro MATSUMOTO License - https://github.com/mattn/go-isatty/blob/master/LICENSE +mattn/go-runewidth - https://github.com/mattn/go-runewidth +Copyright (c) 2016 Yasuhiro Matsumoto +License - https://github.com/mattn/go-runewidth/blob/master/LICENSE + sabhiram/go-gitignore - https://github.com/sabhiram/go-gitignore Copyright (c) 2015 Shaba Abhiram License - https://github.com/sabhiram/go-gitignore/blob/master/LICENSE diff --git a/cmd/lakebox/list.go b/cmd/lakebox/list.go index 2bcef9c4243..7e951ffd7be 100644 --- a/cmd/lakebox/list.go +++ b/cmd/lakebox/list.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" + "github.com/mattn/go-runewidth" "github.com/spf13/cobra" ) @@ -101,21 +102,27 @@ Example: // rendered only when at least one entry sets a display name // different from the ID — there's no point in a column of // pet-names that duplicate the ID column. + // All column widths are measured in *terminal cells*, not + // bytes or runes — emoji and CJK glyphs render as 2 cells + // despite being 1 rune / multi-byte, and using len() here + // (which counts bytes) misaligns the row whenever a `--name` + // includes wide characters. runewidth.StringWidth gives the + // East-Asian-Width-corrected cell count. idCol := 10 autostopCol := 8 nameCol := 4 showName := false for _, e := range entries { - if l := len(e.SandboxID); l > idCol { + if l := runewidth.StringWidth(e.SandboxID); l > idCol { idCol = l } - if l := len(e.autoStopLabel()); l > autostopCol { + if l := runewidth.StringWidth(e.autoStopLabel()); l > autostopCol { autostopCol = l } if e.Name != "" && e.Name != e.SandboxID { showName = true } - if l := len(e.Name); l > nameCol { + if l := runewidth.StringWidth(e.Name); l > nameCol { nameCol = l } } @@ -152,11 +159,11 @@ Example: } // Pad each cell manually so visible-width alignment is // preserved after the helpers wrap them with ANSI escapes. - idPad := max(idCol-len(id), 0) + idPad := max(idCol-runewidth.StringWidth(id), 0) st := status(ctx, e.Status) - stPad := max(statusCol-len(e.Status), 0) + stPad := max(statusCol-runewidth.StringWidth(e.Status), 0) as := e.autoStopLabel() - asPad := max(autostopCol-len(as), 0) + asPad := max(autostopCol-runewidth.StringWidth(as), 0) idStr := cmdio.Bold(ctx, id) if strings.EqualFold(e.Status, "running") { idStr = cmdio.Bold(ctx, cmdio.Cyan(ctx, id)) @@ -166,7 +173,7 @@ Example: if nm == "" || nm == id { nm = "-" } - nmPad := max(nameCol-len(nm), 0) + nmPad := max(nameCol-runewidth.StringWidth(nm), 0) nmStr := nm if nm == "-" { nmStr = cmdio.Faint(ctx, "-") diff --git a/cmd/lakebox/sshkey.go b/cmd/lakebox/sshkey.go index c0a5d915315..5b7f3829aa9 100644 --- a/cmd/lakebox/sshkey.go +++ b/cmd/lakebox/sshkey.go @@ -10,6 +10,7 @@ import ( "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/cmdctx" "github.com/databricks/cli/libs/cmdio" + "github.com/mattn/go-runewidth" "github.com/spf13/cobra" ) @@ -115,9 +116,11 @@ Examples: out := cmd.OutOrStdout() blank(out) + // Measure in terminal cells (runewidth) so wide / emoji + // glyphs in `--name` don't misalign the row. nameCol := 4 for _, k := range keys { - if l := len(k.Name); l > nameCol { + if l := runewidth.StringWidth(k.Name); l > nameCol { nameCol = l } } @@ -136,10 +139,10 @@ Examples: for _, k := range keys { // Pad NAME manually from the raw width because cmdio.Faint // wraps the cell in ANSI escapes that throw off `%-*s`. - displayName, visibleNameLen := k.Name, len(k.Name) + displayName, visibleNameLen := k.Name, runewidth.StringWidth(k.Name) if displayName == "" { displayName = cmdio.Faint(ctx, "(unset)") - visibleNameLen = len("(unset)") + visibleNameLen = runewidth.StringWidth("(unset)") } namePad := max(nameCol-visibleNameLen, 0) gutter := " " diff --git a/go.mod b/go.mod index f35dadb31ce..ddeceadd2f1 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/hexops/gotextdiff v1.0.3 // BSD-3-Clause github.com/jackc/pgx/v5 v5.9.2 // MIT github.com/mattn/go-isatty v0.0.22 // MIT + github.com/mattn/go-runewidth v0.0.23 // MIT github.com/palantir/pkg/yamlpatch v1.5.0 // BSD-3-Clause github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // BSD-2-Clause github.com/quasilyte/go-ruleguard/dsl v0.3.22 // BSD-3-Clause @@ -80,7 +81,6 @@ require ( github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect From d6e585163425cf019f719ff166ec4d963e85b12d Mon Sep 17 00:00:00 2001 From: Akshay Singla Date: Fri, 29 May 2026 17:39:02 +0000 Subject: [PATCH 2/2] lakebox: format `--idle-timeout` bounds and offending value as Go durations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anwell flagged on the bug-bash form: `config --idle-timeout 25h` reported `between 60s and 86400s, got 90000s` — every number expressed in a unit the user didn't type, even though `--help` documented the same bounds as `60s to 24h`. Net effect: the user had to mentally convert 90000s back to 25h to understand the rejection. Route bounds and the offending value through `formatDurationSecs` (already in api.go), so the error reads: Error: idle-timeout must be 0 (clear) or between 1m and 24h, got 25h Also nudge the `--help` lower-bound text from `60s` → `1m` so both strings agree. Co-authored-by: Isaac --- cmd/lakebox/config.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cmd/lakebox/config.go b/cmd/lakebox/config.go index f5ad8ad8081..6ca1d6350ce 100644 --- a/cmd/lakebox/config.go +++ b/cmd/lakebox/config.go @@ -38,7 +38,7 @@ Three knobs are independent — pass any combination: the sandbox after this much idle time. Pass 0 (or 0s) to clear and revert to the manager's global default (10m). Valid range when set: - 60s to 24h. + 1m to 24h. --no-autostop[=true|false] When true, the sandbox is exempt from idle-driven auto-stop entirely. The @@ -157,9 +157,15 @@ func checkIdleSecs(secs int64) (int64, error) { return 0, nil // clear / revert to global default } if secs < minIdleTimeoutSecs || secs > maxIdleTimeoutSecs { + // Format both the bounds and the offending value as Go-style + // durations to match the input form the user typed and the + // flag's --help text (Anwell flagged the prior `86400s` / + // `90000s` echoes as confusing — same unit as input now). return 0, fmt.Errorf( - "idle-timeout must be 0 (clear) or between %ds and %ds, got %ds", - minIdleTimeoutSecs, maxIdleTimeoutSecs, secs, + "idle-timeout must be 0 (clear) or between %s and %s, got %s", + formatDurationSecs(minIdleTimeoutSecs), + formatDurationSecs(maxIdleTimeoutSecs), + formatDurationSecs(secs), ) } return secs, nil