Hmbown/CodeWhale · error · anyhow::Error

Failed to write to {program}: {e}

Error message

Failed to write to {program}: {e}

What it means

write_all to wl-copy's piped stdin failed. Nearly always EPIPE: wl-copy spawned but exited immediately (no reachable Wayland compositor, stale WAYLAND_DISPLAY, clipboard daemon error), closing the pipe before the payload was written. Distinct from wl-copy's own non-zero exit, which is reported separately after wait().

Source

Thrown at crates/tui/src/tui/clipboard.rs:588

        .map_err(|e| anyhow::anyhow!("Failed to run {program}: {e}"))?;
    if !output.status.success() {
        bail!("{program} exited with {}", output.status);
    }
    String::from_utf8(output.stdout).context("wl-paste returned non-UTF-8 text")
}

#[cfg(all(target_os = "linux", not(target_env = "ohos"), not(test)))]
fn write_text_with_wlcopy_using_argv(program: &str, text: &str) -> Result<()> {
    let mut child = Command::new(program)
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .map_err(|e| anyhow::anyhow!("Failed to run {program}: {e}"))?;
    if let Some(mut stdin) = child.stdin.take() {
        stdin
            .write_all(text.as_bytes())
            .map_err(|e| anyhow::anyhow!("Failed to write to {program}: {e}"))?;
    }
    // stdin is dropped here, closing the pipe so wl-copy flushes.
    let status = child
        .wait()
        .map_err(|e| anyhow::anyhow!("Failed to wait on {program}: {e}"))?;
    if !status.success() {
        bail!("{program} exited with {status}");
    }
    Ok(())
}

#[cfg(not(test))]
fn write_text_to_terminal_client(text: &str, in_tmux: bool) -> Result<()> {
    if in_tmux {
        return write_text_with_tmux(text);
    }
    write_text_with_osc52(text)
}

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Verify a live Wayland session in the same shell: `echo $WAYLAND_DISPLAY` then `echo hi | wl-copy`
  2. unset WAYLAND_DISPLAY if the session is not actually Wayland so the OSC 52 fallback wins cleanly
  3. Restart the compositor/session when the socket is stale

Example fix

# before
export WAYLAND_DISPLAY=wayland-0   # stale, compositor gone
codewhale tui                        # copy -> Failed to write to wl-copy: broken pipe

# after
unset WAYLAND_DISPLAY
codewhale tui                        # falls back to OSC 52 / tmux
Defensive patterns

Strategy: fallback

Validate before calling

// Rust - probe the Wayland clipboard socket liveness cheaply before large writes
fn wayland_session_alive() -> bool {
    std::env::var("WAYLAND_DISPLAY").map(|d| {
        let runtime = std::env::var("XDG_RUNTIME_DIR").unwrap_or_default();
        !runtime.is_empty() && std::path::Path::new(&runtime).join(&d).exists()
    }).unwrap_or(false)
}

Try / catch

match write_text_with_wlcopy(text) {
    Err(e) if e.chain().any(|c| c.to_string().contains("broken pipe")) => {
        unset_env_and_use_osc52(); // compositor gone: fall through, never retry wl-copy
    }
    r => r,
}

Prevention

When it happens

Trigger: wl-copy exits at startup because WAYLAND_DISPLAY points at a dead compositor socket or the session bus is unavailable; stdin.write_all then returns BrokenPipe.

Common situations: SSH sessions inheriting a stale WAYLAND_DISPLAY; compositor crash; running wl-copy on X11-only systems.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/b0689240a4f0643c. Report an issue: GitHub.