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 /*.sqlite3
docs/*/build/ docs/*/build/
dev_config.toml dev_config.toml
Pipfile*
artifacts/
build/
debian-package/

View File

@ -1,5 +1,6 @@
stages: stages:
- test - test
- build-doc
- build - build
- upload - upload
- release - release
@ -13,33 +14,45 @@ test:cargo:
- rustup component add clippy - rustup component add clippy
- cargo clippy --all-targets --all-features -- -D warnings - cargo clippy --all-targets --all-features -- -D warnings
- cargo test - 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: build:
stage: build stage: build
image: categulario/rust-cli-image:latest image: categulario/tiempo-build-env:1.65
script: script:
# build the binary - ./scripts/build.sh
- 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
artifacts: artifacts:
paths: paths:
- tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz - artifacts/
- 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
rules: rules:
- if: $CI_COMMIT_BRANCH - if: $CI_COMMIT_BRANCH
when: never when: never
@ -58,10 +71,10 @@ upload:
when: never when: never
- if: $CI_COMMIT_TAG =~ /^v*/ - if: $CI_COMMIT_TAG =~ /^v*/
script: 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 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 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 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 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 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 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}_amd64.deb.sum ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb.sum'
release: release:
stage: release stage: release
@ -80,13 +93,13 @@ release:
assets: assets:
links: links:
- name: 'Any linux binary' - 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' - 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' - 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' - 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: deploy:arch-bin:
stage: aur stage: aur
@ -100,8 +113,17 @@ deploy:arch-bin:
# setup git, because we'll commit # setup git, because we'll commit
- git config --global user.name "$COMMITER_NAME" - git config --global user.name "$COMMITER_NAME"
- git config --global user.email "$COMMITER_EMAIL" - 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 - 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: rules:
- if: $CI_COMMIT_BRANCH - if: $CI_COMMIT_BRANCH
when: never when: never
@ -119,23 +141,18 @@ deploy:arch-git:
# setup git, because we'll commit # setup git, because we'll commit
- git config --global user.name "$COMMITER_NAME" - git config --global user.name "$COMMITER_NAME"
- git config --global user.email "$COMMITER_EMAIL" - git config --global user.email "$COMMITER_EMAIL"
# clone the repo
- git clone $GIT_REPO_URL tiempo-git
# finally run the script # finally run the script
- scripts/release-aur-git.sh - 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: rules:
- if: $CI_COMMIT_BRANCH - if: $CI_COMMIT_BRANCH
when: never when: never
- if: $CI_COMMIT_TAG =~ /^v*/ - 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 # 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 ## 1.3.0
- Archive entries by the total sum of hours passing t-archive the --time - 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]] [[bin]]
name = "t" name = "t"
path = "src/main.rs" path = "src/bin/t.rs"
[package] [package]
name = "tiempo" name = "tiempo"
@ -9,15 +9,15 @@ authors = ["Abraham Toriz <categulario@gmail.com>"]
edition = "2021" edition = "2021"
description = "A command line time tracker" description = "A command line time tracker"
license = "GPL-3.0" license = "GPL-3.0"
documentation = "https://gitlab.com/categulario/tiempo-rs" documentation = "https://tiempo.categulario.xyz"
homepage = "https://gitlab.com/categulario/tiempo-rs" homepage = "https://gitlab.com/categulario/tiempo-rs"
version = "1.3.1" version = "1.6.0"
default-run = "t"
[dependencies] [dependencies]
clap = "2"
thiserror = "1" thiserror = "1"
directories = "3" directories = "3"
serde_yaml = "0.8" serde_yaml = "0.9"
toml = "0.5" toml = "0.5"
itertools = "0.10" itertools = "0.10"
textwrap = "0.14" textwrap = "0.14"
@ -31,6 +31,10 @@ hostname = "0.3"
atty = "0.2" atty = "0.2"
timeago = "0.3" timeago = "0.3"
[dependencies.clap]
version = "3"
features = ["cargo"]
[dependencies.chrono] [dependencies.chrono]
version = "0.4" version = "0.4"
features = ["serde"] 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 # Tiempo
A [timetrap](https://github.com/samg/timetrap/) compatible command line time A [timetrap](https://github.com/samg/timetrap/) compatible command line time
tracking application. tracking application. [Read the fine manual](https://tiempo.categulario.xyz).
## Installation ## Installation
### For Archlinux (and derivatives) users ### 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-bin](https://aur.archlinux.org/packages/tiempo-bin)
* [tiempo-git](https://aur.archlinux.org/packages/tiempo-git) * [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) 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 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. 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 ### For Rust developers
You have `cargo`! you can run: You have `cargo`! you can run:
cargo install tiempo 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 ### For everyone else
You need to compile `tiempo` by yourself. But don't worry! It is not that hard. 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 windows) and it is located inside the `target/release` directory that was
created during compilation. 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 ## How to build
You need [rust](https://rustup.rs), then clone the repo and simply run 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 ### Documentation
The docs are written using [sphinx](https://www.sphinx-doc.org/en/master/), so The docs are written using [sphinx](https://www.sphinx-doc.org/en/master/). To
first you need to install it somehow. Two options I can offer are: 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 virtualenv .venv
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`.
To build the docs just enter the `docs` directory and some of the language then activate it and install the dependencies:
directories (currently `es` or `en`) and run:
source .venv/bin/activate
pip install -r requirements.txt
To build the docs just do:
make html 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 make man
for the man page (output located at `docs/<lang>/build/man/tiempo.1`). If you for the man page (output located at `docs/build/man/tiempo.1`). To test the man
are using pipenv just prefix the commands with `pipenv run` or run `pipenv page you can do:
shell` before running any command.
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 formatted as
[reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html). [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 ## Special Thanks
To [timetrap](https://github.com/samg/timetrap) for existing, to 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 [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. 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 ## Release checklist
(mostly to remind myself) (mostly to remind myself)
* [ ] Ensure tests pass and that clippy doesn't complain * [ ] 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` * [ ] run `vbump`
* [ ] push commits and tags * [ ] git push && git push --tags && cargo publish
* [ ] wait for release * [ ] 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]}" cmd="${COMP_WORDS[1]}"
if [[ ( $cmd = s* || $cmd = d* || $cmd = k* ) && "$COMP_CWORD" = 2 ]]; then 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 return
elif [[ "$COMP_CWORD" = 1 ]]; then elif [[ "$COMP_CWORD" = 1 ]]; then
CMDS="archive backend configure display edit in kill list now out resume sheet week month" CMDS="archive backend configure display edit in kill list now out resume sheet week month"
@ -13,4 +13,4 @@ _tiempo ()
fi 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 SOURCEDIR=source
set BUILDDIR=build set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL %SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 ( if errorlevel 9009 (
echo. echo.
@ -25,6 +23,8 @@ if errorlevel 9009 (
exit /b 1 exit /b 1
) )
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end 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_MANTAINER="Abraham Toriz Cruz"
PROJECT_HOMEPAGE="https://gitlab.com/categulario/tiempo-rs" PROJECT_HOMEPAGE="https://gitlab.com/categulario/tiempo-rs"
PROJECT_NAME=tiempo PROJECT_NAME=tiempo
PROJECT_VERSION=${CI_COMMIT_TAG:1}
PROJECT_BINARY=t PROJECT_BINARY=t
PROJECT_DESCRIPTION="A command line time tracking application" PROJECT_DESCRIPTION="A command line time tracking application"
@ -28,9 +27,9 @@ mkdir -p "${DPKG_DIR}"
DPKG_BASENAME=${PROJECT_NAME} DPKG_BASENAME=${PROJECT_NAME}
DPKG_CONFLICTS= DPKG_CONFLICTS=
DPKG_VERSION=${PROJECT_VERSION} DPKG_VERSION=${CI_COMMIT_TAG:1}
DPKG_ARCH=amd64 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_DEPENDS=
DPKG_SECTION=utils DPKG_SECTION=utils
@ -39,8 +38,11 @@ install -Dm755 "target/release/$PROJECT_BINARY" "${DPKG_DIR}/usr/bin/$PROJECT_BI
# README and LICENSE # README and LICENSE
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md" 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 "LICENSE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE"
install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog" install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/CHANGELOG.md"
gzip -n --best "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog" 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 cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 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 #!/bin/bash
set -e
# some useful variables # some useful variables
VERSION=${CI_COMMIT_TAG:1} VERSION=${CI_COMMIT_TAG:1}
PROJECT_NAME=tiempo PROJECT_NAME=tiempo
PROJECT_BINARY=t PROJECT_BINARY=t
ARCHIVENAME=$PROJECT_NAME-$VERSION-x86_64.tar.gz ARCHIVENAME=$PROJECT_NAME-$CI_COMMIT_TAG-x86_64.tar.gz
# clone the repo
git clone $BIN_REPO_URL $PROJECT_NAME-bin
# enter it
cd $PROJECT_NAME-bin
# get the sum from the artifacts # get the sum from the artifacts
SUM=( `cat ../$ARCHIVENAME.sum` ) SUM=( `cat artifacts/$ARCHIVENAME.sum` )
# Generate the PKGBUILD # Generate the PKGBUILD
echo "# Maintainer: Abraham Toriz <categulario at gmail dot com> echo "# Maintainer: Abraham Toriz <categulario at gmail dot com>
@ -28,20 +24,21 @@ depends=()
optdepends=('sqlite: for manually editing the database') optdepends=('sqlite: for manually editing the database')
provides=('$PROJECT_NAME') provides=('$PROJECT_NAME')
conflicts=('$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') sha256sums=('$SUM')
package() { package() {
cd \"\$srcdir/build\" 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 share/doc/$PROJECT_NAME/README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md
install -Dm644 LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE install -Dm644 share/doc/$PROJECT_NAME/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/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 " | tee PKGBUILD > /dev/null
makepkg --printsrcinfo > .SRCINFO 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_NAME=tiempo
PROJECT_BINARY=t 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> echo "# Maintainer: Abraham Toriz <categulario at gmail dot com>
pkgname=$PROJECT_NAME-git pkgname=$PROJECT_NAME-git
pkgver=$VERSION pkgver=$VERSION
@ -21,7 +15,7 @@ url='https://gitlab.com/categulario/tiempo-rs'
license=('GPL3') license=('GPL3')
depends=() depends=()
optdepends=('sqlite: for manually editing the database') optdepends=('sqlite: for manually editing the database')
makedepends=('cargo' 'git') makedepends=('cargo' 'git' 'python-sphinx' 'python-tomlkit' 'gzip' 'make')
provides=('$PROJECT_NAME') provides=('$PROJECT_NAME')
conflicts=('$PROJECT_NAME') conflicts=('$PROJECT_NAME')
source=('$PROJECT_NAME-git::git+https://gitlab.com/categulario/tiempo-rs') source=('$PROJECT_NAME-git::git+https://gitlab.com/categulario/tiempo-rs')
@ -35,6 +29,9 @@ pkgver() {
build() { build() {
cd \"\$pkgname\" cd \"\$pkgname\"
cargo build --release --locked cargo build --release --locked
cd docs
make man
gzip build/man/$PROJECT_NAME.1
} }
package() { package() {
@ -44,9 +41,11 @@ package() {
install -Dm644 README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md install -Dm644 README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md
install -Dm644 LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE install -Dm644 LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE
install -Dm644 CHANGELOG.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/CHANGELOG.md 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 }" | tee PKGBUILD > /dev/null
makepkg --printsrcinfo > .SRCINFO 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::{ use clap::{
App, Arg, SubCommand, AppSettings, ArgMatches, crate_version, crate_authors, Command, Arg, SubCommand, command, value_parser,
crate_description, crate_name,
}; };
use chrono::Utc;
use regex::Regex;
use lazy_static::lazy_static;
use tiempo::error; pub fn make_cli() -> Command<'static> {
use tiempo::database::SqliteDatabase; // Let's first declare some args that repeat here and there
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
let start_arg = Arg::with_name("start") let start_arg = Arg::with_name("start")
.long("start").short("s") .long("start").short('s')
.takes_value(true).value_name("TIME") .takes_value(true).value_name("TIME")
.help("Include entries that start on this date or later"); .help("Include entries that start on this date or later");
let end_arg = Arg::with_name("end") let end_arg = Arg::with_name("end")
.long("end").short("e") .long("end").short('e')
.takes_value(true).value_name("TIME") .takes_value(true).value_name("TIME")
.help("Include entries that start on this date or earlier"); .help("Include entries that start on this date or earlier");
let ids_arg = Arg::with_name("ids") let ids_arg = Arg::with_name("ids")
.short("v").long("ids") .short('v').long("ids")
.help("Print database ids (for use with edit)"); .help("Print database ids (for use with edit)");
let grep_arg = Arg::with_name("grep") let grep_arg = Arg::with_name("grep")
.long("grep").short("g") .long("grep").short('g')
.takes_value(true).value_name("REGEXP") .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") let format_arg = Arg::with_name("format")
.short("f").long("format") .short('f').long("format")
.takes_value(true).value_name("FORMAT") .takes_value(true).value_name("FORMAT")
.help( .help(
"The output format. Valid built-in formats are ical, csv, json, \ "The output format. Valid built-in formats are chart, text, ical, \
ids, and text. Documentation on defining custom formats can be \ csv, json and ids. Documentation on defining custom formats can be \
found at https://gitlab.com/categulario/tiempo" found at https://tiempo.categulario.xyz or the man page included \
with the installation."
); );
let sheet_arg = Arg::with_name("sheet") let sheet_arg = Arg::with_name("sheet")
@ -124,22 +48,18 @@ fn main() {
let id_arg = Arg::with_name("id") let id_arg = Arg::with_name("id")
.long("id") .long("id")
.takes_value(true).value_name("ID") .takes_value(true).value_name("ID")
.validator(is_number); .value_parser(value_parser!(u64));
let interactive_arg = Arg::with_name("interactive") let interactive_arg = Arg::with_name("interactive")
.short("i") .short('i')
.long("interactive") .long("interactive")
.takes_value(false) .takes_value(false)
.conflicts_with("id") .conflicts_with("id")
.help("Choose an entry of the (unique) last N interactively"); .help("Choose an entry of the (unique) last N interactively");
// Now declar this app's cli // Now declar this app's cli
let matches = App::new("Tiempo") let cli = command!()
.name(crate_name!()) .subcommand_required(true)
.setting(AppSettings::SubcommandRequired)
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
.subcommand(SubCommand::with_name("archive") .subcommand(SubCommand::with_name("archive")
.visible_alias("a") .visible_alias("a")
@ -149,11 +69,11 @@ fn main() {
.arg(grep_arg.clone()) .arg(grep_arg.clone())
.arg(sheet_arg.clone().help("Archive entries from this sheet instead of the current one")) .arg(sheet_arg.clone().help("Archive entries from this sheet instead of the current one"))
.arg(Arg::with_name("fake") .arg(Arg::with_name("fake")
.short("f").long("fake") .short('f').long("fake")
.help("Don't actually archive the entries, just display them") .help("Don't actually archive the entries, just display them")
) )
.arg(Arg::with_name("time") .arg(Arg::with_name("time")
.short("t").long("time") .short('t').long("time")
.takes_value(true).value_name("HOURS") .takes_value(true).value_name("HOURS")
.help("Time in hours to archive. Archived time will be equal or less than this.") .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") .long("round-in-seconds")
.takes_value(true) .takes_value(true)
.value_name("SECONDS") .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)")) .help("The duration of time to use for rounding with the -r flag. Default: 900 (15 m)"))
.arg(Arg::with_name("database_file") .arg(Arg::with_name("database_file")
.long("database-file") .long("database-file")
@ -215,13 +135,13 @@ fn main() {
.long("week-start") .long("week-start")
.takes_value(true) .takes_value(true)
.value_name("DAY") .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")) .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") .arg(Arg::with_name("interactive_entries")
.long("interactive-entries") .long("interactive-entries")
.takes_value(true) .takes_value(true)
.value_name("N") .value_name("N")
.validator(is_number) .value_parser(value_parser!(u64))
.help("How many unique previous notes to show when selecting interactively")) .help("How many unique previous notes to show when selecting interactively"))
) )
@ -276,10 +196,10 @@ fn main() {
.arg(grep_arg.clone()) .arg(grep_arg.clone())
.arg(sheet_arg.clone()) .arg(sheet_arg.clone())
.arg(Arg::with_name("month") .arg(Arg::with_name("month")
.long("month").short("m") .long("month").short('m')
.takes_value(true).value_name("TIME") .takes_value(true).value_name("TIME")
.aliases(&["s", "start"]) .aliases(&["s", "start"])
.possible_values(&[ .possible_values([
"this", "current", "last", "jan", "january", "feb", "this", "current", "last", "jan", "january", "feb",
"february", "mar", "march", "apr", "april", "may", "jun", "february", "mar", "march", "apr", "april", "may", "jun",
"june", "jul", "july", "aug", "august", "sep", "september", "june", "jul", "july", "aug", "august", "sep", "september",
@ -327,8 +247,11 @@ fn main() {
.visible_alias("l") .visible_alias("l")
.about("List existing sheets") .about("List existing sheets")
.arg(Arg::with_name("all") .arg(Arg::with_name("all")
.short("a").long("all") .short('a').long("all")
.help("List archive sheets also")) .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") .subcommand(SubCommand::with_name("kill")
@ -338,12 +261,12 @@ fn main() {
.arg(Arg::with_name("sheet") .arg(Arg::with_name("sheet")
.takes_value(true).value_name("SHEET") .takes_value(true).value_name("SHEET")
.conflicts_with_all(&["id", "last"]) .conflicts_with_all(&["id", "last"])
.required_unless_one(&["id", "last"]) .required_unless_one(["id", "last"])
.help( .help(
"Delete an entire sheet by its name" "Delete an entire sheet by its name"
)) ))
.arg(Arg::with_name("last") .arg(Arg::with_name("last")
.short("l").long("last") .short('l').long("last")
.takes_value(false) .takes_value(false)
.help("Delete the last entry of the current sheet")) .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(end_arg.clone().help("Set this as the end time"))
.arg( .arg(
Arg::with_name("append") 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)") .help("Append to the current note instead of replacing it. The delimiter between appended notes is configurable (see configure)")
) )
.arg( .arg(
Arg::with_name("move") Arg::with_name("move")
.short("m").long("move") .short('m').long("move")
.takes_value(true) .takes_value(true)
.value_name("SHEET") .value_name("SHEET")
.help("Move entry to another sheet") .help("Move entry to another sheet")
@ -377,12 +300,7 @@ fn main() {
.value_name("NOTE") .value_name("NOTE")
.help("The note text. It will replace the previous one unless --append is given") .help("The note text. It will replace the previous one unless --append is given")
) )
) );
.get_matches(); cli
if let Err(e) = error_trap(matches) {
eprintln!("{}", e);
exit(1);
}
} }

View File

@ -64,7 +64,7 @@ impl Default for Facts {
} }
pub trait Command<'a> { 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<()>; 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>, sheet: Option<String>,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> { fn try_from(matches: &'a ArgMatches) -> Result<Self> {
@ -274,22 +274,22 @@ mod tests {
fn entries_are_split_properly() { fn entries_are_split_properly() {
let mut old_entry = Entry { let mut old_entry = Entry {
id: 1, id: 1,
start: Utc.ymd(2022, 7, 29).and_hms(10, 0, 0), start: Utc.with_ymd_and_hms(2022, 7, 29, 10, 0, 0).unwrap(),
end: Some(Utc.ymd(2022, 7, 29).and_hms(11, 0, 0)), end: Some(Utc.with_ymd_and_hms(2022, 7, 29, 11, 0, 0).unwrap()),
note: Some("an entry".to_string()), note: Some("an entry".to_string()),
sheet: "foo".to_string(), sheet: "foo".to_string(),
}; };
assert_eq!(split_entry(&mut old_entry, Duration::minutes(25)), ( assert_eq!(split_entry(&mut old_entry, Duration::minutes(25)), (
Utc.ymd(2022, 7, 29).and_hms(10, 25, 0), Utc.with_ymd_and_hms(2022, 7, 29, 10, 25, 0).unwrap(),
Some(Utc.ymd(2022, 7, 29).and_hms(11, 0, 0)), Some(Utc.with_ymd_and_hms(2022, 7, 29, 11, 0, 0).unwrap()),
Some("an entry".to_string()), Some("an entry".to_string()),
"foo".to_string(), "foo".to_string(),
)); ));
assert_eq!(old_entry, Entry { assert_eq!(old_entry, Entry {
id: 1, id: 1,
start: Utc.ymd(2022, 7, 29).and_hms(10, 0, 0), start: Utc.with_ymd_and_hms(2022, 7, 29, 10, 0, 0).unwrap(),
end: Some(Utc.ymd(2022, 7, 29).and_hms(10, 25, 0)), end: Some(Utc.with_ymd_and_hms(2022, 7, 29, 10, 25, 0).unwrap()),
note: Some("an entry".to_string()), note: Some("an entry".to_string()),
sheet: "foo".to_string(), sheet: "foo".to_string(),
}); });
@ -394,7 +394,7 @@ Proceed? [y/N] ");
}; };
let mut streams = Streams::fake(b"y\n"); let mut streams = Streams::fake(b"y\n");
let facts = Facts::new(); 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_b = time_a + Duration::minutes(90);
let time_d = time_a + Duration::hours(3); 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> { fn yes_no_none(matches: &ArgMatches, opt: &str) -> Option<bool> {
if matches.is_present(opt) { if matches.is_present(opt) {
Some(true) Some(true)
} else if matches.is_present(&format!("no_{}", opt)) { } else if matches.is_present(format!("no_{}", opt)) {
Some(false) Some(false)
} else { } else {
None None
} }
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> { fn try_from(matches: &'a ArgMatches) -> Result<Self> {

View File

@ -98,7 +98,7 @@ pub struct Args {
sheet: Option<Sheet>, sheet: Option<Sheet>,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> { fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -130,7 +130,7 @@ impl<'a> Command<'a> for DisplayCommand {
args.end, args.end,
args.sheet, args.sheet,
streams, 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.ids,
args.grep, args.grep,
facts facts
@ -144,7 +144,7 @@ mod tests {
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use crate::database::SqliteDatabase; use crate::database::SqliteDatabase;
use crate::config::Config; use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*; use super::*;
@ -170,7 +170,7 @@ mod tests {
assert_eq!( assert_eq!(
String::from_utf8_lossy(&streams.err), 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() { fn filter_by_start() {
let args = Args { let args = Args {
format: Some(Formatter::Csv), 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() ..Default::default()
}; };
let mut streams = Streams::fake(b""); let mut streams = Streams::fake(b"");
let facts = Facts::new(); 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.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), 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, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
DisplayCommand::handle(args, &mut streams, &facts).unwrap(); DisplayCommand::handle(args, &mut streams, &facts).unwrap();
@ -204,8 +204,8 @@ mod tests {
let mut streams = Streams::fake(b""); let mut streams = Streams::fake(b"");
let facts = Facts::new(); 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.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), 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, 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(); 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(); let facts = Facts::new();
std::env::set_var("TZ", "CST+6"); 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.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.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.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.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, 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(); DisplayCommand::handle(args, &mut streams, &facts).unwrap();
@ -264,9 +264,9 @@ Timesheet: sheet2
let facts = Facts::new(); let facts = Facts::new();
std::env::set_var("TZ", "CST+6"); 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.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.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.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.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, 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(); DisplayCommand::handle(args, &mut streams, &facts).unwrap();
@ -296,8 +296,8 @@ Timesheet: sheet1
let args = Args { let args = Args {
format: Some(Formatter::Csv), format: Some(Formatter::Csv),
start: Some(Utc.ymd(2021, 6, 29).and_hms(12, 0, 0)), start: Some(Utc.with_ymd_and_hms(2021, 6, 29, 12, 0, 0).unwrap()),
end: Some(Utc.ymd(2021, 6, 29).and_hms(13, 0, 0)), end: Some(Utc.with_ymd_and_hms(2021, 6, 29, 13, 0, 0).unwrap()),
..Default::default() ..Default::default()
}; };
let mut streams = Streams::fake(b"").with_db( let mut streams = Streams::fake(b"").with_db(
@ -317,7 +317,7 @@ Timesheet: sheet1
assert_eq!( assert_eq!(
String::from_utf8_lossy(&streams.err), 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() ..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.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), 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, 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(); 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; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> { fn try_from(matches: &'a ArgMatches) -> Result<Self> {
@ -142,7 +142,7 @@ mod tests {
note: Some("new note".into()), note: Some("new note".into()),
..Default::default() ..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 an_hour_ago = now - Duration::hours(1);
let facts = Facts::new().with_now(now); let facts = Facts::new().with_now(now);
@ -175,7 +175,7 @@ mod tests {
note: Some("new note".into()), note: Some("new note".into()),
..Default::default() ..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 an_hour_ago = now - Duration::hours(1);
let facts = Facts::new().with_now(now); let facts = Facts::new().with_now(now);
@ -198,7 +198,7 @@ mod tests {
fn edit_start() { fn edit_start() {
std::env::set_var("TZ", "CST+6"); std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b""); 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 { let args = Args {
start: Some(now - Duration::minutes(30)), start: Some(now - Duration::minutes(30)),
..Default::default() ..Default::default()
@ -224,7 +224,7 @@ mod tests {
fn edit_end() { fn edit_end() {
std::env::set_var("TZ", "CST+6"); std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b""); 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 { let args = Args {
end: Some(now - Duration::minutes(30)), end: Some(now - Duration::minutes(30)),
..Default::default() ..Default::default()
@ -255,7 +255,7 @@ mod tests {
append: true, append: true,
..Default::default() ..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 an_hour_ago = now - Duration::hours(1);
let facts = Facts::new().with_now(now); let facts = Facts::new().with_now(now);
@ -277,7 +277,7 @@ mod tests {
fn edit_move() { fn edit_move() {
std::env::set_var("TZ", "CST+6"); std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b""); 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 { let args = Args {
r#move: Some("new sheet".to_owned()), r#move: Some("new sheet".to_owned()),
..Default::default() ..Default::default()
@ -309,7 +309,7 @@ mod tests {
append: true, append: true,
..Default::default() ..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 an_hour_ago = now - Duration::hours(1);
let facts = Facts::new().with_now(now).with_config(Config { let facts = Facts::new().with_now(now).with_config(Config {
append_notes_delimiter: ";".to_owned(), append_notes_delimiter: ";".to_owned(),
@ -346,8 +346,8 @@ mod tests {
let mut streams = Streams::fake(b"").with_db( let mut streams = Streams::fake(b"").with_db(
SqliteDatabase::from_path(&database_file).unwrap() SqliteDatabase::from_path(&database_file).unwrap()
); );
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 new_end = Utc.ymd(2021, 6, 29).and_hms(14, 26, 52); let new_end = Utc.with_ymd_and_hms(2021, 6, 29, 14, 26, 52).unwrap();
let args = Args { let args = Args {
end: Some(new_end), end: Some(new_end),
..Default::default() ..Default::default()
@ -365,7 +365,7 @@ mod tests {
"); ");
assert_eq!( assert_eq!(
String::from_utf8_lossy(&streams.err), 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); std::mem::drop(streams.db);

View File

@ -18,7 +18,7 @@ pub struct Args {
pub note: Option<String>, pub note: Option<String>,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> { fn try_from(matches: &'a ArgMatches) -> Result<Self> {
@ -175,9 +175,9 @@ mod tests {
assert_eq!(e.sheet, "default".to_owned()); 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.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 \ "[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] #[test]

View File

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

View File

@ -19,14 +19,16 @@ use super::{Command, Facts};
#[derive(Default)] #[derive(Default)]
pub struct Args { pub struct Args {
all: bool, all: bool,
flat: bool,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &ArgMatches) -> Result<Args> { fn try_from(matches: &ArgMatches) -> Result<Args> {
Ok(Args { Ok(Args {
all: matches.is_present("all"), all: matches.is_present("all"),
flat: matches.is_present("flat"),
}) })
} }
} }
@ -43,15 +45,13 @@ impl<'a> Command<'a> for ListCommand {
O: Write, O: Write,
E: 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 { let entries = if args.all {
streams.db.entries_full(None, None)? streams.db.entries_full(None, None)?
} else { } else {
streams.db.entries_all_visible(None, None)? streams.db.entries_all_visible(None, None)?
}; };
let (mut entries, needs_warning) = entries_or_warning(entries, &streams.db)?; let (mut entries, needs_warning) = entries_or_warning(entries, &streams.db)?;
let current = streams.db.current_sheet()?; let current = streams.db.current_sheet()?;
let last = streams.db.last_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()); entries.sort_unstable_by_key(|e| e.sheet.clone());
let mut total_running = Duration::seconds(0); if args.flat {
let mut total_today = Duration::seconds(0); let sheets: Vec<_> = entries
let mut total = Duration::seconds(0); .into_iter()
.map(|e| e.sheet)
.unique()
.collect();
let sheets: Vec<_> = entries streams.out.write_all(sheets.join("\n").as_bytes())?;
.into_iter() streams.out.write_all(b"\n")?;
.group_by(|e| e.sheet.clone()) } else {
.into_iter() let mut total_running = Duration::seconds(0);
.map(|(key, group)| { let mut total_today = Duration::seconds(0);
let entries: Vec<_> = group.into_iter().collect(); let mut total = Duration::seconds(0);
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)
});
total_running = total_running + s_running; let sheets: Vec<_> = entries
total_today = total_today + s_today; .into_iter()
total = total + s_total; .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)
});
( total_running = total_running + s_running;
if current == key { total_today = total_today + s_today;
"*" total = total + s_total;
} else if last.as_ref() == Some(&key) {
"-"
} else {
""
},
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), format_duration(s_today),
)
})
.collect();
let mut tabs = Tabulate::with_columns(vec![ format_duration(s_total),
// indicator of current or prev sheet )
Col::new().min_width(1).and_alignment(Right), })
.collect();
// sheet name let mut tabs = Tabulate::with_columns(vec![
Col::new().min_width(9).and_alignment(Left), // indicator of current or prev sheet
Col::new().min_width(1).and_alignment(Right),
// running time // sheet name
Col::new().min_width(9).and_alignment(Right) Col::new().min_width(9).and_alignment(Left),
.color_if(Style::new().dimmed(), |s| s == "0:00:00")
.color_if(Style::new().bold(), |s| s != "0:00:00"),
// today // running time
Col::new().min_width(9).and_alignment(Right) Col::new().min_width(9).and_alignment(Right)
.color_if(Style::new().dimmed(), |s| s == "0:00:00") .color_if(Style::new().dimmed(), |s| s == "0:00:00")
.color_if(Style::new().bold(), |s| s != "0:00:00"), .color_if(Style::new().bold(), |s| s != "0:00:00"),
// accumulated // today
Col::new().min_width(12).and_alignment(Right), 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"]); // accumulated
tabs.separator(' '); Col::new().min_width(12).and_alignment(Right),
]);
for sheet in sheets { tabs.feed(vec!["", "Timesheet", "Running", "Today", "Total Time"]);
tabs.feed(vec![sheet.0.to_string(), sheet.1, sheet.2, sheet.3, sheet.4]); 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)?; warn_if_needed(&mut streams.err, needs_warning, &facts.env)?;
Ok(()) Ok(())
@ -153,7 +164,7 @@ impl<'a> Command<'a> for ListCommand {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use chrono::{Utc, TimeZone}; use chrono::{Utc, TimeZone};
use pretty_assertions::assert_eq; use pretty_assertions::assert_str_eq;
use crate::database::{SqliteDatabase, Database}; use crate::database::{SqliteDatabase, Database};
@ -169,18 +180,18 @@ mod tests {
streams.db.set_current_sheet("sheet2").unwrap(); streams.db.set_current_sheet("sheet2").unwrap();
streams.db.set_last_sheet("sheet4").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.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.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.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.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.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.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.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.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, 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); let facts = Facts::new().with_now(now);
ListCommand::handle(args, &mut streams, &facts).unwrap(); 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 sheet1 0:00:00 0:00:00 10:13:55
* sheet2 0:00:00 0:00:00 0:00:00 * sheet2 0:00:00 0:00:00 0:00:00
@ -195,11 +206,12 @@ mod tests {
let args = Args { let args = Args {
all: true, all: true,
..Default::default()
}; };
ListCommand::handle(args, &mut streams, &facts).unwrap(); 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 _archived 0:00:00 0:00:00 1:00:00
sheet1 0:00:00 0:00:00 10:13:55 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() 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)); let facts = Facts::new().with_now(now.with_timezone(&Utc));
ListCommand::handle(args, &mut streams, &facts).unwrap(); 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 * default 0:10:24 0:10:26 0:10:26
-------------------------------------------- --------------------------------------------
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), 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 /// Given a local datetime, returns the time when the month it belongs started
fn beginning_of_month(time: DateTime<Local>) -> DateTime<Utc> { 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 /// Given a datetime compute the time where the previous_month started in UTC
fn beginning_of_previous_month(time: DateTime<Local>) -> DateTime<Utc> { fn beginning_of_previous_month(time: DateTime<Local>) -> DateTime<Utc> {
match time.month() { match time.month() {
1 => { 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 { enum MonthSpec {
Last, Last,
#[default]
This, This,
Month(u32), Month(u32),
} }
impl Default for MonthSpec {
fn default() -> MonthSpec {
MonthSpec::This
}
}
impl FromStr for MonthSpec { impl FromStr for MonthSpec {
type Err = Error; type Err = Error;
@ -74,7 +70,7 @@ pub struct Args {
sheet: Option<Sheet>, sheet: Option<Sheet>,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> { fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -110,21 +106,21 @@ impl<'a> Command<'a> for MonthCommand {
if month < now.month() { if month < now.month() {
// the specified month is in the current year // 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 { 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 { } 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 { } else {
// use previous year // 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 { 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 { } 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), Some(end),
args.sheet, args.sheet,
streams, 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.ids,
args.grep, args.grep,
facts facts
@ -146,7 +142,7 @@ impl<'a> Command<'a> for MonthCommand {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::config::Config; use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*; use super::*;
@ -156,14 +152,40 @@ mod tests {
let args = Default::default(); let args = Default::default();
let mut streams = Streams::fake(b""); 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 { let facts = Facts::new().with_config(Config {
default_formatter: Formatter::Ids, default_formatter: Formatter::Ids,
..Default::default() ..Default::default()
}).with_now(now); }).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.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), 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, 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(); MonthCommand::handle(args, &mut streams, &facts).unwrap();

View File

@ -16,7 +16,7 @@ use super::{Command, Facts};
pub struct Args { pub struct Args {
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(_matches: &'a ArgMatches) -> Result<Args> { 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 (entries, needs_warning) = entries_or_warning(entries, &streams.db)?;
let current = streams.db.current_sheet()?; if entries.is_empty() {
let last = streams.db.last_sheet()?; 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![ let mut tabs = Tabulate::with_columns(vec![
// indicator of current or prev sheet // indicator of current or prev sheet
Col::new().min_width(1).and_alignment(Right), Col::new().min_width(1).and_alignment(Right),
// sheet name // sheet name
Col::new().min_width(9).and_alignment(Left), Col::new().min_width(9).and_alignment(Left),
// running time // running time
Col::new().min_width(9).and_alignment(Right), Col::new().min_width(9).and_alignment(Right),
// activity // activity
Col::new().min_width(0).and_alignment(Left), 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())
]); ]);
}
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)?; warn_if_needed(&mut streams.err, needs_warning, &facts.env)?;
@ -86,7 +90,7 @@ impl<'a> Command<'a> for NowCommand {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use chrono::{Utc, TimeZone, Local}; use chrono::{Utc, TimeZone, Local};
use pretty_assertions::assert_eq; use pretty_assertions::assert_str_eq;
use crate::database::{SqliteDatabase, Database}; use crate::database::{SqliteDatabase, Database};
@ -97,21 +101,21 @@ mod tests {
std::env::set_var("TZ", "CST+6"); std::env::set_var("TZ", "CST+6");
let mut streams = Streams::fake(b""); 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); let facts = Facts::new().with_now(now);
streams.db.set_current_sheet("sheet2").unwrap(); streams.db.set_current_sheet("sheet2").unwrap();
streams.db.set_last_sheet("sheet4").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.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.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.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.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.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.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.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.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, 12, 0, 0).unwrap(), None, Some("some".to_string()), "sheet4").unwrap();
NowCommand::handle(Default::default(), &mut streams, &facts).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 - sheet4 1:52:45 some
"); ");
@ -123,19 +127,33 @@ mod tests {
SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap() 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)); let facts = Facts::new().with_now(now.with_timezone(&Utc));
NowCommand::handle(Default::default(), &mut streams, &facts).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
* default 0:10:24 que * default 0:10:24 que
"); ");
assert_eq!( assert_str_eq!(
String::from_utf8_lossy(&streams.err), 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>>, at: Option<DateTime<Utc>>,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> { 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.out), "Checked out of 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 \ "[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 crate::interactive::note_from_last_entries;
use super::{Command, Facts, r#in, sheet}; use super::{Command, Facts, r#in, sheet};
#[derive(Default)]
enum SelectedEntry { enum SelectedEntry {
Id(u64), Id(u64),
Interactive, Interactive,
#[default]
NotSpecified, NotSpecified,
} }
impl Default for SelectedEntry {
fn default() -> Self {
SelectedEntry::NotSpecified
}
}
#[derive(Default)] #[derive(Default)]
pub struct Args { pub struct Args {
entry: SelectedEntry, entry: SelectedEntry,
at: Option<DateTime<Utc>>, at: Option<DateTime<Utc>>,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Self> { fn try_from(matches: &'a ArgMatches) -> Result<Self> {

View File

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

View File

@ -2,7 +2,7 @@ use std::convert::TryFrom;
use std::io::{BufRead, Write}; use std::io::{BufRead, Write};
use clap::ArgMatches; use clap::ArgMatches;
use chrono::{DateTime, Utc, Local}; use chrono::{DateTime, Utc, Local, Timelike};
use regex::Regex; use regex::Regex;
use crate::error::{Result, Error}; use crate::error::{Result, Error};
@ -23,7 +23,7 @@ pub struct Args {
sheet: Option<Sheet>, sheet: Option<Sheet>,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> { fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -49,14 +49,20 @@ impl<'a> Command<'a> for TodayCommand {
O: Write, O: Write,
E: 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( entries_for_display(
start, start,
args.end, args.end,
args.sheet, args.sheet,
streams, 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.ids,
args.grep, args.grep,
facts facts
@ -68,7 +74,7 @@ impl<'a> Command<'a> for TodayCommand {
mod tests { mod tests {
use chrono::TimeZone; use chrono::TimeZone;
use crate::config::Config; use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*; use super::*;
@ -81,10 +87,35 @@ mod tests {
let facts = Facts::new().with_config(Config { let facts = Facts::new().with_config(Config {
default_formatter: Formatter::Ids, default_formatter: Formatter::Ids,
..Default::default() ..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.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), 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, 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(); TodayCommand::handle(args, &mut streams, &facts).unwrap();

View File

@ -2,7 +2,7 @@ use std::convert::TryFrom;
use std::io::{BufRead, Write}; use std::io::{BufRead, Write};
use clap::ArgMatches; use clap::ArgMatches;
use chrono::{DateTime, Utc, Local, Duration, Weekday, Datelike}; use chrono::{DateTime, Utc, Local, Duration, Weekday, Datelike, Timelike};
use regex::Regex; use regex::Regex;
use crate::error::{Result, Error}; use crate::error::{Result, Error};
@ -56,7 +56,12 @@ fn prev_day(now: DateTime<Local>, week_start: WeekDay) -> DateTime<Utc> {
_ => unreachable!(), _ => 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)] #[derive(Default)]
@ -68,7 +73,7 @@ pub struct Args {
sheet: Option<Sheet>, sheet: Option<Sheet>,
} }
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { impl<'a> TryFrom<&'a ArgMatches> for Args {
type Error = Error; type Error = Error;
fn try_from(matches: &'a ArgMatches) -> Result<Args> { fn try_from(matches: &'a ArgMatches) -> Result<Args> {
@ -101,7 +106,7 @@ impl<'a> Command<'a> for WeekCommand {
args.end, args.end,
args.sheet, args.sheet,
streams, 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.ids,
args.grep, args.grep,
facts facts
@ -113,22 +118,22 @@ impl<'a> Command<'a> for WeekCommand {
mod tests { mod tests {
use chrono::TimeZone; use chrono::TimeZone;
use crate::config::Config; use crate::config::{Config, CommandsSettings, BaseCommandSettings};
use super::*; use super::*;
#[test] #[test]
fn test_prev_day() { fn test_prev_day() {
// starting a saturday // 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::Monday), Local.with_ymd_and_hms(2021, 7, 5, 0, 0, 0).unwrap().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::Tuesday), Local.with_ymd_and_hms(2021, 7, 6, 0, 0, 0).unwrap().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::Wednesday), Local.with_ymd_and_hms(2021, 7, 7, 0, 0, 0).unwrap().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::Thursday), Local.with_ymd_and_hms(2021, 7, 8, 0, 0, 0).unwrap().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::Friday), Local.with_ymd_and_hms(2021, 7, 9, 0, 0, 0).unwrap().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::Saturday), Local.with_ymd_and_hms(2021, 7, 10, 0, 0, 0).unwrap().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::Sunday), Local.with_ymd_and_hms(2021, 7, 4, 0, 0, 0).unwrap().with_timezone(&Utc));
} }
#[test] #[test]
@ -137,14 +142,40 @@ mod tests {
let args = Default::default(); let args = Default::default();
let mut streams = Streams::fake(b""); 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 { let facts = Facts::new().with_config(Config {
default_formatter: Formatter::Ids, default_formatter: Formatter::Ids,
..Default::default() ..Default::default()
}).with_now(now); }).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.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), 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, 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(); WeekCommand::handle(args, &mut streams, &facts).unwrap();

View File

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

View File

@ -7,10 +7,11 @@ use std::collections::HashMap;
use directories::{UserDirs, ProjectDirs}; use directories::{UserDirs, ProjectDirs};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use toml::to_string; use toml::to_string;
use chrono::Weekday;
use crate::{error::{Result, Error::{self, *}}, formatters::Formatter}; 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 { pub enum WeekDay {
Monday, Monday,
Tuesday, 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)] #[serde(default)]
pub struct Config { pub struct Config {
#[serde(skip)] #[serde(skip)]
@ -87,6 +154,12 @@ pub struct Config {
/// and kill) /// and kill)
pub interactive_entries: usize, pub interactive_entries: usize,
/// Individual settings for each formatter
pub formatters: FormattersSettings,
/// Settings for each command
pub commands: CommandsSettings,
#[serde(flatten)] #[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>, pub extra: HashMap<String, serde_json::Value>,
} }
@ -286,6 +359,8 @@ impl Default for Config {
note_editor: None, note_editor: None,
week_start: WeekDay::Monday, week_start: WeekDay::Monday,
interactive_entries: 5, 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::error::{Error, Result};
use crate::models::{Entry, Meta}; use crate::models::{Entry, Meta};
#[derive(PartialEq)] #[derive(PartialEq, Eq)]
pub enum DBVersion { pub enum DBVersion {
Timetrap, Timetrap,
Version(u16), Version(u16),
@ -243,7 +243,7 @@ pub trait Database {
} }
pub struct SqliteDatabase { pub struct SqliteDatabase {
connection: Connection, connection: Connection,
} }
impl SqliteDatabase { impl SqliteDatabase {
@ -279,6 +279,7 @@ impl Database for SqliteDatabase {
Ok(()) Ok(())
} }
#[allow(clippy::let_and_return)]
fn entry_query(&self, query: &str, params: &[&dyn ToSql]) -> Result<Vec<Entry>> { fn entry_query(&self, query: &str, params: &[&dyn ToSql]) -> Result<Vec<Entry>> {
let mut stmt = self.connection.prepare(query)?; let mut stmt = self.connection.prepare(query)?;
@ -323,7 +324,7 @@ impl Database for SqliteDatabase {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use chrono::TimeZone; use chrono::{TimeZone, NaiveDate};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*; use super::*;
@ -333,21 +334,21 @@ mod tests {
let mut db = SqliteDatabase::from_memory().unwrap(); let mut db = SqliteDatabase::from_memory().unwrap();
db.init().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.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), 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, 3, 0, 0).unwrap(), None, None, "OOO").unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0); let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0); let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
// filter by start and end // filter by start and end
assert_eq!( assert_eq!(
db.entries_by_sheet("XXX", Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_by_sheet("XXX", Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
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!( assert_eq!(
db.entries_by_sheet("XXX", Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_by_sheet("XXX", Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
] ]
); );
@ -364,8 +365,8 @@ mod tests {
assert_eq!( assert_eq!(
db.entries_by_sheet("XXX", None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_by_sheet("XXX", None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
] ]
); );
@ -373,9 +374,9 @@ mod tests {
assert_eq!( assert_eq!(
db.entries_by_sheet("XXX", None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_by_sheet("XXX", None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), 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(); let mut db = SqliteDatabase::from_memory().unwrap();
db.init().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.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), 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, 3, 0, 0).unwrap(), None, None, "_OO").unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0); let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0); let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
// filter by start and end // filter by start and end
assert_eq!( assert_eq!(
db.entries_all_visible(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_all_visible(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
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!( assert_eq!(
db.entries_all_visible(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_all_visible(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
] ]
); );
@ -416,8 +417,8 @@ mod tests {
assert_eq!( assert_eq!(
db.entries_all_visible(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_all_visible(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
] ]
); );
@ -425,9 +426,9 @@ mod tests {
assert_eq!( assert_eq!(
db.entries_all_visible(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_all_visible(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), 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(); let mut db = SqliteDatabase::from_memory().unwrap();
db.init().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.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), 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.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), 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, 3, 0, 0).unwrap(), None, None, "_OO").unwrap();
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0); let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0); let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
// filter by start and end // filter by start and end
assert_eq!( assert_eq!(
db.entries_full(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_full(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
] ]
); );
@ -460,10 +461,10 @@ mod tests {
assert_eq!( assert_eq!(
db.entries_full(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_full(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
] ]
); );
@ -471,10 +472,10 @@ mod tests {
assert_eq!( assert_eq!(
db.entries_full(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_full(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
] ]
); );
@ -482,12 +483,12 @@ mod tests {
assert_eq!( assert_eq!(
db.entries_full(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(), db.entries_full(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
vec![ vec![
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), 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(); let mut db = SqliteDatabase::from_memory().unwrap();
db.init().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_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(10, 0, 0), Some(sometime.and_hms(11, 0, 0)), Some("oldest".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 // filter by start and end
assert_eq!( assert_eq!(
@ -513,8 +514,8 @@ mod tests {
Entry { Entry {
id: 1, id: 1,
note: Some("latest".into()), note: Some("latest".into()),
start: sometime.and_hms(11, 0, 0), start: sometime.and_hms_opt(11, 0, 0).unwrap().and_local_timezone(Utc).unwrap(),
end: Some(sometime.and_hms(12, 0, 0)), end: Some(sometime.and_hms_opt(12, 0, 0).unwrap().and_local_timezone(Utc).unwrap()),
sheet: "foo".into(), sheet: "foo".into(),
} }
); );

View File

@ -1,5 +1,5 @@
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::io::{Read, Write, Seek, SeekFrom}; use std::io::{Read, Write, Seek};
use tempfile::NamedTempFile; 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 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() name.to_owned()
} else { } else {
return Err(EditorIsEmpty); 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 { if let Some(contents) = prev_contents {
tmpfile.write_all(contents.as_bytes())?; tmpfile.write_all(contents.as_bytes())?;
tmpfile.seek(SeekFrom::Start(0))?; tmpfile.rewind()?;
} }
c.arg(tmpfile.as_ref()); c.arg(tmpfile.as_ref());

View File

@ -19,6 +19,9 @@ pub enum Error {
#[error("The subcommand '{0}' is not implemented")] #[error("The subcommand '{0}' is not implemented")]
UnimplementedCommand(String), 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 /// 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 /// can only happen in one place in the code. This is what the generic error
/// is for and nothing else. /// is for and nothing else.
@ -213,7 +216,7 @@ which where taken from your config file located at
{config_at} {config_at}
Perhaps is mispelled?", format_paths(.paths))] Perhaps it is mispelled?", format_paths(.paths))]
FormatterNotFound { FormatterNotFound {
name: String, name: String,
paths: Vec<PathBuf>, paths: Vec<PathBuf>,

View File

@ -13,24 +13,21 @@ pub mod json;
pub mod ids; pub mod ids;
pub mod ical; pub mod ical;
pub mod custom; pub mod custom;
pub mod chart;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Formatter { pub enum Formatter {
#[default]
Text, Text,
Csv, Csv,
Json, Json,
Ids, Ids,
Ical, Ical,
Chart,
Custom(String), Custom(String),
} }
impl Default for Formatter {
fn default() -> Formatter {
Formatter::Text
}
}
impl Formatter { impl Formatter {
/// Prints the given entries to the specified output device. /// Prints the given entries to the specified output device.
/// ///
@ -49,6 +46,7 @@ impl Formatter {
Formatter::Json => json::print_formatted(entries, out)?, Formatter::Json => json::print_formatted(entries, out)?,
Formatter::Ids => ids::print_formatted(entries, out)?, Formatter::Ids => ids::print_formatted(entries, out)?,
Formatter::Ical => ical::print_formatted(entries, out, facts.now)?, 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)?, Formatter::Custom(name) => custom::print_formatted(name, entries, out, err, facts)?,
} }
@ -66,6 +64,7 @@ impl FromStr for Formatter {
"json" => Formatter::Json, "json" => Formatter::Json,
"ids" => Formatter::Ids, "ids" => Formatter::Ids,
"ical" => Formatter::Ical, "ical" => Formatter::Ical,
"chart" => Formatter::Chart,
custom_format => Formatter::Custom(custom_format.into()), 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); let mut wtr = Writer::from_writer(out);
if ids { if ids {
wtr.write_record(&["id", "start", "end", "note", "sheet"])?; wtr.write_record(["id", "start", "end", "note", "sheet"])?;
} else { } else {
wtr.write_record(&["start", "end", "note", "sheet"])?; wtr.write_record(["start", "end", "note", "sheet"])?;
} }
for entry in entries { for entry in entries {
@ -46,8 +46,8 @@ mod tests {
#[test] #[test]
fn test_print_formatted() { fn test_print_formatted() {
let entries = vec![ 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(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.ymd(2021, 6, 30).and_hms(18, 12, 34), None), Entry::new_sample(2, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), None),
]; ];
let mut out = Vec::new(); let mut out = Vec::new();
@ -62,8 +62,8 @@ mod tests {
#[test] #[test]
fn test_print_formatted_ids() { fn test_print_formatted_ids() {
let entries = vec![ 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(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.ymd(2021, 6, 30).and_hms(18, 12, 34), None), Entry::new_sample(2, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), None),
]; ];
let mut out = Vec::new(); 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 { fn get_formatter_config(config: &Config, formatter: &str) -> String {
if let Some(formatters) = config.extra.get("formatters") { if let Some(config_for_formatter) = config.formatters.extra.get(formatter) {
if let Some(config_for_formatter) = formatters.get(formatter) { config_for_formatter.to_string()
config_for_formatter.to_string()
} else {
String::from("{}")
}
} else { } else {
String::from("{}") String::from("{}")
} }
@ -114,6 +110,8 @@ where
mod tests { mod tests {
use crate::config::Config; use crate::config::Config;
use pretty_assertions::assert_str_eq;
use super::*; use super::*;
#[test] #[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(); 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: Could not find a formatter with name 'pollo' in any of the following paths:
- /not/a/path - /not/a/path
@ -168,6 +166,6 @@ which where taken from your config file located at
/etc/tiempo/config.toml /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); let mut total = Duration::seconds(0);
for (date, entries) in entries_by_date.into_iter() { for (date, entries) in entries_by_date.into_iter() {
@ -158,13 +158,13 @@ mod tests {
std::env::set_var("TZ", "CST+6"); std::env::set_var("TZ", "CST+6");
let mut output = Vec::new(); let mut output = Vec::new();
let entries = vec![ 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(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.ymd(2008, 10, 3).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(18, 0, 0))), 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.ymd(2008, 10, 5).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0))), 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.ymd(2008, 10, 5).and_hms(18, 0, 0), None), 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); let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap(); print_formatted(entries, &mut output, &facts, false).unwrap();
@ -187,10 +187,13 @@ mod tests {
std::env::set_var("TZ", "CST+6"); std::env::set_var("TZ", "CST+6");
let mut output = Vec::new(); let mut output = Vec::new();
let entries = vec![ 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); let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap(); print_formatted(entries, &mut output, &facts, false).unwrap();
@ -209,11 +212,11 @@ mod tests {
std::env::set_var("TZ", "CST+6"); std::env::set_var("TZ", "CST+6");
let mut output = Vec::new(); let mut output = Vec::new();
let entries = vec![ 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(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.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.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); let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap(); print_formatted(entries, &mut output, &facts, false).unwrap();
@ -234,13 +237,13 @@ mod tests {
std::env::set_var("TZ", "CST+6"); std::env::set_var("TZ", "CST+6");
let mut output = Vec::new(); let mut output = Vec::new();
let entries = vec![ 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(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.ymd(2008, 10, 3).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(18, 0, 0))), 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.ymd(2008, 10, 5).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0))), 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.ymd(2008, 10, 5).and_hms(18, 0, 0), None), 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); let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, true).unwrap(); print_formatted(entries, &mut output, &facts, true).unwrap();
@ -266,13 +269,13 @@ mod tests {
Entry { Entry {
id: 60000, id: 60000,
sheet: "default".into(), sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)), end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some(LONG_NOTE.into()), 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); let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, true).unwrap(); print_formatted(entries, &mut output, &facts, true).unwrap();
@ -301,13 +304,13 @@ mod tests {
Entry { Entry {
id: 1, id: 1,
sheet: "default".into(), sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)), end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some("first line\nand a second line".into()), 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); let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap(); print_formatted(entries, &mut output, &facts, false).unwrap();
@ -330,13 +333,13 @@ mod tests {
Entry { Entry {
id: 1, id: 1,
sheet: "default".into(), sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)), end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some("quiúbole".into()), 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); let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap(); print_formatted(entries, &mut output, &facts, false).unwrap();
@ -359,20 +362,20 @@ mod tests {
Entry { Entry {
id: 1, id: 1,
sheet: "sheet1".to_string(), sheet: "sheet1".to_string(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)), end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some("quiúbole".to_string()), note: Some("quiúbole".to_string()),
}, },
Entry { Entry {
id: 2, id: 2,
sheet: "sheet2".to_string(), sheet: "sheet2".to_string(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)), end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
note: Some("quiúbole".to_string()), 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); let facts = Facts::new().with_now(now);
print_formatted(entries, &mut output, &facts, false).unwrap(); print_formatted(entries, &mut output, &facts, false).unwrap();

View File

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

View File

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

View File

@ -1,7 +1,7 @@
use chrono::{DateTime, Utc, Duration}; use chrono::{DateTime, Utc, Duration};
use serde::Serialize; use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Entry { pub struct Entry {
pub id: u64, pub id: u64,
pub note: Option<String>, 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> { pub fn timespan(&self) -> Option<Duration> {
self.end.map(|e| e - self.start) 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!( writeln!(
err, err,
"{} You are using the old timetrap format, it is advised that \ "{} 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 { if env.stderr_is_tty {
Yellow.bold().paint("[WARNING]") Yellow.bold().paint("[WARNING]")
} else { } else {

View File

@ -2,15 +2,49 @@
use std::borrow::Cow; use std::borrow::Cow;
use ansi_term::Style; 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 { 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 padding + s
} }
fn rpad(s: &str, len: usize) -> String { 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 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 (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 (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() { 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 { if width > *w {
*w = count; *w = width;
} }
if let Some(line) = lines.get_mut(r1 + r2) { if let Some(line) = lines.get_mut(r1 + r2) {
@ -189,6 +223,7 @@ impl Tabulate {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use ansi_term::Color::Fixed;
use super::*; use super::*;
@ -425,4 +460,11 @@ foo key
{} foo {} foo
", Style::new().dimmed().paint("key"))); ", 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, timezone: T, input: &str, year: i32, month: u32, day: u32, hour: u32,
minute: u32, second: u32 minute: u32, second: u32
) -> Result<DateTime<Utc>> { ) -> Result<DateTime<Utc>> {
let try_date = timezone.ymd_opt( let try_date = timezone.with_ymd_and_hms(
year, month, day year, month, day, hour, minute, second
).and_hms_opt(
hour, minute, second
); );
match try_date { match try_date {
@ -36,9 +34,9 @@ fn offset_from_parts(east: bool, hours: i32, minutes: i32) -> FixedOffset {
second += minutes * 60; second += minutes * 60;
if east { if east {
FixedOffset::east(second) FixedOffset::east_opt(second).unwrap()
} else { } 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 // first try to parse as a full datetime with optional timezone
if let Some(caps) = DATETIME_REGEX.captures(input) { if let Some(caps) = DATETIME_REGEX.captures(input) {
let year: i32 = (&caps["year"]).parse().unwrap(); let year: i32 = caps["year"].parse().unwrap();
let month: u32 = (&caps["month"]).parse().unwrap(); let month: u32 = caps["month"].parse().unwrap();
let day: u32 = (&caps["day"]).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 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 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); 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, Utc, input, year, month, day, hour, minute, second,
) )
} else { } else {
let hours: i32 = (&caps["ohour"]).parse().unwrap(); let hours: i32 = caps["ohour"].parse().unwrap();
let minutes: i32 = (&caps["omin"]).parse().unwrap(); let minutes: i32 = caps["omin"].parse().unwrap();
let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes); let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes);
date_from_parts( date_from_parts(
@ -124,7 +122,7 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
} }
if let Some(caps) = HOUR_REGEX.captures(input) { 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 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); 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, Utc, input, year, month, day, hour, minute, second,
) )
} else { } else {
let hours: i32 = (&caps["ohour"]).parse().unwrap(); let hours: i32 = caps["ohour"].parse().unwrap();
let minutes: i32 = (&caps["omin"]).parse().unwrap(); let minutes: i32 = caps["omin"].parse().unwrap();
let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes); let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes);
let (year, month, day) = date_parts(fo.from_utc_datetime(&Utc::now().naive_utc())); 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)] #[cfg(test)]
mod tests { mod tests {
use chrono::{TimeZone, Duration}; use chrono::{TimeZone, Duration, Timelike};
use super::*; use super::*;
#[test] #[test]
fn parse_datetime_string() { 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").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.ymd(2021, 5, 21).and_hms(11, 36, 12)); 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").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap());
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:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
} }
#[test] #[test]
fn parse_date() { 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] #[test]
fn parse_hour() { 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").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc));
assert_eq!(parse_time("11:36:35").unwrap(), localdate.and_hms(11, 36, 35)); 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] #[test]
fn parse_hour_with_timezone() { fn parse_hour_with_timezone() {
let hours: i32 = 3600; 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: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(11, 36, 35)); 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 offset = FixedOffset::west_opt(5 * hours).unwrap();
let todayoffset = offset.from_utc_datetime(&Utc::now().naive_utc()).date(); 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(), todayoffset.and_hms(11, 36, 0)); 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 offset = FixedOffset::east_opt(5 * hours).unwrap();
let todayoffset = offset.from_utc_datetime(&Utc::now().naive_utc()).date(); 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(), todayoffset.and_hms(11, 36, 35)); 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] #[test]
fn parse_with_specified_timezone() { 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: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.ymd(2021, 5, 21).and_hms(14, 36, 12)); 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.ymd(2021, 5, 21).and_hms(8, 36, 12)); 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>) { 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("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("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("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 // mixed
time_diff(parse_time("an hour 10 minutes ago").unwrap(), Local::now() - Duration::minutes(70)); time_diff(parse_time("an hour 10 minutes ago").unwrap(), Local::now() - Duration::minutes(70));