Skip to main content

i3status_rs/blocks/
scratchpad.rs

1//! Scratchpad indicator
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `format`          | A string to customise the output of this block | ` $icon $count.eng(range:1..) \|`
8//!
9//! Placeholder | Value                                      | Type   | Unit
10//! ------------|--------------------------------------------|--------|-----
11//! `icon`      | A static icon                              | Icon   | -
12//! `count`     | Number of windows in scratchpad            | Number | -
13//!
14//! # Example
15//!
16//! ```toml
17//! [[block]]
18//! block = "scratchpad"
19//! ```
20//!
21//! # Icons Used
22//! - `scratchpad` (`$icon`)
23
24use swayipc_async::{Connection, Event as SwayEvent, EventType, Node, WindowChange};
25
26use super::prelude::*;
27
28#[derive(Deserialize, Debug, SmartDefault)]
29#[serde(deny_unknown_fields, default)]
30pub struct Config {
31    pub format: FormatConfig,
32}
33
34fn count_scratchpad_windows(node: &Node) -> usize {
35    node.find_as_ref(|n| n.name.as_deref() == Some("__i3_scratch"))
36        .map_or(0, |node| node.floating_nodes.len())
37}
38
39pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
40    BlockPlan::new(vec![
41        OutputPlan::new(
42            "main",
43            config
44                .format
45                .with_default(" $icon $count.eng(range:1..) |")?,
46        )
47        .icon("icon", IconChoices::one(icons::SCRATCHPAD)),
48    ])
49}
50
51pub(crate) async fn run(_config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
52    let output_main = plan.output("main")?;
53
54    let connection_for_events = Connection::new()
55        .await
56        .error("failed to open connection with swayipc")?;
57
58    let mut connection_for_tree = Connection::new()
59        .await
60        .error("failed to open connection with swayipc")?;
61
62    let mut events = connection_for_events
63        .subscribe(&[EventType::Window])
64        .await
65        .error("could not subscribe to window events")?;
66
67    loop {
68        let mut widget = output_main.new_widget();
69
70        let root_node = connection_for_tree
71            .get_tree()
72            .await
73            .error("could not get windows tree")?;
74        let count = count_scratchpad_windows(&root_node);
75
76        widget.set_values(map! {
77            "icon" => Value::icon(icons::SCRATCHPAD),
78            "count" => Value::number(count),
79        });
80
81        api.set_widget(widget)?;
82
83        loop {
84            let event = events
85                .next()
86                .await
87                .error("swayipc channel closed")?
88                .error("bad event")?;
89
90            match event {
91                SwayEvent::Window(e) if e.change == WindowChange::Move => break,
92                _ => continue,
93            }
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn plan_declares_main_output_with_scratchpad_icon() {
104        let plan = prepare(&Config::default()).unwrap();
105        let declared: Vec<_> = plan.outputs().map(|o| o.id()).collect();
106        assert_eq!(declared, ["main"]);
107        let output = plan.output("main").unwrap();
108        assert_eq!(output.single_icon("icon").unwrap(), "scratchpad");
109    }
110
111    #[test]
112    fn custom_format_is_respected() {
113        let config = Config {
114            format: " $count ".parse().unwrap(),
115        };
116        let plan = prepare(&config).unwrap();
117        let output = plan.output("main").unwrap();
118        assert!(output.format().contains_key("count"));
119        assert!(!output.format().contains_key("icon"));
120    }
121}