Skip to content

Commit

Permalink
refactor(commands)!: Move a lot of the commands into pace-core (#56)
Browse files Browse the repository at this point in the history
* refactor(commands)!: Move a lot of the commands into pace-core

Signed-off-by: simonsan <[email protected]>

* refactor: rename option commands

Signed-off-by: simonsan <[email protected]>

* refactor: move constants to own module

Signed-off-by: simonsan <[email protected]>

* ci: add feature-powerset to local ci

Signed-off-by: simonsan <[email protected]>

* fix: missing attrib

Signed-off-by: simonsan <[email protected]>

* refactor(commands)!: Factor out some resume related things

Signed-off-by: simonsan <[email protected]>

* fix: shorten user prompt

Signed-off-by: simonsan <[email protected]>

---------

Signed-off-by: simonsan <[email protected]>
  • Loading branch information
simonsan authored Feb 28, 2024
1 parent e4c0c23 commit e03b68f
Show file tree
Hide file tree
Showing 30 changed files with 600 additions and 531 deletions.
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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(())
}
}
Loading

0 comments on commit e03b68f

Please sign in to comment.