1mod 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#[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 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 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 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 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}