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