Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
380 changes: 372 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "boat-cli"
version = "0.4.0"
version = "0.5.0"
edition = "2024"
description = "Basic Opinionated Activity Tracker, a command line interface inspired by bartib"
repository = "https://github.com/coko7/boat"
Expand All @@ -18,18 +18,23 @@ path = "src/main.rs"
clap = { version = "4.6.0", features = ["derive"] }
env_logger = "0.11.9"
log = "0.4.29"
# clap-verbosity-flag = "3.0.4"
clap-verbosity-flag = "3.0.4"
anyhow = "1.0.102"
directories = "6.0.0"
serde = { version = "1.0.228", features = ["derive"] }
toml = "1.0.7"
rusqlite = "0.39.0"
chrono = { version = "0.4.44", features = ["serde"] }
serde_json = "1.0.149"
boat-lib = "0.3.2"
boat-lib = "0.4.0"
tabular = { version = "0.2.0", features = ["ansi-cell"] }
yansi = "1.0.1"

[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.0"
tempfile = "3.8"

[features]
default = []
bundled-sqlite = ["boat-lib/bundled-sqlite"]
61 changes: 42 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

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

This is only the code for the command line application. It relies on [`boat-lib`](https://github.com/coko7/boat-lib) for core functions.
Like its name implies, `boat` allows you to track the time you spend on everyday tasks.

It has mainly been designed to be easy to embed in custom bash scripts so that you can augment it with fuzzy-finding.
That said, if you plan to use the CLI directly (without external scripts), it also benefits from a [variety of handy aliases](#-usage).

`boat` stores its data in a SQLite database file which is kept in the config directory by default (`.config/boat/boat.db`).

This repository contains only the code for the command line application.
It relies on [`boat-lib`](https://github.com/coko7/boat-lib) for core functions.

[![Crates info](https://img.shields.io/crates/v/boat-cli.svg)](https://crates.io/crates/boat-cli)
[![License: GPL-3.0](https://img.shields.io/github/license/coko7/boat-cli?color=blue)](LICENSE)
Expand All @@ -15,11 +23,19 @@ This is only the code for the command line application. It relies on [`boat-lib`
> This cli is actively being developed. Since it's in its early stages, things will likely break often.
> Don't use it for now.

## Contents

- [🤔 Why was this tool created?](#🤔-why-was-this-tool-created)
- [🛠️ Installation](#🛠️-installation)
- [Install with a bundled version of SQLite](#install-with-a-bundled-version-of-sqlite)
- [⚙️ Configuration](#⚙️-configuration)
- [✨ Usage](#✨-usage)

## 🤔 Why was this tool created?

The [`bartib`](https://github.com/nikolassv/bartib) cli is what inspired me to create `boat`.
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).
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.
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.

## 🛠️ Installation

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

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

## ✨ Usage

To get a feel of how `boat` can be used, you can try `boat help` to get the list of commands:
If you have ever used [`bartib`](https://github.com/nikolassv/bartib), then `boat` is going to feel very familiar.
Try `boat help` for a quick list of commands:
```help
boat 0.2.1
boat 0.5.0

Basic Opinionated Activity Tracker

Expand All @@ -78,11 +95,13 @@ boat <COMMAND>
Commands:
new Create a new activity
start Start/resume an activity
cancel Cancel the current activity
pause Pause/stop the current activity
modify Modify an activity
delete Delete an activity
get Get the current activity
list List boat objects
list List activities
query Query boat objects
help Print this message or the help of the given subcommand(s)

Options:
Expand All @@ -92,21 +111,25 @@ Options:
Made by @coko7 <contact@coko7.fr>
```

If you want to invoke `boat` from your command-line directly, you can make use of a variety of shorter aliases:
```help
Commands:
new n
start s, st, sail
config c, cfg, conf
pause p
modify m, mod
delete d, del
get g
list l, ls
help h, -h, --help
```
> [!TIP]
> `boat` comes bundled with many command aliases:
> - new: `n`, `new`, `create`
> - start: `s`, `st`, `start`, `sail`, `continue`, `resume`
> - cancel: `c`, `can`, `cancel`
> - pause: `p`, `pause`, `stop`
> - modify: `m`, `mod`, `modify`
> - delete: `d`, `del`, `delete`, `rm`, `rem`, `remove`
> - get: `g`, `get`
> - list: `l`, `ls`, `list`
> - query: `q`, `query`
> - help: `h`, `help`, `-h`, `--help`
>
> Prefer using the full length command names in scripts as they are more explicit and unlikely to be changed (unlike shorter aliases).

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.
That explains why some of the commands do not use a more fitting keyword.

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.
Maybe I will drop this in the future, let's see.

*I have included some fallback in case you type `stop`/`remove` instead of `pause`/`delete` 👀*
74 changes: 53 additions & 21 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,32 @@ use crate::utils;
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
//
// #[command(flatten)]
// pub verbose: clap_verbosity_flag::Verbosity,

#[command(flatten)]
pub verbose: clap_verbosity_flag::Verbosity,
}

#[derive(Subcommand)]
#[command(rename_all = "kebab-case")]
pub enum Commands {
/// Create a new activity
#[command(alias = "n")]
#[command(alias = "n", alias = "create")]
New(CreateActivityArgs),

// create a backup command
/// Start/resume an activity
#[command(alias = "s", alias = "st", alias = "sail")]
#[command(
alias = "s",
alias = "st",
alias = "sail",
alias = "continue",
alias = "resume"
)]
Start(SelectActivityArgs),

// /// Manage configuration
// #[command(alias = "c", alias = "cfg", alias = "conf")]
// Config {},
/// Cancel the current activity
#[command(alias = "c", alias = "can")]
Cancel,

/// Pause/stop the current activity
#[command(alias = "p", alias = "stop")]
Pause,
Expand All @@ -47,18 +53,28 @@ pub enum Commands {
Modify(ModifyActivityArgs),

/// Delete an activity
#[command(alias = "d", alias = "del")]
#[command(
alias = "d",
alias = "del",
alias = "rm",
alias = "rem",
alias = "remove"
)]
Delete(SelectActivityArgs),

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

/// List boat objects
/// List activities
#[command(alias = "l", alias = "ls")]
List {
List(ListActivityArgs),

/// Query boat objects
#[command(alias = "q")]
Query {
#[command(subcommand)]
command: ListSubcommand,
command: QuerySubcommand,
},

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

#[derive(Subcommand)]
#[command(rename_all = "kebab-case")]
pub enum ListSubcommand {
/// List activity logs
pub enum QuerySubcommand {
/// Manage logs
#[command(name = "logs", alias = "l", alias = "log")]
Logs(ListActivityArgs),

/// List activities
/// Manage activities
#[command(
name = "acts",
alias = "act",
Expand All @@ -99,15 +115,15 @@ pub enum ListSubcommand {
)]
Activities(ListArgs),

/// List tags
/// Manage tags
#[command(name = "tags", alias = "t", alias = "tag")]
Tags(ListArgs),
}

#[derive(Args, Debug)]
pub struct ListActivityArgs {
/// Restrict to entries starting in the given <PERIOD>
#[arg(short = 'p', long = "period", value_name = "PERIOD", default_value_t = Period::Today, value_enum, conflicts_with_all = ["from", "to", "date"])]
#[arg(short = 'p', long = "period", value_name = "PERIOD", default_value_t = Period::ThisWeek, value_enum, conflicts_with_all = ["from", "to", "date"])]
pub period: Period,

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

/// Only show activities, do not include their respective logs
#[arg(short = 'a', long = "activities-only", conflicts_with = "no_grouping")]
pub activities_only: bool,

/// Do not group activities by date
#[arg(short = 'n', long = "no-grouping", conflicts_with = "activities_only")]
pub no_grouping: bool,

/// Output in JSON
#[arg(short = 'j', long = "json")]
pub use_json_format: bool,
Expand All @@ -136,11 +160,11 @@ pub struct ListArgs {

#[derive(ValueEnum, Clone, Debug)]
pub enum Period {
#[value(name = "today", alias = "td")]
#[value(name = "today", alias = "td", alias = "tod")]
Today,
#[value(name = "yesterday", alias = "yd", alias = "ytd")]
Yesterday,
#[value(name = "this-week", alias = "tw", alias = "twk")]
#[value(name = "this-week", alias = "tw", alias = "twk", alias = "wk")]
ThisWeek,
#[value(
name = "last-week",
Expand All @@ -151,7 +175,7 @@ pub enum Period {
alias = "ywk"
)]
LastWeek,
#[value(name = "this-month", alias = "tm", alias = "tmo")]
#[value(name = "this-month", alias = "tm", alias = "tmo", alias = "mo")]
ThisMonth,
#[value(
name = "last-month",
Expand Down Expand Up @@ -189,6 +213,10 @@ impl Default for PrintActivityArgs {
pub struct SelectActivityArgs {
/// ID of the activity
pub activity_id: Id,

/// Output in JSON
#[arg(short = 'j', long = "json")]
pub use_json_format: bool,
}

#[derive(Args, Debug)]
Expand All @@ -207,6 +235,10 @@ pub struct CreateActivityArgs {
/// Start the new activity automatically
#[arg(short = 's', long = "start")]
pub auto_start: bool,

/// Output in JSON
#[arg(short = 'j', long = "json")]
pub use_json_format: bool,
}

#[derive(Args, Debug)]
Expand Down
22 changes: 21 additions & 1 deletion src/commands/activity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use rusqlite::Connection;

use crate::{
cli,
models::{TablePrintable, activity_log::PrintableActivityLog},
models::{TablePrintable, activity::PrintableActivity, activity_log::PrintableActivityLog},
};

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

let act = PrintableActivity::from_activity(&created);
if args.use_json_format {
let json = serde_json::to_string(&act)?;
println!("{json}");
return Ok(());
}

println!("{}", created.id);
Ok(())
}
Expand Down Expand Up @@ -90,3 +97,16 @@ pub fn get_current(conn: &mut Connection, args: &cli::PrintActivityArgs) -> Resu
}
Ok(())
}

pub fn cancel_current(conn: &mut Connection) -> Result<()> {
match activities::get_current_ongoing(conn)? {
Some(act) => {
activities::cancel_current(conn)?;
println!("cancelled activity: {act:?}");
}
None => {
println!("no current activity");
}
}
Ok(())
}
Loading
Loading