Skip to content

Commit 7b2fc4d

Browse files
committed
Modify taskwarrior block to support multiple named filters
This commit deprecates `filter_tags` in favour of `filters`. If `filter_tags` are specified, the block behaves like the the existing implementation, and two filters are created: - all: `-COMPLETED -DELETED` - filtered: `-COMPLETED -DELETED <tags>` This emulates the modes of `TaskwarriorBlockMode` which has also been removed. Clicking now cycles through the filters, of which an arbitrary number may be specified. [[block.filters]] name = "today" filter = "+PENDING +OVERDUE or +DUETODAY"
1 parent 75ed800 commit 7b2fc4d

File tree

2 files changed

+83
-68
lines changed

2 files changed

+83
-68
lines changed

blocks.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,26 +1303,30 @@ Key | Values | Required | Default
13031303

13041304
## Taskwarrior
13051305

1306-
Creates a block which displays number of pending and started tasks of the current users taskwarrior list.
1306+
Creates a block which displays the number of tasks matching user-defined filters from the current user's taskwarrior list.
13071307

1308-
Clicking the left mouse button on the icon updates the number of pending tasks immediately.
1308+
Clicking the left mouse button on the icon updates the number of tasks immediately.
1309+
1310+
Clicking the right mouse button on the icon cycles the view of the block through the user's filters.
13091311

1310-
Clicking the right mouse button on the icon toggles the view of the block between filtered (default) and non-filtered
1311-
tasks. If there are no filters configured, the number of tasks stays the same and both modes are behaving
1312-
equally.
13131312

13141313
#### Examples
13151314

13161315
```toml
13171316
[[block]]
13181317
block = "taskwarrior"
13191318
interval = 60
1320-
format = "{count} open tasks"
1321-
format_singular = "{count} open task"
1319+
format = "{count} open tasks ({filter_name})"
1320+
format_singular = "{count} open task ({filter_name})"
13221321
format_everything_done = "nothing to do!"
13231322
warning_threshold = 10
13241323
critical_threshold = 20
1325-
filter = "+work +important"
1324+
[[block.filters]]
1325+
name = "today"
1326+
filter = "+PENDING +OVERDUE or +DUETODAY"
1327+
[[block.filters]]
1328+
name = "some-project"
1329+
filter = "project:some-project +PENDING"
13261330
```
13271331

13281332
#### Options
@@ -1332,8 +1336,8 @@ Key | Values | Required | Default
13321336
`interval` | Update interval, in seconds. | No | `600` (10min)
13331337
`warning_threshold` | The threshold of pending (or started) tasks when the block turns into a warning state. | No | `10`
13341338
`critical_threshold` | The threshold of pending (or started) tasks when the block turns into a critical state. | No | `20`
1335-
`filter_tags` | Deprecated in favour of `filter`. A list of tags a task has to have before its counted as a pending task. | No | ```<empty>```
1336-
`filter` | A filter that a task has to match to be counted as a pending task. | No | ```<empty```
1339+
`filter_tags` | Deprecated in favour of `filters`. A list of tags a task has to have before its counted as a pending task. The list of tags will be appended to the base filter `-COMPLETED -DELETED`. | No | ```<empty>```
1340+
`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. | No | ```[{name = "pending", filter = "-COMPLETED -DELETED"}]```
13371341
`format` | A string to customise the output of this block. See below for available placeholders. Text may need to be escaped, refer to [Escaping Text](#escaping-text). | No | `"{count}"`
13381342
`format_singular` | Same as `format` but for when exactly one task is pending. | No | `"{count}"`
13391343
`format_everything_done` | Same as `format` but for when all tasks are completed. | No | `"{count}"`
@@ -1343,6 +1347,7 @@ Key | Values | Required | Default
13431347
Key | Value
13441348
----|-------
13451349
`{count}` | The number of pending tasks
1350+
`{filter_name}` | The name of the current filter
13461351

13471352
###### [↥ back to top](#list-of-available-blocks)
13481353

src/blocks/taskwarrior.rs

Lines changed: 68 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ pub struct Taskwarrior {
2121
update_interval: Duration,
2222
warning_threshold: u32,
2323
critical_threshold: u32,
24-
filter: String,
25-
block_mode: TaskwarriorBlockMode,
24+
filters: Vec<Filter>,
25+
filter_index: usize,
2626
format: FormatTemplate,
2727
format_singular: FormatTemplate,
2828
format_everything_done: FormatTemplate,
@@ -34,6 +34,29 @@ pub struct Taskwarrior {
3434
tx_update_request: Sender<Task>,
3535
}
3636

37+
#[derive(Deserialize, Debug, Default, Clone)]
38+
#[serde(deny_unknown_fields)]
39+
pub struct Filter {
40+
pub name: String,
41+
pub filter: String,
42+
}
43+
44+
impl Filter {
45+
pub fn new(name: String, filter: String) -> Self {
46+
Filter { name, filter }
47+
}
48+
49+
pub fn legacy(name: String, tags: &[String]) -> Self {
50+
let tags = tags
51+
.iter()
52+
.map(|element| format!("+{}", element))
53+
.collect::<Vec<String>>()
54+
.join(" ");
55+
let filter = format!("-COMPLETED -DELETED {}", tags);
56+
Self::new(name, filter)
57+
}
58+
}
59+
3760
#[derive(Deserialize, Debug, Default, Clone)]
3861
#[serde(deny_unknown_fields)]
3962
pub struct TaskwarriorConfig {
@@ -53,37 +76,31 @@ pub struct TaskwarriorConfig {
5376
pub critical_threshold: u32,
5477

5578
/// A list of tags a task has to have before it's used for counting pending tasks
56-
/// (DEPRECATED) use filter instead
79+
/// (DEPRECATED) use filters instead
5780
#[serde(default = "TaskwarriorConfig::default_filter_tags")]
5881
pub filter_tags: Vec<String>,
5982

60-
/// The search criteria that matching tasks must have to be counted
61-
#[serde(default = "TaskwarriorConfig::default_filter")]
62-
pub filter: String,
83+
/// A list of named filter criteria which must be fulfilled to be counted towards
84+
/// the total, when that filter is active.
85+
#[serde(default = "TaskwarriorConfig::default_filters")]
86+
pub filters: Vec<Filter>,
6387

6488
/// Format override
6589
#[serde(default = "TaskwarriorConfig::default_format")]
6690
pub format: String,
6791

68-
/// Format override if exactly one task is pending
92+
/// Format override if the count is one
6993
#[serde(default = "TaskwarriorConfig::default_format")]
7094
pub format_singular: String,
7195

72-
/// Format override if all tasks are completed
96+
/// Format override if the count is zero
7397
#[serde(default = "TaskwarriorConfig::default_format")]
7498
pub format_everything_done: String,
7599

76100
#[serde(default = "TaskwarriorConfig::default_color_overrides")]
77101
pub color_overrides: Option<BTreeMap<String, String>>,
78102
}
79103

80-
enum TaskwarriorBlockMode {
81-
// Show only the tasks which are filtered by the set tags and which are not completed.
82-
OnlyFilteredPendingTasks,
83-
// Show all pending tasks and ignore the filtering tags.
84-
AllPendingTasks,
85-
}
86-
87104
impl TaskwarriorConfig {
88105
fn default_interval() -> Duration {
89106
Duration::from_secs(600)
@@ -101,8 +118,11 @@ impl TaskwarriorConfig {
101118
vec![]
102119
}
103120

104-
fn default_filter() -> String {
105-
String::new()
121+
fn default_filters() -> Vec<Filter> {
122+
vec![Filter::new(
123+
"pending".to_string(),
124+
"-COMPLETED -DELETED".to_string(),
125+
)]
106126
}
107127

108128
fn default_format() -> String {
@@ -126,19 +146,22 @@ impl ConfigBlock for Taskwarrior {
126146
let output = ButtonWidget::new(config.clone(), &id)
127147
.with_icon("tasks")
128148
.with_text("-");
149+
// If the deprecated `filter_tags` option has been set,
150+
// convert it to the new `filter` format.
151+
let filters = if block_config.filter_tags.len() > 0 {
152+
vec![
153+
Filter::legacy("filtered".to_string(), &block_config.filter_tags),
154+
Filter::legacy("all".to_string(), &vec![]),
155+
]
156+
} else {
157+
block_config.filters
158+
};
129159

130160
Ok(Taskwarrior {
131161
id: pseudo_uuid(),
132162
update_interval: block_config.interval,
133163
warning_threshold: block_config.warning_threshold,
134164
critical_threshold: block_config.critical_threshold,
135-
filter: if block_config.filter_tags.len() > 0 {
136-
tags_to_filter(&block_config.filter_tags)
137-
} else {
138-
block_config.filter
139-
},
140-
block_mode: TaskwarriorBlockMode::OnlyFilteredPendingTasks,
141-
output,
142165
format: FormatTemplate::from_string(&block_config.format).block_error(
143166
"taskwarrior",
144167
"Invalid format specified for taskwarrior::format",
@@ -156,7 +179,10 @@ impl ConfigBlock for Taskwarrior {
156179
"Invalid format specified for taskwarrior::format_everything_done",
157180
)?,
158181
tx_update_request,
182+
filter_index: 0,
159183
config,
184+
filters,
185+
output,
160186
})
161187
}
162188
}
@@ -177,33 +203,20 @@ fn has_taskwarrior() -> Result<bool> {
177203
!= "")
178204
}
179205

180-
fn tags_to_filter(tags: &[String]) -> String {
181-
tags.iter()
182-
.map(|element| format!("+{}", element))
183-
.collect::<Vec<String>>()
184-
.join(" ")
185-
}
186-
187-
fn get_number_of_pending_tasks(filter: &str) -> Result<u32> {
206+
fn get_number_of_tasks(filter: &str) -> Result<u32> {
188207
String::from_utf8(
189208
Command::new("sh")
190-
.args(&[
191-
"-c",
192-
&format!(
193-
"task rc.gc=off -COMPLETED -DELETED {} count",
194-
filter
195-
),
196-
])
209+
.args(&["-c", &format!("task rc.gc=off {} count", filter)])
197210
.output()
198211
.block_error(
199212
"taskwarrior",
200-
"failed to run taskwarrior for getting the number of pending tasks",
213+
"failed to run taskwarrior for getting the number of tasks",
201214
)?
202215
.stdout,
203216
)
204217
.block_error(
205218
"taskwarrior",
206-
"failed to get the number of pending tasks from taskwarrior",
219+
"failed to get the number of tasks from taskwarrior",
207220
)?
208221
.trim()
209222
.parse::<u32>()
@@ -215,20 +228,23 @@ impl Block for Taskwarrior {
215228
if !has_taskwarrior()? {
216229
self.output.set_text("?")
217230
} else {
218-
let filter = match self.block_mode {
219-
TaskwarriorBlockMode::OnlyFilteredPendingTasks => &self.filter,
220-
TaskwarriorBlockMode::AllPendingTasks => "",
221-
};
222-
let number_of_pending_tasks = get_number_of_pending_tasks(filter)?;
223-
let values = map!("{count}" => number_of_pending_tasks);
224-
self.output.set_text(match number_of_pending_tasks {
231+
let filter = self.filters.get(self.filter_index).block_error(
232+
"taskwarrior",
233+
&format!("Filter at index {} does not exist", self.filter_index),
234+
)?;
235+
let number_of_tasks = get_number_of_tasks(&filter.filter)?;
236+
let values = map!(
237+
"{count}" => number_of_tasks.to_string(),
238+
"{filter_name}" => filter.name.clone()
239+
);
240+
self.output.set_text(match number_of_tasks {
225241
0 => self.format_everything_done.render_static_str(&values)?,
226242
1 => self.format_singular.render_static_str(&values)?,
227243
_ => self.format.render_static_str(&values)?,
228244
});
229-
if number_of_pending_tasks >= self.critical_threshold {
245+
if number_of_tasks >= self.critical_threshold {
230246
self.output.set_state(State::Critical);
231-
} else if number_of_pending_tasks >= self.warning_threshold {
247+
} else if number_of_tasks >= self.warning_threshold {
232248
self.output.set_state(State::Warning);
233249
} else {
234250
self.output.set_state(State::Idle);
@@ -250,14 +266,8 @@ impl Block for Taskwarrior {
250266
self.update()?;
251267
}
252268
MouseButton::Right => {
253-
match self.block_mode {
254-
TaskwarriorBlockMode::OnlyFilteredPendingTasks => {
255-
self.block_mode = TaskwarriorBlockMode::AllPendingTasks
256-
}
257-
TaskwarriorBlockMode::AllPendingTasks => {
258-
self.block_mode = TaskwarriorBlockMode::OnlyFilteredPendingTasks
259-
}
260-
}
269+
// Increment the filter_index, rotating at the end
270+
self.filter_index = (self.filter_index + 1) % self.filters.len();
261271
self.update()?;
262272
}
263273
_ => {}

0 commit comments

Comments
 (0)