diff --git a/handler.go b/handler.go index 699e815..bd1e2f3 100644 --- a/handler.go +++ b/handler.go @@ -391,6 +391,8 @@ func (v *Terminal) SetMode(mode ansicode.TerminalMode) { switch mode { case ansicode.TerminalModeCursorKeys: forward = true + case ansicode.TerminalModeInsert: + v.insertMode = true case ansicode.TerminalModeLineWrap: case ansicode.TerminalModeBlinkingCursor: epoch := time.Now() @@ -627,6 +629,8 @@ func (v *Terminal) UnsetMode(mode ansicode.TerminalMode) { switch mode { case ansicode.TerminalModeCursorKeys: forward = true + case ansicode.TerminalModeInsert: + v.insertMode = false case ansicode.TerminalModeLineWrap: case ansicode.TerminalModeBlinkingCursor: v.CursorBlinkEpoch = nil diff --git a/terminal.go b/terminal.go index 5e55bdf..9155ea0 100644 --- a/terminal.go +++ b/terminal.go @@ -52,6 +52,10 @@ type Terminal struct { // to the next line if another character is printed. wrap bool + // insertMode indicates whether printable input + // should shift row contents right. + insertMode bool + *ansicode.Decoder // onResize is a hook called every time the terminal resizes. @@ -147,6 +151,7 @@ func (v *Terminal) Reset() { v.mut.Lock() defer v.mut.Unlock() v.reset() + v.insertMode = false } func (v *Terminal) UsedHeight() int { @@ -247,6 +252,9 @@ func (v *Terminal) put(r rune) { v.wrap = false } x, y, f := v.Cursor.X, v.Cursor.Y, v.Cursor.F + if v.insertMode { + v.insertCharacters(1) + } v.paint(y, x, f, r) if y > v.MaxY { v.MaxY = y diff --git a/terminal_test.go b/terminal_test.go index aa1fe91..9a7d978 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -202,8 +202,70 @@ func TestResizeGrowingHeightThenShrinkWidth(t *testing.T) { require.NoError(t, err) } -func eachFrame(r io.Reader, callback func(frame int, segment []byte)) { - eachNthFrame(r, 1, callback) +func TestInsertModePreservesShiftedContentAcrossLines(t *testing.T) { + term := midterm.NewTerminal(24, 80) + term.Raw = true + + // Seed the two-line prompt before the redraw: + // + // Please answer: first + // (▼ for other options) + _, err := io.WriteString(term, "\r\x1b[JPlease answer: first \r\n(▼ for other options)") + require.NoError(t, err) + + // Replace "first" with "second", + // and insert "▲" and "▼" on the next line + // so the existing space shifts: + // + // Please answer: second + // (▲▼ for other options) + _, err = io.WriteString(term, "\x1b[A\x1b[6Dsecond\x1b[4h \x1b[4l\r\n\x1b[C▲\x1b[4h▼\x1b[4l") + require.NoError(t, err) + + require.Equal(t, "Please answer: second", strings.TrimRight(string(term.Content[0]), " ")) + require.Equal(t, "(▲▼ for other options)", strings.TrimRight(string(term.Content[1]), " ")) +} + +func TestInsertModeShiftsSingleLineContent(t *testing.T) { + term := midterm.NewTerminal(1, 8) + term.Raw = true + + // Start with the cursor after the final "e": + // + // abcde^ + _, err := io.WriteString(term, "abcde") + require.NoError(t, err) + + // Return to column 3, enable insert mode, and insert a space: + // + // abc^de + // abc ^de + _, err = io.WriteString(term, "\r\x1b[3C\x1b[4h \x1b[4l") + require.NoError(t, err) + + require.Equal(t, "abc de", strings.TrimRight(string(term.Content[0]), " ")) +} + +func TestUnsetInsertModeRestoresReplaceMode(t *testing.T) { + term := midterm.NewTerminal(1, 8) + term.Raw = true + + // Start with the cursor after the final "e": + // + // abcde^ + _, err := io.WriteString(term, "abcde") + require.NoError(t, err) + + // Insert a space at column 3, disable insert mode, then overwrite the "d" + // with "Z" at the next cursor position: + // + // abc^de + // abc ^de + // abc Z^e + _, err = io.WriteString(term, "\r\x1b[3C\x1b[4h \x1b[4lZ") + require.NoError(t, err) + + require.Equal(t, "abc Ze", strings.TrimRight(string(term.Content[0]), " ")) } func eachNthFrame(r io.Reader, n int, callback func(frame int, segment []byte)) {