i3status_rs/blocks/
taskwarrior.rs

1//! The number of tasks from the taskwarrior list
2//!
3//! Clicking the right mouse button on the icon cycles the view of the block through the user's filters.
4//!
5//! # Configuration
6//!
7//! Key | Values | Default
8//! ----|--------|--------
9//! `interval` | Update interval in seconds | `600` (10min)
10//! `warning_threshold` | The threshold of pending (or started) tasks when the block turns into a warning state | `10`
11//! `critical_threshold` | The threshold of pending (or started) tasks when the block turns into a critical state | `20`
12//! `filters` | A list of tables with the keys `name` and `filter`. `filter` specifies the criteria that must be met for a task to be counted towards this filter. | ```[{name = "pending", filter = "-COMPLETED -DELETED"}]```
13//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $count.eng(w:1) "`
14//! `format_singular` | Same as `format` but for when exactly one task is pending. | `" $icon $count.eng(w:1) "`
15//! `format_everything_done` | Same as `format` but for when all tasks are completed. | `" $icon $count.eng(w:1) "`
16//! `data_location`| Directory in which taskwarrior stores its data files. Supports path expansions e.g. `~`. | `"~/.task"`
17//!
18//! Placeholder   | Value                                       | Type   | Unit
19//! --------------|---------------------------------------------|--------|-----
20//! `icon`        | A static icon                               | Icon   | -
21//! `count`       | The number of tasks matching current filter | Number | -
22//! `filter_name` | The name of current filter                  | Text   | -
23//!
24//! Action        | Default button
25//! --------------|---------------
26//! `next_filter` | Right
27//!
28//! # Example
29//!
30//! In this example, block will be hidden if `count` is zero.
31//!
32//! ```toml
33//! [[block]]
34//! block = "taskwarrior"
35//! interval = 60
36//! format = " $icon count.eng(w:1) tasks "
37//! format_singular = " $icon 1 task "
38//! format_everything_done = ""
39//! warning_threshold = 10
40//! critical_threshold = 20
41//! [[block.filters]]
42//! name = "today"
43//! filter = "+PENDING +OVERDUE or +DUETODAY"
44//! [[block.filters]]
45//! name = "some-project"
46//! filter = "project:some-project +PENDING"
47//! ```
48//!
49//! # Icons Used
50//! - `tasks`
51
52use super::prelude::*;
53use inotify::{Inotify, WatchMask};
54use tokio::process::Command;
55
56#[derive(Deserialize, Debug)]
57#[serde(deny_unknown_fields, default)]
58pub struct Config {
59    pub interval: Seconds,
60    pub warning_threshold: u32,
61    pub critical_threshold: u32,
62    pub filters: Vec<Filter>,
63    pub format: FormatConfig,
64    pub format_singular: FormatConfig,
65    pub format_everything_done: FormatConfig,
66    pub data_location: ShellString,
67}
68
69impl Default for Config {
70    fn default() -> Self {
71        Self {
72            interval: Seconds::new(600),
73            warning_threshold: 10,
74            critical_threshold: 20,
75            filters: vec![Filter {
76                name: "pending".into(),
77                filter: "-COMPLETED -DELETED".into(),
78            }],
79            format: default(),
80            format_singular: default(),
81            format_everything_done: default(),
82            data_location: ShellString::new("~/.task"),
83        }
84    }
85}
86
87pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
88    let mut actions = api.get_actions()?;
89    api.set_default_actions(&[(MouseButton::Right, None, "next_filter")])?;
90
91    let format = config.format.with_default(" $icon $count.eng(w:1) ")?;
92    let format_singular = config
93        .format_singular
94        .with_default(" $icon $count.eng(w:1) ")?;
95    let format_everything_done = config
96        .format_everything_done
97        .with_default(" $icon $count.eng(w:1) ")?;
98
99    let mut filters = config.filters.iter().cycle();
100    let mut filter = filters.next().error("`filters` is empty")?;
101
102    let notify = Inotify::init().error("Failed to start inotify")?;
103    notify
104        .watches()
105        .add(&*config.data_location.expand()?, WatchMask::MODIFY)
106        .error("Failed to watch data location")?;
107    let mut updates = notify
108        .into_event_stream([0; 1024])
109        .error("Failed to create event stream")?;
110
111    loop {
112        let number_of_tasks = get_number_of_tasks(&filter.filter).await?;
113
114        let mut widget = Widget::new();
115
116        widget.set_format(match number_of_tasks {
117            0 => format_everything_done.clone(),
118            1 => format_singular.clone(),
119            _ => format.clone(),
120        });
121
122        widget.set_values(map! {
123            "icon" => Value::icon("tasks"),
124            "count" => Value::number(number_of_tasks),
125            "filter_name" => Value::text(filter.name.clone()),
126        });
127
128        widget.state = match number_of_tasks {
129            x if x >= config.critical_threshold => State::Critical,
130            x if x >= config.warning_threshold => State::Warning,
131            _ => State::Idle,
132        };
133
134        api.set_widget(widget)?;
135
136        select! {
137            _ = sleep(config.interval.0) =>(),
138            _ = updates.next() => (),
139            _ = api.wait_for_update_request() => (),
140            Some(action) = actions.recv() => match action.as_ref() {
141                "next_filter" => {
142                    filter = filters.next().unwrap();
143                }
144                _ => (),
145            }
146        }
147    }
148}
149
150async fn get_number_of_tasks(filter: &str) -> Result<u32> {
151    let output = Command::new("task")
152        .args(["rc.gc=off", filter, "count"])
153        .output()
154        .await
155        .error("failed to run taskwarrior for getting the number of tasks")?
156        .stdout;
157    std::str::from_utf8(&output)
158        .error("failed to get the number of tasks from taskwarrior (invalid UTF-8)")?
159        .trim()
160        .parse::<u32>()
161        .error("could not parse the result of taskwarrior")
162}
163
164#[derive(Deserialize, Debug, Default, Clone)]
165#[serde(deny_unknown_fields)]
166pub struct Filter {
167    pub name: String,
168    pub filter: String,
169}