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)
s.SetStyle(defStyle)
s.EnableMouse()
s.EnablePaste()
s.Clear()
posfmt := "Mouse: %d, %d "
btnfmt := "Buttons: %s"
keyfmt := "Keys: %s"
pastefmt := "Paste: [%d] %s"
white := tcell.StyleDefault.
Foreground(tcell.ColorWhite).Background(tcell.ColorRed)
@ -131,15 +133,23 @@ func main() {
lchar := '*'
bstr := ""
lks := ""
pstr := ""
ecnt := 0
pasting := false
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, 3, white, fmt.Sprintf(posfmt, mx, my))
emitStr(s, 2, 4, white, fmt.Sprintf(btnfmt, bstr))
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()
bstr = ""
ev := s.PollEvent()
@ -160,6 +170,17 @@ func main() {
s.SetContent(w-1, h-1, 'R', nil, st)
case *tcell.EventKey:
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)
if ev.Key() == tcell.KeyEscape {
ecnt++
@ -176,6 +197,11 @@ func main() {
}
}
lks = ev.Name()
case *tcell.EventPaste:
pasting = ev.Start()
if pasting {
pstr = ""
}
case *tcell.EventMouse:
x, y := ev.Position()
button := ev.Buttons()

View File

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

6
key.go
View File

@ -375,6 +375,12 @@ const (
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,
// perhaps. For example, KeyCtrlH is the same as KeyBackspace.
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()
// EnablePaste enables bracketed paste mode, if supported.
EnablePaste()
// DisablePaste() disables bracketed paste mode.
DisablePaste()
// HasMouse returns true if the terminal (apparently) supports a
// mouse. Note that the a return value of true doesn't guarantee that
// 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");
// you may not use file except in compliance with the License.
@ -97,6 +97,7 @@ type simscreen struct {
cursory int
cursorvis bool
mouse bool
paste bool
charset string
encoder transform.Transformer
decoder transform.Transformer
@ -321,6 +322,14 @@ func (s *simscreen) DisableMouse() {
s.mouse = false
}
func (s *simscreen) EnablePaste() {
s.paste = true
}
func (s *simscreen) DisablePaste() {
s.paste = false
}
func (s *simscreen) Size() (int, int) {
s.Lock()
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");
// 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^"
}
// 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
// the codes to enter and exit mouse mode. Sadly, this is not
// part of the terminfo databases anywhere that I've found, but

View File

@ -213,6 +213,10 @@ type Terminfo struct {
KeyAltShfEnd string
KeyMetaShfHome string
KeyMetaShfEnd string
EnablePaste string // bracketed paste mode
DisablePaste string
PasteStart string
PasteEnd string
Modifiers int
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");
// 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
// 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
// is not supported for any reason.
//
@ -114,6 +114,8 @@ type tScreen struct {
escaped bool
buttondn bool
finiOnce sync.Once
enablePaste string
disablePaste string
sync.Mutex
}
@ -279,6 +281,24 @@ func (t *tScreen) prepareXtermModifiers() {
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) {
t.prepareKeyMod(key, ModNone, val)
}
@ -414,7 +434,10 @@ func (t *tScreen) prepareKeys() {
t.prepareKey(KeyHome, "\x1bOH")
}
t.prepareKey(keyPasteStart, ti.PasteStart)
t.prepareKey(keyPasteEnd, ti.PasteEnd)
t.prepareXtermModifiers()
t.prepareBracketedPaste()
outer:
// Add key mappings for control keys.
@ -435,7 +458,7 @@ outer:
mod := ModCtrl
switch Key(i) {
case KeyBS, KeyTAB, KeyESC, KeyCR:
// directly typeable- no control sequence
// directly type-able- no control sequence
mod = ModNone
}
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.ExitKeypad)
t.TPuts(ti.TParm(ti.MouseMode, 0))
t.TPuts(t.disablePaste)
t.curstyle = styleInvalid
t.clear = false
t.fini = true
@ -681,8 +705,6 @@ func (t *tScreen) drawCell(x, y int) int {
t.cx = -1
}
// XXX: check for hazeltine not being able to display ~
if x > t.w-width {
// too wide to fit; emit a single space instead
width = 1
@ -731,9 +753,9 @@ func (t *tScreen) showCursor() {
// write operation at some point later.
func (t *tScreen) writeString(s string) {
if t.buffering {
io.WriteString(&t.buf, s)
_, _ = io.WriteString(&t.buf, s)
} else {
io.WriteString(t.out, s)
_, _ = io.WriteString(t.out, s)
}
}
@ -809,7 +831,7 @@ func (t *tScreen) draw() {
// restore the cursor
t.showCursor()
t.buf.WriteTo(t.out)
_, _ = t.buf.WriteTo(t.out)
}
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) {
t.Lock()
w, h := t.w, t.h
@ -842,7 +872,7 @@ func (t *tScreen) resize() {
t.h = h
t.w = w
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
for i >= 0 {
buf.ReadByte()
_, _ = buf.ReadByte()
i--
}
*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
}
@ -1190,7 +1220,7 @@ func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool)
case 5:
y = int(b[i]) - 32 - 1
for i >= 0 {
buf.ReadByte()
_, _ = buf.ReadByte()
i--
}
*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
t.escaped = false
}
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++ {
buf.ReadByte()
_, _ = buf.ReadByte()
}
return true, true
}
@ -1242,7 +1279,7 @@ func (t *tScreen) parseRune(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
t.escaped = false
}
*evs = append(*evs, NewEventKey(KeyRune, rune(b[0]), mod))
buf.ReadByte()
_, _ = buf.ReadByte()
return true, true
}
@ -1251,15 +1288,15 @@ func (t *tScreen) parseRune(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
return false, false
}
utfb := make([]byte, 12)
utf := make([]byte, 12)
for l := 1; l <= len(b); l++ {
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 {
continue
}
if nout != 0 {
r, _ := utf8.DecodeRune(utfb[:nout])
if nOut != 0 {
r, _ := utf8.DecodeRune(utf[:nOut])
if r != utf8.RuneError {
mod := ModNone
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))
}
for nin > 0 {
buf.ReadByte()
nin--
for nIn > 0 {
_, _ = buf.ReadByte()
nIn--
}
return true, true
}
@ -1343,7 +1380,7 @@ func (t *tScreen) collectEventsFromInput(buf *bytes.Buffer, expire bool) []Event
} else {
t.escaped = true
}
buf.ReadByte()
_, _ = buf.ReadByte()
continue
}
// Nothing was going to match, or we timed out
@ -1430,7 +1467,7 @@ func (t *tScreen) inputLoop() {
case io.EOF:
case nil:
default:
t.PostEvent(NewEventError(e))
_ = t.PostEvent(NewEventError(e))
return
}
t.keychan <- chunk[:n]