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