srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/ctl/src/main.rs
blob: 2c3c3db44fc873bb3c1f5e67fb1e16b704593163 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//! Control CLI for srdwm's control socket (`crates/platform/src/ipc.rs`,
//! shared by every backend that has one). Deliberately dumb: builds one
//! request line from argv, writes it, reads the response(s), prints them.
//! All real logic - and all JSON encoding - lives server-side; this
//! stays a thin pipe so it never needs a JSON dependency of its own.
//!
//! Usage:
//!   srd clients                        list windows, one JSON object
//!   srd subscribe                      like `clients`, then one JSON
//!                                       object per line forever, each time
//!                                       the window list actually changes
//!   srd dispatch toggle_visibility ID  hide/show a window
//!   srd dispatch focus ID
//!   srd dispatch close ID
//!
//! The socket is Unix-domain; on platforms without one this always fails
//! cleanly rather than not building at all, since it's one binary in a
//! workspace that otherwise targets Windows and macOS too.

#[cfg(unix)]
fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let request = match build_request(&args) {
        Ok(r) => r,
        Err(msg) => {
            eprintln!("srd: {msg}");
            print_usage();
            std::process::exit(2);
        }
    };

    let result = if args.first().map(String::as_str) == Some("subscribe") { unix::stream(&request) } else { unix::send(&request).map(|r| println!("{r}")) };
    if let Err(e) = result {
        eprintln!("srd: {e}");
        std::process::exit(1);
    }
}

#[cfg(not(unix))]
fn main() {
    eprintln!("srd: srdwm's control socket is Unix-only; nothing to connect to on this platform");
    std::process::exit(1);
}

fn build_request(args: &[String]) -> Result<String, String> {
    match args.first().map(String::as_str) {
        Some("clients") => Ok(r#"{"cmd":"clients"}"#.to_string()),
        Some("monitors") => Ok(r#"{"cmd":"monitors"}"#.to_string()),
        Some("subscribe") => Ok(r#"{"cmd":"subscribe"}"#.to_string()),
        Some("dispatch") => {
            let action = args.get(1).ok_or("dispatch needs an action (toggle_visibility/focus/close)")?;
            if !matches!(action.as_str(), "toggle_visibility" | "focus" | "close") {
                return Err(format!("unknown dispatch action '{action}'"));
            }
            let id: u64 = args.get(2).ok_or("dispatch needs a window id")?.parse().map_err(|_| "window id must be a number".to_string())?;
            Ok(format!(r#"{{"cmd":"{action}","id":{id}}}"#))
        }
        _ => Err("expected 'clients', 'monitors', 'subscribe', or 'dispatch <action> <id>'".to_string()),
    }
}

fn print_usage() {
    eprintln!("usage:");
    eprintln!("  srd clients");
    eprintln!("  srd monitors");
    eprintln!("  srd subscribe");
    eprintln!("  srd dispatch toggle_visibility <id>");
    eprintln!("  srd dispatch focus <id>");
    eprintln!("  srd dispatch close <id>");
}

#[cfg(unix)]
mod unix {
    use std::io::{BufRead, BufReader, Write};
    use std::os::unix::net::UnixStream;
    use std::path::PathBuf;

    /// `<display>` in `srdwm-<display>.sock` is whichever env var the
    /// compositor itself used to name it: `WAYLAND_DISPLAY` (Wayland
    /// backend) or `DISPLAY` (X11 backend) - see `srdwm_platform::
    /// IpcServer::bind`'s callers in `crates/wayland`/`crates/x11` for the
    /// matching naming choice on the server side. Reuses `srdwm_platform::
    /// detect()` rather than re-deriving the same Wayland-vs-X11 decision
    /// a second, potentially-drifting way here.
    fn socket_path() -> Result<PathBuf, String> {
        let dir = std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from).ok_or("XDG_RUNTIME_DIR is not set")?;
        let display = match srdwm_platform::detect() {
            srdwm_platform::PlatformKind::X11 => std::env::var("DISPLAY").map_err(|_| "DISPLAY is not set".to_string())?,
            _ => std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".to_string()),
        };
        Ok(dir.join(format!("srdwm-{display}.sock")))
    }

    fn connect() -> Result<UnixStream, String> {
        let path = socket_path()?;
        UnixStream::connect(&path)
            .map_err(|e| format!("can't reach srdwm's control socket at {} ({e}) - is srdwm running, and are WAYLAND_DISPLAY/DISPLAY set correctly?", path.display()))
    }

    pub fn send(request: &str) -> Result<String, String> {
        let mut stream = connect()?;
        stream.write_all(request.as_bytes()).map_err(|e| e.to_string())?;
        stream.write_all(b"\n").map_err(|e| e.to_string())?;
        let mut line = String::new();
        BufReader::new(&stream).read_line(&mut line).map_err(|e| e.to_string())?;
        Ok(line.trim_end().to_string())
    }

    /// `subscribe`'s connection: unlike `send`, this never closes on its
    /// own - the server keeps it open and writes one more line every time
    /// the window list changes, so this prints each one as it arrives
    /// until the process is killed or the compositor closes the socket.
    pub fn stream(request: &str) -> Result<(), String> {
        let mut conn = connect()?;
        conn.write_all(request.as_bytes()).map_err(|e| e.to_string())?;
        conn.write_all(b"\n").map_err(|e| e.to_string())?;
        let reader = BufReader::new(conn);
        for line in reader.lines() {
            match line {
                Ok(line) => println!("{line}"),
                Err(e) => return Err(e.to_string()),
            }
        }
        Ok(())
    }
}