diff --git a/pkg/cli/app.go b/pkg/cli/app.go index e3ad0679f..c943dfed8 100644 --- a/pkg/cli/app.go +++ b/pkg/cli/app.go @@ -260,7 +260,7 @@ func (a *app) redraw(flag redrawFlag) { addons = append([]tk.Widget(nil), s.Addons...) }) - bufNotes := renderNotes(notes, width) + mergedNotes := mergeNotes(notes) isFinalRedraw := flag&finalRedraw != 0 if isFinalRedraw { hideRPrompt := !a.RPromptPersistent() @@ -277,28 +277,27 @@ func (a *app) redraw(flag redrawFlag) { // this with a Buffer that has one empty line. bufMain.ExtendDown(&term.Buffer{Lines: [][]term.Cell{nil}}, true) - a.TTY.UpdateBuffer(bufNotes, bufMain, flag&fullRedraw != 0) + a.TTY.UpdateBuffer(mergedNotes, bufMain, flag&fullRedraw != 0) a.TTY.ResetBuffer() } else { bufMain := renderApp(append([]tk.Widget{a.codeArea}, addons...), width, height) - a.TTY.UpdateBuffer(bufNotes, bufMain, flag&fullRedraw != 0) + a.TTY.UpdateBuffer(mergedNotes, bufMain, flag&fullRedraw != 0) } } -// Renders notes. This does not respect height so that overflow notes end up in -// the scrollback buffer. -func renderNotes(notes []ui.Text, width int) *term.Buffer { +// Merges notes, separating them with newlines. +func mergeNotes(notes []ui.Text) ui.Text { if len(notes) == 0 { return nil } - bb := term.NewBufferBuilder(width) + tb := new(ui.TextBuilder) for i, note := range notes { if i > 0 { - bb.Newline() + tb.WriteText(ui.T("\n")) } - bb.WriteStyled(note) + tb.WriteText(note) } - return bb.Buffer() + return tb.Text() } // Renders the codearea, and uses the rest of the height for the listing. diff --git a/pkg/cli/app_test.go b/pkg/cli/app_test.go index e35419bc0..cb46ba631 100644 --- a/pkg/cli/app_test.go +++ b/pkg/cli/app_test.go @@ -484,7 +484,7 @@ func TestReadCode_NotifiesAboutUnboundKey(t *testing.T) { f.TTY.Inject(term.K(ui.F1)) - f.TestTTYNotes(t, "Unbound key: F1") + f.TTY.TestMsg(t, ui.T("Unbound key: F1")) } // Misc features. @@ -531,9 +531,8 @@ func TestReadCode_ShowNotes(t *testing.T) { f.App.Notify(ui.T("note 2")) unblock <- struct{}{} - // Test that the note is rendered onto the notes buffer. - wantNotesBuf := bb().Write("note").Newline().Write("note 2").Buffer() - f.TTY.TestNotesBuffer(t, wantNotesBuf) + // Test that the message is rendered. + f.TTY.TestMsg(t, ui.T("note\nnote 2")) // Test that notes are flushed after being rendered. if n := len(f.App.CopyState().Notes); n > 0 { diff --git a/pkg/cli/clitest/apptest.go b/pkg/cli/clitest/apptest.go index 4f407fc24..f7860770f 100644 --- a/pkg/cli/clitest/apptest.go +++ b/pkg/cli/clitest/apptest.go @@ -86,12 +86,6 @@ func (f *Fixture) TestTTY(t *testing.T, args ...any) { f.TTY.TestBuffer(t, f.MakeBuffer(args...)) } -// TestTTYNotes is equivalent to f.TTY.TestNotesBuffer(f.MakeBuffer(args...)). -func (f *Fixture) TestTTYNotes(t *testing.T, args ...any) { - t.Helper() - f.TTY.TestNotesBuffer(t, f.MakeBuffer(args...)) -} - // StartReadCode starts the readCode function asynchronously, and returns two // channels that deliver its return values. The two channels are closed after // return values are delivered, so that subsequent reads will return zero values diff --git a/pkg/cli/clitest/apptest_test.go b/pkg/cli/clitest/apptest_test.go index abc4364d0..cb54ea430 100644 --- a/pkg/cli/clitest/apptest_test.go +++ b/pkg/cli/clitest/apptest_test.go @@ -34,7 +34,7 @@ func TestFixture(t *testing.T) { f.TestTTY(t, "test", term.DotHere) f.App.Notify(ui.T("something")) - f.TestTTYNotes(t, "something") + f.TTY.TestMsg(t, ui.T("something")) f.App.CommitCode() if code, err := f.Wait(); code != "test" || err != nil { diff --git a/pkg/cli/clitest/fake_tty.go b/pkg/cli/clitest/fake_tty.go index f5512bab0..311309f27 100644 --- a/pkg/cli/clitest/fake_tty.go +++ b/pkg/cli/clitest/fake_tty.go @@ -10,6 +10,7 @@ import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" "src.elv.sh/pkg/testutil" + "src.elv.sh/pkg/ui" ) const ( @@ -31,9 +32,13 @@ type fakeTTY struct { // Mutex for synchronizing writing and closing eventCh. eventChMutex sync.Mutex // Channel for publishing updates of the main buffer and notes buffer. - bufCh, notesBufCh chan *term.Buffer + bufCh chan *term.Buffer + // Channel for publishing updates of the message text. + msgCh chan ui.Text // Records history of the main buffer and notes buffer. - bufs, notesBufs []*term.Buffer + bufs []*term.Buffer + // Records history of the message text. + msgs []ui.Text // Mutexes for guarding bufs and notesBufs. bufMutex sync.RWMutex // Channel that NotifySignals returns. Can be used to inject signals. @@ -59,11 +64,11 @@ const ( // size of the terminal is FakeTTYHeight and FakeTTYWidth. func NewFakeTTY() (cli.TTY, TTYCtrl) { tty := &fakeTTY{ - eventCh: make(chan term.Event, fakeTTYEvents), - sigCh: make(chan os.Signal, fakeTTYSignals), - bufCh: make(chan *term.Buffer, fakeTTYBufferUpdates), - notesBufCh: make(chan *term.Buffer, fakeTTYBufferUpdates), - height: FakeTTYHeight, width: FakeTTYWidth, + eventCh: make(chan term.Event, fakeTTYEvents), + sigCh: make(chan os.Signal, fakeTTYSignals), + bufCh: make(chan *term.Buffer, fakeTTYBufferUpdates), + msgCh: make(chan ui.Text, fakeTTYBufferUpdates), + height: FakeTTYHeight, width: FakeTTYWidth, } return tty, TTYCtrl{tty} } @@ -118,10 +123,10 @@ func (t *fakeTTY) ResetBuffer() { // UpdateBuffer records a new pair of buffers, i.e. sending them to their // respective channels and appending them to their respective slices. -func (t *fakeTTY) UpdateBuffer(bufNotes, buf *term.Buffer, _ bool) error { +func (t *fakeTTY) UpdateBuffer(msg ui.Text, buf *term.Buffer, _ bool) error { t.bufMutex.Lock() defer t.bufMutex.Unlock() - t.recordNotesBuf(bufNotes) + t.recordMsg(msg) t.recordBuf(buf) return nil } @@ -145,9 +150,9 @@ func (t *fakeTTY) recordBuf(buf *term.Buffer) { t.bufCh <- buf } -func (t *fakeTTY) recordNotesBuf(buf *term.Buffer) { - t.notesBufs = append(t.notesBufs, buf) - t.notesBufCh <- buf +func (t *fakeTTY) recordMsg(msg ui.Text) { + t.msgs = append(t.msgs, msg) + t.msgCh <- msg } // TTYCtrl is an interface for controlling a fake terminal. @@ -238,21 +243,21 @@ func (t TTYCtrl) TestBuffer(tt *testing.T, b *term.Buffer) { } } -// TestNotesBuffer verifies that a notes buffer will appear within 100ms, and -// aborts the test if it doesn't. -func (t TTYCtrl) TestNotesBuffer(tt *testing.T, b *term.Buffer) { +// TestNotesBuffer verifies that a message will appear within 100ms, and aborts +// the test if it doesn't. +func (t TTYCtrl) TestMsg(tt *testing.T, m ui.Text) { tt.Helper() - ok := testBuffer(b, t.notesBufCh) + ok := testBuffer(m, t.msgCh) if !ok { - tt.Logf("wanted notes buffer not shown:\n%s", b.TTYString()) + tt.Logf("wanted notes buffer not shown:\n%s", m.VTString()) t.bufMutex.RLock() defer t.bufMutex.RUnlock() - bufs := t.NotesBufferHistory() - tt.Logf("There has been %d notes buffers. None-nil ones are:", len(bufs)) + bufs := t.MsgHistory() + tt.Logf("There has been %d messages. Non-nil ones are:", len(bufs)) for i, buf := range bufs { if buf != nil { - tt.Logf("#%d:\n%s", i, buf.TTYString()) + tt.Logf("#%d:\n%s", i, buf.VTString()) } } tt.FailNow() @@ -277,23 +282,23 @@ func (t TTYCtrl) LastBuffer() *term.Buffer { } // NotesBufferHistory returns a slice of all notes buffers that have appeared. -func (t TTYCtrl) NotesBufferHistory() []*term.Buffer { +func (t TTYCtrl) MsgHistory() []ui.Text { t.bufMutex.RLock() defer t.bufMutex.RUnlock() - return t.notesBufs + return t.msgs } -func (t TTYCtrl) LastNotesBuffer() *term.Buffer { +func (t TTYCtrl) LastMsg() ui.Text { t.bufMutex.RLock() defer t.bufMutex.RUnlock() - if len(t.notesBufs) == 0 { + if len(t.msgs) == 0 { return nil } - return t.notesBufs[len(t.notesBufs)-1] + return t.msgs[len(t.msgs)-1] } -// Tests that an buffer appears on the channel within 100ms. -func testBuffer(want *term.Buffer, ch <-chan *term.Buffer) bool { +// Tests that a value appears on the channel within 100ms (scaled). +func testBuffer[T any](want T, ch <-chan T) bool { timeout := time.After(testutil.Scaled(100 * time.Millisecond)) for { select { diff --git a/pkg/cli/clitest/fake_tty_test.go b/pkg/cli/clitest/fake_tty_test.go index ae3a576a1..c43f9d461 100644 --- a/pkg/cli/clitest/fake_tty_test.go +++ b/pkg/cli/clitest/fake_tty_test.go @@ -7,6 +7,7 @@ import ( "src.elv.sh/pkg/cli" "src.elv.sh/pkg/cli/term" + "src.elv.sh/pkg/ui" ) func TestFakeTTY_Setup(t *testing.T) { @@ -67,54 +68,54 @@ func TestFakeTTY_Signals(t *testing.T) { } func TestFakeTTY_Buffer(t *testing.T) { - bufNotes1 := term.NewBufferBuilder(10).Write("notes 1").Buffer() + msg1 := ui.T("msg 1") buf1 := term.NewBufferBuilder(10).Write("buf 1").Buffer() - bufNotes2 := term.NewBufferBuilder(10).Write("notes 2").Buffer() + msg2 := ui.T("msg 2") buf2 := term.NewBufferBuilder(10).Write("buf 2").Buffer() - bufNotes3 := term.NewBufferBuilder(10).Write("notes 3").Buffer() + msg3 := ui.T("msg 3") buf3 := term.NewBufferBuilder(10).Write("buf 3").Buffer() tty, ttyCtrl := NewFakeTTY() - if ttyCtrl.LastNotesBuffer() != nil { - t.Errorf("LastNotesBuffer -> %v, want nil", ttyCtrl.LastNotesBuffer()) + if ttyCtrl.LastMsg() != nil { + t.Errorf("LastNotesBuffer -> %v, want nil", ttyCtrl.LastMsg()) } if ttyCtrl.LastBuffer() != nil { t.Errorf("LastBuffer -> %v, want nil", ttyCtrl.LastBuffer()) } - tty.UpdateBuffer(bufNotes1, buf1, true) - if ttyCtrl.LastNotesBuffer() != bufNotes1 { - t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastNotesBuffer(), bufNotes1) + tty.UpdateBuffer(msg1, buf1, true) + if !reflect.DeepEqual(ttyCtrl.LastMsg(), msg1) { + t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastMsg(), msg1) } if ttyCtrl.LastBuffer() != buf1 { t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastBuffer(), buf1) } ttyCtrl.TestBuffer(t, buf1) - ttyCtrl.TestNotesBuffer(t, bufNotes1) + ttyCtrl.TestMsg(t, msg1) - tty.UpdateBuffer(bufNotes2, buf2, true) - if ttyCtrl.LastNotesBuffer() != bufNotes2 { - t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastNotesBuffer(), bufNotes2) + tty.UpdateBuffer(msg2, buf2, true) + if !reflect.DeepEqual(ttyCtrl.LastMsg(), msg2) { + t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastMsg(), msg2) } if ttyCtrl.LastBuffer() != buf2 { t.Errorf("LastBuffer -> %v, want %v", ttyCtrl.LastBuffer(), buf2) } ttyCtrl.TestBuffer(t, buf2) - ttyCtrl.TestNotesBuffer(t, bufNotes2) + ttyCtrl.TestMsg(t, msg2) // Test Test{,Notes}Buffer - tty.UpdateBuffer(bufNotes3, buf3, true) + tty.UpdateBuffer(msg3, buf3, true) ttyCtrl.TestBuffer(t, buf3) - ttyCtrl.TestNotesBuffer(t, bufNotes3) + ttyCtrl.TestMsg(t, msg3) // Cannot test the failure branch as that will fail the test wantBufs := []*term.Buffer{buf1, buf2, buf3} - wantNotesBufs := []*term.Buffer{bufNotes1, bufNotes2, bufNotes3} + wantMsgs := []ui.Text{msg1, msg2, msg3} if !reflect.DeepEqual(ttyCtrl.BufferHistory(), wantBufs) { t.Errorf("BufferHistory did not return {buf1, buf2}") } - if !reflect.DeepEqual(ttyCtrl.NotesBufferHistory(), wantNotesBufs) { + if !reflect.DeepEqual(ttyCtrl.MsgHistory(), wantMsgs) { t.Errorf("NotesBufferHistory did not return {bufNotes1, bufNotes2}") } } diff --git a/pkg/cli/modes/histwalk_test.go b/pkg/cli/modes/histwalk_test.go index 52612f9ce..26ef011ff 100644 --- a/pkg/cli/modes/histwalk_test.go +++ b/pkg/cli/modes/histwalk_test.go @@ -80,9 +80,7 @@ func TestHistWalk_NoWalker(t *testing.T) { defer f.Stop() startHistwalk(f.App, HistwalkSpec{}) - f.TestTTYNotes(t, - "error: no history store", Styles, - "!!!!!!") + f.TTY.TestMsg(t, ui.Concat(ui.T("error:", ui.FgRed), ui.T(" no history store"))) } func TestHistWalk_NoMatch(t *testing.T) { @@ -99,9 +97,7 @@ func TestHistWalk_NoMatch(t *testing.T) { cfg := HistwalkSpec{Store: store, Prefix: "ls"} startHistwalk(f.App, cfg) // Test that an error message has been written to the notes buffer. - f.TestTTYNotes(t, - "error: end of history", Styles, - "!!!!!!") + f.TTY.TestMsg(t, ui.Concat(ui.T("error:", ui.FgRed), ui.T(" end of history"))) // Test that buffer has not changed - histwalk addon is not active. f.TTY.TestBuffer(t, buf0) } diff --git a/pkg/cli/modes/location_test.go b/pkg/cli/modes/location_test.go index 0e68643cd..b85684c2b 100644 --- a/pkg/cli/modes/location_test.go +++ b/pkg/cli/modes/location_test.go @@ -105,9 +105,8 @@ func TestLocation_FullWorkflow(t *testing.T) { // There should be no change to codearea after accepting. f.TestTTY(t /* nothing */) // Error from Chdir should be sent to notes. - f.TestTTYNotes(t, - "error: mock chdir error", Styles, - "!!!!!!") + f.TTY.TestMsg(t, ui.Concat( + ui.T("error:", ui.FgRed), ui.T(" mock chdir error"))) // Chdir should be called. wantChdir := fixPath("/tmp/foo/bar/lorem/ipsum") select { diff --git a/pkg/cli/modes/navigation_test.go b/pkg/cli/modes/navigation_test.go index 57a4b3c68..b514e16f8 100644 --- a/pkg/cli/modes/navigation_test.go +++ b/pkg/cli/modes/navigation_test.go @@ -38,9 +38,8 @@ func TestErrorInAscend(t *testing.T) { startNavigation(f.App, NavigationSpec{Cursor: c}) f.TTY.Inject(term.K(ui.Left)) - f.TestTTYNotes(t, - "error: cannot ascend", Styles, - "!!!!!!") + f.TTY.TestMsg(t, ui.Concat( + ui.T("error:", ui.FgRed), ui.T(" cannot ascend"))) } func TestErrorInDescend(t *testing.T) { @@ -53,9 +52,8 @@ func TestErrorInDescend(t *testing.T) { f.TTY.Inject(term.K(ui.Down)) f.TTY.Inject(term.K(ui.Right)) - f.TestTTYNotes(t, - "error: cannot descend", Styles, - "!!!!!!") + f.TTY.TestMsg(t, ui.Concat( + ui.T("error:", ui.FgRed), ui.T(" cannot descend"))) } func TestErrorInCurrent(t *testing.T) { diff --git a/pkg/cli/term/writer.go b/pkg/cli/term/writer.go index 58158d387..3b3bbf4b5 100644 --- a/pkg/cli/term/writer.go +++ b/pkg/cli/term/writer.go @@ -4,6 +4,8 @@ import ( "bytes" "fmt" "io" + + "src.elv.sh/pkg/ui" ) var logWriterDetail = false @@ -15,7 +17,7 @@ type Writer interface { // ResetBuffer resets the current buffer. ResetBuffer() // UpdateBuffer updates the terminal display to reflect current buffer. - UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error + UpdateBuffer(msg ui.Text, buf *Buffer, fullRefresh bool) error // ClearScreen clears the terminal screen and places the cursor at the top // left corner. ClearScreen() @@ -69,33 +71,44 @@ const ( ) // UpdateBuffer updates the terminal display to reflect current buffer. -func (w *writer) UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error { - if buf.Width != w.curBuf.Width && w.curBuf.Lines != nil { - // Width change, force full refresh - w.curBuf.Lines = nil +func (w *writer) UpdateBuffer(msg ui.Text, buf *Buffer, fullRefresh bool) error { + if (buf.Width != w.curBuf.Width && w.curBuf.Lines != nil) || msg != nil { + // If the width has changed or we have any message to write, we can't do + // delta rendering meaningfully, so force a full refresh. fullRefresh = true } - bytesBuf := new(bytes.Buffer) + // Store all the output write in a buffer, so that we only write to the + // terminal once. + output := new(bytes.Buffer) - bytesBuf.WriteString(hideCursor) + // Hide cursor at the beginning to minimize flickering. + output.WriteString(hideCursor) - // Rewind cursor + // Rewind cursor. if pLine := w.curBuf.Dot.Line; pLine > 0 { - fmt.Fprintf(bytesBuf, "\033[%dA", pLine) + fmt.Fprintf(output, "\033[%dA", pLine) } - bytesBuf.WriteString("\r") + output.WriteString("\r") if fullRefresh { - // Erase from here. We may be in the top right corner of the screen; if - // we simply do an erase here, tmux will save the current screen in the - // scrollback buffer (presumably as a heuristics to detect full-screen - // applications), but that is not something we want. So we write a space - // first, and then erase, before rewinding back. + // Erase from here. We may be in the top left corner of the screen; if + // we simply do an erase here, tmux (and possibly other terminal + // emulators) will save the current screen in the scrollback buffer, + // presumably as a heuristic to detect full-screen applications, but + // that is not something we want. + // + // To defeat tmux's heuristic, we write a space, erase, and then rewind. // // Source code for tmux behavior: // https://github.com/tmux/tmux/blob/5f5f029e3b3a782dc616778739b2801b00b17c0e/screen-write.c#L1139 - bytesBuf.WriteString(" \033[J\r") + output.WriteString(" \033[J\r") + } + + if msg != nil { + // Write the message with the terminal's line wrapping enabled, for + // easier copy-pasting by the user. + output.WriteString("\033[?7h" + msg.VTString() + "\n\033[?7l") } // style of last written cell. @@ -103,7 +116,7 @@ func (w *writer) UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error { switchStyle := func(newstyle string) { if newstyle != style { - fmt.Fprintf(bytesBuf, "\033[0;%sm", newstyle) + fmt.Fprintf(output, "\033[0;%sm", newstyle) style = newstyle } } @@ -111,24 +124,7 @@ func (w *writer) UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error { writeCells := func(cs []Cell) { for _, c := range cs { switchStyle(c.Style) - bytesBuf.WriteString(c.Text) - } - } - - if bufNoti != nil { - if logWriterDetail { - logger.Printf("going to write %d lines of notifications", len(bufNoti.Lines)) - } - - // Write notifications - for _, line := range bufNoti.Lines { - writeCells(line) - switchStyle("") - bytesBuf.WriteString("\033[K\n") - } - // TODO(xiaq): This is hacky; try to improve it. - if len(w.curBuf.Lines) > 0 { - w.curBuf.Lines = w.curBuf.Lines[1:] + output.WriteString(c.Text) } } @@ -138,48 +134,59 @@ func (w *writer) UpdateBuffer(bufNoti, buf *Buffer, fullRefresh bool) error { for i, line := range buf.Lines { if i > 0 { - bytesBuf.WriteString("\n") + // Move cursor down one line and to the leftmost column. Shorter + // than "\033[B\r". + output.WriteString("\n") } - var j int // First column where buf and oldBuf differ - // No need to update current line - if !fullRefresh && i < len(w.curBuf.Lines) { - var eq bool - if eq, j = compareCells(line, w.curBuf.Lines[i]); eq { - continue - } + if fullRefresh || i >= len(w.curBuf.Lines) { + // When doing a full refresh or writing new lines, we have an empty + // canvas to work with, so just write the current line. + writeCells(line) + continue + } + // Delta update below. + eq, j := compareCells(line, w.curBuf.Lines[i]) + if eq { + // This line hasn't changed + continue } - // Move to the first differing column if necessary. - firstCol := cellsWidth(line[:j]) - if firstCol != 0 { - fmt.Fprintf(bytesBuf, "\033[%dC", firstCol) + // This line has changed, and j is the first differing cell. Move to its + // corresponding column. + if firstCol := cellsWidth(line[:j]); firstCol != 0 { + fmt.Fprintf(output, "\033[%dC", firstCol) } - // Erase the rest of the line if necessary. - if !fullRefresh && i < len(w.curBuf.Lines) && j < len(w.curBuf.Lines[i]) { + // Erase the rest of the line; this is not necessary if the old version + // of the line is a prefix of the current version of the line. + if j < len(w.curBuf.Lines[i]) { switchStyle("") - bytesBuf.WriteString("\033[K") + output.WriteString("\033[K") } + // Now write the new content. writeCells(line[j:]) } - if len(w.curBuf.Lines) > len(buf.Lines) && !fullRefresh { + if !fullRefresh && len(w.curBuf.Lines) > len(buf.Lines) { // If the old buffer is higher, erase old content. + // // Note that we cannot simply write \033[J, because if the cursor is // just over the last column -- which is precisely the case if we have a - // rprompt, \033[J will also erase the last column. + // rprompt, \033[J will also erase the last column. Since the old buffer + // is higher, we know that the \n we write won't create a bogus new + // line. switchStyle("") - bytesBuf.WriteString("\n\033[J\033[A") + output.WriteString("\n\033[J\033[A") } switchStyle("") cursor := endPos(buf) - bytesBuf.Write(deltaPos(cursor, buf.Dot)) + output.Write(deltaPos(cursor, buf.Dot)) // Show cursor. - bytesBuf.WriteString(showCursor) + output.WriteString(showCursor) if logWriterDetail { - logger.Printf("going to write %q", bytesBuf.String()) + logger.Printf("going to write %q", output.String()) } - _, err := w.file.Write(bytesBuf.Bytes()) + _, err := w.file.Write(output.Bytes()) if err != nil { return err } diff --git a/pkg/cli/term/writer_test.go b/pkg/cli/term/writer_test.go index bf78f2570..baa7c624c 100644 --- a/pkg/cli/term/writer_test.go +++ b/pkg/cli/term/writer_test.go @@ -3,6 +3,8 @@ package term import ( "strings" "testing" + + "src.elv.sh/pkg/ui" ) func TestWriter(t *testing.T) { @@ -17,8 +19,8 @@ func TestWriter(t *testing.T) { w := NewWriter(sb) w.UpdateBuffer( - NewBufferBuilder(10).Write("note 1").Buffer(), + ui.T("note 1"), NewBufferBuilder(10).Write("line 1").SetDotHere().Buffer(), false) - testOutput(hideCursor + "\rnote 1\033[K\n" + "line 1\r\033[6C" + showCursor) + testOutput(hideCursor + "\r \033[J\r\033[?7h\033[mnote 1\n\033[?7lline 1\r\033[6C" + showCursor) } diff --git a/pkg/edit/builtins_test.go b/pkg/edit/builtins_test.go index bfef38c44..c384d3e81 100644 --- a/pkg/edit/builtins_test.go +++ b/pkg/edit/builtins_test.go @@ -67,7 +67,7 @@ func TestEndOfHistory(t *testing.T) { f := setup(t) evals(f.Evaler, `edit:end-of-history`) - f.TestTTYNotes(t, "End of history") + f.TTYCtrl.TestMsg(t, ui.T("End of history")) } func TestKey(t *testing.T) { @@ -111,12 +111,10 @@ func TestClear(t *testing.T) { func TestNotify(t *testing.T) { f := setup(t) evals(f.Evaler, "edit:notify string") - f.TestTTYNotes(t, "string") + f.TTYCtrl.TestMsg(t, ui.T("string")) evals(f.Evaler, "edit:notify (styled styled red)") - f.TestTTYNotes(t, - "styled", Styles, - "!!!!!!") + f.TTYCtrl.TestMsg(t, ui.T("styled", ui.FgRed)) evals(f.Evaler, "var err = ?(edit:notify [])") if _, hasErr := getGlobal(f.Evaler, "err").(error); !hasErr { @@ -311,9 +309,8 @@ func TestBuiltins_FocusedWidgetNotCodeArea(t *testing.T) { f.Editor.app.PushAddon(tk.Label{}) evals(f.Evaler, code) - f.TestTTYNotes(t, - "error: "+modes.ErrFocusedWidgetNotCodeArea.Error(), Styles, - "!!!!!!") + f.TTYCtrl.TestMsg(t, + ui.Concat(ui.T("error:", ui.FgRed), ui.T(" "+modes.ErrFocusedWidgetNotCodeArea.Error()))) }) } } diff --git a/pkg/edit/editor_test.go b/pkg/edit/editor_test.go index ecd3ddb85..6d65e0140 100644 --- a/pkg/edit/editor_test.go +++ b/pkg/edit/editor_test.go @@ -29,7 +29,7 @@ func TestEditor_DoesNotAddEmptyCommandToHistory(t *testing.T) { func TestEditor_Notify(t *testing.T) { f := setup(t) f.Editor.Notify(ui.T("note")) - f.TestTTYNotes(t, "note") + f.TTYCtrl.TestMsg(t, ui.T("note")) } func testCommands(t *testing.T, store storedefs.Store, wantCmds ...storedefs.Cmd) { diff --git a/pkg/edit/histwalk_test.go b/pkg/edit/histwalk_test.go index 5aa221503..2f0fb2d25 100644 --- a/pkg/edit/histwalk_test.go +++ b/pkg/edit/histwalk_test.go @@ -12,9 +12,8 @@ func TestHistWalk_Up_EndOfHistory(t *testing.T) { f := startHistwalkTest(t) f.TTYCtrl.Inject(term.K(ui.Up)) - f.TestTTYNotes(t, - "error: end of history", Styles, - "!!!!!!") + f.TTYCtrl.TestMsg(t, + ui.Concat(ui.T("error:", ui.FgRed), ui.T(" end of history"))) } func TestHistWalk_Down_EndOfHistory(t *testing.T) { @@ -22,9 +21,8 @@ func TestHistWalk_Down_EndOfHistory(t *testing.T) { // Not bound by default, so we need to use evals. evals(f.Evaler, `edit:history:down`) - f.TestTTYNotes(t, - "error: end of history", Styles, - "!!!!!!") + f.TTYCtrl.TestMsg(t, + ui.Concat(ui.T("error:", ui.FgRed), ui.T(" end of history"))) } func TestHistWalk_Accept(t *testing.T) { diff --git a/pkg/edit/prompt_test.go b/pkg/edit/prompt_test.go index 4ba2029bc..1adf33e51 100644 --- a/pkg/edit/prompt_test.go +++ b/pkg/edit/prompt_test.go @@ -38,15 +38,15 @@ func TestPrompt_NotifiesInvalidValueOutput(t *testing.T) { f := setup(t, rc(`set edit:prompt = { put good [bad] good2 }`)) f.TestTTY(t, "goodgood2", term.DotHere) - f.TestTTYNotes(t, "invalid output type from prompt: list") + f.TTYCtrl.TestMsg(t, ui.T("invalid output type from prompt: list")) } func TestPrompt_NotifiesException(t *testing.T) { f := setup(t, rc(`set edit:prompt = { fail ERROR }`)) - f.TestTTYNotes(t, - "[prompt error] ERROR\n", - `see stack trace with "show $edit:exceptions[0]"`) + f.TTYCtrl.TestMsg(t, ui.T( + "[prompt error] ERROR\n"+ + `see stack trace with "show $edit:exceptions[0]"`)) evals(f.Evaler, `var excs = (count $edit:exceptions)`) testGlobal(t, f.Evaler, "excs", 1) } @@ -105,9 +105,9 @@ func TestPromptStaleTransform_Exception(t *testing.T) { `set edit:prompt-stale-threshold = `+scaledMsAsSec(50), `set edit:prompt-stale-transform = {|_| fail ERROR }`)) - f.TestTTYNotes(t, - "[prompt stale transform error] ERROR\n", - `see stack trace with "show $edit:exceptions[0]"`) + f.TTYCtrl.TestMsg(t, ui.T( + "[prompt stale transform error] ERROR\n"+ + `see stack trace with "show $edit:exceptions[0]"`)) evals(f.Evaler, `var excs = (count $edit:exceptions)`) testGlobal(t, f.Evaler, "excs", 1) } diff --git a/pkg/edit/testutils_test.go b/pkg/edit/testutils_test.go index aa96951ae..0afd75930 100644 --- a/pkg/edit/testutils_test.go +++ b/pkg/edit/testutils_test.go @@ -98,11 +98,6 @@ func (f *fixture) TestTTY(t *testing.T, args ...any) { f.TTYCtrl.TestBuffer(t, f.MakeBuffer(args...)) } -func (f *fixture) TestTTYNotes(t *testing.T, args ...any) { - t.Helper() - f.TTYCtrl.TestNotesBuffer(t, f.MakeBuffer(args...)) -} - func (f *fixture) SetCodeBuffer(b tk.CodeBuffer) { codeArea(f.Editor.app).MutateState(func(s *tk.CodeAreaState) { s.Buffer = b