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