diff --git a/terminal.go b/terminal.go index 13e9a64..bddb2e2 100644 --- a/terminal.go +++ b/terminal.go @@ -146,6 +146,7 @@ const ( keyCtrlD = 4 keyCtrlU = 21 keyEnter = '\r' + keyLF = '\n' keyEscape = 27 keyBackspace = 127 keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota @@ -497,7 +498,7 @@ func (t *Terminal) historyAdd(entry string) { // handleKey processes the given key and, optionally, returns a line of text // that the user has entered. func (t *Terminal) handleKey(key rune) (line string, ok bool) { - if t.pasteActive && key != keyEnter { + if t.pasteActive && key != keyEnter && key != keyLF { t.addKeyToLine(key) return } @@ -567,7 +568,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { t.setLine(runes, len(runes)) } } - case keyEnter: + case keyEnter, keyLF: t.moveCursorToPos(len(t.line)) t.queue([]rune("\r\n")) line = string(t.line) @@ -812,6 +813,10 @@ func (t *Terminal) readLine() (line string, err error) { if !t.pasteActive { lineIsPasted = false } + // If we have CR, consume LF if present (CRLF sequence) to avoid returning an extra empty line. + if key == keyEnter && len(rest) > 0 && rest[0] == keyLF { + rest = rest[1:] + } line, lineOk = t.handleKey(key) } if len(rest) > 0 { diff --git a/terminal_test.go b/terminal_test.go index 29dd874..5d35cc5 100644 --- a/terminal_test.go +++ b/terminal_test.go @@ -6,6 +6,8 @@ package term import ( "bytes" + "errors" + "fmt" "io" "os" "runtime" @@ -208,12 +210,24 @@ var keyPressTests = []struct { line: "efgh", throwAwayLines: 1, }, + { + // Newline in bracketed paste mode should still work. + in: "abc\x1b[200~d\nefg\x1b[201~h\r", + line: "efgh", + throwAwayLines: 1, + }, { // Lines consisting entirely of pasted data should be indicated as such. in: "\x1b[200~a\r", line: "a", err: ErrPasteIndicator, }, + { + // Lines consisting entirely of pasted data should be indicated as such (\n paste). + in: "\x1b[200~a\n", + line: "a", + err: ErrPasteIndicator, + }, { // Ctrl-C terminates readline in: "\003", @@ -296,6 +310,36 @@ func TestRender(t *testing.T) { } } +func TestCRLF(t *testing.T) { + c := &MockTerminal{ + toSend: []byte("line1\rline2\r\nline3\n"), + // bytesPerRead 0 in this test means read all at once + // CR+LF need to be in same read for ReadLine to not produce an extra empty line + // which is what terminals do for reasonably small paste. if way many lines are pasted + // and going over say 1k-16k buffer, readline current implementation will possibly generate 1 + // extra empty line, if the CR is in chunk1 and LF in chunk2 (and that's fine). + } + + ss := NewTerminal(c, "> ") + for i := range 3 { + line, err := ss.ReadLine() + if err != nil { + t.Fatalf("failed to read line %d: %v", i+1, err) + } + expected := fmt.Sprintf("line%d", i+1) + if line != expected { + t.Fatalf("expected '%s', got '%s'", expected, line) + } + } + line, err := ss.ReadLine() + if !errors.Is(err, io.EOF) { + t.Fatalf("expected EOF after 3 lines, got '%s' with error %v", line, err) + } + if line != "" { + t.Fatalf("expected empty line after EOF, got '%s'", line) + } +} + func TestPasswordNotSaved(t *testing.T) { c := &MockTerminal{ toSend: []byte("password\r\x1b[A\r"),