From 29028cb601cdd39dfe4306be5f3920f7995f06f1 Mon Sep 17 00:00:00 2001 From: Mustafa Senoglu Date: Sat, 5 Sep 2026 15:08:56 +0300 Subject: [PATCH] ui: truncate long text to terminal width in Select/Prompt templates screenbuf assumes each Write() is exactly one terminal line, but when text is wider than the terminal it wraps, causing stale content to remain on screen (issue #466). Add a truncFunc template helper that measures the terminal width via golang.org/x/term and truncates the string so the rendered line stays within bounds. The function is registered in promptui.FuncMap and applied to the default Select and NamedSelect templates (Label, Active, Inactive) with a prefixLen of 4 to account for the page arrow, spacing, and icon characters. --- go.mod | 1 + ui/templates.go | 48 ++++++++++++++++++++++++++++++++++++++++++------ ui/terminal.go | 18 ++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 ui/terminal.go diff --git a/go.mod b/go.mod index 5ded2a8..c360aa6 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( go.step.sm/crypto v0.89.0 golang.org/x/net v0.58.0 golang.org/x/sys v0.47.0 + golang.org/x/term v0.45.0 ) require ( diff --git a/ui/templates.go b/ui/templates.go index cc37891..9c317c6 100644 --- a/ui/templates.go +++ b/ui/templates.go @@ -3,6 +3,7 @@ package ui import ( "fmt" "runtime" + "unicode/utf8" "github.com/chzyer/readline" "github.com/manifoldco/promptui" @@ -40,6 +41,34 @@ func init() { promptui.KeyForward = readline.CharForward promptui.KeyForwardDisplay = "→" } + + // Register trunc template function in promptui's FuncMap so all + // templates (label, active, inactive, selected, help) can use it. + promptui.FuncMap["trunc"] = truncFunc +} + +// truncFunc truncates s so that the rendered line does not exceed the +// terminal width. prefixLen is the visible character count of everything +// that appears before the template content on the same line (icons, +// arrows, indentation). An ellipsis "..." is appended when truncation +// occurs. +func truncFunc(s string, prefixLen int) string { + w := getTerminalWidth() + if w <= 0 { + return s + } + max := w - prefixLen + if max <= 0 { + return "" + } + n := utf8.RuneCountInString(s) + if n <= max { + return s + } + if max <= 3 { + return string([]rune(s)[:max]) + } + return string([]rune(s)[:max-3]) + "..." } // PrintSelectedTemplate returns the default template used in PrintSelected. @@ -71,11 +100,18 @@ func SimplePromptTemplates() *promptui.PromptTemplates { // SelectTemplates returns the default promptui.SelectTemplate for string // slices. The given name is the prompt of the selected option. +// +// The prefixLen values account for the visible characters before the +// item text on each line: +// +// Label: "?" + " " + ... + ": " → 4 +// Active: "▸" + " " → 4 (incl. page arrow + space) +// Inactive: " " → 4 func SelectTemplates(name string) *promptui.SelectTemplates { return &promptui.SelectTemplates{ - Label: fmt.Sprintf("%s {{ . }}: ", IconInitial), - Active: fmt.Sprintf("%s {{ . | underline }}", IconSelect), - Inactive: " {{ . }}", + Label: fmt.Sprintf("%s {{ . | trunc 4 }}: ", IconInitial), + Active: fmt.Sprintf("%s {{ . | trunc 4 | underline }}", IconSelect), + Inactive: " {{ . | trunc 4 }}", Selected: fmt.Sprintf(`{{ %q | green }} {{ "%s:" | bold }} {{ .Name }}`, IconGood, name), } } @@ -85,9 +121,9 @@ func SelectTemplates(name string) *promptui.SelectTemplates { // option. func NamedSelectTemplates(name string) *promptui.SelectTemplates { return &promptui.SelectTemplates{ - Label: fmt.Sprintf("%s {{.Name}}: ", IconInitial), - Active: fmt.Sprintf("%s {{ .Name | underline }}", IconSelect), - Inactive: " {{.Name}}", + Label: fmt.Sprintf("%s {{ .Name | trunc 4 }}: ", IconInitial), + Active: fmt.Sprintf("%s {{ .Name | trunc 4 | underline }}", IconSelect), + Inactive: " {{ .Name | trunc 4 }}", Selected: fmt.Sprintf(`{{ %q | green }} {{ "%s:" | bold }} {{ .Name }}`, IconGood, name), } } diff --git a/ui/terminal.go b/ui/terminal.go new file mode 100644 index 0000000..b180580 --- /dev/null +++ b/ui/terminal.go @@ -0,0 +1,18 @@ +package ui + +import ( + "os" + + "golang.org/x/term" +) + +// getTerminalWidth returns the width of the terminal in columns. +// Returns 0 if the width cannot be determined. +func getTerminalWidth() int { + fd := int(os.Stderr.Fd()) + w, _, err := term.GetSize(fd) + if err != nil { + return 0 + } + return w +}