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