lib_bspwm/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::{fmt::Display, process::Command};
3use thiserror::Error;
4
5#[derive(Debug, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub enum Layout {
8    Tiled,
9    PsuedoTiled,
10    Floating,
11    Monocle,
12}
13
14impl Default for Layout {
15    fn default() -> Self {
16        Self::Tiled
17    }
18}
19
20impl Display for Layout {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            Self::Tiled => write!(f, "tiled"),
24            Self::PsuedoTiled => write!(f, "pseudo-tiled"),
25            Self::Floating => write!(f, "floating"),
26            Self::Monocle => write!(f, "monocle"),
27        }
28    }
29}
30
31#[derive(Debug, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub enum SplitType {
34    Vertical,
35    Horizontal,
36}
37
38impl Default for SplitType {
39    fn default() -> Self {
40        Self::Vertical
41    }
42}
43
44impl Display for SplitType {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            SplitType::Vertical => write!(f, "vertical"),
48            SplitType::Horizontal => write!(f, "horizontal"),
49        }
50    }
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub enum Layer {
56    Normal,
57}
58
59impl Default for Layer {
60    fn default() -> Self {
61        Self::Normal
62    }
63}
64
65impl Display for Layer {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            Layer::Normal => write!(f, "normal"),
69        }
70    }
71}
72
73#[derive(Debug, Default, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct Constraints {
76    pub min_width: usize,
77    pub min_height: usize,
78}
79
80#[derive(Debug, Default, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct Padding {
83    pub top: usize,
84    pub right: usize,
85    pub bottom: usize,
86    pub left: usize,
87}
88
89#[derive(Debug, Default, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct Rectangle {
92    pub x: usize,
93    pub y: usize,
94    pub width: usize,
95    pub height: usize,
96}
97
98#[derive(Debug, Default, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct Focus {
101    monitor_id: usize,
102    desktop_id: usize,
103    node_id: usize,
104}
105
106#[derive(Debug, Default, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct State {
109    pub focused_monitor_id: usize,
110    pub primary_monitor_id: usize,
111    pub clients_count: usize,
112    pub monitors: Vec<Monitor>,
113    pub focus_history: Vec<Focus>,
114    pub stacking_list: Vec<usize>,
115}
116
117#[derive(Debug, Default, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct Monitor {
120    pub name: String,
121    pub id: usize,
122    pub randr_id: usize,
123    pub wired: bool,
124    pub sticky_count: usize,
125    pub window_gap: usize,
126    pub border_width: usize,
127    pub focused_desktop_id: usize,
128    pub desktops: Vec<Desktop>,
129}
130
131#[derive(Debug, Default, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct Desktop {
134    pub name: String,
135    pub id: usize,
136    pub layout: Layout,
137    pub user_layout: Layout,
138    pub window_gap: usize,
139    pub border_width: usize,
140    pub focused_node_id: usize,
141    pub padding: Padding,
142    pub root: Option<Tree>,
143}
144
145#[derive(Debug, Default, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct Tree {
148    pub id: usize,
149    pub split_type: SplitType,
150    pub vacant: bool,
151    pub hidden: bool,
152    pub sticky: bool,
153    pub private: bool,
154    pub locked: bool,
155    pub marked: bool,
156    pub presel: Option<String>,
157    pub rectangle: Rectangle,
158}
159
160#[derive(Debug, Default, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct Node {
163    pub id: usize,
164    pub split_type: SplitType,
165    pub split_ratio: f32,
166    pub vacant: bool,
167    pub hidden: bool,
168    pub sticky: bool,
169    pub private: bool,
170    pub locked: bool,
171    pub marked: bool,
172    pub presel: Option<String>,
173    pub rectangle: Rectangle,
174    pub constrainnts: Option<Constraints>,
175    pub first_child: Option<Box<Node>>,
176    pub second_child: Option<Box<Node>>,
177    pub client: Client,
178}
179
180#[derive(Debug, Default, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct Client {
183    pub class_name: String,
184    pub instance_name: String,
185    pub border_width: usize,
186    pub state: Layout,
187    pub last_state: Layout,
188    pub layer: Layer,
189    pub last_layer: Layer,
190    pub urgent: bool,
191    pub shown: bool,
192    pub tiled_rectangle: Rectangle,
193    pub floating_rectangle: Rectangle,
194}
195
196#[derive(Debug, Error)]
197pub enum Error {
198    #[error("Failed to deserialize output from `bspc wm -d` or `bspc query <DOMAIN>`")]
199    Deserialize,
200
201    #[error(transparent)]
202    IoError(#[from] std::io::Error),
203}
204
205const PROG: &'static str = "bspc";
206const DUMP_ARGS: &'static [&'static str; 2] = &["wm", "-d"];
207const QUERY_ARGS: &'static [&'static str; 1] = &["query"];
208const QUERY_TREE_FLAG: &'static str = "-T";
209const MON_SEL_QUERY_FLAG: &'static str = "-m";
210const DESK_SEL_QUERY_FLAG: &'static str = "-d";
211const NODE_SEL_QUERY_FLAG: &'static str = "-n";
212const _MON_ID_OUT_FLAG: &'static str = "-M";
213const _DESK_ID_OUT_FLAG: &'static str = "-D";
214const _NODE_ID_OUT_FLAG: &'static str = "-N";
215
216pub fn dump_state() -> Result<State, self::Error> {
217    match serde_json::from_slice::<State>(&Command::new(PROG).args(DUMP_ARGS).output()?.stdout) {
218        Ok(state) => Ok(state),
219        Err(_) => Err(self::Error::Deserialize),
220    }
221}
222
223#[derive(Debug)]
224pub struct Query {
225    args: Vec<String>,
226}
227
228impl Query {
229    pub fn new() -> Self {
230        Self {
231            args: QUERY_ARGS.iter().map(|s| s.to_string()).collect(),
232        }
233    }
234
235    pub fn monitor(mut self, name: &str) -> Self {
236        if name.is_empty() {
237            return self;
238        }
239
240        // Make a new Vec so that we're aren't passing multiple domain selectors.
241        // `bspc` doesn't like it.
242        let mut args: Vec<String> = QUERY_ARGS.iter().map(|s| s.to_string()).collect();
243        args.push(MON_SEL_QUERY_FLAG.to_string());
244        args.push(name.to_string());
245        self.args = args;
246
247        self
248    }
249
250    pub fn desktop(mut self, name: &str) -> Self {
251        if name.is_empty() {
252            return self;
253        }
254
255        let mut args: Vec<String> = QUERY_ARGS.iter().map(|s| s.to_string()).collect();
256        args.push(DESK_SEL_QUERY_FLAG.to_string());
257        args.push(name.to_string());
258        self.args = args;
259
260        self
261    }
262
263    pub fn node(mut self, id: usize) -> Self {
264        let mut args: Vec<String> = QUERY_ARGS.iter().map(|s| s.to_string()).collect();
265        args.push(NODE_SEL_QUERY_FLAG.to_string());
266        args.push(id.to_string());
267        self.args = args;
268
269        self
270    }
271
272    fn _get_monitor_ids(&mut self) -> Vec<usize> {
273        self.args.push(_MON_ID_OUT_FLAG.to_string());
274        todo!()
275    }
276
277    fn _get_desktop_ids(&mut self) -> Vec<usize> {
278        self.args.push(_DESK_ID_OUT_FLAG.to_string());
279        todo!()
280    }
281
282    fn _get_node_ids(&mut self) -> Vec<usize> {
283        self.args.push(_NODE_ID_OUT_FLAG.to_string());
284        todo!()
285    }
286
287    pub fn get_monitor_tree(&mut self) -> Result<Monitor, self::Error> {
288        // `bspc` requires at least one domain flag, even if it's passed
289        // parameter-less, so here we count the flags and add a single `-m` if
290        // necessary.
291        if self.args.len() == 1 {
292            self.args.push(MON_SEL_QUERY_FLAG.to_string());
293        }
294
295        self.args.push(QUERY_TREE_FLAG.to_string());
296        match serde_json::from_slice::<Monitor>(
297            &Command::new(PROG).args(&self.args).output()?.stdout,
298        ) {
299            Ok(m) => Ok(m),
300            Err(_) => Err(self::Error::Deserialize),
301        }
302    }
303
304    pub fn get_desktop_tree(&mut self) -> Result<Desktop, self::Error> {
305        if self.args.len() == 1 {
306            self.args.push(DESK_SEL_QUERY_FLAG.to_string());
307        }
308
309        self.args.push(QUERY_TREE_FLAG.to_string());
310        match serde_json::from_slice::<Desktop>(
311            &Command::new(PROG).args(&self.args).output()?.stdout,
312        ) {
313            Ok(d) => Ok(d),
314            Err(_) => Err(self::Error::Deserialize),
315        }
316    }
317
318    pub fn get_node_tree(&mut self) -> Result<Node, self::Error> {
319        if self.args.len() == 1 {
320            self.args.push(NODE_SEL_QUERY_FLAG.to_string());
321        }
322
323        self.args.push(QUERY_TREE_FLAG.to_string());
324        match serde_json::from_slice::<Node>(&Command::new(PROG).args(&self.args).output()?.stdout)
325        {
326            Ok(n) => Ok(n),
327            Err(_) => Err(self::Error::Deserialize),
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use std::process::Command;
335
336    use crate::{dump_state, Query, DUMP_ARGS, PROG, QUERY_ARGS};
337
338    #[test]
339    fn dump_args() {
340        let mut args = vec![PROG];
341        for arg in DUMP_ARGS {
342            args.push(arg)
343        }
344
345        assert_eq!(args, vec![PROG, "wm", "-d"])
346    }
347
348    #[test]
349    fn query_args() {
350        let mut args = vec![PROG];
351        for arg in QUERY_ARGS {
352            args.push(arg)
353        }
354
355        assert_eq!(args, vec![PROG, "query"])
356    }
357
358    #[test]
359    fn outputs() {
360        if let Err(e) = Command::new(PROG).args(DUMP_ARGS).output() {
361            assert!(false, "{}", e)
362        }
363    }
364
365    #[test]
366    fn deserializes() {
367        if let Err(e) = dump_state() {
368            assert!(false, "{}", e)
369        }
370    }
371
372    #[test]
373    fn monitor_tree() {
374        let mut query = Query::new();
375        if let Err(e) = query.get_monitor_tree() {
376            eprintln!("{:#?}", query);
377            assert!(false, "{}", e)
378        }
379    }
380
381    #[test]
382    fn desktop_tree() {
383        let mut query = Query::new();
384        if let Err(e) = query.get_desktop_tree() {
385            eprintln!("{:#?}", query);
386            assert!(false, "{}", e)
387        }
388    }
389
390    #[test]
391    fn node_tree() {
392        let mut query = Query::new();
393        if let Err(e) = query.get_node_tree() {
394            eprintln!("{:#?}", query);
395            assert!(false, "{}", e)
396        }
397    }
398
399    #[test]
400    fn with_param() {
401        let query = Query::new().monitor("testies");
402        assert_eq!(query.args, vec!["query", "-m", "testies"])
403    }
404
405    #[test]
406    fn try_many_param() {
407        let query = Query::new().monitor("discarded").monitor("testies");
408        assert_eq!(query.args, vec!["query", "-m", "testies"])
409    }
410}