osc52.go

  1// OSC52 is a terminal escape sequence that allows copying text to the clipboard.
  2//
  3// The sequence consists of the following:
  4//
  5//	OSC 52 ; Pc ; Pd BEL
  6//
  7// Pc is the clipboard choice:
  8//
  9//	c: clipboard
 10//	p: primary
 11//	q: secondary (not supported)
 12//	s: select (not supported)
 13//	0-7: cut-buffers (not supported)
 14//
 15// Pd is the data to copy to the clipboard. This string should be encoded in
 16// base64 (RFC-4648).
 17//
 18// If Pd is "?", the terminal replies to the host with the current contents of
 19// the clipboard.
 20//
 21// If Pd is neither a base64 string nor "?", the terminal clears the clipboard.
 22//
 23// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
 24// where Ps = 52 => Manipulate Selection Data.
 25//
 26// Examples:
 27//
 28//	// copy "hello world" to the system clipboard
 29//	fmt.Fprint(os.Stderr, osc52.New("hello world"))
 30//
 31//	// copy "hello world" to the primary Clipboard
 32//	fmt.Fprint(os.Stderr, osc52.New("hello world").Primary())
 33//
 34//	// limit the size of the string to copy 10 bytes
 35//	fmt.Fprint(os.Stderr, osc52.New("0123456789").Limit(10))
 36//
 37//	// escape the OSC52 sequence for screen using DCS sequences
 38//	fmt.Fprint(os.Stderr, osc52.New("hello world").Screen())
 39//
 40//	// escape the OSC52 sequence for Tmux
 41//	fmt.Fprint(os.Stderr, osc52.New("hello world").Tmux())
 42//
 43//	// query the system Clipboard
 44//	fmt.Fprint(os.Stderr, osc52.Query())
 45//
 46//	// query the primary clipboard
 47//	fmt.Fprint(os.Stderr, osc52.Query().Primary())
 48//
 49//	// clear the system Clipboard
 50//	fmt.Fprint(os.Stderr, osc52.Clear())
 51//
 52//	// clear the primary Clipboard
 53//	fmt.Fprint(os.Stderr, osc52.Clear().Primary())
 54package osc52
 55
 56import (
 57	"encoding/base64"
 58	"fmt"
 59	"io"
 60	"strings"
 61)
 62
 63// Clipboard is the clipboard buffer to use.
 64type Clipboard rune
 65
 66const (
 67	// SystemClipboard is the system clipboard buffer.
 68	SystemClipboard Clipboard = 'c'
 69	// PrimaryClipboard is the primary clipboard buffer (X11).
 70	PrimaryClipboard = 'p'
 71)
 72
 73// Mode is the mode to use for the OSC52 sequence.
 74type Mode uint
 75
 76const (
 77	// DefaultMode is the default OSC52 sequence mode.
 78	DefaultMode Mode = iota
 79	// ScreenMode escapes the OSC52 sequence for screen using DCS sequences.
 80	ScreenMode
 81	// TmuxMode escapes the OSC52 sequence for tmux. Not needed if tmux
 82	// clipboard is set to `set-clipboard on`
 83	TmuxMode
 84)
 85
 86// Operation is the OSC52 operation.
 87type Operation uint
 88
 89const (
 90	// SetOperation is the copy operation.
 91	SetOperation Operation = iota
 92	// QueryOperation is the query operation.
 93	QueryOperation
 94	// ClearOperation is the clear operation.
 95	ClearOperation
 96)
 97
 98// Sequence is the OSC52 sequence.
 99type Sequence struct {
100	str       string
101	limit     int
102	op        Operation
103	mode      Mode
104	clipboard Clipboard
105}
106
107var _ fmt.Stringer = Sequence{}
108
109var _ io.WriterTo = Sequence{}
110
111// String returns the OSC52 sequence.
112func (s Sequence) String() string {
113	var seq strings.Builder
114	// mode escape sequences start
115	seq.WriteString(s.seqStart())
116	// actual OSC52 sequence start
117	seq.WriteString(fmt.Sprintf("\x1b]52;%c;", s.clipboard))
118	switch s.op {
119	case SetOperation:
120		str := s.str
121		if s.limit > 0 && len(str) > s.limit {
122			return ""
123		}
124		b64 := base64.StdEncoding.EncodeToString([]byte(str))
125		switch s.mode {
126		case ScreenMode:
127			// Screen doesn't support OSC52 but will pass the contents of a DCS
128			// sequence to the outer terminal unchanged.
129			//
130			// Here, we split the encoded string into 76 bytes chunks and then
131			// join the chunks with <end-dsc><start-dsc> sequences. Finally,
132			// wrap the whole thing in
133			// <start-dsc><start-osc52><joined-chunks><end-osc52><end-dsc>.
134			// s := strings.SplitN(b64, "", 76)
135			s := make([]string, 0, len(b64)/76+1)
136			for i := 0; i < len(b64); i += 76 {
137				end := i + 76
138				if end > len(b64) {
139					end = len(b64)
140				}
141				s = append(s, b64[i:end])
142			}
143			seq.WriteString(strings.Join(s, "\x1b\\\x1bP"))
144		default:
145			seq.WriteString(b64)
146		}
147	case QueryOperation:
148		// OSC52 queries the clipboard using "?"
149		seq.WriteString("?")
150	case ClearOperation:
151		// OSC52 clears the clipboard if the data is neither a base64 string nor "?"
152		// we're using "!" as a default
153		seq.WriteString("!")
154	}
155	// actual OSC52 sequence end
156	seq.WriteString("\x07")
157	// mode escape end
158	seq.WriteString(s.seqEnd())
159	return seq.String()
160}
161
162// WriteTo writes the OSC52 sequence to the writer.
163func (s Sequence) WriteTo(out io.Writer) (int64, error) {
164	n, err := out.Write([]byte(s.String()))
165	return int64(n), err
166}
167
168// Mode sets the mode for the OSC52 sequence.
169func (s Sequence) Mode(m Mode) Sequence {
170	s.mode = m
171	return s
172}
173
174// Tmux sets the mode to TmuxMode.
175// Used to escape the OSC52 sequence for `tmux`.
176//
177// Note: this is not needed if tmux clipboard is set to `set-clipboard on`. If
178// TmuxMode is used, tmux must have `allow-passthrough on` set.
179//
180// This is a syntactic sugar for s.Mode(TmuxMode).
181func (s Sequence) Tmux() Sequence {
182	return s.Mode(TmuxMode)
183}
184
185// Screen sets the mode to ScreenMode.
186// Used to escape the OSC52 sequence for `screen`.
187//
188// This is a syntactic sugar for s.Mode(ScreenMode).
189func (s Sequence) Screen() Sequence {
190	return s.Mode(ScreenMode)
191}
192
193// Clipboard sets the clipboard buffer for the OSC52 sequence.
194func (s Sequence) Clipboard(c Clipboard) Sequence {
195	s.clipboard = c
196	return s
197}
198
199// Primary sets the clipboard buffer to PrimaryClipboard.
200// This is the X11 primary clipboard.
201//
202// This is a syntactic sugar for s.Clipboard(PrimaryClipboard).
203func (s Sequence) Primary() Sequence {
204	return s.Clipboard(PrimaryClipboard)
205}
206
207// Limit sets the limit for the OSC52 sequence.
208// The default limit is 0 (no limit).
209//
210// Strings longer than the limit get ignored. Settting the limit to 0 or a
211// negative value disables the limit. Each terminal defines its own escapse
212// sequence limit.
213func (s Sequence) Limit(l int) Sequence {
214	if l < 0 {
215		s.limit = 0
216	} else {
217		s.limit = l
218	}
219	return s
220}
221
222// Operation sets the operation for the OSC52 sequence.
223// The default operation is SetOperation.
224func (s Sequence) Operation(o Operation) Sequence {
225	s.op = o
226	return s
227}
228
229// Clear sets the operation to ClearOperation.
230// This clears the clipboard.
231//
232// This is a syntactic sugar for s.Operation(ClearOperation).
233func (s Sequence) Clear() Sequence {
234	return s.Operation(ClearOperation)
235}
236
237// Query sets the operation to QueryOperation.
238// This queries the clipboard contents.
239//
240// This is a syntactic sugar for s.Operation(QueryOperation).
241func (s Sequence) Query() Sequence {
242	return s.Operation(QueryOperation)
243}
244
245// SetString sets the string for the OSC52 sequence. Strings are joined with a
246// space character.
247func (s Sequence) SetString(strs ...string) Sequence {
248	s.str = strings.Join(strs, " ")
249	return s
250}
251
252// New creates a new OSC52 sequence with the given string(s). Strings are
253// joined with a space character.
254func New(strs ...string) Sequence {
255	s := Sequence{
256		str:       strings.Join(strs, " "),
257		limit:     0,
258		mode:      DefaultMode,
259		clipboard: SystemClipboard,
260		op:        SetOperation,
261	}
262	return s
263}
264
265// Query creates a new OSC52 sequence with the QueryOperation.
266// This returns a new OSC52 sequence to query the clipboard contents.
267//
268// This is a syntactic sugar for New().Query().
269func Query() Sequence {
270	return New().Query()
271}
272
273// Clear creates a new OSC52 sequence with the ClearOperation.
274// This returns a new OSC52 sequence to clear the clipboard.
275//
276// This is a syntactic sugar for New().Clear().
277func Clear() Sequence {
278	return New().Clear()
279}
280
281func (s Sequence) seqStart() string {
282	switch s.mode {
283	case TmuxMode:
284		// Write the start of a tmux escape sequence.
285		return "\x1bPtmux;\x1b"
286	case ScreenMode:
287		// Write the start of a DCS sequence.
288		return "\x1bP"
289	default:
290		return ""
291	}
292}
293
294func (s Sequence) seqEnd() string {
295	switch s.mode {
296	case TmuxMode:
297		// Terminate the tmux escape sequence.
298		return "\x1b\\"
299	case ScreenMode:
300		// Write the end of a DCS sequence.
301		return "\x1b\x5c"
302	default:
303		return ""
304	}
305}