diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9869cbb5..dc88985a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -307,7 +307,7 @@ jobs: cd /tmp git clone https://github.com/GothenburgBitFactory/taskwarrior cd taskwarrior - git checkout v2.6.1 + git checkout v3.0.0 cmake -DCMAKE_BUILD_TYPE=release -DENABLE_SYNC=OFF . make sudo make install diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3734b434..0f891cbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: cd /tmp git clone https://github.com/GothenburgBitFactory/taskwarrior cd taskwarrior - git checkout v2.6.1 + git checkout v3.0.0 cmake -DCMAKE_BUILD_TYPE=release -DENABLE_SYNC=OFF . make sudo make install diff --git a/Cargo.lock b/Cargo.lock index 8981dc33..4310507c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1329,7 +1329,7 @@ dependencies = [ [[package]] name = "taskwarrior-tui" -version = "0.25.4" +version = "0.26.0" dependencies = [ "anyhow", "better-panic", diff --git a/Cargo.toml b/Cargo.toml index d4ef53bc..4de2253b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "taskwarrior-tui" -version = "0.25.4" +version = "0.26.0" license = "MIT" description = "A Taskwarrior Terminal User Interface" repository = "https://github.com/kdheepak/taskwarrior-tui/" @@ -18,9 +18,7 @@ better-panic = "0.3.0" cassowary = "0.3.0" chrono = "0.4.26" clap = { version = "4.4.1", features = ["derive"] } -crossterm = { version = "0.27.0", features = [ - "event-stream", -] } +crossterm = { version = "0.27.0", features = ["event-stream"] } dirs = "5.0.1" futures = "0.3.28" itertools = "0.11.0" @@ -57,7 +55,7 @@ taskwarrior-tui = { path = "/usr/bin/taskwarrior-tui" } [profile.release] debug = 1 incremental = true -lto = "off" +lto = "fat" [build-dependencies] clap = { version = "4.4.1", features = ["derive"] } diff --git a/README.md b/README.md index 318b7ade..0df46f78 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # `taskwarrior-tui` > [!IMPORTANT] -> `taskwarrior-tui` is only tested with `taskwarrior` v2.x. [`taskwarrior` v3.x](https://github.com/GothenburgBitFactory/taskwarrior/releases/tag/v3.0.0) may not work as intended. +> [`taskwarrior` v3.x](https://github.com/GothenburgBitFactory/taskwarrior/releases/tag/v3.0.0) may break `taskwarrior-tui` features in unexpected ways. Please file a bug report if you encounter a bug. [![CI](https://github.com/kdheepak/taskwarrior-tui/workflows/CI/badge.svg)](https://github.com/kdheepak/taskwarrior-tui/actions?query=workflow%3ACI) [![](https://img.shields.io/github/license/kdheepak/taskwarrior-tui)](./LICENSE) diff --git a/src/app.rs b/src/app.rs index 271e4d46..031aa9c3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -64,7 +64,7 @@ const MAX_LINE: usize = 4096; lazy_static! { static ref START_TIME: Instant = Instant::now(); - static ref TASKWARRIOR_VERSION_SUPPORTED: Versioning = Versioning::new("2.6.0").unwrap(); + static ref TASKWARRIOR_VERSION_SUPPORTED: Versioning = Versioning::new("3.0.0").unwrap(); } #[derive(Debug)] @@ -1304,12 +1304,11 @@ impl TaskwarriorTui { self.get_context()?; let task_uuids = self.selected_task_uuids(); if self.current_selection_uuid.is_none() && self.current_selection_id.is_none() && task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { + if let Some(uuid) = task_uuids.first() { self.current_selection_uuid = Some(*uuid); } } - self.last_export = Some(std::time::SystemTime::now()); self.task_report_table.export_headers(None, &self.report)?; self.export_tasks()?; if self.config.uda_task_report_use_all_tasks_for_completion { @@ -1321,6 +1320,10 @@ impl TaskwarriorTui { self.task_details.clear(); self.dirty = false; self.save_history()?; + + // Some operations like export or summary change the taskwarrior database. + // The export time therefore gets set at the end, to avoid an infinite update loop. + self.last_export = Some(std::time::SystemTime::now()); } self.cursor_fix(); self.update_task_table_state(); @@ -1608,20 +1611,21 @@ impl TaskwarriorTui { } } - fn get_task_files_max_mtime(&self) -> Result { - let data_dir = shellexpand::tilde(&self.config.data_location).into_owned(); - ["backlog.data", "completed.data", "pending.data"] - .iter() - .map(|n| fs::metadata(Path::new(&data_dir).join(n)).map(|m| m.modified())) - .filter_map(Result::ok) - .filter_map(Result::ok) - .max() - .ok_or_else(|| anyhow!("Unable to get task files max time")) + fn get_task_database_mtime(&self) -> Result { + let data_dir = shellexpand::tilde(&self.config.data_location); + let database_path = Path::new(data_dir.as_ref()).join("taskchampion.sqlite3"); + + let metadata = fs::metadata(database_path).context("Fetching the metadate of the task database failed")?; + let mtime = metadata + .modified() + .context("Could not get mtime of task database, but fetching metadata succeeded")?; + + Ok(mtime) } pub fn tasks_changed_since(&mut self, prev: Option) -> Result { if let Some(prev) = prev { - let mtime = self.get_task_files_max_mtime()?; + let mtime = self.get_task_database_mtime()?; if mtime > prev { Ok(true) } else { @@ -1794,7 +1798,7 @@ impl TaskwarriorTui { }; if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { + if let Some(uuid) = task_uuids.first() { self.current_selection_uuid = Some(*uuid); } } @@ -1904,7 +1908,7 @@ impl TaskwarriorTui { }; if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { + if let Some(uuid) = task_uuids.first() { self.current_selection_uuid = Some(*uuid); } } @@ -1957,7 +1961,7 @@ impl TaskwarriorTui { }; if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { + if let Some(uuid) = task_uuids.first() { self.current_selection_uuid = Some(*uuid); } } @@ -2009,7 +2013,7 @@ impl TaskwarriorTui { }; if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { + if let Some(uuid) = task_uuids.first() { self.current_selection_uuid = Some(*uuid); } } @@ -2096,7 +2100,7 @@ impl TaskwarriorTui { } if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { + if let Some(uuid) = task_uuids.first() { self.current_selection_uuid = Some(*uuid); } } @@ -2136,7 +2140,7 @@ impl TaskwarriorTui { } if task_uuids.len() == 1 { - if let Some(uuid) = task_uuids.get(0) { + if let Some(uuid) = task_uuids.first() { self.current_selection_uuid = Some(*uuid); } } @@ -3763,13 +3767,28 @@ pub fn remove_tag(task: &mut Task, tag: &str) { } #[cfg(test)] +// Disabled, as "'" should be a String for more readable shlex shell escaping. +#[allow(clippy::single_char_pattern)] mod tests { - use std::{ffi::OsStr, fmt::Write, fs::File, io, path::Path}; + use std::{ + ffi::OsStr, + fmt::Write, + fs::File, + io, + path::{Path, PathBuf}, + }; use ratatui::{backend::TestBackend, buffer::Buffer}; use super::*; + fn get_taskdata_path() -> PathBuf { + let taskdata_env_var = std::env::var("TASKDATA").expect("TASKDATA environment variable not set."); + let taskdata_path = Path::new(&taskdata_env_var).to_owned(); + + taskdata_path + } + /// Returns a string representation of the given buffer for debugging purpose. fn buffer_view(buffer: &Buffer) -> String { let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3); @@ -3801,7 +3820,7 @@ mod tests { fn setup() { use std::process::Stdio; - let mut f = File::open(Path::new(env!("TASKDATA")).parent().unwrap().join("export.json")).unwrap(); + let mut f = File::open(get_taskdata_path().parent().unwrap().join("export.json")).unwrap(); let mut s = String::new(); f.read_to_string(&mut s).unwrap(); let tasks = task_hookrs::import::import(s.as_bytes()).unwrap(); @@ -3812,7 +3831,7 @@ mod tests { } fn teardown() { - let cd = Path::new(env!("TASKDATA")); + let cd = get_taskdata_path(); std::fs::remove_dir_all(cd).unwrap(); } @@ -3892,24 +3911,16 @@ mod tests { // teardown(); } - #[test] - fn test_taskwarrior_tui() { - let r = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { _test_taskwarrior_tui().await }); - } - - async fn _test_taskwarrior_tui() { + #[tokio::test] + async fn test_taskwarrior_tui() { let app = TaskwarriorTui::new("next", false).await.unwrap(); assert!( app.task_by_index(0).is_none(), "Expected task data to be empty but found {} tasks. Delete contents of {:?} and {:?} and run the tests again.", app.tasks.len(), - Path::new(env!("TASKDATA")), - Path::new(env!("TASKDATA")).parent().unwrap().join(".config") + get_taskdata_path(), + get_taskdata_path().parent().unwrap().join(".config") ); let app = TaskwarriorTui::new("next", false).await.unwrap(); @@ -3958,7 +3969,7 @@ mod tests { let mut app = TaskwarriorTui::new("next", false).await.unwrap(); let task = app.task_by_id(11).unwrap(); - let tags = vec!["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] + let tags = ["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] .iter() .map(ToString::to_string) .collect::>(); @@ -3977,7 +3988,7 @@ mod tests { app.update(true).await.unwrap(); let task = app.task_by_id(11).unwrap(); - let tags = vec!["next", "finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] + let tags = ["next", "finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] .iter() .map(ToString::to_string) .collect::>(); @@ -3989,7 +4000,7 @@ mod tests { app.update(true).await.unwrap(); let task = app.task_by_id(11).unwrap(); - let tags = vec!["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] + let tags = ["finance", "UNBLOCKED", "PENDING", "TAGGED", "UDA"] .iter() .map(ToString::to_string) .collect::>(); diff --git a/src/calendar.rs b/src/calendar.rs index 04d50566..a3c6efb6 100644 --- a/src/calendar.rs +++ b/src/calendar.rs @@ -249,7 +249,7 @@ impl<'a> Widget for Calendar<'a> { impl<'a> Calendar<'a> { fn generate_month_names() -> [&'a str; 12] { - let month_names = [ + [ Month::January.name(), Month::February.name(), Month::March.name(), @@ -262,7 +262,6 @@ impl<'a> Calendar<'a> { Month::October.name(), Month::November.name(), Month::December.name(), - ]; - month_names + ] } }