Back to Fresh

OSC 52 in Client-Server Session Attach Mode: Analysis

docs/internal/osc52-client-server-analysis.md

0.3.27.0 KB
Original Source

OSC 52 in Client-Server Session Attach Mode: Analysis

The Problem

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.

Code Path (current)

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.

What the Design Doc Intended

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.


The Fix (Straightforward Path)

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:

  1. Clipboard::copy() should return (or store) the OSC 52 bytes rather than writing to stdout when in session mode, OR
  2. The copy call site (app/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.

Sketch

rust
// 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.


Trade-offs of the Direct Fix

AspectProCon
SimplicityMinimal code change, mirrors existing patternNone significant
CorrectnessBytes reach the terminal that the user is looking atMultiple attached clients all receive the OSC 52 (usually fine — they all see the same editor)
Arboard fallbackStill works for non-session modeIn session/daemon mode, arboard likely fails (no display server access) — but that's the status quo
TestingEasy to unit test (check queued bytes)Integration testing requires a terminal that supports OSC 52

Deeper Trade-offs and Alternatives

Alternative 1: Route OSC 52 Through a Control Message

Instead of embedding OSC 52 in the data stream, define a new ServerControl variant:

rust
enum ServerControl {
    // ...existing variants...
    SetClipboard { text: String },
}

The client would then generate and write the OSC 52 sequence locally.

Pros:

  • Client can choose the best clipboard method (OSC 52, pbcopy, xclip, etc.)
  • Client knows its own terminal's capabilities
  • Cleaner separation: data stream = rendering, control stream = side-effects
  • Client could use arboard directly (it has display server access)

Cons:

  • More protocol surface area
  • Client needs clipboard logic (currently zero clipboard code in client)
  • Protocol versioning concern (old client + new server)

Alternative 2: Hybrid — Data Stream + Client-Side Clipboard

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:

  • Maximum compatibility
  • OSC 52 works for terminals that support it (zero client changes)
  • Control message enables arboard fallback on the client side

Cons:

  • Dual mechanisms could result in double-copy (minor, harmless)
  • More complexity

Alternative 3: Client-Only Clipboard (No Server Involvement)

Have the client intercept specific escape sequences in the data stream and handle clipboard operations itself, or use an out-of-band mechanism.

Pros:

  • Server stays clipboard-agnostic

Cons:

  • Client must parse the data stream (currently it's a dumb relay — this is a valuable property to preserve)
  • Fragile; any change to escape sequence format breaks it

Alternative 4: Do Nothing for OSC 52 — Rely on Arboard Only

Disable OSC 52 in session mode entirely and rely on arboard in the server.

Pros:

  • No escape sequence routing needed

Cons:

  • Arboard does not work in daemon mode on most setups. The daemonized server has no $DISPLAY / $WAYLAND_DISPLAY access. This is explicitly called out in the design doc. This alternative does not solve the problem.

Chosen Approach: Control Message with Client-Side OSC 52 + Arboard

We implemented Alternative 1 (control message) combined with the hybrid client strategy from Alternative 2:

  1. 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.

  2. 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.

Open Questions

  1. 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.

  2. 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.

  3. 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.