Compare commits

...

110 Commits
v1.3.1 ... main

Author SHA1 Message Date
Abraham Toriz 08ff8254d1
derive some defaults to make clippy happy 2023-03-14 16:04:57 -05:00
Abraham Toriz 22d16ff5ba separate cli definition into its own file 2023-03-11 19:29:09 -06:00
Abraham Toriz 530dd8dc15
simpler way of specifying no command 2023-02-13 19:58:13 -06:00
Abraham Toriz 858302839c
upgrade clap to v3 2023-02-13 18:17:47 -06:00
Abraham Toriz 56393636c9
change wording in readme 2023-02-03 12:17:09 -06:00
Abraham Toriz 518fe26c82
fix changelog 2022-11-26 21:16:46 -06:00
Abraham Toriz dd6b9f9a8d
Redefined CI and smaller features 2022-11-26 21:01:52 -06:00
Abraham Toriz 6afb517535
changelog and docs for 1.6 2022-11-26 21:00:11 -06:00
Abraham Toriz 3f83485769
another two paths were incorrect 2022-11-26 19:56:51 -06:00
Abraham Toriz 5d8d80fcfd
condense podman command lines in scripts 2022-11-26 19:43:38 -06:00
Abraham Toriz 4d1e3537df
fix package name again in archlinux-bin 2022-11-26 19:26:40 -06:00
Abraham Toriz a8990533bc
changelog for new release 2022-11-26 18:57:12 -06:00
Abraham Toriz 9cc6a88de6
a task for later 2022-11-26 18:54:26 -06:00
Abraham Toriz 5ae5e32cfb
fix variable names in CI 2022-11-26 18:54:17 -06:00
Abraham Toriz d56ef65f73
add a (un)install script to package 2022-11-26 18:52:41 -06:00
Abraham Toriz c9f5782f59
fix path to artifacts in CI 2022-11-26 18:23:55 -06:00
Abraham Toriz 889295fd58
finish restructure of the CI pipelines 2022-11-26 18:00:47 -06:00
Abraham Toriz b3959782b1
dont run test and build in different steps 2022-11-26 01:20:02 -06:00
Abraham Toriz ec87086b26
container accomplished 2022-11-26 01:15:48 -06:00
Abraham Toriz c939ae5753
next task 2022-11-26 01:07:56 -06:00
Abraham Toriz 1e68467d36
a container image capable of building tiempo consistently 2022-11-26 01:07:47 -06:00
Abraham Toriz 2dc6cda4f2
move my TODO list to the readme 2022-11-26 00:07:02 -06:00
Abraham Toriz 709cfda05b
actually display the timesheets after the charts so it is always visible 2022-11-25 09:09:23 -06:00
Abraham Toriz 83498ac654
display sheet name(s) in chart formatter 2022-11-25 09:03:40 -06:00
Abraham Toriz b75b412c23
adjust release steps 2022-11-10 11:45:00 -06:00
Abraham Toriz 28c91053be
update changelog 2022-11-09 14:22:40 -06:00
Abraham Toriz 9b07bedf55
display a nicer message when no entries are running with t now 2022-11-09 14:19:11 -06:00
Abraham Toriz 57ec60226d
path in pipeline definition was wrong, again 2022-11-07 00:41:23 -06:00
Abraham Toriz c588db6b45
fix paths in aur-git package 2022-11-07 00:39:24 -06:00
Abraham Toriz e7d261f75f
aur-bin archive was missing the verification sum 2022-11-07 00:33:39 -06:00
Abraham Toriz 2f56a6ff79
bash completions were in the wrong place 2022-11-07 00:04:47 -06:00
Abraham Toriz 1c9527f3a0
put artifacts in a folder because gitlab is not rendering my variables anymore 2022-11-06 23:23:41 -06:00
Abraham Toriz 2948adac15
fix som paths in the debian archive 2022-11-06 22:02:41 -06:00
Abraham Toriz c6225f3821
fix latest ci pipeline 2022-11-06 21:29:33 -06:00
Abraham Toriz 425df3aeca
fix directory name in release CI 2022-11-06 21:28:43 -06:00
Abraham Toriz 37a779b0f1
publish completions 2022-11-06 21:11:53 -06:00
Abraham Toriz ec7375d33e
document --flat as a feature released in 1.5.3 2022-11-06 21:09:44 -06:00
Abraham Toriz 48a8fa22f2
explain how to install the binary in linux 2022-11-06 21:05:59 -06:00
Abraham Toriz 6496d684ff
add completions to debian, binary and arch packages 2022-11-06 20:46:45 -06:00
Abraham Toriz bc2b96d425
clippy lints 2022-11-05 20:34:35 -06:00
Abraham Toriz 3fa0509319
document --flat in changelog and man page 2022-11-04 22:46:19 -06:00
Abraham Toriz 9433903cdf
add zsh completion 2022-11-04 22:00:41 -06:00
Abraham Toriz cfb2989853
use list's --flat option for bash completion 2022-11-04 22:00:18 -06:00
Abraham Toriz 762520797a
add a --flat option to list and fish completion 2022-11-04 21:56:41 -06:00
Abraham Toriz a689a68267
normalize family -> variants in the docs 2022-10-31 12:29:25 -06:00
Abraham Toriz 540e5ce53f
deb and aur-git release fixes 2022-10-31 01:05:36 -06:00
Abraham Toriz 2da1708885
add missing python-tomlkit dependency to aur-git package 2022-10-31 01:05:14 -06:00
Abraham Toriz ca2ddbb8de
add man page to deb package 2022-10-31 01:04:42 -06:00
Abraham Toriz 3d587c9c61
fix ci steps ordering 2022-10-31 00:32:31 -06:00
Abraham Toriz 44137b835a
build-doc must be a step prior to build 2022-10-31 00:32:11 -06:00
Abraham Toriz 7d4e4645fc
shorten publish command 2022-10-31 00:21:47 -06:00
Abraham Toriz bcc929dbc0
First release to include man page 2022-10-31 00:15:00 -06:00
Abraham Toriz 1b6077b1a0
some clippy lints 2022-10-31 00:03:11 -06:00
Abraham Toriz aaf5f89c86
theres no master branch 2022-10-30 23:40:41 -06:00
Abraham Toriz f4e733f9b8
set pipeline to build docs on master 2022-10-30 23:38:19 -06:00
Abraham Toriz f3bdc85d4c
link to new docs in crates.io 2022-10-30 23:30:57 -06:00
Abraham Toriz 0eb601b466
remove old section of why rewriting 2022-10-30 23:25:49 -06:00
Abraham Toriz 610e3e7f9d
mention in readme that man page is not installed by cargo 2022-10-30 23:25:16 -06:00
Abraham Toriz 16970f7557
finish reshaping of the tutorial 2022-10-30 23:13:14 -06:00
Abraham Toriz aa6fca4245
remove duplicated word 2022-10-30 11:11:29 -06:00
Abraham Toriz 7c9334da5c
remove dashes from subcommand titles as they might be confusing 2022-10-30 11:11:11 -06:00
Abraham Toriz 45f83113ff
more style fixes 2022-10-29 10:05:55 -05:00
Abraham Toriz 4d96688b9b
normalize style, sorry :( 2022-10-28 18:50:50 -05:00
Abraham Toriz ed3d8a7ab5
command for running autobuild was wrong 2022-10-28 18:50:28 -05:00
perro tuerto 7ede6ff1da
Tutorial writing 2022-10-28 18:10:36 -05:00
Abraham Toriz 805bdd2de1
shorten sample url to not disturb mobile view 2022-09-30 11:16:54 -04:00
Abraham Toriz f032216e16
document files, paths and environment variables 2022-09-30 07:42:38 -04:00
Abraham Toriz e52aafaf7b
finish documenting settings 2022-09-30 07:19:40 -04:00
Abraham Toriz 3d63a581a4
improve example of custom formatter 2022-09-28 23:33:45 -04:00
Abraham Toriz 37893a9e9a
make subcommand table reference each subcommand in html version 2022-09-28 23:33:20 -04:00
Abraham Toriz d15f120ef9
dont suggest to file an issue if there is one 2022-09-28 15:00:40 -04:00
Abraham Toriz b515b0a317
document most settings 2022-09-27 22:47:54 -04:00
Abraham Toriz f3b4c99760
t-out, t-resume and t-sheet documented 2022-09-27 21:42:49 -04:00
Abraham Toriz 22510dfcd2
docs on how to build the docs and autoreload 2022-09-27 21:42:19 -04:00
Abraham Toriz 41a414d6d2
document t-list and t-now 2022-09-26 23:45:51 -04:00
Abraham Toriz bd1785058e
document t-archive and t-kill 2022-09-26 10:24:26 -04:00
Abraham Toriz 7115ddfe54
more docs 2022-09-25 23:35:09 -04:00
Abraham Toriz 5f7fc63ef4
document all display commands 2022-09-25 21:33:01 -04:00
Abraham Toriz 4c486ae888
add logo to docs 2022-09-25 21:32:43 -04:00
Abraham Toriz 49e068c5a3
more docs for some commands 2022-09-25 18:14:06 -04:00
Abraham Toriz 5e4fd1cb37
enhance description in the docs 2022-09-25 16:09:10 -04:00
Abraham Toriz 61f80f2be6
install all requirements.txt for building docs 2022-09-24 23:32:53 -04:00
Abraham Toriz 023f78bdae
dont run cargo in the man-page branch for now 2022-09-24 23:30:46 -04:00
Abraham Toriz a997e25a5d
use correct pipeline syntax 2022-09-24 23:29:03 -04:00
Abraham Toriz f742264e29
attempt to publish the docs 2022-09-24 23:25:29 -04:00
Abraham Toriz 6be61a26d0
explain how to test the man page 2022-09-20 10:52:42 -04:00
Abraham Toriz b617257f0c
define the structure of tiempo's man page 2022-09-20 10:45:27 -04:00
Abraham Toriz 78abfa16e6
adapt CI to include man file 2022-09-20 10:43:43 -04:00
Abraham Toriz 0bd576523b
list subcommands in docs 2022-09-18 23:24:44 -04:00
Abraham Toriz 2ca3189fe6
read version from Cargo.toml 2022-09-18 22:55:46 -04:00
Abraham Toriz efc5055262
move current docs from readme to docs 2022-09-18 22:54:56 -04:00
Abraham Toriz a41817f438
restart the docs 2022-09-17 23:22:22 -04:00
Abraham Toriz a61a44d130
fix config not being passed to custom formatters 2022-09-10 23:35:54 -04:00
Abraham Toriz 189ce814d0
restore ability to pass config to custom formatters 2022-09-10 23:34:59 -04:00
Abraham Toriz dbbeea9f78
document the different formatters 2022-08-31 08:44:55 -04:00
Abraham Toriz de1a4a2849
Chart formatter and per-command default formatters 2022-08-31 08:19:16 -04:00
Abraham Toriz 2ed3f84454
clippy, stop getting in my way, first warning 2022-08-30 09:33:39 -04:00
Abraham Toriz dd40c1acc8
clippy lints 2022-08-29 23:41:50 -04:00
Abraham Toriz 086feb0e0b
allow to set a per-command default formatter 2022-08-29 19:45:45 -04:00
Abraham Toriz 8ec245038f
fix message for selecting custom formatter 2022-08-29 19:05:02 -04:00
Abraham Toriz d2e35d8ba1
most tests of the chart formatter passing 2022-08-29 19:04:15 -04:00
Abraham Toriz bfef1004e3
explain in message how to silence timetrap warning 2022-08-29 19:03:11 -04:00
Abraham Toriz ca7c424b46
properly handle timezone in --interactive 2022-08-28 16:49:31 -04:00
Abraham Toriz 88371b312e
mostly defined the chart formatter 2022-08-28 16:48:47 -04:00
Abraham Toriz 8420e7f776
fix some output messages 2022-08-26 12:33:56 -04:00
Abraham Toriz f980bb89b5
fix bash-completions 2022-08-26 12:30:09 -04:00
Abraham Toriz 6d9fa0cf7b
add the long release command to the release directions 2022-08-04 19:07:19 +08:00
Abraham Toriz b4fb858208
List latest entries last in --interactive 2022-08-04 19:00:40 +08:00
Abraham Toriz f968e6605c
list entries by most recent last in --interactive 2022-08-04 18:51:17 +08:00
Abraham Toriz a9bfc1a95a
changelog for 1.3.1 2022-08-04 14:50:08 +08:00
71 changed files with 3179 additions and 1800 deletions

4
.gitignore vendored
View File

@ -4,3 +4,7 @@
/*.sqlite3
docs/*/build/
dev_config.toml
Pipfile*
artifacts/
build/
debian-package/

View File

@ -1,5 +1,6 @@
stages:
- test
- build-doc
- build
- upload
- release
@ -13,33 +14,45 @@ test:cargo:
- rustup component add clippy
- cargo clippy --all-targets --all-features -- -D warnings
- cargo test
rules:
- if: $CI_COMMIT_BRANCH == "main"
build-doc:
stage: build-doc
image: python:3.10
script:
- cd docs/
- pip install -r requirements.txt
- make html
- make man
- gzip build/man/tiempo.1
rules:
- if: $CI_COMMIT_BRANCH == "main"
artifacts:
paths:
- docs/build/html
- docs/build/man/tiempo.1.gz
publish-doc:
stage: release
image: kroniak/ssh-client
script:
- mkdir -p ~/.ssh
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
- eval $(ssh-agent -s)
- ssh-add <(echo "$PRIVATE_KEY")
- scp -r docs/build/html/* $SERVER_USER@$SERVER_HOST:$APP_PATH
rules:
- if: $CI_COMMIT_BRANCH == "main"
build:
stage: build
image: categulario/rust-cli-image:latest
image: categulario/tiempo-build-env:1.65
script:
# build the binary
- cargo build --locked --release
# create the tar package
- mkdir -p build
# move things to the build directory
- cp target/release/t build/t
- cp CHANGELOG.md build/CHANGELOG.md
- cp README.md build/README.md
- cp LICENSE build/LICENSE
# compress the tar file
- tar -cvzf tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz build/
# makes the debian archive
- ./debpackage.sh
# computes the sums
- sha256sum tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz > tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum
- sha256sum debian-package/tiempo_${CI_COMMIT_TAG:1}_amd64.deb > tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum
- ./scripts/build.sh
artifacts:
paths:
- tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz
- debian-package/tiempo_${CI_COMMIT_TAG:1}_amd64.deb
- tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum
- tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum
- artifacts/
rules:
- if: $CI_COMMIT_BRANCH
when: never
@ -58,10 +71,10 @@ upload:
when: never
- if: $CI_COMMIT_TAG =~ /^v*/
script:
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz ${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz'
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum ${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum'
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file debian-package/tiempo_${CI_COMMIT_TAG:1}_amd64.deb ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG:1}_amd64.deb'
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum'
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz ${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz'
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz.sum ${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz.sum'
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/tiempo_${CI_COMMIT_TAG}_amd64.deb ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb'
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/tiempo_${CI_COMMIT_TAG}_amd64.deb.sum ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb.sum'
release:
stage: release
@ -80,13 +93,13 @@ release:
assets:
links:
- name: 'Any linux binary'
url: '${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz'
url: '${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz'
- name: 'Any linux binary sha256 sum'
url: '${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum'
url: '${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz.sum'
- name: 'Debian archive'
url: '${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG:1}_amd64.deb'
url: '${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb'
- name: 'Debian archive sha256 sum'
url: '${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum'
url: '${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb.sum'
deploy:arch-bin:
stage: aur
@ -100,8 +113,17 @@ deploy:arch-bin:
# setup git, because we'll commit
- git config --global user.name "$COMMITER_NAME"
- git config --global user.email "$COMMITER_EMAIL"
# finally run the script
# Clone the repo
- git clone $BIN_REPO_URL tiempo-bin
# generate the PKGBUILD in the current directory
- scripts/release-aur-bin.sh
- mv PKGBUILD tiempo-bin/
- mv .SRCINFO tiempo-bin/
# commit
- cd tiempo-bin
- git add .
- git commit -m "Release version $CI_COMMIT_TAG"
- git push
rules:
- if: $CI_COMMIT_BRANCH
when: never
@ -119,23 +141,18 @@ deploy:arch-git:
# setup git, because we'll commit
- git config --global user.name "$COMMITER_NAME"
- git config --global user.email "$COMMITER_EMAIL"
# clone the repo
- git clone $GIT_REPO_URL tiempo-git
# finally run the script
- scripts/release-aur-git.sh
- mv PKGBUILD tiempo-git/
- mv .SRCINFO tiempo-git/
# and commit
- cd tiempo-git
- git add .
- git commit -m "Release version $VERSION"
- git push
rules:
- if: $CI_COMMIT_BRANCH
when: never
- if: $CI_COMMIT_TAG =~ /^v*/
# pages:
# image: python:3.8-alpine
# stage: deploy
# script:
# - pip install -U sphinx
# - mkdir -p public/{es,en}
# - sphinx-build -b html ./docs/es/source/ public/es
# - sphinx-build -b html ./docs/en/source/ public/en
# artifacts:
# paths:
# - public
# rules:
# - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH

View File

@ -1,5 +1,45 @@
# Changes
## 1.6
- `t now` displays a nicer message if no entries are running.
- `chart` formatter now shows the sheet name(s) that are being displayed.
- any-linux package now includes install script.
## 1.5.3
- added `--flat` option to `list` that displays only available sheet names one
per line
- completions for bash, fish and zsh added to the package
- binary package structure changed
## 1.5
- First release to include the man page. Documentation is now published in
https://tiempo.categulario.xyz
## 1.4.1
- Fix config not being passed to custom formatters
## 1.4.0
- Brand new `chart` formatter that shows your work history
- Fix bash-completions file
- Improve some output messages
- Fix timezone handling (in old databases) for --interactive
- Tell how to silence the timetrap warning as part of the warning :(
- Allow to set per-command default formatters
## 1.3.2
- Sort entries by most recent last in --interactive. Since most of the time you
are likely recovering a recent entry you'll find it faster.
## 1.3.1
- Set correctly the description and dependencies for .deb package
## 1.3.0
- Archive entries by the total sum of hours passing t-archive the --time

585
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[[bin]]
name = "t"
path = "src/main.rs"
path = "src/bin/t.rs"
[package]
name = "tiempo"
@ -9,15 +9,15 @@ authors = ["Abraham Toriz <categulario@gmail.com>"]
edition = "2021"
description = "A command line time tracker"
license = "GPL-3.0"
documentation = "https://gitlab.com/categulario/tiempo-rs"
documentation = "https://tiempo.categulario.xyz"
homepage = "https://gitlab.com/categulario/tiempo-rs"
version = "1.3.1"
version = "1.6.0"
default-run = "t"
[dependencies]
clap = "2"
thiserror = "1"
directories = "3"
serde_yaml = "0.8"
serde_yaml = "0.9"
toml = "0.5"
itertools = "0.10"
textwrap = "0.14"
@ -31,6 +31,10 @@ hostname = "0.3"
atty = "0.2"
timeago = "0.3"
[dependencies.clap]
version = "3"
features = ["cargo"]
[dependencies.chrono]
version = "0.4"
features = ["serde"]

12
Containerfile Normal file
View File

@ -0,0 +1,12 @@
# This Containerfile builds the tiempo-build-env container image used to build
# tiempo in CI and local environments.
FROM docker.io/rust:1.65
RUN apt-get update && apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
fakeroot \
python3-minimal \
python3-sphinx \
python3-tomlkit \
&& apt-get -q clean \
&& rm -rf /var/lib/apt/lists/*

356
README.md
View File

@ -1,29 +1,35 @@
# Tiempo
A [timetrap](https://github.com/samg/timetrap/) compatible command line time
tracking application.
tracking application. [Read the fine manual](https://tiempo.categulario.xyz).
## Installation
### For Archlinux (and derivatives) users
There are a binary and a source package in the AUR:
There are both binary and source packages in the AUR:
* [tiempo-bin](https://aur.archlinux.org/packages/tiempo-bin)
* [tiempo-git](https://aur.archlinux.org/packages/tiempo-git)
### For every other linux users
### For all other linux users
Go to [gitlab releases page](https://gitlab.com/categulario/tiempo-rs/-/releases)
and grab the latest binary for your linux. There is a `.deb` file for Debian and
Ubuntu as well as a binary for any `x86_64` Linux.
In the case of the tar archive you just need to run the included `install.sh`
script.
### For Rust developers
You have `cargo`! you can run:
cargo install tiempo
However that will not install the beautiful man page. Although you can still see
it at https://tiempo.categulario.xyz .
### For everyone else
You need to compile `tiempo` by yourself. But don't worry! It is not that hard.
@ -36,271 +42,6 @@ inside the repository. The binary will be named `t` (or `t.exe` if you use
windows) and it is located inside the `target/release` directory that was
created during compilation.
## Tutorial
First of all, you can abbreviate all commands to their first letter, so `t in`
and `t i` are equivalent.
### Managing entries
Register the start of an activity in the default timesheet with:
t in 'Doing some coding'
which sets the activity's start time to the current time. Later when you're done
use
t out
to mark it as finished. If you forgot to start the activity before you can do so
with:
t i --at '20 min ago'
the same applies for `t out`.
Edit an entry with
t edit [options]
where the options are
-i, --id <id:i> Alter entry with id <id> instead of the running entry
-s, --start <time:qs> Change the start time to <time>
-e, --end <time:qs> Change the end time to <time>
-a, --append Append to the current note instead of replacing it
the delimiter between appended notes is
configurable (see configure)
-m, --move <sheet> Move to another sheet
You can remove an entry with
t kill --id 123
or an entire timesheet with
t kill somesheet
check bellow to see how to get the ids.
### Displaying entries
At any point in time you can check your time spent in the current or other
timesheet with:
t display [options] [SHEET | all | full]
the available options are
-v, --ids Print database ids (for use with edit)
-s, --start <date:qs> Include entries that start on this date or later
-e, --end <date:qs> Include entries that start on this date or earlier
-f, --format <format> The output format. Valid built-in formats are
ical, csv, json, ids, factor, and text (default).
Check the docs on defining custom formats bellow.
-g, --grep <regexp> Include entries where the note matches this regexp
Some shortcuts available are:
`today` - Display entries that started today
t today [--ids] [--format FMT] [SHEET | all]
`yesterday` - Display entries that started yesterday
t yesterday [--ids] [--format FMT] [SHEET | all]
`week` - Entries of this week so far. The default start of the week is Monday
(configurable).
t week [--ids] [--end DATE] [--format FMT] [SHEET | all]
`month` - Entries of this month or a specified one.
t month [--ids] [--start MONTH] [--format FMT] [SHEET | all]
### Using different timesheets
You can organize your activities in different timesheets by first switching to
an existing one, then starting an activity:
t sheet somename
t in 'some activity'
which will also create the timesheet if it doesn't exist.
List all existing timesheets using
t list [all]
(defaults to not showing archive timesheets with names preceded by an
underscore)
### Advanced management
You can archive entries from a timesheet using:
t archive [--start DATE] [--end DATE] [SHEET]
which defaults to archiving all entries in the current sheet, or you can be more
specific using these options:
-s, --start <date:qs> Include entries that start on this date or later.
-e, --end <date:qs> Include entries that start on this date or earlier.
-g, --grep <regexp> Include entries where the note matches this regexp.
-t, --time <hours> Only archive up to `hours` hours.
This subcommand will move the selected entries to a hidden timesheet named
`_[SHEET]` (the name of the timesheet preceded by an underscore).
It is possible to access directly the sqlite database using
t backend
### Configuration
`tiempo` keeps a config file, whose location you can learn usign `t configure`.
It is also possible to edit the config file in-place passing arguments to
`t configure` like this:
t c --append-notes-delimiter ';'
it will print the resulting config file. Beware that it wont keep comments added
to the file.
## Specifying times
Some arguments accept a time as value, like `t in`'s `--at` or `t d --start`.
These are the accepted formats:
**Something similar to ISO format** will be parsed as a time in the computer's
timezone.
* `2021-01-13` a date
* `2019-05-03 11:13` a date with portions of a time
**ISO format with offset or UTC** will be parsed as a time in the specified
timezone. Use `Z` for `UTC` and an offset for everything else
* `2021-01-13Z`
* `2005-10-14 19:20:35+05:00`
**something that looks like an hour** will be parsed as a time in the current
day in the computer's timezone. Add `Z` or an offset to specify the timezone.
* `11:30`
* `23:50:45` (with seconds)
**some human times**, for now restricted to time ago:
* `an hour ago`
* `a minute ago`
* `50 min ago`
* `1h30m ago`
* `two hours thirty minutes ago`
## Custom formatters
You can implement your own formatters for all subcommands that display entries
(like `t display`, `t week` etc.). It is as easy as creating an executable file
written in any programming language (interpreted or compiled) and placing it in
a path listed in the config value for `formatter_search_paths`.
This executable will be given as standard input a csv stream with each row
representing a time entry with the same structure as the `csv` formatter output.
It will also be given a command line argument representing user settings for
this formatter stored in the config file and formatted as JSON.
### Example
Suppose we have this config file:
```toml
database_file = "/home/user/.config/tiempo/database.sqlite3"
round_in_seconds = 900
append_notes_delimiter = " "
formatter_search_paths = ["/home/user/.config/tiempo/formatters"]
default_formatter = "text"
auto_sheet = "dotfiles"
auto_sheet_search_paths = ["/home/user/.config/tiempo/auto_sheets"]
auto_checkout = false
require_note = true
week_start = "Monday"
[formatters.earnings]
hourly_rate = 300
currency = "USD"
```
then we can create the `earnings` formatter by placing the following file in
`/home/user/.config/tiempo/formatters/earnings`:
```python
#!/usr/bin/env python3
import sys
import json
import csv
from datetime import datetime, timezone
from datetime import timedelta
from math import ceil
config = json.loads(sys.argv[1])
reader = csv.DictReader(
sys.stdin,
fieldnames=['id', 'start', 'end', 'note', 'sheet'],
)
total = timedelta(seconds=0)
for line in reader:
start = datetime.strptime(line['start'], '%Y-%m-%dT%H:%M:%S.%fZ')
if not line['end']:
end = datetime.utcnow()
else:
end = datetime.strptime(line['end'], '%Y-%m-%dT%H:%M:%S.%fZ')
total += end - start
hours = total.total_seconds() / 3600
earnings = hours * config['hourly_rate']
currency = config['currency']
print(f'You have earned: ${earnings:.2f} {currency}')
```
Now if you run `t display -f earnings` you will get something like:
```
You have earned: 2400 USD
```
## Why did you write this instead of improving timetrap?
* timetrap is [hard to install](https://github.com/samg/timetrap/issues/176),
hard to keep [updated](https://github.com/samg/timetrap/issues/174) (because
of ruby). With tiempo you can get (or build) a binary, put it somewhere in
your `PATH`, and it will just work forever in that machine. I'm bundling
sqlite.
* timetrap is slow (no way around it, because of ruby), some commands take up to
a second. Tiempo always feels snappy.
* needed major refactor to fix the timezone problem (in a language I'm not
proficient with). I was aware of this problem and designed tiempo to store
timestamps in UTC while at the same time being able to work with a database
made by timetrap without messing up. And there are a lot of tests backing this
assertions.
### Other advantages
* Columns in the output are always aligned.
* Fixed some input inconsistencies.
* CLI interface is easier to discover (ask -h for any sub-command)
* End times are printed with +1d to indicate that the activity ended the next
day in the 'text' formatter.
* Solved some old issues in timetrap.
* Added new features!
## How to build
You need [rust](https://rustup.rs), then clone the repo and simply run
@ -335,44 +76,89 @@ and when I want to test some commands against such config file I just source it:
### Documentation
The docs are written using [sphinx](https://www.sphinx-doc.org/en/master/), so
first you need to install it somehow. Two options I can offer are:
The docs are written using [sphinx](https://www.sphinx-doc.org/en/master/). To
install the required dependencies enter the `docs` directory and create a virual
environment:
* using your computer's package manager. Install a package with a name similar
to `python-sphinx`.
* using [pipenv](https://duckduckgo.com/?t=ffab&q=pipenv&ia=web). Just ensure
you have python 3.9 on your computer, enter the `docs` directory and do
`pipenv install`.
virtualenv .venv
To build the docs just enter the `docs` directory and some of the language
directories (currently `es` or `en`) and run:
then activate it and install the dependencies:
source .venv/bin/activate
pip install -r requirements.txt
To build the docs just do:
make html
for the html version (output located at `docs/<lang>/build/html`), or
for the html version (output located at `docs/build/html`), or
make man
for the man page (output located at `docs/<lang>/build/man/tiempo.1`). If you
are using pipenv just prefix the commands with `pipenv run` or run `pipenv
shell` before running any command.
for the man page (output located at `docs/build/man/tiempo.1`). To test the man
page you can do:
The contents of the docs are located in `docs/<lang>/source/index.rst`,
man -l build/man/tiempo.1
To get a live-reloaded server with the html docs do:
sphinx-autobuild source/ build/html/
The contents of the man page are located in `docs/source/index.rst`,
formatted as
[reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html).
### Building the packages like in CI but locally
First pull the image:
podman pull tiempo-build-env
Or build it yourself from this repo:
podman build -t tiempo-build-env .
Then build the artifacts:
./scripts/podman-build.sh
To build the archlinux PKGBUILDs (depends on the artifacts folder created by the
previous command):
./scripts/podman-build-aur-bin.sh
./scripts/podman-build-aur-git.sh
Both of the previous commands produce PKGBUILDs in the current directory so if
you run both sequentially you'll lose the PKGBUILD of the first command.
## Special Thanks
To [timetrap](https://github.com/samg/timetrap) for existing, to
[samg](https://github.com/samg) for creating it. It is the tool I was looking
for and whose design I took as reference, keeping compatibility when possible.
## What I'm working on
(more or less ordered by priority)
* fix the pipeline because it's broken. The last release and its artifacts are
wrong.
* add an install/uninstall script to the any-linux package.
* finish the `summary` formatter.
* match formatters by prefix (so there's no need to type all of its name if the
prefix is unambiguous).
* let resume --interactive receive a string as the text for a new entry.
* add a command that opens the doc in the browser just like fish
* compile a package for windows in CI
## Release checklist
(mostly to remind myself)
* [ ] Ensure tests pass and that clippy doesn't complain
* [ ] Create an entry in `CHANGELOG.md` with the target version
* [ ] Add documentation about the new features
* [ ] Create an entry in `CHANGELOG.md` with the target version, commit it
* [ ] run `vbump`
* [ ] push commits and tags
* [ ] wait for release
* [ ] git push && git push --tags && cargo publish
* [ ] wait for release and then test the releases (aur bin and git and
packaged).

View File

@ -5,7 +5,7 @@ _tiempo ()
cmd="${COMP_WORDS[1]}"
if [[ ( $cmd = s* || $cmd = d* || $cmd = k* ) && "$COMP_CWORD" = 2 ]]; then
COMPREPLY=($(compgen -W "$(echo "select distinct sheet from entries where sheet not like '\_%';" | z b)" $cur))
COMPREPLY=($(compgen -W "$(t l --all --flat)" $cur))
return
elif [[ "$COMP_CWORD" = 1 ]]; then
CMDS="archive backend configure display edit in kill list now out resume sheet week month"
@ -13,4 +13,4 @@ _tiempo ()
fi
}
complete -F _tiempo 'z'
complete -F _tiempo 't'

16
completions/fish/t.fish Normal file
View File

@ -0,0 +1,16 @@
# tiempo does not accept files as arguments
complete --command t --no-files
# define all subcommands
set -l sheetcommandslong display kill sheet week month
set -l sheetcommandsshort d k s w m
set -l sheetcommandsall $sheetcommandslong $sheetcommandsshort
set -l commandslong archive backend configure edit in list now out resume
set -l commandsshort a b c e i l n o r
set -l subcommands $sheetcommandslong $commandslong $sheetcommandsshort $commandsshort
# add subcommands
complete --command t --condition "not __fish_seen_subcommand_from $subcommands" -a "$subcommands"
# complete sheet name
complete --command t --condition "__fish_seen_subcommand_from $sheetcommandsall" -a "(t l --all --flat)"

30
completions/zsh/_t Normal file
View File

@ -0,0 +1,30 @@
#compdef t
_t() {
local curcontext="$curcontext" state line
typeset -A opt_args
_arguments \
'1: :->t_command'\
'2: :->first_arg'
case $state in
t_command)
compadd "$@" archive backend configure display edit in kill\
list now out resume sheet week month
;;
first_arg)
# If the first argument starts with s or d (sheet or display),
# the second argument can be autocompleted to one of the existing
# non-archived sheets.
if [[ $words[2] == s* || $words[2] == d* ]]; then
t l --all --flat | while read sheet; do
compadd "$@" $sheet
done
fi
;;
esac
}
_t "$@"

1
docs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build/

View File

@ -1,12 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
sphinx = "*"
[dev-packages]
[requires]
python_version = "3.9"

268
docs/Pipfile.lock generated
View File

@ -1,268 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "e879b252b55733ab98d1dcd005f029f322788f15b4ee5dd35a2a3d7730680920"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"alabaster": {
"hashes": [
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
],
"version": "==0.7.12"
},
"babel": {
"hashes": [
"sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9",
"sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.9.1"
},
"certifi": {
"hashes": [
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
"version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
"sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0",
"sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"
],
"markers": "python_version >= '3'",
"version": "==2.0.7"
},
"docutils": {
"hashes": [
"sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
"sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.17.1"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"version": "==3.3"
},
"imagesize": {
"hashes": [
"sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1",
"sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.2.0"
},
"jinja2": {
"hashes": [
"sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45",
"sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.2"
},
"markupsafe": {
"hashes": [
"sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
"sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
"sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
"sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194",
"sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
"sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
"sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724",
"sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
"sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646",
"sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
"sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6",
"sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a",
"sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6",
"sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad",
"sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
"sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38",
"sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac",
"sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
"sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6",
"sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047",
"sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
"sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
"sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b",
"sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
"sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
"sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a",
"sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
"sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1",
"sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9",
"sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864",
"sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
"sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee",
"sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f",
"sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
"sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
"sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
"sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
"sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b",
"sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
"sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86",
"sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6",
"sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
"sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
"sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
"sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28",
"sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e",
"sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
"sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
"sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f",
"sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d",
"sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
"sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
"sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145",
"sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
"sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c",
"sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1",
"sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a",
"sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207",
"sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
"sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53",
"sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd",
"sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134",
"sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85",
"sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9",
"sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
"sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
"sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
"sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
"sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.1"
},
"packaging": {
"hashes": [
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
],
"markers": "python_version >= '3.6'",
"version": "==21.0"
},
"pygments": {
"hashes": [
"sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
"sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
],
"markers": "python_version >= '3.5'",
"version": "==2.10.0"
},
"pyparsing": {
"hashes": [
"sha256:84196357aa3566d64ad123d7a3c67b0e597a115c4934b097580e5ce220b91531",
"sha256:fd93fc45c47893c300bd98f5dd1b41c0e783eaeb727e7cea210dcc09d64ce7c3"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.1"
},
"pytz": {
"hashes": [
"sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c",
"sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"
],
"version": "==2021.3"
},
"requests": {
"hashes": [
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==2.26.0"
},
"snowballstemmer": {
"hashes": [
"sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2",
"sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"
],
"version": "==2.1.0"
},
"sphinx": {
"hashes": [
"sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6",
"sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0"
],
"index": "pypi",
"version": "==4.2.0"
},
"sphinxcontrib-applehelp": {
"hashes": [
"sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a",
"sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"
],
"markers": "python_version >= '3.5'",
"version": "==1.0.2"
},
"sphinxcontrib-devhelp": {
"hashes": [
"sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
"sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
],
"markers": "python_version >= '3.5'",
"version": "==1.0.2"
},
"sphinxcontrib-htmlhelp": {
"hashes": [
"sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07",
"sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.0"
},
"sphinxcontrib-jsmath": {
"hashes": [
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
],
"markers": "python_version >= '3.5'",
"version": "==1.0.1"
},
"sphinxcontrib-qthelp": {
"hashes": [
"sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
"sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
],
"markers": "python_version >= '3.5'",
"version": "==1.0.3"
},
"sphinxcontrib-serializinghtml": {
"hashes": [
"sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd",
"sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"
],
"markers": "python_version >= '3.5'",
"version": "==1.1.5"
},
"urllib3": {
"hashes": [
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.7"
}
},
"develop": {}
}

View File

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -1,12 +0,0 @@
Advanced Usage
==============
Subcommands
-----------
bla
Settings
--------
bla

View File

@ -1,62 +0,0 @@
Intro
=====
What is Tiempo
--------------
Tiempo is a command line time tracker. It helps you keep track of the time spent
in different activities optionally organized in projects or `sheets`. Tiempo is
also a `Timetrap`_ compatible command line time tracking application.
Why another time tracking instead of improving Timetrap?
* Timetrap is `hard to install`_, hard to keep `updated`_ (because of Ruby).
With Tiempo you can get or build a binary, put it somewhere, and it will
just work forever in that machine. I'm bundling SQLite.
* Timetrap is slow (no way around it, because of Ruby), some commands take up to
a second. Tiempo always feels snappy.
* Timetrap needed major refactor to fix the timezone problem (in a language I'm not
proficient with). I was aware of this problem and designed Tiempo to store
timestamps in UTC while at the same time being able to work with a database
made by Timetrap without messing up. And there are a lot of tests backing this
assertions.
Tiempo has other advantages:
* Fixed some input inconsistencies.
* Solved some old issues in Timetrap.
* Columns in the output are always aligned.
* CLI interface is easier to discover (ask ``-h`` for any sub-command).
* End times are printed with +1d to indicate that the activity ended the next
day in the 'text' formatter.
Installation
------------
Just do:
.. code:: bash
cargo install tiempo
If you use Arch Linux, install `tiempo-git` package from the `AUR`_, for example:
.. code:: bash
git clone https://aur.archlinux.org/tiempo-git.git && cd tiempo-git && makepkg -si
.. NOTE::
You need to install `Rust`_ and `Cargo`_. For more info `click here`_.
Quickstart
----------
TODO
.. _Timetrap: https://github.com/samg/timetrap/
.. _hard to install: https://github.com/samg/timetrap/issues/176
.. _updated: https://github.com/samg/timetrap/issues/174
.. _click here: https://doc.rust-lang.org/cargo/getting-started/installation.html
.. _Rust: https://rust-lang.org
.. _Cargo: https://doc.rust-lang.org/book/ch01-03-hello-cargo.html
.. _AUR: https://aur.archlinux.org/packages/tiempo-git/

View File

@ -1,55 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'tiempo'
copyright = '2021, Abbraham Toriz'
author = 'Abbraham Toriz'
# The full version, including alpha/beta/rc tags
release = 'v1.1.1'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

View File

@ -1,52 +0,0 @@
Tiempo docs
===========
.. toctree::
:maxdepth: 2
:caption: Contents:
basic_usage
advanced_usage
Tiempo is a command line time tracker. It helps you keep track of the time spent
in different activities optionally organized in projects or `sheets`. Tiempo is
also compatible with `Timetrap`_, its predecesor.
Installation
------------
Provided you have a `Rust`_ toolchain installed, just do:
.. code:: bash
cargo install tiempo
If you use Arch Linux, install ``tiempo-git`` package from the `AUR`_, for
example:
.. code:: bash
git clone https://aur.archlinux.org/tiempo-git.git && cd tiempo-git && makepkg -si
.. NOTE::
You need to install `Rust`_ and `Cargo`_. For more info `click here`_.
Quickstart
----------
Go `here`_...
Special Thanks
--------------
To `Timetrap`_ for existing, to `samg`_ for creating it. It is the tool I was
looking for and whose design I took as reference, keeping compatibility when
possible.
.. _Timetrap: https://github.com/samg/timetrap/
.. _samg: https://github.com/samg
.. _here: basic_usage.html#quickstart
.. _click here: https://doc.rust-lang.org/cargo/getting-started/installation.html
.. _Rust: https://rust-lang.org
.. _Cargo: https://doc.rust-lang.org/book/ch01-03-hello-cargo.html
.. _AUR: https://aur.archlinux.org/packages/tiempo-git/

View File

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -1,12 +0,0 @@
Advanced Usage
==============
Subcommands
-----------
bla
Settings
--------
bla

View File

@ -1,62 +0,0 @@
Introducción
============
¿Qué es Tiempo?
---------------
Tiempo es una herramienta de línea de comandos para rastrear el tiempo. Te ayuda a grabar
el tiempo empleado en diferentes actividades y de manera opcional te permite organizarlas
en proyectos o `sheets` (hojas). Tiempo es compatible con `Timetrap`_, su predecesor.
¿Por qué otro rastreador de tiempo en lugar de mejorar Timetrap?
* Timetrap es `difícil de instalar`_, difícil de mantenerlo `actualizado`_ (debido a Ruby).
Con Tiempo puedes obtener o compilar un binario, ponerlo donde sea y simplemente funcionará
en la máquina. Estoy embebiendo SQLite.
* Timetrap es lento (no hay modo de sacarle la vuelta, es por Ruby), algunos comandos
tardan hasta un segundo. Tiempo siempre se siente rápido.
* Timetrap necesitaba una refactorización mayor para arreglar un problema con el huso
horario (en un lenguaje en el que no soy experto). Estaba al tanto de este problema
y diseñé Tiempo para guardar marcas de tiempo en UTC así como es posible
trabajar con la base de datos hecha por Timetrap sin estropearla. Y muchas pruebas
se han realizado que respaldan esta afirmación.
Tiempo tiene otras ventajas:
* Arreglé algunas inconsistencias en el input.
* Resolví algunas incidencias antiguas de Timetrap.
* Las columnas del output están siempre alineadas.
* La interfaz de línea de comandos es más fácil de descubrir (pregunta ``-h`` para cualquier subcomando).
* Los tiempos de conclusión se imprimen con un +1d para indicar que la actividad acabó al siguiente
día.
Instalación
-----------
Si tienes instaladas las herramientas de `Rust`_, solo ejecuta:
.. code:: bash
cargo install tiempo
Si usas Arch Linux, instala el paquete `tiempo-git` desde `AUR`_, por ejemplo:
.. code:: bash
git clone https://aur.archlinux.org/tiempo-git.git && cd tiempo-git && makepkg -si
.. NOTE::
Requieres tener instalado `Rust`_ y `Cargo`_. Para más información haz `clic aquí`_.
Inicio rápido
-------------
TODO
.. _Timetrap: https://github.com/samg/timetrap/
.. _difícil de instalar: https://github.com/samg/timetrap/issues/176
.. _actualizado: https://github.com/samg/timetrap/issues/174
.. _clic aquí: https://doc.rust-lang.org/cargo/getting-started/installation.html
.. _Rust: https://rust-lang.org
.. _Cargo: https://doc.rust-lang.org/book/ch01-03-hello-cargo.html
.. _AUR: https://aur.archlinux.org/packages/tiempo-git/

View File

@ -1,62 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'tiempo'
copyright = '2021, Abraham Toriz'
author = 'Abraham Toriz'
# The full version, including alpha/beta/rc tags
release = 'v1.1.1'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'es'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

View File

@ -1,50 +0,0 @@
Documentación de Tiempo
=======================
.. toctree::
:maxdepth: 2
:caption: Contenidos:
basic_usage
advanced_usage
Tiempo es una herramienta de línea de comandos para rastrear el tiempo. Te ayuda a grabar
el tiempo empleado en diferentes actividades y de manera opcional te permite organizarlas
en proyectos o `sheets` (hojas). Tiempo es compatible con `Timetrap`_, su predecesor.
Instalación
-----------
Si tienes instaladas las herramientas de `Rust`_, solo ejecuta:
.. code:: bash
cargo install tiempo
Si usas Arch Linux, instala el paquete `tiempo-git` desde `AUR`_, por ejemplo:
.. code:: bash
git clone https://aur.archlinux.org/tiempo-git.git && cd tiempo-git && makepkg -si
.. NOTE::
Requieres tener instalado `Rust`_ y `Cargo`_. Para más información haz `clic aquí`_.
Inicio rápido
-------------
Ve `aquí`_...
Agradecimientos especiales
--------------------------
A `Timetrap`_ por existir y a `samg`_ por hacerlo. Esta herramienta es lo que estaba
buscando y a la que tomé de referencia, manteniendo compatibilidad en lo posible.
.. _Timetrap: https://github.com/samg/timetrap/
.. _samg: https://github.com/samg
.. _aquí: basic_usage.html#inicio-rapido
.. _clic aquí: https://doc.rust-lang.org/cargo/getting-started/installation.html
.. _Rust: https://rust-lang.org
.. _Cargo: https://doc.rust-lang.org/book/ch01-03-hello-cargo.html
.. _AUR: https://aur.archlinux.org/packages/tiempo-git/

View File

@ -10,8 +10,6 @@ if "%SPHINXBUILD%" == "" (
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
@ -25,6 +23,8 @@ if errorlevel 9009 (
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end

4
docs/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
sphinx
sphinx-autobuild
tomlkit
furo

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

34
docs/source/conf.py Normal file
View File

@ -0,0 +1,34 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
from pathlib import Path
from tomlkit import parse
# Make the Cargo.toml file the only source for 'version'
_cargo_toml = Path(__file__).resolve().parent.parent.parent / 'Cargo.toml'
project = 'Tiempo'
copyright = '2022, Categulario'
author = 'Categulario, Perrotuerto'
release = str(parse(open(_cargo_toml).read())['package']['version'])
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = []
templates_path = ['_templates']
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo'
html_static_path = ['_static']
html_logo = "_static/logo.png"

1117
docs/source/index.rst Normal file

File diff suppressed because it is too large Load Diff

45
scripts/build.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
# Fail if any of the commands fail
set -e
# Build the docs
cd docs/
make man
gzip -f build/man/tiempo.1
cd ..
# build the binary
rustup component add clippy
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo build --locked --release
# move binary
install -Dm755 target/release/t build/bin/t
# move documentation
install -Dm644 CHANGELOG.md build/share/doc/tiempo/CHANGELOG.md
install -Dm644 README.md build/share/doc/tiempo/README.md
install -Dm644 LICENSE build/share/doc/tiempo/LICENSE
# move man page
install -Dm644 docs/build/man/tiempo.1.gz build/share/man/man1/tiempo.1.gz
# move completions
install -Dm644 completions/bash/t build/share/bash-completion/completions/t
install -Dm644 completions/fish/t.fish build/share/fish/vendor_completions.d/t.fish
install -Dm644 completions/zsh/_t build/share/zsh/site-functions/_t
# move install scripts
install -Dm755 scripts/install.sh build/install.sh
install -Dm755 scripts/uninstall.sh build/uninstall.sh
# compress the tar file
tar -cvzf tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz build/
# makes the debian archive
./scripts/debpackage.sh
# computes the sums
sha256sum tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz > tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz.sum
sha256sum debian-package/tiempo_${CI_COMMIT_TAG}_amd64.deb > tiempo_${CI_COMMIT_TAG}_amd64.deb.sum
mkdir -p artifacts
mv tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz artifacts/
mv debian-package/tiempo_${CI_COMMIT_TAG}_amd64.deb artifacts/
mv tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz.sum artifacts/
mv tiempo_${CI_COMMIT_TAG}_amd64.deb.sum artifacts/

View File

@ -20,7 +20,6 @@ DPKG_DIR="${DPKG_STAGING}/dpkg"
PROJECT_MANTAINER="Abraham Toriz Cruz"
PROJECT_HOMEPAGE="https://gitlab.com/categulario/tiempo-rs"
PROJECT_NAME=tiempo
PROJECT_VERSION=${CI_COMMIT_TAG:1}
PROJECT_BINARY=t
PROJECT_DESCRIPTION="A command line time tracking application"
@ -28,9 +27,9 @@ mkdir -p "${DPKG_DIR}"
DPKG_BASENAME=${PROJECT_NAME}
DPKG_CONFLICTS=
DPKG_VERSION=${PROJECT_VERSION}
DPKG_VERSION=${CI_COMMIT_TAG:1}
DPKG_ARCH=amd64
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
DPKG_NAME="${DPKG_BASENAME}_${CI_COMMIT_TAG}_${DPKG_ARCH}.deb"
DPKG_DEPENDS=
DPKG_SECTION=utils
@ -39,8 +38,11 @@ install -Dm755 "target/release/$PROJECT_BINARY" "${DPKG_DIR}/usr/bin/$PROJECT_BI
# README and LICENSE
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
install -Dm644 "LICENSE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE"
install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
gzip -n --best "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/CHANGELOG.md"
install -Dm644 "docs/build/man/$PROJECT_NAME.1.gz" "${DPKG_DIR}/usr/share/man/man1/$PROJECT_NAME.1.gz"
install -Dm644 "completions/bash/t" "${DPKG_DIR}/usr/share/bash-completion/completions/t"
install -Dm644 "completions/fish/t.fish" "${DPKG_DIR}/usr/share/fish/vendor_completions.d/t.fish"
install -Dm644 "completions/zsh/_t" "${DPKG_DIR}/usr/share/zsh/site-functions/_t"
cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/

13
scripts/install.sh Normal file
View File

@ -0,0 +1,13 @@
#!/bin/bash
__dir=$(dirname $(realpath $0))
prefix=${1:-/usr}
install -Dm755 $__dir/bin/t $prefix/bin/t
install -Dm644 $__dir/share/fish/vendor_completions.d/t.fish $prefix/share/fish/vendor_completions.d/t.fish
install -Dm644 $__dir/share/doc/tiempo/README.md $prefix/share/doc/tiempo/README.md
install -Dm644 $__dir/share/doc/tiempo/CHANGELOG.md $prefix/share/doc/tiempo/CHANGELOG.md
install -Dm644 $__dir/share/doc/tiempo/LICENSE $prefix/share/doc/tiempo/LICENSE
install -Dm644 $__dir/share/bash-completion/completions/t $prefix/share/bash-completion/completions/t
install -Dm644 $__dir/share/zsh/site-functions/_t $prefix/share/zsh/site-functions/_t
install -Dm644 $__dir/share/man/man1/tiempo.1.gz $prefix/share/man/man1/tiempo.1.gz

14
scripts/podman-build-aur-bin.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
if [[ -z $1 ]]; then
echo "please pass a tag as first argument. Something like v1.6.0"
exit 1
fi
podman run -it --rm \
--workdir /app \
--volume ./:/app \
--env CI_COMMIT_TAG=$1 \
--env CI_PROJECT_ID=27545092 \
--uidmap 1000:0:1 --uidmap 0:1:1000 \
categulario/makepkg ./scripts/release-aur-bin.sh

13
scripts/podman-build-aur-git.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
if [[ -z $1 ]]; then
echo "please pass a tag as first argument. Something like v1.6.0"
exit 1
fi
podman run -it --rm \
--workdir /app \
--volume ./:/app \
--env CI_COMMIT_TAG=$1 \
--uidmap 1000:0:1 --uidmap 0:1:1000 \
categulario/makepkg ./scripts/release-aur-git.sh

13
scripts/podman-build.sh Normal file
View File

@ -0,0 +1,13 @@
#!/bin/bash
if [[ -z $1 ]]; then
echo "please pass a tag as first argument. Something like v1.6.0"
exit 1
fi
podman run -it --rm \
--workdir /app \
--volume ./:/app \
--volume tiempo_cargo_index:/usr/local/cargo/registry \
--env CI_COMMIT_TAG=$1 \
tiempo-build-env ./scripts/build.sh

View File

@ -1,19 +1,15 @@
#!/bin/bash
set -e
# some useful variables
VERSION=${CI_COMMIT_TAG:1}
PROJECT_NAME=tiempo
PROJECT_BINARY=t
ARCHIVENAME=$PROJECT_NAME-$VERSION-x86_64.tar.gz
# clone the repo
git clone $BIN_REPO_URL $PROJECT_NAME-bin
# enter it
cd $PROJECT_NAME-bin
ARCHIVENAME=$PROJECT_NAME-$CI_COMMIT_TAG-x86_64.tar.gz
# get the sum from the artifacts
SUM=( `cat ../$ARCHIVENAME.sum` )
SUM=( `cat artifacts/$ARCHIVENAME.sum` )
# Generate the PKGBUILD
echo "# Maintainer: Abraham Toriz <categulario at gmail dot com>
@ -28,20 +24,21 @@ depends=()
optdepends=('sqlite: for manually editing the database')
provides=('$PROJECT_NAME')
conflicts=('$PROJECT_NAME')
source=(\"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/generic/v$VERSION/v$VERSION/$PROJECT_NAME-\${pkgver}-x86_64.tar.gz\")
source=(\"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/generic/$CI_COMMIT_TAG/$CI_COMMIT_TAG/$PROJECT_NAME-v\${pkgver}-x86_64.tar.gz\")
sha256sums=('$SUM')
package() {
cd \"\$srcdir/build\"
install -Dm755 $PROJECT_BINARY \"\$pkgdir\"/usr/bin/$PROJECT_BINARY
install -Dm755 bin/$PROJECT_BINARY \"\$pkgdir\"/usr/bin/$PROJECT_BINARY
install -Dm644 README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md
install -Dm644 LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE
install -Dm644 CHANGELOG.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/CHANGELOG.md
install -Dm644 share/doc/$PROJECT_NAME/README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md
install -Dm644 share/doc/$PROJECT_NAME/LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE
install -Dm644 share/doc/$PROJECT_NAME/CHANGELOG.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/CHANGELOG.md
install -Dm644 share/man/man1/$PROJECT_NAME.1.gz \"\$pkgdir\"/usr/share/man/man1/$PROJECT_NAME.1.gz
install -Dm644 share/bash-completion/completions/t \"\$pkgdir\"/usr/share/bash-completion/completions/t
install -Dm644 share/fish/vendor_completions.d/t.fish \"\$pkgdir\"/usr/share/fish/vendor_completions.d/t.fish
install -Dm644 share/zsh/site-functions/_t \"\$pkgdir\"/usr/share/zsh/site-functions/_t
}
" | tee PKGBUILD > /dev/null
makepkg --printsrcinfo > .SRCINFO
git add .
git commit -m "Release version $VERSION"
git push

View File

@ -5,12 +5,6 @@ VERSION=${CI_COMMIT_TAG:1}
PROJECT_NAME=tiempo
PROJECT_BINARY=t
# clone the repo
git clone $GIT_REPO_URL $PROJECT_NAME-git
# enter it
cd $PROJECT_NAME-git
echo "# Maintainer: Abraham Toriz <categulario at gmail dot com>
pkgname=$PROJECT_NAME-git
pkgver=$VERSION
@ -21,7 +15,7 @@ url='https://gitlab.com/categulario/tiempo-rs'
license=('GPL3')
depends=()
optdepends=('sqlite: for manually editing the database')
makedepends=('cargo' 'git')
makedepends=('cargo' 'git' 'python-sphinx' 'python-tomlkit' 'gzip' 'make')
provides=('$PROJECT_NAME')
conflicts=('$PROJECT_NAME')
source=('$PROJECT_NAME-git::git+https://gitlab.com/categulario/tiempo-rs')
@ -35,6 +29,9 @@ pkgver() {
build() {
cd \"\$pkgname\"
cargo build --release --locked
cd docs
make man
gzip build/man/$PROJECT_NAME.1
}
package() {
@ -44,9 +41,11 @@ package() {
install -Dm644 README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md
install -Dm644 LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE
install -Dm644 CHANGELOG.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/CHANGELOG.md
install -Dm644 docs/build/man/$PROJECT_NAME.1.gz \"\$pkgdir\"/usr/share/man/man1/$PROJECT_NAME.1.gz
install -Dm644 completions/bash/t \"\$pkgdir\"/usr/share/bash-completion/completions/t
install -Dm644 completions/fish/t.fish \"\$pkgdir\"/usr/share/fish/vendor_completions.d/t.fish
install -Dm644 completions/zsh/_t \"\$pkgdir\"/usr/share/zsh/site-functions/_t
}" | tee PKGBUILD > /dev/null
makepkg --printsrcinfo > .SRCINFO
git add .
git commit -m "Release version $VERSION"
git push

12
scripts/uninstall.sh Normal file
View File

@ -0,0 +1,12 @@
#!/bin/bash
rm -f /usr/bin/t
prefix=${1:-/usr}
rm -f $prefix/bin/t
rm -rf $prefix/share/doc/tiempo/
rm -f $prefix/share/fish/vendor_completions.d/t.fish
rm -f $prefix/share/bash-completion/completions/t
rm -f $prefix/share/zsh/site-functions/_t
rm -f $prefix/share/man/man1/tiempo.1.gz

80
src/bin/t.rs Normal file
View File

@ -0,0 +1,80 @@
use std::convert::TryInto;
use std::process::exit;
use std::io;
use clap::ArgMatches;
use chrono::Utc;
use regex::Regex;
use lazy_static::lazy_static;
use tiempo::error;
use tiempo::database::SqliteDatabase;
use tiempo::env::Env;
use tiempo::config::Config;
use tiempo::commands::{
Command, Facts, r#in::InCommand, display::DisplayCommand,
sheet::SheetCommand, today::TodayCommand, yesterday::YesterdayCommand,
week::WeekCommand, month::MonthCommand, list::ListCommand, out::OutCommand,
resume::ResumeCommand, backend::BackendCommand, kill::KillCommand,
now::NowCommand, edit::EditCommand, archive::ArchiveCommand,
configure::ConfigureCommand,
};
use tiempo::io::Streams;
use tiempo::cli::make_cli;
lazy_static! {
// https://regex101.com/r/V9zYQu/1/
pub static ref NUMBER_RE: Regex = Regex::new(r"^\d+$").unwrap();
}
fn error_trap(matches: ArgMatches) -> error::Result<()> {
let env = Env::read();
let facts = Facts {
config: Config::read(env.timetrap_config_file.as_deref())?,
env,
now: Utc::now(),
};
if let Some(_matches) = matches.subcommand_matches("backend") {
return BackendCommand::handle(&facts.config);
}
let mut streams = Streams {
db: SqliteDatabase::from_path_or_create(&facts.config.database_file)?,
r#in: io::BufReader::new(io::stdin()),
out: io::stdout(),
err: io::stderr(),
};
match matches.subcommand() {
Some(("in", matches)) => InCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("out", matches)) => OutCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("resume", matches)) => ResumeCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("display", matches)) => DisplayCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("today", matches)) => TodayCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("yesterday", matches)) => YesterdayCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("week", matches)) => WeekCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("month", matches)) => MonthCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("sheet", matches)) => SheetCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("list", matches)) => ListCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("kill", matches)) => KillCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("now", matches)) => NowCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("edit", matches)) => EditCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("archive", matches)) => ArchiveCommand::handle(matches.try_into()?, &mut streams, &facts),
Some(("configure", matches)) => ConfigureCommand::handle(matches.try_into()?, &mut streams, &facts),
Some((cmd, _)) => Err(error::Error::UnimplementedCommand(cmd.into())),
None => Err(error::Error::MissingSubcommand),
}
}
fn main() {
let matches = make_cli().get_matches();
if let Err(e) = error_trap(matches) {
eprintln!("{}", e);
exit(1);
}
}

View File

@ -1,112 +1,36 @@
use std::convert::TryInto;
use std::process::exit;
use std::io;
use clap::{
App, Arg, SubCommand, AppSettings, ArgMatches, crate_version, crate_authors,
crate_description, crate_name,
Command, Arg, SubCommand, command, value_parser,
};
use chrono::Utc;
use regex::Regex;
use lazy_static::lazy_static;
use tiempo::error;
use tiempo::database::SqliteDatabase;
use tiempo::env::Env;
use tiempo::config::Config;
use tiempo::commands::{
Command, Facts, r#in::InCommand, display::DisplayCommand,
sheet::SheetCommand, today::TodayCommand, yesterday::YesterdayCommand,
week::WeekCommand, month::MonthCommand, list::ListCommand, out::OutCommand,
resume::ResumeCommand, backend::BackendCommand, kill::KillCommand,
now::NowCommand, edit::EditCommand, archive::ArchiveCommand,
configure::ConfigureCommand,
};
use tiempo::io::Streams;
lazy_static! {
// https://regex101.com/r/V9zYQu/1/
pub static ref NUMBER_RE: Regex = Regex::new(r"^\d+$").unwrap();
}
fn is_number(v: String) -> Result<(), String>{
if NUMBER_RE.is_match(&v) {
Ok(())
} else {
Err(format!("'{}' is not a valid number", v))
}
}
fn error_trap(matches: ArgMatches) -> error::Result<()> {
let env = Env::read();
let facts = Facts {
config: Config::read(env.timetrap_config_file.as_deref())?,
env,
now: Utc::now(),
};
if let Some(_matches) = matches.subcommand_matches("backend") {
return BackendCommand::handle(&facts.config);
}
let mut streams = Streams {
db: SqliteDatabase::from_path_or_create(&facts.config.database_file)?,
r#in: io::BufReader::new(io::stdin()),
out: io::stdout(),
err: io::stderr(),
};
match matches.subcommand() {
("in", Some(matches)) => InCommand::handle(matches.try_into()?, &mut streams, &facts),
("out", Some(matches)) => OutCommand::handle(matches.try_into()?, &mut streams, &facts),
("resume", Some(matches)) => ResumeCommand::handle(matches.try_into()?, &mut streams, &facts),
("display", Some(matches)) => DisplayCommand::handle(matches.try_into()?, &mut streams, &facts),
("today", Some(matches)) => TodayCommand::handle(matches.try_into()?, &mut streams, &facts),
("yesterday", Some(matches)) => YesterdayCommand::handle(matches.try_into()?, &mut streams, &facts),
("week", Some(matches)) => WeekCommand::handle(matches.try_into()?, &mut streams, &facts),
("month", Some(matches)) => MonthCommand::handle(matches.try_into()?, &mut streams, &facts),
("sheet", Some(matches)) => SheetCommand::handle(matches.try_into()?, &mut streams, &facts),
("list", Some(matches)) => ListCommand::handle(matches.try_into()?, &mut streams, &facts),
("kill", Some(matches)) => KillCommand::handle(matches.try_into()?, &mut streams, &facts),
("now", Some(matches)) => NowCommand::handle(matches.try_into()?, &mut streams, &facts),
("edit", Some(matches)) => EditCommand::handle(matches.try_into()?, &mut streams, &facts),
("archive", Some(matches)) => ArchiveCommand::handle(matches.try_into()?, &mut streams, &facts),
("configure", Some(matches)) => ConfigureCommand::handle(matches.try_into()?, &mut streams, &facts),
(cmd, _) => Err(error::Error::UnimplementedCommand(cmd.into())),
}
}
fn main() {
// Lets first declar some args that repeat here and there
pub fn make_cli() -> Command<'static> {
// Let's first declare some args that repeat here and there
let start_arg = Arg::with_name("start")
.long("start").short("s")
.long("start").short('s')
.takes_value(true).value_name("TIME")
.help("Include entries that start on this date or later");
let end_arg = Arg::with_name("end")
.long("end").short("e")
.long("end").short('e')
.takes_value(true).value_name("TIME")
.help("Include entries that start on this date or earlier");
let ids_arg = Arg::with_name("ids")
.short("v").long("ids")
.short('v').long("ids")
.help("Print database ids (for use with edit)");
let grep_arg = Arg::with_name("grep")
.long("grep").short("g")
.long("grep").short('g')
.takes_value(true).value_name("REGEXP")
.help("Include entries where the note matches this regexp.");
.help("Only include entries whose note matches this regular expression");
let format_arg = Arg::with_name("format")
.short("f").long("format")
.short('f').long("format")
.takes_value(true).value_name("FORMAT")
.help(
"The output format. Valid built-in formats are ical, csv, json, \
ids, and text. Documentation on defining custom formats can be \
found at https://gitlab.com/categulario/tiempo"
"The output format. Valid built-in formats are chart, text, ical, \
csv, json and ids. Documentation on defining custom formats can be \
found at https://tiempo.categulario.xyz or the man page included \
with the installation."
);
let sheet_arg = Arg::with_name("sheet")
@ -124,22 +48,18 @@ fn main() {
let id_arg = Arg::with_name("id")
.long("id")
.takes_value(true).value_name("ID")
.validator(is_number);
.value_parser(value_parser!(u64));
let interactive_arg = Arg::with_name("interactive")
.short("i")
.short('i')
.long("interactive")
.takes_value(false)
.conflicts_with("id")
.help("Choose an entry of the (unique) last N interactively");
// Now declar this app's cli
let matches = App::new("Tiempo")
.name(crate_name!())
.setting(AppSettings::SubcommandRequired)
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
let cli = command!()
.subcommand_required(true)
.subcommand(SubCommand::with_name("archive")
.visible_alias("a")
@ -149,11 +69,11 @@ fn main() {
.arg(grep_arg.clone())
.arg(sheet_arg.clone().help("Archive entries from this sheet instead of the current one"))
.arg(Arg::with_name("fake")
.short("f").long("fake")
.short('f').long("fake")
.help("Don't actually archive the entries, just display them")
)
.arg(Arg::with_name("time")
.short("t").long("time")
.short('t').long("time")
.takes_value(true).value_name("HOURS")
.help("Time in hours to archive. Archived time will be equal or less than this.")
)
@ -171,7 +91,7 @@ fn main() {
.long("round-in-seconds")
.takes_value(true)
.value_name("SECONDS")
.validator(is_number)
.value_parser(value_parser!(u64))
.help("The duration of time to use for rounding with the -r flag. Default: 900 (15 m)"))
.arg(Arg::with_name("database_file")
.long("database-file")
@ -215,13 +135,13 @@ fn main() {
.long("week-start")
.takes_value(true)
.value_name("DAY")
.possible_values(&["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"])
.possible_values(["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"])
.help("The day of the week to use as the start of the week for t week. Default: monday"))
.arg(Arg::with_name("interactive_entries")
.long("interactive-entries")
.takes_value(true)
.value_name("N")
.validator(is_number)
.value_parser(value_parser!(u64))
.help("How many unique previous notes to show when selecting interactively"))
)
@ -276,10 +196,10 @@ fn main() {
.arg(grep_arg.clone())
.arg(sheet_arg.clone())
.arg(Arg::with_name("month")
.long("month").short("m")
.long("month").short('m')
.takes_value(true).value_name("TIME")
.aliases(&["s", "start"])
.possible_values(&[
.possible_values([
"this", "current", "last", "jan", "january", "feb",
"february", "mar", "march", "apr", "april", "may", "jun",
"june", "jul", "july", "aug", "august", "sep", "september",
@ -327,8 +247,11 @@ fn main() {
.visible_alias("l")
.about("List existing sheets")
.arg(Arg::with_name("all")
.short("a").long("all")
.short('a').long("all")
.help("List archive sheets also"))
.arg(Arg::with_name("flat")
.short('f').long("flat")
.help("show only the sheet names"))
)
.subcommand(SubCommand::with_name("kill")
@ -338,12 +261,12 @@ fn main() {
.arg(Arg::with_name("sheet")
.takes_value(true).value_name("SHEET")
.conflicts_with_all(&["id", "last"])
.required_unless_one(&["id", "last"])
.required_unless_one(["id", "last"])
.help(
"Delete an entire sheet by its name"
))
.arg(Arg::with_name("last")
.short("l").long("last")
.short('l').long("last")
.takes_value(false)
.help("Delete the last entry of the current sheet"))
)
@ -361,12 +284,12 @@ fn main() {
.arg(end_arg.clone().help("Set this as the end time"))
.arg(
Arg::with_name("append")
.long("append").short("z")
.long("append").short('z')
.help("Append to the current note instead of replacing it. The delimiter between appended notes is configurable (see configure)")
)
.arg(
Arg::with_name("move")
.short("m").long("move")
.short('m').long("move")
.takes_value(true)
.value_name("SHEET")
.help("Move entry to another sheet")
@ -377,12 +300,7 @@ fn main() {
.value_name("NOTE")
.help("The note text. It will replace the previous one unless --append is given")
)
)
);
.get_matches();
if let Err(e) = error_trap(matches) {
eprintln!("{}", e);
exit(1);
}
cli
}

View File

@ -64,7 +64,7 @@ impl Default for Facts {
}
pub trait Command<'a> {
type Args: TryFrom<&'a ArgMatches<'a>>;
type Args: TryFrom<&'a ArgMatches>;
fn handle<D: Database, I: BufRead, O: Write, E: Write>(args: Self::Args, streams: &mut Streams<D, I, O, E>, facts: &Facts) -> Result<()>;
}

View File

@ -26,7 +26,7 @@ pub struct Args {
sheet: Option<String>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
@ -274,22 +274,22 @@ mod tests {
fn entries_are_split_properly() {
let mut old_entry = Entry {
id: 1,
start: Utc.ymd(2022, 7, 29).and_hms(10, 0, 0),
end: Some(Utc.ymd(2022, 7, 29).and_hms(11, 0, 0)),
start: Utc.with_ymd_and_hms(2022, 7, 29, 10, 0, 0).unwrap(),
end: Some(Utc.with_ymd_and_hms(2022, 7, 29, 11, 0, 0).unwrap()),
note: Some("an entry".to_string()),
sheet: "foo".to_string(),
};
assert_eq!(split_entry(&mut old_entry, Duration::minutes(25)), (
Utc.ymd(2022, 7, 29).and_hms(10, 25, 0),
Some(Utc.ymd(2022, 7, 29).and_hms(11, 0, 0)),
Utc.with_ymd_and_hms(2022, 7, 29, 10, 25, 0).unwrap(),
Some(Utc.with_ymd_and_hms(2022, 7, 29, 11, 0, 0).unwrap()),
Some("an entry".to_string()),
"foo".to_string(),
));
assert_eq!(old_entry, Entry {
id: 1,
start: Utc.ymd(2022, 7, 29).and_hms(10, 0, 0),
end: Some(Utc.ymd(2022, 7, 29).and_hms(10, 25, 0)),
start: Utc.with_ymd_and_hms(2022, 7, 29, 10, 0, 0).unwrap(),
end: Some(Utc.with_ymd_and_hms(2022, 7, 29, 10, 25, 0).unwrap()),
note: Some("an entry".to_string()),
sheet: "foo".to_string(),
});
@ -394,7 +394,7 @@ Proceed? [y/N] ");
};
let mut streams = Streams::fake(b"y\n");
let facts = Facts::new();
let time_a = Utc.ymd(2022, 8, 1).and_hms(10, 0, 0);
let time_a = Utc.with_ymd_and_hms(2022, 8, 1, 10, 0, 0).unwrap();
let time_b = time_a + Duration::minutes(90);
let time_d = time_a + Duration::hours(3);

View File

@ -53,14 +53,14 @@ impl Args {
fn yes_no_none(matches: &ArgMatches, opt: &str) -> Option<bool> {
if matches.is_present(opt) {
Some(true)
} else if matches.is_present(&format!("no_{}", opt)) {
} else if matches.is_present(format!("no_{}", opt)) {
Some(false)
} else {
None
}
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> {

View File

@ -98,7 +98,7 @@ pub struct Args {
sheet: Option<Sheet>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -130,7 +130,7 @@ impl<'a> Command<'a> for DisplayCommand {
args.end,
args.sheet,
streams,
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
args.format.unwrap_or_else(|| facts.config.commands.display.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
args.ids,
args.grep,
facts
@ -144,7 +144,7 @@ mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use crate::database::SqliteDatabase;
use crate::config::Config;
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*;
@ -170,7 +170,7 @@ mod tests {
assert_eq!(
String::from_utf8_lossy(&streams.err),
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
);
}
@ -178,14 +178,14 @@ mod tests {
fn filter_by_start() {
let args = Args {
format: Some(Formatter::Csv),
start: Some(Utc.ymd(2021, 6, 30).and_hms(10, 5, 0)),
start: Some(Utc.with_ymd_and_hms(2021, 6, 30, 10, 5, 0).unwrap()),
..Default::default()
};
let mut streams = Streams::fake(b"");
let facts = Facts::new();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
@ -204,8 +204,8 @@ mod tests {
let mut streams = Streams::fake(b"");
let facts = Facts::new();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("adios".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("adios".into()), "default").unwrap();
entries_for_display(None, None, None, &mut streams, Formatter::Csv, true, Some("io".parse().unwrap()), &facts).unwrap();
@ -229,9 +229,9 @@ mod tests {
let facts = Facts::new();
std::env::set_var("TZ", "CST+6");
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "sheet2").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap()), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap()), None, "sheet2").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 13, 0, 0).unwrap()), None, "sheet1").unwrap();
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
@ -264,9 +264,9 @@ Timesheet: sheet2
let facts = Facts::new();
std::env::set_var("TZ", "CST+6");
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "_sheet2").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap()), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap()), None, "_sheet2").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 13, 0, 0).unwrap()), None, "sheet1").unwrap();
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
@ -296,8 +296,8 @@ Timesheet: sheet1
let args = Args {
format: Some(Formatter::Csv),
start: Some(Utc.ymd(2021, 6, 29).and_hms(12, 0, 0)),
end: Some(Utc.ymd(2021, 6, 29).and_hms(13, 0, 0)),
start: Some(Utc.with_ymd_and_hms(2021, 6, 29, 12, 0, 0).unwrap()),
end: Some(Utc.with_ymd_and_hms(2021, 6, 29, 13, 0, 0).unwrap()),
..Default::default()
};
let mut streams = Streams::fake(b"").with_db(
@ -317,7 +317,7 @@ Timesheet: sheet1
assert_eq!(
String::from_utf8_lossy(&streams.err),
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
);
}
@ -332,8 +332,33 @@ Timesheet: sheet1
..Default::default()
});
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
assert_eq!(String::from_utf8_lossy(&streams.err), "");
}
#[test]
fn respect_command_default_formatter() {
std::env::set_var("TZ", "CST+6");
let args = Default::default();
let mut streams = Streams::fake(b"");
let facts = Facts::new().with_config(Config {
commands: CommandsSettings {
display: BaseCommandSettings {
default_formatter: Some(Formatter::Ids),
},
..Default::default()
},
..Default::default()
});
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
DisplayCommand::handle(args, &mut streams, &facts).unwrap();

View File

@ -32,7 +32,7 @@ impl Args {
}
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
@ -142,7 +142,7 @@ mod tests {
note: Some("new note".into()),
..Default::default()
};
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
let an_hour_ago = now - Duration::hours(1);
let facts = Facts::new().with_now(now);
@ -175,7 +175,7 @@ mod tests {
note: Some("new note".into()),
..Default::default()
};
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
let an_hour_ago = now - Duration::hours(1);
let facts = Facts::new().with_now(now);
@ -198,7 +198,7 @@ mod tests {
fn edit_start() {
std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b"");
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
let args = Args {
start: Some(now - Duration::minutes(30)),
..Default::default()
@ -224,7 +224,7 @@ mod tests {
fn edit_end() {
std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b"");
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
let args = Args {
end: Some(now - Duration::minutes(30)),
..Default::default()
@ -255,7 +255,7 @@ mod tests {
append: true,
..Default::default()
};
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
let an_hour_ago = now - Duration::hours(1);
let facts = Facts::new().with_now(now);
@ -277,7 +277,7 @@ mod tests {
fn edit_move() {
std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b"");
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
let args = Args {
r#move: Some("new sheet".to_owned()),
..Default::default()
@ -309,7 +309,7 @@ mod tests {
append: true,
..Default::default()
};
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
let an_hour_ago = now - Duration::hours(1);
let facts = Facts::new().with_now(now).with_config(Config {
append_notes_delimiter: ";".to_owned(),
@ -346,8 +346,8 @@ mod tests {
let mut streams = Streams::fake(b"").with_db(
SqliteDatabase::from_path(&database_file).unwrap()
);
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
let new_end = Utc.ymd(2021, 6, 29).and_hms(14, 26, 52);
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
let new_end = Utc.with_ymd_and_hms(2021, 6, 29, 14, 26, 52).unwrap();
let args = Args {
end: Some(new_end),
..Default::default()
@ -365,7 +365,7 @@ mod tests {
");
assert_eq!(
String::from_utf8_lossy(&streams.err),
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
);
std::mem::drop(streams.db);

View File

@ -18,7 +18,7 @@ pub struct Args {
pub note: Option<String>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
@ -175,9 +175,9 @@ mod tests {
assert_eq!(e.sheet, "default".to_owned());
assert_eq!(&String::from_utf8_lossy(&streams.out), "Checked into sheet \"default\".\n");
assert_eq!(&String::from_utf8_lossy(&streams.err),
assert_eq!(&String::from_utf8_lossy(&streams.err),
"[WARNING] You are using the old timetrap format, it is advised that \
you update your database using t migrate\n");
you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n");
}
#[test]

View File

@ -17,7 +17,7 @@ pub enum Args {
Last,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(args: &'a ArgMatches) -> Result<Args> {

View File

@ -19,14 +19,16 @@ use super::{Command, Facts};
#[derive(Default)]
pub struct Args {
all: bool,
flat: bool,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &ArgMatches) -> Result<Args> {
Ok(Args {
all: matches.is_present("all"),
flat: matches.is_present("flat"),
})
}
}
@ -43,15 +45,13 @@ impl<'a> Command<'a> for ListCommand {
O: Write,
E: Write,
{
let today = facts.now.with_timezone(&Local).date().and_hms(0, 0, 0).with_timezone(&Utc);
let today = facts.now.with_timezone(&Local).date_naive().and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Utc).unwrap();
let entries = if args.all {
streams.db.entries_full(None, None)?
} else {
streams.db.entries_all_visible(None, None)?
};
let (mut entries, needs_warning) = entries_or_warning(entries, &streams.db)?;
let current = streams.db.current_sheet()?;
let last = streams.db.last_sheet()?;
@ -62,88 +62,99 @@ impl<'a> Command<'a> for ListCommand {
entries.sort_unstable_by_key(|e| e.sheet.clone());
let mut total_running = Duration::seconds(0);
let mut total_today = Duration::seconds(0);
let mut total = Duration::seconds(0);
if args.flat {
let sheets: Vec<_> = entries
.into_iter()
.map(|e| e.sheet)
.unique()
.collect();
let sheets: Vec<_> = entries
.into_iter()
.group_by(|e| e.sheet.clone())
.into_iter()
.map(|(key, group)| {
let entries: Vec<_> = group.into_iter().collect();
let s_running = facts.now - entries.iter().find(|e| e.end.is_none()).map(|e| e.start).unwrap_or(facts.now);
let s_today = entries.iter().filter(|e| e.start > today).fold(Duration::seconds(0), |acc, e| {
acc + (e.end.unwrap_or(facts.now) - e.start)
});
let s_total = entries.into_iter().fold(Duration::seconds(0), |acc, e| {
acc + (e.end.unwrap_or(facts.now) - e.start)
});
streams.out.write_all(sheets.join("\n").as_bytes())?;
streams.out.write_all(b"\n")?;
} else {
let mut total_running = Duration::seconds(0);
let mut total_today = Duration::seconds(0);
let mut total = Duration::seconds(0);
total_running = total_running + s_running;
total_today = total_today + s_today;
total = total + s_total;
let sheets: Vec<_> = entries
.into_iter()
.group_by(|e| e.sheet.clone())
.into_iter()
.map(|(key, group)| {
let entries: Vec<_> = group.into_iter().collect();
let s_running = facts.now - entries.iter().find(|e| e.end.is_none()).map(|e| e.start).unwrap_or(facts.now);
let s_today = entries.iter().filter(|e| e.start > today).fold(Duration::seconds(0), |acc, e| {
acc + (e.end.unwrap_or(facts.now) - e.start)
});
let s_total = entries.into_iter().fold(Duration::seconds(0), |acc, e| {
acc + (e.end.unwrap_or(facts.now) - e.start)
});
(
if current == key {
"*"
} else if last.as_ref() == Some(&key) {
"-"
} else {
""
},
total_running = total_running + s_running;
total_today = total_today + s_today;
total = total + s_total;
key,
(
if current == key {
"*"
} else if last.as_ref() == Some(&key) {
"-"
} else {
""
},
format_duration(s_running),
key,
format_duration(s_today),
format_duration(s_running),
format_duration(s_total),
)
})
.collect();
format_duration(s_today),
let mut tabs = Tabulate::with_columns(vec![
// indicator of current or prev sheet
Col::new().min_width(1).and_alignment(Right),
format_duration(s_total),
)
})
.collect();
// sheet name
Col::new().min_width(9).and_alignment(Left),
let mut tabs = Tabulate::with_columns(vec![
// indicator of current or prev sheet
Col::new().min_width(1).and_alignment(Right),
// running time
Col::new().min_width(9).and_alignment(Right)
.color_if(Style::new().dimmed(), |s| s == "0:00:00")
.color_if(Style::new().bold(), |s| s != "0:00:00"),
// sheet name
Col::new().min_width(9).and_alignment(Left),
// today
Col::new().min_width(9).and_alignment(Right)
.color_if(Style::new().dimmed(), |s| s == "0:00:00")
.color_if(Style::new().bold(), |s| s != "0:00:00"),
// running time
Col::new().min_width(9).and_alignment(Right)
.color_if(Style::new().dimmed(), |s| s == "0:00:00")
.color_if(Style::new().bold(), |s| s != "0:00:00"),
// accumulated
Col::new().min_width(12).and_alignment(Right),
]);
// today
Col::new().min_width(9).and_alignment(Right)
.color_if(Style::new().dimmed(), |s| s == "0:00:00")
.color_if(Style::new().bold(), |s| s != "0:00:00"),
tabs.feed(vec!["", "Timesheet", "Running", "Today", "Total Time"]);
tabs.separator(' ');
// accumulated
Col::new().min_width(12).and_alignment(Right),
]);
for sheet in sheets {
tabs.feed(vec![sheet.0.to_string(), sheet.1, sheet.2, sheet.3, sheet.4]);
tabs.feed(vec!["", "Timesheet", "Running", "Today", "Total Time"]);
tabs.separator(' ');
for sheet in sheets {
tabs.feed(vec![sheet.0.to_string(), sheet.1, sheet.2, sheet.3, sheet.4]);
}
tabs.separator('-');
tabs.feed(vec![
"".to_string(),
"".to_string(),
format_duration(total_running),
format_duration(total_today),
format_duration(total),
]);
streams.out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
}
tabs.separator('-');
tabs.feed(vec![
"".to_string(),
"".to_string(),
format_duration(total_running),
format_duration(total_today),
format_duration(total),
]);
streams.out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
warn_if_needed(&mut streams.err, needs_warning, &facts.env)?;
Ok(())
@ -153,7 +164,7 @@ impl<'a> Command<'a> for ListCommand {
#[cfg(test)]
mod tests {
use chrono::{Utc, TimeZone};
use pretty_assertions::assert_eq;
use pretty_assertions::assert_str_eq;
use crate::database::{SqliteDatabase, Database};
@ -169,18 +180,18 @@ mod tests {
streams.db.set_current_sheet("sheet2").unwrap();
streams.db.set_last_sheet("sheet4").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(1, 0, 0)), None, "_archived").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(10,13, 55)), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(7, 39, 18)), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(13, 52, 45)), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), None, None, "sheet4").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 1, 0, 0).unwrap()), None, "_archived").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 10,13, 55).unwrap()), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 7, 39, 18).unwrap()), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap()), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), None, None, "sheet4").unwrap();
let now = Utc.ymd(2021, 1, 1).and_hms(13, 52, 45);
let now = Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap();
let facts = Facts::new().with_now(now);
ListCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
sheet1 0:00:00 0:00:00 10:13:55
* sheet2 0:00:00 0:00:00 0:00:00
@ -195,11 +206,12 @@ mod tests {
let args = Args {
all: true,
..Default::default()
};
ListCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
_archived 0:00:00 0:00:00 1:00:00
sheet1 0:00:00 0:00:00 10:13:55
@ -218,21 +230,59 @@ mod tests {
SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap()
);
let now = Local.ymd(2021, 7, 16).and_hms(11, 30, 45);
let now = Local.with_ymd_and_hms(2021, 7, 16, 11, 30, 45).unwrap();
let facts = Facts::new().with_now(now.with_timezone(&Utc));
ListCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
* default 0:10:24 0:10:26 0:10:26
--------------------------------------------
0:10:24 0:10:26 0:10:26
");
assert_eq!(
assert_str_eq!(
String::from_utf8_lossy(&streams.err),
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
);
}
#[test]
fn flat_display() {
std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b"");
streams.db.set_current_sheet("sheet2").unwrap();
streams.db.set_last_sheet("sheet4").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 1, 0, 0).unwrap()), None, "_archived").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 10,13, 55).unwrap()), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 7, 39, 18).unwrap()), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap()), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), None, None, "sheet4").unwrap();
let now = Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap();
let facts = Facts::new().with_now(now);
let args = Args {
flat: true,
..Default::default()
};
ListCommand::handle(args, &mut streams, &facts).unwrap();
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "sheet1\nsheet2\nsheet3\nsheet4\n");
let facts = Facts::new().with_now(now);
let args = Args {
flat: true,
all: true,
};
streams.out.clear();
ListCommand::handle(args, &mut streams, &facts).unwrap();
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "_archived\nsheet1\nsheet2\nsheet3\nsheet4\n");
}
}

View File

@ -16,31 +16,27 @@ use super::{Command, Facts, display::{Sheet, entries_for_display}};
/// Given a local datetime, returns the time when the month it belongs started
fn beginning_of_month(time: DateTime<Local>) -> DateTime<Utc> {
time.date().with_day(1).unwrap().and_hms(0, 0, 0).with_timezone(&Utc)
time.date_naive().with_day(1).unwrap().and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Utc).unwrap()
}
/// Given a datetime compute the time where the previous_month started in UTC
fn beginning_of_previous_month(time: DateTime<Local>) -> DateTime<Utc> {
match time.month() {
1 => {
Local.ymd(time.year()-1, 12, 1).and_hms(0, 0, 0).with_timezone(&Utc)
Local.with_ymd_and_hms(time.year()-1, 12, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
}
n => Local.ymd(time.year(), n-1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
n => Local.with_ymd_and_hms(time.year(), n-1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
}
}
#[derive(Default)]
enum MonthSpec {
Last,
#[default]
This,
Month(u32),
}
impl Default for MonthSpec {
fn default() -> MonthSpec {
MonthSpec::This
}
}
impl FromStr for MonthSpec {
type Err = Error;
@ -74,7 +70,7 @@ pub struct Args {
sheet: Option<Sheet>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -110,21 +106,21 @@ impl<'a> Command<'a> for MonthCommand {
if month < now.month() {
// the specified month is in the current year
(
Local.ymd(now.year(), month, 1).and_hms(0, 0, 0).with_timezone(&Utc),
Local.with_ymd_and_hms(now.year(), month, 1, 0, 0, 0).unwrap().with_timezone(&Utc),
if month < 12 {
Local.ymd(now.year(), month+1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
Local.with_ymd_and_hms(now.year(), month+1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
} else {
Local.ymd(now.year()+1, 1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
Local.with_ymd_and_hms(now.year()+1, 1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
}
)
} else {
// use previous year
(
Local.ymd(now.year() - 1, month, 1).and_hms(0, 0, 0).with_timezone(&Utc),
Local.with_ymd_and_hms(now.year() - 1, month, 1, 0, 0, 0).unwrap().with_timezone(&Utc),
if month < 12 {
Local.ymd(now.year() - 1, month + 1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
Local.with_ymd_and_hms(now.year() - 1, month + 1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
} else {
Local.ymd(now.year(), 1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
Local.with_ymd_and_hms(now.year(), 1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
}
)
}
@ -136,7 +132,7 @@ impl<'a> Command<'a> for MonthCommand {
Some(end),
args.sheet,
streams,
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
args.format.unwrap_or_else(|| facts.config.commands.month.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
args.ids,
args.grep,
facts
@ -146,7 +142,7 @@ impl<'a> Command<'a> for MonthCommand {
#[cfg(test)]
mod tests {
use crate::config::Config;
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*;
@ -156,14 +152,40 @@ mod tests {
let args = Default::default();
let mut streams = Streams::fake(b"");
let now = Utc.ymd(2021, 6, 30).and_hms(11, 0, 0);
let now = Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap();
let facts = Facts::new().with_config(Config {
default_formatter: Formatter::Ids,
..Default::default()
}).with_now(now);
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
MonthCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
assert_eq!(String::from_utf8_lossy(&streams.err), "");
}
#[test]
fn respect_command_default_formatter() {
std::env::set_var("TZ", "CST+6");
let args = Default::default();
let mut streams = Streams::fake(b"");
let now = Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap();
let facts = Facts::new().with_config(Config {
commands: CommandsSettings {
month: BaseCommandSettings {
default_formatter: Some(Formatter::Ids),
},
..Default::default()
},
..Default::default()
}).with_now(now);
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
MonthCommand::handle(args, &mut streams, &facts).unwrap();

View File

@ -16,7 +16,7 @@ use super::{Command, Facts};
pub struct Args {
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(_matches: &'a ArgMatches) -> Result<Args> {
@ -40,42 +40,46 @@ impl<'a> Command<'a> for NowCommand {
let (entries, needs_warning) = entries_or_warning(entries, &streams.db)?;
let current = streams.db.current_sheet()?;
let last = streams.db.last_sheet()?;
if entries.is_empty() {
streams.out.write_all(b"No running entries\n")?;
} else {
let current = streams.db.current_sheet()?;
let last = streams.db.last_sheet()?;
let mut tabs = Tabulate::with_columns(vec![
// indicator of current or prev sheet
Col::new().min_width(1).and_alignment(Right),
let mut tabs = Tabulate::with_columns(vec![
// indicator of current or prev sheet
Col::new().min_width(1).and_alignment(Right),
// sheet name
Col::new().min_width(9).and_alignment(Left),
// sheet name
Col::new().min_width(9).and_alignment(Left),
// running time
Col::new().min_width(9).and_alignment(Right),
// running time
Col::new().min_width(9).and_alignment(Right),
// activity
Col::new().min_width(0).and_alignment(Left),
]);
tabs.feed(vec!["", "Timesheet", "Running", "Activity"]);
tabs.separator(' ');
for entry in entries {
tabs.feed(vec![
if current == entry.sheet {
"*"
} else if last.as_ref() == Some(&entry.sheet) {
"-"
} else {
""
}.to_string(),
entry.sheet,
format_duration(facts.now - entry.start),
entry.note.unwrap_or_else(|| "".to_string())
// activity
Col::new().min_width(0).and_alignment(Left),
]);
}
streams.out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
tabs.feed(vec!["", "Timesheet", "Running", "Activity"]);
tabs.separator(' ');
for entry in entries {
tabs.feed(vec![
if current == entry.sheet {
"*"
} else if last.as_ref() == Some(&entry.sheet) {
"-"
} else {
""
}.to_string(),
entry.sheet,
format_duration(facts.now - entry.start),
entry.note.unwrap_or_default(),
]);
}
streams.out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
}
warn_if_needed(&mut streams.err, needs_warning, &facts.env)?;
@ -86,7 +90,7 @@ impl<'a> Command<'a> for NowCommand {
#[cfg(test)]
mod tests {
use chrono::{Utc, TimeZone, Local};
use pretty_assertions::assert_eq;
use pretty_assertions::assert_str_eq;
use crate::database::{SqliteDatabase, Database};
@ -97,21 +101,21 @@ mod tests {
std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b"");
let now = Utc.ymd(2021, 1, 1).and_hms(13, 52, 45);
let now = Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap();
let facts = Facts::new().with_now(now);
streams.db.set_current_sheet("sheet2").unwrap();
streams.db.set_last_sheet("sheet4").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(1, 0, 0)), None, "_archived").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(10,13, 55)), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(7, 39, 18)), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(13, 52, 45)), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), None, Some("some".to_string()), "sheet4").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 1, 0, 0).unwrap()), None, "_archived").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 10,13, 55).unwrap()), None, "sheet1").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 7, 39, 18).unwrap()), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap()), None, "sheet3").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), None, Some("some".to_string()), "sheet4").unwrap();
NowCommand::handle(Default::default(), &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Activity
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Activity
- sheet4 1:52:45 some
");
@ -123,19 +127,33 @@ mod tests {
SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap()
);
let now = Local.ymd(2021, 7, 16).and_hms(11, 30, 45);
let now = Local.with_ymd_and_hms(2021, 7, 16, 11, 30, 45).unwrap();
let facts = Facts::new().with_now(now.with_timezone(&Utc));
NowCommand::handle(Default::default(), &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Activity
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Activity
* default 0:10:24 que
");
assert_eq!(
assert_str_eq!(
String::from_utf8_lossy(&streams.err),
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
);
}
#[test]
fn with_no_running_entries_display_nicer_message() {
std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b"");
let now = Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap();
let facts = Facts::new().with_now(now);
NowCommand::handle(Default::default(), &mut streams, &facts).unwrap();
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "No running entries
");
}
}

View File

@ -17,7 +17,7 @@ pub struct Args {
at: Option<DateTime<Utc>>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -114,6 +114,6 @@ mod tests {
assert_eq!(&String::from_utf8_lossy(&streams.out), "Checked out of sheet \"default\".\n");
assert_eq!(&String::from_utf8_lossy(&streams.err),
"[WARNING] You are using the old timetrap format, it is advised that \
you update your database using t migrate\n");
you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n");
}
}

View File

@ -12,25 +12,21 @@ use crate::io::Streams;
use crate::interactive::note_from_last_entries;
use super::{Command, Facts, r#in, sheet};
#[derive(Default)]
enum SelectedEntry {
Id(u64),
Interactive,
#[default]
NotSpecified,
}
impl Default for SelectedEntry {
fn default() -> Self {
SelectedEntry::NotSpecified
}
}
#[derive(Default)]
pub struct Args {
entry: SelectedEntry,
at: Option<DateTime<Utc>>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> {

View File

@ -14,7 +14,7 @@ pub struct Args {
pub sheet: Option<String>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> {

View File

@ -2,7 +2,7 @@ use std::convert::TryFrom;
use std::io::{BufRead, Write};
use clap::ArgMatches;
use chrono::{DateTime, Utc, Local};
use chrono::{DateTime, Utc, Local, Timelike};
use regex::Regex;
use crate::error::{Result, Error};
@ -23,7 +23,7 @@ pub struct Args {
sheet: Option<Sheet>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -49,14 +49,20 @@ impl<'a> Command<'a> for TodayCommand {
O: Write,
E: Write,
{
let start = Some(facts.now.with_timezone(&Local).date().and_hms(0, 0, 0).with_timezone(&Utc));
let start = Some(facts.now
.with_timezone(&Local)
.with_hour(0).unwrap()
.with_minute(0).unwrap()
.with_second(0).unwrap()
.with_nanosecond(0).unwrap()
.with_timezone(&Utc));
entries_for_display(
start,
args.end,
args.sheet,
streams,
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
args.format.unwrap_or_else(|| facts.config.commands.today.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
args.ids,
args.grep,
facts
@ -68,7 +74,7 @@ impl<'a> Command<'a> for TodayCommand {
mod tests {
use chrono::TimeZone;
use crate::config::Config;
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*;
@ -81,10 +87,35 @@ mod tests {
let facts = Facts::new().with_config(Config {
default_formatter: Formatter::Ids,
..Default::default()
}).with_now(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0));
}).with_now(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap());
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
TodayCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
assert_eq!(String::from_utf8_lossy(&streams.err), "");
}
#[test]
fn respect_command_default_formatter() {
std::env::set_var("TZ", "CST+6");
let args = Default::default();
let mut streams = Streams::fake(b"");
let facts = Facts::new().with_config(Config {
commands: CommandsSettings {
today: BaseCommandSettings {
default_formatter: Some(Formatter::Ids),
},
..Default::default()
},
..Default::default()
}).with_now(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap());
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
TodayCommand::handle(args, &mut streams, &facts).unwrap();

View File

@ -2,7 +2,7 @@ use std::convert::TryFrom;
use std::io::{BufRead, Write};
use clap::ArgMatches;
use chrono::{DateTime, Utc, Local, Duration, Weekday, Datelike};
use chrono::{DateTime, Utc, Local, Duration, Weekday, Datelike, Timelike};
use regex::Regex;
use crate::error::{Result, Error};
@ -56,7 +56,12 @@ fn prev_day(now: DateTime<Local>, week_start: WeekDay) -> DateTime<Utc> {
_ => unreachable!(),
};
begining.date().and_hms(0, 0, 0).with_timezone(&Utc)
begining
.with_hour(0).unwrap()
.with_minute(0).unwrap()
.with_second(0).unwrap()
.with_nanosecond(0).unwrap()
.with_timezone(&Utc)
}
#[derive(Default)]
@ -68,7 +73,7 @@ pub struct Args {
sheet: Option<Sheet>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -101,7 +106,7 @@ impl<'a> Command<'a> for WeekCommand {
args.end,
args.sheet,
streams,
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
args.format.unwrap_or_else(|| facts.config.commands.week.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
args.ids,
args.grep,
facts
@ -113,22 +118,22 @@ impl<'a> Command<'a> for WeekCommand {
mod tests {
use chrono::TimeZone;
use crate::config::Config;
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*;
#[test]
fn test_prev_day() {
// starting a saturday
let now = Local.ymd(2021, 7, 10).and_hms(18, 31, 0);
let now = Local.with_ymd_and_hms(2021, 7, 10, 18, 31, 0).unwrap();
assert_eq!(prev_day(now, WeekDay::Monday), Local.ymd(2021, 7, 5).and_hms(0, 0, 0).with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Tuesday), Local.ymd(2021, 7, 6).and_hms(0, 0, 0).with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Wednesday), Local.ymd(2021, 7, 7).and_hms(0, 0, 0).with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Thursday), Local.ymd(2021, 7, 8).and_hms(0, 0, 0).with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Friday), Local.ymd(2021, 7, 9).and_hms(0, 0, 0).with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Saturday), Local.ymd(2021, 7, 10).and_hms(0, 0, 0).with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Sunday), Local.ymd(2021, 7, 4).and_hms(0, 0, 0).with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Monday), Local.with_ymd_and_hms(2021, 7, 5, 0, 0, 0).unwrap().with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Tuesday), Local.with_ymd_and_hms(2021, 7, 6, 0, 0, 0).unwrap().with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Wednesday), Local.with_ymd_and_hms(2021, 7, 7, 0, 0, 0).unwrap().with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Thursday), Local.with_ymd_and_hms(2021, 7, 8, 0, 0, 0).unwrap().with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Friday), Local.with_ymd_and_hms(2021, 7, 9, 0, 0, 0).unwrap().with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Saturday), Local.with_ymd_and_hms(2021, 7, 10, 0, 0, 0).unwrap().with_timezone(&Utc));
assert_eq!(prev_day(now, WeekDay::Sunday), Local.with_ymd_and_hms(2021, 7, 4, 0, 0, 0).unwrap().with_timezone(&Utc));
}
#[test]
@ -137,14 +142,40 @@ mod tests {
let args = Default::default();
let mut streams = Streams::fake(b"");
let now = Utc.ymd(2021, 7, 1).and_hms(10, 0, 0);
let now = Utc.with_ymd_and_hms(2021, 7, 1, 10, 0, 0).unwrap();
let facts = Facts::new().with_config(Config {
default_formatter: Formatter::Ids,
..Default::default()
}).with_now(now);
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
WeekCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
assert_eq!(String::from_utf8_lossy(&streams.err), "");
}
#[test]
fn respect_command_default_formatter() {
std::env::set_var("TZ", "CST+6");
let args = Default::default();
let mut streams = Streams::fake(b"");
let now = Utc.with_ymd_and_hms(2021, 7, 1, 10, 0, 0).unwrap();
let facts = Facts::new().with_config(Config {
commands: CommandsSettings {
week: BaseCommandSettings {
default_formatter: Some(Formatter::Ids),
},
..Default::default()
},
..Default::default()
}).with_now(now);
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
WeekCommand::handle(args, &mut streams, &facts).unwrap();

View File

@ -21,7 +21,7 @@ pub struct Args {
sheet: Option<Sheet>,
}
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -46,16 +46,16 @@ impl<'a> Command<'a> for YesterdayCommand {
O: Write,
E: Write,
{
let today = facts.now.with_timezone(&Local).date();
let start = Some((today - Duration::days(1)).and_hms(0, 0, 0).with_timezone(&Utc));
let end = Some(today.and_hms(0, 0, 0).with_timezone(&Utc));
let today = facts.now.with_timezone(&Local).date_naive();
let start = Some((today - Duration::days(1)).and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Utc).unwrap());
let end = Some(today.and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Utc).unwrap());
entries_for_display(
start,
end,
args.sheet,
streams,
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
args.format.unwrap_or_else(|| facts.config.commands.yesterday.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
args.ids,
args.grep,
facts
@ -68,7 +68,7 @@ mod tests {
use chrono::{Duration, TimeZone};
use pretty_assertions::assert_eq;
use crate::config::Config;
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*;
@ -79,20 +79,20 @@ mod tests {
..Default::default()
};
let mut streams = Streams::fake(b"");
let two_days_ago = Local::now().date() - Duration::days(2);
let yesterday = Local::now().date() - Duration::days(1);
let today = Local::now().date();
let two_days_ago = Local::now().date_naive() - Duration::days(2);
let yesterday = Local::now().date_naive() - Duration::days(1);
let today = Local::now().date_naive();
let facts = Facts::new();
streams.db.entry_insert(two_days_ago.and_hms(1, 2, 3).with_timezone(&Utc), None, None, "default").unwrap();
streams.db.entry_insert(yesterday.and_hms(1, 2, 3).with_timezone(&Utc), None, Some("This!".into()), "default").unwrap();
streams.db.entry_insert(today.and_hms(1, 2, 3).with_timezone(&Utc), None, None, "default").unwrap();
streams.db.entry_insert(two_days_ago.and_hms_opt(1, 2, 3).unwrap().and_local_timezone(Utc).unwrap(), None, None, "default").unwrap();
streams.db.entry_insert(yesterday.and_hms_opt(1, 2, 3).unwrap().and_local_timezone(Utc).unwrap(), None, Some("This!".into()), "default").unwrap();
streams.db.entry_insert(today.and_hms_opt(1, 2, 3).unwrap().and_local_timezone(Utc).unwrap(), None, None, "default").unwrap();
YesterdayCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), &format!("start,end,note,sheet
{},,This!,default
", yesterday.and_hms(1, 2, 3).with_timezone(&Utc).to_rfc3339_opts(chrono::SecondsFormat::Micros, true)));
", yesterday.and_hms_opt(1, 2, 3).unwrap().and_local_timezone(Utc).unwrap().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)));
assert_eq!(
String::from_utf8_lossy(&streams.err),
@ -106,14 +106,40 @@ mod tests {
let args = Default::default();
let mut streams = Streams::fake(b"");
let now = Utc.ymd(2021, 7, 1).and_hms(10, 0, 0);
let now = Utc.with_ymd_and_hms(2021, 7, 1, 10, 0, 0).unwrap();
let facts = Facts::new().with_config(Config {
default_formatter: Formatter::Ids,
..Default::default()
}).with_now(now);
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
YesterdayCommand::handle(args, &mut streams, &facts).unwrap();
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
assert_eq!(String::from_utf8_lossy(&streams.err), "");
}
#[test]
fn respect_command_default_formatter() {
std::env::set_var("TZ", "CST+6");
let args = Default::default();
let mut streams = Streams::fake(b"");
let now = Utc.with_ymd_and_hms(2021, 7, 1, 10, 0, 0).unwrap();
let facts = Facts::new().with_config(Config {
commands: CommandsSettings {
yesterday: BaseCommandSettings {
default_formatter: Some(Formatter::Ids),
},
..Default::default()
},
..Default::default()
}).with_now(now);
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
YesterdayCommand::handle(args, &mut streams, &facts).unwrap();

View File

@ -7,10 +7,11 @@ use std::collections::HashMap;
use directories::{UserDirs, ProjectDirs};
use serde::{Serialize, Deserialize};
use toml::to_string;
use chrono::Weekday;
use crate::{error::{Result, Error::{self, *}}, formatters::Formatter};
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
pub enum WeekDay {
Monday,
Tuesday,
@ -38,7 +39,73 @@ impl FromStr for WeekDay {
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
impl From<Weekday> for WeekDay {
fn from(wd: Weekday) -> WeekDay {
match wd {
Weekday::Mon => WeekDay::Monday,
Weekday::Tue => WeekDay::Tuesday,
Weekday::Wed => WeekDay::Wednesday,
Weekday::Thu => WeekDay::Thursday,
Weekday::Fri => WeekDay::Friday,
Weekday::Sat => WeekDay::Saturday,
Weekday::Sun => WeekDay::Sunday,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ChartFormatterSettings {
/// This setting is used to highlight hours that go beyond the daily goal.
/// If unset all hours will look the same.
pub daily_goal_hours: u32,
/// If set, weekly hour count will be highlighted in green if equal or
/// higher than the weekly goal or in red if lower. If not set number will
/// be displayed in the default color.
pub weekly_goal_hours: u32,
/// This is the amount of minutes that each character represents in the
/// chart
pub character_equals_minutes: usize,
}
impl Default for ChartFormatterSettings {
fn default() -> Self {
Self {
daily_goal_hours: 0,
weekly_goal_hours: 0,
character_equals_minutes: 30,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct FormattersSettings {
pub chart: ChartFormatterSettings,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct BaseCommandSettings {
pub default_formatter: Option<Formatter>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct CommandsSettings {
pub display: BaseCommandSettings,
pub month: BaseCommandSettings,
pub today: BaseCommandSettings,
pub week: BaseCommandSettings,
pub yesterday: BaseCommandSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
#[serde(skip)]
@ -87,6 +154,12 @@ pub struct Config {
/// and kill)
pub interactive_entries: usize,
/// Individual settings for each formatter
pub formatters: FormattersSettings,
/// Settings for each command
pub commands: CommandsSettings,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
@ -286,6 +359,8 @@ impl Default for Config {
note_editor: None,
week_start: WeekDay::Monday,
interactive_entries: 5,
formatters: Default::default(),
commands: Default::default(),
}
}
}

View File

@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
use crate::error::{Error, Result};
use crate::models::{Entry, Meta};
#[derive(PartialEq)]
#[derive(PartialEq, Eq)]
pub enum DBVersion {
Timetrap,
Version(u16),
@ -243,7 +243,7 @@ pub trait Database {
}
pub struct SqliteDatabase {
connection: Connection,
connection: Connection,
}
impl SqliteDatabase {
@ -279,6 +279,7 @@ impl Database for SqliteDatabase {
Ok(())
}
#[allow(clippy::let_and_return)]
fn entry_query(&self, query: &str, params: &[&dyn ToSql]) -> Result<Vec<Entry>> {
let mut stmt = self.connection.prepare(query)?;
@ -323,7 +324,7 @@ impl Database for SqliteDatabase {
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use chrono::{TimeZone, NaiveDate};
use pretty_assertions::assert_eq;
use super::*;
@ -333,21 +334,21 @@ mod tests {
let mut db = SqliteDatabase::from_memory().unwrap();
db.init().unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "OOO").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "OOO").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "OOO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "OOO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "OOO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "OOO").unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
// filter by start and end
assert_eq!(
db.entries_by_sheet("XXX", Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
]
);
@ -355,8 +356,8 @@ mod tests {
assert_eq!(
db.entries_by_sheet("XXX", Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
]
);
@ -364,8 +365,8 @@ mod tests {
assert_eq!(
db.entries_by_sheet("XXX", None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
]
);
@ -373,9 +374,9 @@ mod tests {
assert_eq!(
db.entries_by_sheet("XXX", None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
]
);
}
@ -385,21 +386,21 @@ mod tests {
let mut db = SqliteDatabase::from_memory().unwrap();
db.init().unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "_OO").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "_OO").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "_OO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "_OO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "_OO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "_OO").unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
// filter by start and end
assert_eq!(
db.entries_all_visible(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
]
);
@ -407,8 +408,8 @@ mod tests {
assert_eq!(
db.entries_all_visible(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
]
);
@ -416,8 +417,8 @@ mod tests {
assert_eq!(
db.entries_all_visible(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
]
);
@ -425,9 +426,9 @@ mod tests {
assert_eq!(
db.entries_all_visible(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
]
);
}
@ -437,22 +438,22 @@ mod tests {
let mut db = SqliteDatabase::from_memory().unwrap();
db.init().unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "_OO").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "_OO").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX").unwrap();
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "_OO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "_OO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "_OO").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "XXX").unwrap();
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "_OO").unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
// filter by start and end
assert_eq!(
db.entries_full(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
]
);
@ -460,10 +461,10 @@ mod tests {
assert_eq!(
db.entries_full(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
]
);
@ -471,10 +472,10 @@ mod tests {
assert_eq!(
db.entries_full(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
]
);
@ -482,12 +483,12 @@ mod tests {
assert_eq!(
db.entries_full(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
]
);
}
@ -502,10 +503,10 @@ mod tests {
let mut db = SqliteDatabase::from_memory().unwrap();
db.init().unwrap();
let sometime = Utc.ymd(2022, 7, 27);
let sometime = NaiveDate::from_ymd_opt(2022, 7, 27).unwrap();
db.entry_insert(sometime.and_hms(11, 0, 0), Some(sometime.and_hms(12, 0, 0)), Some("latest".into()), "foo").unwrap();
db.entry_insert(sometime.and_hms(10, 0, 0), Some(sometime.and_hms(11, 0, 0)), Some("oldest".into()), "foo").unwrap();
db.entry_insert(sometime.and_hms_opt(11, 0, 0).unwrap().and_local_timezone(Utc).unwrap(), Some(sometime.and_hms_opt(12, 0, 0).unwrap().and_local_timezone(Utc).unwrap()), Some("latest".into()), "foo").unwrap();
db.entry_insert(sometime.and_hms_opt(10, 0, 0).unwrap().and_local_timezone(Utc).unwrap(), Some(sometime.and_hms_opt(11, 0, 0).unwrap().and_local_timezone(Utc).unwrap()), Some("oldest".into()), "foo").unwrap();
// filter by start and end
assert_eq!(
@ -513,8 +514,8 @@ mod tests {
Entry {
id: 1,
note: Some("latest".into()),
start: sometime.and_hms(11, 0, 0),
end: Some(sometime.and_hms(12, 0, 0)),
start: sometime.and_hms_opt(11, 0, 0).unwrap().and_local_timezone(Utc).unwrap(),
end: Some(sometime.and_hms_opt(12, 0, 0).unwrap().and_local_timezone(Utc).unwrap()),
sheet: "foo".into(),
}
);

View File

@ -1,5 +1,5 @@
use std::process::{Command, Stdio};
use std::io::{Read, Write, Seek, SeekFrom};
use std::io::{Read, Write, Seek};
use tempfile::NamedTempFile;
@ -20,7 +20,7 @@ pub fn get_string(note_editor: Option<&str>, prev_contents: Option<String>) -> R
};
let parts: Vec<_> = note_editor.split(' ').filter(|p| !p.is_empty()).collect();
let editor = if let Some(name) = parts.get(0) {
let editor = if let Some(name) = parts.first() {
name.to_owned()
} else {
return Err(EditorIsEmpty);
@ -38,7 +38,7 @@ pub fn get_string(note_editor: Option<&str>, prev_contents: Option<String>) -> R
if let Some(contents) = prev_contents {
tmpfile.write_all(contents.as_bytes())?;
tmpfile.seek(SeekFrom::Start(0))?;
tmpfile.rewind()?;
}
c.arg(tmpfile.as_ref());

View File

@ -19,6 +19,9 @@ pub enum Error {
#[error("The subcommand '{0}' is not implemented")]
UnimplementedCommand(String),
#[error("A subcommand was not specified and no default command is set")]
MissingSubcommand,
/// Sometimes a specific variant for an error is not necessary if the error
/// can only happen in one place in the code. This is what the generic error
/// is for and nothing else.
@ -213,7 +216,7 @@ which where taken from your config file located at
{config_at}
Perhaps is mispelled?", format_paths(.paths))]
Perhaps it is mispelled?", format_paths(.paths))]
FormatterNotFound {
name: String,
paths: Vec<PathBuf>,

View File

@ -13,24 +13,21 @@ pub mod json;
pub mod ids;
pub mod ical;
pub mod custom;
pub mod chart;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Formatter {
#[default]
Text,
Csv,
Json,
Ids,
Ical,
Chart,
Custom(String),
}
impl Default for Formatter {
fn default() -> Formatter {
Formatter::Text
}
}
impl Formatter {
/// Prints the given entries to the specified output device.
///
@ -49,6 +46,7 @@ impl Formatter {
Formatter::Json => json::print_formatted(entries, out)?,
Formatter::Ids => ids::print_formatted(entries, out)?,
Formatter::Ical => ical::print_formatted(entries, out, facts.now)?,
Formatter::Chart => chart::print_formatted(entries, out, facts)?,
Formatter::Custom(name) => custom::print_formatted(name, entries, out, err, facts)?,
}
@ -66,6 +64,7 @@ impl FromStr for Formatter {
"json" => Formatter::Json,
"ids" => Formatter::Ids,
"ical" => Formatter::Ical,
"chart" => Formatter::Chart,
custom_format => Formatter::Custom(custom_format.into()),
})
}

382
src/formatters/chart.rs Normal file
View File

@ -0,0 +1,382 @@
use std::io::Write;
use std::fmt::Write as _;
use std::collections::{HashMap, HashSet};
use crate::tabulate::{Tabulate, Col, Align::*};
use chrono::{Local, Datelike, NaiveDate, Duration};
use ansi_term::{Style, Color::{Green, Red, White, Fixed}};
use crate::commands::Facts;
use crate::models::Entry;
use crate::error::Result;
use crate::config::WeekDay;
struct Dates {
current: NaiveDate,
end: NaiveDate,
}
impl Dates {
fn range(from: NaiveDate, to: NaiveDate) -> Dates {
Dates {
current: from,
end: to,
}
}
}
impl Iterator for Dates {
type Item = NaiveDate;
fn next(&mut self) -> Option<Self::Item> {
if self.current > self.end {
None
} else {
let val = self.current;
self.current += Duration::days(1);
Some(val)
}
}
}
/// Takes a floating-point amount of hours and returns an integer representing
/// the number of whole blocks of size `block_minutes` that fit the amount of
/// hours.
fn hour_blocks(hours: f64, block_minutes: usize) -> usize {
(hours * 60.0) as usize / block_minutes
}
fn week_total(week_accumulated: f64, weekly_goal_hours: f64) -> String {
let formatted_hours = if weekly_goal_hours == 0.0 {
format!("{week_accumulated:.1}")
} else if week_accumulated >= weekly_goal_hours {
Green.paint(format!("{week_accumulated:.1}")).to_string()
} else {
Red.paint(format!("{week_accumulated:.1}")).to_string()
};
formatted_hours + &if weekly_goal_hours == 0.0 {
String::from("")
} else {
format!("/{weekly_goal_hours:.1}")
}
}
pub fn print_formatted<W: Write>(entries: Vec<Entry>, out: &mut W, facts: &Facts) -> Result<()> {
if entries.is_empty() {
writeln!(out, "No entries to display")?;
return Ok(());
}
let mut tabs = Tabulate::with_columns(vec![
Col::new().and_alignment(Right), // date
Col::new().and_alignment(Right), // day of week
Col::new().and_alignment(Left), // chart
Col::new().and_alignment(Left), // hours
]);
// Lets group entries by their date and compute some values
let mut entries_by_date = HashMap::new();
let mut first_date = None;
let mut last_date = None;
let mut timesheets = HashSet::new();
for entry in entries.into_iter() {
let entrys_date = entry.start.with_timezone(&Local).date_naive();
let hours = entry.hours(facts.now);
if first_date.is_none() {
first_date = Some(entrys_date);
} else {
first_date = first_date.map(|d| d.min(entrys_date));
}
if last_date.is_none() {
last_date = Some(entrys_date);
} else {
last_date = last_date.map(|d| d.max(entrys_date));
}
timesheets.insert(entry.sheet);
let e = entries_by_date.entry(entrys_date).or_insert(0.0);
*e += hours;
}
tabs.feed(vec![
"Date", "Day", "Chart", "Hours",
]);
tabs.separator(' ');
let start_of_week = facts.config.week_start;
let mut week_accumulated = 0.0;
let dates = Dates::range(first_date.unwrap(), last_date.unwrap());
let daily_goal_hours = facts.config.formatters.chart.daily_goal_hours;
let weekly_goal_hours = facts.config.formatters.chart.weekly_goal_hours as f64;
let block_size_minutes = facts.config.formatters.chart.character_equals_minutes;
for (i, date) in dates.enumerate() {
let hours = *entries_by_date.get(&date).unwrap_or(&0.0);
let current_day = WeekDay::from(date.weekday());
if current_day == start_of_week && i != 0 {
tabs.separator(' ');
tabs.feed(vec![
String::from(""),
String::from("Week"),
week_total(week_accumulated, weekly_goal_hours),
]);
tabs.separator(' ');
week_accumulated = 0.0;
}
week_accumulated += hours;
let daily_goal_blocks = hour_blocks(daily_goal_hours as f64, block_size_minutes);
let chart_print = {
// first print at most `daily_goal_blocks` characters in green
let total_blocks = hour_blocks(hours, block_size_minutes);
let greens = daily_goal_blocks.min(total_blocks);
let mut out = if greens > 0 {
Style::new().on(Green).paint(" ".repeat(greens)).to_string()
} else {
String::from("")
};
if greens < daily_goal_blocks {
// print the missing blocks in gray
write!(&mut out, "{}", Style::new().on(White).paint(" ".repeat(daily_goal_blocks - greens))).unwrap();
} else if total_blocks > daily_goal_blocks {
write!(&mut out, "{}", Style::new().on(Fixed(10)).paint(" ".repeat(total_blocks - daily_goal_blocks))).unwrap();
}
out
};
tabs.feed(vec![
if i == 0 || current_day == start_of_week || date.day() == 1 {
format!("{} {:>2}", date.format("%b"), date.day())
} else {
date.day().to_string()
},
date.weekday().to_string(),
chart_print,
format!("{hours:.1}"),
]);
}
// last week wasn't shown, so lets show it
tabs.separator(' ');
tabs.feed(vec![
String::from(""),
String::from("Week"),
week_total(week_accumulated, weekly_goal_hours),
]);
out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
if timesheets.len() == 1 {
out.write_all(format!("\nTimesheet: {}\n", timesheets.into_iter().next().unwrap()).as_bytes())?;
} else {
let mut timesheets: Vec<_> = timesheets.into_iter().collect();
timesheets.sort_unstable();
out.write_all(format!("\nTimesheets: {}\n", timesheets.join(", ")).as_bytes())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_str_eq;
use chrono::{Utc, TimeZone, Duration};
use crate::config::{Config, FormattersSettings, ChartFormatterSettings};
use super::*;
#[test]
fn sample_printing() {
std::env::set_var("TZ", "CST+6");
let day1 = Utc.with_ymd_and_hms(2022, 8, 15, 12, 0, 0).unwrap();
let day2 = Utc.with_ymd_and_hms(2022, 8, 16, 12, 0, 0).unwrap();
let day3 = Utc.with_ymd_and_hms(2022, 8, 17, 12, 0, 0).unwrap();
let day4 = Utc.with_ymd_and_hms(2022, 8, 18, 12, 0, 0).unwrap();
let entries = vec![
Entry::new_sample(1, day1, Some(day1 + Duration::hours(5))),
Entry::new_sample(2, day2, Some(day2 + Duration::minutes(60 * 3 + 30))),
Entry::new_sample(3, day3, Some(day3 + Duration::hours(4))),
Entry::new_sample(4, day4, Some(day4 + Duration::hours(2))),
];
let mut out = Vec::new();
let config = Config {
formatters: FormattersSettings {
chart: ChartFormatterSettings {
daily_goal_hours: 4,
weekly_goal_hours: 20,
..Default::default()
},
extra: HashMap::new(),
},
..Default::default()
};
let facts = Facts::new().with_config(config);
print_formatted(entries, &mut out, &facts).unwrap();
assert_str_eq!(String::from_utf8_lossy(&out), " Date Day Chart Hours
Aug 15 Mon \u{1b}[42m \u{1b}[0m\u{1b}[48;5;10m \u{1b}[0m 5.0
16 Tue \u{1b}[42m \u{1b}[0m\u{1b}[47m \u{1b}[0m 3.5
17 Wed \u{1b}[42m \u{1b}[0m 4.0
18 Thu \u{1b}[42m \u{1b}[0m\u{1b}[47m \u{1b}[0m 2.0
Week \u{1b}[31m14.5\u{1b}[0m/20.0
Timesheet: default
");
}
/// If entries span more than one week, both are shown with a weekly result
#[test]
fn partitioned_week() {
std::env::set_var("TZ", "CST+6");
let day1 = Utc.with_ymd_and_hms(2022, 8, 28, 12, 0, 0).unwrap();
let day2 = Utc.with_ymd_and_hms(2022, 8, 29, 12, 0, 0).unwrap();
let entries = vec![
Entry::new_sample(1, day1, Some(day1 + Duration::hours(5))),
Entry::new_sample(2, day2, Some(day2 + Duration::hours(3))),
];
let mut out = Vec::new();
let facts = Facts::new();
print_formatted(entries, &mut out, &facts).unwrap();
assert_str_eq!(String::from_utf8_lossy(&out), " Date Day Chart Hours
Aug 28 Sun \u{1b}[48;5;10m \u{1b}[0m 5.0
Week 5.0
Aug 29 Mon \u{1b}[48;5;10m \u{1b}[0m 3.0
Week 3.0
Timesheet: default
");
}
#[test]
fn empty_search() {
let entries = Vec::new();
let mut out = Vec::new();
let facts = Facts::new();
print_formatted(entries, &mut out, &facts).unwrap();
assert_str_eq!(String::from_utf8_lossy(&out), "No entries to display\n");
}
#[test]
fn days_without_hours_appear() {
std::env::set_var("TZ", "CST+6");
let day1 = Utc.with_ymd_and_hms(2022, 8, 15, 12, 0, 0).unwrap();
let day3 = Utc.with_ymd_and_hms(2022, 8, 17, 12, 0, 0).unwrap();
let entries = vec![
Entry::new_sample(1, day1, Some(day1 + Duration::hours(5))),
Entry::new_sample(3, day3, Some(day3 + Duration::hours(4))),
];
let mut out = Vec::new();
let facts = Facts::new();
print_formatted(entries, &mut out, &facts).unwrap();
assert_str_eq!(String::from_utf8_lossy(&out), " Date Day Chart Hours
Aug 15 Mon \u{1b}[48;5;10m \u{1b}[0m 5.0
16 Tue 0.0
17 Wed \u{1b}[48;5;10m \u{1b}[0m 4.0
Week 9.0
Timesheet: default
");
}
#[test]
fn display_without_goals_set() {
std::env::set_var("TZ", "CST+6");
let day1 = Utc.with_ymd_and_hms(2022, 8, 15, 12, 0, 0).unwrap();
let day2 = Utc.with_ymd_and_hms(2022, 8, 16, 12, 0, 0).unwrap();
let day3 = Utc.with_ymd_and_hms(2022, 8, 17, 12, 0, 0).unwrap();
let day4 = Utc.with_ymd_and_hms(2022, 8, 18, 12, 0, 0).unwrap();
let entries = vec![
Entry::new_sample(1, day1, Some(day1 + Duration::hours(5))),
Entry::new_sample(2, day2, Some(day2 + Duration::minutes(60 * 3 + 30))),
Entry::new_sample(3, day3, Some(day3 + Duration::hours(4))),
Entry::new_sample(4, day4, Some(day4 + Duration::hours(2))),
];
let mut out = Vec::new();
let facts = Facts::new();
print_formatted(entries, &mut out, &facts).unwrap();
assert_str_eq!(String::from_utf8_lossy(&out), " Date Day Chart Hours
Aug 15 Mon \u{1b}[48;5;10m \u{1b}[0m 5.0
16 Tue \u{1b}[48;5;10m \u{1b}[0m 3.5
17 Wed \u{1b}[48;5;10m \u{1b}[0m 4.0
18 Thu \u{1b}[48;5;10m \u{1b}[0m 2.0
Week 14.5
Timesheet: default
");
}
#[test]
fn multiple_timesheets_to_display() {
std::env::set_var("TZ", "CST+6");
let day1 = Utc.with_ymd_and_hms(2022, 8, 15, 12, 0, 0).unwrap();
let day2 = Utc.with_ymd_and_hms(2022, 8, 16, 12, 0, 0).unwrap();
let day3 = Utc.with_ymd_and_hms(2022, 8, 17, 12, 0, 0).unwrap();
let day4 = Utc.with_ymd_and_hms(2022, 8, 18, 12, 0, 0).unwrap();
let entries = vec![
Entry::new_sample(1, day1, Some(day1 + Duration::hours(5))).with_sheet("var"),
Entry::new_sample(2, day2, Some(day2 + Duration::minutes(60 * 3 + 30))).with_sheet("var"),
Entry::new_sample(3, day3, Some(day3 + Duration::hours(4))).with_sheet("foo"),
Entry::new_sample(4, day4, Some(day4 + Duration::hours(2))).with_sheet("foo"),
];
let mut out = Vec::new();
let facts = Facts::new();
print_formatted(entries, &mut out, &facts).unwrap();
assert_str_eq!(String::from_utf8_lossy(&out), " Date Day Chart Hours
Aug 15 Mon \u{1b}[48;5;10m \u{1b}[0m 5.0
16 Tue \u{1b}[48;5;10m \u{1b}[0m 3.5
17 Wed \u{1b}[48;5;10m \u{1b}[0m 4.0
18 Thu \u{1b}[48;5;10m \u{1b}[0m 2.0
Week 14.5
Timesheets: foo, var
");
}
}

View File

@ -10,9 +10,9 @@ pub fn print_formatted<W: Write>(entries: Vec<Entry>, out: &mut W, ids: bool) ->
let mut wtr = Writer::from_writer(out);
if ids {
wtr.write_record(&["id", "start", "end", "note", "sheet"])?;
wtr.write_record(["id", "start", "end", "note", "sheet"])?;
} else {
wtr.write_record(&["start", "end", "note", "sheet"])?;
wtr.write_record(["start", "end", "note", "sheet"])?;
}
for entry in entries {
@ -46,8 +46,8 @@ mod tests {
#[test]
fn test_print_formatted() {
let entries = vec![
Entry::new_sample(1, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), Some(Utc.ymd(2021, 6, 30).and_hms(19, 0, 0))),
Entry::new_sample(2, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), None),
Entry::new_sample(1, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 19, 0, 0).unwrap())),
Entry::new_sample(2, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), None),
];
let mut out = Vec::new();
@ -62,8 +62,8 @@ mod tests {
#[test]
fn test_print_formatted_ids() {
let entries = vec![
Entry::new_sample(1, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), Some(Utc.ymd(2021, 6, 30).and_hms(19, 0, 0))),
Entry::new_sample(2, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), None),
Entry::new_sample(1, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 19, 0, 0).unwrap())),
Entry::new_sample(2, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), None),
];
let mut out = Vec::new();

View File

@ -32,12 +32,8 @@ fn is_valid_formatter_name(formatter: &str) -> bool {
}
fn get_formatter_config(config: &Config, formatter: &str) -> String {
if let Some(formatters) = config.extra.get("formatters") {
if let Some(config_for_formatter) = formatters.get(formatter) {
config_for_formatter.to_string()
} else {
String::from("{}")
}
if let Some(config_for_formatter) = config.formatters.extra.get(formatter) {
config_for_formatter.to_string()
} else {
String::from("{}")
}
@ -114,6 +110,8 @@ where
mod tests {
use crate::config::Config;
use pretty_assertions::assert_str_eq;
use super::*;
#[test]
@ -158,7 +156,7 @@ t config --formatter-search-paths <path>..");
});
let err = print_formatted("pollo", Vec::new(), &mut out, &mut err, &facts).unwrap_err();
assert_eq!(err.to_string(), "\
assert_str_eq!(err.to_string(), "\
Could not find a formatter with name 'pollo' in any of the following paths:
- /not/a/path
@ -168,6 +166,6 @@ which where taken from your config file located at
/etc/tiempo/config.toml
Perhaps is mispelled?");
Perhaps it is mispelled?");
}
}

View File

@ -81,7 +81,7 @@ pub fn print_formatted<W: Write>(entries: Vec<Entry>, out: &mut W, facts: &Facts
]);
}
let entries_by_date = group.group_by(|e| e.start.with_timezone(&Local).date());
let entries_by_date = group.group_by(|e| e.start.with_timezone(&Local).date_naive());
let mut total = Duration::seconds(0);
for (date, entries) in entries_by_date.into_iter() {
@ -158,13 +158,13 @@ mod tests {
std::env::set_var("TZ", "CST+6");
let mut output = Vec::new();
let entries = vec![
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(18, 0, 0))),
Entry::new_sample(3, Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0))),
Entry::new_sample(4, Utc.ymd(2008, 10, 5).and_hms(18, 0, 0), None),
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 18, 0, 0).unwrap())),
Entry::new_sample(3, Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap())),
Entry::new_sample(4, Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap(), None),
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap();
@ -187,10 +187,13 @@ mod tests {
std::env::set_var("TZ", "CST+6");
let mut output = Vec::new();
let entries = vec![
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms_milli(12, 0, 0, 432), Some(Utc.ymd(2008, 10, 3).and_hms_milli(14, 0, 0, 312))),
Entry::new_sample(
1,
Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap().with_nanosecond(432_000_000).unwrap(),
Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap().with_nanosecond(312_000_000).unwrap())),
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap();
@ -209,11 +212,11 @@ mod tests {
std::env::set_var("TZ", "CST+6");
let mut output = Vec::new();
let entries = vec![
Entry::new_sample(1, Utc.ymd(2008, 10, 1).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap();
@ -234,13 +237,13 @@ mod tests {
std::env::set_var("TZ", "CST+6");
let mut output = Vec::new();
let entries = vec![
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(18, 0, 0))),
Entry::new_sample(3, Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0))),
Entry::new_sample(4, Utc.ymd(2008, 10, 5).and_hms(18, 0, 0), None),
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 18, 0, 0).unwrap())),
Entry::new_sample(3, Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap())),
Entry::new_sample(4, Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap(), None),
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, true).unwrap();
@ -266,13 +269,13 @@ mod tests {
Entry {
id: 60000,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some(LONG_NOTE.into()),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, true).unwrap();
@ -301,13 +304,13 @@ mod tests {
Entry {
id: 1,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some("first line\nand a second line".into()),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap();
@ -330,13 +333,13 @@ mod tests {
Entry {
id: 1,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some("quiúbole".into()),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap();
@ -359,20 +362,20 @@ mod tests {
Entry {
id: 1,
sheet: "sheet1".to_string(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some("quiúbole".to_string()),
},
Entry {
id: 2,
sheet: "sheet2".to_string(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some("quiúbole".to_string()),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap();

View File

@ -11,6 +11,7 @@ use crate::commands::Facts;
use crate::models::Entry;
use crate::tabulate::{Tabulate, Col, Align};
use crate::formatters::text::format_duration;
use crate::old::entries_or_warning;
fn read_line<I: BufRead>(mut r#in: I) -> io::Result<String> {
let mut pre_n = String::new();
@ -65,6 +66,7 @@ where
E: Write,
{
let entries = streams.db.entries_by_sheet(current_sheet, None, None)?;
let entries = entries_or_warning(entries, &streams.db)?.0;
let mut uniques = HashMap::new();
struct GroupedEntry {
@ -76,9 +78,7 @@ where
// From all possible entries belonging to this sheet keep only those with a
// note
let entries_with_notes = entries
// Iterate all entries
.into_iter()
// preserve only those with a text note
.filter_map(|e| e.note.map(|n| GroupedEntry {
note: n,
last_start: e.start,
@ -120,7 +120,7 @@ where
table.feed(vec!["#", "Note", "Total time", "Last started"]);
table.separator(' ');
for (i, entry) in uniques.iter().take(facts.config.interactive_entries).enumerate() {
for (i, entry) in uniques.iter().take(facts.config.interactive_entries).enumerate().rev() {
let i = i + 1;
let ago = formatter.convert_chrono(entry.last_start, facts.now);
@ -185,7 +185,7 @@ are you sure you want to delete entry {id} with note
\"{note}\"
{duration})"))? {
{duration}?"))? {
streams.db.delete_entry_by_id(entry.id)?;
writeln!(streams.out, "Gone")?;
} else {
@ -216,15 +216,15 @@ mod tests {
streams.db.entry_insert(one_hour_ago, Some(facts.now), Some("second task".into()), "default").unwrap();
// call the command interactively
note_from_last_entries(&mut streams, &facts, "default").unwrap();
assert_eq!(note_from_last_entries(&mut streams, &facts, "default").unwrap().unwrap(), "second task");
// check the output
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Latest entries of sheet 'default':
# Note Total time Last started
1 second task 1:00:00 1 hour ago
2 first task 1:00:00 2 hours ago
1 second task 1:00:00 1 hour ago
enter number or q to cancel
>> ");
@ -251,17 +251,17 @@ enter number or q to cancel
streams.db.entry_insert(facts.now - Duration::minutes(4), Some(facts.now - Duration::minutes(3)), Some("task 6".into()), "default").unwrap();
// call the command interactively
note_from_last_entries(&mut streams, &facts, "default").unwrap();
assert_eq!(note_from_last_entries(&mut streams, &facts, "default").unwrap().unwrap(), "task 6");
// check the output
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Latest entries of sheet 'default':
# Note Total time Last started
1 task 6 0:01:00 4 minutes ago
2 task 5 0:01:00 5 minutes ago
3 task 4 0:01:00 6 minutes ago
4 task 3 0:01:00 7 minutes ago
3 task 4 0:01:00 6 minutes ago
2 task 5 0:01:00 5 minutes ago
1 task 6 0:01:00 4 minutes ago
enter number or q to cancel
>> ");

View File

@ -15,3 +15,4 @@ pub mod old;
pub mod interactive;
pub mod env;
pub mod io;
pub mod cli;

View File

@ -1,7 +1,7 @@
use chrono::{DateTime, Utc, Duration};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Entry {
pub id: u64,
pub note: Option<String>,
@ -28,7 +28,22 @@ impl Entry {
}
}
pub fn with_sheet(self, sheet: &str) -> Entry {
Entry {
sheet: sheet.into(),
..self
}
}
pub fn timespan(&self) -> Option<Duration> {
self.end.map(|e| e - self.start)
}
// returns the number of hours of this entry as decimal. If entry is
// unfinished return its elapsed time so far.
pub fn hours(&self, now: DateTime<Utc>) -> f64 {
let d = self.end.unwrap_or(now) - self.start;
d.num_hours() as f64 + (d.num_minutes() % 60) as f64 / 60.0 + (d.num_seconds() % 60) as f64 / 3600.0
}
}

View File

@ -78,7 +78,8 @@ pub fn warn_if_needed<E: Write>(err: &mut E, needs_warning: bool, env: &Env) ->
writeln!(
err,
"{} You are using the old timetrap format, it is advised that \
you update your database using t migrate",
you update your database using t migrate. To supress this warning \
set TIEMPO_SUPRESS_TIMETRAP_WARNING=1",
if env.stderr_is_tty {
Yellow.bold().paint("[WARNING]")
} else {

View File

@ -2,15 +2,49 @@
use std::borrow::Cow;
use ansi_term::Style;
use regex::Regex;
lazy_static! {
// https://en.wikipedia.org/wiki/ANSI_escape_code#DOS,_OS/2,_and_Windows
//
// For Control Sequence Introducer, or CSI, commands, the ESC [ is followed
// by any number (including none) of "parameter bytes" in the range
// 0x300x3F (ASCII 09:;<=>?), then by any number of "intermediate bytes"
// in the range 0x200x2F (ASCII space and !"#$%&'()*+,-./), then finally
// by a single "final byte" in the range 0x400x7E (ASCII @AZ[\]^_`az{|}~)
//
// The lazy regex bellow doesn't cover all of that. It just works on ansi
// colors.
pub static ref ANSI_REGEX: Regex = Regex::new("\x1b\\[[\\d;]*m").unwrap();
}
/// An abstract way of getting the visual size of a string in a terminal
pub trait VisualSize {
fn size(&self) -> usize;
}
impl VisualSize for &str {
fn size(&self) -> usize {
let s = ANSI_REGEX.replace_all(self, "");
s.chars().count()
}
}
impl VisualSize for String {
fn size(&self) -> usize {
self.as_str().size()
}
}
fn lpad(s: &str, len: usize) -> String {
let padding = " ".repeat(len.saturating_sub(s.chars().count()));
let padding = " ".repeat(len.saturating_sub(s.size()));
padding + s
}
fn rpad(s: &str, len: usize) -> String {
let padding = " ".repeat(len.saturating_sub(s.chars().count()));
let padding = " ".repeat(len.saturating_sub(s.size()));
s.to_string() + &padding
}
@ -110,10 +144,10 @@ impl Tabulate {
for (col, ((w, d), c)) in self.widths.iter_mut().zip(data.iter()).zip(self.cols.iter()).enumerate() {
for (r1, dl) in d.as_ref().split('\n').enumerate() {
for (r2, l) in constrained_lines(dl, c.max_width.unwrap_or(usize::MAX)).into_iter().enumerate() {
let count = l.chars().count();
let width = l.as_ref().size();
if count > *w {
*w = count;
if width > *w {
*w = width;
}
if let Some(line) = lines.get_mut(r1 + r2) {
@ -189,6 +223,7 @@ impl Tabulate {
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use ansi_term::Color::Fixed;
use super::*;
@ -425,4 +460,11 @@ foo key
{} foo
", Style::new().dimmed().paint("key")));
}
#[test]
fn sizes_of_things() {
assert_eq!("🥦".size(), 1);
assert_eq!("á".size(), 1);
assert_eq!(Fixed(10).paint("hola").to_string().size(), 4);
}
}

View File

@ -14,10 +14,8 @@ fn date_from_parts<T: TimeZone>(
timezone: T, input: &str, year: i32, month: u32, day: u32, hour: u32,
minute: u32, second: u32
) -> Result<DateTime<Utc>> {
let try_date = timezone.ymd_opt(
year, month, day
).and_hms_opt(
hour, minute, second
let try_date = timezone.with_ymd_and_hms(
year, month, day, hour, minute, second
);
match try_date {
@ -36,9 +34,9 @@ fn offset_from_parts(east: bool, hours: i32, minutes: i32) -> FixedOffset {
second += minutes * 60;
if east {
FixedOffset::east(second)
FixedOffset::east_opt(second).unwrap()
} else {
FixedOffset::west(second)
FixedOffset::west_opt(second).unwrap()
}
}
@ -94,9 +92,9 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
// first try to parse as a full datetime with optional timezone
if let Some(caps) = DATETIME_REGEX.captures(input) {
let year: i32 = (&caps["year"]).parse().unwrap();
let month: u32 = (&caps["month"]).parse().unwrap();
let day: u32 = (&caps["day"]).parse().unwrap();
let year: i32 = caps["year"].parse().unwrap();
let month: u32 = caps["month"].parse().unwrap();
let day: u32 = caps["day"].parse().unwrap();
let hour: u32 = caps.name("hour").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
let minute: u32 = caps.name("minute").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
let second: u32 = caps.name("second").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
@ -107,8 +105,8 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
Utc, input, year, month, day, hour, minute, second,
)
} else {
let hours: i32 = (&caps["ohour"]).parse().unwrap();
let minutes: i32 = (&caps["omin"]).parse().unwrap();
let hours: i32 = caps["ohour"].parse().unwrap();
let minutes: i32 = caps["omin"].parse().unwrap();
let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes);
date_from_parts(
@ -124,7 +122,7 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
}
if let Some(caps) = HOUR_REGEX.captures(input) {
let hour: u32 = (&caps["hour"]).parse().unwrap();
let hour: u32 = caps["hour"].parse().unwrap();
let minute: u32 = caps.name("minute").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
let second: u32 = caps.name("second").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
@ -136,8 +134,8 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
Utc, input, year, month, day, hour, minute, second,
)
} else {
let hours: i32 = (&caps["ohour"]).parse().unwrap();
let minutes: i32 = (&caps["omin"]).parse().unwrap();
let hours: i32 = caps["ohour"].parse().unwrap();
let minutes: i32 = caps["omin"].parse().unwrap();
let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes);
let (year, month, day) = date_parts(fo.from_utc_datetime(&Utc::now().naive_utc()));
@ -163,54 +161,54 @@ pub fn parse_hours(input: &str) -> Result<u16> {
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Duration};
use chrono::{TimeZone, Duration, Timelike};
use super::*;
#[test]
fn parse_datetime_string() {
assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 0));
assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 12));
assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap());
assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 0));
assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 12));
assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap());
assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
}
#[test]
fn parse_date() {
assert_eq!(parse_time("2021-05-21").unwrap(), Local.ymd(2021, 5, 21).and_hms(0, 0, 0));
assert_eq!(parse_time("2021-05-21").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 0, 0, 0).unwrap());
}
#[test]
fn parse_hour() {
let localdate = Local::now().date();
let localdate = Local::now().with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
assert_eq!(parse_time("11:36").unwrap(), localdate.and_hms(11, 36, 0));
assert_eq!(parse_time("11:36:35").unwrap(), localdate.and_hms(11, 36, 35));
assert_eq!(parse_time("11:36").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc));
assert_eq!(parse_time("11:36:35").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_second(35).unwrap().with_timezone(&Utc));
}
#[test]
fn parse_hour_with_timezone() {
let hours: i32 = 3600;
let todayutc = Utc::now().date();
let todayutc = Utc::now().date_naive();
assert_eq!(parse_time("11:36Z").unwrap(), todayutc.and_hms(11, 36, 0));
assert_eq!(parse_time("11:36:35z").unwrap(), todayutc.and_hms(11, 36, 35));
assert_eq!(parse_time("11:36Z").unwrap(), todayutc.and_hms_opt(11, 36, 0).unwrap().and_local_timezone(Utc).unwrap());
assert_eq!(parse_time("11:36:35z").unwrap(), todayutc.and_hms_opt(11, 36, 35).unwrap().and_local_timezone(Utc).unwrap());
let offset = FixedOffset::west(5 * hours);
let todayoffset = offset.from_utc_datetime(&Utc::now().naive_utc()).date();
assert_eq!(parse_time("11:36-5:00").unwrap(), todayoffset.and_hms(11, 36, 0));
let offset = FixedOffset::west_opt(5 * hours).unwrap();
let today_at_offset = offset.from_utc_datetime(&Utc::now().naive_utc()).with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
assert_eq!(parse_time("11:36-5:00").unwrap(), today_at_offset.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc));
let offset = FixedOffset::east(5 * hours);
let todayoffset = offset.from_utc_datetime(&Utc::now().naive_utc()).date();
assert_eq!(parse_time("11:36:35+5:00").unwrap(), todayoffset.and_hms(11, 36, 35));
let offset = FixedOffset::east_opt(5 * hours).unwrap();
let today_at_offset = offset.from_utc_datetime(&Utc::now().naive_utc()).with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
assert_eq!(parse_time("11:36:35+5:00").unwrap(), today_at_offset.with_hour(11).unwrap().with_minute(36).unwrap().with_second(35).unwrap().with_timezone(&Utc));
}
#[test]
fn parse_with_specified_timezone() {
assert_eq!(parse_time("2021-05-21T11:36:12Z").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12));
assert_eq!(parse_time("2021-05-21T11:36:12-3:00").unwrap(), Utc.ymd(2021, 5, 21).and_hms(14, 36, 12));
assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.ymd(2021, 5, 21).and_hms(8, 36, 12));
assert_eq!(parse_time("2021-05-21T11:36:12Z").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
assert_eq!(parse_time("2021-05-21T11:36:12-3:00").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 14, 36, 12).unwrap());
assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 8, 36, 12).unwrap());
}
fn time_diff(t1: DateTime<Utc>, t2: DateTime<Local>) {
@ -235,7 +233,7 @@ mod tests {
time_diff(parse_time("forty one minutes ago").unwrap(), Local::now() - Duration::minutes(41));
time_diff(parse_time("1 minute ago").unwrap(), Local::now() - Duration::minutes(1));
time_diff(parse_time("23 minutes ago").unwrap(), Local::now() - Duration::minutes(23));
time_diff(parse_time("half an hour ago").unwrap(), dbg!(Local::now() - Duration::minutes(30)));
time_diff(parse_time("half an hour ago").unwrap(), Local::now() - Duration::minutes(30));
// mixed
time_diff(parse_time("an hour 10 minutes ago").unwrap(), Local::now() - Duration::minutes(70));