i3status_rs/blocks/
taskwarrior.rs1use super::prelude::*;
66use inotify::{Inotify, WatchMask};
67use tokio::process::Command;
68
69#[derive(Deserialize, Debug)]
70#[serde(deny_unknown_fields, default)]
71pub struct Config {
72 pub interval: Seconds,
73 pub warning_threshold: u32,
74 pub critical_threshold: u32,
75 pub filters: Vec<Filter>,
76 pub format: FormatConfig,
77 pub format_singular: FormatConfig,
78 pub format_everything_done: FormatConfig,
79 pub data_location: ShellString,
80}
81
82impl Default for Config {
83 fn default() -> Self {
84 Self {
85 interval: Seconds::new(600),
86 warning_threshold: 10,
87 critical_threshold: 20,
88 filters: vec![Filter {
89 name: "pending".into(),
90 filter: "-COMPLETED -DELETED".into(),
91 config_override: Default::default(),
92 }],
93 format: default(),
94 format_singular: default(),
95 format_everything_done: default(),
96 data_location: ShellString::new("~/.task"),
97 }
98 }
99}
100
101pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
102 BlockPlan::new(vec![
103 OutputPlan::new(
104 "main",
105 config.format.with_default(" $icon $count.eng(w:1) ")?,
106 )
107 .icon("icon", IconChoices::one(icons::TASKS)),
108 OutputPlan::new(
109 "singular",
110 config
111 .format_singular
112 .with_default(" $icon $count.eng(w:1) ")?,
113 )
114 .icon("icon", IconChoices::one(icons::TASKS)),
115 OutputPlan::new(
116 "everything_done",
117 config
118 .format_everything_done
119 .with_default(" $icon $count.eng(w:1) ")?,
120 )
121 .icon("icon", IconChoices::one(icons::TASKS)),
122 ])
123}
124
125pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
126 let mut actions = api.get_actions()?;
127 api.set_default_actions(&[(MouseButton::Right, None, "next_filter")])?;
128
129 let output_main = plan.output("main")?;
130 let output_singular = plan.output("singular")?;
131 let output_everything_done = plan.output("everything_done")?;
132
133 let mut filters = config.filters.iter().cycle();
134 let mut filter = filters.next().error("`filters` is empty")?;
135
136 let notify = Inotify::init().error("Failed to start inotify")?;
137 notify
138 .watches()
139 .add(&*config.data_location.expand()?, WatchMask::MODIFY)
140 .error("Failed to watch data location")?;
141 let mut updates = notify
142 .into_event_stream([0; 1024])
143 .error("Failed to create event stream")?;
144
145 loop {
146 let number_of_tasks = get_number_of_tasks(filter).await?;
147
148 let output = match number_of_tasks {
149 0 => &output_everything_done,
150 1 => &output_singular,
151 _ => &output_main,
152 };
153 let mut widget = output.new_widget();
154
155 widget.set_values(map! {
156 "icon" => Value::icon(icons::TASKS),
157 "count" => Value::number(number_of_tasks),
158 "filter_name" => Value::text(filter.name.clone()),
159 });
160
161 widget.state = match number_of_tasks {
162 x if x >= config.critical_threshold => State::Critical,
163 x if x >= config.warning_threshold => State::Warning,
164 _ => State::Idle,
165 };
166
167 api.set_widget(widget)?;
168
169 loop {
170 select! {
171 _ = sleep(config.interval.0) => break,
172 Some(Ok(event)) = updates.next() => {
173 if let Some(name) = event.name {
178 let name_str = name.to_string_lossy();
179 if name_str.ends_with("-shm") || name_str.ends_with("-wal") || name_str.ends_with("-journal") {
180 continue;
181 }
182 }
183 break;
184 }
185 _ = api.wait_for_update_request() => break,
186 Some(action) = actions.recv() => {
187 match action.as_ref() {
188 "next_filter" => {
189 filter = filters.next().unwrap();
190 }
191 _ => (),
192 }
193 break;
194 }
195 }
196 }
197 }
198}
199
200fn count_args(filter: &Filter) -> impl Iterator<Item = &str> {
207 filter.config_override.iter().map(String::as_str).chain([
208 "rc.gc=off",
209 "rc.recurrence.limit=0",
210 &filter.filter,
211 "count",
212 ])
213}
214
215async fn get_number_of_tasks(filter: &Filter) -> Result<u32> {
216 let output = Command::new("task")
217 .args(count_args(filter))
218 .output()
219 .await
220 .error("failed to run taskwarrior for getting the number of tasks")?
221 .stdout;
222 std::str::from_utf8(&output)
223 .error("failed to get the number of tasks from taskwarrior (invalid UTF-8)")?
224 .trim()
225 .parse::<u32>()
226 .error("could not parse the result of taskwarrior")
227}
228
229#[derive(Deserialize, Debug, Default, Clone)]
230#[serde(deny_unknown_fields)]
231pub struct Filter {
232 pub name: String,
233 pub filter: String,
234 #[serde(default)]
235 pub config_override: Vec<String>,
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn count_is_side_effect_free() {
244 let filter = Filter {
245 name: "pending".into(),
246 filter: "-COMPLETED -DELETED".into(),
247 config_override: vec!["rc.context=work".into()],
248 };
249 let args: Vec<&str> = count_args(&filter).collect();
250 assert_eq!(
251 args,
252 [
253 "rc.context=work",
254 "rc.gc=off",
255 "rc.recurrence.limit=0",
256 "-COMPLETED -DELETED",
257 "count",
258 ]
259 );
260 let gc = args.iter().position(|a| *a == "rc.gc=off").unwrap();
262 let rec = args
263 .iter()
264 .position(|a| *a == "rc.recurrence.limit=0")
265 .unwrap();
266 let user = args.iter().position(|a| *a == "rc.context=work").unwrap();
267 assert!(user < gc && user < rec);
268 }
269
270 #[test]
271 fn plan_declares_count_states_with_tasks_icon() {
272 let plan = prepare(&Config::default()).unwrap();
273 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
274 assert_eq!(ids, ["main", "singular", "everything_done"]);
275 for id in ["main", "singular", "everything_done"] {
276 let output = plan.output(id).unwrap();
277 assert_eq!(output.single_icon("icon").unwrap(), "tasks", "{id}");
278 }
279 }
280
281 #[test]
282 fn each_count_state_resolves_its_own_format() {
283 let config = Config {
286 format_everything_done: "".parse().unwrap(),
287 ..Config::default()
288 };
289 let plan = prepare(&config).unwrap();
290 assert!(
291 !plan
292 .output("everything_done")
293 .unwrap()
294 .format()
295 .contains_key("count")
296 );
297 for id in ["main", "singular"] {
298 assert!(
299 plan.output(id).unwrap().format().contains_key("count"),
300 "{id}"
301 );
302 }
303 }
304}