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()) 54 package osc52 55 56 import ( 57 "encoding/base64" 58 "fmt" 59 "io" 60 "strings" 61 ) 62 63 // Clipboard is the clipboard buffer to use. 64 type Clipboard rune 65 66 const ( 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. 74 type Mode uint 75 76 const ( 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. 87 type Operation uint 88 89 const ( 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. 99 type Sequence struct { 100 str string 101 limit int 102 op Operation 103 mode Mode 104 clipboard Clipboard 105 } 106 107 var _ fmt.Stringer = Sequence{} 108 109 var _ io.WriterTo = Sequence{} 110 111 // String returns the OSC52 sequence. 112 func (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. 163 func (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. 169 func (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). 181 func (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). 189 func (s Sequence) Screen() Sequence { 190 return s.Mode(ScreenMode) 191 } 192 193 // Clipboard sets the clipboard buffer for the OSC52 sequence. 194 func (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). 203 func (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. 213 func (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. 224 func (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). 233 func (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). 241 func (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. 247 func (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. 254 func 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(). 269 func 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(). 277 func Clear() Sequence { 278 return New().Clear() 279 } 280 281 func (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 294 func (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 } 306