forked from Mirrors/bubbletea
feat: bracketed paste (#397)
* feat: 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 useful because pasted input may need sanitation in individual widgets. * fix(key): support bracketed paste with short reads Some terminal emulators feed the bracketed paste data in multiple chunks, which may not be aligned on a 256 byte boundary. So it's possible for `input.Read` to return less than 256 bytes read but while there's still more data to be read to complete a bracketed paste input. --------- Co-authored-by: Christian Muehlhaeuser <muesli@gmail.com>
This commit is contained in:
parent
ab7e5ea8b2
commit
2b46020ca0
|
@ -1,3 +1,3 @@
|
||||||
[?25lHi. This program will exit in 10 seconds. To quit sooner press any key
|
[?25l[?2004hHi. This program will exit in 10 seconds. To quit sooner press any key
|
||||||
[70D[1A[70D[2KHi. This program will exit in 9 seconds. To quit sooner press any key.
|
[70D[1A[70D[2KHi. This program will exit in 9 seconds. To quit sooner press any key.
|
||||||
[70D[2K[?25h[?1002l[?1003l[?1006l
|
[70D[2K[?2004l[?25h[?1002l[?1003l[?1006l
|
32
key.go
32
key.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ type Key struct {
|
||||||
Type KeyType
|
Type KeyType
|
||||||
Runes []rune
|
Runes []rune
|
||||||
Alt bool
|
Alt bool
|
||||||
|
Paste bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a friendly string representation for a key. It's safe (and
|
// String returns a friendly string representation for a key. It's safe (and
|
||||||
|
@ -63,15 +65,28 @@ type Key struct {
|
||||||
// fmt.Println(k)
|
// fmt.Println(k)
|
||||||
// // Output: enter
|
// // Output: enter
|
||||||
func (k Key) String() (str string) {
|
func (k Key) String() (str string) {
|
||||||
|
var buf strings.Builder
|
||||||
if k.Alt {
|
if k.Alt {
|
||||||
str += "alt+"
|
buf.WriteString("alt+")
|
||||||
}
|
}
|
||||||
if k.Type == KeyRunes {
|
if k.Type == KeyRunes {
|
||||||
str += string(k.Runes)
|
if k.Paste {
|
||||||
return str
|
// Note: bubbles/keys bindings currently do string compares to
|
||||||
|
// recognize shortcuts. Since pasted text should never activate
|
||||||
|
// shortcuts, we need to ensure that the binding code doesn't
|
||||||
|
// match Key events that result from pastes. We achieve this
|
||||||
|
// here by enclosing pastes in '[...]' so that the string
|
||||||
|
// comparison in Matches() fails in that case.
|
||||||
|
buf.WriteByte('[')
|
||||||
|
}
|
||||||
|
buf.WriteString(string(k.Runes))
|
||||||
|
if k.Paste {
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
} else if s, ok := keyNames[k.Type]; ok {
|
} else if s, ok := keyNames[k.Type]; ok {
|
||||||
str += s
|
buf.WriteString(s)
|
||||||
return str
|
return buf.String()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -613,6 +628,13 @@ func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect bracketed paste.
|
||||||
|
var foundbp bool
|
||||||
|
foundbp, w, msg = detectBracketedPaste(b)
|
||||||
|
if foundbp {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Detect escape sequence and control characters other than NUL,
|
// Detect escape sequence and control characters other than NUL,
|
||||||
// possibly with an escape character in front to mark the Alt
|
// possibly with an escape character in front to mark the Alt
|
||||||
// modifier.
|
// modifier.
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package tea
|
package tea
|
||||||
|
|
||||||
import "sort"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
// extSequences is used by the map-based algorithm below. It contains
|
// extSequences is used by the map-based algorithm below. It contains
|
||||||
// the sequences plus their alternatives with an escape character
|
// the sequences plus their alternatives with an escape character
|
||||||
|
@ -69,3 +73,47 @@ func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) {
|
||||||
|
|
||||||
return false, 0, nil
|
return false, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// detectBracketedPaste detects an input pasted while bracketed
|
||||||
|
// paste mode was enabled.
|
||||||
|
//
|
||||||
|
// Note: this function is a no-op if bracketed paste was not enabled
|
||||||
|
// on the terminal, since in that case we'd never see this
|
||||||
|
// particular escape sequence.
|
||||||
|
func detectBracketedPaste(input []byte) (hasBp bool, width int, msg Msg) {
|
||||||
|
// Detect the start sequence.
|
||||||
|
const bpStart = "\x1b[200~"
|
||||||
|
if len(input) < len(bpStart) || string(input[:len(bpStart)]) != bpStart {
|
||||||
|
return false, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip over the start sequence.
|
||||||
|
input = input[len(bpStart):]
|
||||||
|
|
||||||
|
// If we saw the start sequence, then we must have an end sequence
|
||||||
|
// as well. Find it.
|
||||||
|
const bpEnd = "\x1b[201~"
|
||||||
|
idx := bytes.Index(input, []byte(bpEnd))
|
||||||
|
inputLen := len(bpStart) + idx + len(bpEnd)
|
||||||
|
if idx == -1 {
|
||||||
|
// We have encountered the end of the input buffer without seeing
|
||||||
|
// the marker for the end of the bracketed paste.
|
||||||
|
// Tell the outer loop we have done a short read and we want more.
|
||||||
|
return true, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The paste is everything in-between.
|
||||||
|
paste := input[:idx]
|
||||||
|
|
||||||
|
// All there is in-between is runes, not to be interpreted further.
|
||||||
|
k := Key{Type: KeyRunes, Paste: true}
|
||||||
|
for len(paste) > 0 {
|
||||||
|
r, w := utf8.DecodeRune(paste)
|
||||||
|
if r != utf8.RuneError {
|
||||||
|
k.Runes = append(k.Runes, r)
|
||||||
|
}
|
||||||
|
paste = paste[w:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, inputLen, KeyMsg(k)
|
||||||
|
}
|
||||||
|
|
26
key_test.go
26
key_test.go
|
@ -434,23 +434,25 @@ func TestReadInput(t *testing.T) {
|
||||||
[]byte{'\x1b', '\x1b'},
|
[]byte{'\x1b', '\x1b'},
|
||||||
[]Msg{KeyMsg{Type: KeyEsc, Alt: true}},
|
[]Msg{KeyMsg{Type: KeyEsc, Alt: true}},
|
||||||
},
|
},
|
||||||
// Bracketed paste does not work yet.
|
{"[a b] o",
|
||||||
{"?CSI[50 48 48 126]? a b ?CSI[50 48 49 126]?",
|
|
||||||
[]byte{
|
[]byte{
|
||||||
'\x1b', '[', '2', '0', '0', '~',
|
'\x1b', '[', '2', '0', '0', '~',
|
||||||
'a', ' ', 'b',
|
'a', ' ', 'b',
|
||||||
|
'\x1b', '[', '2', '0', '1', '~',
|
||||||
|
'o',
|
||||||
|
},
|
||||||
|
[]Msg{
|
||||||
|
KeyMsg{Type: KeyRunes, Runes: []rune("a b"), Paste: true},
|
||||||
|
KeyMsg{Type: KeyRunes, Runes: []rune("o")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{"[a\x03\nb]",
|
||||||
|
[]byte{
|
||||||
|
'\x1b', '[', '2', '0', '0', '~',
|
||||||
|
'a', '\x03', '\n', 'b',
|
||||||
'\x1b', '[', '2', '0', '1', '~'},
|
'\x1b', '[', '2', '0', '1', '~'},
|
||||||
[]Msg{
|
[]Msg{
|
||||||
// What we expect once bracketed paste is recognized properly:
|
KeyMsg{Type: KeyRunes, Runes: []rune("a\x03\nb"), Paste: true},
|
||||||
//
|
|
||||||
// KeyMsg{Type: KeyRunes, Runes: []rune("a b")},
|
|
||||||
//
|
|
||||||
// What we get instead (for now):
|
|
||||||
unknownCSISequenceMsg{0x1b, 0x5b, 0x32, 0x30, 0x30, 0x7e},
|
|
||||||
KeyMsg{Type: KeyRunes, Runes: []rune{'a'}},
|
|
||||||
KeyMsg{Type: KeySpace, Runes: []rune{' '}},
|
|
||||||
KeyMsg{Type: KeyRunes, Runes: []rune{'b'}},
|
|
||||||
unknownCSISequenceMsg{0x1b, 0x5b, 0x32, 0x30, 0x31, 0x7e},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,23 @@ package tea
|
||||||
|
|
||||||
type nilRenderer struct{}
|
type nilRenderer struct{}
|
||||||
|
|
||||||
func (n nilRenderer) start() {}
|
func (n nilRenderer) start() {}
|
||||||
func (n nilRenderer) stop() {}
|
func (n nilRenderer) stop() {}
|
||||||
func (n nilRenderer) kill() {}
|
func (n nilRenderer) kill() {}
|
||||||
func (n nilRenderer) write(_ string) {}
|
func (n nilRenderer) write(_ string) {}
|
||||||
func (n nilRenderer) repaint() {}
|
func (n nilRenderer) repaint() {}
|
||||||
func (n nilRenderer) clearScreen() {}
|
func (n nilRenderer) clearScreen() {}
|
||||||
func (n nilRenderer) altScreen() bool { return false }
|
func (n nilRenderer) altScreen() bool { return false }
|
||||||
func (n nilRenderer) enterAltScreen() {}
|
func (n nilRenderer) enterAltScreen() {}
|
||||||
func (n nilRenderer) exitAltScreen() {}
|
func (n nilRenderer) exitAltScreen() {}
|
||||||
func (n nilRenderer) showCursor() {}
|
func (n nilRenderer) showCursor() {}
|
||||||
func (n nilRenderer) hideCursor() {}
|
func (n nilRenderer) hideCursor() {}
|
||||||
func (n nilRenderer) enableMouseCellMotion() {}
|
func (n nilRenderer) enableMouseCellMotion() {}
|
||||||
func (n nilRenderer) disableMouseCellMotion() {}
|
func (n nilRenderer) disableMouseCellMotion() {}
|
||||||
func (n nilRenderer) enableMouseAllMotion() {}
|
func (n nilRenderer) enableMouseAllMotion() {}
|
||||||
func (n nilRenderer) disableMouseAllMotion() {}
|
func (n nilRenderer) disableMouseAllMotion() {}
|
||||||
func (n nilRenderer) enableMouseSGRMode() {}
|
func (n nilRenderer) enableBracketedPaste() {}
|
||||||
func (n nilRenderer) disableMouseSGRMode() {}
|
func (n nilRenderer) disableBracketedPaste() {}
|
||||||
|
func (n nilRenderer) enableMouseSGRMode() {}
|
||||||
|
func (n nilRenderer) disableMouseSGRMode() {}
|
||||||
|
func (n nilRenderer) bracketedPasteActive() bool { return false }
|
||||||
|
|
|
@ -101,6 +101,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
|
// WithMouseCellMotion starts the program with the mouse enabled in "cell
|
||||||
// motion" mode.
|
// motion" mode.
|
||||||
//
|
//
|
||||||
|
|
|
@ -81,6 +81,10 @@ func TestOptions(t *testing.T) {
|
||||||
exercise(t, WithAltScreen(), withAltScreen)
|
exercise(t, WithAltScreen(), withAltScreen)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("bracketed paste disabled", func(t *testing.T) {
|
||||||
|
exercise(t, WithoutBracketedPaste(), withoutBracketedPaste)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("ansi compression", func(t *testing.T) {
|
t.Run("ansi compression", func(t *testing.T) {
|
||||||
exercise(t, WithANSICompressor(), withANSICompressor)
|
exercise(t, WithANSICompressor(), withANSICompressor)
|
||||||
})
|
})
|
||||||
|
@ -115,8 +119,8 @@ func TestOptions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("multiple", func(t *testing.T) {
|
t.Run("multiple", func(t *testing.T) {
|
||||||
p := NewProgram(nil, WithMouseAllMotion(), WithAltScreen(), WithInputTTY())
|
p := NewProgram(nil, WithMouseAllMotion(), WithoutBracketedPaste(), WithAltScreen(), WithInputTTY())
|
||||||
for _, opt := range []startupOptions{withMouseAllMotion, withAltScreen} {
|
for _, opt := range []startupOptions{withMouseAllMotion, withoutBracketedPaste, withAltScreen} {
|
||||||
if !p.startupOptions.has(opt) {
|
if !p.startupOptions.has(opt) {
|
||||||
t.Errorf("expected startup options have %v, got %v", opt, p.startupOptions)
|
t.Errorf("expected startup options have %v, got %v", opt, p.startupOptions)
|
||||||
}
|
}
|
||||||
|
|
11
renderer.go
11
renderer.go
|
@ -56,6 +56,17 @@ type renderer interface {
|
||||||
|
|
||||||
// disableMouseSGRMode disables mouse extended mode (SGR).
|
// disableMouseSGRMode disables mouse extended mode (SGR).
|
||||||
disableMouseSGRMode()
|
disableMouseSGRMode()
|
||||||
|
|
||||||
|
// enableBracketedPaste enables bracketed paste, where characters
|
||||||
|
// inside the input are not interpreted when pasted as a whole.
|
||||||
|
enableBracketedPaste()
|
||||||
|
|
||||||
|
// disableBracketedPaste disables bracketed paste.
|
||||||
|
disableBracketedPaste()
|
||||||
|
|
||||||
|
// bracketedPasteActive reports whether bracketed paste mode is
|
||||||
|
// currently enabled.
|
||||||
|
bracketedPasteActive() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// repaintMsg forces a full repaint.
|
// repaintMsg forces a full repaint.
|
||||||
|
|
28
screen.go
28
screen.go
|
@ -116,6 +116,34 @@ func ShowCursor() Msg {
|
||||||
// this message with ShowCursor.
|
// this message with ShowCursor.
|
||||||
type showCursorMsg struct{}
|
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
|
// EnterAltScreen enters the alternate screen buffer, which consumes the entire
|
||||||
// terminal window. ExitAltScreen will return the terminal to its former state.
|
// terminal window. ExitAltScreen will return the terminal to its former state.
|
||||||
//
|
//
|
||||||
|
|
|
@ -14,42 +14,47 @@ func TestClearMsg(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "clear_screen",
|
name: "clear_screen",
|
||||||
cmds: []Cmd{ClearScreen},
|
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\x1b[?1006l",
|
expected: "\x1b[?25l\x1b[?2004h\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "altscreen",
|
name: "altscreen",
|
||||||
cmds: []Cmd{EnterAltScreen, ExitAltScreen},
|
cmds: []Cmd{EnterAltScreen, ExitAltScreen},
|
||||||
expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "altscreen_autoexit",
|
name: "altscreen_autoexit",
|
||||||
cmds: []Cmd{EnterAltScreen},
|
cmds: []Cmd{EnterAltScreen},
|
||||||
expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25lsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h",
|
expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25lsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mouse_cellmotion",
|
name: "mouse_cellmotion",
|
||||||
cmds: []Cmd{EnableMouseCellMotion},
|
cmds: []Cmd{EnableMouseCellMotion},
|
||||||
expected: "\x1b[?25l\x1b[?1002h\x1b[?1006hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
expected: "\x1b[?25l\x1b[?2004h\x1b[?1002h\x1b[?1006hsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mouse_allmotion",
|
name: "mouse_allmotion",
|
||||||
cmds: []Cmd{EnableMouseAllMotion},
|
cmds: []Cmd{EnableMouseAllMotion},
|
||||||
expected: "\x1b[?25l\x1b[?1003h\x1b[?1006hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006hsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mouse_disable",
|
name: "mouse_disable",
|
||||||
cmds: []Cmd{EnableMouseAllMotion, DisableMouse},
|
cmds: []Cmd{EnableMouseAllMotion, DisableMouse},
|
||||||
expected: "\x1b[?25l\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006lsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cursor_hide",
|
name: "cursor_hide",
|
||||||
cmds: []Cmd{HideCursor},
|
cmds: []Cmd{HideCursor},
|
||||||
expected: "\x1b[?25l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
expected: "\x1b[?25l\x1b[?2004h\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cursor_hideshow",
|
name: "cursor_hideshow",
|
||||||
cmds: []Cmd{HideCursor, ShowCursor},
|
cmds: []Cmd{HideCursor, ShowCursor},
|
||||||
expected: "\x1b[?25l\x1b[?25l\x1b[?25hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\x1b[?25hsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bp_stop_start",
|
||||||
|
cmds: []Cmd{DisableBracketedPaste, EnableBracketedPaste},
|
||||||
|
expected: "\x1b[?25l\x1b[?2004h\x1b[?2004l\x1b[?2004hsuccess\r\n\x1b[0D\x1b[2K\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +74,7 @@ func TestClearMsg(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if buf.String() != test.expected {
|
if buf.String() != test.expected {
|
||||||
t.Errorf("expected embedded sequence, got %q", buf.String())
|
t.Errorf("expected embedded sequence:\n%q\ngot:\n%q", test.expected, buf.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,9 @@ type standardRenderer struct {
|
||||||
// essentially whether or not we're using the full size of the terminal
|
// essentially whether or not we're using the full size of the terminal
|
||||||
altScreenActive bool
|
altScreenActive bool
|
||||||
|
|
||||||
|
// whether or not we're currently using bracketed paste
|
||||||
|
bpActive bool
|
||||||
|
|
||||||
// renderer dimensions; usually the size of the window
|
// renderer dimensions; usually the size of the window
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
@ -410,6 +413,29 @@ func (r *standardRenderer) disableMouseSGRMode() {
|
||||||
r.out.DisableMouseExtendedMode()
|
r.out.DisableMouseExtendedMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) enableBracketedPaste() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.out.EnableBracketedPaste()
|
||||||
|
r.bpActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) disableBracketedPaste() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.out.DisableBracketedPaste()
|
||||||
|
r.bpActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) bracketedPasteActive() bool {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
return r.bpActive
|
||||||
|
}
|
||||||
|
|
||||||
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
|
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
|
||||||
// renderer.
|
// renderer.
|
||||||
func (r *standardRenderer) setIgnoredLines(from int, to int) {
|
func (r *standardRenderer) setIgnoredLines(from int, to int) {
|
||||||
|
|
20
tea.go
20
tea.go
|
@ -81,7 +81,7 @@ func (i inputType) String() string {
|
||||||
// generally set with ProgramOptions.
|
// generally set with ProgramOptions.
|
||||||
//
|
//
|
||||||
// The options here are treated as bits.
|
// The options here are treated as bits.
|
||||||
type startupOptions byte
|
type startupOptions int16
|
||||||
|
|
||||||
func (s startupOptions) has(option startupOptions) bool {
|
func (s startupOptions) has(option startupOptions) bool {
|
||||||
return s&option != 0
|
return s&option != 0
|
||||||
|
@ -93,12 +93,12 @@ const (
|
||||||
withMouseAllMotion
|
withMouseAllMotion
|
||||||
withANSICompressor
|
withANSICompressor
|
||||||
withoutSignalHandler
|
withoutSignalHandler
|
||||||
|
|
||||||
// Catching panics is incredibly useful for restoring the terminal to a
|
// Catching panics is incredibly useful for restoring the terminal to a
|
||||||
// usable state after a panic occurs. When this is set, Bubble Tea will
|
// usable state after a panic occurs. When this is set, Bubble Tea will
|
||||||
// recover from panics, print the stack trace, and disable raw mode. This
|
// recover from panics, print the stack trace, and disable raw mode. This
|
||||||
// feature is on by default.
|
// feature is on by default.
|
||||||
withoutCatchPanics
|
withoutCatchPanics
|
||||||
|
withoutBracketedPaste
|
||||||
)
|
)
|
||||||
|
|
||||||
// handlers manages series of channels returned by various processes. It allows
|
// handlers manages series of channels returned by various processes. It allows
|
||||||
|
@ -156,6 +156,8 @@ type Program struct {
|
||||||
altScreenWasActive bool
|
altScreenWasActive bool
|
||||||
ignoreSignals uint32
|
ignoreSignals uint32
|
||||||
|
|
||||||
|
bpWasActive bool // was the bracketed paste mode active before releasing the terminal?
|
||||||
|
|
||||||
// Stores the original reference to stdin for cases where input is not a
|
// 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.
|
// TTY on windows and we've automatically opened CONIN$ to receive input.
|
||||||
// When the program exits this will be restored.
|
// When the program exits this will be restored.
|
||||||
|
@ -360,6 +362,12 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
|
||||||
case hideCursorMsg:
|
case hideCursorMsg:
|
||||||
p.renderer.hideCursor()
|
p.renderer.hideCursor()
|
||||||
|
|
||||||
|
case enableBracketedPasteMsg:
|
||||||
|
p.renderer.enableBracketedPaste()
|
||||||
|
|
||||||
|
case disableBracketedPasteMsg:
|
||||||
|
p.renderer.disableBracketedPaste()
|
||||||
|
|
||||||
case execMsg:
|
case execMsg:
|
||||||
// NB: this blocks.
|
// NB: this blocks.
|
||||||
p.exec(msg.cmd, msg.fn)
|
p.exec(msg.cmd, msg.fn)
|
||||||
|
@ -496,6 +504,9 @@ func (p *Program) Run() (Model, error) {
|
||||||
if p.startupOptions&withAltScreen != 0 {
|
if p.startupOptions&withAltScreen != 0 {
|
||||||
p.renderer.enterAltScreen()
|
p.renderer.enterAltScreen()
|
||||||
}
|
}
|
||||||
|
if p.startupOptions&withoutBracketedPaste == 0 {
|
||||||
|
p.renderer.enableBracketedPaste()
|
||||||
|
}
|
||||||
if p.startupOptions&withMouseCellMotion != 0 {
|
if p.startupOptions&withMouseCellMotion != 0 {
|
||||||
p.renderer.enableMouseCellMotion()
|
p.renderer.enableMouseCellMotion()
|
||||||
p.renderer.enableMouseSGRMode()
|
p.renderer.enableMouseSGRMode()
|
||||||
|
@ -656,6 +667,7 @@ func (p *Program) ReleaseTerminal() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
p.altScreenWasActive = p.renderer.altScreen()
|
p.altScreenWasActive = p.renderer.altScreen()
|
||||||
|
p.bpWasActive = p.renderer.bracketedPasteActive()
|
||||||
return p.restoreTerminalState()
|
return p.restoreTerminalState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -671,7 +683,6 @@ func (p *Program) RestoreTerminal() error {
|
||||||
if err := p.initCancelReader(); err != nil {
|
if err := p.initCancelReader(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.altScreenWasActive {
|
if p.altScreenWasActive {
|
||||||
p.renderer.enterAltScreen()
|
p.renderer.enterAltScreen()
|
||||||
} else {
|
} else {
|
||||||
|
@ -681,6 +692,9 @@ func (p *Program) RestoreTerminal() error {
|
||||||
if p.renderer != nil {
|
if p.renderer != nil {
|
||||||
p.renderer.start()
|
p.renderer.start()
|
||||||
}
|
}
|
||||||
|
if p.bpWasActive {
|
||||||
|
p.renderer.enableBracketedPaste()
|
||||||
|
}
|
||||||
|
|
||||||
// If the output is a terminal, it may have been resized while another
|
// If the output is a terminal, it may have been resized while another
|
||||||
// process was at the foreground, in which case we may not have received
|
// process was at the foreground, in which case we may not have received
|
||||||
|
|
1
tty.go
1
tty.go
|
@ -34,6 +34,7 @@ func (p *Program) initTerminal() error {
|
||||||
// Bubble Tea program.
|
// Bubble Tea program.
|
||||||
func (p *Program) restoreTerminalState() error {
|
func (p *Program) restoreTerminalState() error {
|
||||||
if p.renderer != nil {
|
if p.renderer != nil {
|
||||||
|
p.renderer.disableBracketedPaste()
|
||||||
p.renderer.showCursor()
|
p.renderer.showCursor()
|
||||||
p.disableMouse()
|
p.disableMouse()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue