diff --git a/modules/bonfire/live.go b/modules/bonfire/live.go index 5baa76bf..829cffc2 100644 --- a/modules/bonfire/live.go +++ b/modules/bonfire/live.go @@ -539,8 +539,15 @@ func printAbove(owner *console, s string) bool { return false default: } + block := strings.TrimSuffix(s, "\n") + if block == "" && s != "" { + // A blank line (Println()): bubbletea v2.1.0's insertAbove + // returns early for an empty string, which would drop it, so + // send a single space, which prints as a blank line. + block = " " + } // One message for the whole block, sent while holding liveMu: blocks // from different goroutines keep the order of their calls. - v.prog.Send(printLine(strings.TrimSuffix(s, "\n"))) + v.prog.Send(printLine(block)) return true } diff --git a/modules/bonfire/live_test.go b/modules/bonfire/live_test.go index 04056b00..d6a89af9 100644 --- a/modules/bonfire/live_test.go +++ b/modules/bonfire/live_test.go @@ -8,6 +8,7 @@ import ( "io" "os" "regexp" + "slices" "strings" "sync" "testing" @@ -712,3 +713,33 @@ func TestPrintAboveAfterCancel(t *testing.T) { t.Error("a view is still registered after Process returned") } } + +// insertedLine matches one line bubbletea v2 inserts above the view: +// insert line (CSI L), the text, erase to the end of the line (CSI K). +var insertedLine = regexp.MustCompile(`\x1b\[L(.*?)\x1b\[K`) + +// TestPrintAboveBlankLine (IN-11): a blank line printed during a live view +// (out.Println()) prints a blank line above the view instead of being +// dropped. bubbletea writes each line above the view as an inserted line, +// so the test reads those insertions from the raw output: stripping the +// escape sequences would also drop the cursor movement and show a blank +// line that the terminal never gets. +func TestPrintAboveBlankLine(t *testing.T) { + out := &lockedBuffer{} + c := newConsole(strings.NewReader(""), out, io.Discard, noEnv, boolPtr(true), boolPtr(true)) + if err := c.Spinner("working", func() error { + c.Println("before-blank") + c.Println() + c.Println("after-blank") + return nil + }); err != nil { + t.Fatal(err) + } + var lines []string + for _, m := range insertedLine.FindAllStringSubmatch(out.String(), -1) { + lines = append(lines, strings.TrimSpace(ansi.Strip(m[1]))) + } + if want := []string{"before-blank", "", "after-blank"}; !slices.Equal(lines, want) { + t.Errorf("lines inserted above the view = %q, want %q\nraw: %q", lines, want, out.String()) + } +} diff --git a/scripts/check-phase19.8.sh b/scripts/check-phase19.8.sh index 272cc17e..faea0f52 100755 --- a/scripts/check-phase19.8.sh +++ b/scripts/check-phase19.8.sh @@ -134,6 +134,8 @@ BONFIRE_TESTS=( # quick 261028-rqd (info findings): IN-03, IN-06, IN-02, IN-04. TestPluginErrorWordedUnknownCommand TestExecuteKeepsRootHelp TestErrorBlockUsesParsedGlobals TestFlagBool + # quick 261028-rqd: IN-11. + TestPrintAboveBlankLine ) DOCSITE_TESTS=( # Plan 04: the docs checker reads past the global options.