i3status_rs/blocks/
taskwarrior.rs
1use 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}