Skip to content

fixes #561 Add clipboard support. #714

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions _demos/clipboard.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
6 changes: 6 additions & 0 deletions console_win.go
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,12 @@ func (s *cScreen) HasMouse() bool {
return true
}

func (s *cScreen) SetClipboard(_ []byte) {
}

func (s *cScreen) GetClipboard() {
}

func (s *cScreen) Resize(int, int, int, int) {}

func (s *cScreen) HasKey(k Key) bool {
Expand Down
32 changes: 28 additions & 4 deletions paste.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 The TCell Authors
// 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.
Expand All @@ -19,12 +19,14 @@ import (
)

// 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.
//
// An event with .Start() true will be sent to mark the start of a bracketed paste,
// followed by a number of keys (string data) for the content, ending with the
// an event with .End() true.
type EventPaste struct {
start bool
t time.Time
data []byte
}

// When returns the time when this EventPaste was created.
Expand All @@ -46,3 +48,25 @@ func (ev *EventPaste) End() bool {
func NewEventPaste(start bool) *EventPaste {
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 changes: 13 additions & 0 deletions screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,17 @@ type Screen interface {
// 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.
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
Expand Down Expand Up @@ -343,6 +354,8 @@ type screenImpl interface {
SetSize(int, int)
SetTitle(string)
Tty() (Tty, bool)
SetClipboard([]byte)
GetClipboard()

// Following methods are not part of the Screen api, but are used for interaction with
// the common layer code.
Expand Down
21 changes: 20 additions & 1 deletion simulation.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@ type SimulationScreen interface {
// GetCursor returns the cursor details.
GetCursor() (x int, y int, visible bool)

// GetTitle gets the set title
// GetTitle gets the previously set title.
GetTitle() string

// GetClipboardData gets the actual data for the clipboard.
GetClipboardData() []byte
}

// SimCell represents a simulated screen cell. The purpose of this
Expand Down Expand Up @@ -102,6 +105,7 @@ type simscreen struct {
fillstyle Style
fallback map[rune]string
title string
clipboard []byte

Screen
sync.Mutex
Expand Down Expand Up @@ -507,3 +511,18 @@ func (s *simscreen) SetTitle(title string) {
func (s *simscreen) GetTitle() string {
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 changes: 94 additions & 0 deletions tscreen.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package tcell

import (
"bytes"
"encoding/base64"
"errors"
"io"
"os"
Expand Down Expand Up @@ -175,6 +176,7 @@ type tScreen struct {
saveTitle string
restoreTitle string
title string
setClipboard string

sync.Mutex
}
Expand Down Expand Up @@ -447,7 +449,13 @@ func (t *tScreen) prepareExtendedOSC() {
t.restoreTitle = "\x1b[23;2t"
// this also tries to request that UTF-8 is allowed in the title
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\\"
}
}

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

func (t *tScreen) prepareKeys() {
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(KeyF1, ti.KeyF1)
t.prepareKey(KeyF2, ti.KeyF2)
Expand Down Expand Up @@ -1499,6 +1512,61 @@ func (t *tScreen) parseFocus(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
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
// X11 mouse record.
func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
Expand Down Expand Up @@ -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 b[0] == '\x1b' {
if len(b) == 1 {
Expand Down Expand Up @@ -2053,3 +2129,21 @@ func (t *tScreen) SetTitle(title string) {
}
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()
}