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 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#[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 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 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 pub fn set_error(&self, error: Error) -> Result<()> {
281 self.set_error_with_restartable(error, false)
282 }
283
284 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 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}