123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128(*****************************************************************************)(* *)(* SPDX-License-Identifier: MIT *)(* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com> *)(* *)(*****************************************************************************)[@@@warning"-32-34-37-69"]typet={copy:string->unit;copy_available:unit->bool}letkey:tCapability.key=Capability.create~name:"Clipboard"letsetv=Capability.setkeyvletget()=Capability.getkeyletrequire()=Capability.requirekey(** Base64 encoding alphabet *)letbase64_chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"(** Encode a string to base64. *)letbase64_encodes=letlen=String.lengthsinletoutput_len=(len+2)/3*4inletbuf=Buffer.createoutput_leninleti=ref0inwhile!i<lendoletb0=Char.codes.[!i]inletb1=if!i+1<lenthenChar.codes.[!i+1]else0inletb2=if!i+2<lenthenChar.codes.[!i+2]else0inBuffer.add_charbufbase64_chars.[(b0lsr2)land0x3F];Buffer.add_charbufbase64_chars.[(b0lsl4)lor(b1lsr4)land0x3F];if!i+1<lenthenBuffer.add_charbufbase64_chars.[(b1lsl2)lor(b2lsr6)land0x3F]elseBuffer.add_charbuf'=';if!i+2<lenthenBuffer.add_charbufbase64_chars.[b2land0x3F]elseBuffer.add_charbuf'=';i:=!i+3done;Buffer.contentsbuf(** Encode text as OSC 52 escape sequence for clipboard.
Format: ESC ] 52 ; c ; <base64> BEL
- "c" specifies the clipboard selection (system clipboard)
- base64-encoded payload is the text to copy
- BEL (\007) is more widely supported than ESC \ as terminator *)letosc52_encodetext=letb64=base64_encodetextinPrintf.sprintf"\027]52;c;%s\007"b64(** Run a shell command with text written to a temp file.
Uses temp file + shell redirection since piping stdin via
create_process doesn't work reliably with wl-copy on Wayland
(OCaml 5 domains prevent using fork). *)letrun_clipboard_cmdtextcmd_fmt=trylettmp=Filename.temp_file"miaou_clip"".txt"inletoc=open_outtmpinoutput_stringoctext;close_outoc;letcmd=Printf.sprintfcmd_fmt(Filename.quotetmp)inletdev_null=Unix.openfile"/dev/null"[Unix.O_RDWR]0inletpid=Unix.create_process"sh"[|"sh";"-c";cmd|]dev_nulldev_nulldev_nullinUnix.closedev_null;(* Wait for shell to finish *)(Unix.sleepf[@allow_forbidden"clipboard needs blocking wait for process"])0.15;letwaited_pid,status=Unix.waitpid[Unix.WNOHANG]pidinletsuccess=ifwaited_pid=0thenbegin(* Process still running - wait a bit more then assume success *)(Unix.sleepf[@allow_forbidden"clipboard needs blocking wait for process"])0.1;trueendelsematchstatuswith|Unix.WEXITED0->true|Unix.WEXITED_|Unix.WSIGNALED_|Unix.WSTOPPED_->falsein(* Clean up temp file *)(Unix.sleepf[@allow_forbidden"clipboard needs blocking wait for cleanup"])0.05;(tryUnix.unlinktmpwith_->());successwith_->false(** Copy to system clipboard (Ctrl+V paste).
Uses setsid to run wl-copy in a new session so it survives parent exit. *)letcopy_nativetext=run_clipboard_cmdtext"setsid wl-copy < %s"||run_clipboard_cmdtext"wl-copy < %s"||run_clipboard_cmdtext"xclip -selection clipboard < %s"||run_clipboard_cmdtext"xsel --clipboard --input < %s"||run_clipboard_cmdtext"pbcopy < %s"(** Copy to primary selection (middle-click paste).
Uses setsid to run wl-copy in a new session so it survives parent exit. *)letcopy_primarytext=run_clipboard_cmdtext"setsid wl-copy --primary < %s"||run_clipboard_cmdtext"wl-copy --primary < %s"||run_clipboard_cmdtext"xclip -selection primary < %s"||run_clipboard_cmdtext"xsel --primary --input < %s"(* macOS doesn't have primary selection *)(** Copy to both clipboard and primary selection. *)letcopy_bothtext=let_native_ok=copy_nativetextinlet_primary_ok=copy_primarytextintrueletregister~write?on_copy?(enabled=true)()=letcopy_fn=ifenabledthen(funtext->(* Copy to both clipboard and primary selection for middle-click paste.
Fall back to OSC 52 if native tools unavailable. *)ifnot(copy_bothtext)thenwrite(osc52_encodetext);matchon_copywithSomef->ftext|None->())elsefun_->()inletcap:t={copy=copy_fn;copy_available=(fun()->enabled)}insetcap