Skip to main content

i3status_rs/
lib.rs

1// The block plan is internal (see `block_plan`). Denying this turns any
2// future leak of it into a public signature — which would put an
3// unnameable type in the published API — into a compile error.
4#![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;
14/// The per-block output contracts the framework prepares and enforces.
15/// Internal: the published surface is the blocks' own configuration and
16/// output.
17pub(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/// A feature-rich and resource-friendly replacement for i3status(1), written in Rust. The
87/// i3status-rs program writes a stream of configurable "blocks" of system information (time,
88/// battery status, volume, etc.) to standard output in the JSON format understood by i3bar(1) and
89/// sway-bar(5).
90#[derive(Debug, clap::Parser)]
91#[clap(author, about, long_about, version = env!("VERSION"))]
92pub struct CliArgs {
93    /// Sets a TOML config file
94    ///
95    /// 1. If full absolute path given, then use it as is: `/home/foo/i3rs-config.toml`
96    ///
97    /// 2. If filename given, e.g. "custom_theme.toml", then first look in `$XDG_CONFIG_HOME/i3status-rust`
98    ///
99    /// 3. Then look for it in `$XDG_DATA_HOME/i3status-rust`
100    ///
101    /// 4. Otherwise look for it in `/usr/share/i3status-rust`
102    #[clap(default_value = "config.toml")]
103    pub config: String,
104    /// Ignore any attempts by i3 to pause the bar when hidden/fullscreen
105    #[clap(long = "never-pause")]
106    pub never_pause: bool,
107    /// Do not send the init sequence
108    #[clap(hide = true, long = "no-init")]
109    pub no_init: bool,
110    /// The maximum number of blocking threads spawned by tokio
111    #[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        // The bar renders this widget itself rather than through
221        // `CommonApi::set_widget`, so nothing downstream would catch drift
222        // between `error_plan` and this renderer.
223        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            // TODO: async
261            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        // Overrides
276        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            // Without a retry limit the block retries forever and the
307            // restart button is never rendered.
308            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            // Poll blocks
402            Some(()) = self.running_blocks.next() => (),
403            // Receive messages from blocks
404            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            // Handle scheduled updates
411            Some(ids) = self.widget_updates_stream.next() => {
412                for id in ids {
413                    self.render_block(id)?;
414                }
415                self.render();
416            }
417            // Handle clicks
418            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            // Handle signals
467            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                    // This should never happen. If this code runs, it could mean that we
493                    // got an error while trying to display and error. We better stop here.
494                    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}