atagen

monolith

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