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::geolocator::{Geolocator, IPAddressInfo};
44use crate::widget::Widget;
45use crate::{BoxedFuture, Request, RequestCmd};
46
47macro_rules! define_blocks {
48    {
49        $(
50            $(#[cfg(feature = $feat: literal)])?
51            $(#[deprecated($($dep_k: ident = $dep_v: literal),+)])?
52            $block: ident $(,)?
53        )*
54    } => {
55        $(
56            $(#[cfg(feature = $feat)])?
57            $(#[cfg_attr(docsrs, doc(cfg(feature = $feat)))])?
58            $(#[deprecated($($dep_k = $dep_v),+)])?
59            pub mod $block;
60        )*
61
62        #[derive(Debug)]
63        pub enum BlockConfig {
64            $(
65                $(#[cfg(feature = $feat)])?
66                #[allow(non_camel_case_types)]
67                #[allow(deprecated)]
68                $block($block::Config),
69            )*
70            Err(&'static str, Error),
71        }
72
73        impl BlockConfig {
74            pub fn name(&self) -> &'static str {
75                match self {
76                    $(
77                        $(#[cfg(feature = $feat)])?
78                        Self::$block { .. } => stringify!($block),
79                    )*
80                    Self::Err(name, _err) => name,
81                }
82            }
83
84            pub fn spawn(self, api: CommonApi, futures: &mut FuturesUnordered<BoxedFuture<()>>) {
85                match self {
86                    $(
87                        $(#[cfg(feature = $feat)])?
88                        #[allow(deprecated)]
89                        Self::$block(config) => futures.push(async move {
90                            while let Err(err) = $block::run(&config, &api).await {
91                                if api.set_error(err).is_err() {
92                                    return;
93                                }
94                                tokio::select! {
95                                    _ = tokio::time::sleep(api.error_interval) => (),
96                                    _ = api.wait_for_update_request() => (),
97                                }
98                            }
99                        }.boxed_local()),
100                    )*
101                    Self::Err(_name, err) => {
102                        let _ = api.set_error(Error {
103                            message: Some("Configuration error".into()),
104                            cause: Some(Arc::new(err)),
105                        });
106                    },
107                }
108            }
109        }
110
111        impl<'de> Deserialize<'de> for BlockConfig {
112            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113            where
114                D: de::Deserializer<'de>,
115            {
116                use de::Error as _;
117
118                let mut table = toml::Table::deserialize(deserializer)?;
119                let block_name = table.remove("block").ok_or_else(|| D::Error::missing_field("block"))?;
120                let block_name = block_name.as_str().ok_or_else(|| D::Error::custom("block must be a string"))?;
121
122                match block_name {
123                    $(
124                        $(#[cfg(feature = $feat)])?
125                        #[allow(deprecated)]
126                        stringify!($block) => match $block::Config::deserialize(table) {
127                            Ok(config) => Ok(BlockConfig::$block(config)),
128                            Err(err) => Ok(BlockConfig::Err(stringify!($block), crate::errors::Error::new(err.to_string()))),
129                        }
130                        $(
131                            #[cfg(not(feature = $feat))]
132                            stringify!($block) => Err(D::Error::custom(format!(
133                                "block {} is behind a feature gate '{}' which must be enabled at compile time",
134                                stringify!($block),
135                                $feat,
136                            ))),
137                        )?
138                    )*
139                    other => Err(D::Error::custom(format!("unknown block '{other}'")))
140                }
141            }
142        }
143    };
144}
145
146define_blocks!(
147    amd_gpu,
148    backlight,
149    battery,
150    bluetooth,
151    calendar,
152    cpu,
153    custom,
154    custom_dbus,
155    disk_iostats,
156    disk_space,
157    docker,
158    external_ip,
159    focused_window,
160    github,
161    hueshift,
162    kdeconnect,
163    load,
164    #[cfg(feature = "maildir")]
165    maildir,
166    menu,
167    memory,
168    music,
169    net,
170    notify,
171    #[cfg(feature = "notmuch")]
172    notmuch,
173    nvidia_gpu,
174    packages,
175    pomodoro,
176    privacy,
177    rofication,
178    service_status,
179    scratchpad,
180    sound,
181    speedtest,
182    keyboard_layout,
183    taskwarrior,
184    temperature,
185    time,
186    tea_timer,
187    toggle,
188    uptime,
189    vpn,
190    watson,
191    weather,
192    xrandr,
193);
194
195/// An error which originates from a block
196#[derive(Debug, thiserror::Error)]
197#[error("In block {}: {}", .block_name, .error)]
198pub struct BlockError {
199    pub block_id: usize,
200    pub block_name: &'static str,
201    pub error: Error,
202}
203
204pub type BlockAction = Cow<'static, str>;
205
206#[derive(Clone)]
207pub struct CommonApi {
208    pub(crate) id: usize,
209    pub(crate) update_request: Arc<Notify>,
210    pub(crate) request_sender: mpsc::UnboundedSender<Request>,
211    pub(crate) error_interval: Duration,
212    pub(crate) geolocator: Arc<Geolocator>,
213}
214
215impl CommonApi {
216    /// Sends the widget to be displayed.
217    pub fn set_widget(&self, widget: Widget) -> Result<()> {
218        self.request_sender
219            .send(Request {
220                block_id: self.id,
221                cmd: RequestCmd::SetWidget(widget),
222            })
223            .error("Failed to send Request")
224    }
225
226    /// Hides the block. Send new widget to make it visible again.
227    pub fn hide(&self) -> Result<()> {
228        self.request_sender
229            .send(Request {
230                block_id: self.id,
231                cmd: RequestCmd::UnsetWidget,
232            })
233            .error("Failed to send Request")
234    }
235
236    /// Sends the error to be displayed.
237    pub fn set_error(&self, error: Error) -> Result<()> {
238        self.request_sender
239            .send(Request {
240                block_id: self.id,
241                cmd: RequestCmd::SetError(error),
242            })
243            .error("Failed to send Request")
244    }
245
246    pub fn set_default_actions(
247        &self,
248        actions: &'static [(MouseButton, Option<&'static str>, &'static str)],
249    ) -> Result<()> {
250        self.request_sender
251            .send(Request {
252                block_id: self.id,
253                cmd: RequestCmd::SetDefaultActions(actions),
254            })
255            .error("Failed to send Request")
256    }
257
258    pub fn get_actions(&self) -> Result<mpsc::UnboundedReceiver<BlockAction>> {
259        let (tx, rx) = mpsc::unbounded_channel();
260        self.request_sender
261            .send(Request {
262                block_id: self.id,
263                cmd: RequestCmd::SubscribeToActions(tx),
264            })
265            .error("Failed to send Request")?;
266        Ok(rx)
267    }
268
269    pub async fn wait_for_update_request(&self) {
270        self.update_request.notified().await;
271    }
272
273    fn locator_name(&self) -> Cow<'static, str> {
274        self.geolocator.name()
275    }
276
277    /// No-op if last API call was made in the last `interval` seconds.
278    pub async fn find_ip_location(
279        &self,
280        client: &reqwest::Client,
281        interval: Duration,
282    ) -> Result<IPAddressInfo> {
283        self.geolocator.find_ip_location(client, interval).await
284    }
285}