Skip to main content

i3status_rs/blocks/
maildir.rs

1//! Unread mail. Only supports maildir format.
2//!
3//! Note that you need to enable `maildir` feature to use this block:
4//! ```sh
5//! cargo build --release --features maildir
6//! ```
7//!
8//! # Configuration
9//!
10//! Key | Values | Default
11//! ----|--------|--------
12//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $status "`
13//! `inboxes` | List of maildir inboxes to look for mails in. Supports path/glob expansions (e.g. `~` and `*`). | **Required**
14//! `threshold_warning` | Number of unread mails where state is set to warning. | `1`
15//! `threshold_critical` | Number of unread mails where state is set to critical. | `10`
16//! `interval` | Update interval, in seconds. | `5`
17//! `display_type` | Which part of the maildir to count: `"new"`, `"cur"`, or `"all"`. | `"new"`
18//!
19//! Placeholder  | Value                  | Type   | Unit
20//! -------------|------------------------|--------|-----
21//! `icon`       | A static icon          | Icon   | -
22//! `status`     | Number of emails       | Number | -
23//!
24//! # Examples
25//!
26//! ```toml
27//! [[block]]
28//! block = "maildir"
29//! interval = 60
30//! inboxes = ["~/mail/local", "~/maildir/account1/*"]
31//! threshold_warning = 1
32//! threshold_critical = 10
33//! display_type = "new"
34//! ```
35//!
36//! # Icons Used
37//! - `mail` (`$icon`)
38
39use super::prelude::*;
40use maildir::Maildir;
41use std::path::PathBuf;
42
43#[derive(Deserialize, Debug, SmartDefault)]
44#[serde(deny_unknown_fields, default)]
45pub struct Config {
46    pub format: FormatConfig,
47    #[default(5.into())]
48    pub interval: Seconds,
49    pub inboxes: Vec<String>,
50    #[default(1)]
51    pub threshold_warning: usize,
52    #[default(10)]
53    pub threshold_critical: usize,
54    #[default(MailType::New)]
55    pub display_type: MailType,
56}
57
58fn expand_inbox(inbox: &str) -> Result<impl Iterator<Item = PathBuf>> {
59    let expanded = shellexpand::full(inbox).error("Failed to expand inbox")?;
60    let paths = glob::glob(&expanded).error("Glob expansion failed")?;
61    Ok(paths.filter_map(|p| p.ok()))
62}
63
64pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
65    BlockPlan::new(vec![
66        OutputPlan::new("main", config.format.with_default(" $icon $status ")?)
67            .icon("icon", IconChoices::one(icons::MAIL)),
68    ])
69}
70
71pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
72    let output_main = plan.output("main")?;
73
74    let mut inboxes = Vec::with_capacity(config.inboxes.len());
75    for inbox in &config.inboxes {
76        inboxes.extend(expand_inbox(inbox)?.map(Maildir::from));
77    }
78
79    loop {
80        let mut newmails = 0;
81        for inbox in &inboxes {
82            // TODO: spawn_blocking?
83            newmails += match config.display_type {
84                MailType::New => inbox.count_new(),
85                MailType::Cur => inbox.count_cur(),
86                MailType::All => inbox.count_new() + inbox.count_cur(),
87            };
88        }
89
90        let mut widget = output_main.new_widget();
91        widget.state = if newmails >= config.threshold_critical {
92            State::Critical
93        } else if newmails >= config.threshold_warning {
94            State::Warning
95        } else {
96            State::Idle
97        };
98        widget.set_values(map!(
99            "icon" => Value::icon(icons::MAIL),
100            "status" => Value::number(newmails)
101        ));
102        api.set_widget(widget)?;
103
104        select! {
105            _ = sleep(config.interval.0) => (),
106            _ = api.wait_for_update_request() => (),
107        }
108    }
109}
110
111#[derive(Clone, Debug, Deserialize)]
112#[serde(rename_all = "lowercase")]
113pub enum MailType {
114    New,
115    Cur,
116    All,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn plan_declares_main_output_with_mail_icon() {
125        let plan = prepare(&Config::default()).unwrap();
126        let declared: Vec<_> = plan.outputs().map(|o| o.id()).collect();
127        assert_eq!(declared, ["main"]);
128        let output = plan.output("main").unwrap();
129        assert_eq!(output.single_icon("icon").unwrap(), "mail");
130    }
131
132    #[test]
133    fn custom_format_is_respected() {
134        let config = Config {
135            format: " $status ".parse().unwrap(),
136            ..Config::default()
137        };
138        let plan = prepare(&config).unwrap();
139        let output = plan.output("main").unwrap();
140        assert!(output.format().contains_key("status"));
141        assert!(!output.format().contains_key("icon"));
142    }
143}