osc52.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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. import (
  56. "encoding/base64"
  57. "fmt"
  58. "io"
  59. "strings"
  60. )
  61. // Clipboard is the clipboard buffer to use.
  62. type Clipboard rune
  63. const (
  64. // SystemClipboard is the system clipboard buffer.
  65. SystemClipboard Clipboard = 'c'
  66. // PrimaryClipboard is the primary clipboard buffer (X11).
  67. PrimaryClipboard = 'p'
  68. )
  69. // Mode is the mode to use for the OSC52 sequence.
  70. type Mode uint
  71. const (
  72. // DefaultMode is the default OSC52 sequence mode.
  73. DefaultMode Mode = iota
  74. // ScreenMode escapes the OSC52 sequence for screen using DCS sequences.
  75. ScreenMode
  76. // TmuxMode escapes the OSC52 sequence for tmux. Not needed if tmux
  77. // clipboard is set to `set-clipboard on`
  78. TmuxMode
  79. )
  80. // Operation is the OSC52 operation.
  81. type Operation uint
  82. const (
  83. // SetOperation is the copy operation.
  84. SetOperation Operation = iota
  85. // QueryOperation is the query operation.
  86. QueryOperation
  87. // ClearOperation is the clear operation.
  88. ClearOperation
  89. )
  90. // Sequence is the OSC52 sequence.
  91. type Sequence struct {
  92. str string
  93. limit int
  94. op Operation
  95. mode Mode
  96. clipboard Clipboard
  97. }
  98. var _ fmt.Stringer = Sequence{}
  99. var _ io.WriterTo = Sequence{}
  100. // String returns the OSC52 sequence.
  101. func (s Sequence) String() string {
  102. var seq strings.Builder
  103. // mode escape sequences start
  104. seq.WriteString(s.seqStart())
  105. // actual OSC52 sequence start
  106. seq.WriteString(fmt.Sprintf("\x1b]52;%c;", s.clipboard))
  107. switch s.op {
  108. case SetOperation:
  109. str := s.str
  110. if s.limit > 0 && len(str) > s.limit {
  111. return ""
  112. }
  113. b64 := base64.StdEncoding.EncodeToString([]byte(str))
  114. switch s.mode {
  115. case ScreenMode:
  116. // Screen doesn't support OSC52 but will pass the contents of a DCS
  117. // sequence to the outer terminal unchanged.
  118. //
  119. // Here, we split the encoded string into 76 bytes chunks and then
  120. // join the chunks with <end-dsc><start-dsc> sequences. Finally,
  121. // wrap the whole thing in
  122. // <start-dsc><start-osc52><joined-chunks><end-osc52><end-dsc>.
  123. // s := strings.SplitN(b64, "", 76)
  124. s := make([]string, 0, len(b64)/76+1)
  125. for i := 0; i < len(b64); i += 76 {
  126. end := i + 76
  127. if end > len(b64) {
  128. end = len(b64)
  129. }
  130. s = append(s, b64[i:end])
  131. }
  132. seq.WriteString(strings.Join(s, "\x1b\\\x1bP"))
  133. default:
  134. seq.WriteString(b64)
  135. }
  136. case QueryOperation:
  137. // OSC52 queries the clipboard using "?"
  138. seq.WriteString("?")
  139. case ClearOperation:
  140. // OSC52 clears the clipboard if the data is neither a base64 string nor "?"
  141. // we're using "!" as a default
  142. seq.WriteString("!")
  143. }
  144. // actual OSC52 sequence end
  145. seq.WriteString("\x07")
  146. // mode escape end
  147. seq.WriteString(s.seqEnd())
  148. return seq.String()
  149. }
  150. // WriteTo writes the OSC52 sequence to the writer.
  151. func (s Sequence) WriteTo(out io.Writer) (int64, error) {
  152. n, err := out.Write([]byte(s.String()))
  153. return int64(n), err
  154. }
  155. // Mode sets the mode for the OSC52 sequence.
  156. func (s Sequence) Mode(m Mode) Sequence {
  157. s.mode = m
  158. return s
  159. }
  160. // Tmux sets the mode to TmuxMode.
  161. // Used to escape the OSC52 sequence for `tmux`.
  162. //
  163. // Note: this is not needed if tmux clipboard is set to `set-clipboard on`. If
  164. // TmuxMode is used, tmux must have `allow-passthrough on` set.
  165. //
  166. // This is a syntactic sugar for s.Mode(TmuxMode).
  167. func (s Sequence) Tmux() Sequence {
  168. return s.Mode(TmuxMode)
  169. }
  170. // Screen sets the mode to ScreenMode.
  171. // Used to escape the OSC52 sequence for `screen`.
  172. //
  173. // This is a syntactic sugar for s.Mode(ScreenMode).
  174. func (s Sequence) Screen() Sequence {
  175. return s.Mode(ScreenMode)
  176. }
  177. // Clipboard sets the clipboard buffer for the OSC52 sequence.
  178. func (s Sequence) Clipboard(c Clipboard) Sequence {
  179. s.clipboard = c
  180. return s
  181. }
  182. // Primary sets the clipboard buffer to PrimaryClipboard.
  183. // This is the X11 primary clipboard.
  184. //
  185. // This is a syntactic sugar for s.Clipboard(PrimaryClipboard).
  186. func (s Sequence) Primary() Sequence {
  187. return s.Clipboard(PrimaryClipboard)
  188. }
  189. // Limit sets the limit for the OSC52 sequence.
  190. // The default limit is 0 (no limit).
  191. //
  192. // Strings longer than the limit get ignored. Settting the limit to 0 or a
  193. // negative value disables the limit. Each terminal defines its own escapse
  194. // sequence limit.
  195. func (s Sequence) Limit(l int) Sequence {
  196. if l < 0 {
  197. s.limit = 0
  198. } else {
  199. s.limit = l
  200. }
  201. return s
  202. }
  203. // Operation sets the operation for the OSC52 sequence.
  204. // The default operation is SetOperation.
  205. func (s Sequence) Operation(o Operation) Sequence {
  206. s.op = o
  207. return s
  208. }
  209. // Clear sets the operation to ClearOperation.
  210. // This clears the clipboard.
  211. //
  212. // This is a syntactic sugar for s.Operation(ClearOperation).
  213. func (s Sequence) Clear() Sequence {
  214. return s.Operation(ClearOperation)
  215. }
  216. // Query sets the operation to QueryOperation.
  217. // This queries the clipboard contents.
  218. //
  219. // This is a syntactic sugar for s.Operation(QueryOperation).
  220. func (s Sequence) Query() Sequence {
  221. return s.Operation(QueryOperation)
  222. }
  223. // SetString sets the string for the OSC52 sequence. Strings are joined with a
  224. // space character.
  225. func (s Sequence) SetString(strs ...string) Sequence {
  226. s.str = strings.Join(strs, " ")
  227. return s
  228. }
  229. // New creates a new OSC52 sequence with the given string(s). Strings are
  230. // joined with a space character.
  231. func New(strs ...string) Sequence {
  232. s := Sequence{
  233. str: strings.Join(strs, " "),
  234. limit: 0,
  235. mode: DefaultMode,
  236. clipboard: SystemClipboard,
  237. op: SetOperation,
  238. }
  239. return s
  240. }
  241. // Query creates a new OSC52 sequence with the QueryOperation.
  242. // This returns a new OSC52 sequence to query the clipboard contents.
  243. //
  244. // This is a syntactic sugar for New().Query().
  245. func Query() Sequence {
  246. return New().Query()
  247. }
  248. // Clear creates a new OSC52 sequence with the ClearOperation.
  249. // This returns a new OSC52 sequence to clear the clipboard.
  250. //
  251. // This is a syntactic sugar for New().Clear().
  252. func Clear() Sequence {
  253. return New().Clear()
  254. }
  255. func (s Sequence) seqStart() string {
  256. switch s.mode {
  257. case TmuxMode:
  258. // Write the start of a tmux escape sequence.
  259. return "\x1bPtmux;\x1b"
  260. case ScreenMode:
  261. // Write the start of a DCS sequence.
  262. return "\x1bP"
  263. default:
  264. return ""
  265. }
  266. }
  267. func (s Sequence) seqEnd() string {
  268. switch s.mode {
  269. case TmuxMode:
  270. // Terminate the tmux escape sequence.
  271. return "\x1b\\"
  272. case ScreenMode:
  273. // Write the end of a DCS sequence.
  274. return "\x1b\x5c"
  275. default:
  276. return ""
  277. }
  278. }