diff --git a/_demos/mouse.go b/_demos/mouse.go index 9d501802..9dbaee73 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 8ee5eef3..c2ac7409 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 3545215a..9741e699 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 00000000..71cf8b1c --- /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 c264abbf..a3c9e467 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 80e282d1..7c8d74ba 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 225e8e47..db253ca0 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 06bde8d2..5e875e8b 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 529af421..46740352 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]