Skip to main content

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                            // The block's declared output contract, resolved
94                            // once and passed into run. Every block module
95                            // must define `prepare(config) ->
96                            // Result<Arc<BlockPlan>>`; a block without one
97                            // fails to compile here.
98                            let plan = match $block::prepare(&config) {
99                                Ok(plan) => plan,
100                                Err(err) => {
101                                    let _ = api.set_error(Error {
102                                        message: Some("Failed to prepare block".into()),
103                                        cause: Some(Arc::new(err)),
104                                    });
105                                    return;
106                                }
107                            };
108                            let mut error_count: u8 = 0;
109                            while let Err(mut err) = $block::run(&config, &api, &plan).await {
110                                let Ok(mut actions) = api.get_actions() else { return };
111                                if api.set_default_actions(&[
112                                    (MouseButton::Left, Some(RESTART_BLOCK_BTN), "error_count_reset"),
113                                ]).is_err() {
114                                    return;
115                                }
116                                let should_retry = api
117                                    .max_retries
118                                    .map_or(true, |max_retries| error_count < max_retries);
119                                if !should_retry {
120                                    err = Error {
121                                        message: Some("Block terminated".into()),
122                                        cause: Some(Arc::new(err)),
123                                    };
124                                }
125                                if api.set_error_with_restartable(err, !should_retry).is_err() {
126                                    return;
127                                }
128                                tokio::select! {
129                                    _ = tokio::time::sleep(api.error_interval), if should_retry => (),
130                                    Some(action) = actions.recv(), if !should_retry  => match action.as_ref(){
131                                        "error_count_reset" => {
132                                            error_count = 0;
133                                        },
134                                        _ => (),
135                                    },
136                                    _ = api.wait_for_update_request() => (),
137                                }
138                                error_count = error_count.saturating_add(1);
139                            }
140                        }.boxed_local()),
141                    )*
142                    Self::Err(_name, err) => {
143                        let _ = api.set_error(Error {
144                            message: Some("Configuration error".into()),
145                            cause: Some(Arc::new(err)),
146                        });
147                    },
148                }
149            }
150        }
151
152        impl<'de> Deserialize<'de> for BlockConfig {
153            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
154            where
155                D: de::Deserializer<'de>,
156            {
157                use de::Error as _;
158
159                let mut table = toml::Table::deserialize(deserializer)?;
160                let block_name = table.remove("block").ok_or_else(|| D::Error::missing_field("block"))?;
161                let block_name = block_name.as_str().ok_or_else(|| D::Error::custom("block must be a string"))?;
162
163                match block_name {
164                    $(
165                        $(#[cfg(feature = $feat)])?
166                        #[allow(deprecated)]
167                        stringify!($block) => match $block::Config::deserialize(table) {
168                            Ok(config) => Ok(BlockConfig::$block(config)),
169                            Err(err) => Ok(BlockConfig::Err(stringify!($block), crate::errors::Error::new(err.to_string()))),
170                        }
171                        $(
172                            #[cfg(not(feature = $feat))]
173                            stringify!($block) => Err(D::Error::custom(format!(
174                                "block {} is behind a feature gate '{}' which must be enabled at compile time",
175                                stringify!($block),
176                                $feat,
177                            ))),
178                        )?
179                    )*
180                    other => Err(D::Error::custom(format!("unknown block '{other}'")))
181                }
182            }
183        }
184    };
185}
186
187define_blocks!(
188    amd_gpu,
189    backlight,
190    battery,
191    bluetooth,
192    calendar,
193    cpu,
194    custom,
195    custom_dbus,
196    disk_iostats,
197    disk_space,
198    docker,
199    external_ip,
200    focused_window,
201    github,
202    hueshift,
203    kdeconnect,
204    load,
205    #[cfg(feature = "maildir")]
206    maildir,
207    menu,
208    memory,
209    music,
210    net,
211    notify,
212    #[cfg(feature = "notmuch")]
213    notmuch,
214    nvidia_gpu,
215    packages,
216    pomodoro,
217    privacy,
218    rofication,
219    service_status,
220    scratchpad,
221    sound,
222    speedtest,
223    keyboard_layout,
224    taskwarrior,
225    temperature,
226    time,
227    tea_timer,
228    toggle,
229    uptime,
230    vpn,
231    watson,
232    weather,
233    xrandr,
234);
235
236/// An error which originates from a block
237#[derive(Debug, thiserror::Error)]
238#[error("In block {}: {}", .block_name, .error)]
239pub struct BlockError {
240    pub block_id: usize,
241    pub block_name: &'static str,
242    pub error: Error,
243}
244
245pub type BlockAction = Cow<'static, str>;
246
247#[derive(Clone)]
248pub struct CommonApi {
249    pub(crate) id: usize,
250    pub(crate) update_request: Arc<Notify>,
251    pub(crate) request_sender: mpsc::UnboundedSender<Request>,
252    pub(crate) error_interval: Duration,
253    pub(crate) geolocator: Arc<Geolocator>,
254    pub(crate) max_retries: Option<u8>,
255}
256
257impl CommonApi {
258    /// Sends the widget to be displayed.
259    pub fn set_widget(&self, widget: Widget) -> Result<()> {
260        widget.check_contract()?;
261        self.request_sender
262            .send(Request {
263                block_id: self.id,
264                cmd: RequestCmd::SetWidget(widget),
265            })
266            .error("Failed to send Request")
267    }
268
269    /// Hides the block. Send new widget to make it visible again.
270    pub fn hide(&self) -> Result<()> {
271        self.request_sender
272            .send(Request {
273                block_id: self.id,
274                cmd: RequestCmd::UnsetWidget,
275            })
276            .error("Failed to send Request")
277    }
278
279    /// Sends the error to be displayed, no restart button will be shown.
280    pub fn set_error(&self, error: Error) -> Result<()> {
281        self.set_error_with_restartable(error, false)
282    }
283
284    /// Sends the error to be displayed.
285    pub fn set_error_with_restartable(&self, error: Error, restartable: bool) -> Result<()> {
286        self.request_sender
287            .send(Request {
288                block_id: self.id,
289                cmd: RequestCmd::SetError { error, restartable },
290            })
291            .error("Failed to send Request")
292    }
293
294    pub fn set_default_actions(
295        &self,
296        actions: &'static [(MouseButton, Option<&'static str>, &'static str)],
297    ) -> Result<()> {
298        self.request_sender
299            .send(Request {
300                block_id: self.id,
301                cmd: RequestCmd::SetDefaultActions(actions),
302            })
303            .error("Failed to send Request")
304    }
305
306    pub fn get_actions(&self) -> Result<mpsc::UnboundedReceiver<BlockAction>> {
307        let (tx, rx) = mpsc::unbounded_channel();
308        self.request_sender
309            .send(Request {
310                block_id: self.id,
311                cmd: RequestCmd::SubscribeToActions(tx),
312            })
313            .error("Failed to send Request")?;
314        Ok(rx)
315    }
316
317    pub async fn wait_for_update_request(&self) {
318        self.update_request.notified().await;
319    }
320
321    fn locator_name(&self) -> Cow<'static, str> {
322        self.geolocator.name()
323    }
324
325    fn locator_rate_limit_interval(&self) -> Duration {
326        self.geolocator.rate_limit_interval()
327    }
328
329    /// No-op if last API call was made in the last `interval` seconds.
330    pub async fn find_ip_location(
331        &self,
332        client: &reqwest::Client,
333        interval: Duration,
334    ) -> Result<IPAddressInfo> {
335        self.geolocator.find_ip_location(client, interval).await
336    }
337}