Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 1703 timewarrior block #1878

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ words:
- sysfs
- tebi
- tera
- timew
- tzname
- tzset
- udev
Expand Down
1 change: 1 addition & 0 deletions src/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ define_blocks!(
taskwarrior,
temperature,
time,
timewarrior,
tea_timer,
toggle,
uptime,
Expand Down
193 changes: 193 additions & 0 deletions src/blocks/timewarrior.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//! Time and information of the current timewarrior task
//!
//! Clicking left mouse stops or resumes the task
//!
//! # Configuration
//!
//! Key | Values | Default
//! ----|--------|--------
//! `interval` | Update interval in seconds | `30`
//! `format` | A string to customise the output of the block. See placeholders. | <code>" $icon {$elapsed &vert;}"</code>
//! `info` | The threshold of minutes the task turns into a info state | -
//! `good` | The threshold of minutes the task turns into a good state | -
//! `warning` | The threshold of minutes the task turns into a warning state | -
//! `critical` | The threshold of minutes the task turns into a critical state | -
//!
//! Placeholder | Value | Type | Unit
//! ------------|-------|------|------
//! `icon` | A static icon | Icon | -
//! `elapsed`| Elapsed time in format H:MM (Only present if task is active) | Text | -
//! `tags` | Tags of the active task separated by space (Only present if task is active) | Text | -
//! `annotation` | Annotation of the active task (Only present if task is active) | Text | -
//!
//! Action | Default button
//! ----------------|----------------
//! `stop_continue` | Left
//!
//! # Example
//! ```toml
//! [[block]]
//! block = "timewarrior"
//! format = " $icon {$tags.str(w:8,rot_interval:4) $elapsed|}"
//! ```
//!
//! # Icons Used
//! - `tasks`

use super::prelude::*;
use chrono::DateTime;
use tokio::process::Command;

#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
#[default(30.into())]
interval: Seconds,
format: FormatConfig,

info: Option<u64>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not make these i64 to avoid casting (elapsed.num_minutes() as u64)?

good: Option<u64>,
warning: Option<u64>,
critical: Option<u64>,
}

pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[(MouseButton::Left, None, "stop_continue")])?;

let widget = Widget::new().with_format(config.format.with_default(" $icon {$elapsed|}")?);

loop {
let mut values = map! {
"icon" => Value::icon("tasks"),
};
let mut state = State::Idle;
let mut widget = widget.clone();

let data = get_current_timewarrior_task().await?;
if let Some(tw) = data {
if tw.end.is_none() {
// only show active tasks
let elapsed = chrono::Utc::now() - tw.start;

// calculate state
for (level, st) in [
(&config.critical, State::Critical),
(&config.warning, State::Warning),
(&config.good, State::Good),
(&config.info, State::Info),
] {
if let Some(value) = level {
if (elapsed.num_minutes() as u64) >= *value {
state = st;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of keeping a state variable and then later setting the widget state you can just set the widget's state directly.

break;
}
}
}

values.insert("tags".into(), Value::text(tw.tags.join(" ")));

let elapsedstr =
format!("{}:{:0>2}", elapsed.num_hours(), elapsed.num_minutes() % 60);
values.insert("elapsed".into(), Value::text(elapsedstr));

if let Some(annotation) = tw.annotation {
values.insert("annotation".into(), Value::text(annotation));
}
}
}

widget.state = state;
widget.set_values(values);
api.set_widget(widget)?;

select! {
_ = sleep(config.interval.0) => (),
_ = api.wait_for_update_request() => (),
Some(action) = actions.recv() => match action.as_ref() {
"stop_continue" => { stop_continue().await?; }
_ => (),
}
}
}
}

/// Raw output from timew
#[derive(Deserialize, Debug)]
struct TimewarriorRAW {
pub id: u32,
pub start: String,
pub tags: Vec<String>,
pub annotation: Option<String>,
pub end: Option<String>,
}

/// TimeWarrior entry
#[derive(Debug, PartialEq, Deserialize)]
#[serde(from = "TimewarriorRAW")]
struct TimewarriorData {
pub id: u32,
pub start: DateTime<chrono::offset::Utc>,
pub tags: Vec<String>,
pub annotation: Option<String>,
pub end: Option<DateTime<chrono::offset::Utc>>,
}

impl From<TimewarriorRAW> for TimewarriorData {
fn from(item: TimewarriorRAW) -> Self {
Self {
id: item.id,
tags: item.tags,
annotation: item.annotation,
start: chrono::TimeZone::from_utc_datetime(
&chrono::Utc,
&chrono::NaiveDateTime::parse_from_str(&item.start, "%Y%m%dT%H%M%SZ").unwrap()
),
end: item.end.map(|v| {
chrono::TimeZone::from_utc_datetime(
&chrono::Utc,
&chrono::NaiveDateTime::parse_from_str(&v, "%Y%m%dT%H%M%SZ").unwrap()
)
}),
}
}
}

/// Format a DateTime given a format string
#[allow(dead_code)]
fn format_datetime(date: &DateTime<chrono::Utc>, format: &str) -> String {
date.format(format).to_string()
}

/// Execute "timew export now" and return the current task (if any)
async fn get_current_timewarrior_task() -> Result<Option<TimewarriorData>> {
let out = Command::new("timew")
.args(["export", "now"])
.output()
.await
.error("failed to run timewarrior")?
.stdout;
Ok(serde_json::from_slice::<Vec<TimewarriorData>>(&out)
.unwrap_or_default()
.into_iter()
.next())
}

/// Stop or continue a task
async fn stop_continue() -> Result<()> {
let is_stopped = get_current_timewarrior_task()
.await?
.map_or(true, |tw| tw.end.is_some());
let args = if is_stopped { "continue" } else { "stop" };
Command::new("timew")
.args([args])
.stdout(std::process::Stdio::null())
.spawn()
.error("Error spawning timew")?
.wait()
.await
.error("Error executing stop/continue")?
.success()
.then_some(())
.error("timew exited with non-zero value when attempting to stop/continue")
}