Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
/target
**/*.rs.bk
.idea/
coverage/
lcov.info
19 changes: 18 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ pub struct Config {
}

impl Config {
pub fn new(activity_paths: Vec<PathBuf>) -> Self {
let mut config = Self::default();
for path in activity_paths {
config.add_activity_path(path);
}
config
}

fn add_activity_path(&mut self, p: PathBuf) {
self.activity_paths.insert(canonicalize(p).unwrap());
if let Ok(canonical_path) = canonicalize(p) {
self.activity_paths.insert(canonical_path);
}
}

pub fn get_file_paths(&self) -> BTreeSet<PathBuf> {
Expand Down Expand Up @@ -394,6 +404,13 @@ mod tests {
Some(&Quadrant::Q1)
);
}

#[test]
fn test_add_activity_path_non_existent() {
let mut config = Config::default();
config.add_activity_path(PathBuf::from("non_existent_path"));
assert!(config.get_file_paths().is_empty());
}
}

pub enum MetaCategory<'a> {
Expand Down
37 changes: 37 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,40 @@ fn main() -> anyhow::Result<()> {

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use tempfile::NamedTempFile;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;

#[test]
fn test_main_loop_basic() {
let mut activity_file = NamedTempFile::new().unwrap();
writeln!(activity_file, "2024-01-01 10:00\n1 0 Task 1 @work").unwrap();

let plan_file = NamedTempFile::new().unwrap();
let summary_file = NamedTempFile::new().unwrap();

let mut config = Config::new(vec![activity_file.path().to_path_buf()]);
config.plan_out = Some(plan_file.path().to_str().unwrap().to_string());
config.summary_out = Some(summary_file.path().to_str().unwrap().to_string());
config.quiet = true;

let result = main_loop(&config);
assert!(result.is_ok());

let plan_output = std::fs::read_to_string(plan_file.path()).unwrap();
assert!(plan_output.contains("-- 10:00 2024-01-01"));
assert!(plan_output.contains("-> 11h00 Task 1 @work"));

let summary_output = std::fs::read_to_string(summary_file.path()).unwrap();
assert!(!summary_output.contains("2024-01-01+00:00 (summary)"));
assert!(summary_output.contains("(all past summaries)"));
assert!(summary_output.contains("@work: 01h00"));
}
}
184 changes: 150 additions & 34 deletions src/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,61 +13,85 @@ use crate::parse::{LifeChunk, TimedLifeChunk};
use notify_rust::Notification;
use std::thread::JoinHandle;

pub fn spawn_notification_thread(q: VecDeque<TimedLifeChunk>, rx: Receiver<()>) -> JoinHandle<()> {
// XXX: should join somehow
pub trait NotificationHandler {
fn show_notification(&self, summary: &str, body: &str) -> Result<(), notify_rust::error::Error>;
}

struct DefaultNotificationHandler;

impl NotificationHandler for DefaultNotificationHandler {
fn show_notification(&self, summary: &str, body: &str) -> Result<(), notify_rust::error::Error> {
Notification::new()
.summary(summary)
.body(body)
.timeout(10000)
.show()
.map(|_| ())
}
}

pub fn spawn_notification_thread(
q: VecDeque<TimedLifeChunk>,
rx: Receiver<()>,
) -> JoinHandle<()> {
spawn_notification_thread_with_handler(q, rx, DefaultNotificationHandler)
}

pub fn spawn_notification_thread_with_handler<T: NotificationHandler + Send + 'static>(
q: VecDeque<TimedLifeChunk>,
rx: Receiver<()>,
notification_handler: T,
) -> JoinHandle<()> {
let mut lc_ls = VecDeque::new();
for tlc in q {
lc_ls.push_back((tlc.start, tlc.life_chunk));
}

thread::spawn(move || {
while let Some((t, lc)) = lc_ls.pop_front() {
let notify = || notify(lc);

{
let upper_bound = Local::now() - Duration::minutes(5);
if t < upper_bound {
continue;
} else if t < Local::now() {
notify();
continue;
let notify = || notify(&notification_handler, lc.clone());

let now = Local::now();
let upper_bound = now - Duration::minutes(5);

if t < upper_bound {
continue;
} else if t <= now {
if notify() {
return;
}
continue;
}

// XXX: probably buggy in case of timeout of 0
let delay = Duration::minutes(5);
let time_to_notification = unwrap_dur((t - Local::now() - delay).to_std());
match rx.recv_timeout(time_to_notification) {
Ok(()) | Err(RecvTimeoutError::Disconnected) => {
return;
}
Err(RecvTimeoutError::Timeout) => {
if notify() {
return;
let time_to_notification = (t - now - delay).to_std();

if let Ok(time_to_notification) = time_to_notification {
match rx.recv_timeout(time_to_notification) {
Ok(()) | Err(RecvTimeoutError::Disconnected) => return,
Err(RecvTimeoutError::Timeout) => {
if notify() {
return;
}
}
}
}
}
notify_nothing();
notify_nothing(&notification_handler);
})
}

fn notify(lc: LifeChunk) -> bool {
let mut n = Notification::new();
n.summary("Tiro: Time to switch!").timeout(10000);
n.body(&format!("Next activity: {}", lc.get_input()));
let r = n.show();

r.is_err()
fn notify<T: NotificationHandler>(notification_handler: &T, lc: LifeChunk) -> bool {
notification_handler
.show_notification("Tiro: Time to switch!", &format!("Next activity: {}", lc.get_input()))
.is_err()
}

fn notify_nothing() -> bool {
let mut n = Notification::new();
n.summary("Tiro: No activity planned!").timeout(100_000);

let r = n.show();

r.is_err()
fn notify_nothing<T: NotificationHandler>(notification_handler: &T) -> bool {
notification_handler
.show_notification("Tiro: No activity planned!", "")
.is_err()
}

fn unwrap_dur(r: Result<StdDuration, OutOfRangeError>) -> StdDuration {
Expand All @@ -76,3 +100,95 @@ fn unwrap_dur(r: Result<StdDuration, OutOfRangeError>) -> StdDuration {
Err(_) => StdDuration::new(0, 0),
}
}

#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
use crate::parse::LifeChunk;
use std::sync::{Arc, Mutex};
use notify_rust::error::Error;

struct MockNotificationHandler {
invocations: Arc<Mutex<Vec<(String, String)>>>,
}

impl NotificationHandler for MockNotificationHandler {
fn show_notification(&self, summary: &str, body: &str) -> Result<(), Error> {
let mut invocations = self.invocations.lock().unwrap();
invocations.push((summary.to_string(), body.to_string()));
Ok(())
}
}

#[test]
fn test_notify() {
let invocations = Arc::new(Mutex::new(Vec::new()));
let handler = MockNotificationHandler {
invocations: invocations.clone(),
};
let lc = LifeChunk::new(
"test input".to_string(),
time::Duration::minutes(1),
vec![],
crate::config::Quadrant::Q1,
false,
"test input".to_string(),
);

notify(&handler, lc);

let invocations = invocations.lock().unwrap();
assert_eq!(invocations.len(), 1);
assert_eq!(invocations[0].0, "Tiro: Time to switch!");
assert_eq!(invocations[0].1, "Next activity: test input");
}

#[test]
fn test_notify_nothing() {
let invocations = Arc::new(Mutex::new(Vec::new()));
let handler = MockNotificationHandler {
invocations: invocations.clone(),
};

notify_nothing(&handler);

let invocations = invocations.lock().unwrap();
assert_eq!(invocations.len(), 1);
assert_eq!(invocations[0].0, "Tiro: No activity planned!");
assert_eq!(invocations[0].1, "");
}

#[test]
fn test_spawn_notification_thread_with_handler() {
let invocations = Arc::new(Mutex::new(Vec::new()));
let handler = MockNotificationHandler {
invocations: invocations.clone(),
};
let (_tx, rx) = std::sync::mpsc::channel();

let q = VecDeque::new();

let handle = spawn_notification_thread_with_handler(q, rx, handler);
handle.join().unwrap();

let invocations = invocations.lock().unwrap();
assert_eq!(invocations.len(), 1);
assert_eq!(invocations[0].0, "Tiro: No activity planned!");
}

#[test]
fn test_unwrap_dur_ok() {
let dur = StdDuration::new(1, 0);
let result = Ok(dur);
assert_eq!(unwrap_dur(result), dur);
}

#[test]
fn test_unwrap_dur_err() {
let neg_chrono_dur = Duration::seconds(-1);
let result = neg_chrono_dur.to_std();
assert!(result.is_err());
assert_eq!(unwrap_dur(result), StdDuration::new(0, 0));
}
}
8 changes: 4 additions & 4 deletions tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fn cli_basic_run_with_activities_file_outputs_to_stdout() {
-> 11h00 Task 1 @work
-> 13h00 Task 2 @home

2020-12-01+01:00 (summary)
2020-12-01+00:00 (summary)
@home: 02h00
@work: 01h00

Expand Down Expand Up @@ -232,11 +232,11 @@ fn cli_outputs_both_summary_and_global_summary() {
-> 12h00 Development @project
-> 13h00 Review @meeting

2020-12-01+01:00 (summary)
2020-12-01+00:00 (summary)
@meeting: 01h30
@project: 02h00

2020-12-02+01:00 (summary)
2020-12-02+00:00 (summary)
@meeting: 01h00
@project: 03h00

Expand Down Expand Up @@ -374,7 +374,7 @@ fn cli_with_stdin_input_processes_activities() {
-- 09:00 2020-12-01
-> 10h00 Morning task @urgent

2020-12-01+01:00 (summary)
2020-12-01+00:00 (summary)
@urgent: 01h00

<DATE> (all past summaries)
Expand Down
Loading