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:
parent
aeb3a11948
commit
197faf3eae
@ -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()
|
||||
@ -206,7 +232,7 @@ func main() {
|
||||
switch ev.Buttons() {
|
||||
case tcell.ButtonNone:
|
||||
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,
|
||||
up.Background(bg),
|
||||
lchar)
|
||||
|
@ -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
6
key.go
@ -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
48
paste.go
Normal 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}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
163
tscreen.go
163
tscreen.go
@ -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.
|
||||
//
|
||||
@ -75,45 +75,47 @@ type tKeyCode struct {
|
||||
|
||||
// tScreen represents a screen backed by a terminfo implementation.
|
||||
type tScreen struct {
|
||||
ti *terminfo.Terminfo
|
||||
h int
|
||||
w int
|
||||
fini bool
|
||||
cells CellBuffer
|
||||
in *os.File
|
||||
out *os.File
|
||||
buffering bool // true if we are collecting writes to buf instead of sending directly to out
|
||||
buf bytes.Buffer
|
||||
curstyle Style
|
||||
style Style
|
||||
evch chan Event
|
||||
sigwinch chan os.Signal
|
||||
quit chan struct{}
|
||||
indoneq chan struct{}
|
||||
keyexist map[Key]bool
|
||||
keycodes map[string]*tKeyCode
|
||||
keychan chan []byte
|
||||
keytimer *time.Timer
|
||||
keyexpire time.Time
|
||||
cx int
|
||||
cy int
|
||||
mouse []byte
|
||||
clear bool
|
||||
cursorx int
|
||||
cursory int
|
||||
tiosp *termiosPrivate
|
||||
wasbtn bool
|
||||
acs map[rune]string
|
||||
charset string
|
||||
encoder transform.Transformer
|
||||
decoder transform.Transformer
|
||||
fallback map[rune]string
|
||||
colors map[Color]Color
|
||||
palette []Color
|
||||
truecolor bool
|
||||
escaped bool
|
||||
buttondn bool
|
||||
finiOnce sync.Once
|
||||
ti *terminfo.Terminfo
|
||||
h int
|
||||
w int
|
||||
fini bool
|
||||
cells CellBuffer
|
||||
in *os.File
|
||||
out *os.File
|
||||
buffering bool // true if we are collecting writes to buf instead of sending directly to out
|
||||
buf bytes.Buffer
|
||||
curstyle Style
|
||||
style Style
|
||||
evch chan Event
|
||||
sigwinch chan os.Signal
|
||||
quit chan struct{}
|
||||
indoneq chan struct{}
|
||||
keyexist map[Key]bool
|
||||
keycodes map[string]*tKeyCode
|
||||
keychan chan []byte
|
||||
keytimer *time.Timer
|
||||
keyexpire time.Time
|
||||
cx int
|
||||
cy int
|
||||
mouse []byte
|
||||
clear bool
|
||||
cursorx int
|
||||
cursory int
|
||||
tiosp *termiosPrivate
|
||||
wasbtn bool
|
||||
acs map[rune]string
|
||||
charset string
|
||||
encoder transform.Transformer
|
||||
decoder transform.Transformer
|
||||
fallback map[rune]string
|
||||
colors map[Color]Color
|
||||
palette []Color
|
||||
truecolor bool
|
||||
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
|
||||
}
|
||||
*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++ {
|
||||
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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user