atagen

monolith

public
Star0
Watch0
272bdf93aeadc79daea64d4765ba1f31e693d592
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git
crates/ipc/src/lib.rsRust317 lines9.0 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
    Container,
50
}
51
52
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
53
#[serde(tag = "command", rename_all = "kebab-case", deny_unknown_fields)]
54
pub enum Command {
55
    Spawn {
56
        argv: Vec<String>,
57
    },
58
    Close,
59
    Quit,
60
    Reload,
61
    Focus {
62
        dir: Dir,
63
    },
64
    Move {
65
        dir: Dir,
66
    },
67
    MoveContainer {
68
        dir: Dir,
69
    },
70
    Resize {
71
        axis: Axis,
72
        delta: f64,
73
    },
74
    Expand {
75
        scope: ExpandScope,
76
    },
77
    /// Expands every container above the focused window towards it.
78
    Maximize,
79
    /// Makes the next window move do the opposite of the configured default,
80
    /// swap or merge. Again before moving cancels it.
81
    InvertMove,
82
    Flip,
83
    Float,
84
    Fullscreen,
85
    Sticky,
86
    View {
87
        tag: String,
88
    },
89
    ToggleView {
90
        tag: String,
91
    },
92
    ViewEverywhere {
93
        tag: String,
94
    },
95
    /// Sends the focused window to a tag.
96
    Tag {
97
        tag: String,
98
    },
99
    ReorderTag {
100
        tag: String,
101
        index: usize,
102
    },
103
    FocusOutput {
104
        dir: Dir,
105
    },
106
    SendToOutput {
107
        dir: Dir,
108
    },
109
    /// Adds the focused window to a scratchpad.
110
    Scratchpad {
111
        name: String,
112
    },
113
    Summon {
114
        name: String,
115
    },
116
    Dismiss {
117
        name: Option<String>,
118
    },
119
}
120
121
#[derive(Debug, Clone, PartialEq, Eq)]
122
pub struct ParseError(pub String);
123
124
impl fmt::Display for ParseError {
125
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126
        f.write_str(&self.0)
127
    }
128
}
129
130
impl std::error::Error for ParseError {}
131
132
impl Command {
133
    /// Parses the word form, as in `["move-container", "left"]`.
134
    pub fn parse<S: AsRef<str>>(words: &[S]) -> Result<Self, ParseError> {
135
        let words: Vec<&str> = words.iter().map(AsRef::as_ref).collect();
136
        let Some((&name, args)) = words.split_first() else {
137
            return Err(ParseError("empty command".into()));
138
        };
139
        let err = |m: &str| ParseError(format!("{name}: {m}"));
140
        let one = || match args {
141
            [a] => Ok(a.to_string()),
142
            _ => Err(err("takes exactly one argument")),
143
        };
144
        let none = |c: Command| {
145
            if args.is_empty() {
146
                Ok(c)
147
            } else {
148
                Err(err("takes no arguments"))
149
            }
150
        };
151
        let dir = || -> Result<Dir, ParseError> {
152
            match one()?.as_str() {
153
                "left" | "h" => Ok(Dir::Left),
154
                "right" | "l" => Ok(Dir::Right),
155
                "up" | "k" => Ok(Dir::Up),
156
                "down" | "j" => Ok(Dir::Down),
157
                d => Err(err(&format!("unknown direction {d}"))),
158
            }
159
        };
160
        Ok(match name {
161
            "spawn" if !args.is_empty() => Command::Spawn {
162
                argv: args.iter().map(|a| a.to_string()).collect(),
163
            },
164
            "spawn" => return Err(err("needs a program")),
165
            "close" => none(Command::Close)?,
166
            "quit" => none(Command::Quit)?,
167
            "reload" => none(Command::Reload)?,
168
            "flip" => none(Command::Flip)?,
169
            "maximize" => none(Command::Maximize)?,
170
            "invert-move" => none(Command::InvertMove)?,
171
            "float" => none(Command::Float)?,
172
            "fullscreen" => none(Command::Fullscreen)?,
173
            "sticky" => none(Command::Sticky)?,
174
            "focus" => Command::Focus { dir: dir()? },
175
            "move" => Command::Move { dir: dir()? },
176
            "move-container" => Command::MoveContainer { dir: dir()? },
177
            "focus-output" => Command::FocusOutput { dir: dir()? },
178
            "send-to-output" => Command::SendToOutput { dir: dir()? },
179
            "resize" => {
180
                let [axis, delta] = args else {
181
                    return Err(err("takes an axis and a delta"));
182
                };
183
                let axis = match *axis {
184
                    "horizontal" | "h" => Axis::Horizontal,
185
                    "vertical" | "v" => Axis::Vertical,
186
                    a => return Err(err(&format!("unknown axis {a}"))),
187
                };
188
                let delta = delta
189
                    .parse()
190
                    .map_err(|_| err(&format!("bad delta {delta}")))?;
191
                Command::Resize { axis, delta }
192
            }
193
            "expand" => Command::Expand {
194
                scope: match args {
195
                    [] | ["window"] => ExpandScope::Window,
196
                    ["container"] => ExpandScope::Container,
197
                    _ => return Err(err("takes window or container")),
198
                },
199
            },
200
            "view" => Command::View { tag: one()? },
201
            "toggle-view" => Command::ToggleView { tag: one()? },
202
            "view-everywhere" => Command::ViewEverywhere { tag: one()? },
203
            "tag" => Command::Tag { tag: one()? },
204
            "reorder-tag" => {
205
                let [tag, index] = args else {
206
                    return Err(err("takes a tag and an index"));
207
                };
208
                let index = index
209
                    .parse()
210
                    .map_err(|_| err(&format!("bad index {index}")))?;
211
                Command::ReorderTag {
212
                    tag: tag.to_string(),
213
                    index,
214
                }
215
            }
216
            "scratchpad" => Command::Scratchpad { name: one()? },
217
            "summon" => Command::Summon { name: one()? },
218
            "dismiss" => Command::Dismiss {
219
                name: match args {
220
                    [] => None,
221
                    [n] => Some(n.to_string()),
222
                    _ => return Err(err("takes at most one name")),
223
                },
224
            },
225
            _ => return Err(ParseError(format!("unknown command {name}"))),
226
        })
227
    }
228
}
229
230
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
231
#[serde(rename_all = "kebab-case")]
232
pub enum Request {
233
    Run(Command),
234
    Get(Query),
235
    /// Streams a `State` event after every change until the client hangs up.
236
    Subscribe {},
237
}
238
239
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
240
#[serde(rename_all = "kebab-case")]
241
pub enum Query {
242
    State,
243
    Version,
244
}
245
246
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
247
pub struct Envelope<T> {
248
    pub v: u32,
249
    #[serde(default, skip_serializing_if = "Option::is_none")]
250
    pub id: Option<u64>,
251
    #[serde(flatten)]
252
    pub body: T,
253
}
254
255
impl<T> Envelope<T> {
256
    pub fn new(id: Option<u64>, body: T) -> Self {
257
        Self {
258
            v: VERSION,
259
            id,
260
            body,
261
        }
262
    }
263
}
264
265
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
266
#[serde(rename_all = "kebab-case")]
267
pub enum Reply {
268
    Ok(Option<Answer>),
269
    Error(String),
270
    Event(State),
271
}
272
273
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
274
#[serde(rename_all = "kebab-case")]
275
pub enum Answer {
276
    State(State),
277
    Version(String),
278
}
279
280
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
281
pub struct State {
282
    /// The next window move does the opposite of the configured default.
283
    pub move_inverted: bool,
284
    pub tags: Vec<Tag>,
285
    pub outputs: Vec<Output>,
286
    pub windows: Vec<Window>,
287
}
288
289
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
290
pub struct Tag {
291
    pub name: String,
292
    pub urgent: bool,
293
}
294
295
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
296
pub struct Output {
297
    pub name: String,
298
    /// x, y, width, height in logical pixels.
299
    pub area: [f64; 4],
300
    pub viewed: Vec<String>,
301
    pub active: bool,
302
}
303
304
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
305
pub struct Window {
306
    pub id: u64,
307
    pub app_id: String,
308
    pub title: String,
309
    pub tag: String,
310
    pub output: Option<String>,
311
    pub floating: bool,
312
    pub fullscreen: bool,
313
    pub sticky: bool,
314
    pub scratchpad: Option<String>,
315
    pub visible: bool,
316
    pub focused: bool,
317
}