i3status_rs/blocks/
menu.rs

1//! A custom menu
2//!
3//! This block allows you to quickly run a custom shell command. Left-click on this block to
4//! activate it, then scroll through configured items. Left-click on the item to run it and
5//! optionally confirm your action by left-clicking again. Right-click any time to deactivate this
6//! block.
7//!
8//! # Configuration
9//!
10//! Key | Values | Default
11//! ----|--------|--------
12//! `text` | Text that will be displayed when the block is inactive. | **Required**
13//! `items` | A list of "items". See examples below. | **Required**
14//!
15//! # Example
16//!
17//! ```toml
18//! [[block]]
19//! block = "menu"
20//! text = "\uf011"
21//! [[block.items]]
22//! display = " ->   Sleep   <-"
23//! cmd = "systemctl suspend"
24//! [[block.items]]
25//! display = " -> Power Off <-"
26//! cmd = "poweroff"
27//! confirm_msg = "Are you sure you want to power off?"
28//! [[block.items]]
29//! display = " ->  Reboot   <-"
30//! cmd = "reboot"
31//! confirm_msg = "Are you sure you want to reboot?"
32//! ```
33
34use tokio::sync::mpsc::UnboundedReceiver;
35
36use super::{BlockAction, prelude::*};
37use crate::subprocess::spawn_shell;
38
39#[derive(Deserialize, Debug)]
40#[serde(deny_unknown_fields)]
41pub struct Config {
42    pub text: String,
43    pub items: Vec<Item>,
44}
45
46#[derive(Deserialize, Debug, Clone)]
47#[serde(deny_unknown_fields)]
48pub struct Item {
49    pub display: String,
50    pub cmd: String,
51    #[serde(default)]
52    pub confirm_msg: Option<String>,
53}
54
55struct Block<'a> {
56    actions: UnboundedReceiver<BlockAction>,
57    api: &'a CommonApi,
58    text: &'a str,
59    items: &'a [Item],
60}
61
62impl Block<'_> {
63    async fn reset(&mut self) -> Result<()> {
64        self.set_text(self.text.to_owned()).await
65    }
66
67    async fn set_text(&mut self, text: String) -> Result<()> {
68        self.api.set_widget(Widget::new().with_text(text))
69    }
70
71    async fn wait_for_click(&mut self, button: &str) -> Result<()> {
72        while self.actions.recv().await.error("channel closed")? != button {}
73        Ok(())
74    }
75
76    async fn run_menu(&mut self) -> Result<Option<Item>> {
77        let mut index = 0;
78        loop {
79            self.set_text(self.items[index].display.clone()).await?;
80            match &*self.actions.recv().await.error("channel closed")? {
81                "_up" => index += 1,
82                "_down" => index += self.items.len() + 1,
83                "_left" => return Ok(Some(self.items[index].clone())),
84                "_right" => return Ok(None),
85                _ => (),
86            }
87            index %= self.items.len();
88        }
89    }
90
91    async fn confirm(&mut self, msg: String) -> Result<bool> {
92        self.set_text(msg).await?;
93        Ok(self.actions.recv().await.as_deref() == Some("_left"))
94    }
95}
96
97pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
98    api.set_default_actions(&[
99        (MouseButton::Left, None, "_left"),
100        (MouseButton::Right, None, "_right"),
101        (MouseButton::WheelUp, None, "_up"),
102        (MouseButton::WheelDown, None, "_down"),
103    ])?;
104
105    let mut block = Block {
106        actions: api.get_actions()?,
107        api,
108        text: &config.text,
109        items: &config.items,
110    };
111
112    loop {
113        block.reset().await?;
114        block.wait_for_click("_left").await?;
115        if let Some(res) = block.run_menu().await? {
116            if let Some(msg) = res.confirm_msg {
117                if !block.confirm(msg).await? {
118                    continue;
119                }
120            }
121            spawn_shell(&res.cmd).or_error(|| format!("Failed to run '{}'", res.cmd))?;
122        }
123    }
124}