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