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}