i3status_rs/
blocks.rs

1//! The collection of blocks
2//!
3//! Blocks are defined as a [TOML array of tables](https://github.com/toml-lang/toml/blob/main/toml.md#user-content-array-of-tables): `[[block]]`
4//!
5//! Key | Description | Default
6//! ----|-------------|----------
7//! `block` | Name of the i3status-rs block you want to use. See [modules](#modules) below for valid block names. | -
8//! `signal` | Signal value that causes an update for this block with `0` corresponding to `-SIGRTMIN+0` and the largest value being `-SIGRTMAX` | None
9//! `if_command` | Only display the block if the supplied command returns 0 on startup. | None
10//! `merge_with_next` | If true this will group the block with the next one, so rendering such as alternating_tint will apply to the whole group | `false`
11//! `icons_format` | Overrides global `icons_format` | None
12//! `error_format` | Overrides global `error_format` | None
13//! `error_fullscreen_format` | Overrides global `error_fullscreen_format` | None
14//! `error_interval` | How long to wait until restarting the block after an error occurred. | `5`
15//! `[block.theme_overrides]` | Same as the top-level config option, but for this block only. Refer to `Themes and Icons` below. | None
16//! `[block.icons_overrides]` | Same as the top-level config option, but for this block only. Refer to `Themes and Icons` below. | None
17//! `[[block.click]]` | Set or override click action for the block. See below for details. | Block default / None
18//!
19//! Per block click configuration `[[block.click]]`:
20//!
21//! Key | Description | Default
22//! ----|-------------|----------
23//! `button` | `left`, `middle`, `right`, `up`/`wheel_up`, `down`/`wheel_down`, `wheel_left`, `wheel_right`, `forward`, `back` or [`double_left`](MouseButton). | -
24//! `widget` | To which part of the block this entry applies (accepts regex) | `"block"`
25//! `cmd` | Command to run when the mouse button event is detected. | None
26//! `action` | Which block action to trigger | None
27//! `sync` | Whether to wait for command to exit or not. | `false`
28//! `update` | Whether to update the block on click. | `false`
29
30mod prelude;
31
32use futures::future::FutureExt as _;
33use futures::stream::FuturesUnordered;
34use serde::de::{self, Deserialize};
35use tokio::sync::{Notify, mpsc};
36
37use std::borrow::Cow;
38use std::sync::Arc;
39use std::time::Duration;
40
41use crate::click::MouseButton;
42use crate::errors::*;
43use crate::widget::Widget;
44use crate::{BoxedFuture, Request, RequestCmd};
45
46macro_rules! define_blocks {
47    {
48        $(
49            $(#[cfg(feature = $feat: literal)])?
50            $(#[deprecated($($dep_k: ident = $dep_v: literal),+)])?
51            $block: ident $(,)?
52        )*
53    } => {
54        $(
55            $(#[cfg(feature = $feat)])?
56            $(#[cfg_attr(docsrs, doc(cfg(feature = $feat)))])?
57            $(#[deprecated($($dep_k = $dep_v),+)])?
58            pub mod $block;
59        )*
60
61        #[derive(Debug)]
62        pub enum BlockConfig {
63            $(
64                $(#[cfg(feature = $feat)])?
65                #[allow(non_camel_case_types)]
66                #[allow(deprecated)]
67                $block($block::Config),
68            )*
69            Err(&'static str, Error),
70        }
71
72        impl BlockConfig {
73            pub fn name(&self) -> &'static str {
74                match self {
75                    $(
76                        $(#[cfg(feature = $feat)])?
77                        Self::$block { .. } => stringify!($block),
78                    )*
79                    Self::Err(name, _err) => name,
80                }
81            }
82
83            pub fn spawn(self, api: CommonApi, futures: &mut FuturesUnordered<BoxedFuture<()>>) {
84                match self {
85                    $(
86                        $(#[cfg(feature = $feat)])?
87                        #[allow(deprecated)]
88                        Self::$block(config) => futures.push(async move {
89                            while let Err(err) = $block::run(&config, &api).await {
90                                if api.set_error(err).is_err() {
91                                    return;
92                                }
93                                tokio::select! {
94                                    _ = tokio::time::sleep(api.error_interval) => (),
95                                    _ = api.wait_for_update_request() => (),
96                                }
97                            }
98                        }.boxed_local()),
99                    )*
100                    Self::Err(_name, err) => {
101                        let _ = api.set_error(Error {
102                            message: Some("Configuration error".into()),
103                            cause: Some(Arc::new(err)),
104                        });
105                    },
106                }
107            }
108        }
109
110        impl<'de> Deserialize<'de> for BlockConfig {
111            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
112            where
113                D: de::Deserializer<'de>,
114            {
115                use de::Error as _;
116
117                let mut table = toml::Table::deserialize(deserializer)?;
118                let block_name = table.remove("block").ok_or_else(|| D::Error::missing_field("block"))?;
119                let block_name = block_name.as_str().ok_or_else(|| D::Error::custom("block must be a string"))?;
120
121                match block_name {
122                    $(
123                        $(#[cfg(feature = $feat)])?
124                        #[allow(deprecated)]
125                        stringify!($block) => match $block::Config::deserialize(table) {
126                            Ok(config) => Ok(BlockConfig::$block(config)),
127                            Err(err) => Ok(BlockConfig::Err(stringify!($block), crate::errors::Error::new(err.to_string()))),
128                        }
129                        $(
130                            #[cfg(not(feature = $feat))]
131                            stringify!($block) => Err(D::Error::custom(format!(
132                                "block {} is behind a feature gate '{}' which must be enabled at compile time",
133                                stringify!($block),
134                                $feat,
135                            ))),
136                        )?
137                    )*
138                    other => Err(D::Error::custom(format!("unknown block '{other}'")))
139                }
140            }
141        }
142    };
143}
144
145define_blocks!(
146    amd_gpu,
147    backlight,
148    battery,
149    bluetooth,
150    calendar,
151    cpu,
152    custom,
153    custom_dbus,
154    disk_iostats,
155    disk_space,
156    docker,
157    external_ip,
158    focused_window,
159    github,
160    hueshift,
161    kdeconnect,
162    load,
163    #[cfg(feature = "maildir")]
164    maildir,
165    menu,
166    memory,
167    music,
168    net,
169    notify,
170    #[cfg(feature = "notmuch")]
171    notmuch,
172    nvidia_gpu,
173    packages,
174    pomodoro,
175    privacy,
176    rofication,
177    service_status,
178    scratchpad,
179    sound,
180    speedtest,
181    keyboard_layout,
182    taskwarrior,
183    temperature,
184    time,
185    tea_timer,
186    toggle,
187    uptime,
188    vpn,
189    watson,
190    weather,
191    xrandr,
192);
193
194/// An error which originates from a block
195#[derive(Debug, thiserror::Error)]
196#[error("In block {}: {}", .block_name, .error)]
197pub struct BlockError {
198    pub block_id: usize,
199    pub block_name: &'static str,
200    pub error: Error,
201}
202
203pub type BlockAction = Cow<'static, str>;
204
205#[derive(Clone)]
206pub struct CommonApi {
207    pub(crate) id: usize,
208    pub(crate) update_request: Arc<Notify>,
209    pub(crate) request_sender: mpsc::UnboundedSender<Request>,
210    pub(crate) error_interval: Duration,
211}
212
213impl CommonApi {
214    /// Sends the widget to be displayed.
215    pub fn set_widget(&self, widget: Widget) -> Result<()> {
216        self.request_sender
217            .send(Request {
218                block_id: self.id,
219                cmd: RequestCmd::SetWidget(widget),
220            })
221            .error("Failed to send Request")
222    }
223
224    /// Hides the block. Send new widget to make it visible again.
225    pub fn hide(&self) -> Result<()> {
226        self.request_sender
227            .send(Request {
228                block_id: self.id,
229                cmd: RequestCmd::UnsetWidget,
230            })
231            .error("Failed to send Request")
232    }
233
234    /// Sends the error to be displayed.
235    pub fn set_error(&self, error: Error) -> Result<()> {
236        self.request_sender
237            .send(Request {
238                block_id: self.id,
239                cmd: RequestCmd::SetError(error),
240            })
241            .error("Failed to send Request")
242    }
243
244    pub fn set_default_actions(
245        &self,
246        actions: &'static [(MouseButton, Option<&'static str>, &'static str)],
247    ) -> Result<()> {
248        self.request_sender
249            .send(Request {
250                block_id: self.id,
251                cmd: RequestCmd::SetDefaultActions(actions),
252            })
253            .error("Failed to send Request")
254    }
255
256    pub fn get_actions(&self) -> Result<mpsc::UnboundedReceiver<BlockAction>> {
257        let (tx, rx) = mpsc::unbounded_channel();
258        self.request_sender
259            .send(Request {
260                block_id: self.id,
261                cmd: RequestCmd::SubscribeToActions(tx),
262            })
263            .error("Failed to send Request")?;
264        Ok(rx)
265    }
266
267    pub async fn wait_for_update_request(&self) {
268        self.update_request.notified().await;
269    }
270}