From 197faf3eae1cbee2ce063768796efc5498b3ab5c Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Thu, 15 Oct 2020 22:13:08 -0700 Subject: [PATCH] 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(). --- _demos/mouse.go | 30 ++++++- console_win.go | 4 + key.go | 6 ++ paste.go | 48 +++++++++++ screen.go | 6 ++ simulation.go | 11 ++- terminfo/dynamic/dynamic.go | 20 ++++- terminfo/terminfo.go | 4 + tscreen.go | 163 ++++++++++++++++++++++-------------- 9 files changed, 225 insertions(+), 67 deletions(-) create mode 100644 paste.go diff --git a/_demos/mouse.go b/_demos/mouse.go index 9d50180..9dbaee7 100644 --- a/_demos/mouse.go +++ b/_demos/mouse.go @@ -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) diff --git a/console_win.go b/console_win.go index 8ee5eef..c2ac740 100644 --- a/console_win.go +++ b/console_win.go @@ -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) } diff --git a/key.go b/key.go index 3545215..9741e69 100644 --- a/key.go +++ b/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 ( diff --git a/paste.go b/paste.go new file mode 100644 index 0000000..71cf8b1 --- /dev/null +++ b/paste.go @@ -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} +} diff --git a/screen.go b/screen.go index c264abb..a3c9e46 100644 --- a/screen.go +++ b/screen.go @@ -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 diff --git a/simulation.go b/simulation.go index 80e282d..7c8d74b 100644 --- a/simulation.go +++ b/simulation.go @@ -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() diff --git a/terminfo/dynamic/dynamic.go b/terminfo/dynamic/dynamic.go index 225e8e4..db253ca 100644 --- a/terminfo/dynamic/dynamic.go +++ b/terminfo/dynamic/dynamic.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. @@ -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 diff --git a/terminfo/terminfo.go b/terminfo/terminfo.go index 06bde8d..5e875e8 100644 --- a/terminfo/terminfo.go +++ b/terminfo/terminfo.go @@ -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 } diff --git a/tscreen.go b/tscreen.go index 529af42..4674035 100644 --- a/tscreen.go +++ b/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]