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 async fn run(config: &Config, api: &CommonApi) -> Result<()> {
102 let mut actions = api.get_actions()?;
103 api.set_default_actions(&[(MouseButton::Right, None, "next_filter")])?;
104
105 let format = config.format.with_default(" $icon $count.eng(w:1) ")?;
106 let format_singular = config
107 .format_singular
108 .with_default(" $icon $count.eng(w:1) ")?;
109 let format_everything_done = config
110 .format_everything_done
111 .with_default(" $icon $count.eng(w:1) ")?;
112
113 let mut filters = config.filters.iter().cycle();
114 let mut filter = filters.next().error("`filters` is empty")?;
115
116 let notify = Inotify::init().error("Failed to start inotify")?;
117 notify
118 .watches()
119 .add(&*config.data_location.expand()?, WatchMask::MODIFY)
120 .error("Failed to watch data location")?;
121 let mut updates = notify
122 .into_event_stream([0; 1024])
123 .error("Failed to create event stream")?;
124
125 loop {
126 let number_of_tasks = get_number_of_tasks(filter).await?;
127
128 let mut widget = Widget::new();
129
130 widget.set_format(match number_of_tasks {
131 0 => format_everything_done.clone(),
132 1 => format_singular.clone(),
133 _ => format.clone(),
134 });
135
136 widget.set_values(map! {
137 "icon" => Value::icon("tasks"),
138 "count" => Value::number(number_of_tasks),
139 "filter_name" => Value::text(filter.name.clone()),
140 });
141
142 widget.state = match number_of_tasks {
143 x if x >= config.critical_threshold => State::Critical,
144 x if x >= config.warning_threshold => State::Warning,
145 _ => State::Idle,
146 };
147
148 api.set_widget(widget)?;
149
150 select! {
151 _ = sleep(config.interval.0) =>(),
152 _ = updates.next() => (),
153 _ = api.wait_for_update_request() => (),
154 Some(action) = actions.recv() => match action.as_ref() {
155 "next_filter" => {
156 filter = filters.next().unwrap();
157 }
158 _ => (),
159 }
160 }
161 }
162}
163
164async fn get_number_of_tasks(filter: &Filter) -> Result<u32> {
165 let args_iter = filter.config_override.iter().map(String::as_str).chain([
166 "rc.gc=off",
167 &filter.filter,
168 "count",
169 ]);
170 let output = Command::new("task")
171 .args(args_iter)
172 .output()
173 .await
174 .error("failed to run taskwarrior for getting the number of tasks")?
175 .stdout;
176 std::str::from_utf8(&output)
177 .error("failed to get the number of tasks from taskwarrior (invalid UTF-8)")?
178 .trim()
179 .parse::<u32>()
180 .error("could not parse the result of taskwarrior")
181}
182
183#[derive(Deserialize, Debug, Default, Clone)]
184#[serde(deny_unknown_fields)]
185pub struct Filter {
186 pub name: String,
187 pub filter: String,
188 #[serde(default)]
189 pub config_override: Vec<String>,
190}