Skip to content

Commit

Permalink
Support bracketed paste
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
knz authored and muesli committed Oct 13, 2022
1 parent db66482 commit 1eacdc1
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 29 deletions.
129 changes: 119 additions & 10 deletions 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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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[:]...)
}
}

0 comments on commit 1eacdc1

Please sign in to comment.