atagen

monolith

public
Star0
Watch0
96db497859dc3dec0b217c73d5ddb53541766ae0
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/ipc/src/lib.rsRust310 lines8.6 KB
1
//! The control protocol. Newline-delimited JSON over a unix socket, one
2
//! message per line, each carrying the protocol version `v`.
3
//!
4
//! Commands also have a word form, `focus left` or `spawn foot`, shared by
5
//! the CLI and keybindings in the config.
6
7
use serde::{Deserialize, Serialize};
8
use std::fmt;
9
use std::path::PathBuf;
10
11
pub const VERSION: u32 = 1;
12
13
/// Where the compositor serving `display` listens.
14
pub fn socket_path_for(display: &str) -> Option<PathBuf> {
15
    let dir = std::env::var_os("XDG_RUNTIME_DIR")?;
16
    Some(PathBuf::from(dir).join(format!("monolith-{display}.sock")))
17
}
18
19
/// Where a client should connect. `$MONOLITH_SOCKET`, else the socket of
20
/// the compositor serving `$WAYLAND_DISPLAY`.
21
pub fn socket_path() -> Option<PathBuf> {
22
    if let Some(p) = std::env::var_os("MONOLITH_SOCKET") {
23
        return Some(p.into());
24
    }
25
    socket_path_for(&std::env::var("WAYLAND_DISPLAY").ok()?)
26
}
27
28
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
29
#[serde(rename_all = "kebab-case")]
30
pub enum Dir {
31
    Left,
32
    Right,
33
    Up,
34
    Down,
35
}
36
37
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
38
#[serde(rename_all = "kebab-case")]
39
pub enum Axis {
40
    Horizontal,
41
    Vertical,
42
}
43
44
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
45
#[serde(rename_all = "kebab-case")]
46
pub enum ExpandScope {
47
    #[default]
48
    Window,
49
    Block,
50
    All,
51
}
52
53
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
54
#[serde(tag = "command", rename_all = "kebab-case", deny_unknown_fields)]
55
pub enum Command {
56
    Spawn {
57
        argv: Vec<String>,
58
    },
59
    Close,
60
    Quit,
61
    Reload,
62
    Focus {
63
        dir: Dir,
64
    },
65
    Move {
66
        dir: Dir,
67
    },
68
    MoveBlock {
69
        dir: Dir,
70
    },
71
    Resize {
72
        axis: Axis,
73
        delta: f64,
74
    },
75
    Expand {
76
        scope: ExpandScope,
77
    },
78
    Flip,
79
    Float,
80
    Fullscreen,
81
    Sticky,
82
    View {
83
        tag: String,
84
    },
85
    ToggleView {
86
        tag: String,
87
    },
88
    ViewEverywhere {
89
        tag: String,
90
    },
91
    /// Sends the focused window to a tag.
92
    Tag {
93
        tag: String,
94
    },
95
    ReorderTag {
96
        tag: String,
97
        index: usize,
98
    },
99
    FocusOutput {
100
        dir: Dir,
101
    },
102
    SendToOutput {
103
        dir: Dir,
104
    },
105
    /// Adds the focused window to a scratchpad.
106
    Scratchpad {
107
        name: String,
108
    },
109
    Summon {
110
        name: String,
111
    },
112
    Dismiss {
113
        name: Option<String>,
114
    },
115
}
116
117
#[derive(Debug, Clone, PartialEq, Eq)]
118
pub struct ParseError(pub String);
119
120
impl fmt::Display for ParseError {
121
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122
        f.write_str(&self.0)
123
    }
124
}
125
126
impl std::error::Error for ParseError {}
127
128
impl Command {
129
    /// Parses the word form, as in `["move-block", "left"]`.
130
    pub fn parse<S: AsRef<str>>(words: &[S]) -> Result<Self, ParseError> {
131
        let words: Vec<&str> = words.iter().map(AsRef::as_ref).collect();
132
        let Some((&name, args)) = words.split_first() else {
133
            return Err(ParseError("empty command".into()));
134
        };
135
        let err = |m: &str| ParseError(format!("{name}: {m}"));
136
        let one = || match args {
137
            [a] => Ok(a.to_string()),
138
            _ => Err(err("takes exactly one argument")),
139
        };
140
        let none = |c: Command| {
141
            if args.is_empty() {
142
                Ok(c)
143
            } else {
144
                Err(err("takes no arguments"))
145
            }
146
        };
147
        let dir = || -> Result<Dir, ParseError> {
148
            match one()?.as_str() {
149
                "left" | "h" => Ok(Dir::Left),
150
                "right" | "l" => Ok(Dir::Right),
151
                "up" | "k" => Ok(Dir::Up),
152
                "down" | "j" => Ok(Dir::Down),
153
                d => Err(err(&format!("unknown direction {d}"))),
154
            }
155
        };
156
        Ok(match name {
157
            "spawn" if !args.is_empty() => Command::Spawn {
158
                argv: args.iter().map(|a| a.to_string()).collect(),
159
            },
160
            "spawn" => return Err(err("needs a program")),
161
            "close" => none(Command::Close)?,
162
            "quit" => none(Command::Quit)?,
163
            "reload" => none(Command::Reload)?,
164
            "flip" => none(Command::Flip)?,
165
            "float" => none(Command::Float)?,
166
            "fullscreen" => none(Command::Fullscreen)?,
167
            "sticky" => none(Command::Sticky)?,
168
            "focus" => Command::Focus { dir: dir()? },
169
            "move" => Command::Move { dir: dir()? },
170
            "move-block" => Command::MoveBlock { dir: dir()? },
171
            "focus-output" => Command::FocusOutput { dir: dir()? },
172
            "send-to-output" => Command::SendToOutput { dir: dir()? },
173
            "resize" => {
174
                let [axis, delta] = args else {
175
                    return Err(err("takes an axis and a delta"));
176
                };
177
                let axis = match *axis {
178
                    "horizontal" | "h" => Axis::Horizontal,
179
                    "vertical" | "v" => Axis::Vertical,
180
                    a => return Err(err(&format!("unknown axis {a}"))),
181
                };
182
                let delta = delta
183
                    .parse()
184
                    .map_err(|_| err(&format!("bad delta {delta}")))?;
185
                Command::Resize { axis, delta }
186
            }
187
            "expand" => Command::Expand {
188
                scope: match args {
189
                    [] | ["window"] => ExpandScope::Window,
190
                    ["block"] => ExpandScope::Block,
191
                    ["all"] => ExpandScope::All,
192
                    _ => return Err(err("takes window, block or all")),
193
                },
194
            },
195
            "view" => Command::View { tag: one()? },
196
            "toggle-view" => Command::ToggleView { tag: one()? },
197
            "view-everywhere" => Command::ViewEverywhere { tag: one()? },
198
            "tag" => Command::Tag { tag: one()? },
199
            "reorder-tag" => {
200
                let [tag, index] = args else {
201
                    return Err(err("takes a tag and an index"));
202
                };
203
                let index = index
204
                    .parse()
205
                    .map_err(|_| err(&format!("bad index {index}")))?;
206
                Command::ReorderTag {
207
                    tag: tag.to_string(),
208
                    index,
209
                }
210
            }
211
            "scratchpad" => Command::Scratchpad { name: one()? },
212
            "summon" => Command::Summon { name: one()? },
213
            "dismiss" => Command::Dismiss {
214
                name: match args {
215
                    [] => None,
216
                    [n] => Some(n.to_string()),
217
                    _ => return Err(err("takes at most one name")),
218
                },
219
            },
220
            _ => return Err(ParseError(format!("unknown command {name}"))),
221
        })
222
    }
223
}
224
225
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
226
#[serde(rename_all = "kebab-case")]
227
pub enum Request {
228
    Run(Command),
229
    Get(Query),
230
    /// Streams a `State` event after every change until the client hangs up.
231
    Subscribe {},
232
}
233
234
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
235
#[serde(rename_all = "kebab-case")]
236
pub enum Query {
237
    State,
238
    Version,
239
}
240
241
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
242
pub struct Envelope<T> {
243
    pub v: u32,
244
    #[serde(default, skip_serializing_if = "Option::is_none")]
245
    pub id: Option<u64>,
246
    #[serde(flatten)]
247
    pub body: T,
248
}
249
250
impl<T> Envelope<T> {
251
    pub fn new(id: Option<u64>, body: T) -> Self {
252
        Self {
253
            v: VERSION,
254
            id,
255
            body,
256
        }
257
    }
258
}
259
260
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
261
#[serde(rename_all = "kebab-case")]
262
pub enum Reply {
263
    Ok(Option<Answer>),
264
    Error(String),
265
    Event(State),
266
}
267
268
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
269
#[serde(rename_all = "kebab-case")]
270
pub enum Answer {
271
    State(State),
272
    Version(String),
273
}
274
275
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
276
pub struct State {
277
    pub tags: Vec<Tag>,
278
    pub outputs: Vec<Output>,
279
    pub windows: Vec<Window>,
280
}
281
282
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
283
pub struct Tag {
284
    pub name: String,
285
    pub urgent: bool,
286
}
287
288
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
289
pub struct Output {
290
    pub name: String,
291
    /// x, y, width, height in logical pixels.
292
    pub area: [f64; 4],
293
    pub viewed: Vec<String>,
294
    pub active: bool,
295
}
296
297
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
298
pub struct Window {
299
    pub id: u64,
300
    pub app_id: String,
301
    pub title: String,
302
    pub tag: String,
303
    pub output: Option<String>,
304
    pub floating: bool,
305
    pub fullscreen: bool,
306
    pub sticky: bool,
307
    pub scratchpad: Option<String>,
308
    pub visible: bool,
309
    pub focused: bool,
310
}