Skip to content

Commit 4b58199

Browse files
authored
chore: refactor and add tests (#1)
* refactor(cli,models,commands,utils): modularize activity logic, update list cmd, and reorganize utils - move activity command logic into src/commands/activity.rs for improved modularity - remove activity_with_log.rs, introduce activity_log.rs with a focused log model - refactor list and log printing logic for consistency; improve JSON and table output - enhance CLI: add better period/date filters, clarify subcommands and argument validation - split utils.rs into utils/date.rs, add utils/mod.rs and placeholder utils/common.rs - add tags to PrintableActivity, improve models and row output - simplify main.rs by delegating logic to command modules - **BREAKING CHANGE:** activity listing model and CLI argument structure have changed * test(cli): add comprehensive CLI integration and unit tests - add integration tests covering activity flow, error scenarios, and CLI output validation using assert_cmd, predicates, and tempfile. - add unit tests for formatting, parsing, and edge case handling in models and utility modules. - fix command argument handling and config injection in tests to use `BOAT_CONFIG` env var. - update error message assertions for compatibility with current clap output. - ensure robust duration formatting and corresponding expectation in tests.
1 parent c5efbf1 commit 4b58199

File tree

16 files changed

+732
-110
lines changed

16 files changed

+732
-110
lines changed

Cargo.lock

Lines changed: 371 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,23 @@ path = "src/main.rs"
1818
clap = { version = "4.6.0", features = ["derive"] }
1919
env_logger = "0.11.9"
2020
log = "0.4.29"
21-
# clap-verbosity-flag = "3.0.4"
21+
clap-verbosity-flag = "3.0.4"
2222
anyhow = "1.0.102"
2323
directories = "6.0.0"
2424
serde = { version = "1.0.228", features = ["derive"] }
2525
toml = "1.0.7"
2626
rusqlite = "0.39.0"
2727
chrono = { version = "0.4.44", features = ["serde"] }
2828
serde_json = "1.0.149"
29-
boat-lib = "0.3.2"
29+
boat-lib = "0.4.0"
3030
tabular = { version = "0.2.0", features = ["ansi-cell"] }
3131
yansi = "1.0.1"
3232

33+
[dev-dependencies]
34+
assert_cmd = "2.0"
35+
predicates = "3.0"
36+
tempfile = "3.8"
37+
3338
[features]
3439
default = []
3540
bundled-sqlite = ["boat-lib/bundled-sqlite"]

README.md

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22

33
`boat` - A **B**asic **O**pinionated **A**ctivity **T**racker, inspired by [bartib](https://github.com/nikolassv/bartib).
44

5-
This is only the code for the command line application. It relies on [`boat-lib`](https://github.com/coko7/boat-lib) for core functions.
5+
Like its name implies, `boat` allows you to track the time you spend on everyday tasks.
6+
7+
It has mainly been designed to be easy to embed in custom bash scripts so that you can augment it with fuzzy-finding.
8+
That said, if you plan to use the CLI directly (without external scripts), it also benefits from a [variety of handy aliases](#-usage).
9+
10+
`boat` stores its data in a SQLite database file which is kept in the config directory by default (`.config/boat/boat.db`).
11+
12+
This repository contains only the code for the command line application.
13+
It relies on [`boat-lib`](https://github.com/coko7/boat-lib) for core functions.
614

715
[![Crates info](https://img.shields.io/crates/v/boat-cli.svg)](https://crates.io/crates/boat-cli)
816
[![License: GPL-3.0](https://img.shields.io/github/license/coko7/boat-cli?color=blue)](LICENSE)
@@ -15,11 +23,19 @@ This is only the code for the command line application. It relies on [`boat-lib`
1523
> This cli is actively being developed. Since it's in its early stages, things will likely break often.
1624
> Don't use it for now.
1725
26+
## Contents
27+
28+
- [🤔 Why was this tool created?](#🤔-why-was-this-tool-created)
29+
- [🛠️ Installation](#🛠️-installation)
30+
- [Install with a bundled version of SQLite](#install-with-a-bundled-version-of-sqlite)
31+
- [⚙️ Configuration](#⚙️-configuration)
32+
- [✨ Usage](#✨-usage)
33+
1834
## 🤔 Why was this tool created?
1935

2036
The [`bartib`](https://github.com/nikolassv/bartib) cli is what inspired me to create `boat`.
2137
It's a feature-full tool that I used for a while, but I found it quite limiting for my usage due to its [lack of support for machine-readable output](https://github.com/nikolassv/bartib/pull/26).
22-
That's it, I wanted an activity tracker that I could combine easily with [`jq`](https://github.com/jqlang/jq) and so I decided to make my own tool.
38+
And that's it. All I wanted was an activity tracker that I could combine easily with [`jq`](https://github.com/jqlang/jq) and so I decided to make my own tool.
2339

2440
## 🛠️ Installation
2541

@@ -58,17 +74,18 @@ By default, `boat` will create a configuration file in one of the following dirs
5874
- 🪟 **Windows:** `C:\Users\<user>\AppData\Roaming\boat\config.toml`
5975
- 🍎 **macOS:** `/Users/<user>/Library/Application Support/boat/config.toml`
6076

61-
It will also keep the SQLite database file `boat.db` in the same directory (unless specified otherwise in config):
77+
It will also store the SQLite database file `boat.db` in the same directory (unless specified otherwise in config):
6278
```toml
6379
database_path = "/home/<user>/.config/boat/boat.db"
6480
```
6581
You can override the default configuration file path by setting the `BOAT_CONFIG` environment variable.
6682

6783
## ✨ Usage
6884

69-
To get a feel of how `boat` can be used, you can try `boat help` to get the list of commands:
85+
If you have ever used [`bartib`](https://github.com/nikolassv/bartib), then `boat` is going to feel very familiar.
86+
Try `boat help` for a quick list of commands:
7087
```help
71-
boat 0.2.1
88+
boat 0.5.0
7289
7390
Basic Opinionated Activity Tracker
7491
@@ -78,11 +95,13 @@ boat <COMMAND>
7895
Commands:
7996
new Create a new activity
8097
start Start/resume an activity
98+
cancel Cancel the current activity
8199
pause Pause/stop the current activity
82100
modify Modify an activity
83101
delete Delete an activity
84102
get Get the current activity
85-
list List boat objects
103+
list List activities
104+
query Query boat objects
86105
help Print this message or the help of the given subcommand(s)
87106
88107
Options:
@@ -92,21 +111,25 @@ Options:
92111
Made by @coko7 <contact@coko7.fr>
93112
```
94113

95-
If you want to invoke `boat` from your command-line directly, you can make use of a variety of shorter aliases:
96-
```help
97-
Commands:
98-
new n
99-
start s, st, sail
100-
config c, cfg, conf
101-
pause p
102-
modify m, mod
103-
delete d, del
104-
get g
105-
list l, ls
106-
help h, -h, --help
107-
```
114+
> [!TIP]
115+
> `boat` comes bundled with many command aliases:
116+
> - new: `n`, `new`, `create`
117+
> - start: `s`, `st`, `start`, `sail`, `continue`, `resume`
118+
> - cancel: `c`, `can`, `cancel`
119+
> - pause: `p`, `pause`, `stop`
120+
> - modify: `m`, `mod`, `modify`
121+
> - delete: `d`, `del`, `delete`, `rm`, `rem`, `remove`
122+
> - get: `g`, `get`
123+
> - list: `l`, `ls`, `list`
124+
> - query: `q`, `query`
125+
> - help: `h`, `help`, `-h`, `--help`
126+
>
127+
> Prefer using the full length command names in scripts as they are more explicit and unlikely to be changed (unlike shorter aliases).
128+
108129
I really wanted to have each command start with a different character so that I could assign a single-char alias to all of them.
109130
That explains why some of the commands do not use a more fitting keyword.
110131

111132
Like `stop` would have been a better command than `pause` but since it shares the same starting charcter as the `start` command, I could not use it.
112133
Maybe I will drop this in the future, let's see.
134+
135+
*I have included some fallback in case you type `stop`/`remove` instead of `pause`/`delete` 👀*

src/cli.rs

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,32 @@ use crate::utils;
1818
pub struct Cli {
1919
#[command(subcommand)]
2020
pub command: Commands,
21-
//
22-
// #[command(flatten)]
23-
// pub verbose: clap_verbosity_flag::Verbosity,
21+
22+
#[command(flatten)]
23+
pub verbose: clap_verbosity_flag::Verbosity,
2424
}
2525

2626
#[derive(Subcommand)]
2727
#[command(rename_all = "kebab-case")]
2828
pub enum Commands {
2929
/// Create a new activity
30-
#[command(alias = "n")]
30+
#[command(alias = "n", alias = "create")]
3131
New(CreateActivityArgs),
3232

33-
// create a backup command
3433
/// Start/resume an activity
35-
#[command(alias = "s", alias = "st", alias = "sail")]
34+
#[command(
35+
alias = "s",
36+
alias = "st",
37+
alias = "sail",
38+
alias = "continue",
39+
alias = "resume"
40+
)]
3641
Start(SelectActivityArgs),
3742

38-
// /// Manage configuration
39-
// #[command(alias = "c", alias = "cfg", alias = "conf")]
40-
// Config {},
43+
/// Cancel the current activity
44+
#[command(alias = "c", alias = "can")]
45+
Cancel,
46+
4147
/// Pause/stop the current activity
4248
#[command(alias = "p", alias = "stop")]
4349
Pause,
@@ -47,18 +53,28 @@ pub enum Commands {
4753
Modify(ModifyActivityArgs),
4854

4955
/// Delete an activity
50-
#[command(alias = "d", alias = "del")]
56+
#[command(
57+
alias = "d",
58+
alias = "del",
59+
alias = "rm",
60+
alias = "rem",
61+
alias = "remove"
62+
)]
5163
Delete(SelectActivityArgs),
5264

5365
/// Get the current activity
5466
#[command(alias = "g")]
5567
Get(PrintActivityArgs),
5668

57-
/// List boat objects
69+
/// List activities
5870
#[command(alias = "l", alias = "ls")]
59-
List {
71+
List(ListActivityArgs),
72+
73+
/// Query boat objects
74+
#[command(alias = "q")]
75+
Query {
6076
#[command(subcommand)]
61-
command: ListSubcommand,
77+
command: QuerySubcommand,
6278
},
6379

6480
// This is ONLY way I could find to use the 'h' short alias for help.
@@ -84,12 +100,12 @@ pub enum Commands {
84100

85101
#[derive(Subcommand)]
86102
#[command(rename_all = "kebab-case")]
87-
pub enum ListSubcommand {
88-
/// List activity logs
103+
pub enum QuerySubcommand {
104+
/// Manage logs
89105
#[command(name = "logs", alias = "l", alias = "log")]
90106
Logs(ListActivityArgs),
91107

92-
/// List activities
108+
/// Manage activities
93109
#[command(
94110
name = "acts",
95111
alias = "act",
@@ -99,15 +115,15 @@ pub enum ListSubcommand {
99115
)]
100116
Activities(ListArgs),
101117

102-
/// List tags
118+
/// Manage tags
103119
#[command(name = "tags", alias = "t", alias = "tag")]
104120
Tags(ListArgs),
105121
}
106122

107123
#[derive(Args, Debug)]
108124
pub struct ListActivityArgs {
109125
/// 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"])]
126+
#[arg(short = 'p', long = "period", value_name = "PERIOD", default_value_t = Period::ThisWeek, value_enum, conflicts_with_all = ["from", "to", "date"])]
111127
pub period: Period,
112128

113129
/// Restrict to entries starting after <DATE> (YYYY-MM-DD format)
@@ -122,6 +138,14 @@ pub struct ListActivityArgs {
122138
#[arg(short = 'd', long = "date", value_name = "DATE", value_parser = utils::date::parse_date, conflicts_with_all = ["period", "from", "to"])]
123139
pub date: Option<NaiveDate>,
124140

141+
/// Only show activities, do not include their respective logs
142+
#[arg(short = 'a', long = "activities-only", conflicts_with = "no_grouping")]
143+
pub activities_only: bool,
144+
145+
/// Do not group activities by date
146+
#[arg(short = 'n', long = "no-grouping", conflicts_with = "activities_only")]
147+
pub no_grouping: bool,
148+
125149
/// Output in JSON
126150
#[arg(short = 'j', long = "json")]
127151
pub use_json_format: bool,
@@ -136,11 +160,11 @@ pub struct ListArgs {
136160

137161
#[derive(ValueEnum, Clone, Debug)]
138162
pub enum Period {
139-
#[value(name = "today", alias = "td")]
163+
#[value(name = "today", alias = "td", alias = "tod")]
140164
Today,
141165
#[value(name = "yesterday", alias = "yd", alias = "ytd")]
142166
Yesterday,
143-
#[value(name = "this-week", alias = "tw", alias = "twk")]
167+
#[value(name = "this-week", alias = "tw", alias = "twk", alias = "wk")]
144168
ThisWeek,
145169
#[value(
146170
name = "last-week",
@@ -151,7 +175,7 @@ pub enum Period {
151175
alias = "ywk"
152176
)]
153177
LastWeek,
154-
#[value(name = "this-month", alias = "tm", alias = "tmo")]
178+
#[value(name = "this-month", alias = "tm", alias = "tmo", alias = "mo")]
155179
ThisMonth,
156180
#[value(
157181
name = "last-month",
@@ -189,6 +213,10 @@ impl Default for PrintActivityArgs {
189213
pub struct SelectActivityArgs {
190214
/// ID of the activity
191215
pub activity_id: Id,
216+
217+
/// Output in JSON
218+
#[arg(short = 'j', long = "json")]
219+
pub use_json_format: bool,
192220
}
193221

194222
#[derive(Args, Debug)]
@@ -207,6 +235,10 @@ pub struct CreateActivityArgs {
207235
/// Start the new activity automatically
208236
#[arg(short = 's', long = "start")]
209237
pub auto_start: bool,
238+
239+
/// Output in JSON
240+
#[arg(short = 'j', long = "json")]
241+
pub use_json_format: bool,
210242
}
211243

212244
#[derive(Args, Debug)]

src/commands/activity.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use rusqlite::Connection;
44

55
use crate::{
66
cli,
7-
models::{TablePrintable, activity_log::PrintableActivityLog},
7+
models::{TablePrintable, activity::PrintableActivity, activity_log::PrintableActivityLog},
88
};
99

1010
pub fn create(conn: &mut Connection, args: &cli::CreateActivityArgs) -> Result<()> {
@@ -19,6 +19,13 @@ pub fn create(conn: &mut Connection, args: &cli::CreateActivityArgs) -> Result<(
1919
activities::start(conn, created.id)?;
2020
}
2121

22+
let act = PrintableActivity::from_activity(&created);
23+
if args.use_json_format {
24+
let json = serde_json::to_string(&act)?;
25+
println!("{json}");
26+
return Ok(());
27+
}
28+
2229
println!("{}", created.id);
2330
Ok(())
2431
}
@@ -90,3 +97,16 @@ pub fn get_current(conn: &mut Connection, args: &cli::PrintActivityArgs) -> Resu
9097
}
9198
Ok(())
9299
}
100+
101+
pub fn cancel_current(conn: &mut Connection) -> Result<()> {
102+
match activities::get_current_ongoing(conn)? {
103+
Some(act) => {
104+
activities::cancel_current(conn)?;
105+
println!("cancelled activity: {act:?}");
106+
}
107+
None => {
108+
println!("no current activity");
109+
}
110+
}
111+
Ok(())
112+
}

0 commit comments

Comments
 (0)