srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/ctl/src/main.rs
blob: a82fe4a72a63692db0f1c47b9175d1536742ec27 (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
127
128
129
130
//! 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
//!   srd dispatch toggle_maximize ID
//!   srd dispatch toggle_fullscreen 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/toggle_maximize/toggle_fullscreen)")?;
            if !matches!(action.as_str(), "toggle_visibility" | "focus" | "close" | "toggle_maximize" | "toggle_fullscreen") {
                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>");
    eprintln!("  srd dispatch toggle_maximize <id>");
    eprintln!("  srd dispatch toggle_fullscreen <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(())
    }
}