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//! `max_retries` | How many times should a block be restarted the block after an error occurred. If no limit is specified none will be enforced. | `None`
16//! `[block.theme_overrides]` | Same as the top-level config option, but for this block only. Refer to `Themes and Icons` below. | None
17//! `[block.icons_overrides]` | Same as the top-level config option, but for this block only. Refer to `Themes and Icons` below. | None
18//! `[[block.click]]` | Set or override click action for the block. See below for details. | Block default / None
19//!
20//! Per block click configuration `[[block.click]]`:
21//!
22//! Key | Description | Default
23//! ----|-------------|----------
24//! `button` | `left`, `middle`, `right`, `up`/`wheel_up`, `down`/`wheel_down`, `wheel_left`, `wheel_right`, `forward`, `back` or [`double_left`](MouseButton). | -
25//! `widget` | To which part of the block this entry applies (accepts regex) | `"block"`
26//! `cmd` | Command to run when the mouse button event is detected. | None
27//! `action` | Which block action to trigger | None
28//! `sync` | Whether to wait for command to exit or not. | `false`
29//! `update` | Whether to update the block on click. | `false`
30
31mod prelude;
32
33use futures::future::FutureExt as _;
34use futures::stream::FuturesUnordered;
35use serde::de::{self, Deserialize};
36use tokio::sync::{Notify, mpsc};
37
38use std::borrow::Cow;
39use std::sync::Arc;
40use std::time::Duration;
41
42use crate::click::MouseButton;
43use crate::errors::*;
44use crate::geolocator::{Geolocator, IPAddressInfo};
45use crate::widget::Widget;
46use crate::{BoxedFuture, Request, RequestCmd};
47
48pub(super) const RESTART_BLOCK_BTN: &str = "restart_block_btn";
49
50macro_rules! define_blocks {
51    {
52        $(
53            $(#[cfg(feature = $feat: literal)])?
54            $(#[deprecated($($dep_k: ident = $dep_v: literal),+)])?
55            $block: ident $(,)?
56        )*
57    } => {
58        $(
59            $(#[cfg(feature = $feat)])?
60            $(#[cfg_attr(docsrs, doc(cfg(feature = $feat)))])?
61            $(#[deprecated($($dep_k = $dep_v),+)])?
62            pub mod $block;
63        )*
64
65        #[derive(Debug)]
66        pub enum BlockConfig {
67            $(
68                $(#[cfg(feature = $feat)])?
69                #[allow(non_camel_case_types)]
70                #[allow(deprecated)]
71                $block($block::Config),
72            )*
73            Err(&'static str, Error),
74        }
75
76        impl BlockConfig {
77            pub fn name(&self) -> &'static str {
78                match self {
79                    $(
80                        $(#[cfg(feature = $feat)])?
81                        Self::$block { .. } => stringify!($block),
82                    )*
83                    Self::Err(name, _err) => name,
84                }
85            }
86
87            pub fn spawn(self, api: CommonApi, futures: &mut FuturesUnordered<BoxedFuture<()>>) {
88                match self {
89                    $(
90                        $(#[cfg(feature = $feat)])?
91                        #[allow(deprecated)]
92                        Self::$block(config) => futures.push(async move {
93                            let mut error_count: u8 = 0;
94                            while let Err(mut err) = $block::run(&config, &api).await {
95                                let Ok(mut actions) = api.get_actions() else { return };
96                                if api.set_default_actions(&[
97                                    (MouseButton::Left, Some(RESTART_BLOCK_BTN), "error_count_reset"),
98                                ]).is_err() {
99                                    return;
100                                }
101                                let should_retry = api
102                                    .max_retries
103                                    .map_or(true, |max_retries| error_count < max_retries);
104                                if !should_retry {
105                                    err = Error {
106                                        message: Some("Block terminated".into()),
107                                        cause: Some(Arc::new(err)),
108                                    };
109                                }
110                                if api.set_error_with_restartable(err, !should_retry).is_err() {
111                                    return;
112                                }
113                                tokio::select! {
114                                    _ = tokio::time::sleep(api.error_interval), if should_retry => (),
115                                    Some(action) = actions.recv(), if !should_retry  => match action.as_ref(){
116                                        "error_count_reset" => {
117                                            error_count = 0;
118                                        },
119                                        _ => (),
120                                    },
121                                    _ = api.wait_for_update_request() => (),
122                                }
123                                error_count = error_count.saturating_add(1);
124                            }
125                        }.boxed_local()),
126                    )*
127                    Self::Err(_name, err) => {
128                        let _ = api.set_error(Error {
129                            message: Some("Configuration error".into()),
130                            cause: Some(Arc::new(err)),
131                        });
132                    },
133                }
134            }
135        }
136
137        impl<'de> Deserialize<'de> for BlockConfig {
138            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
139            where
140                D: de::Deserializer<'de>,
141            {
142                use de::Error as _;
143
144                let mut table = toml::Table::deserialize(deserializer)?;
145                let block_name = table.remove("block").ok_or_else(|| D::Error::missing_field("block"))?;
146                let block_name = block_name.as_str().ok_or_else(|| D::Error::custom("block must be a string"))?;
147
148                match block_name {
149                    $(
150                        $(#[cfg(feature = $feat)])?
151                        #[allow(deprecated)]
152                        stringify!($block) => match $block::Config::deserialize(table) {
153                            Ok(config) => Ok(BlockConfig::$block(config)),
154                            Err(err) => Ok(BlockConfig::Err(stringify!($block), crate::errors::Error::new(err.to_string()))),
155                        }
156                        $(
157                            #[cfg(not(feature = $feat))]
158                            stringify!($block) => Err(D::Error::custom(format!(
159                                "block {} is behind a feature gate '{}' which must be enabled at compile time",
160                                stringify!($block),
161                                $feat,
162                            ))),
163                        )?
164                    )*
165                    other => Err(D::Error::custom(format!("unknown block '{other}'")))
166                }
167            }
168        }
169    };
170}
171
172define_blocks!(
173    amd_gpu,
174    backlight,
175    battery,
176    bluetooth,
177    calendar,
178    cpu,
179    custom,
180    custom_dbus,
181    disk_iostats,
182    disk_space,
183    docker,
184    external_ip,
185    focused_window,
186    github,
187    hueshift,
188    kdeconnect,
189    load,
190    #[cfg(feature = "maildir")]
191    maildir,
192    menu,
193    memory,
194    music,
195    net,
196    notify,
197    #[cfg(feature = "notmuch")]
198    notmuch,
199    nvidia_gpu,
200    packages,
201    pomodoro,
202    privacy,
203    rofication,
204    service_status,
205    scratchpad,
206    sound,
207    speedtest,
208    keyboard_layout,
209    taskwarrior,
210    temperature,
211    time,
212    tea_timer,
213    toggle,
214    uptime,
215    vpn,
216    watson,
217    weather,
218    xrandr,
219);
220
221/// An error which originates from a block
222#[derive(Debug, thiserror::Error)]
223#[error("In block {}: {}", .block_name, .error)]
224pub struct BlockError {
225    pub block_id: usize,
226    pub block_name: &'static str,
227    pub error: Error,
228}
229
230pub type BlockAction = Cow<'static, str>;
231
232#[derive(Clone)]
233pub struct CommonApi {
234    pub(crate) id: usize,
235    pub(crate) update_request: Arc<Notify>,
236    pub(crate) request_sender: mpsc::UnboundedSender<Request>,
237    pub(crate) error_interval: Duration,
238    pub(crate) geolocator: Arc<Geolocator>,
239    pub(crate) max_retries: Option<u8>,
240}
241
242impl CommonApi {
243    /// Sends the widget to be displayed.
244    pub fn set_widget(&self, widget: Widget) -> Result<()> {
245        self.request_sender
246            .send(Request {
247                block_id: self.id,
248                cmd: RequestCmd::SetWidget(widget),
249            })
250            .error("Failed to send Request")
251    }
252
253    /// Hides the block. Send new widget to make it visible again.
254    pub fn hide(&self) -> Result<()> {
255        self.request_sender
256            .send(Request {
257                block_id: self.id,
258                cmd: RequestCmd::UnsetWidget,
259            })
260            .error("Failed to send Request")
261    }
262
263    /// Sends the error to be displayed, no restart button will be shown.
264    pub fn set_error(&self, error: Error) -> Result<()> {
265        self.set_error_with_restartable(error, false)
266    }
267
268    /// Sends the error to be displayed.
269    pub fn set_error_with_restartable(&self, error: Error, restartable: bool) -> Result<()> {
270        self.request_sender
271            .send(Request {
272                block_id: self.id,
273                cmd: RequestCmd::SetError { error, restartable },
274            })
275            .error("Failed to send Request")
276    }
277
278    pub fn set_default_actions(
279        &self,
280        actions: &'static [(MouseButton, Option<&'static str>, &'static str)],
281    ) -> Result<()> {
282        self.request_sender
283            .send(Request {
284                block_id: self.id,
285                cmd: RequestCmd::SetDefaultActions(actions),
286            })
287            .error("Failed to send Request")
288    }
289
290    pub fn get_actions(&self) -> Result<mpsc::UnboundedReceiver<BlockAction>> {
291        let (tx, rx) = mpsc::unbounded_channel();
292        self.request_sender
293            .send(Request {
294                block_id: self.id,
295                cmd: RequestCmd::SubscribeToActions(tx),
296            })
297            .error("Failed to send Request")?;
298        Ok(rx)
299    }
300
301    pub async fn wait_for_update_request(&self) {
302        self.update_request.notified().await;
303    }
304
305    fn locator_name(&self) -> Cow<'static, str> {
306        self.geolocator.name()
307    }
308
309    /// No-op if last API call was made in the last `interval` seconds.
310    pub async fn find_ip_location(
311        &self,
312        client: &reqwest::Client,
313        interval: Duration,
314    ) -> Result<IPAddressInfo> {
315        self.geolocator.find_ip_location(client, interval).await
316    }
317}