docs/internal/osc52-client-server-analysis.md
OSC 52 clipboard copy does not work when the editor runs in client-server
(daemon/session) mode. The root cause is straightforward: the clipboard service
writes OSC 52 escape sequences directly to the server process's stdout(),
which is detached from any terminal. The sequences never reach the client's
terminal emulator.
User yanks text
→ Clipboard::copy() called in the server process
→ execute!(stdout(), CopyToClipboard::to_clipboard_from(&text)) // clipboard.rs:112
→ stdout().flush() // clipboard.rs:118
In daemon mode, the server's stdout is /dev/null (or a log file). The OSC 52
bytes vanish. The client relay loop (relay_unix.rs / relay_windows.rs) only
forwards bytes it reads from the data socket, not from the server's stdout.
The design doc (session-persistence-design.md:604-651) correctly identifies this
issue and prescribes:
Server generates: \x1b]52;c;BASE64_TEXT\x07
↓
Server writes to data socket
↓
Client relays to stdout (no parsing)
↓
Terminal emulator receives and sets system clipboard
The infrastructure to do this already exists: Editor::queue_escape_sequences()
and Editor::take_pending_escape_sequences() (in app/mod.rs:3206-3212). This
mechanism is already used for cursor style changes in session mode
(app/input.rs:3322-3324). But the clipboard code was never wired into it.
When session_mode is true, instead of writing OSC 52 to stdout(), the
clipboard service should produce the raw escape sequence bytes and the editor
should route them via queue_escape_sequences().
Concretely:
Clipboard::copy() should return (or store) the OSC 52 bytes rather
than writing to stdout when in session mode, ORapp/input_dispatch.rs:328-331) should generate and
queue the OSC 52 sequence itself after calling clipboard.copy().Option 2 is simpler since it mirrors the cursor-style pattern at
app/input.rs:3322-3324 and avoids threading session-mode awareness into the
clipboard service.
// In input_dispatch.rs, after clipboard.copy(text):
if self.session_mode && self.clipboard.use_osc52() {
let encoded = base64::encode(&text);
let osc52 = format!("\x1b]52;c;{}\x07", encoded);
self.queue_escape_sequences(osc52.as_bytes());
}
The queued bytes flow through take_pending_escape_sequences() →
render_and_broadcast() → client data socket → client stdout → terminal
emulator. This path is already proven to work for cursor style sequences.
| Aspect | Pro | Con |
|---|---|---|
| Simplicity | Minimal code change, mirrors existing pattern | None significant |
| Correctness | Bytes reach the terminal that the user is looking at | Multiple attached clients all receive the OSC 52 (usually fine — they all see the same editor) |
| Arboard fallback | Still works for non-session mode | In session/daemon mode, arboard likely fails (no display server access) — but that's the status quo |
| Testing | Easy to unit test (check queued bytes) | Integration testing requires a terminal that supports OSC 52 |
Instead of embedding OSC 52 in the data stream, define a new ServerControl
variant:
enum ServerControl {
// ...existing variants...
SetClipboard { text: String },
}
The client would then generate and write the OSC 52 sequence locally.
Pros:
Cons:
Use the data stream for OSC 52 (the simple fix) but also send a control message. The client can then additionally try arboard/native clipboard for terminals that don't support OSC 52.
Pros:
Cons:
Have the client intercept specific escape sequences in the data stream and handle clipboard operations itself, or use an out-of-band mechanism.
Pros:
Cons:
Disable OSC 52 in session mode entirely and rely on arboard in the server.
Pros:
Cons:
$DISPLAY / $WAYLAND_DISPLAY access. This is explicitly called out
in the design doc. This alternative does not solve the problem.We implemented Alternative 1 (control message) combined with the hybrid client strategy from Alternative 2:
Server side: Clipboard::copy() detects session_mode, skips stdout
writes, and queues the text in pending_clipboard. The server polls this
and broadcasts a SetClipboard control message to all clients.
Client side: On receiving SetClipboard, the client uses both
OSC 52 (for terminals that support it) and arboard (for native X11/Wayland/
macOS clipboard). This maximizes compatibility.
This gives clean separation (data stream = rendering, control stream = side-effects) and lets the client — which has access to both the terminal and the display server — use the best clipboard method available.
Multi-client semantics: When multiple clients are attached, all receive the OSC 52 sequence. This is probably fine (they're all viewing the same editor state), but worth noting.
OSC 52 size limits: Some terminals cap the payload at ~100KB. For very large yanks, should we skip OSC 52 and show a status message? The current non-session code doesn't handle this either, so it's not a regression.
Should Clipboard::copy() skip writing to stdout when session_mode is
true? Currently it writes to the server's stdout (harmless but wasteful).
Cleanest approach: add a session_mode flag to Clipboard and suppress
the direct stdout() write, letting the caller handle routing.