diff --git a/apps/tui-go/internal/app/input_test.go b/apps/tui-go/internal/app/input_test.go new file mode 100644 index 00000000..6078df32 --- /dev/null +++ b/apps/tui-go/internal/app/input_test.go @@ -0,0 +1,26 @@ +package app + +import ( + "strings" + "testing" +) + +// F-006: a multi-line paste must collapse to a single display line so the fixed-height +// input box can't overflow and leave a smeared/stale region. +func TestFlattenForDisplay(t *testing.T) { + cases := map[string]string{ + "plain text": "plain text", + "line one\nline two": "line one↵ line two", + "crlf\r\nhere": "crlf↵ here", + "tab\tseparated": "tab separated", + "a\nb\nc": "a↵ b↵ c", + } + for in, want := range cases { + if got := flattenForDisplay(in); got != want { + t.Errorf("flattenForDisplay(%q) = %q, want %q", in, got, want) + } + if strings.ContainsAny(flattenForDisplay(in), "\n\r\t") { + t.Errorf("flattenForDisplay(%q) still contains a control char", in) + } + } +} diff --git a/apps/tui-go/internal/app/view.go b/apps/tui-go/internal/app/view.go index 721a90fa..e0eee513 100644 --- a/apps/tui-go/internal/app/view.go +++ b/apps/tui-go/internal/app/view.go @@ -206,6 +206,14 @@ func (m Model) renderMain(h int) string { // --- input bar --- +// flattenForDisplay collapses control characters (newlines from a multi-line paste, tabs) +// to a single visible line so the fixed-height input box can't overflow and smear stale cells +// (F-006). The underlying inputBuffer keeps its real characters for submission. +func flattenForDisplay(s string) string { + repl := strings.NewReplacer("\r\n", "↵ ", "\r", "↵ ", "\n", "↵ ", "\t", " ") + return repl.Replace(s) +} + func (m Model) renderInput() string { t := m.theme @@ -231,7 +239,7 @@ func (m Model) renderInput() string { case m.inputBuffer == "": line = caret + t.span(placeholder, t.P.Faint) default: - line = t.span(m.inputBuffer, t.P.FgStrong) + caret + line = t.span(flattenForDisplay(m.inputBuffer), t.P.FgStrong) + caret } // status sub-line: · ·