diff --git a/Cargo.lock b/Cargo.lock index a39a9e06..56e36f95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,7 +133,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -143,7 +143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -152,6 +152,21 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "assert_cmd" +version = "2.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00ad3f3a942eee60335ab4342358c161ee296829e0d16ff42fc1d6cb07815467" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-condvar-fair" version = "1.0.0" @@ -202,6 +217,17 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "regex-automata 0.4.5", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -266,6 +292,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299353be8209bd133b049bf1c63582d184a8b39fd9c04f15fe65f50f88bdfe6c" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.4.7" @@ -310,14 +345,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] -name = "derive-getters" -version = "0.3.0" +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "directories" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2c35ab6e03642397cdda1dd58abbc05d418aef8e36297f336d5aba060fe8df" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", ] [[package]] @@ -326,12 +377,34 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd1e4a14ee49d368aa9378b0d570c639179489b6b1f89ea558bce6a15ca5b96a" +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "eyre" version = "0.6.12" @@ -354,6 +427,21 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fs-err" version = "2.11.0" @@ -469,6 +557,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "gimli" version = "0.28.1" @@ -557,6 +657,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "js-sys" version = "0.3.68" @@ -578,6 +687,17 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall", +] + [[package]] name = "libsqlite3-sys" version = "0.27.0" @@ -589,6 +709,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "lock_api" version = "0.4.11" @@ -629,6 +755,12 @@ dependencies = [ "adler", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -673,6 +805,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -690,13 +828,18 @@ name = "pace-rs" version = "0.4.0" dependencies = [ "abscissa_core", + "assert_cmd", "chrono", "clap", + "clap_complete", + "directories", "eyre", "once_cell", "pace_core", + "predicates", "serde", "serde_derive", + "tempfile", "thiserror", ] @@ -706,8 +849,10 @@ version = "0.3.0" dependencies = [ "async-condvar-fair", "chrono", - "derive-getters", + "directories", "futures", + "getset", + "itertools", "log", "rstest", "rusqlite", @@ -769,6 +914,60 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -826,6 +1025,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.3" @@ -936,6 +1146,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -1079,6 +1302,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1088,6 +1323,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.56" @@ -1422,6 +1663,15 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 2b1562d8..b1d7e5fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,8 @@ eula = false [dependencies] chrono = { version = "0.4.33", features = ["serde"] } clap = "4" +clap_complete = "4.5.0" +directories = "5.0.1" eyre = "0.6.12" pace_core = { workspace = true } serde = "1" @@ -46,7 +48,10 @@ version = "0.7.0" [dev-dependencies] abscissa_core = { version = "0.7.0", features = ["testing"] } +assert_cmd = "2.0.13" once_cell = "1.19" +predicates = "3.1.0" +tempfile = "3.10.0" # The profile that 'cargo dist' will build with [profile.dist] diff --git a/README.md b/README.md index e4e81e41..d5a177ef 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,17 @@ Currently they are stating the intended functionality and may not be fully implemented yet (e.g. using activities instead of tasks). -🪧 **`pace begin `** +🪧 **`pace begin`** - **Description:** Starts tracking time for the specified task. You can optionally specify a category or project to help organize your tasks. -- **Usage:** `pace begin "Design Work" --category "Freelance"` +- **Usage:** `pace begin "Design Work" --category "Freelance" --time 10:00` -🪧 **`pace end `** +🪧 **`pace end`** - **Description:** Stops time tracking for the specified task, marking it as completed or finished for the day. -- **Usage:** `pace end "Design Work"` +- **Usage:** `pace end --time 11:30 --only-last` 🪧 **`pace now`** @@ -46,19 +46,19 @@ implemented yet (e.g. using activities instead of tasks). what you're currently tracking. - **Usage:** `pace now` -❌ **`pace report --daily/--weekly/--monthly`** +⏲️ **`pace review`** -- **Description:** Generates a report for your tasks. You can specify the time - frame for daily, weekly, or monthly reports. -- **Usage:** `pace report --weekly --summary` +- **Description:** Gain insight in your activities and tasks. You can specify + the time frame for daily, weekly, or monthly insights. +- **Usage:** `pace review --weekly` -❌ **`pace resume `** +❌ **`pace resume`** - **Description:** Resumes time tracking for a previously paused task, allowing you to continue where you left off. - **Usage:** `pace resume "Design Work"` -❌ **`pace hold `** +❌ **`pace hold`** - **Description:** Pauses the time tracking for the specified task. This is useful for taking breaks without ending the task. @@ -84,7 +84,7 @@ implemented yet (e.g. using activities instead of tasks). all projects, subprojects and their associated tasks. - **Usage:** `pace projects` -❌ **`pace pomo `** +❌ **`pace pomo`** - **Description:** Starts a Pomodoro session for the specified task, integrating the Pomodoro technique directly with your tasks. @@ -92,14 +92,14 @@ implemented yet (e.g. using activities instead of tasks). ❌ **`pace export --json/--csv`** -- **Description:** Exports your tracked data and reports in JSON or CSV format, +- **Description:** Exports your tracked data and insights in JSON or CSV format, suitable for analysis or record-keeping. - **Usage:** `pace export --csv --from 2021-01-01 --to 2021-01-31` ❌ **`pace set`** - **Description:** Sets various application configurations, including Pomodoro - lengths and preferred report formats. + lengths and preferred review formats. - **Usage:** `pace set --work 25 --break 5` ## License diff --git a/config/pace.toml b/config/pace.toml index d418c740..25f59a79 100644 --- a/config/pace.toml +++ b/config/pace.toml @@ -12,11 +12,11 @@ category_separator = "::" # Default priority for new tasks default_priority = "Medium" -[reporting] -# Format of the reports generated by the application: "pdf", "html", "markdown", etc. -report_format = "html" -# Directory where reports will be stored -report_directory = "/path/to/your/reports/" +[reviews] +# Format of the review generated by the pace: "pdf", "html", "markdown", etc. +review_format = "html" +# Directory where the reviews will be stored +review_directory = "/path/to/your/reviews/" [export] export_include_tags = true diff --git a/config/project.toml b/config/projects.pace.toml similarity index 88% rename from config/project.toml rename to config/projects.pace.toml index c84755a1..5654e42d 100644 --- a/config/project.toml +++ b/config/projects.pace.toml @@ -11,6 +11,7 @@ id = "018d84a0-7847-7b2b-9cdb-6ba213f99a1d" name = "Pace Project" description = "An example project managed with Pace." root_tasks_file = "tasks.toml" # Path to the root tasks file +filters = ["*pace*"] # Optional: Define default filters for your project [defaults] # Optional: Define a default category for your project @@ -41,6 +42,10 @@ id = "018d84a0-a2d1-7450-98ef-8b47e0ff42b5" name = "Pace Subproject A" description = "" tasks_file = "subproject-a/tasks.toml" +# Optional: Define default filters for your project +filters = [ + "*pace*, *subproject-a", +] [[subprojects]] # Optional: Define subprojects or directories with their own tasks @@ -48,3 +53,7 @@ id = "018d84a0-cc74-71b4-8dd4-d7d26d1f5924" name = "Pace Subproject B" description = "" tasks_file = "subproject-b/tasks.toml" +# Optional: Define default filters for your project +filters = [ + "*pace*, *subproject-b", +] diff --git a/config/tasks.toml b/config/tasks.pace.toml similarity index 100% rename from config/tasks.toml rename to config/tasks.pace.toml diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index daf5b575..937a12e6 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -21,8 +21,10 @@ include = [ [dependencies] async-condvar-fair = "1.0.0" chrono = { version = "0.4.33", features = ["serde"] } -derive-getters = "0.3.0" +directories = "5.0.1" futures = "0.3.30" +getset = "0.1.2" +itertools = "0.12.1" log = "0.4.20" rusqlite = { version = "0.30.0", features = ["bundled", "chrono", "uuid"] } serde = "1.0.196" diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index deb2d034..da1f0290 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -5,9 +5,11 @@ use std::fs; use std::path::{Path, PathBuf}; use chrono::NaiveDateTime; -use derive_getters::Getters; +use getset::{CopyGetters, Getters, MutGetters, Setters}; use serde_derive::{Deserialize, Serialize}; +use directories::ProjectDirs; + use crate::{ domain::category::Category, error::{PaceErrorKind, PaceResult}, @@ -16,8 +18,9 @@ use crate::{ #[derive(Debug, Deserialize, Default, Serialize, Getters)] #[serde(deny_unknown_fields)] pub struct PaceConfig { + #[getset(get = "pub")] general: GeneralConfig, - reporting: ReportingConfig, + reviews: ReviewConfig, export: ExportConfig, database: Option, // Optional because it's only needed if log_storage is "database" pomodoro: PomodoroConfig, @@ -25,9 +28,10 @@ pub struct PaceConfig { auto_archival: AutoArchivalConfig, } -#[derive(Debug, Deserialize, Default, Serialize, Getters)] +#[derive(Debug, Deserialize, Default, Serialize, Getters, MutGetters)] pub struct GeneralConfig { log_storage: String, + #[getset(get = "pub", get_mut = "pub")] activity_log_file_path: String, log_format: String, autogenerate_ids: bool, @@ -36,9 +40,9 @@ pub struct GeneralConfig { } #[derive(Debug, Deserialize, Default, Serialize, Getters)] -pub struct ReportingConfig { - report_format: String, - report_directory: String, +pub struct ReviewConfig { + review_format: String, + review_directory: String, } #[derive(Debug, Deserialize, Default, Serialize, Getters)] @@ -123,17 +127,113 @@ pub fn find_config_file(starting_directory: impl AsRef, file_name: &str) - /// # Returns /// /// The path to the file if found -pub fn find_config_file_path_from_current_dir(file_name: &str) -> PaceResult { - let current_dir = env::current_dir()?; +pub fn find_root_config_file_path( + current_dir: impl AsRef, + file_name: &str, +) -> PaceResult { find_config_file(¤t_dir, file_name).ok_or( PaceErrorKind::ConfigFileNotFound { - current_dir: current_dir.clone().to_string_lossy().to_string(), + current_dir: current_dir.as_ref().to_string_lossy().to_string(), file_name: file_name.to_string(), } .into(), ) } +/// Get the paths to the config file +/// +/// # Arguments +/// +/// * `filename` - name of the config file +/// +/// # Returns +/// +/// A vector of [`PathBuf`]s to the config files +fn get_config_paths(filename: &str) -> Vec { + #[allow(unused_mut)] + let mut paths = vec![ + get_home_config_path(), + ProjectDirs::from("", "", "pace") + .map(|project_dirs| project_dirs.config_dir().to_path_buf()), + get_global_config_path(), + Some(PathBuf::from(".")), + ]; + + #[cfg(target_os = "windows")] + { + if let Some(win_compatibility_paths) = get_windows_portability_config_directories() { + paths.extend(win_compatibility_paths); + }; + } + + paths + .into_iter() + .filter_map(|path| path.map(|p| p.join(filename))) + .collect::>() +} + +/// Get the path to the home config directory. +/// +/// # Returns +/// +/// The path to the home config directory. +/// If the environment variable `PACE_HOME` is not set, `None` is returned. +fn get_home_config_path() -> Option { + std::env::var_os("PACE_HOME").map(|home_dir| PathBuf::from(home_dir).join(r"config")) +} + +/// Get the paths to the user profile config directories on Windows. +/// +/// # Returns +/// +/// A collection of possible paths to the user profile config directory on Windows. +/// +/// # Note +/// +/// If the environment variable `USERPROFILE` is not set, `None` is returned. +#[cfg(target_os = "windows")] +fn get_windows_portability_config_directories() -> Option>> { + std::env::var_os("USERPROFILE").map(|path| { + vec![ + Some(PathBuf::from(path.clone()).join(r".config\pace")), + Some(PathBuf::from(path).join(".pace")), + ] + }) +} + +/// Get the path to the global config directory on Windows. +/// +/// # Returns +/// +/// The path to the global config directory on Windows. +/// If the environment variable `PROGRAMDATA` is not set, `None` is returned. +#[cfg(target_os = "windows")] +fn get_global_config_path() -> Option { + std::env::var_os("PROGRAMDATA") + .map(|program_data| PathBuf::from(program_data).join(r"pace\config")) +} + +/// Get the path to the global config directory on ios and wasm targets. +/// +/// # Returns +/// +/// `None` is returned. +#[cfg(any(target_os = "ios", target_arch = "wasm32"))] +fn get_global_config_path() -> Option { + None +} + +/// Get the path to the global config directory on non-Windows, +/// non-iOS, non-wasm targets. +/// +/// # Returns +/// +/// "/etc/pace" is returned. +#[cfg(not(any(target_os = "windows", target_os = "ios", target_arch = "wasm32")))] +fn get_global_config_path() -> Option { + Some(PathBuf::from("/etc/pace")) +} + #[cfg(test)] mod tests { @@ -155,7 +255,7 @@ mod tests { #[rstest] fn test_parse_project_file_passes( - #[files("../../config/project.toml")] config_path: PathBuf, + #[files("../../config/projects.pace.toml")] config_path: PathBuf, ) -> TestResult<()> { let toml_string = fs::read_to_string(config_path)?; let _ = toml::from_str::(&toml_string)?; @@ -165,7 +265,7 @@ mod tests { #[rstest] fn test_parse_tasks_file_passes( - #[files("../../config/tasks.toml")] config_path: PathBuf, + #[files("../../config/tasks.pace.toml")] config_path: PathBuf, ) -> TestResult<()> { let toml_string = fs::read_to_string(config_path)?; let _ = toml::from_str::(&toml_string)?; diff --git a/crates/core/src/domain.rs b/crates/core/src/domain.rs index d1ab6b9a..6aa738de 100644 --- a/crates/core/src/domain.rs +++ b/crates/core/src/domain.rs @@ -2,10 +2,12 @@ pub mod activity; pub mod category; +pub mod filter; pub mod inbox; pub mod intermission; pub mod priority; pub mod project; +pub mod review; pub mod status; pub mod tag; pub mod task; diff --git a/crates/core/src/domain/activity.rs b/crates/core/src/domain/activity.rs index ca53173a..1f2e8e6b 100644 --- a/crates/core/src/domain/activity.rs +++ b/crates/core/src/domain/activity.rs @@ -1,12 +1,13 @@ //! Activity entity and business logic use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, SubsecRound, TimeZone}; -use derive_getters::Getters; +use getset::{CopyGetters, Getters, MutGetters, Setters}; use serde_derive::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashSet, VecDeque}, fmt::{format, Display}, fs, + iter::FromIterator, path::Path, }; use typed_builder::TypedBuilder; @@ -15,6 +16,7 @@ use uuid::Uuid; use crate::{ domain::{ category::Category, + filter::ActivityFilter, intermission::{self, IntermissionPeriod}, status::ItemStatus, tag::Tag, @@ -44,9 +46,10 @@ enum PomodoroCycle { Intermission, } -#[derive(Debug, Serialize, Deserialize, TypedBuilder, Getters, Clone)] +#[derive(Debug, Serialize, Deserialize, TypedBuilder, Getters, MutGetters, Clone)] pub struct Activity { #[builder(default = Some(ActivityId::default()), setter(strip_option))] + #[getset(get = "pub", get_mut = "pub")] id: Option, // TODO: We had it as a struct before with an ID, but it's questionable if we should go for this @@ -58,13 +61,17 @@ pub struct Activity { description: Option, #[builder(default, setter(strip_option))] + #[getset(get = "pub", get_mut = "pub")] end_date: Option, #[builder(default, setter(strip_option))] + #[getset(get = "pub", get_mut = "pub")] end_time: Option, + #[getset(get = "pub")] start_date: NaiveDate, + #[getset(get = "pub")] start_time: NaiveTime, kind: ActivityKind, @@ -148,24 +155,27 @@ impl Activity { } } } + + pub fn archived(&self) -> bool { + self.end_date.is_some() && self.end_time.is_some() + } } -#[derive(Debug, Serialize, Deserialize, Default, Getters)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, Getters, MutGetters)] pub struct ActivityLog { + #[getset(get = "pub", get_mut = "pub")] activities: VecDeque, } -impl ActivityLog { - pub fn load(activity_path: impl AsRef) -> PaceResult { - let toml_string = fs::read_to_string(activity_path)?; - Ok(toml::from_str::(&toml_string)?) - } - - pub fn add(&mut self, activity: Activity) -> PaceResult<()> { - self.activities.push_front(activity); - Ok(()) +impl FromIterator for ActivityLog { + fn from_iter>(iter: T) -> Self { + Self { + activities: iter.into_iter().collect::>(), + } } +} +impl ActivityLog { pub fn current_activities(&self) -> Option> { let current_activities = self .activities @@ -181,54 +191,6 @@ impl ActivityLog { Some(current_activities) } - pub fn end_all_unfinished_activities( - &mut self, - time: Option, - ) -> PaceResult>> { - // TODO: Make date formats configurable - let date = Local::now().date_naive(); - let time = time.unwrap_or_else(|| Local::now().time().round_subsecs(0)); - - let unfinished_activities = self - .activities - .iter_mut() - .filter(|activity| activity.end_date.is_none() || activity.end_time.is_none()) - .map(|activity| { - activity.end_date = Some(date); - activity.end_time = Some(time); - activity.clone() - }) - .collect::>(); - - if unfinished_activities.is_empty() { - return Ok(None); - } - - Ok(Some(unfinished_activities)) - } - - pub fn end_last_unfinished_activity( - &mut self, - time: Option, - ) -> PaceResult> { - let Some(last_activity) = self.activities.front_mut() else { - return Err(ActivityLogErrorKind::NoActivityToEnd.into()); - }; - - // TODO: Make date formats configurable - let date = Local::now().date_naive(); - let time = time.unwrap_or_else(|| Local::now().time().round_subsecs(0)); - - if last_activity.end_date.is_some() && last_activity.end_time.is_some() { - return Ok(None); - } - - last_activity.end_date = Some(date); - last_activity.end_time = Some(time); - - Ok(Some(last_activity.clone())) - } - // pub fn activities_by_id(&self) -> PaceResult> { // let activities_by_id = self // .activities diff --git a/crates/core/src/domain/filter.rs b/crates/core/src/domain/filter.rs new file mode 100644 index 00000000..f6a4971e --- /dev/null +++ b/crates/core/src/domain/filter.rs @@ -0,0 +1,29 @@ +use crate::domain::activity::{Activity, ActivityLog}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ActivityFilter { + #[default] + All, + Active, + Archived, + Ended, +} + +#[derive(Debug, Clone)] +pub enum FilteredActivities { + All(ActivityLog), + Active(ActivityLog), + Archived(ActivityLog), + Ended(ActivityLog), +} + +impl FilteredActivities { + pub fn into_log(self) -> ActivityLog { + match self { + FilteredActivities::All(activities) + | FilteredActivities::Active(activities) + | FilteredActivities::Archived(activities) + | FilteredActivities::Ended(activities) => activities, + } + } +} diff --git a/crates/core/src/domain/project.rs b/crates/core/src/domain/project.rs index 8b37310d..55e987e4 100644 --- a/crates/core/src/domain/project.rs +++ b/crates/core/src/domain/project.rs @@ -40,8 +40,10 @@ pub struct Project { root_tasks_file: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, TypedBuilder)] struct Subproject { + #[builder(default, setter(strip_option))] + id: Option, name: String, description: String, tasks_file: String, diff --git a/crates/core/src/domain/review.rs b/crates/core/src/domain/review.rs new file mode 100644 index 00000000..936183f5 --- /dev/null +++ b/crates/core/src/domain/review.rs @@ -0,0 +1 @@ +pub struct ActivityStats {} diff --git a/crates/core/src/domain/time.rs b/crates/core/src/domain/time.rs index fbf55e3f..444ffde1 100644 --- a/crates/core/src/domain/time.rs +++ b/crates/core/src/domain/time.rs @@ -1,9 +1,34 @@ -use chrono::{DateTime, Local}; +use chrono::{DateTime, Local, NaiveDate, NaiveTime, SubsecRound}; -/// Converts Timespec to nice readable relative time string -pub fn duration_to_str(init: DateTime) -> String { +use crate::error::PaceResult; + +pub enum TimeFrame { + Custom { + start: DateTime, + end: DateTime, + }, + Daily, + DaysInThePast(u32), + Monthly, + MonthsInThePast(u32), + Weekly, + WeeksInThePast(u32), + Yearly, + YearsInThePast(u32), +} + +/// Converts timespec to nice readable relative time string +/// +/// # Arguments +/// +/// * `initial_time` - The initial time to calculate the relative time from +/// +/// # Returns +/// +/// A string representing the relative time from the initial time +pub fn duration_to_str(initial_time: DateTime) -> String { let now = Local::now(); - let delta = now.signed_duration_since(init); + let delta = now.signed_duration_since(initial_time); let delta = ( delta.num_days(), @@ -13,7 +38,7 @@ pub fn duration_to_str(init: DateTime) -> String { ); match delta { - (days, ..) if days > 5 => format!("{}", init.format("%b %d, %Y")), + (days, ..) if days > 5 => format!("{}", initial_time.format("%b %d, %Y")), (days @ 2..=5, ..) => format!("{days} days ago"), (1, ..) => "one day ago".to_string(), @@ -27,3 +52,31 @@ pub fn duration_to_str(init: DateTime) -> String { _ => "just now".to_string(), } } + +/// Extracts time from the given string or returns the current time +/// +/// # Arguments +/// +/// * `time` - The time to extract or None +/// +/// # Errors +/// +/// [`chrono::ParseError`] - If the time cannot be parsed +/// +/// # Returns +/// +/// A tuple containing the time and date +pub fn extract_time_or_now(time: &Option) -> PaceResult<(NaiveTime, NaiveDate)> { + Ok(if let Some(ref time) = time { + ( + NaiveTime::parse_from_str(time, "%H:%M")?, + Local::now().date_naive(), + ) + } else { + // if no time is given, use the current time + ( + Local::now().time().round_subsecs(0), + Local::now().date_naive(), + ) + }) +} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index a3202e3e..25204f04 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -7,7 +7,9 @@ use std::{ use strum_macros::Display; use thiserror::Error; -#[cfg(test)] +use crate::domain::activity::ActivityId; + +/// Result type that is being returned from test functions and methods that can fail and thus have errors. pub type TestResult = Result>; /// Result type that is being returned from methods that can fail and thus have [`PaceError`]s. @@ -56,6 +58,8 @@ pub enum PaceErrorKind { ActivityLog(#[from] ActivityLogErrorKind), #[error(transparent)] SQLite(#[from] rusqlite::Error), + #[error(transparent)] + ChronoParse(#[from] chrono::ParseError), /// Config file {file_name} not found in directory hierarchy starting from {current_dir} ConfigFileNotFound { current_dir: String, @@ -70,6 +74,8 @@ pub enum PaceErrorKind { #[derive(Error, Debug, Display)] pub enum ActivityLogErrorKind { NoActivityToEnd, + NoActivitiesFound, + FailedToReadActivity(ActivityId), } trait PaceErrorMarker: Error {} @@ -78,6 +84,7 @@ impl PaceErrorMarker for std::io::Error {} impl PaceErrorMarker for toml::de::Error {} impl PaceErrorMarker for toml::ser::Error {} impl PaceErrorMarker for rusqlite::Error {} +impl PaceErrorMarker for chrono::ParseError {} impl PaceErrorMarker for ActivityLogErrorKind {} impl From for PaceError diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index e760b23a..0124857c 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -15,7 +15,6 @@ pub mod error; pub mod service; pub mod storage; pub mod util; -// pub mod event; use std::collections::{BinaryHeap, HashMap, VecDeque}; use std::time::{SystemTime, UNIX_EPOCH}; diff --git a/crates/core/src/service/activity_store.rs b/crates/core/src/service/activity_store.rs index 97065bda..1a257265 100644 --- a/crates/core/src/service/activity_store.rs +++ b/crates/core/src/service/activity_store.rs @@ -5,9 +5,12 @@ use serde_derive::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - domain::activity::{Activity, ActivityId, ActivityLog}, + domain::{ + activity::{Activity, ActivityId, ActivityLog}, + filter::FilteredActivities, + }, error::PaceResult, - storage::ActivityStorage, + storage::{ActivityReadOps, ActivityStateManagement, ActivityStorage, ActivityWriteOps}, }; pub struct ActivityStore { @@ -21,21 +24,74 @@ pub struct ActivityStoreCache { last_entries: VecDeque, } +impl ActivityStore { + pub fn new(storage: Box) -> Self { + ActivityStore { + cache: ActivityStoreCache::default(), + storage, + } + } +} + impl ActivityStorage for ActivityStore { fn setup_storage(&self) -> PaceResult<()> { self.storage.setup_storage() } +} - fn load_all_activities(&self) -> PaceResult { - self.storage.load_all_activities() +impl ActivityReadOps for ActivityStore { + fn read_activity(&self, activity_id: ActivityId) -> PaceResult> { + self.storage.read_activity(activity_id) } - fn list_current_activities(&self) -> PaceResult>> { - self.storage.list_current_activities() + fn list_activities( + &self, + filter: crate::domain::filter::ActivityFilter, + ) -> PaceResult> { + self.storage.list_activities(filter) } + // TODO: Caching? + // fn read_activity(&self, activity_id: ActivityId) -> PaceResult> { + // if let Some(activity) = self.cache.activities_by_id.get(&activity_id) { + // return Ok(Some(activity.clone())); + // } - fn save_activity(&self, activity: &Activity) -> PaceResult<()> { - self.storage.save_activity(activity) + // let activity = self.storage.read_activity(activity_id)?; + + // if let Some(activity) = activity.clone() { + // self.cache.activities_by_id.insert(activity_id, activity.clone()); + // self.cache.last_entries.push_back(activity_id); + // } + + // Ok(activity) + // } +} + +impl ActivityWriteOps for ActivityStore { + fn create_activity(&self, activity: &Activity) -> PaceResult { + self.storage.create_activity(activity) + } + + fn update_activity(&self, activity_id: ActivityId, activity: &Activity) -> PaceResult<()> { + self.storage.update_activity(activity_id, activity) + } + + fn delete_activity(&self, activity_id: ActivityId) -> PaceResult<()> { + self.storage.delete_activity(activity_id) + } +} + +impl ActivityStateManagement for ActivityStore { + fn start_activity(&self, activity: &Activity) -> PaceResult { + self.storage.start_activity(activity) + } + + fn end_activity( + &self, + activity_id: ActivityId, + end_time: Option, + ) -> PaceResult { + self.storage.end_activity(activity_id, end_time) } fn end_all_unfinished_activities( @@ -51,29 +107,4 @@ impl ActivityStorage for ActivityStore { ) -> PaceResult> { self.storage.end_last_unfinished_activity(time) } - - fn get_activities_by_id( - &self, - uuid: Uuid, - ) -> PaceResult>> { - self.storage.get_activities_by_id(uuid) - } -} - -impl ActivityStore { - pub fn new(storage: Box) -> Self { - ActivityStore { - cache: ActivityStoreCache::default(), - storage, - } - } - // pub fn get_activities_by_id(&self, id: &ActivityId) -> PaceResult { - // self.load_activities().and_then(|activity_log|) - // self.activities_by_id.get(&id) - // } - - // pub fn init(&mut self) -> PaceResult<()> { - // self.load_activities().and_then(|activity_log|) - // Ok(()) - // } } diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index 593dc6d0..afa974b8 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -1,38 +1,289 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, VecDeque}; -use chrono::NaiveTime; +use chrono::{NaiveDate, NaiveTime}; use serde::{Deserialize, Serialize}; use crate::{ - domain::activity::{Activity, ActivityId, ActivityLog}, + domain::{ + activity::{Activity, ActivityId, ActivityLog}, + filter::{ActivityFilter, FilteredActivities}, + review::ActivityStats, + time::TimeFrame, + }, error::{PaceErrorKind, PaceResult}, }; pub mod file; -// TODO: Implement in-memory Storage -// pub mod in_memory; +pub mod in_memory; // TODO: Implement conversion FromSQL and ToSQL // pub mod sqlite; -pub trait ActivityStorage { +/// The trait that all storage backends must implement. This allows us to swap out the storage +/// backend without changing the rest of the application. +pub trait ActivityStorage: ActivityReadOps + ActivityWriteOps + ActivityStateManagement +// TODO!: Implement other traits +// + ActivityTagging +// + ActivityArchiving +// + ActivityStatistics +{ + // This main trait combines all aspects of activity storage. + // You can add methods here that require access to multiple areas of functionality, + // or simply use it as a marker trait for objects that implement all aspects of activity storage. + + /// Setup the storage backend. This is called once when the application starts. + /// + /// This is where you would create the database tables, open the file, etc. + /// + /// # Errors + /// + /// This function should return an error if the storage backend cannot be setup. fn setup_storage(&self) -> PaceResult<()>; +} + +/// Basic Read Operations for Activities in the storage backend. +pub trait ActivityReadOps { + /// Read an activity from the storage backend. + /// + /// # Arguments + /// + /// * `activity_id` - The ID of the activity to read. + /// + /// # Errors + /// + /// This function should return an error if the activity cannot be read. + /// + /// # Returns + /// + /// The activity that was read from the storage backend. If no activity is found, it should return `Ok(None)`. + fn read_activity(&self, activity_id: ActivityId) -> PaceResult>; + + /// List activities from the storage backend. + /// + /// # Arguments + /// + /// * `filter` - The filter to apply to the activities. + /// + /// # Errors + /// + /// This function should return an error if the activities cannot be loaded. + /// + /// # Returns + /// + /// A collection of the activities that were loaded from the storage backend. + fn list_activities(&self, filter: ActivityFilter) -> PaceResult>; +} - fn load_all_activities(&self) -> PaceResult; +/// Basic CRUD Operations for Activities in the storage backend. +pub trait ActivityWriteOps: ActivityReadOps { + /// Create an activity in the storage backend. + /// + /// # Arguments + /// + /// * `activity` - The activity to create. + /// + /// # Errors + /// + /// This function should return an error if the activity cannot be created. + /// + /// # Returns + /// + /// If the activity was created successfully it should return the ID of the created activity. + fn create_activity(&self, activity: &Activity) -> PaceResult; - fn list_current_activities(&self) -> PaceResult>>; + /// Update an existing activity in the storage backend. + /// + /// # Arguments + /// + /// * `activity_id` - The ID of the activity to update. + /// * `activity` - The updated activity data. + /// + /// # Errors + /// + /// This function should return an error if the activity cannot be updated. + fn update_activity(&self, activity_id: ActivityId, activity: &Activity) -> PaceResult<()>; - fn save_activity(&self, activity: &Activity) -> PaceResult<()>; + /// Delete an activity from the storage backend. + /// + /// # Arguments + /// + /// * `activity_id` - The ID of the activity to delete. + /// + /// # Errors + /// + /// This function should return an error if the activity cannot be deleted. + fn delete_activity(&self, activity_id: ActivityId) -> PaceResult<()>; +} + +/// Managing Activity State +pub trait ActivityStateManagement: ActivityReadOps + ActivityWriteOps { + /// Start an activity in the storage backend. + /// + /// # Arguments + /// + /// * `activity` - The activity to start. + /// + /// # Errors + /// + /// This function should return an error if the activity cannot be started. + /// + /// # Returns + /// + /// If the activity was started successfully it should return the ID of the started activity. + fn start_activity(&self, activity: &Activity) -> PaceResult; + /// End an activity in the storage backend. + /// + /// # Arguments + /// + /// * `activity_id` - The ID of the activity to end. + /// * `end_time` - The time (HH:MM) to end the activity at. If `None`, the current time is used. + /// + /// # Errors + /// + /// This function should return an error if the activity cannot be ended. + /// + /// # Returns + /// + /// If the activity was ended successfully it should return the ID of the ended activity. + fn end_activity( + &self, + activity_id: ActivityId, + end_time: Option, + ) -> PaceResult; + + /// End all unfinished activities in the storage backend. + /// + /// # Arguments + /// + /// * `time` - The time (HH:MM) to end the activities at. If `None`, the current time is used. + /// + /// # Errors + /// + /// This function should return an error if the activities cannot be ended. + /// + /// # Returns + /// + /// A collection of the activities that were ended. fn end_all_unfinished_activities( &self, time: Option, ) -> PaceResult>>; + /// End the last unfinished activity in the storage backend. + /// + /// # Arguments + /// + /// * `time` - The time (HH:MM) to end the activity at. If `None`, the current time is used. + /// + /// # Errors + /// + /// This function should return an error if the activity cannot be ended. + /// + /// # Returns + /// + /// The activity that was ended. fn end_last_unfinished_activity(&self, time: Option) -> PaceResult>; +} - fn get_activities_by_id( +/// Querying Activities +pub trait ActivityQuerying: ActivityReadOps { + /// List all currently active activities from the storage backend. + /// + /// # Errors + /// + /// This function should return an error if the activities cannot be loaded. + /// In case of no activities, it should return `Ok(None)`. + /// + /// # Returns + /// + /// A collection of the activities that are currently active. + // TODO: should just use `list_activities` with a filter for `active = true` + // TODO: Implement this as default + fn list_current_activities(&self) -> PaceResult>; + + /// Find activities within a specific date range. + /// + /// # Arguments + /// + /// * `start_date` - The start date of the range. + /// * `end_date` - The end date of the range. + /// + /// # Errors + /// + /// This function should return an error if the activities cannot be loaded. + /// + /// # Returns + /// + /// A collection of the activities that fall within the specified date range. + // TODO: should just use `list_activities` with a filter for `start_date <= date <= end_date` + // TODO: Implement this as default + fn find_activities_in_date_range( &self, - uuid: uuid::Uuid, - ) -> PaceResult>>; + start_date: NaiveDate, + end_date: NaiveDate, + ) -> PaceResult; + + /// Get all activities by their ID. + /// + /// # Errors + /// + /// This function should return an error if the activities cannot be loaded. + /// + /// # Returns + /// + /// A collection of the activities that were loaded from the storage backend by their ID in a BTreeMap. + /// If no activities are found, it should return `Ok(None)`. + fn list_activities_by_id(&self) -> PaceResult>>; +} + +pub trait ActivityTagging { + /// Add a tag to an activity. + /// + /// # Arguments + /// + /// * `activity_id` - The ID of the activity to tag. + /// * `tag` - The tag to add. + fn add_tag_to_activity(&self, activity_id: ActivityId, tag: &str) -> PaceResult<()>; + + /// Remove a tag from an activity. + /// + /// # Arguments + /// + /// * `activity_id` - The ID of the activity to untag. + /// * `tag` - The tag to remove. + fn remove_tag_from_activity(&self, activity_id: ActivityId, tag: &str) -> PaceResult<()>; +} + +pub trait ActivityArchiving { + /// Archive an activity. + /// + /// # Arguments + /// + /// * `activity_id` - The ID of the activity to archive. + fn archive_activity(&self, activity_id: ActivityId) -> PaceResult<()>; + + /// Unarchive an activity. + /// + /// # Arguments + /// + /// * `activity_id` - The ID of the activity to unarchive. + fn unarchive_activity(&self, activity_id: ActivityId) -> PaceResult<()>; +} + +pub trait ActivityStatistics { + /// Generate statistics or summary of activities. + /// + /// # Arguments + /// + /// * `time_frame` - The time frame to generate statistics for (e.g., daily, weekly, monthly). + /// + /// # Errors + /// + /// This function should return an error if the statistics cannot be generated. + /// + /// # Returns + /// + /// A summary or statistics of activities within the specified time frame. + fn generate_activity_statistics(&self, time_frame: TimeFrame) -> PaceResult; } diff --git a/crates/core/src/storage/file.rs b/crates/core/src/storage/file.rs index 2e3f037a..b33ecf14 100644 --- a/crates/core/src/storage/file.rs +++ b/crates/core/src/storage/file.rs @@ -1,6 +1,6 @@ -use chrono::{Local, NaiveTime}; +use chrono::{Local, NaiveTime, SubsecRound}; use std::{ - collections::BTreeMap, + collections::{BTreeMap, VecDeque}, fs::{self, File}, }; use std::{fs::OpenOptions, path::PathBuf}; @@ -11,10 +11,15 @@ use std::{ use toml; use uuid::Uuid; +use itertools::Itertools; + use crate::{ - domain::activity::{Activity, ActivityId, ActivityLog}, - error::{PaceErrorKind, PaceResult}, - storage::ActivityStorage, + domain::{ + activity::{self, Activity, ActivityId, ActivityLog}, + filter::{ActivityFilter, FilteredActivities}, + }, + error::{ActivityLogErrorKind, PaceErrorKind, PaceResult}, + storage::{ActivityReadOps, ActivityStateManagement, ActivityStorage, ActivityWriteOps}, }; pub struct TomlActivityStorage { @@ -37,62 +42,202 @@ impl ActivityStorage for TomlActivityStorage { .parent() .ok_or(PaceErrorKind::ParentDirNotFound(self.path.clone()))?, )?; + let mut file = OpenOptions::new() .write(true) .create(true) .open(&self.path)?; + file.write_all(b"")?; } Ok(()) } - fn load_all_activities(&self) -> PaceResult { - let contents = fs::read_to_string(&self.path)?; - let activities: ActivityLog = toml::from_str(&contents)?; - Ok(activities) - } +} - fn list_current_activities(&self) -> PaceResult>> { - let activities = self.load_all_activities()?; - Ok(activities.current_activities()) +impl ActivityReadOps for TomlActivityStorage { + fn read_activity(&self, activity_id: ActivityId) -> PaceResult> { + self.list_activities(ActivityFilter::default())? + .map(|filtered| { + filtered + .into_log() + .activities() + .iter() + .find(|activity| { + if let Some(id) = activity.id() { + *id == activity_id + } else { + false + } + }) + .cloned() + }) + .ok_or(ActivityLogErrorKind::FailedToReadActivity(activity_id).into()) } - fn save_activity(&self, activity: &Activity) -> PaceResult<()> { - let mut activities = self.load_all_activities()?; - activities.add(activity.clone())?; + fn list_activities(&self, filter: ActivityFilter) -> PaceResult> { + let contents = fs::read_to_string(&self.path)?; + let activity_log: ActivityLog = toml::from_str(&contents)?; + + let filtered = activity_log + .activities() + .iter() + .filter(|activity| match filter { + ActivityFilter::Active => { + activity.end_date().is_none() || activity.end_time().is_none() + } + ActivityFilter::Ended => { + activity.end_date().is_some() && activity.end_time().is_some() + } + ActivityFilter::All => true, + ActivityFilter::Archived => false, // TODO: Implement archived filter + }) + .cloned() + .collect::(); - let toml = toml::to_string_pretty(&activities)?; + if filtered.activities().is_empty() { + return Ok(None); + } - // Write the new contents back to the file - fs::write(&self.path, toml)?; - Ok(()) + match filter { + ActivityFilter::Active => Ok(Some(FilteredActivities::Active(filtered))), + ActivityFilter::Ended => Ok(Some(FilteredActivities::Ended(filtered))), + ActivityFilter::All => Ok(Some(FilteredActivities::All(filtered))), + ActivityFilter::Archived => Ok(Some(FilteredActivities::Archived(filtered))), + } } +} +impl ActivityStateManagement for TomlActivityStorage { fn end_all_unfinished_activities( &self, time: Option, ) -> PaceResult>> { - let mut activities = self.load_all_activities()?; - let unfinished = activities.end_all_unfinished_activities(time)?; - let toml = toml::to_string_pretty(&activities)?; - fs::write(&self.path, toml)?; - Ok(unfinished) + // TODO: Make date formats configurable + let date = Local::now().date_naive(); + let time = time.unwrap_or_else(|| Local::now().time().round_subsecs(0)); + + let mut unfinished_activities: Vec = vec![]; + + let activities = self.list_activities(ActivityFilter::All)?.map(|filtered| { + filtered + .into_log() + .activities_mut() + .iter_mut() + .map(|activity| { + if activity.end_date().is_none() && activity.end_time().is_none() { + activity.end_date_mut().replace(date); + activity.end_time_mut().replace(time); + unfinished_activities.push(activity.clone()); + } + + activity.clone() + }) + .collect::() + }); + + // Return early with Ok(None) if there are no activities to end + if unfinished_activities.is_empty() { + Ok(None) + } else { + // Sort the activities by start date + unfinished_activities.sort_by(|a, b| a.start_date().cmp(b.start_date())); + + // Write the updated (all activities) content back to the file + let toml = toml::to_string_pretty(&activities)?; + fs::write(&self.path, toml)?; + + // Return the activities that were ended + Ok(Some(unfinished_activities)) + } } fn end_last_unfinished_activity( &self, time: Option, ) -> PaceResult> { - let mut activities = self.load_all_activities()?; - let unfinished = activities.end_last_unfinished_activity(time)?; - let toml = toml::to_string_pretty(&activities)?; + let mut activity_log = self + .list_activities(ActivityFilter::Active)? + .ok_or(ActivityLogErrorKind::NoActivityToEnd)? + .into_log(); + + let activity: Activity; + + // Return early with Ok(None) if there are no activities to end + if activity_log.activities().is_empty() { + return Ok(None); + } + + // Scope for mutable borrow of last_activity + { + let Some(last_activity) = activity_log.activities_mut().front_mut() else { + return Err(ActivityLogErrorKind::NoActivityToEnd.into()); + }; + + // TODO: Make date formats configurable + let date = Local::now().date_naive(); + let time = time.unwrap_or_else(|| Local::now().time().round_subsecs(0)); + + // If the last activity already has an end date and time, return early with Ok(None) + if last_activity.end_date().is_some() && last_activity.end_time().is_some() { + return Ok(None); + } + + last_activity.end_date_mut().replace(date); + last_activity.end_time_mut().replace(time); + + // Clone the last activity to return it after the mutable borrow ends + activity = last_activity.clone(); + } + + let toml = toml::to_string_pretty(&activity_log.clone())?; fs::write(&self.path, toml)?; - Ok(unfinished) + + Ok(Some(activity)) + } + + fn start_activity(&self, _activity: &Activity) -> PaceResult { + todo!() } - fn get_activities_by_id( + fn end_activity( &self, - _uuid: Uuid, - ) -> PaceResult>> { + _activity_id: ActivityId, + _end_time: Option, + ) -> PaceResult { + todo!() + } +} + +impl ActivityWriteOps for TomlActivityStorage { + fn create_activity(&self, activity: &Activity) -> PaceResult { + let mut activity = activity.clone(); + + let mut activity_log = self + .list_activities(ActivityFilter::default())? + .ok_or(ActivityLogErrorKind::NoActivitiesFound)? + .into_log(); + + // Generate an ID for the activity if it doesn't have one + _ = activity.id_mut().get_or_insert_with(ActivityId::default); + + let activity_id = activity.id().clone().unwrap(); + + activity_log.activities_mut().push_front(activity); + + let toml = toml::to_string_pretty(&activity_log)?; + + // Write the new contents back to the file + fs::write(&self.path, toml)?; + + // Return the ID of the newly created activity + Ok(activity_id) + } + + fn update_activity(&self, _activity_id: ActivityId, _activity: &Activity) -> PaceResult<()> { + todo!() + } + + fn delete_activity(&self, _activity_id: ActivityId) -> PaceResult<()> { todo!() } } diff --git a/crates/core/src/storage/in_memory.rs b/crates/core/src/storage/in_memory.rs index 2cc90fb5..c89cf42e 100644 --- a/crates/core/src/storage/in_memory.rs +++ b/crates/core/src/storage/in_memory.rs @@ -19,23 +19,3 @@ impl InMemoryActivityStorage { } } } - -impl ActivityStorage for InMemoryActivityStorage { - fn load_activities(&self) -> PaceResult { - // Cloning the vector to simulate loading from a persistent store - Ok(self - .activities - .into_inner() - .expect("Getting inner from unpoisened Mutex should succeed.")) - } - - fn save_activity(&self, activity: &Activity) -> PaceResult<()> { - // Simply push the new activity onto the vector - let mut guard = self - .activities - .lock() - .expect("Mutex should not be poisened."); - guard.add(activity.clone()); - Ok(()) - } -} diff --git a/crates/core/tests/.gitkeep b/crates/core/tests/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/core/tests/find_configs.rs b/crates/core/tests/find_configs.rs new file mode 100644 index 00000000..e3db14c2 --- /dev/null +++ b/crates/core/tests/find_configs.rs @@ -0,0 +1,28 @@ +use pace_core::{config::find_root_config_file_path, error::TestResult}; + +use std::env; + +use rstest::rstest; + +#[rstest] +fn test_find_root_projects_file() -> TestResult<()> { + let current_dir = env::current_dir()?; + let projects_config_name = "projects.pace.toml"; + + // navigate to the test directory for the fixtures + let root = current_dir + .join("tests/fixtures/project1/subproject-a/") + .canonicalize()?; + + // get the path to the projects config file + let projects_config_path = find_root_config_file_path(root, projects_config_name)?; + + assert_eq!( + projects_config_path, + current_dir + .join("tests/fixtures/project1/projects.pace.toml") + .canonicalize()? + ); + + Ok(()) +} diff --git a/crates/core/tests/fixtures/project1/projects.pace.toml b/crates/core/tests/fixtures/project1/projects.pace.toml new file mode 100644 index 00000000..f1d09ed7 --- /dev/null +++ b/crates/core/tests/fixtures/project1/projects.pace.toml @@ -0,0 +1,21 @@ +[project] +id = "018d84a2-d8fa-7e51-8eab-4bd917dda8f8" +name = "Pace Project" +description = "An example project managed with Pace." +root_tasks_file = "tasks.toml" # Path to the root tasks file + +[[subprojects]] +# Optional: Define subprojects or directories with their own tasks +id = "018d84a3-08af-75a4-bb03-544f70b10b5e" +name = "Pace Subproject A" +description = "" +tasks_file = "subproject-a/tasks.toml" + +[[subprojects]] +# Optional: Define subprojects or directories with their own tasks +id = "018d84a3-18cd-7c87-ab58-5c6586e148e4" +name = "Pace Subproject B" +description = "" +tasks_file = "subproject-b/tasks.toml" + +# subproject-c is missing here, it should be in here, when we run `pace` in the corresponding subdir diff --git a/crates/core/tests/fixtures/project1/subproject-a/tasks.toml b/crates/core/tests/fixtures/project1/subproject-a/tasks.toml new file mode 100644 index 00000000..38545913 --- /dev/null +++ b/crates/core/tests/fixtures/project1/subproject-a/tasks.toml @@ -0,0 +1,18 @@ +[[tasks]] +id = "018d84a1-d7b2-7383-a6ab-8cfee92b5308" +title = "Implement feature X" +created_at = "2024-02-04T12:34:56" +finished_at = "2024-02-05T13:34:56" +description = "Detailed description of feature X to be implemented." +priority = "High" +status = "Pending" +tags = ["feature", "X"] + +[[tasks]] +id = "018d84a1-ea37-7716-8ec0-845a1a396e20" +title = "Fix bug Y" +created_at = "2024-02-06T12:34:56" +description = "Detailed description of bug Y to be fixed." +priority = "Medium" +status = "InProgress" +tags = ["bug", "Y"] diff --git a/crates/core/tests/fixtures/project1/subproject-b/README.md b/crates/core/tests/fixtures/project1/subproject-b/README.md new file mode 100644 index 00000000..b13ab700 --- /dev/null +++ b/crates/core/tests/fixtures/project1/subproject-b/README.md @@ -0,0 +1,6 @@ +# Description + +subproject-b is a folder with an existing `tasks.toml`, which has been already +set up correctly in the upper `project.toml`. We want to make sure now, that it +is being used correctly for tasks management, when navigating in this folder and +lower folders. diff --git a/crates/core/tests/fixtures/project1/subproject-b/tasks.toml b/crates/core/tests/fixtures/project1/subproject-b/tasks.toml new file mode 100644 index 00000000..21e07513 --- /dev/null +++ b/crates/core/tests/fixtures/project1/subproject-b/tasks.toml @@ -0,0 +1,18 @@ +[[tasks]] +id = "018d84a2-5868-7647-9ebd-7319c1d5db69" +title = "Implement feature X" +created_at = "2024-02-04T12:34:56" +finished_at = "2024-02-05T13:34:56" +description = "Detailed description of feature X to be implemented." +priority = "High" +status = "Pending" +tags = ["feature", "X"] + +[[tasks]] +id = "018d84a2-663d-754f-a2dc-eb2a1a684918" +title = "Fix bug Y" +created_at = "2024-02-06T12:34:56" +description = "Detailed description of bug Y to be fixed." +priority = "Medium" +status = "InProgress" +tags = ["bug", "Y"] diff --git a/crates/core/tests/fixtures/project1/subproject-b/test1/.gitkeep b/crates/core/tests/fixtures/project1/subproject-b/test1/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/core/tests/fixtures/project1/subproject-b/test2/.gitkeep b/crates/core/tests/fixtures/project1/subproject-b/test2/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/crates/core/tests/fixtures/project1/subproject-c/README.md b/crates/core/tests/fixtures/project1/subproject-c/README.md new file mode 100644 index 00000000..02ed4027 --- /dev/null +++ b/crates/core/tests/fixtures/project1/subproject-c/README.md @@ -0,0 +1,5 @@ +# Description + +subproject-c is a folder, where a new `tasks.toml` has been initialized manually +and we want to make sure now, it is being set up correctly in the upper +`project.toml` diff --git a/crates/core/tests/fixtures/project1/subproject-c/tasks.toml b/crates/core/tests/fixtures/project1/subproject-c/tasks.toml new file mode 100644 index 00000000..1fe20d20 --- /dev/null +++ b/crates/core/tests/fixtures/project1/subproject-c/tasks.toml @@ -0,0 +1,18 @@ +[[tasks]] +id = "018d84a2-9648-74d8-9543-60af7f1a8bc9" +title = "Implement feature X" +created_at = "2024-02-04T12:34:56" +finished_at = "2024-02-05T13:34:56" +description = "Detailed description of feature X to be implemented." +priority = "High" +status = "Pending" +tags = ["feature", "X"] + +[[tasks]] +id = "018d84a2-b1b2-768c-9aa4-b78c7cacfea4" +title = "Fix bug Y" +created_at = "2024-02-06T12:34:56" +description = "Detailed description of bug Y to be fixed." +priority = "Medium" +status = "InProgress" +tags = ["bug", "Y"] diff --git a/crates/core/tests/fixtures/project1/subproject-d/README.md b/crates/core/tests/fixtures/project1/subproject-d/README.md new file mode 100644 index 00000000..42b702dd --- /dev/null +++ b/crates/core/tests/fixtures/project1/subproject-d/README.md @@ -0,0 +1,4 @@ +# Description + +subproject-d is an empty folder, where we want to initialize a new `tasks.toml` +and make sure, it is being set up correctly in the upper `project.toml` diff --git a/src/commands.rs b/src/commands.rs index e7149bca..a4b5c245 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -14,11 +14,12 @@ mod begin; mod end; mod export; // TODO: mod import; +mod completions; mod hold; mod now; mod pomo; -mod report; mod resume; +mod review; mod set; mod tasks; @@ -41,10 +42,13 @@ pub enum PaceCmd { /// your tasks. Begin(begin::BeginCmd), + /// Generate shell completions for the specified shell + Completions(completions::CompletionsCmd), + /// Stops time tracking for the specified task, marking it as completed or finished for the day. End(end::EndCmd), - /// Exports your tracked data and reports in JSON or CSV format, suitable for analysis or record-keeping. + /// Exports your tracked data and reviews in JSON or CSV format, suitable for analysis or record-keeping. Export(export::ExportCmd), /// Pauses the time tracking for the specified task. This is @@ -57,13 +61,13 @@ pub enum PaceCmd { /// Starts a Pomodoro session for the specified task, integrating the Pomodoro technique directly with your tasks. Pomo(pomo::PomoCmd), - /// Generates a report for your tasks. You can specify the time frame for daily, weekly, or monthly reports. - Report(report::ReportCmd), + /// Get insights on your activities and tasks. You can specify the time frame for daily, weekly, or monthly insights. + Review(review::ReviewCmd), /// Resumes time tracking for a previously paused task, allowing you to continue where you left off. Resume(resume::ResumeCmd), - /// Sets various application configurations, including Pomodoro lengths and preferred report formats. + /// Sets various application configurations, including Pomodoro lengths and preferred review formats. Set(set::SetCmd), /// Lists all tasks with optional filters. Use this to view active, completed, or today's tasks. @@ -93,6 +97,10 @@ pub struct EntryPoint { /// Use the specified config file #[arg(short, long)] pub config: Option, + + /// Use the specified activity log file + #[arg(short, long)] + pub activity_log_file: Option, } impl Runnable for EntryPoint { diff --git a/src/commands/begin.rs b/src/commands/begin.rs index f41c2001..9f5adc86 100644 --- a/src/commands/begin.rs +++ b/src/commands/begin.rs @@ -1,16 +1,18 @@ //! `begin` subcommand use abscissa_core::{status_err, Application, Command, Runnable, Shutdown}; -use chrono::{Local, NaiveTime, SubsecRound}; use clap::Parser; use eyre::Result; use crate::prelude::PACE_APP; use pace_core::{ - domain::activity::{Activity, ActivityKind}, + domain::{ + activity::{Activity, ActivityKind}, + time::extract_time_or_now, + }, service::activity_store::ActivityStore, - storage::{file::TomlActivityStorage, ActivityStorage}, + storage::{file::TomlActivityStorage, ActivityStorage, ActivityWriteOps}, }; /// `begin` subcommand @@ -59,19 +61,8 @@ impl BeginCmd { .. } = self; - // parse time from string or set now - let (time, date) = if let Some(ref time) = time { - ( - NaiveTime::parse_from_str(time, "%H:%M")?, - Local::now().date_naive(), - ) - } else { - // if no time is given, use the current time - ( - Local::now().time().round_subsecs(0), - Local::now().date_naive(), - ) - }; + // parse time from string or get now + let (time, date) = extract_time_or_now(time)?; // TODO: Parse categories and subcategories from string // let (category, subcategory) = if let Some(ref category) = category { @@ -106,7 +97,9 @@ impl BeginCmd { ))); activity_store.setup_storage()?; - activity_store.save_activity(&activity)?; + activity_store.create_activity(&activity)?; + + println!("{activity}"); Ok(()) } diff --git a/src/commands/completions.rs b/src/commands/completions.rs new file mode 100644 index 00000000..0e330438 --- /dev/null +++ b/src/commands/completions.rs @@ -0,0 +1,59 @@ +//! `completions` subcommand + +use abscissa_core::{Command, Runnable}; + +use std::io::Write; + +use clap::CommandFactory; + +use clap_complete::{generate, shells, Generator}; + +/// `completions` subcommand +#[derive(clap::Parser, Command, Debug)] +pub struct CompletionsCmd { + /// Shell to generate completions for + #[clap(value_enum)] + sh: Variant, +} + +#[derive(Clone, Debug, clap::ValueEnum)] +pub enum Variant { + Bash, + Fish, + Zsh, + Powershell, +} + +impl Runnable for CompletionsCmd { + fn run(&self) { + match self.sh { + Variant::Bash => generate_completion(shells::Bash, &mut std::io::stdout()), + Variant::Fish => generate_completion(shells::Fish, &mut std::io::stdout()), + Variant::Zsh => generate_completion(shells::Zsh, &mut std::io::stdout()), + Variant::Powershell => generate_completion(shells::PowerShell, &mut std::io::stdout()), + } + } +} + +pub fn generate_completion(shell: G, buf: &mut dyn Write) { + let mut command = crate::commands::EntryPoint::command(); + generate( + shell, + &mut command, + option_env!("CARGO_BIN_NAME").unwrap_or("pace"), + buf, + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_completions() { + generate_completion(shells::Bash, &mut std::io::sink()); + generate_completion(shells::Fish, &mut std::io::sink()); + generate_completion(shells::PowerShell, &mut std::io::sink()); + generate_completion(shells::Zsh, &mut std::io::sink()); + } +} diff --git a/src/commands/end.rs b/src/commands/end.rs index 0fa35951..44ff0f98 100644 --- a/src/commands/end.rs +++ b/src/commands/end.rs @@ -9,7 +9,7 @@ use crate::prelude::PACE_APP; use pace_core::{ service::activity_store::ActivityStore, - storage::{file::TomlActivityStorage, ActivityStorage}, + storage::{file::TomlActivityStorage, ActivityStateManagement, ActivityStorage}, }; /// `end` subcommand @@ -64,6 +64,8 @@ impl EndCmd { for activity in unfinished_activities { println!("Ended {activity}"); } + } else { + println!("No unfinished activities to end."); } Ok(()) diff --git a/src/commands/now.rs b/src/commands/now.rs index ae42ea74..46e646bb 100644 --- a/src/commands/now.rs +++ b/src/commands/now.rs @@ -7,8 +7,9 @@ use eyre::Result; use crate::prelude::PACE_APP; use pace_core::{ + domain::filter::ActivityFilter, service::activity_store::ActivityStore, - storage::{file::TomlActivityStorage, ActivityStorage}, + storage::{file::TomlActivityStorage, ActivityReadOps, ActivityStorage}, }; /// `now` subcommand @@ -33,14 +34,17 @@ impl NowCmd { activity_store.setup_storage()?; - let current_activities = activity_store.list_current_activities()?; - - if let Some(activities) = current_activities { - for activity in activities { - println!("{}", activity); + match activity_store.list_activities(ActivityFilter::Active)? { + Some(activities) => { + activities + .into_log() + .activities() + .iter() + .for_each(|activity| println!("{}", activity)); + } + None => { + println!("No activities are currently running."); } - } else { - println!("No activities are currently running"); } Ok(()) diff --git a/src/commands/report.rs b/src/commands/review.rs similarity index 86% rename from src/commands/report.rs rename to src/commands/review.rs index 20ed22fa..0b993334 100644 --- a/src/commands/report.rs +++ b/src/commands/review.rs @@ -1,9 +1,9 @@ -//! `report` subcommand +//! `review` subcommand use abscissa_core::{Command, Runnable}; use clap::Parser; -/// `report` subcommand +/// `review` subcommand /// /// The `Parser` proc macro generates an option parser based on the struct /// definition, and is defined in the `clap` crate. See their documentation @@ -11,7 +11,7 @@ use clap::Parser; /// /// #[derive(Command, Debug, Parser)] -pub struct ReportCmd { +pub struct ReviewCmd { // /// Option foobar. Doc comments are the help description // #[clap(short)] // foobar: Option @@ -24,7 +24,7 @@ pub struct ReportCmd { // free_args: Vec, } -impl Runnable for ReportCmd { +impl Runnable for ReviewCmd { /// Start the application. fn run(&self) { // Your code goes here diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 00000000..9624e571 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,87 @@ +use assert_cmd::Command; +use predicates::prelude::predicate; +// use tempfile::{tempdir, TempDir}; + +pub type TestResult = std::result::Result>; + +pub fn pace_runner(/*temp_dir: &TempDir*/) -> TestResult { + // TODO: when we have implemented init, we can use this to create a new pace project + // let _repo_dir = temp_dir.path(); + + let runner = Command::new(env!("CARGO_BIN_EXE_pace")); + + Ok(runner) +} + +// TODO: when we have implemented init, we can use this to create a new pace project +// fn setup() -> TestResult { +// let temp_dir = tempdir()?; +// pace_runner(&temp_dir)?.args(["init"]).assert().success(); + +// Ok(temp_dir) +// } + +#[test] +fn test_version_command_passes() -> TestResult<()> { + pace_runner()? + .arg("--version") + .assert() + .success() + .stdout(predicate::str::contains(env!("CARGO_PKG_VERSION"))); + + Ok(()) +} + +#[test] +fn test_help_command_passes() -> TestResult<()> { + pace_runner()? + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains("Usage:")); + + Ok(()) +} + +// TODO: Test begin command +// #[test] +// fn test_begin_command_passes() -> TestResult<()> { +// pace_runner()? +// .args([ +// "-a", +// "./activity_log.toml", +// "begin", +// "This is my task description", +// "-c", +// "MyCategory::SubCategory", +// ]) +// .assert() +// .success() +// .stdout(predicate::str::contains("started")); // TODO + +// Ok(()) +// } + +// TODO: Test end command +// #[test] +// fn test_end_command_passes() -> TestResult<()> { +// pace_runner()? +// .args(["-a", "./activity_log.toml", "end"]) +// .assert() +// .success() +// .stdout(predicate::str::contains("finished")); // TODO + +// Ok(()) +// } + +// TODO: Test now command +// #[test] +// fn test_now_command_passes() -> TestResult<()> { +// pace_runner()? +// .args(["-a", "./activity_log.toml", "now"]) +// .assert() +// .success() +// .stdout(predicate::str::contains("current")); // TODO + +// Ok(()) +// } diff --git a/tests/fixtures/pace.toml b/tests/fixtures/pace.toml new file mode 100644 index 00000000..25f59a79 --- /dev/null +++ b/tests/fixtures/pace.toml @@ -0,0 +1,52 @@ +[general] +# Define where to store the activity log: options include "file", "database", etc. +log_storage = "file" +# Path to the activity log file, used if log_storage is set to "file" +activity_log_file_path = "C:\\Users\\dailyuse\\backup\\PARA\\Personal\\2 Areas\\Developer\\Maintenance\\pace\\pace-rs\\pace\\data\\activity_2024-02.toml" +# Specify the default format for new activity logs: "toml" or "yaml" +log_format = "toml" +# Autogenerate identifiers for tasks, projects and activities that have been manually created +autogenerate_ids = true +# Category separator used in the cli +category_separator = "::" +# Default priority for new tasks +default_priority = "Medium" + +[reviews] +# Format of the review generated by the pace: "pdf", "html", "markdown", etc. +review_format = "html" +# Directory where the reviews will be stored +review_directory = "/path/to/your/reviews/" + +[export] +export_include_tags = true +export_include_descriptions = true +export_time_format = "%Y-%m-%d %H:%M" + +[database] +# Database configurations are used if log_storage is set to "database" +type = "sqlite" # only option supported for now is "sqlite" +connection_string = "path/to/your/database.db" + +[pomodoro] +# Pomodoro technique specific configurations +work_duration_minutes = 25 +break_duration_minutes = 5 +long_break_duration_minutes = 15 +sessions_before_long_break = 4 + +[inbox] +# Maximum number of items the inbox can hold +max_size = 100 +# Default priority for new tasks added to the inbox +default_priority = "Medium" +# Specifies whether new tasks should be auto-archived after a certain period +auto_archive_after_days = 30 + +[auto_archival] +# Enable or disable automatic archival of completed tasks +enabled = true +# Number of days after which completed tasks are automatically archived +archive_after_days = 90 +# Path to the archival location (relevant if log_storage is "file") +archive_path = "/path/to/your/archive/"