Skip to content

Commit 65e80cd

Browse files
committed
refactor(cli,models,commands): modularize activity logic, update list handling, and reorganize utils
- move activity-related command logic into src/commands/activity.rs - update CLI commands and subcommands for improved UX, including better period/date filtering, richer aliases, and input validation - refactor list command and printable logic; optimize data flow for listing activities, logs, and tags - Update models to add tags to activities, rework how activity rows are rendered - Clean up main.rs command dispatch and remove obsolete local implementations, using refactored modules instead - various small improvements and internal API changes
1 parent 927dbbc commit 65e80cd

File tree

14 files changed

+274
-278
lines changed

14 files changed

+274
-278
lines changed

src/cli.rs

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use boat_lib::repository::Id;
2+
use chrono::NaiveDate;
23
use clap::ColorChoice;
34
use clap::Parser;
45
use clap::{ArgAction, Args, Subcommand, ValueEnum};
56

7+
use crate::utils;
8+
69
#[derive(Parser)]
710
#[command(
811
name = "boat",
@@ -83,24 +86,42 @@ pub enum Commands {
8386
#[command(rename_all = "kebab-case")]
8487
pub enum ListSubcommand {
8588
/// List activity logs
86-
#[command(alias = "l", alias = "log")]
89+
#[command(name = "logs", alias = "l", alias = "log")]
8790
Logs(ListActivityArgs),
8891

8992
/// List activities
90-
#[command(alias = "a", alias = "act", alias = "acts", alias = "activity")]
93+
#[command(
94+
name = "acts",
95+
alias = "act",
96+
alias = "a",
97+
alias = "activity",
98+
alias = "activities"
99+
)]
91100
Activities(ListArgs),
92101

93102
/// List tags
94-
#[command(alias = "t", alias = "tag")]
103+
#[command(name = "tags", alias = "t", alias = "tag")]
95104
Tags(ListArgs),
96105
}
97106

98107
#[derive(Args, Debug)]
99108
pub struct ListActivityArgs {
100-
/// Only show activities matching a certain period
101-
#[arg(short = 'p', long = "period", value_name = "PERIOD", default_value_t = Period::Today, value_enum)]
109+
/// Restrict to entries starting in the given <PERIOD>
110+
#[arg(short = 'p', long = "period", value_name = "PERIOD", default_value_t = Period::Today, value_enum, conflicts_with_all = ["from", "to", "date"])]
102111
pub period: Period,
103112

113+
/// Restrict to entries starting after <DATE> (YYYY-MM-DD format)
114+
#[arg(short = 'f', long = "from", value_name = "DATE", value_parser = utils::date::parse_date, conflicts_with = "date")]
115+
pub from: Option<NaiveDate>,
116+
117+
/// Restrict to entries starting before <DATE> (YYYY-MM-DD format)
118+
#[arg(short = 't', long = "to", value_name = "DATE", value_parser = utils::date::parse_date, conflicts_with = "date")]
119+
pub to: Option<NaiveDate>,
120+
121+
/// Restrict to entries starting and ending on <DATE> (YYYY-MM-DD format)
122+
#[arg(short = 'd', long = "date", value_name = "DATE", value_parser = utils::date::parse_date, conflicts_with_all = ["period", "from", "to"])]
123+
pub date: Option<NaiveDate>,
124+
104125
/// Output in JSON
105126
#[arg(short = 'j', long = "json")]
106127
pub use_json_format: bool,
@@ -115,23 +136,32 @@ pub struct ListArgs {
115136

116137
#[derive(ValueEnum, Clone, Debug)]
117138
pub enum Period {
118-
#[value(name = "today", alias = "tod", alias = "td")]
139+
#[value(name = "today", alias = "td")]
119140
Today,
120-
#[value(name = "yesterday", alias = "yes", alias = "yd")]
141+
#[value(name = "yesterday", alias = "yd", alias = "ytd")]
121142
Yesterday,
122-
#[value(name = "this-week", alias = "wk", alias = "week", alias = "toweek")]
143+
#[value(name = "this-week", alias = "tw", alias = "twk")]
123144
ThisWeek,
124-
#[value(name = "last-week", alias = "last-wk", alias = "yesterweek")]
145+
#[value(
146+
name = "last-week",
147+
alias = "lw",
148+
alias = "lwk",
149+
alias = "yesterweek",
150+
alias = "yw",
151+
alias = "ywk"
152+
)]
125153
LastWeek,
126-
#[value(name = "this-month", alias = "mo", alias = "month", alias = "tomonth")]
154+
#[value(name = "this-month", alias = "tm", alias = "tmo")]
127155
ThisMonth,
128-
#[value(name = "last-month", alias = "last-mo", alias = "yestermonth")]
156+
#[value(
157+
name = "last-month",
158+
alias = "lm",
159+
alias = "lmo",
160+
alias = "yestermonth",
161+
alias = "ym",
162+
alias = "ymo"
163+
)]
129164
LastMonth,
130-
// #[value(name = "range")]
131-
// DateRange {
132-
// #[arg(value_parser = range_parser)]
133-
// range: RangeInclusive<u32>,
134-
// },
135165
}
136166

137167
#[derive(Args, Debug)]

src/commands/activity.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use anyhow::{Context, Result};
2+
use boat_lib::{models::activity::NewActivity, repository::activities_repository as activities};
3+
use rusqlite::Connection;
4+
5+
use crate::{
6+
cli,
7+
models::{TablePrintable, activity_log::PrintableActivityLog},
8+
};
9+
10+
pub fn create(conn: &mut Connection, args: &cli::CreateActivityArgs) -> Result<()> {
11+
let new_activity = NewActivity {
12+
name: args.name.clone(),
13+
description: args.description.clone(),
14+
tags: args.tags.clone(),
15+
};
16+
17+
let created = activities::create(conn, new_activity)?;
18+
if args.auto_start {
19+
activities::start(conn, created.id)?;
20+
}
21+
22+
println!("{}", created.id);
23+
Ok(())
24+
}
25+
26+
pub fn start(conn: &mut Connection, args: &cli::SelectActivityArgs) -> Result<()> {
27+
if let Some(current) = activities::get_current_ongoing(conn)? {
28+
println!("stopping activity: {current:?}");
29+
}
30+
31+
let act = activities::get_by_id(conn, args.activity_id)?;
32+
activities::start(conn, args.activity_id)?;
33+
println!("started activity: {act:?}");
34+
Ok(())
35+
}
36+
37+
pub fn pause_current(conn: &mut Connection) -> Result<()> {
38+
if let Some(current) = activities::get_current_ongoing(conn)? {
39+
let current = PrintableActivityLog::from_activity(&current);
40+
activities::stop_current(conn)?;
41+
println!("stopped activity: {current:?}");
42+
} else {
43+
println!("no current activity");
44+
}
45+
Ok(())
46+
}
47+
48+
pub fn modify(conn: &mut Connection, args: &cli::ModifyActivityArgs) -> Result<()> {
49+
activities::update(
50+
conn,
51+
args.id,
52+
args.update.name.as_deref(),
53+
args.update.description.as_deref(),
54+
args.update.tags.as_deref(),
55+
)?;
56+
let act = activities::get_by_id(conn, args.id)?;
57+
println!("modified activity: {act:?}");
58+
Ok(())
59+
}
60+
61+
pub fn delete(conn: &mut Connection, args: &cli::SelectActivityArgs) -> Result<()> {
62+
let act = activities::get_by_id(conn, args.activity_id)?;
63+
activities::delete(conn, args.activity_id)?;
64+
println!("deleted activity: {act:?}");
65+
Ok(())
66+
}
67+
68+
pub fn get_current(conn: &mut Connection, args: &cli::PrintActivityArgs) -> Result<()> {
69+
let act = activities::get_current_ongoing(conn)?;
70+
match act {
71+
Some(v) => {
72+
let activity_logs = PrintableActivityLog::from_activity(&v);
73+
let ongoing_log = activity_logs
74+
.iter()
75+
.find(|l| l.log.ends_at.is_none())
76+
.context("there should be an ongoing log")?
77+
.clone();
78+
79+
if args.use_json_format {
80+
let json = serde_json::to_string(&ongoing_log)?;
81+
println!("{json}");
82+
return Ok(());
83+
}
84+
85+
let items = vec![ongoing_log];
86+
let table = items.to_printable_table();
87+
println!("{table}");
88+
}
89+
None => println!("no current activity"),
90+
}
91+
Ok(())
92+
}

src/commands/list.rs

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ use boat_lib::repository::{activities_repository as activities, tags_repository
33
use rusqlite::Connection;
44
use serde::Serialize;
55
use std::cmp::Reverse;
6-
use tabular::{Row, Table};
7-
use yansi::Paint;
86

97
use crate::{
108
cli,
119
models::{
1210
RowPrintable, TablePrintable, activity::PrintableActivity,
13-
activity_with_log::PrintableActivityWithLogs, tag::PrintableTag,
11+
activity_log::PrintableActivityLog, tag::PrintableTag,
1412
},
1513
utils,
1614
};
@@ -23,11 +21,11 @@ pub fn list(conn: &mut Connection, command: &cli::ListSubcommand) -> Result<()>
2321
}
2422
}
2523

26-
fn list_printable_items<T: RowPrintable + Serialize>(
24+
pub fn list_printable_items<T: RowPrintable + Serialize>(
2725
items: Vec<T>,
28-
args: &cli::ListArgs,
26+
show_as_json: bool,
2927
) -> Result<()> {
30-
if args.use_json_format {
28+
if show_as_json {
3129
let json = serde_json::to_string(&items)?;
3230
println!("{json}");
3331
return Ok(());
@@ -45,7 +43,7 @@ fn list_tags(conn: &mut Connection, args: &cli::ListArgs) -> Result<()> {
4543
.collect();
4644
all_tags.sort_by_key(|t| Reverse(t.id));
4745

48-
list_printable_items(all_tags, args)
46+
list_printable_items(all_tags, args.use_json_format)
4947
}
5048

5149
fn list_activities(conn: &mut Connection, args: &cli::ListArgs) -> Result<()> {
@@ -55,61 +53,22 @@ fn list_activities(conn: &mut Connection, args: &cli::ListArgs) -> Result<()> {
5553
.collect();
5654
all_acts.sort_by_key(|a| Reverse(a.id));
5755

58-
list_printable_items(all_acts, args)
56+
list_printable_items(all_acts, args.use_json_format)
5957
}
6058

6159
fn list_activity_logs(conn: &mut Connection, args: &cli::ListActivityArgs) -> Result<()> {
6260
let all: Vec<_> = activities::get_all(conn)?
6361
.iter()
64-
.map(PrintableActivityWithLogs::from_activity)
62+
.flat_map(PrintableActivityLog::from_activity)
63+
.filter(|al| match args.period {
64+
cli::Period::Today => utils::date::is_today(al.log.starts_at),
65+
cli::Period::Yesterday => utils::date::is_yesterday(al.log.starts_at),
66+
cli::Period::ThisWeek => utils::date::is_this_week(al.log.starts_at),
67+
cli::Period::LastWeek => utils::date::is_last_week(al.log.starts_at),
68+
cli::Period::ThisMonth => utils::date::is_this_month(al.log.starts_at),
69+
cli::Period::LastMonth => utils::date::is_last_month(al.log.starts_at),
70+
})
6571
.collect();
6672

67-
if args.use_json_format {
68-
let json = serde_json::to_string(&all)?;
69-
println!("{json}");
70-
} else {
71-
let mut table = Table::new("{:>} {:<} {:<} {:<} {:<} {:<} {:<}");
72-
table.add_row(
73-
Row::new()
74-
.with_ansi_cell("ID".underline())
75-
.with_ansi_cell("Name".underline())
76-
.with_ansi_cell("Description".underline())
77-
.with_ansi_cell("Tags".underline())
78-
.with_ansi_cell("Start".underline())
79-
.with_ansi_cell("End".underline())
80-
.with_ansi_cell("Duration".underline()),
81-
);
82-
83-
let mut log_lines: Vec<_> = all
84-
.iter()
85-
.flat_map(|act| act.logs.iter().zip(std::iter::repeat(act)))
86-
.filter(|(log, _act)| match args.period {
87-
cli::Period::Today => utils::is_today(log.starts_at),
88-
cli::Period::Yesterday => utils::is_yesterday(log.starts_at),
89-
cli::Period::ThisWeek => utils::is_this_week(log.starts_at),
90-
cli::Period::LastWeek => utils::is_last_week(log.starts_at),
91-
cli::Period::ThisMonth => utils::is_this_month(log.starts_at),
92-
cli::Period::LastMonth => utils::is_last_month(log.starts_at),
93-
})
94-
// .filter(|(log, _act)| utils::is_today(log.starts_at))
95-
.collect();
96-
97-
log_lines.sort_by_key(|(log, _)| log.starts_at);
98-
99-
for &(log, act) in log_lines.iter() {
100-
let mut row = Row::new();
101-
let values = utils::convert_to_log_line(log, act);
102-
for val in values.iter() {
103-
if log.ends_at.is_none() {
104-
row = row.with_ansi_cell(val.green());
105-
} else {
106-
row = row.with_cell(val);
107-
}
108-
}
109-
table.add_row(row);
110-
}
111-
println!("{table}");
112-
}
113-
114-
Ok(())
73+
list_printable_items(all, args.use_json_format)
11574
}

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
pub mod activity;
12
pub mod list;

0 commit comments

Comments
 (0)