clonetty 0.0.1

Spawn a new Alacritty window cloning an existing terminal's working directory and its nested-shell environment stack, so Ctrl-D peels back one shell layer at a time.
//! Resolve the PID of the X11-focused window so `clonetty --focused` can clone
//! whatever terminal currently has input focus, without the caller plumbing a
//! PID in from the window manager.
//!
//! Mechanism (EWMH — Extended Window Manager Hints,
//! <https://specifications.freedesktop.org/wm-spec/latest/>):
//!   1. Read `_NET_ACTIVE_WINDOW` (a `WINDOW`) off the root window. The window
//!      manager (i3, bspwm, ...) maintains this; it names the focused top-level
//!      *managed* window — exactly the terminal the user is looking at.
//!   2. Read `_NET_WM_PID` (a `CARDINAL`) off that window. The client
//!      (e.g. Alacritty) sets it to its own PID.
//!
//! The returned PID is then fed to [`crate::proc::Terminal::from_pid`], whose
//! `resolve_shell_pid` recognises an `alacritty` PID and descends to the
//! innermost shell — so this is precisely the `--pid <that window>` path, just
//! with the PID discovered for you.
//!
//! **Xorg only.** Under a pure Wayland compositor there is no X root window to
//! query, so we fail with a message pointing at `--pid`. XWayland clients would
//! still appear here; a native Wayland terminal would not.
//!
//! We use pure-Rust `x11rb` (`RustConnection`), which speaks the X11 protocol
//! over the display socket directly: no libX11/libxcb link and no subprocess
//! (contrast `xdotool`, which would reintroduce a runtime PATH dependency — the
//! very failure mode this feature exists to avoid).

use anyhow::{Context, Result, bail};
use x11rb::connection::Connection;
use x11rb::protocol::xproto::{Atom, AtomEnum, ConnectionExt, Window};

/// PID of the client that owns the currently focused (active) X11 window.
///
/// Errors, with actionable messages, when there is no X display (Wayland or
/// headless — use `--pid`), when no window is active, or when the focused window
/// advertises no `_NET_WM_PID` (a client that never set one, or a window manager
/// that does not implement EWMH).
pub fn focused_window_pid() -> Result<u32> {
    let (conn, screen_num) = x11rb::connect(None)
        .context("connecting to the X server (no DISPLAY? on Wayland use --pid)")?;
    let root = conn.setup().roots[screen_num].root;

    let active_atom = intern(&conn, b"_NET_ACTIVE_WINDOW")?;
    let pid_atom = intern(&conn, b"_NET_WM_PID")?;

    // _NET_ACTIVE_WINDOW: a single WINDOW id, published on the root window.
    let active: Window = single_u32(&conn, root, active_atom, AtomEnum::WINDOW.into())?
        .filter(|&w| w != 0)
        .context("no active window (_NET_ACTIVE_WINDOW unset or zero)")?;

    // _NET_WM_PID: a single CARDINAL, published on the active window itself.
    match single_u32(&conn, active, pid_atom, AtomEnum::CARDINAL.into())? {
        Some(pid) => Ok(pid),
        None => bail!(
            "focused window {active:#x} advertises no _NET_WM_PID; its client did \
             not set one \u{2014} pass --pid <shell PID> instead"
        ),
    }
}

/// Intern an EWMH atom by name (`only_if_exists = false`, so it is created if
/// the server has not seen it — harmless for read-only queries).
fn intern(conn: &impl Connection, name: &[u8]) -> Result<Atom> {
    let show = || String::from_utf8_lossy(name).into_owned();
    Ok(conn
        .intern_atom(false, name)
        .with_context(|| format!("requesting atom {}", show()))?
        .reply()
        .with_context(|| format!("interning atom {}", show()))?
        .atom)
}

/// Read the first 32-bit value of `property` (of type `type_`) on `window`, or
/// `None` if the property is absent/empty.
fn single_u32(
    conn: &impl Connection,
    window: Window,
    property: Atom,
    type_: Atom,
) -> Result<Option<u32>> {
    Ok(conn
        .get_property(false, window, property, type_, 0, 1)
        .context("requesting property")?
        .reply()
        .context("reading property")?
        .value32()
        .and_then(|mut values| values.next()))
}