From 1eacdc1c296669a8bd2261743232719340c63bbb Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Sun, 14 Aug 2022 20:18:20 +0200 Subject: [PATCH] Support bracketed paste This introduces support for input via bracketed paste, where escape characters in the pasted input are not interpreted. Pasted input are marked as a special field in the KeyMsg. This is because pasted input may need sanitation in individual widgets. --- key.go | 129 +++++++++++++++++++++++++++++++++++++++---- key_test.go | 79 ++++++++++++++++++++++++-- nil_renderer.go | 3 + nil_renderer_test.go | 5 ++ options.go | 7 +++ options_test.go | 8 ++- renderer.go | 7 +++ screen.go | 28 ++++++++++ screen_test.go | 16 +++--- standard_renderer.go | 34 ++++++++++++ tea.go | 20 ++++++- tty.go | 6 +- 12 files changed, 313 insertions(+), 29 deletions(-) diff --git a/key.go b/key.go index c2e5e3ab01..ba9301a780 100644 --- a/key.go +++ b/key.go @@ -1,11 +1,14 @@ package tea import ( + "bytes" "errors" "io" + "unicode" "unicode/utf8" "github.com/mattn/go-localereader" + "github.com/muesli/termenv" ) // KeyMsg contains information about a keypress. KeyMsgs are always sent to @@ -54,6 +57,7 @@ type Key struct { Type KeyType Runes []rune Alt bool + Paste bool } // String returns a friendly string representation for a key. It's safe (and @@ -67,7 +71,13 @@ func (k Key) String() (str string) { str += "alt+" } if k.Type == KeyRunes { + if k.Paste { + str += "[" + } str += string(k.Runes) + if k.Paste { + str += "]" + } return str } else if s, ok := keyNames[k.Type]; ok { str += s @@ -563,16 +573,31 @@ var sequences = map[string]Key{ "\x1bOD": {Type: KeyLeft, Alt: false}, } +const ( + // Bracketed paste sequences. + bpStartSeq = termenv.CSI + termenv.StartBracketedPasteSeq + bpEndSeq = termenv.CSI + termenv.EndBracketedPasteSeq +) + // readInputs reads keypress and mouse inputs from a TTY and returns messages // containing information about the key or mouse events accordingly. -func readInputs(input io.Reader) ([]Msg, error) { - var buf [256]byte +func readInputs(input io.Reader, bpEnabled bool) ([]Msg, error) { + var inputBuf [256]byte + buf := inputBuf[:] // Read and block numBytes, err := input.Read(buf[:]) if err != nil { return nil, err } + if numBytes == len(buf) { + // This can happen when a large amount of text is suddenly pasted. + buf, numBytes, err = readMore(buf, input) + if err != nil { + return nil, err + } + } + b := buf[:numBytes] b, err = localereader.UTF8(b) if err != nil { @@ -590,37 +615,81 @@ func readInputs(input io.Reader) ([]Msg, error) { return m, nil } - var runeSets [][]rune + type runeSet struct { + bracketedPaste bool + runes []rune + } + var runeSets []runeSet var runes []rune // Translate input into runes. In most cases we'll receive exactly one // rune, but there are cases, particularly when an input method editor is // used, where we can receive multiple runes at once. + inBracketedPaste := false for i, w := 0, 0; i < len(b); i += w { r, width := utf8.DecodeRune(b[i:]) if r == utf8.RuneError { return nil, errors.New("could not decode rune") } - if r == '\x1b' && len(runes) > 1 { - // a new key sequence has started - runeSets = append(runeSets, runes) - runes = []rune{} + if r == '\x1b' { + if !bpEnabled { + // Simple, no-bracketed-paste behavior. + if len(runes) > 0 { + runeSets = append(runeSets, runeSet{runes: runes}) + runes = []rune{} + } + } else { + // Bracketed paste enabled. + // If outside of a bp block, look for start sequences. + if !inBracketedPaste { + // End the previous sequence if there was one. + if len(runes) > 0 { + runeSets = append(runeSets, runeSet{runes: runes}) + runes = []rune{} + } + if i+len(bpStartSeq) <= len(b) && bytes.Equal(b[i:i+len(bpStartSeq)], []byte(bpStartSeq)) { + inBracketedPaste = true + w = len(bpStartSeq) + continue + } + } else { + // Currently in a bracketed paste block. + if i+len(bpEndSeq) <= len(b) && bytes.Equal(b[i:i+len(bpEndSeq)], []byte(bpEndSeq)) { + // End of block; create a sequence with the input so far. + runeSets = append(runeSets, runeSet{runes: runes, bracketedPaste: true}) + runes = []rune{} + inBracketedPaste = false + w = len(bpEndSeq) + continue + } + } + } } runes = append(runes, r) w = width } - // add the final set of runes we decoded - runeSets = append(runeSets, runes) + // add the final set of runes we decoded, if any. + if len(runes) > 0 { + runeSets = append(runeSets, runeSet{runes: runes, bracketedPaste: inBracketedPaste}) + } if len(runeSets) == 0 { return nil, errors.New("received 0 runes from input") } var msgs []Msg - for _, runes := range runeSets { + for _, set := range runeSets { + // Is it a literal pasted block? + if set.bracketedPaste { + // Paste the characters as-is. + msgs = append(msgs, KeyMsg(Key{Type: KeyRunes, Runes: set.runes, Paste: true})) + continue + } + // Is it a sequence, like an arrow key? + runes := set.runes if k, ok := sequences[string(runes)]; ok { msgs = append(msgs, KeyMsg(k)) continue @@ -662,3 +731,43 @@ func readInputs(input io.Reader) ([]Msg, error) { return msgs, nil } + +// SanitizeRunes removes control characters from runes in a KeyRunes +// message, and optionally replaces newline/carriage return by a +// specified character. +// +// The rune array is modified in-place. The returned slice +// is the original slice shortened after the control characters have been removed. +func SanitizeRunes(runes []rune, replaceNewLine rune) []rune { + var dst int + for src := 0; src < len(runes); src++ { + r := runes[src] + if r == '\r' || r == '\n' { + runes[dst] = replaceNewLine + dst++ + } else if r == utf8.RuneError || unicode.IsControl(r) { + // skip + } else { + // Keep the character. + runes[dst] = runes[src] + dst++ + } + } + return runes[:dst] +} + +// readMore extends the input with additional bytes from the input. +// This is called when there's a spike in input. (e.g. copy-paste) +func readMore(buf []byte, input io.Reader) ([]byte, int, error) { + var inputBuf [256]byte + for { + numBytes, err := input.Read(inputBuf[:]) + if err != nil { + return nil, 0, err + } + if numBytes < len(inputBuf) { + return buf, len(buf), nil + } + buf = append(buf, inputBuf[:]...) + } +} diff --git a/key_test.go b/key_test.go index 07c2743c65..55fa615d09 100644 --- a/key_test.go +++ b/key_test.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "testing" + "unicode/utf8" ) func TestKeyString(t *testing.T) { @@ -50,9 +51,10 @@ func TestKeyTypeString(t *testing.T) { func TestReadInput(t *testing.T) { type test struct { - keyname string - in []byte - out []Msg + keyname string + in []byte + out []Msg + bpEnabled bool } for i, td := range []test{ {"a", @@ -63,6 +65,7 @@ func TestReadInput(t *testing.T) { Runes: []rune{'a'}, }, }, + false, }, {" ", []byte{' '}, @@ -72,6 +75,7 @@ func TestReadInput(t *testing.T) { Runes: []rune{' '}, }, }, + false, }, {"ctrl+a", []byte{byte(keySOH)}, @@ -80,6 +84,7 @@ func TestReadInput(t *testing.T) { Type: KeyCtrlA, }, }, + false, }, {"alt+a", []byte{byte(0x1b), 'a'}, @@ -90,6 +95,7 @@ func TestReadInput(t *testing.T) { Runes: []rune{'a'}, }, }, + false, }, {"abcd", []byte{'a', 'b', 'c', 'd'}, @@ -111,6 +117,7 @@ func TestReadInput(t *testing.T) { Runes: []rune{'d'}, }, }, + false, }, {"up", []byte("\x1b[A"), @@ -119,6 +126,7 @@ func TestReadInput(t *testing.T) { Type: KeyUp, }, }, + false, }, {"wheel up", []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, @@ -127,6 +135,7 @@ func TestReadInput(t *testing.T) { Type: MouseWheelUp, }, }, + false, }, {"shift+tab", []byte{'\x1b', '[', 'Z'}, @@ -135,6 +144,26 @@ func TestReadInput(t *testing.T) { Type: KeyShiftTab, }, }, + false, + }, + {"[hello world]", + []byte("b\x1b[200~hello world\x1b[201~a"), + []Msg{ + KeyMsg{ + Type: KeyRunes, + Runes: []rune{'b'}, + }, + KeyMsg{ + Type: KeyRunes, + Runes: []rune{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}, + Paste: true, + }, + KeyMsg{ + Type: KeyRunes, + Runes: []rune{'a'}, + }, + }, + true, }, {"alt+enter", []byte{'\x1b', '\r'}, @@ -144,6 +173,7 @@ func TestReadInput(t *testing.T) { Alt: true, }, }, + false, }, {"insert", []byte{'\x1b', '[', '2', '~'}, @@ -152,6 +182,7 @@ func TestReadInput(t *testing.T) { Type: KeyInsert, }, }, + false, }, {"alt+ctrl+a", []byte{'\x1b', byte(keySOH)}, @@ -161,44 +192,52 @@ func TestReadInput(t *testing.T) { Alt: true, }, }, + false, }, {"unrecognized CSI", []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, []Msg{}, + false, }, // Powershell sequences. {"up", []byte{'\x1b', 'O', 'A'}, []Msg{KeyMsg{Type: KeyUp}}, + false, }, {"down", []byte{'\x1b', 'O', 'B'}, []Msg{KeyMsg{Type: KeyDown}}, + false, }, {"right", []byte{'\x1b', 'O', 'C'}, []Msg{KeyMsg{Type: KeyRight}}, + false, }, {"left", []byte{'\x1b', 'O', 'D'}, []Msg{KeyMsg{Type: KeyLeft}}, + false, }, {"alt+enter", []byte{'\x1b', '\x0d'}, []Msg{KeyMsg{Type: KeyEnter, Alt: true}}, + false, }, {"alt+backspace", []byte{'\x1b', '\x7f'}, []Msg{KeyMsg{Type: KeyBackspace, Alt: true}}, + false, }, } { - t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) { - msgs, err := readInputs(bytes.NewReader(td.in)) + t.Run(fmt.Sprintf("%d: %s %t", i, td.keyname, td.bpEnabled), func(t *testing.T) { + msgs, err := readInputs(bytes.NewReader(td.in), td.bpEnabled) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(msgs) != len(td.out) { - t.Fatalf("unexpected message list length") + t.Fatalf("unexpected message list length: expected %d, got %d", len(td.out), len(msgs)) } if len(msgs) == 1 { @@ -222,3 +261,31 @@ func TestReadInput(t *testing.T) { }) } } + +func TestSanitizeRunes(t *testing.T) { + td := []struct { + input, output string + }{ + {"hello", "hello"}, + {"hel\nlo", "helXlo"}, + {"hel\rlo", "helXlo"}, + {"hel\x1blo", "hello"}, + {"hello\xc2", "hello"}, // invalid utf8 + } + + for _, tc := range td { + runes := make([]rune, 0, len(tc.input)) + b := []byte(tc.input) + for i, w := 0, 0; i < len(b); i += w { + var r rune + r, w = utf8.DecodeRune(b[i:]) + runes = append(runes, r) + } + t.Logf("input runes: %+v", runes) + result := SanitizeRunes(runes, 'X') + rs := string(result) + if tc.output != rs { + t.Errorf("%q: expected %q, got %q (%+v)", tc.input, tc.output, rs, result) + } + } +} diff --git a/nil_renderer.go b/nil_renderer.go index a0226364e7..d2422dee4a 100644 --- a/nil_renderer.go +++ b/nil_renderer.go @@ -13,6 +13,9 @@ func (n nilRenderer) enterAltScreen() {} func (n nilRenderer) exitAltScreen() {} func (n nilRenderer) showCursor() {} func (n nilRenderer) hideCursor() {} +func (n nilRenderer) bracketedPaste() bool { return false } +func (n nilRenderer) enableBracketedPaste() {} +func (n nilRenderer) disableBracketedPaste() {} func (n nilRenderer) enableMouseCellMotion() {} func (n nilRenderer) disableMouseCellMotion() {} func (n nilRenderer) enableMouseAllMotion() {} diff --git a/nil_renderer_test.go b/nil_renderer_test.go index ab94e34d23..ca3f1043ff 100644 --- a/nil_renderer_test.go +++ b/nil_renderer_test.go @@ -17,6 +17,11 @@ func TestNilRenderer(t *testing.T) { r.clearScreen() r.showCursor() r.hideCursor() + r.enableBracketedPaste() + if r.bracketedPaste() { + t.Errorf("bracketedPaste should always return false") + } + r.disableBracketedPaste() r.enableMouseCellMotion() r.disableMouseCellMotion() r.enableMouseAllMotion() diff --git a/options.go b/options.go index 59d068524e..7ce768bf65 100644 --- a/options.go +++ b/options.go @@ -76,6 +76,13 @@ func WithAltScreen() ProgramOption { } } +// WithoutBracketedPaste starts the program with bracketed paste disabled. +func WithoutBracketedPaste() ProgramOption { + return func(p *Program) { + p.startupOptions |= withoutBracketedPaste + } +} + // WithMouseCellMotion starts the program with the mouse enabled in "cell // motion" mode. // diff --git a/options_test.go b/options_test.go index 7e08f58eee..77f330ea10 100644 --- a/options_test.go +++ b/options_test.go @@ -51,6 +51,10 @@ func TestOptions(t *testing.T) { exercise(t, WithAltScreen(), withAltScreen) }) + t.Run("bracketed space disabled", func(t *testing.T) { + exercise(t, WithoutBracketedPaste(), withoutBracketedPaste) + }) + t.Run("ansi compression", func(t *testing.T) { exercise(t, WithANSICompressor(), withANSICompressor) }) @@ -85,8 +89,8 @@ func TestOptions(t *testing.T) { }) t.Run("multiple", func(t *testing.T) { - p := NewProgram(nil, WithMouseAllMotion(), WithAltScreen(), WithInputTTY()) - for _, opt := range []startupOptions{withMouseAllMotion, withAltScreen, withInputTTY} { + p := NewProgram(nil, WithMouseAllMotion(), WithoutBracketedPaste(), WithAltScreen(), WithInputTTY()) + for _, opt := range []startupOptions{withMouseAllMotion, withoutBracketedPaste, withAltScreen, withInputTTY} { if !p.startupOptions.has(opt) { t.Errorf("expected startup options have %v, got %v", opt, p.startupOptions) } diff --git a/renderer.go b/renderer.go index a6f416277f..c37bef7c9c 100644 --- a/renderer.go +++ b/renderer.go @@ -36,6 +36,13 @@ type renderer interface { // Hide the cursor. hideCursor() + // Whether or not bracketed paste mode is enabled. + bracketedPaste() bool + // enableBracketedPaste enables bracketed space. + enableBracketedPaste() + // disableBracketedPaste disables bracketed space. + disableBracketedPaste() + // enableMouseCellMotion enables mouse click, release, wheel and motion // events if a mouse button is pressed (i.e., drag events). enableMouseCellMotion() diff --git a/screen.go b/screen.go index 899db3d257..b777c3d8b3 100644 --- a/screen.go +++ b/screen.go @@ -116,6 +116,34 @@ func ShowCursor() Msg { // this message with ShowCursor. type showCursorMsg struct{} +// EnableBracketedPaste is a special command that tells the Bubble Tea program +// to accept bracketed paste input. +// +// Note that bracketed paste will be automatically disabled when the +// program quits. +func EnableBracketedPaste() Msg { + return enableBracketedPasteMsg{} +} + +// enableBracketedPasteMsg in an internal message signals that +// bracketed paste should be enabled. You can send an +// enableBracketedPasteMsg with EnableBracketedPaste. +type enableBracketedPasteMsg struct{} + +// DisableBracketedPaste is a special command that tells the Bubble Tea program +// to accept bracketed paste input. +// +// Note that bracketed paste will be automatically disabled when the +// program quits. +func DisableBracketedPaste() Msg { + return disableBracketedPasteMsg{} +} + +// disableBracketedPasteMsg in an internal message signals that +// bracketed paste should be disabled. You can send an +// disableBracketedPasteMsg with DisableBracketedPaste. +type disableBracketedPasteMsg struct{} + // EnterAltScreen enters the alternate screen buffer, which consumes the entire // terminal window. ExitAltScreen will return the terminal to its former state. // diff --git a/screen_test.go b/screen_test.go index 33b51695b2..b2c7ba79f4 100644 --- a/screen_test.go +++ b/screen_test.go @@ -14,42 +14,42 @@ func TestClearMsg(t *testing.T) { { name: "clear_screen", cmds: []Cmd{ClearScreen}, - expected: "\x1b[?25l\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?2004h\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?2004l", }, { name: "altscreen", cmds: []Cmd{EnterAltScreen, ExitAltScreen}, - expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?1049lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?1049lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?2004l", }, { name: "altscreen_autoexit", cmds: []Cmd{EnterAltScreen}, - expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1049l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?2004l\x1b[?1049l", }, { name: "mouse_cellmotion", cmds: []Cmd{EnableMouseCellMotion}, - expected: "\x1b[?25l\x1b[?1002hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1002hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?2004l", }, { name: "mouse_allmotion", cmds: []Cmd{EnableMouseAllMotion}, - expected: "\x1b[?25l\x1b[?1003hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1003hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?2004l", }, { name: "mouse_disable", cmds: []Cmd{EnableMouseAllMotion, DisableMouse}, - expected: "\x1b[?25l\x1b[?1003h\x1b[?1002l\x1b[?1003lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1002l\x1b[?1003lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?2004l", }, { name: "cursor_hide", cmds: []Cmd{HideCursor}, - expected: "\x1b[?25l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?2004l", }, { name: "cursor_hideshow", cmds: []Cmd{HideCursor, ShowCursor}, - expected: "\x1b[?25l\x1b[?25l\x1b[?25hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\x1b[?25hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?2004l", }, } diff --git a/standard_renderer.go b/standard_renderer.go index 9260e5e00b..58f24028ea 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -41,6 +41,9 @@ type standardRenderer struct { // essentially whether or not we're using the full size of the terminal altScreenActive bool + // whether bracketed pastes are enabled + bracketedPasteEnabled bool + // renderer dimensions; usually the size of the window width int height int @@ -263,6 +266,37 @@ func (r *standardRenderer) clearScreen() { r.repaint() } +func (r *standardRenderer) bracketedPaste() bool { + r.mtx.Lock() + defer r.mtx.Unlock() + + return r.bracketedPasteEnabled +} + +func (r *standardRenderer) enableBracketedPaste() { + r.mtx.Lock() + defer r.mtx.Unlock() + + if r.bracketedPasteEnabled { + return + } + + r.bracketedPasteEnabled = true + r.out.EnableBracketedPaste() +} + +func (r *standardRenderer) disableBracketedPaste() { + r.mtx.Lock() + defer r.mtx.Unlock() + + if !r.bracketedPasteEnabled { + return + } + + r.bracketedPasteEnabled = false + r.out.DisableBracketedPaste() +} + func (r *standardRenderer) altScreen() bool { return r.altScreenActive } diff --git a/tea.go b/tea.go index b94ca4e25e..2b743f6f0c 100644 --- a/tea.go +++ b/tea.go @@ -64,7 +64,7 @@ type handlers []chan struct{} // generally set with ProgramOptions. // // The options here are treated as bits. -type startupOptions byte +type startupOptions uint16 func (s startupOptions) has(option startupOptions) bool { return s&option != 0 @@ -78,6 +78,7 @@ const ( withCustomInput withANSICompressor withoutSignalHandler + withoutBracketedPaste // Catching panics is incredibly useful for restoring the terminal to a // usable state after a panic occurs. When this is set, Bubble Tea will @@ -115,6 +116,9 @@ type Program struct { altScreenWasActive bool ignoreSignals bool + // was the bracketed paste mode active before releasing the terminal? + bpWasActive bool + // Stores the original reference to stdin for cases where input is not a // TTY on windows and we've automatically opened CONIN$ to receive input. // When the program exits this will be restored. @@ -279,6 +283,12 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { case clearScreenMsg: p.renderer.clearScreen() + case enableBracketedPasteMsg: + p.renderer.enableBracketedPaste() + + case disableBracketedPasteMsg: + p.renderer.disableBracketedPaste() + case enterAltScreenMsg: p.renderer.enterAltScreen() @@ -412,6 +422,9 @@ func (p *Program) Run() (Model, error) { if p.startupOptions&withAltScreen != 0 { p.renderer.enterAltScreen() } + if p.startupOptions&withoutBracketedPaste == 0 { + p.renderer.enableBracketedPaste() + } if p.startupOptions&withMouseCellMotion != 0 { p.renderer.enableMouseCellMotion() } else if p.startupOptions&withMouseAllMotion != 0 { @@ -535,7 +548,6 @@ func (p *Program) shutdown(kill bool) { p.renderer.stop() } } - _ = p.restoreTerminalState() if p.restoreOutput != nil { _ = p.restoreOutput() @@ -550,6 +562,7 @@ func (p *Program) ReleaseTerminal() error { p.waitForReadLoop() p.altScreenWasActive = p.renderer.altScreen() + p.bpWasActive = p.renderer.bracketedPaste() return p.restoreTerminalState() } @@ -566,6 +579,9 @@ func (p *Program) RestoreTerminal() error { return err } + if p.bpWasActive { + p.renderer.enableBracketedPaste() + } if p.altScreenWasActive { p.renderer.enterAltScreen() } else { diff --git a/tty.go b/tty.go index 4f33d86602..7c405e5e81 100644 --- a/tty.go +++ b/tty.go @@ -33,6 +33,10 @@ func (p *Program) restoreTerminalState() error { p.renderer.disableMouseCellMotion() p.renderer.disableMouseAllMotion() + if p.renderer.bracketedPaste() { + p.renderer.disableBracketedPaste() + } + if p.renderer.altScreen() { p.renderer.exitAltScreen() @@ -73,7 +77,7 @@ func (p *Program) readLoop() { return } - msgs, err := readInputs(p.cancelReader) + msgs, err := readInputs(p.cancelReader, p.renderer.bracketedPaste()) if err != nil { if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { p.errs <- err