1#![warn(clippy::match_same_arms)]
2#![warn(clippy::semicolon_if_nothing_returned)]
3#![warn(clippy::unnecessary_wraps)]
4#![warn(clippy::unused_trait_names)]
5#![allow(clippy::single_match)]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9pub mod util;
10pub mod blocks;
11pub mod click;
12pub mod config;
13pub mod errors;
14pub mod escape;
15pub mod formatting;
16pub mod geolocator;
17pub mod icons;
18mod netlink;
19pub mod protocol;
20mod signals;
21mod subprocess;
22pub mod themes;
23pub mod widget;
24mod wrappers;
25
26pub use env_logger;
27pub use serde_json;
28pub use tokio;
29
30use std::borrow::Cow;
31use std::pin::Pin;
32use std::sync::{Arc, LazyLock};
33use std::time::Duration;
34
35use futures::Stream;
36use futures::stream::{FuturesUnordered, StreamExt as _};
37use tokio::process::Command;
38use tokio::sync::{Notify, mpsc};
39
40use crate::blocks::{BlockAction, BlockError, CommonApi};
41use crate::click::{ClickHandler, MouseButton};
42use crate::config::{BlockConfigEntry, Config, SharedConfig};
43use crate::errors::*;
44use crate::formatting::Format;
45use crate::formatting::value::Value;
46use crate::protocol::i3bar_block::I3BarBlock;
47use crate::protocol::i3bar_event::{self, I3BarEvent};
48use crate::signals::Signal;
49use crate::widget::{State, Widget};
50
51const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
52const REQWEST_TIMEOUT: Duration = Duration::from_secs(10);
53
54static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
55 reqwest::Client::builder()
56 .user_agent(APP_USER_AGENT)
57 .timeout(REQWEST_TIMEOUT)
58 .build()
59 .unwrap()
60});
61
62static REQWEST_CLIENT_IPV4: LazyLock<reqwest::Client> = LazyLock::new(|| {
63 reqwest::Client::builder()
64 .user_agent(APP_USER_AGENT)
65 .local_address(Some(std::net::Ipv4Addr::UNSPECIFIED.into()))
66 .timeout(REQWEST_TIMEOUT)
67 .build()
68 .unwrap()
69});
70
71type BoxedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
72
73type BoxedStream<T> = Pin<Box<dyn Stream<Item = T>>>;
74
75type WidgetUpdatesSender = mpsc::UnboundedSender<(usize, Vec<u64>)>;
76
77#[derive(Debug, clap::Parser)]
82#[clap(author, about, long_about, version = env!("VERSION"))]
83pub struct CliArgs {
84 #[clap(default_value = "config.toml")]
94 pub config: String,
95 #[clap(long = "never-pause")]
97 pub never_pause: bool,
98 #[clap(hide = true, long = "no-init")]
100 pub no_init: bool,
101 #[clap(long = "threads", short = 'j', default_value = "2")]
103 pub blocking_threads: usize,
104}
105
106pub struct BarState {
107 config: Config,
108
109 blocks: Vec<Block>,
110 fullscreen_block: Option<usize>,
111 running_blocks: FuturesUnordered<BoxedFuture<()>>,
112
113 widget_updates_sender: WidgetUpdatesSender,
114 blocks_render_cache: Vec<RenderedBlock>,
115
116 request_sender: mpsc::UnboundedSender<Request>,
117 request_receiver: mpsc::UnboundedReceiver<Request>,
118
119 widget_updates_stream: BoxedStream<Vec<usize>>,
120 signals_stream: BoxedStream<Signal>,
121 events_stream: BoxedStream<I3BarEvent>,
122}
123
124#[derive(Debug)]
125struct Request {
126 block_id: usize,
127 cmd: RequestCmd,
128}
129
130#[derive(Debug)]
131enum RequestCmd {
132 SetWidget(Widget),
133 UnsetWidget,
134 SetError(Error),
135 SetDefaultActions(&'static [(MouseButton, Option<&'static str>, &'static str)]),
136 SubscribeToActions(mpsc::UnboundedSender<BlockAction>),
137}
138
139#[derive(Debug, Clone)]
140struct RenderedBlock {
141 pub segments: Vec<I3BarBlock>,
142 pub merge_with_next: bool,
143}
144
145#[derive(Debug)]
146pub struct Block {
147 id: usize,
148 name: &'static str,
149
150 update_request: Arc<Notify>,
151 action_sender: Option<mpsc::UnboundedSender<BlockAction>>,
152
153 click_handler: ClickHandler,
154 default_actions: &'static [(MouseButton, Option<&'static str>, &'static str)],
155 signal: Option<i32>,
156 shared_config: SharedConfig,
157
158 error_format: Format,
159 error_fullscreen_format: Format,
160
161 state: BlockState,
162}
163
164#[derive(Debug)]
165enum BlockState {
166 None,
167 Normal { widget: Widget },
168 Error { widget: Widget },
169}
170
171impl Block {
172 fn notify_intervals(&self, tx: &WidgetUpdatesSender) {
173 let intervals = match &self.state {
174 BlockState::None => Vec::new(),
175 BlockState::Normal { widget } | BlockState::Error { widget } => widget.intervals(),
176 };
177 let _ = tx.send((self.id, intervals));
178 }
179
180 fn send_action(&mut self, action: BlockAction) {
181 if let Some(sender) = &self.action_sender {
182 if sender.send(action).is_err() {
183 self.action_sender = None;
184 }
185 }
186 }
187
188 fn set_error(&mut self, fullscreen: bool, error: Error) {
189 let error = BlockError {
190 block_id: self.id,
191 block_name: self.name,
192 error,
193 };
194
195 let mut widget = Widget::new()
196 .with_state(State::Critical)
197 .with_format(if fullscreen {
198 self.error_fullscreen_format.clone()
199 } else {
200 self.error_format.clone()
201 });
202 widget.set_values(map! {
203 "full_error_message" => Value::text(error.to_string()),
204 [if let Some(v) = &error.error.message] "short_error_message" => Value::text(v.to_string()),
205 });
206 self.state = BlockState::Error { widget };
207 }
208}
209
210impl BarState {
211 pub fn new(config: Config) -> Self {
212 let (request_sender, request_receiver) = mpsc::unbounded_channel();
213 let (widget_updates_sender, widget_updates_stream) =
214 formatting::scheduling::manage_widgets_updates();
215 Self {
216 blocks: Vec::new(),
217 fullscreen_block: None,
218 running_blocks: FuturesUnordered::new(),
219
220 widget_updates_sender,
221 blocks_render_cache: Vec::new(),
222
223 request_sender,
224 request_receiver,
225
226 widget_updates_stream,
227 signals_stream: signals::signals_stream(),
228 events_stream: i3bar_event::events_stream(
229 config.invert_scrolling,
230 Duration::from_millis(config.double_click_delay),
231 ),
232
233 config,
234 }
235 }
236
237 pub async fn spawn_block(&mut self, block_config: BlockConfigEntry) -> Result<()> {
238 if let Some(cmd) = &block_config.common.if_command {
239 if !Command::new("sh")
241 .args(["-c", cmd])
242 .output()
243 .await
244 .error("failed to run if_command")?
245 .status
246 .success()
247 {
248 return Ok(());
249 }
250 }
251
252 let mut shared_config = self.config.shared.clone();
253
254 if let Some(icons_format) = block_config.common.icons_format {
256 shared_config.icons_format = Arc::new(icons_format);
257 }
258 if let Some(theme_overrides) = block_config.common.theme_overrides {
259 Arc::make_mut(&mut shared_config.theme).apply_overrides(theme_overrides)?;
260 }
261 if let Some(icons_overrides) = block_config.common.icons_overrides {
262 Arc::make_mut(&mut shared_config.icons).apply_overrides(icons_overrides);
263 }
264
265 let update_request = Arc::new(Notify::new());
266
267 let api = CommonApi {
268 id: self.blocks.len(),
269 update_request: update_request.clone(),
270 request_sender: self.request_sender.clone(),
271 error_interval: Duration::from_secs(block_config.common.error_interval),
272 geolocator: self.config.geolocator.clone(),
273 };
274
275 let error_format = block_config
276 .common
277 .error_format
278 .with_default_config(&self.config.error_format);
279 let error_fullscreen_format = block_config
280 .common
281 .error_fullscreen_format
282 .with_default_config(&self.config.error_fullscreen_format);
283
284 let block = Block {
285 id: self.blocks.len(),
286 name: block_config.config.name(),
287
288 update_request,
289 action_sender: None,
290
291 click_handler: block_config.common.click,
292 default_actions: &[],
293 signal: block_config.common.signal,
294 shared_config,
295
296 error_format,
297 error_fullscreen_format,
298
299 state: BlockState::None,
300 };
301
302 block_config.config.spawn(api, &mut self.running_blocks);
303
304 self.blocks.push(block);
305 self.blocks_render_cache.push(RenderedBlock {
306 segments: Vec::new(),
307 merge_with_next: block_config.common.merge_with_next,
308 });
309
310 Ok(())
311 }
312
313 fn process_request(&mut self, request: Request) {
314 let block = &mut self.blocks[request.block_id];
315 match request.cmd {
316 RequestCmd::SetWidget(widget) => {
317 block.state = BlockState::Normal { widget };
318 if self.fullscreen_block == Some(request.block_id) {
319 self.fullscreen_block = None;
320 }
321 }
322 RequestCmd::UnsetWidget => {
323 block.state = BlockState::None;
324 if self.fullscreen_block == Some(request.block_id) {
325 self.fullscreen_block = None;
326 }
327 }
328 RequestCmd::SetError(error) => {
329 block.set_error(self.fullscreen_block == Some(request.block_id), error);
330 }
331 RequestCmd::SetDefaultActions(actions) => {
332 block.default_actions = actions;
333 }
334 RequestCmd::SubscribeToActions(action_sender) => {
335 block.action_sender = Some(action_sender);
336 }
337 }
338 block.notify_intervals(&self.widget_updates_sender);
339 }
340
341 fn render_block(&mut self, id: usize) -> Result<(), BlockError> {
342 let block = &mut self.blocks[id];
343 let data = &mut self.blocks_render_cache[id].segments;
344 match &block.state {
345 BlockState::None => {
346 data.clear();
347 }
348 BlockState::Normal { widget } | BlockState::Error { widget, .. } => {
349 *data = widget
350 .get_data(&block.shared_config, id)
351 .map_err(|error| BlockError {
352 block_id: id,
353 block_name: block.name,
354 error,
355 })?;
356 }
357 }
358 Ok(())
359 }
360
361 fn render(&self) {
362 if let Some(id) = self.fullscreen_block {
363 protocol::print_blocks(&[&self.blocks_render_cache[id]], &self.config.shared);
364 } else {
365 protocol::print_blocks(&self.blocks_render_cache, &self.config.shared);
366 }
367 }
368
369 async fn process_event(&mut self, restart: fn() -> !) -> Result<(), BlockError> {
370 tokio::select! {
371 Some(()) = self.running_blocks.next() => (),
373 Some(request) = self.request_receiver.recv() => {
375 let id = request.block_id;
376 self.process_request(request);
377 self.render_block(id)?;
378 self.render();
379 }
380 Some(ids) = self.widget_updates_stream.next() => {
382 for id in ids {
383 self.render_block(id)?;
384 }
385 self.render();
386 }
387 Some(event) = self.events_stream.next() => {
389 let block = self.blocks.get_mut(event.id).expect("Events receiver: ID out of bounds");
390 match &mut block.state {
391 BlockState::None => (),
392 BlockState::Normal { .. } => {
393 let result = block.click_handler.handle(&event).await.map_err(|error| BlockError {
394 block_id: event.id,
395 block_name: block.name,
396 error,
397 })?;
398 match result {
399 Some(post_actions) => {
400 if let Some(action) = post_actions.action {
401 block.send_action(Cow::Owned(action));
402 }
403 if post_actions.update {
404 block.update_request.notify_one();
405 }
406 }
407 None => {
408 if let Some((_, _, action)) = block.default_actions
409 .iter()
410 .find(|(btn, widget, _)| *btn == event.button && *widget == event.instance.as_deref()) {
411 block.send_action(Cow::Borrowed(action));
412 }
413 }
414 }
415 }
416 BlockState::Error { widget } => {
417 if self.fullscreen_block == Some(event.id) {
418 self.fullscreen_block = None;
419 widget.set_format(block.error_format.clone());
420 } else {
421 self.fullscreen_block = Some(event.id);
422 widget.set_format(block.error_fullscreen_format.clone());
423 }
424 block.notify_intervals(&self.widget_updates_sender);
425 self.render_block(event.id)?;
426 self.render();
427 }
428 }
429 }
430 Some(signal) = self.signals_stream.next() => match signal {
432 Signal::Usr1 => {
433 for block in &self.blocks {
434 block.update_request.notify_one();
435 }
436 }
437 Signal::Usr2 => restart(),
438 Signal::Custom(signal) => {
439 for block in &self.blocks {
440 if block.signal == Some(signal) {
441 block.update_request.notify_one();
442 }
443 }
444 }
445 }
446 }
447 Ok(())
448 }
449
450 pub async fn run_event_loop(mut self, restart: fn() -> !) -> Result<(), BlockError> {
451 loop {
452 if let Err(error) = self.process_event(restart).await {
453 let block = &mut self.blocks[error.block_id];
454
455 if matches!(block.state, BlockState::Error { .. }) {
456 return Err(error);
459 }
460
461 block.set_error(self.fullscreen_block == Some(block.id), error.error);
462 block.notify_intervals(&self.widget_updates_sender);
463
464 self.render_block(error.block_id)?;
465 self.render();
466 }
467 }
468 }
469}