1
0
mirror of https://github.com/gdamore/tcell.git synced 2025-04-24 13:48:51 +08:00

fixes #120 Support for bracketed paste mode

This adds Bracketed Paste support for terminals that have mouse
support and support it.  The bracketing events are EventPaste,
with methods to note Start() or End() of the paste.  Content
comes in as normal rune events.  Programs must opt-in to this by
calling screen.EnablePaste().
This commit is contained in:
Garrett D'Amore 2020-10-15 22:13:08 -07:00
parent aeb3a11948
commit 197faf3eae
9 changed files with 225 additions and 67 deletions

View File

@ -116,11 +116,13 @@ func main() {
Foreground(tcell.ColorReset) Foreground(tcell.ColorReset)
s.SetStyle(defStyle) s.SetStyle(defStyle)
s.EnableMouse() s.EnableMouse()
s.EnablePaste()
s.Clear() s.Clear()
posfmt := "Mouse: %d, %d " posfmt := "Mouse: %d, %d "
btnfmt := "Buttons: %s" btnfmt := "Buttons: %s"
keyfmt := "Keys: %s" keyfmt := "Keys: %s"
pastefmt := "Paste: [%d] %s"
white := tcell.StyleDefault. white := tcell.StyleDefault.
Foreground(tcell.ColorWhite).Background(tcell.ColorRed) Foreground(tcell.ColorWhite).Background(tcell.ColorRed)
@ -131,15 +133,23 @@ func main() {
lchar := '*' lchar := '*'
bstr := "" bstr := ""
lks := "" lks := ""
pstr := ""
ecnt := 0 ecnt := 0
pasting := false
for { for {
drawBox(s, 1, 1, 42, 6, white, ' ') drawBox(s, 1, 1, 42, 7, white, ' ')
emitStr(s, 2, 2, white, "Press ESC twice to exit, C to clear.") emitStr(s, 2, 2, white, "Press ESC twice to exit, C to clear.")
emitStr(s, 2, 3, white, fmt.Sprintf(posfmt, mx, my)) emitStr(s, 2, 3, white, fmt.Sprintf(posfmt, mx, my))
emitStr(s, 2, 4, white, fmt.Sprintf(btnfmt, bstr)) emitStr(s, 2, 4, white, fmt.Sprintf(btnfmt, bstr))
emitStr(s, 2, 5, white, fmt.Sprintf(keyfmt, lks)) emitStr(s, 2, 5, white, fmt.Sprintf(keyfmt, lks))
ps := pstr
if len(ps) > 26 {
ps = "..." + ps[len(ps)-24:]
}
emitStr(s, 2, 6, white, fmt.Sprintf(pastefmt, len(pstr), ps))
s.Show() s.Show()
bstr = "" bstr = ""
ev := s.PollEvent() ev := s.PollEvent()
@ -160,6 +170,17 @@ func main() {
s.SetContent(w-1, h-1, 'R', nil, st) s.SetContent(w-1, h-1, 'R', nil, st)
case *tcell.EventKey: case *tcell.EventKey:
s.SetContent(w-2, h-2, ev.Rune(), nil, st) s.SetContent(w-2, h-2, ev.Rune(), nil, st)
if pasting {
s.SetContent(w-1, h-1, 'P', nil, st)
if ev.Key() == tcell.KeyRune {
pstr = pstr + string(ev.Rune())
} else {
pstr = pstr + "\ufffd" // replacement for now
}
lks = ""
continue
}
pstr = ""
s.SetContent(w-1, h-1, 'K', nil, st) s.SetContent(w-1, h-1, 'K', nil, st)
if ev.Key() == tcell.KeyEscape { if ev.Key() == tcell.KeyEscape {
ecnt++ ecnt++
@ -176,6 +197,11 @@ func main() {
} }
} }
lks = ev.Name() lks = ev.Name()
case *tcell.EventPaste:
pasting = ev.Start()
if pasting {
pstr = ""
}
case *tcell.EventMouse: case *tcell.EventMouse:
x, y := ev.Position() x, y := ev.Position()
button := ev.Buttons() button := ev.Buttons()
@ -206,7 +232,7 @@ func main() {
switch ev.Buttons() { switch ev.Buttons() {
case tcell.ButtonNone: case tcell.ButtonNone:
if ox >= 0 { if ox >= 0 {
bg := tcell.Color((lchar - '0') * 2) | tcell.ColorValid bg := tcell.Color((lchar-'0')*2) | tcell.ColorValid
drawBox(s, ox, oy, x, y, drawBox(s, ox, oy, x, y,
up.Background(bg), up.Background(bg),
lchar) lchar)

View File

@ -249,6 +249,10 @@ func (s *cScreen) DisableMouse() {
s.setInMode(modeResizeEn | modeExtndFlg) s.setInMode(modeResizeEn | modeExtndFlg)
} }
func (s *cScreen) EnablePaste() {}
func (s *cScreen) DisablePaste() {}
func (s *cScreen) Fini() { func (s *cScreen) Fini() {
s.finiOnce.Do(s.finish) s.finiOnce.Do(s.finish)
} }

6
key.go
View File

@ -375,6 +375,12 @@ const (
KeyF64 KeyF64
) )
const (
// These key codes are used internally, and will never appear to applications.
keyPasteStart Key = iota + 16384
keyPasteEnd
)
// These are the control keys. Note that they overlap with other keys, // These are the control keys. Note that they overlap with other keys,
// perhaps. For example, KeyCtrlH is the same as KeyBackspace. // perhaps. For example, KeyCtrlH is the same as KeyBackspace.
const ( const (

48
paste.go Normal file
View File

@ -0,0 +1,48 @@
// Copyright 2020 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcell
import (
"time"
)
// EventPaste is used to mark the start and end of a bracketed paste.
// An event with .Start() true will be sent to mark the start.
// Then a number of keys will be sent to indicate that the content
// is pasted in. At the end, an event with .Start() false will be sent.
type EventPaste struct {
start bool
t time.Time
}
// When returns the time when this EventMouse was created.
func (ev *EventPaste) When() time.Time {
return ev.t
}
// Start returns true if this is the start of a paste.
func (ev *EventPaste) Start() bool {
return ev.start
}
// End returns true if this is the end of a paste.
func (ev *EventPaste) End() bool {
return !ev.start
}
// NewEventPaste returns a new EventPaste.
func NewEventPaste(start bool) *EventPaste {
return &EventPaste{t: time.Now(), start: start}
}

View File

@ -104,6 +104,12 @@ type Screen interface {
// DisableMouse disables the mouse. // DisableMouse disables the mouse.
DisableMouse() DisableMouse()
// EnablePaste enables bracketed paste mode, if supported.
EnablePaste()
// DisablePaste() disables bracketed paste mode.
DisablePaste()
// HasMouse returns true if the terminal (apparently) supports a // HasMouse returns true if the terminal (apparently) supports a
// mouse. Note that the a return value of true doesn't guarantee that // mouse. Note that the a return value of true doesn't guarantee that
// a mouse/pointing device is present; a false return definitely // a mouse/pointing device is present; a false return definitely

View File

@ -1,4 +1,4 @@
// Copyright 2016 The TCell Authors // Copyright 2020 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
@ -97,6 +97,7 @@ type simscreen struct {
cursory int cursory int
cursorvis bool cursorvis bool
mouse bool mouse bool
paste bool
charset string charset string
encoder transform.Transformer encoder transform.Transformer
decoder transform.Transformer decoder transform.Transformer
@ -321,6 +322,14 @@ func (s *simscreen) DisableMouse() {
s.mouse = false s.mouse = false
} }
func (s *simscreen) EnablePaste() {
s.paste = true
}
func (s *simscreen) DisablePaste() {
s.paste = false
}
func (s *simscreen) Size() (int, int) { func (s *simscreen) Size() (int, int) {
s.Lock() s.Lock()
w, h := s.back.Size() w, h := s.back.Size()

View File

@ -1,4 +1,4 @@
// Copyright 2019 The TCell Authors // Copyright 2020 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
@ -376,6 +376,24 @@ func LoadTerminfo(name string) (*terminfo.Terminfo, string, error) {
t.KeyCtrlEnd = "\x1b[8^" t.KeyCtrlEnd = "\x1b[8^"
} }
// Technically the RGB flag that is provided for xterm-direct is not
// quite right. The problem is that the -direct flag that was introduced
// with ncurses 6.1 requires a parsing for the parameters that we lack.
// For this case we'll just assume it's XTerm compatible. Someday this
// may be incorrect, but right now it is correct, and nobody uses it
// anyway.
if tc.getflag("Tc") {
// This presumes XTerm 24-bit true color.
t.TrueColor = true
} else if tc.getflag("RGB") {
// This is for xterm-direct, which uses a different scheme entirely.
// (ncurses went a very different direction from everyone else, and
// so it's unlikely anything is using this definition.)
t.TrueColor = true
t.SetBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m"
t.SetFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"
}
// If the kmous entry is present, then we need to record the // If the kmous entry is present, then we need to record the
// the codes to enter and exit mouse mode. Sadly, this is not // the codes to enter and exit mouse mode. Sadly, this is not
// part of the terminfo databases anywhere that I've found, but // part of the terminfo databases anywhere that I've found, but

View File

@ -213,6 +213,10 @@ type Terminfo struct {
KeyAltShfEnd string KeyAltShfEnd string
KeyMetaShfHome string KeyMetaShfHome string
KeyMetaShfEnd string KeyMetaShfEnd string
EnablePaste string // bracketed paste mode
DisablePaste string
PasteStart string
PasteEnd string
Modifiers int Modifiers int
TrueColor bool // true if the terminal supports direct color TrueColor bool // true if the terminal supports direct color
} }

View File

@ -1,4 +1,4 @@
// Copyright 2019 The TCell Authors // Copyright 2020 The TCell Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License. // you may not use file except in compliance with the License.
@ -33,7 +33,7 @@ import (
) )
// NewTerminfoScreen returns a Screen that uses the stock TTY interface // NewTerminfoScreen returns a Screen that uses the stock TTY interface
// and POSIX termios, combined with a terminfo description taken from // and POSIX terminal control, combined with a terminfo description taken from
// the $TERM environment variable. It returns an error if the terminal // the $TERM environment variable. It returns an error if the terminal
// is not supported for any reason. // is not supported for any reason.
// //
@ -75,45 +75,47 @@ type tKeyCode struct {
// tScreen represents a screen backed by a terminfo implementation. // tScreen represents a screen backed by a terminfo implementation.
type tScreen struct { type tScreen struct {
ti *terminfo.Terminfo ti *terminfo.Terminfo
h int h int
w int w int
fini bool fini bool
cells CellBuffer cells CellBuffer
in *os.File in *os.File
out *os.File out *os.File
buffering bool // true if we are collecting writes to buf instead of sending directly to out buffering bool // true if we are collecting writes to buf instead of sending directly to out
buf bytes.Buffer buf bytes.Buffer
curstyle Style curstyle Style
style Style style Style
evch chan Event evch chan Event
sigwinch chan os.Signal sigwinch chan os.Signal
quit chan struct{} quit chan struct{}
indoneq chan struct{} indoneq chan struct{}
keyexist map[Key]bool keyexist map[Key]bool
keycodes map[string]*tKeyCode keycodes map[string]*tKeyCode
keychan chan []byte keychan chan []byte
keytimer *time.Timer keytimer *time.Timer
keyexpire time.Time keyexpire time.Time
cx int cx int
cy int cy int
mouse []byte mouse []byte
clear bool clear bool
cursorx int cursorx int
cursory int cursory int
tiosp *termiosPrivate tiosp *termiosPrivate
wasbtn bool wasbtn bool
acs map[rune]string acs map[rune]string
charset string charset string
encoder transform.Transformer encoder transform.Transformer
decoder transform.Transformer decoder transform.Transformer
fallback map[rune]string fallback map[rune]string
colors map[Color]Color colors map[Color]Color
palette []Color palette []Color
truecolor bool truecolor bool
escaped bool escaped bool
buttondn bool buttondn bool
finiOnce sync.Once finiOnce sync.Once
enablePaste string
disablePaste string
sync.Mutex sync.Mutex
} }
@ -279,6 +281,24 @@ func (t *tScreen) prepareXtermModifiers() {
t.prepareKeyModXTerm(KeyF12, t.ti.KeyF12) t.prepareKeyModXTerm(KeyF12, t.ti.KeyF12)
} }
func (t *tScreen) prepareBracketedPaste() {
// Another workaround for lack of reporting in terminfo.
// We assume if the terminal has a mouse entry, that it
// offers bracketed paste. But we allow specific overrides
// via our terminal database.
if t.ti.EnablePaste != "" {
t.enablePaste = t.ti.EnablePaste
t.disablePaste = t.ti.DisablePaste
t.prepareKey(keyPasteStart, t.ti.PasteStart)
t.prepareKey(keyPasteEnd, t.ti.PasteEnd)
} else if t.ti.MouseMode != "" {
t.enablePaste = "\x1b[?2004h"
t.disablePaste = "\x1b[?2004l"
t.prepareKey(keyPasteStart, "\x1b[200~")
t.prepareKey(keyPasteEnd, "\x1b[201~")
}
}
func (t *tScreen) prepareKey(key Key, val string) { func (t *tScreen) prepareKey(key Key, val string) {
t.prepareKeyMod(key, ModNone, val) t.prepareKeyMod(key, ModNone, val)
} }
@ -414,7 +434,10 @@ func (t *tScreen) prepareKeys() {
t.prepareKey(KeyHome, "\x1bOH") t.prepareKey(KeyHome, "\x1bOH")
} }
t.prepareKey(keyPasteStart, ti.PasteStart)
t.prepareKey(keyPasteEnd, ti.PasteEnd)
t.prepareXtermModifiers() t.prepareXtermModifiers()
t.prepareBracketedPaste()
outer: outer:
// Add key mappings for control keys. // Add key mappings for control keys.
@ -435,7 +458,7 @@ outer:
mod := ModCtrl mod := ModCtrl
switch Key(i) { switch Key(i) {
case KeyBS, KeyTAB, KeyESC, KeyCR: case KeyBS, KeyTAB, KeyESC, KeyCR:
// directly typeable- no control sequence // directly type-able- no control sequence
mod = ModNone mod = ModNone
} }
t.keycodes[string(rune(i))] = &tKeyCode{key: Key(i), mod: mod} t.keycodes[string(rune(i))] = &tKeyCode{key: Key(i), mod: mod}
@ -458,6 +481,7 @@ func (t *tScreen) finish() {
t.TPuts(ti.ExitCA) t.TPuts(ti.ExitCA)
t.TPuts(ti.ExitKeypad) t.TPuts(ti.ExitKeypad)
t.TPuts(ti.TParm(ti.MouseMode, 0)) t.TPuts(ti.TParm(ti.MouseMode, 0))
t.TPuts(t.disablePaste)
t.curstyle = styleInvalid t.curstyle = styleInvalid
t.clear = false t.clear = false
t.fini = true t.fini = true
@ -681,8 +705,6 @@ func (t *tScreen) drawCell(x, y int) int {
t.cx = -1 t.cx = -1
} }
// XXX: check for hazeltine not being able to display ~
if x > t.w-width { if x > t.w-width {
// too wide to fit; emit a single space instead // too wide to fit; emit a single space instead
width = 1 width = 1
@ -731,9 +753,9 @@ func (t *tScreen) showCursor() {
// write operation at some point later. // write operation at some point later.
func (t *tScreen) writeString(s string) { func (t *tScreen) writeString(s string) {
if t.buffering { if t.buffering {
io.WriteString(&t.buf, s) _, _ = io.WriteString(&t.buf, s)
} else { } else {
io.WriteString(t.out, s) _, _ = io.WriteString(t.out, s)
} }
} }
@ -809,7 +831,7 @@ func (t *tScreen) draw() {
// restore the cursor // restore the cursor
t.showCursor() t.showCursor()
t.buf.WriteTo(t.out) _, _ = t.buf.WriteTo(t.out)
} }
func (t *tScreen) EnableMouse() { func (t *tScreen) EnableMouse() {
@ -824,6 +846,14 @@ func (t *tScreen) DisableMouse() {
} }
} }
func (t *tScreen) EnablePaste() {
t.TPuts(t.enablePaste)
}
func (t *tScreen) DisablePaste() {
t.TPuts(t.disablePaste)
}
func (t *tScreen) Size() (int, int) { func (t *tScreen) Size() (int, int) {
t.Lock() t.Lock()
w, h := t.w, t.h w, h := t.w, t.h
@ -842,7 +872,7 @@ func (t *tScreen) resize() {
t.h = h t.h = h
t.w = w t.w = w
ev := NewEventResize(w, h) ev := NewEventResize(w, h)
t.PostEvent(ev) _ = t.PostEvent(ev)
} }
} }
} }
@ -1137,7 +1167,7 @@ func (t *tScreen) parseSgrMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
} }
// consume the event bytes // consume the event bytes
for i >= 0 { for i >= 0 {
buf.ReadByte() _, _ = buf.ReadByte()
i-- i--
} }
*evs = append(*evs, t.buildMouseEvent(x, y, btn)) *evs = append(*evs, t.buildMouseEvent(x, y, btn))
@ -1145,7 +1175,7 @@ func (t *tScreen) parseSgrMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
} }
} }
// incomplete & inconclusve at this point // incomplete & inconclusive at this point
return true, false return true, false
} }
@ -1190,7 +1220,7 @@ func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool)
case 5: case 5:
y = int(b[i]) - 32 - 1 y = int(b[i]) - 32 - 1
for i >= 0 { for i >= 0 {
buf.ReadByte() _, _ = buf.ReadByte()
i-- i--
} }
*evs = append(*evs, t.buildMouseEvent(x, y, btn)) *evs = append(*evs, t.buildMouseEvent(x, y, btn))
@ -1219,9 +1249,16 @@ func (t *tScreen) parseFunctionKey(buf *bytes.Buffer, evs *[]Event) (bool, bool)
mod |= ModAlt mod |= ModAlt
t.escaped = false t.escaped = false
} }
*evs = append(*evs, NewEventKey(k.key, r, mod)) switch k.key {
case keyPasteStart:
*evs = append(*evs, NewEventPaste(true))
case keyPasteEnd:
*evs = append(*evs, NewEventPaste(false))
default:
*evs = append(*evs, NewEventKey(k.key, r, mod))
}
for i := 0; i < len(esc); i++ { for i := 0; i < len(esc); i++ {
buf.ReadByte() _, _ = buf.ReadByte()
} }
return true, true return true, true
} }
@ -1242,7 +1279,7 @@ func (t *tScreen) parseRune(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
t.escaped = false t.escaped = false
} }
*evs = append(*evs, NewEventKey(KeyRune, rune(b[0]), mod)) *evs = append(*evs, NewEventKey(KeyRune, rune(b[0]), mod))
buf.ReadByte() _, _ = buf.ReadByte()
return true, true return true, true
} }
@ -1251,15 +1288,15 @@ func (t *tScreen) parseRune(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
return false, false return false, false
} }
utfb := make([]byte, 12) utf := make([]byte, 12)
for l := 1; l <= len(b); l++ { for l := 1; l <= len(b); l++ {
t.decoder.Reset() t.decoder.Reset()
nout, nin, e := t.decoder.Transform(utfb, b[:l], true) nOut, nIn, e := t.decoder.Transform(utf, b[:l], true)
if e == transform.ErrShortSrc { if e == transform.ErrShortSrc {
continue continue
} }
if nout != 0 { if nOut != 0 {
r, _ := utf8.DecodeRune(utfb[:nout]) r, _ := utf8.DecodeRune(utf[:nOut])
if r != utf8.RuneError { if r != utf8.RuneError {
mod := ModNone mod := ModNone
if t.escaped { if t.escaped {
@ -1268,9 +1305,9 @@ func (t *tScreen) parseRune(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
} }
*evs = append(*evs, NewEventKey(KeyRune, r, mod)) *evs = append(*evs, NewEventKey(KeyRune, r, mod))
} }
for nin > 0 { for nIn > 0 {
buf.ReadByte() _, _ = buf.ReadByte()
nin-- nIn--
} }
return true, true return true, true
} }
@ -1343,7 +1380,7 @@ func (t *tScreen) collectEventsFromInput(buf *bytes.Buffer, expire bool) []Event
} else { } else {
t.escaped = true t.escaped = true
} }
buf.ReadByte() _, _ = buf.ReadByte()
continue continue
} }
// Nothing was going to match, or we timed out // Nothing was going to match, or we timed out
@ -1430,7 +1467,7 @@ func (t *tScreen) inputLoop() {
case io.EOF: case io.EOF:
case nil: case nil:
default: default:
t.PostEvent(NewEventError(e)) _ = t.PostEvent(NewEventError(e))
return return
} }
t.keychan <- chunk[:n] t.keychan <- chunk[:n]