11use std:: env;
2+ use std:: fs:: OpenOptions ;
23use std:: io:: { self , Write } ;
34use std:: process:: { Command , Stdio } ;
45
56use anyhow:: { bail, Context , Result } ;
67
8+ /// The terminal multiplexer environment we are running inside, if any.
9+ enum Muxer {
10+ /// tmux — requires DCS passthrough wrapping with doubled ESC bytes.
11+ Tmux ,
12+ /// GNU Screen — requires DCS passthrough wrapping.
13+ Screen ,
14+ /// Zellij — supports OSC 52 natively, no wrapping needed.
15+ Zellij ,
16+ /// No known multiplexer detected.
17+ None ,
18+ }
19+
20+ fn detect_muxer ( ) -> Muxer {
21+ // Order matters: it is possible to nest muxers (e.g. tmux inside Zellij).
22+ // Check the innermost (most specific) first.
23+ if env:: var ( "ZELLIJ" ) . is_ok ( ) {
24+ Muxer :: Zellij
25+ } else if env:: var ( "TMUX" ) . is_ok ( ) {
26+ Muxer :: Tmux
27+ } else if env:: var ( "STY" ) . is_ok ( ) {
28+ Muxer :: Screen
29+ } else {
30+ Muxer :: None
31+ }
32+ }
33+
34+ /// Build the OSC 52 clipboard write sequence, wrapped appropriately for the
35+ /// current terminal multiplexer.
36+ ///
37+ /// The raw OSC 52 sequence is:
38+ /// ESC ] 52 ; c ; <base64> BEL
39+ ///
40+ /// Multiplexer wrapping:
41+ /// tmux — `ESC P tmux; ESC <osc52_with_doubled_escs> ESC \`
42+ /// screen — `ESC P <osc52> ESC \`
43+ /// zellij / bare terminal — no wrapping needed.
44+ fn build_osc52_sequence ( text : & str ) -> String {
45+ use base64:: Engine ;
46+ let encoded = base64:: engine:: general_purpose:: STANDARD . encode ( text. as_bytes ( ) ) ;
47+
48+ let osc52 = format ! ( "\x1b ]52;c;{encoded}\x07 " ) ;
49+
50+ match detect_muxer ( ) {
51+ Muxer :: Tmux => {
52+ // Inside tmux the ESC bytes in the inner sequence must be doubled
53+ // and the whole thing wrapped in a DCS passthrough.
54+ let inner = osc52. replace ( '\x1b' , "\x1b \x1b " ) ;
55+ format ! ( "\x1b Ptmux;{inner}\x1b \\ " )
56+ }
57+ Muxer :: Screen => {
58+ // GNU Screen DCS passthrough.
59+ format ! ( "\x1b P{osc52}\x1b \\ " )
60+ }
61+ Muxer :: Zellij | Muxer :: None => osc52,
62+ }
63+ }
64+
65+ /// Write to the clipboard using the OSC 52 escape sequence.
66+ ///
67+ /// The sequence is written directly to the controlling TTY (`/dev/tty`) so it
68+ /// reaches the outer terminal emulator even when stdout is owned by a TUI
69+ /// framework like ratatui / crossterm.
70+ fn write_osc52 ( text : & str ) -> Result < ( ) > {
71+ let seq = build_osc52_sequence ( text) ;
72+
73+ let mut tty = OpenOptions :: new ( )
74+ . write ( true )
75+ . open ( "/dev/tty" )
76+ . context ( "open /dev/tty for OSC 52 clipboard write" ) ?;
77+
78+ tty. write_all ( seq. as_bytes ( ) )
79+ . context ( "write OSC 52 sequence to /dev/tty" ) ?;
80+ tty. flush ( ) . context ( "flush /dev/tty after OSC 52 write" ) ?;
81+
82+ Ok ( ( ) )
83+ }
84+
85+ /// Return `true` when we should prefer OSC 52 over a system clipboard command.
86+ ///
87+ /// This is the case when we are inside any known terminal multiplexer, or when
88+ /// no suitable clipboard command can be found on the system.
89+ fn should_use_osc52 ( ) -> bool {
90+ !matches ! ( detect_muxer( ) , Muxer :: None )
91+ }
92+
793fn get_cmd ( ) -> Result < Command > {
894 let cmd = match env:: consts:: OS {
995 "macos" => Command :: new ( "pbcopy" ) ,
@@ -25,15 +111,15 @@ fn get_cmd() -> Result<Command> {
25111 Ok ( cmd)
26112}
27113
28- pub fn write_clipboard ( text : & str ) -> Result < ( ) > {
114+ fn write_clipboard_cmd ( text : & str ) -> Result < ( ) > {
29115 let mut cmd = get_cmd ( ) ?;
30116 cmd. stdin ( Stdio :: piped ( ) ) ;
31117
32118 let mut child = match cmd. spawn ( ) {
33119 Ok ( child) => child,
34120 Err ( err) if err. kind ( ) == io:: ErrorKind :: NotFound => {
35- let program = cmd . get_program ( ) . to_string_lossy ( ) ;
36- bail ! ( "cannot find clipboard program '{program}' in your system, please install it first to support clipboard" )
121+ // No system clipboard tool — fall back to OSC 52 as a last resort.
122+ return write_osc52 ( text ) ;
37123 }
38124 Err ( err) => return Err ( err) . context ( "launch clipboard program failed" ) ,
39125 } ;
@@ -54,3 +140,10 @@ pub fn write_clipboard(text: &str) -> Result<()> {
54140
55141 Ok ( ( ) )
56142}
143+
144+ pub fn write_clipboard ( text : & str ) -> Result < ( ) > {
145+ if should_use_osc52 ( ) {
146+ return write_osc52 ( text) ;
147+ }
148+ write_clipboard_cmd ( text)
149+ }
0 commit comments