From 8c9ca9db0032fbb64e2b5b747d6c600a855f387b Mon Sep 17 00:00:00 2001 From: kami Date: Wed, 10 Jun 2026 11:13:45 +0400 Subject: [PATCH] fix(tui): F-006 flatten multi-line paste in input render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A multi-line paste put raw newlines into the single-line input slot, overflowing the fixed-height box and leaving a smeared/stale region. flattenForDisplay collapses newlines (↵) and tabs for rendering only; the inputBuffer keeps real characters for submission. --- apps/tui-go/internal/app/input_test.go | 26 ++++++++++++++++++++++++++ apps/tui-go/internal/app/view.go | 10 +++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 apps/tui-go/internal/app/input_test.go 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: · ·