use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use serde_derive::Deserialize;
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::de::deserialize_duration;
use crate::errors::*;
use crate::formatting::value::Value;
use crate::formatting::FormatTemplate;
use crate::protocol::i3bar_event::{I3BarEvent, MouseButton};
use crate::scheduler::Task;
use crate::util::expand_string;
use crate::widgets::text::TextWidget;
use crate::widgets::{I3BarWidget, State};
use inotify::{EventMask, Inotify, WatchMask};
pub struct Taskwarrior {
output: TextWidget,
update_interval: Duration,
warning_threshold: u32,
critical_threshold: u32,
filters: Vec<Filter>,
filter_index: usize,
format: FormatTemplate,
format_singular: FormatTemplate,
format_everything_done: FormatTemplate,
}
#[derive(Deserialize, Debug, Default, Clone)]
#[serde(deny_unknown_fields)]
pub struct Filter {
pub name: String,
pub filter: String,
}
impl Filter {
pub fn new(name: String, filter: String) -> Self {
Filter { name, filter }
}
pub fn legacy(name: String, tags: &[String]) -> Self {
let tags = tags
.iter()
.map(|element| format!("+{}", element))
.collect::<Vec<String>>()
.join(" ");
let filter = format!("-COMPLETED -DELETED {}", tags);
Self::new(name, filter)
}
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct TaskwarriorConfig {
#[serde(deserialize_with = "deserialize_duration")]
pub interval: Duration,
pub warning_threshold: u32,
pub critical_threshold: u32,
pub filter_tags: Vec<String>,
pub filters: Vec<Filter>,
pub format: FormatTemplate,
pub format_singular: FormatTemplate,
pub format_everything_done: FormatTemplate,
pub data_location: String,
}
impl Default for TaskwarriorConfig {
fn default() -> Self {
Self {
interval: Duration::from_secs(600),
warning_threshold: 10,
critical_threshold: 20,
filter_tags: vec![],
filters: vec![Filter::new(
"pending".to_string(),
"-COMPLETED -DELETED".to_string(),
)],
format: FormatTemplate::default(),
format_singular: FormatTemplate::default(),
format_everything_done: FormatTemplate::default(),
data_location: "~/.task".to_string(),
}
}
}
impl ConfigBlock for Taskwarrior {
type Config = TaskwarriorConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
tx_update_request: Sender<Task>,
) -> Result<Self> {
let output = TextWidget::new(id, 0, shared_config)
.with_icon("tasks")?
.with_text("-");
let filters = if !block_config.filter_tags.is_empty() {
vec![
Filter::legacy("filtered".to_string(), &block_config.filter_tags),
Filter::legacy("all".to_string(), &[]),
]
} else {
block_config.filters
};
let data_location = block_config.data_location.clone();
let data_location = expand_string(&data_location)?;
thread::Builder::new()
.name("taskwarrior".into())
.spawn(move || {
let mut notify = Inotify::init().expect("Failed to start inotify");
notify
.add_watch(data_location, WatchMask::MODIFY)
.expect("Failed to watch task directory");
let mut buffer = [0; 1024];
loop {
let mut events = notify
.read_events_blocking(&mut buffer)
.expect("Error while reading inotify events");
if events.any(|event| event.mask.contains(EventMask::MODIFY)) {
tx_update_request
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
thread::sleep(Duration::from_millis(250))
}
})
.unwrap();
Ok(Taskwarrior {
update_interval: block_config.interval,
warning_threshold: block_config.warning_threshold,
critical_threshold: block_config.critical_threshold,
format: block_config.format.with_default("{count}")?,
format_singular: block_config.format_singular.with_default("{count}")?,
format_everything_done: block_config
.format_everything_done
.with_default("{count}")?,
filter_index: 0,
filters,
output,
})
}
}
fn has_taskwarrior() -> Result<bool> {
Ok(String::from_utf8(
Command::new("sh")
.args(&["-c", "type -P task"])
.output()
.error_msg("failed to start command to check for taskwarrior")?
.stdout,
)
.error_msg("failed to check for taskwarrior")?
.trim()
!= "")
}
fn get_number_of_tasks(filter: &str) -> Result<u32> {
String::from_utf8(
Command::new("sh")
.args(&["-c", &format!("task rc.gc=off {} count", filter)])
.output()
.error_msg("failed to run taskwarrior for getting the number of tasks")?
.stdout,
)
.error_msg("failed to get the number of tasks from taskwarrior")?
.trim()
.parse::<u32>()
.error_msg("could not parse the result of taskwarrior")
}
impl Block for Taskwarrior {
fn name(&self) -> &'static str {
"taskwarrior"
}
fn update(&mut self) -> Result<Option<Update>> {
if !has_taskwarrior()? {
self.output.set_text("?".to_string())
} else {
let filter = self.filters.get(self.filter_index).error_msg(format!(
"Filter at index {} does not exist",
self.filter_index
))?;
let number_of_tasks = get_number_of_tasks(&filter.filter)?;
let values = map!(
"count" => Value::from_integer(number_of_tasks as i64),
"filter_name" => Value::from_string(filter.name.clone()),
);
self.output.set_texts(match number_of_tasks {
0 => self.format_everything_done.render(&values)?,
1 => self.format_singular.render(&values)?,
_ => self.format.render(&values)?,
});
if number_of_tasks >= self.critical_threshold {
self.output.set_state(State::Critical);
} else if number_of_tasks >= self.warning_threshold {
self.output.set_state(State::Warning);
} else {
self.output.set_state(State::Idle);
}
}
Ok(Some(self.update_interval.into()))
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
vec![&self.output]
}
fn click(&mut self, event: &I3BarEvent) -> Result<()> {
match event.button {
MouseButton::Left => {
self.update()?;
}
MouseButton::Right => {
self.filter_index = (self.filter_index + 1) % self.filters.len();
self.update()?;
}
_ => {}
}
Ok(())
}
}