Skip to content

Commit 4f53e0c

Browse files
ldemaillyprattmic
authored andcommitted
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: 0cf26df GitHub-Pull-Request: #21 Reviewed-on: https://go-review.googlesource.com/c/term/+/687755 Reviewed-by: Michael Pratt <[email protected]> Reviewed-by: Michael Knyszek <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 27f29d8 commit 4f53e0c

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

terminal.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ const (
146146
keyCtrlD = 4
147147
keyCtrlU = 21
148148
keyEnter = '\r'
149+
keyLF = '\n'
149150
keyEscape = 27
150151
keyBackspace = 127
151152
keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota
@@ -497,7 +498,7 @@ func (t *Terminal) historyAdd(entry string) {
497498
// handleKey processes the given key and, optionally, returns a line of text
498499
// that the user has entered.
499500
func (t *Terminal) handleKey(key rune) (line string, ok bool) {
500-
if t.pasteActive && key != keyEnter {
501+
if t.pasteActive && key != keyEnter && key != keyLF {
501502
t.addKeyToLine(key)
502503
return
503504
}
@@ -567,7 +568,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
567568
t.setLine(runes, len(runes))
568569
}
569570
}
570-
case keyEnter:
571+
case keyEnter, keyLF:
571572
t.moveCursorToPos(len(t.line))
572573
t.queue([]rune("\r\n"))
573574
line = string(t.line)
@@ -812,6 +813,10 @@ func (t *Terminal) readLine() (line string, err error) {
812813
if !t.pasteActive {
813814
lineIsPasted = false
814815
}
816+
// If we have CR, consume LF if present (CRLF sequence) to avoid returning an extra empty line.
817+
if key == keyEnter && len(rest) > 0 && rest[0] == keyLF {
818+
rest = rest[1:]
819+
}
815820
line, lineOk = t.handleKey(key)
816821
}
817822
if len(rest) > 0 {

terminal_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package term
66

77
import (
88
"bytes"
9+
"errors"
10+
"fmt"
911
"io"
1012
"os"
1113
"runtime"
@@ -208,12 +210,24 @@ var keyPressTests = []struct {
208210
line: "efgh",
209211
throwAwayLines: 1,
210212
},
213+
{
214+
// Newline in bracketed paste mode should still work.
215+
in: "abc\x1b[200~d\nefg\x1b[201~h\r",
216+
line: "efgh",
217+
throwAwayLines: 1,
218+
},
211219
{
212220
// Lines consisting entirely of pasted data should be indicated as such.
213221
in: "\x1b[200~a\r",
214222
line: "a",
215223
err: ErrPasteIndicator,
216224
},
225+
{
226+
// Lines consisting entirely of pasted data should be indicated as such (\n paste).
227+
in: "\x1b[200~a\n",
228+
line: "a",
229+
err: ErrPasteIndicator,
230+
},
217231
{
218232
// Ctrl-C terminates readline
219233
in: "\003",
@@ -296,6 +310,36 @@ func TestRender(t *testing.T) {
296310
}
297311
}
298312

313+
func TestCRLF(t *testing.T) {
314+
c := &MockTerminal{
315+
toSend: []byte("line1\rline2\r\nline3\n"),
316+
// bytesPerRead 0 in this test means read all at once
317+
// CR+LF need to be in same read for ReadLine to not produce an extra empty line
318+
// which is what terminals do for reasonably small paste. if way many lines are pasted
319+
// and going over say 1k-16k buffer, readline current implementation will possibly generate 1
320+
// extra empty line, if the CR is in chunk1 and LF in chunk2 (and that's fine).
321+
}
322+
323+
ss := NewTerminal(c, "> ")
324+
for i := range 3 {
325+
line, err := ss.ReadLine()
326+
if err != nil {
327+
t.Fatalf("failed to read line %d: %v", i+1, err)
328+
}
329+
expected := fmt.Sprintf("line%d", i+1)
330+
if line != expected {
331+
t.Fatalf("expected '%s', got '%s'", expected, line)
332+
}
333+
}
334+
line, err := ss.ReadLine()
335+
if !errors.Is(err, io.EOF) {
336+
t.Fatalf("expected EOF after 3 lines, got '%s' with error %v", line, err)
337+
}
338+
if line != "" {
339+
t.Fatalf("expected empty line after EOF, got '%s'", line)
340+
}
341+
}
342+
299343
func TestPasswordNotSaved(t *testing.T) {
300344
c := &MockTerminal{
301345
toSend: []byte("password\r\x1b[A\r"),

0 commit comments

Comments
 (0)