term: allow multi-line bracketed paste to not create single line with verbatim LFs
Treat "\n" (LF) like "Enter" (CR)
Avoids that when pasting 3 lines
(with a terminal like kitty, ghostty, alacritty that do not change the clipboard
in bracketed paste mode)
it turns into 1 prompt looking like:
Test> line one
..............line.two
......................line.three
Fixes golang/go#74600
Change-Id: I4a86044a4a175eccb3a96dbf7021fee97a5940ce
GitHub-Last-Rev: 0cf26df9aec994dfc61392e98b9034fe7133fb7f
GitHub-Pull-Request: golang/term#21
Reviewed-on: https://go-review.googlesource.com/c/term/+/687755
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/terminal.go b/terminal.go
index 13e9a64..bddb2e2 100644
--- a/terminal.go
+++ b/terminal.go
@@ -146,6 +146,7 @@
keyCtrlD = 4
keyCtrlU = 21
keyEnter = '\r'
+ keyLF = '\n'
keyEscape = 27
keyBackspace = 127
keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota
@@ -497,7 +498,7 @@
// 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 @@
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 @@
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 @@
import (
"bytes"
+ "errors"
+ "fmt"
"io"
"os"
"runtime"
@@ -209,12 +211,24 @@
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",
err: io.EOF,
@@ -296,6 +310,36 @@
}
}
+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"),