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