Skip to content

Commit 78110e3

Browse files
committed
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.
1 parent feef990 commit 78110e3

File tree

6 files changed

+276
-5
lines changed

6 files changed

+276
-5
lines changed

_demos/clipboard.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//go:build ignore
2+
// +build ignore
3+
4+
// Copyright 2024 The TCell Authors
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use file except in compliance with the License.
8+
// You may obtain a copy of the license at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
18+
package main
19+
20+
import (
21+
"fmt"
22+
"os"
23+
"unicode/utf8"
24+
25+
"github.com/gdamore/tcell/v2"
26+
"github.com/gdamore/tcell/v2/encoding"
27+
28+
"github.com/mattn/go-runewidth"
29+
)
30+
31+
func emitStr(s tcell.Screen, x, y int, style tcell.Style, str string) {
32+
for _, c := range str {
33+
var comb []rune
34+
w := runewidth.RuneWidth(c)
35+
if w == 0 {
36+
comb = []rune{c}
37+
c = ' '
38+
w = 1
39+
}
40+
s.SetContent(x, y, c, comb, style)
41+
x += w
42+
}
43+
}
44+
45+
var clipboard []byte
46+
47+
func displayHelloWorld(s tcell.Screen) {
48+
w, h := s.Size()
49+
s.Clear()
50+
style := tcell.StyleDefault.Foreground(tcell.ColorCadetBlue.TrueColor()).Background(tcell.ColorWhite)
51+
emitStr(s, w/2-14, h/2, style, "Press 1 to set clipboard")
52+
emitStr(s, w/2-14, h/2+1, style, "Press 2 to get clipboard")
53+
54+
msg := ""
55+
if utf8.Valid(clipboard) {
56+
cp := string(clipboard)
57+
if len(cp) >= w-25 {
58+
cp = cp[:21] + " ..."
59+
}
60+
msg = fmt.Sprintf("Clipboard (%d bytes): %s", len(clipboard), cp)
61+
} else if clipboard != nil {
62+
msg = fmt.Sprintf("Clipboard (%d bytes) Not Valid UTF-8", len(clipboard))
63+
} else {
64+
msg = "No clipboard data"
65+
}
66+
emitStr(s, (w-len(msg))/2, h/2+3, tcell.StyleDefault, msg)
67+
emitStr(s, w/2-9, h/2+5, tcell.StyleDefault, "Press ESC to exit.")
68+
s.Show()
69+
}
70+
71+
// This program just prints "Hello, World!". Press ESC to exit.
72+
func main() {
73+
encoding.Register()
74+
75+
s, e := tcell.NewScreen()
76+
if e != nil {
77+
fmt.Fprintf(os.Stderr, "%v\n", e)
78+
os.Exit(1)
79+
}
80+
if e := s.Init(); e != nil {
81+
fmt.Fprintf(os.Stderr, "%v\n", e)
82+
os.Exit(1)
83+
}
84+
85+
defStyle := tcell.StyleDefault.
86+
Background(tcell.ColorBlack).
87+
Foreground(tcell.ColorWhite)
88+
s.SetStyle(defStyle)
89+
90+
displayHelloWorld(s)
91+
92+
for {
93+
switch ev := s.PollEvent().(type) {
94+
case *tcell.EventResize:
95+
s.Sync()
96+
displayHelloWorld(s)
97+
case *tcell.EventKey:
98+
switch ev.Key() {
99+
case tcell.KeyRune:
100+
switch ev.Rune() {
101+
case '1':
102+
s.SetClipboard([]byte("Enjoy your new clipboard content!"))
103+
case '2':
104+
s.GetClipboard()
105+
}
106+
case tcell.KeyEscape:
107+
s.Fini()
108+
os.Exit(0)
109+
}
110+
case *tcell.EventClipboard:
111+
clipboard = ev.Data()
112+
displayHelloWorld(s)
113+
}
114+
}
115+
}

console_win.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,6 +1312,12 @@ func (s *cScreen) HasMouse() bool {
13121312
return true
13131313
}
13141314

1315+
func (s *cScreen) SetClipboard(_ []byte) {
1316+
}
1317+
1318+
func (s *cScreen) GetClipboard() {
1319+
}
1320+
13151321
func (s *cScreen) Resize(int, int, int, int) {}
13161322

13171323
func (s *cScreen) HasKey(k Key) bool {

paste.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2020 The TCell Authors
1+
// Copyright 2024 The TCell Authors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use file except in compliance with the License.
@@ -19,12 +19,14 @@ import (
1919
)
2020

2121
// EventPaste is used to mark the start and end of a bracketed paste.
22-
// An event with .Start() true will be sent to mark the start.
23-
// Then a number of keys will be sent to indicate that the content
24-
// is pasted in. At the end, an event with .Start() false will be sent.
22+
//
23+
// An event with .Start() true will be sent to mark the start of a bracketed paste,
24+
// followed by a number of keys (string data) for the content, ending with the
25+
// an event with .End() true.
2526
type EventPaste struct {
2627
start bool
2728
t time.Time
29+
data []byte
2830
}
2931

3032
// When returns the time when this EventPaste was created.
@@ -46,3 +48,25 @@ func (ev *EventPaste) End() bool {
4648
func NewEventPaste(start bool) *EventPaste {
4749
return &EventPaste{t: time.Now(), start: start}
4850
}
51+
52+
// NewEventClipboard returns a new NewEventClipboard with a data payload
53+
func NewEventClipboard(data []byte) *EventClipboard {
54+
return &EventClipboard{t: time.Now(), data: data}
55+
}
56+
57+
// EventClipboard represents data from the clipboard,
58+
// in response to a GetClipboard request.
59+
type EventClipboard struct {
60+
t time.Time
61+
data []byte
62+
}
63+
64+
// Data returns the attached binary data.
65+
func (ev *EventClipboard) Data() []byte {
66+
return ev.data
67+
}
68+
69+
// When returns the time when this event was created.
70+
func (ev *EventClipboard) When() time.Time {
71+
return ev.t
72+
}

screen.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,17 @@ type Screen interface {
272272
// Tcell may attempt to save and restore the window title on entry and exit, but
273273
// the results may vary. Use of unicode characters may not be supported.
274274
SetTitle(string)
275+
276+
// SetClipboard is used to post arbitrary data to the system clipboard.
277+
// This need not be UTF-8 string data. It's up to the recipient to decode the
278+
// data meaningfully. Terminals may prevent this for security reasons.
279+
SetClipboard([]byte)
280+
281+
// GetClipboard is used to request the clipboard contents. It may be ignored.
282+
// If the terminal is willing, it will be post the clipboard contents using an
283+
// EventPaste with the clipboard content as the Data() field. Terminals may
284+
// prevent this for security reasons.
285+
GetClipboard()
275286
}
276287

277288
// NewScreen returns a default Screen suitable for the user's terminal
@@ -343,6 +354,8 @@ type screenImpl interface {
343354
SetSize(int, int)
344355
SetTitle(string)
345356
Tty() (Tty, bool)
357+
SetClipboard([]byte)
358+
GetClipboard()
346359

347360
// Following methods are not part of the Screen api, but are used for interaction with
348361
// the common layer code.

simulation.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ type SimulationScreen interface {
6161
// GetCursor returns the cursor details.
6262
GetCursor() (x int, y int, visible bool)
6363

64-
// GetTitle gets the set title
64+
// GetTitle gets the previously set title.
6565
GetTitle() string
66+
67+
// GetClipboardData gets the actual data for the clipboard.
68+
GetClipboardData() []byte
6669
}
6770

6871
// SimCell represents a simulated screen cell. The purpose of this
@@ -102,6 +105,7 @@ type simscreen struct {
102105
fillstyle Style
103106
fallback map[rune]string
104107
title string
108+
clipboard []byte
105109

106110
Screen
107111
sync.Mutex
@@ -507,3 +511,18 @@ func (s *simscreen) SetTitle(title string) {
507511
func (s *simscreen) GetTitle() string {
508512
return s.title
509513
}
514+
515+
func (s *simscreen) SetClipboard(data []byte) {
516+
s.clipboard = data
517+
}
518+
519+
func (s *simscreen) GetClipboard() {
520+
if s.clipboard != nil {
521+
ev := NewEventClipboard(s.clipboard)
522+
s.postEvent(ev)
523+
}
524+
}
525+
526+
func (s *simscreen) GetClipboardData() []byte {
527+
return s.clipboard
528+
}

tscreen.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package tcell
1919

2020
import (
2121
"bytes"
22+
"encoding/base64"
2223
"errors"
2324
"io"
2425
"os"
@@ -175,6 +176,7 @@ type tScreen struct {
175176
saveTitle string
176177
restoreTitle string
177178
title string
179+
setClipboard string
178180

179181
sync.Mutex
180182
}
@@ -447,7 +449,13 @@ func (t *tScreen) prepareExtendedOSC() {
447449
t.restoreTitle = "\x1b[23;2t"
448450
// this also tries to request that UTF-8 is allowed in the title
449451
t.setTitle = "\x1b[>2t\x1b]2;%p1%s\x1b\\"
452+
}
450453

454+
if t.setClipboard == "" && t.ti.XTermLike {
455+
// this string takes a base64 string and sends it to the clipboard.
456+
// it will also be able to retrieve the clipboard using "?" as the
457+
// sent string, when we support that.
458+
t.setClipboard = "\x1b]52;c;%p1%s\x1b\\"
451459
}
452460
}
453461

@@ -499,6 +507,11 @@ func (t *tScreen) prepareKey(key Key, val string) {
499507

500508
func (t *tScreen) prepareKeys() {
501509
ti := t.ti
510+
if strings.HasPrefix(ti.Name, "xterm") {
511+
// assume its some form of XTerm clone
512+
t.ti.XTermLike = true
513+
ti.XTermLike = true
514+
}
502515
t.prepareKey(KeyBackspace, ti.KeyBackspace)
503516
t.prepareKey(KeyF1, ti.KeyF1)
504517
t.prepareKey(KeyF2, ti.KeyF2)
@@ -1499,6 +1512,61 @@ func (t *tScreen) parseFocus(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
14991512
return true, false
15001513
}
15011514

1515+
func (t *tScreen) parseClipboard(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
1516+
b := buf.Bytes()
1517+
state := 0
1518+
prefix := []byte("\x1b]52;c;")
1519+
1520+
if len(prefix) >= len(b) {
1521+
if bytes.HasPrefix(prefix, b) {
1522+
// inconclusive so far
1523+
return true, false
1524+
}
1525+
// definitely not a match
1526+
return false, false
1527+
}
1528+
b = b[len(prefix):]
1529+
1530+
for _, c := range b {
1531+
// valid base64 digits
1532+
if (state == 0) {
1533+
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c == '+') || (c == '/') || (c == '=') {
1534+
continue
1535+
}
1536+
if (c == '\x1b') {
1537+
state = 1
1538+
continue
1539+
}
1540+
if (c == '\a') {
1541+
// matched with BEL instead of ST
1542+
b = b[:len(b)-1] // drop the trailing BEL
1543+
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
1544+
if num, err := base64.StdEncoding.Decode(decoded, b); err == nil {
1545+
*evs = append(*evs, NewEventClipboard(decoded[:num]))
1546+
}
1547+
_, _ = buf.ReadBytes('\a')
1548+
return true, true
1549+
}
1550+
return false, false
1551+
}
1552+
if (state == 1) {
1553+
if (c == '\\') {
1554+
b = b[:len(b)-2] // drop the trailing ST (\x1b\\)
1555+
// now decode the data
1556+
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
1557+
if num, err := base64.StdEncoding.Decode(decoded, b); err == nil {
1558+
*evs = append(*evs, NewEventClipboard(decoded[:num]))
1559+
}
1560+
_, _ = buf.ReadBytes('\\')
1561+
return true, true
1562+
}
1563+
return false, false
1564+
}
1565+
}
1566+
// not enough data yet (not terminated)
1567+
return true, false
1568+
}
1569+
15021570
// parseXtermMouse is like parseSgrMouse, but it parses a legacy
15031571
// X11 mouse record.
15041572
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
17021770
}
17031771
}
17041772

1773+
if t.setClipboard != "" {
1774+
if part, comp := t.parseClipboard(buf, &res); comp {
1775+
continue
1776+
} else if part {
1777+
partials++
1778+
}
1779+
}
1780+
17051781
if partials == 0 || expire {
17061782
if b[0] == '\x1b' {
17071783
if len(b) == 1 {
@@ -2053,3 +2129,21 @@ func (t *tScreen) SetTitle(title string) {
20532129
}
20542130
t.Unlock()
20552131
}
2132+
2133+
func (t *tScreen) SetClipboard(data []byte) {
2134+
// Post binary data to the system clipboard. It might be UTF-8, it might not be.
2135+
t.Lock()
2136+
if t.setClipboard != "" {
2137+
encoded := base64.StdEncoding.EncodeToString(data)
2138+
t.TPuts(t.ti.TParm(t.setClipboard, encoded))
2139+
}
2140+
t.Unlock()
2141+
}
2142+
2143+
func (t *tScreen) GetClipboard() {
2144+
t.Lock()
2145+
if t.setClipboard != "" {
2146+
t.TPuts(t.ti.TParm(t.setClipboard, "?"))
2147+
}
2148+
t.Unlock()
2149+
}

0 commit comments

Comments
 (0)