mirror of https://github.com/gdamore/tcell.git
fixes #561 Add clipboard support.
This is not supported for Windows or WebAssembly yet. It's possible for applications to post to the clipboard using Screen.SetClipboard (any data), and they can retrieve the clipboard (if permitted) using GetClipboard. The terminal may well reject either of these. Retrieval will arrive as a new EventClipboard, if it can. (There is no good way to make this synchronous.) This work was inspired by a PR submitted by Consolatis (#562), and has some work based on it, but it was also substantially improved and now includes both sides of the clipboard access pattern.
This commit is contained in:
parent
feef990b56
commit
78110e30f8
|
@ -0,0 +1,115 @@
|
||||||
|
//go:build ignore
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
|
// Copyright 2024 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/gdamore/tcell/v2/encoding"
|
||||||
|
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func emitStr(s tcell.Screen, x, y int, style tcell.Style, str string) {
|
||||||
|
for _, c := range str {
|
||||||
|
var comb []rune
|
||||||
|
w := runewidth.RuneWidth(c)
|
||||||
|
if w == 0 {
|
||||||
|
comb = []rune{c}
|
||||||
|
c = ' '
|
||||||
|
w = 1
|
||||||
|
}
|
||||||
|
s.SetContent(x, y, c, comb, style)
|
||||||
|
x += w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var clipboard []byte
|
||||||
|
|
||||||
|
func displayHelloWorld(s tcell.Screen) {
|
||||||
|
w, h := s.Size()
|
||||||
|
s.Clear()
|
||||||
|
style := tcell.StyleDefault.Foreground(tcell.ColorCadetBlue.TrueColor()).Background(tcell.ColorWhite)
|
||||||
|
emitStr(s, w/2-14, h/2, style, "Press 1 to set clipboard")
|
||||||
|
emitStr(s, w/2-14, h/2+1, style, "Press 2 to get clipboard")
|
||||||
|
|
||||||
|
msg := ""
|
||||||
|
if utf8.Valid(clipboard) {
|
||||||
|
cp := string(clipboard)
|
||||||
|
if len(cp) >= w-25 {
|
||||||
|
cp = cp[:21] + " ..."
|
||||||
|
}
|
||||||
|
msg = fmt.Sprintf("Clipboard (%d bytes): %s", len(clipboard), cp)
|
||||||
|
} else if clipboard != nil {
|
||||||
|
msg = fmt.Sprintf("Clipboard (%d bytes) Not Valid UTF-8", len(clipboard))
|
||||||
|
} else {
|
||||||
|
msg = "No clipboard data"
|
||||||
|
}
|
||||||
|
emitStr(s, (w-len(msg))/2, h/2+3, tcell.StyleDefault, msg)
|
||||||
|
emitStr(s, w/2-9, h/2+5, tcell.StyleDefault, "Press ESC to exit.")
|
||||||
|
s.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This program just prints "Hello, World!". Press ESC to exit.
|
||||||
|
func main() {
|
||||||
|
encoding.Register()
|
||||||
|
|
||||||
|
s, e := tcell.NewScreen()
|
||||||
|
if e != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", e)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if e := s.Init(); e != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", e)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
defStyle := tcell.StyleDefault.
|
||||||
|
Background(tcell.ColorBlack).
|
||||||
|
Foreground(tcell.ColorWhite)
|
||||||
|
s.SetStyle(defStyle)
|
||||||
|
|
||||||
|
displayHelloWorld(s)
|
||||||
|
|
||||||
|
for {
|
||||||
|
switch ev := s.PollEvent().(type) {
|
||||||
|
case *tcell.EventResize:
|
||||||
|
s.Sync()
|
||||||
|
displayHelloWorld(s)
|
||||||
|
case *tcell.EventKey:
|
||||||
|
switch ev.Key() {
|
||||||
|
case tcell.KeyRune:
|
||||||
|
switch ev.Rune() {
|
||||||
|
case '1':
|
||||||
|
s.SetClipboard([]byte("Enjoy your new clipboard content!"))
|
||||||
|
case '2':
|
||||||
|
s.GetClipboard()
|
||||||
|
}
|
||||||
|
case tcell.KeyEscape:
|
||||||
|
s.Fini()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
case *tcell.EventClipboard:
|
||||||
|
clipboard = ev.Data()
|
||||||
|
displayHelloWorld(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1312,6 +1312,12 @@ func (s *cScreen) HasMouse() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *cScreen) SetClipboard(_ []byte) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *cScreen) GetClipboard() {
|
||||||
|
}
|
||||||
|
|
||||||
func (s *cScreen) Resize(int, int, int, int) {}
|
func (s *cScreen) Resize(int, int, int, int) {}
|
||||||
|
|
||||||
func (s *cScreen) HasKey(k Key) bool {
|
func (s *cScreen) HasKey(k Key) bool {
|
||||||
|
|
32
paste.go
32
paste.go
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 The TCell Authors
|
// Copyright 2024 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.
|
||||||
|
@ -19,12 +19,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventPaste is used to mark the start and end of a bracketed paste.
|
// 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
|
// An event with .Start() true will be sent to mark the start of a bracketed paste,
|
||||||
// is pasted in. At the end, an event with .Start() false will be sent.
|
// followed by a number of keys (string data) for the content, ending with the
|
||||||
|
// an event with .End() true.
|
||||||
type EventPaste struct {
|
type EventPaste struct {
|
||||||
start bool
|
start bool
|
||||||
t time.Time
|
t time.Time
|
||||||
|
data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// When returns the time when this EventPaste was created.
|
// When returns the time when this EventPaste was created.
|
||||||
|
@ -46,3 +48,25 @@ func (ev *EventPaste) End() bool {
|
||||||
func NewEventPaste(start bool) *EventPaste {
|
func NewEventPaste(start bool) *EventPaste {
|
||||||
return &EventPaste{t: time.Now(), start: start}
|
return &EventPaste{t: time.Now(), start: start}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewEventClipboard returns a new NewEventClipboard with a data payload
|
||||||
|
func NewEventClipboard(data []byte) *EventClipboard {
|
||||||
|
return &EventClipboard{t: time.Now(), data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventClipboard represents data from the clipboard,
|
||||||
|
// in response to a GetClipboard request.
|
||||||
|
type EventClipboard struct {
|
||||||
|
t time.Time
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data returns the attached binary data.
|
||||||
|
func (ev *EventClipboard) Data() []byte {
|
||||||
|
return ev.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// When returns the time when this event was created.
|
||||||
|
func (ev *EventClipboard) When() time.Time {
|
||||||
|
return ev.t
|
||||||
|
}
|
||||||
|
|
13
screen.go
13
screen.go
|
@ -272,6 +272,17 @@ type Screen interface {
|
||||||
// Tcell may attempt to save and restore the window title on entry and exit, but
|
// Tcell may attempt to save and restore the window title on entry and exit, but
|
||||||
// the results may vary. Use of unicode characters may not be supported.
|
// the results may vary. Use of unicode characters may not be supported.
|
||||||
SetTitle(string)
|
SetTitle(string)
|
||||||
|
|
||||||
|
// SetClipboard is used to post arbitrary data to the system clipboard.
|
||||||
|
// This need not be UTF-8 string data. It's up to the recipient to decode the
|
||||||
|
// data meaningfully. Terminals may prevent this for security reasons.
|
||||||
|
SetClipboard([]byte)
|
||||||
|
|
||||||
|
// GetClipboard is used to request the clipboard contents. It may be ignored.
|
||||||
|
// If the terminal is willing, it will be post the clipboard contents using an
|
||||||
|
// EventPaste with the clipboard content as the Data() field. Terminals may
|
||||||
|
// prevent this for security reasons.
|
||||||
|
GetClipboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScreen returns a default Screen suitable for the user's terminal
|
// NewScreen returns a default Screen suitable for the user's terminal
|
||||||
|
@ -343,6 +354,8 @@ type screenImpl interface {
|
||||||
SetSize(int, int)
|
SetSize(int, int)
|
||||||
SetTitle(string)
|
SetTitle(string)
|
||||||
Tty() (Tty, bool)
|
Tty() (Tty, bool)
|
||||||
|
SetClipboard([]byte)
|
||||||
|
GetClipboard()
|
||||||
|
|
||||||
// Following methods are not part of the Screen api, but are used for interaction with
|
// Following methods are not part of the Screen api, but are used for interaction with
|
||||||
// the common layer code.
|
// the common layer code.
|
||||||
|
|
|
@ -61,8 +61,11 @@ type SimulationScreen interface {
|
||||||
// GetCursor returns the cursor details.
|
// GetCursor returns the cursor details.
|
||||||
GetCursor() (x int, y int, visible bool)
|
GetCursor() (x int, y int, visible bool)
|
||||||
|
|
||||||
// GetTitle gets the set title
|
// GetTitle gets the previously set title.
|
||||||
GetTitle() string
|
GetTitle() string
|
||||||
|
|
||||||
|
// GetClipboardData gets the actual data for the clipboard.
|
||||||
|
GetClipboardData() []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimCell represents a simulated screen cell. The purpose of this
|
// SimCell represents a simulated screen cell. The purpose of this
|
||||||
|
@ -102,6 +105,7 @@ type simscreen struct {
|
||||||
fillstyle Style
|
fillstyle Style
|
||||||
fallback map[rune]string
|
fallback map[rune]string
|
||||||
title string
|
title string
|
||||||
|
clipboard []byte
|
||||||
|
|
||||||
Screen
|
Screen
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
@ -507,3 +511,18 @@ func (s *simscreen) SetTitle(title string) {
|
||||||
func (s *simscreen) GetTitle() string {
|
func (s *simscreen) GetTitle() string {
|
||||||
return s.title
|
return s.title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *simscreen) SetClipboard(data []byte) {
|
||||||
|
s.clipboard = data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *simscreen) GetClipboard() {
|
||||||
|
if s.clipboard != nil {
|
||||||
|
ev := NewEventClipboard(s.clipboard)
|
||||||
|
s.postEvent(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *simscreen) GetClipboardData() []byte {
|
||||||
|
return s.clipboard
|
||||||
|
}
|
||||||
|
|
94
tscreen.go
94
tscreen.go
|
@ -19,6 +19,7 @@ package tcell
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -175,6 +176,7 @@ type tScreen struct {
|
||||||
saveTitle string
|
saveTitle string
|
||||||
restoreTitle string
|
restoreTitle string
|
||||||
title string
|
title string
|
||||||
|
setClipboard string
|
||||||
|
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
@ -447,7 +449,13 @@ func (t *tScreen) prepareExtendedOSC() {
|
||||||
t.restoreTitle = "\x1b[23;2t"
|
t.restoreTitle = "\x1b[23;2t"
|
||||||
// this also tries to request that UTF-8 is allowed in the title
|
// this also tries to request that UTF-8 is allowed in the title
|
||||||
t.setTitle = "\x1b[>2t\x1b]2;%p1%s\x1b\\"
|
t.setTitle = "\x1b[>2t\x1b]2;%p1%s\x1b\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.setClipboard == "" && t.ti.XTermLike {
|
||||||
|
// this string takes a base64 string and sends it to the clipboard.
|
||||||
|
// it will also be able to retrieve the clipboard using "?" as the
|
||||||
|
// sent string, when we support that.
|
||||||
|
t.setClipboard = "\x1b]52;c;%p1%s\x1b\\"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,6 +507,11 @@ func (t *tScreen) prepareKey(key Key, val string) {
|
||||||
|
|
||||||
func (t *tScreen) prepareKeys() {
|
func (t *tScreen) prepareKeys() {
|
||||||
ti := t.ti
|
ti := t.ti
|
||||||
|
if strings.HasPrefix(ti.Name, "xterm") {
|
||||||
|
// assume its some form of XTerm clone
|
||||||
|
t.ti.XTermLike = true
|
||||||
|
ti.XTermLike = true
|
||||||
|
}
|
||||||
t.prepareKey(KeyBackspace, ti.KeyBackspace)
|
t.prepareKey(KeyBackspace, ti.KeyBackspace)
|
||||||
t.prepareKey(KeyF1, ti.KeyF1)
|
t.prepareKey(KeyF1, ti.KeyF1)
|
||||||
t.prepareKey(KeyF2, ti.KeyF2)
|
t.prepareKey(KeyF2, ti.KeyF2)
|
||||||
|
@ -1499,6 +1512,61 @@ func (t *tScreen) parseFocus(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
|
||||||
return true, false
|
return true, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *tScreen) parseClipboard(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
|
||||||
|
b := buf.Bytes()
|
||||||
|
state := 0
|
||||||
|
prefix := []byte("\x1b]52;c;")
|
||||||
|
|
||||||
|
if len(prefix) >= len(b) {
|
||||||
|
if bytes.HasPrefix(prefix, b) {
|
||||||
|
// inconclusive so far
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
// definitely not a match
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
b = b[len(prefix):]
|
||||||
|
|
||||||
|
for _, c := range b {
|
||||||
|
// valid base64 digits
|
||||||
|
if (state == 0) {
|
||||||
|
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c == '+') || (c == '/') || (c == '=') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (c == '\x1b') {
|
||||||
|
state = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (c == '\a') {
|
||||||
|
// matched with BEL instead of ST
|
||||||
|
b = b[:len(b)-1] // drop the trailing BEL
|
||||||
|
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
|
||||||
|
if num, err := base64.StdEncoding.Decode(decoded, b); err == nil {
|
||||||
|
*evs = append(*evs, NewEventClipboard(decoded[:num]))
|
||||||
|
}
|
||||||
|
_, _ = buf.ReadBytes('\a')
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
if (state == 1) {
|
||||||
|
if (c == '\\') {
|
||||||
|
b = b[:len(b)-2] // drop the trailing ST (\x1b\\)
|
||||||
|
// now decode the data
|
||||||
|
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
|
||||||
|
if num, err := base64.StdEncoding.Decode(decoded, b); err == nil {
|
||||||
|
*evs = append(*evs, NewEventClipboard(decoded[:num]))
|
||||||
|
}
|
||||||
|
_, _ = buf.ReadBytes('\\')
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// not enough data yet (not terminated)
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
|
||||||
// parseXtermMouse is like parseSgrMouse, but it parses a legacy
|
// parseXtermMouse is like parseSgrMouse, but it parses a legacy
|
||||||
// X11 mouse record.
|
// X11 mouse record.
|
||||||
func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
|
func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
|
||||||
|
@ -1702,6 +1770,14 @@ func (t *tScreen) collectEventsFromInput(buf *bytes.Buffer, expire bool) []Event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.setClipboard != "" {
|
||||||
|
if part, comp := t.parseClipboard(buf, &res); comp {
|
||||||
|
continue
|
||||||
|
} else if part {
|
||||||
|
partials++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if partials == 0 || expire {
|
if partials == 0 || expire {
|
||||||
if b[0] == '\x1b' {
|
if b[0] == '\x1b' {
|
||||||
if len(b) == 1 {
|
if len(b) == 1 {
|
||||||
|
@ -2053,3 +2129,21 @@ func (t *tScreen) SetTitle(title string) {
|
||||||
}
|
}
|
||||||
t.Unlock()
|
t.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *tScreen) SetClipboard(data []byte) {
|
||||||
|
// Post binary data to the system clipboard. It might be UTF-8, it might not be.
|
||||||
|
t.Lock()
|
||||||
|
if t.setClipboard != "" {
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(data)
|
||||||
|
t.TPuts(t.ti.TParm(t.setClipboard, encoded))
|
||||||
|
}
|
||||||
|
t.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tScreen) GetClipboard() {
|
||||||
|
t.Lock()
|
||||||
|
if t.setClipboard != "" {
|
||||||
|
t.TPuts(t.ti.TParm(t.setClipboard, "?"))
|
||||||
|
}
|
||||||
|
t.Unlock()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue