use chrono::{offset::Local, DateTime};
use dirs::config_dir;
use inotify::{Inotify, WatchMask};
use serde::de::Deserializer;
use std::path::PathBuf;
use tokio::fs::read_to_string;
use super::prelude::*;
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
pub format: FormatConfig,
pub state_path: Option<ShellString>,
#[default(60.into())]
pub interval: Seconds,
pub show_time: bool,
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[(MouseButton::Left, None, "toggle_show_time")])?;
let format = config.format.with_default(" $text |")?;
let mut show_time = config.show_time;
let (state_dir, state_file, state_path) = match &config.state_path {
Some(p) => {
let mut p: PathBuf = (*p.expand()?).into();
let path = p.clone();
let file = p.file_name().error("Failed to parse state_dir")?.to_owned();
p.pop();
(p, file, path)
}
None => {
let mut path = config_dir().error("xdg config directory not found")?;
path.push("watson");
let dir = path.clone();
path.push("state");
(dir, "state".into(), path)
}
};
let notify = Inotify::init().error("Failed to start inotify")?;
notify
.watches()
.add(&state_dir, WatchMask::CREATE | WatchMask::MOVED_TO)
.error("Failed to watch watson state directory")?;
let mut state_updates = notify
.into_event_stream([0; 1024])
.error("Failed to create event stream")?;
let mut timer = config.interval.timer();
let mut prev_state = None;
loop {
let state = read_to_string(&state_path)
.await
.error("Failed to read state file")?;
let state = serde_json::from_str(&state).unwrap_or(WatsonState::Idle {});
let mut widget = Widget::new().with_format(format.clone());
match state {
state @ WatsonState::Active { .. } => {
widget.state = State::Good;
widget.set_values(map!(
"text" => Value::text(state.format(show_time, "started", format_delta_past))
));
prev_state = Some(state);
}
WatsonState::Idle {} => {
if let Some(prev @ WatsonState::Active { .. }) = &prev_state {
widget.state = State::Idle;
widget.set_values(map!(
"text" => Value::text(prev.format(true, "stopped", format_delta_after))
));
} else {
widget.state = State::Idle;
widget.set_values(Values::default());
}
prev_state = Some(state);
}
}
api.set_widget(widget)?;
loop {
select! {
_ = timer.tick() => break,
_ = api.wait_for_update_request() => break,
Some(update) = state_updates.next() => {
let update = update.error("Bad inotify update")?;
if update.name.is_some_and(|x| state_file == x) {
break;
}
}
Some(action) = actions.recv() => match action.as_ref() {
"toggle_show_time" => {
show_time = !show_time;
break;
}
_ => (),
}
}
}
}
}
fn format_delta_past(delta: &chrono::Duration) -> String {
let spans = &[
("week", delta.num_weeks()),
("day", delta.num_days()),
("hour", delta.num_hours()),
("minute", delta.num_minutes()),
];
spans
.iter()
.filter(|&(_, n)| *n != 0)
.map(|&(label, n)| format!("{n} {label}{} ago", if n > 1 { "s" } else { "" }))
.next()
.unwrap_or_else(|| "now".into())
}
fn format_delta_after(delta: &chrono::Duration) -> String {
let spans = &[
("week", delta.num_weeks()),
("day", delta.num_days()),
("hour", delta.num_hours()),
("minute", delta.num_minutes()),
("second", delta.num_seconds()),
];
spans
.iter()
.find(|&(_, n)| *n != 0)
.map(|&(label, n)| format!("after {n} {label}{}", if n > 1 { "s" } else { "" }))
.unwrap_or_else(|| "now".into())
}
#[derive(Deserialize, Clone, Debug)]
#[serde(untagged)]
enum WatsonState {
Active {
project: String,
#[serde(deserialize_with = "deserialize_local_timestamp")]
start: DateTime<Local>,
tags: Vec<String>,
},
Idle {},
}
impl WatsonState {
fn format(&self, show_time: bool, verb: &str, f: fn(&chrono::Duration) -> String) -> String {
if let WatsonState::Active {
project,
start,
tags,
} = self
{
let mut s = project.clone();
if let [first, other @ ..] = &tags[..] {
s.push_str(" [");
s.push_str(first);
for tag in other {
s.push(' ');
s.push_str(tag);
}
s.push(']');
}
if show_time {
s.push(' ');
s.push_str(verb);
let delta = Local::now() - *start;
s.push(' ');
s.push_str(&f(&delta));
}
s
} else {
panic!("WatsonState::Idle does not have a specified format")
}
}
}
pub fn deserialize_local_timestamp<'de, D>(deserializer: D) -> Result<DateTime<Local>, D::Error>
where
D: Deserializer<'de>,
{
use chrono::TimeZone;
i64::deserialize(deserializer).map(|seconds| Local.timestamp_opt(seconds, 0).single().unwrap())
}