use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::thread;
use std::time::{Duration, Instant};
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::de::deserialize_duration;
use crate::de::deserialize_local_timestamp;
use crate::errors::*;
use crate::protocol::i3bar_event::I3BarEvent;
use crate::scheduler::Task;
use crate::util::{expand_string, xdg_config_home};
use crate::widgets::text::TextWidget;
use crate::widgets::{I3BarWidget, State};
use chrono::offset::Local;
use chrono::DateTime;
use crossbeam_channel::Sender;
use inotify::{Inotify, WatchMask};
use serde_derive::Deserialize;
pub struct Watson {
text: TextWidget,
state_path: PathBuf,
show_time: bool,
prev_state: Option<WatsonState>,
update_interval: Duration,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(deny_unknown_fields, default)]
pub struct WatsonConfig {
pub state_path: PathBuf,
#[serde(deserialize_with = "deserialize_duration")]
pub interval: Duration,
pub show_time: bool,
}
impl Default for WatsonConfig {
fn default() -> Self {
let mut config_dir = xdg_config_home();
config_dir.push("watson/state");
Self {
state_path: config_dir,
interval: Duration::from_secs(60),
show_time: false,
}
}
}
impl ConfigBlock for Watson {
type Config = WatsonConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
tx_update_request: Sender<Task>,
) -> Result<Self> {
let state_path_string = block_config.state_path.to_str().unwrap();
let watson = Watson {
text: TextWidget::new(id, 0, shared_config),
state_path: PathBuf::from(expand_string(state_path_string)?),
show_time: block_config.show_time,
update_interval: block_config.interval,
prev_state: None,
};
thread::spawn(move || {
let (file_name, parent_dir) = {
let name = block_config
.state_path
.file_name()
.expect("watson state file had no name")
.to_owned();
let mut s = block_config.state_path;
s.pop();
(name, s)
};
let mut notify = Inotify::init().expect("failed to start inotify");
notify
.add_watch(&parent_dir, WatchMask::CREATE | WatchMask::MOVED_TO)
.expect("failed to watch watson state file");
let mut buffer = [0; 1024];
loop {
let events = notify
.read_events_blocking(&mut buffer)
.expect("error while reading inotify events");
for _event in events.filter(|e| e.name == Some(&file_name)) {
tx_update_request
.send(Task {
id,
update_time: Instant::now(),
})
.expect("unable to send task from watson watcher");
}
}
});
Ok(watson)
}
}
impl Block for Watson {
fn name(&self) -> &'static str {
"watson"
}
fn update(&mut self) -> Result<Option<Update>> {
let state = {
let file = BufReader::new(
File::open(&self.state_path).error_msg("unable to open state file")?,
);
serde_json::from_reader(file).error_msg("unable to deserialize state")?
};
match state {
state @ WatsonState::Active { .. } => {
self.text.set_state(State::Good);
self.text
.set_text(state.format(self.show_time, "started", format_delta_past));
self.prev_state = Some(state);
Ok(if self.show_time {
Some(self.update_interval.into())
} else {
None
})
}
WatsonState::Idle {} => {
if let Some(prev_state @ WatsonState::Active { .. }) = &self.prev_state {
self.text
.set_text(prev_state.format(true, "stopped", format_delta_after));
self.text.set_state(State::Idle);
self.prev_state = Some(state);
Ok(Some(Duration::from_secs(5).into()))
} else {
self.text.set_state(State::Idle);
self.text.set_text(String::new());
self.prev_state = Some(state);
Ok(None)
}
}
}
}
fn click(&mut self, _e: &I3BarEvent) -> Result<()> {
self.show_time = !self.show_time;
self.update()?;
Ok(())
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
vec![&self.text]
}
}
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!("{} {}{} ago", n, label, 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()
.filter(|&(_, n)| *n != 0)
.map(|&(label, n)| format!("after {} {}{}", n, label, if n > 1 { "s" } else { "" }))
.next()
.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 = String::with_capacity(16);
s.push_str(project);
if !tags.is_empty() {
s.push(' ');
s.push('[');
s.push_str(&tags.join(" "));
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")
}
}
}