1mod 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#[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 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 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 pub fn set_error(&self, error: Error) -> Result<()> {
265 self.set_error_with_restartable(error, false)
266 }
267
268 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 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}