i3status_rs/
lib.rs

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/// A feature-rich and resource-friendly replacement for i3status(1), written in Rust. The
80/// i3status-rs program writes a stream of configurable "blocks" of system information (time,
81/// battery status, volume, etc.) to standard output in the JSON format understood by i3bar(1) and
82/// sway-bar(5).
83#[derive(Debug, clap::Parser)]
84#[clap(author, about, long_about, version = env!("VERSION"))]
85pub struct CliArgs {
86    /// Sets a TOML config file
87    ///
88    /// 1. If full absolute path given, then use it as is: `/home/foo/i3rs-config.toml`
89    ///
90    /// 2. If filename given, e.g. "custom_theme.toml", then first look in `$XDG_CONFIG_HOME/i3status-rust`
91    ///
92    /// 3. Then look for it in `$XDG_DATA_HOME/i3status-rust`
93    ///
94    /// 4. Otherwise look for it in `/usr/share/i3status-rust`
95    #[clap(default_value = "config.toml")]
96    pub config: String,
97    /// Ignore any attempts by i3 to pause the bar when hidden/fullscreen
98    #[clap(long = "never-pause")]
99    pub never_pause: bool,
100    /// Do not send the init sequence
101    #[clap(hide = true, long = "no-init")]
102    pub no_init: bool,
103    /// The maximum number of blocking threads spawned by tokio
104    #[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            // TODO: async
247            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        // Overrides
262        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            // Poll blocks
384            Some(()) = self.running_blocks.next() => (),
385            // Receive messages from blocks
386            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            // Handle scheduled updates
393            Some(ids) = self.widget_updates_stream.next() => {
394                for id in ids {
395                    self.render_block(id)?;
396                }
397                self.render();
398            }
399            // Handle clicks
400            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            // Handle signals
449            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                    // This should never happen. If this code runs, it could mean that we
475                    // got an error while trying to display and error. We better stop here.
476                    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}