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

refactor(commands)!: Move a lot of the commands into pace-core #56

Merged
merged 7 commits into from Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions Cargo.toml
Expand Up @@ -14,7 +14,6 @@ clap = { version = "4", features = ["env", "wrap_help", "derive"] }
eyre = "0.6.12"
pace_cli = { path = "crates/cli", version = "0" }
pace_core = { path = "crates/core", version = "0" }
pace_server = { path = "crates/server", version = "0" }
similar-asserts = { version = "1.5.0", features = ["serde"] }

[package]
Expand Down Expand Up @@ -52,7 +51,6 @@ directories = "5.0.1"
eyre = { workspace = true }
human-panic = "1.2.3"
insta = { version = "1.35.1", features = ["toml"] }
open = "5.0.2"
pace_cli = { workspace = true }
pace_core = { workspace = true, features = ["cli"] }
serde = "1"
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/lib.rs
Expand Up @@ -7,6 +7,6 @@ pub(crate) mod setup;

// Public API
pub use crate::{
prompt::confirmation_or_break,
prompt::{confirmation_or_break, prompt_resume_activity},
setup::{setup_config, PathOptions},
};
22 changes: 21 additions & 1 deletion crates/cli/src/prompt.rs
@@ -1,6 +1,6 @@
use std::path::PathBuf;

use dialoguer::{theme::ColorfulTheme, Confirm, Select};
use dialoguer::{theme::ColorfulTheme, Confirm, FuzzySelect, Select};
use eyre::Result;
use tracing::debug;

Expand Down Expand Up @@ -123,3 +123,23 @@ pub fn confirmation_or_break(prompt: &str) -> Result<()> {

Ok(())
}

/// Prompts the user to select an activity to resume
///
/// # Arguments
///
/// * `string_repr` - The list of activities represented as a String to resume
///
/// # Errors
///
/// Returns an error if the prompt fails
///
/// # Returns
///
/// Returns the index of the selected activity
pub fn prompt_resume_activity(string_repr: Vec<String>) -> Result<usize, dialoguer::Error> {
FuzzySelect::with_theme(&ColorfulTheme::default())
.with_prompt("Which activity do you want to continue?")
.items(&string_repr)
.interact()
}
4 changes: 2 additions & 2 deletions crates/cli/src/setup.rs
Expand Up @@ -15,8 +15,8 @@ use tracing::{debug, info};
use typed_builder::TypedBuilder;

use pace_core::{
get_activity_log_paths, get_config_paths, toml, ActivityLog, PaceConfig,
PACE_ACTIVITY_LOG_FILENAME, PACE_CONFIG_FILENAME,
constants::PACE_ACTIVITY_LOG_FILENAME, constants::PACE_CONFIG_FILENAME, get_activity_log_paths,
get_config_paths, toml, ActivityLog, PaceConfig,
};

use crate::prompt::{prompt_activity_log_path, prompt_config_file_path};
Expand Down
1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Expand Up @@ -37,6 +37,7 @@ itertools = "0.12.1"
log = "0.4.21"
merge = "0.1.0"
miette = { version = "7.1.0", features = ["fancy"] }
open = "5.0.2"
parking_lot = { version = "0.12.1", features = ["deadlock_detection"] }
rayon = "1.9.0"
rusqlite = { version = "0.31.0", features = ["bundled", "chrono", "uuid"], optional = true }
Expand Down
5 changes: 5 additions & 0 deletions crates/core/src/commands.rs
@@ -1,5 +1,10 @@
pub mod begin;
pub mod docs;
pub mod end;
pub mod hold;
pub mod now;
pub mod resume;
pub mod review;

use getset::Getters;
use typed_builder::TypedBuilder;
Expand Down
101 changes: 101 additions & 0 deletions crates/core/src/commands/begin.rs
@@ -0,0 +1,101 @@
use std::collections::HashSet;

#[cfg(feature = "clap")]
use clap::Parser;

use crate::{
extract_time_or_now, get_storage_from_config, Activity, ActivityKind, ActivityStateManagement,
ActivityStore, PaceConfig, PaceResult, SyncStorage,
};

/// `begin` subcommand
#[derive(Debug)]
#[cfg_attr(feature = "clap", derive(Parser))]
pub struct BeginCommandOptions {
/// The Category of the activity you want to start
///
/// You can use the separator you setup in the configuration file
/// to specify a subcategory.
#[cfg_attr(feature = "clap", clap(short, long, name = "Category"))]
category: Option<String>,

/// The time the activity has been started at. Format: HH:MM
// FIXME: We should directly parse that into PaceTime or PaceDateTime
#[cfg_attr(feature = "clap", clap(long, name = "Starting Time", alias = "at"))]
start: Option<String>,

/// The description of the activity you want to start
#[cfg_attr(feature = "clap", clap(name = "Activity Description"))]
description: String,

/// The tags you want to associate with the activity, separated by a comma
#[cfg_attr(
feature = "clap",
clap(short, long, name = "Tag", value_delimiter = ',')
)]
tags: Option<Vec<String>>,

/// TODO: The project you want to start tracking time for
/// FIXME: involves parsing the project configuration first
#[cfg_attr(feature = "clap", clap(skip))]
_projects: Option<Vec<String>>,
}

impl BeginCommandOptions {
/// Inner run implementation for the begin command
pub fn handle_begin(&self, config: &PaceConfig) -> PaceResult<()> {
let Self {
category,
start: time,
description,
tags,
.. // TODO: exclude projects for now
} = self;

// parse tags from string or get an empty set
let tags = tags
.as_ref()
.map(|tags| tags.iter().cloned().collect::<HashSet<String>>());

// parse time from string or get now
let date_time = extract_time_or_now(time)?;

// TODO: Parse categories and subcategories from string
// let (category, subcategory) = if let Some(ref category) = category {
// let separator = config.general().category_separator();
// extract_categories(category.as_str(), separator.as_str())
// } else {
// // if no category is given, use the default category
// // FIXME: This should be the default category from the project configuration
// // but for now, we'll just use category defaults
// //
// // FIXME: We might also want to merge the project configuration with the general configuration first to have precedence
// //
// // let category = if let Some(category) = PACE_APP.config().general().default_category() {
// // category
// // } else {
// // &Category::default()
// // };

// (Category::default(), None)
// };

let activity = Activity::builder()
.description(description.clone())
.begin(date_time)
.kind(ActivityKind::default())
.category(category.clone())
.tags(tags.clone())
.build();

let activity_store = ActivityStore::new(get_storage_from_config(config)?);

let activity_item = activity_store.begin_activity(activity.clone())?;

activity_store.sync()?;

println!("{}", activity_item.activity());

Ok(())
}
}
24 changes: 24 additions & 0 deletions crates/core/src/commands/docs.rs
@@ -0,0 +1,24 @@
#[cfg(feature = "clap")]
use clap::Parser;

use crate::{constants::PACE_DEV_DOCS_URL, constants::PACE_DOCS_URL, PaceResult};

/// Opens the documentation.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "clap", derive(Parser))]
pub struct DocsCommandOptions {
/// Open the development documentation
#[cfg_attr(feature = "clap", clap(short, long))]
dev: bool,
}

impl DocsCommandOptions {
pub fn handle_docs(&self) -> PaceResult<()> {
match self.dev {
true => open::that(PACE_DEV_DOCS_URL)?,
false => open::that(PACE_DOCS_URL)?,
}

Ok(())
}
}
53 changes: 53 additions & 0 deletions crates/core/src/commands/end.rs
@@ -0,0 +1,53 @@
#[cfg(feature = "clap")]
use clap::Parser;
use getset::Getters;
use typed_builder::TypedBuilder;

use crate::{
get_storage_from_config, parse_time_from_user_input, ActivityStateManagement, ActivityStore,
EndOptions, PaceConfig, PaceResult, SyncStorage,
};

/// `end` subcommand
#[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)]
#[getset(get = "pub")]
#[non_exhaustive]
#[cfg_attr(feature = "clap", derive(Parser))]
pub struct EndCommandOptions {
/// The time the activity has ended (defaults to the current time if not provided). Format: HH:MM
#[cfg_attr(feature = "clap", clap(long, name = "Finishing Time", alias = "at"))]
// FIXME: We should directly parse that into PaceTime or PaceDateTime
end: Option<String>,

/// End only the last unfinished activity
#[cfg_attr(feature = "clap", clap(long))]
only_last: bool,
}

impl EndCommandOptions {
pub fn handle_end(&self, config: &PaceConfig) -> PaceResult<()> {
let time = parse_time_from_user_input(&self.end)?;

let activity_store = ActivityStore::new(get_storage_from_config(config)?);

let end_opts = EndOptions::builder().end_time(time).build();

if self.only_last {
if let Some(last_activity) = activity_store.end_last_unfinished_activity(end_opts)? {
println!("Ended {}", last_activity.activity());
}
} else if let Some(unfinished_activities) =
activity_store.end_all_unfinished_activities(end_opts)?
{
for activity in &unfinished_activities {
println!("Ended {}", activity.activity());
}
} else {
println!("No unfinished activities to end.");
}

activity_store.sync()?;

Ok(())
}
}
57 changes: 56 additions & 1 deletion crates/core/src/commands/hold.rs
@@ -1,7 +1,62 @@
#[cfg(feature = "clap")]
use clap::Parser;

use getset::Getters;
use typed_builder::TypedBuilder;

use crate::{IntermissionAction, PaceDateTime};
use crate::{
get_storage_from_config, parse_time_from_user_input, ActivityStateManagement, ActivityStore,
IntermissionAction, PaceConfig, PaceDateTime, PaceResult, SyncStorage,
};

/// `hold` subcommand>
#[derive(Debug)]
#[cfg_attr(feature = "clap", derive(Parser))]
pub struct HoldCommandOptions {
/// The time the activity has been holded (defaults to the current time if not provided). Format: HH:MM
#[cfg_attr(feature = "clap", clap(long, name = "Pause Time", alias = "at"))]
// FIXME: We should directly parse that into PaceTime or PaceDateTime
pause_at: Option<String>,

/// The reason for the intermission, if this is not set, the description of the activity to be held will be used
#[cfg_attr(feature = "clap", clap(short, long, name = "Reason"))]
reason: Option<String>,

/// If there are existing intermissions, they will be finished and a new one is being created
///
/// This is useful, if you want to also track the purpose of an interruption to an activity.
#[cfg_attr(feature = "clap", clap(long))]
new_if_exists: bool,
}

impl HoldCommandOptions {
pub fn handle_hold(&self, config: &PaceConfig) -> PaceResult<()> {
let action = if self.new_if_exists {
IntermissionAction::New
} else {
IntermissionAction::Extend
};

let time = parse_time_from_user_input(&self.pause_at)?;

let hold_opts = HoldOptions::builder()
.action(action)
.reason(self.reason.clone())
.begin_time(time)
.build();

let activity_store = ActivityStore::new(get_storage_from_config(config)?);

if let Some(activity) = activity_store.hold_most_recent_active_activity(hold_opts)? {
activity_store.sync()?;
println!("Held {}", activity.activity());
} else {
println!("No unfinished activities to hold.");
};

Ok(())
}
}

/// Options for holding an activity
#[derive(Debug, Clone, PartialEq, TypedBuilder, Eq, Hash, Default, Getters)]
Expand Down
36 changes: 36 additions & 0 deletions crates/core/src/commands/now.rs
@@ -0,0 +1,36 @@
#[cfg(feature = "clap")]
use clap::Parser;

use crate::{
get_storage_from_config, ActivityItem, ActivityQuerying, ActivityReadOps, ActivityStatusFilter,
ActivityStore, PaceConfig, PaceResult,
};

/// `now` subcommand
#[derive(Debug)]
#[cfg_attr(feature = "clap", derive(Parser))]
pub struct NowCommandOptions {}

impl NowCommandOptions {
pub fn handle_now(&self, config: &PaceConfig) -> PaceResult<()> {
let activity_store = ActivityStore::new(get_storage_from_config(config)?);

match activity_store.list_current_activities(ActivityStatusFilter::Active)? {
Some(activities) => {
let activity_items = activities
.iter()
.flat_map(|activity_id| activity_store.read_activity(*activity_id))
.collect::<Vec<ActivityItem>>();

activity_items.iter().for_each(|activity| {
println!("{}", activity.activity());
});
}
None => {
println!("No activities are currently running.");
}
}

Ok(())
}
}