Compare commits
132 Commits
Author | SHA1 | Date |
---|---|---|
Abraham Toriz | 08ff8254d1 | |
Abraham Toriz | 22d16ff5ba | |
Abraham Toriz | 530dd8dc15 | |
Abraham Toriz | 858302839c | |
Abraham Toriz | 56393636c9 | |
Abraham Toriz | 518fe26c82 | |
Abraham Toriz | dd6b9f9a8d | |
Abraham Toriz | 6afb517535 | |
Abraham Toriz | 3f83485769 | |
Abraham Toriz | 5d8d80fcfd | |
Abraham Toriz | 4d1e3537df | |
Abraham Toriz | a8990533bc | |
Abraham Toriz | 9cc6a88de6 | |
Abraham Toriz | 5ae5e32cfb | |
Abraham Toriz | d56ef65f73 | |
Abraham Toriz | c9f5782f59 | |
Abraham Toriz | 889295fd58 | |
Abraham Toriz | b3959782b1 | |
Abraham Toriz | ec87086b26 | |
Abraham Toriz | c939ae5753 | |
Abraham Toriz | 1e68467d36 | |
Abraham Toriz | 2dc6cda4f2 | |
Abraham Toriz | 709cfda05b | |
Abraham Toriz | 83498ac654 | |
Abraham Toriz | b75b412c23 | |
Abraham Toriz | 28c91053be | |
Abraham Toriz | 9b07bedf55 | |
Abraham Toriz | 57ec60226d | |
Abraham Toriz | c588db6b45 | |
Abraham Toriz | e7d261f75f | |
Abraham Toriz | 2f56a6ff79 | |
Abraham Toriz | 1c9527f3a0 | |
Abraham Toriz | 2948adac15 | |
Abraham Toriz | c6225f3821 | |
Abraham Toriz | 425df3aeca | |
Abraham Toriz | 37a779b0f1 | |
Abraham Toriz | ec7375d33e | |
Abraham Toriz | 48a8fa22f2 | |
Abraham Toriz | 6496d684ff | |
Abraham Toriz | bc2b96d425 | |
Abraham Toriz | 3fa0509319 | |
Abraham Toriz | 9433903cdf | |
Abraham Toriz | cfb2989853 | |
Abraham Toriz | 762520797a | |
Abraham Toriz | a689a68267 | |
Abraham Toriz | 540e5ce53f | |
Abraham Toriz | 2da1708885 | |
Abraham Toriz | ca2ddbb8de | |
Abraham Toriz | 3d587c9c61 | |
Abraham Toriz | 44137b835a | |
Abraham Toriz | 7d4e4645fc | |
Abraham Toriz | bcc929dbc0 | |
Abraham Toriz | 1b6077b1a0 | |
Abraham Toriz | aaf5f89c86 | |
Abraham Toriz | f4e733f9b8 | |
Abraham Toriz | f3bdc85d4c | |
Abraham Toriz | 0eb601b466 | |
Abraham Toriz | 610e3e7f9d | |
Abraham Toriz | 16970f7557 | |
Abraham Toriz | aa6fca4245 | |
Abraham Toriz | 7c9334da5c | |
Abraham Toriz | 45f83113ff | |
Abraham Toriz | 4d96688b9b | |
Abraham Toriz | ed3d8a7ab5 | |
perro tuerto | 7ede6ff1da | |
Abraham Toriz | 805bdd2de1 | |
Abraham Toriz | f032216e16 | |
Abraham Toriz | e52aafaf7b | |
Abraham Toriz | 3d63a581a4 | |
Abraham Toriz | 37893a9e9a | |
Abraham Toriz | d15f120ef9 | |
Abraham Toriz | b515b0a317 | |
Abraham Toriz | f3b4c99760 | |
Abraham Toriz | 22510dfcd2 | |
Abraham Toriz | 41a414d6d2 | |
Abraham Toriz | bd1785058e | |
Abraham Toriz | 7115ddfe54 | |
Abraham Toriz | 5f7fc63ef4 | |
Abraham Toriz | 4c486ae888 | |
Abraham Toriz | 49e068c5a3 | |
Abraham Toriz | 5e4fd1cb37 | |
Abraham Toriz | 61f80f2be6 | |
Abraham Toriz | 023f78bdae | |
Abraham Toriz | a997e25a5d | |
Abraham Toriz | f742264e29 | |
Abraham Toriz | 6be61a26d0 | |
Abraham Toriz | b617257f0c | |
Abraham Toriz | 78abfa16e6 | |
Abraham Toriz | 0bd576523b | |
Abraham Toriz | 2ca3189fe6 | |
Abraham Toriz | efc5055262 | |
Abraham Toriz | a41817f438 | |
Abraham Toriz | a61a44d130 | |
Abraham Toriz | 189ce814d0 | |
Abraham Toriz | dbbeea9f78 | |
Abraham Toriz | de1a4a2849 | |
Abraham Toriz | 2ed3f84454 | |
Abraham Toriz | dd40c1acc8 | |
Abraham Toriz | 086feb0e0b | |
Abraham Toriz | 8ec245038f | |
Abraham Toriz | d2e35d8ba1 | |
Abraham Toriz | bfef1004e3 | |
Abraham Toriz | ca7c424b46 | |
Abraham Toriz | 88371b312e | |
Abraham Toriz | 8420e7f776 | |
Abraham Toriz | f980bb89b5 | |
Abraham Toriz | 6d9fa0cf7b | |
Abraham Toriz | b4fb858208 | |
Abraham Toriz | f968e6605c | |
Abraham Toriz | a9bfc1a95a | |
Abraham Toriz | 2b406803f6 | |
Abraham Toriz | 646f309026 | |
Abraham Toriz | 3093d944e6 | |
Abraham Toriz | 5807ed23cd | |
Abraham Toriz | 152d67b664 | |
Abraham Toriz | 7b0e045ae1 | |
Abraham Toriz | c6c0d813c1 | |
Abraham Toriz | bc0f4236ac | |
Abraham Toriz | 6889945e1e | |
Abraham Toriz | 3c2e5acbc1 | |
Abraham Toriz | 411c0891a3 | |
Abraham Toriz | 0392e91985 | |
Abraham Toriz | 2b8cc6f94a | |
Abraham Toriz | d18319fd33 | |
Abraham Toriz | 62d432b5ba | |
Abraham Toriz | 458a069ce6 | |
Abraham Toriz | 030f7acb1e | |
Abraham Toriz | 844bb0d631 | |
Abraham Toriz | d4cc5b8fc8 | |
Abraham Toriz | 43d8464531 | |
Abraham Toriz | d04217151e | |
Abraham Toriz | 17657cc2a7 |
|
@ -4,3 +4,7 @@
|
|||
/*.sqlite3
|
||||
docs/*/build/
|
||||
dev_config.toml
|
||||
Pipfile*
|
||||
artifacts/
|
||||
build/
|
||||
debian-package/
|
||||
|
|
141
.gitlab-ci.yml
141
.gitlab-ci.yml
|
@ -1,5 +1,6 @@
|
|||
stages:
|
||||
- test
|
||||
- build-doc
|
||||
- build
|
||||
- upload
|
||||
- release
|
||||
|
@ -13,33 +14,45 @@ test:cargo:
|
|||
- rustup component add clippy
|
||||
- cargo clippy --all-targets --all-features -- -D warnings
|
||||
- cargo test
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
|
||||
build-doc:
|
||||
stage: build-doc
|
||||
image: python:3.10
|
||||
script:
|
||||
- cd docs/
|
||||
- pip install -r requirements.txt
|
||||
- make html
|
||||
- make man
|
||||
- gzip build/man/tiempo.1
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
artifacts:
|
||||
paths:
|
||||
- docs/build/html
|
||||
- docs/build/man/tiempo.1.gz
|
||||
|
||||
publish-doc:
|
||||
stage: release
|
||||
image: kroniak/ssh-client
|
||||
script:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
|
||||
- eval $(ssh-agent -s)
|
||||
- ssh-add <(echo "$PRIVATE_KEY")
|
||||
- scp -r docs/build/html/* $SERVER_USER@$SERVER_HOST:$APP_PATH
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: categulario/rust-cli-image:latest
|
||||
image: categulario/tiempo-build-env:1.65
|
||||
script:
|
||||
# build the binary
|
||||
- cargo build --locked --release
|
||||
# create the tar package
|
||||
- mkdir -p build
|
||||
# move things to the build directory
|
||||
- cp target/release/t build/t
|
||||
- cp CHANGELOG.md build/CHANGELOG.md
|
||||
- cp README.md build/README.md
|
||||
- cp LICENSE build/LICENSE
|
||||
# compress the tar file
|
||||
- tar -cvzf tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz build/
|
||||
# makes the debian archive
|
||||
- ./debpackage.sh
|
||||
# computes the sums
|
||||
- sha256sum tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz > tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum
|
||||
- sha256sum debian-package/tiempo_${CI_COMMIT_TAG:1}_amd64.deb > tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum
|
||||
- ./scripts/build.sh
|
||||
artifacts:
|
||||
paths:
|
||||
- tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz
|
||||
- debian-package/tiempo_${CI_COMMIT_TAG:1}_amd64.deb
|
||||
- tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum
|
||||
- tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum
|
||||
- artifacts/
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: never
|
||||
|
@ -58,10 +71,10 @@ upload:
|
|||
when: never
|
||||
- if: $CI_COMMIT_TAG =~ /^v*/
|
||||
script:
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz ${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz'
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum ${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum'
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file debian-package/tiempo_${CI_COMMIT_TAG:1}_amd64.deb ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG:1}_amd64.deb'
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum'
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz ${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz'
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz.sum ${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz.sum'
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/tiempo_${CI_COMMIT_TAG}_amd64.deb ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb'
|
||||
- 'curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/tiempo_${CI_COMMIT_TAG}_amd64.deb.sum ${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb.sum'
|
||||
|
||||
release:
|
||||
stage: release
|
||||
|
@ -80,13 +93,13 @@ release:
|
|||
assets:
|
||||
links:
|
||||
- name: 'Any linux binary'
|
||||
url: '${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz'
|
||||
url: '${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz'
|
||||
- name: 'Any linux binary sha256 sum'
|
||||
url: '${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG:1}-x86_64.tar.gz.sum'
|
||||
url: '${PACKAGE_REGISTRY_URL}/tiempo-${CI_COMMIT_TAG}-x86_64.tar.gz.sum'
|
||||
- name: 'Debian archive'
|
||||
url: '${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG:1}_amd64.deb'
|
||||
url: '${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb'
|
||||
- name: 'Debian archive sha256 sum'
|
||||
url: '${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG:1}_amd64.deb.sum'
|
||||
url: '${PACKAGE_REGISTRY_URL}/tiempo_${CI_COMMIT_TAG}_amd64.deb.sum'
|
||||
|
||||
deploy:arch-bin:
|
||||
stage: aur
|
||||
|
@ -100,42 +113,46 @@ deploy:arch-bin:
|
|||
# setup git, because we'll commit
|
||||
- git config --global user.name "$COMMITER_NAME"
|
||||
- git config --global user.email "$COMMITER_EMAIL"
|
||||
# finally run the script
|
||||
# Clone the repo
|
||||
- git clone $BIN_REPO_URL tiempo-bin
|
||||
# generate the PKGBUILD in the current directory
|
||||
- scripts/release-aur-bin.sh
|
||||
- mv PKGBUILD tiempo-bin/
|
||||
- mv .SRCINFO tiempo-bin/
|
||||
# commit
|
||||
- cd tiempo-bin
|
||||
- git add .
|
||||
- git commit -m "Release version $CI_COMMIT_TAG"
|
||||
- git push
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: never
|
||||
- if: $CI_COMMIT_TAG =~ /^v*/
|
||||
|
||||
# deploy:arch-git:
|
||||
# stage: aur
|
||||
# image: categulario/makepkg
|
||||
# script:
|
||||
# # setup ssh
|
||||
# - mkdir -p ~/.ssh
|
||||
# - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
|
||||
# - eval $(ssh-agent -s)
|
||||
# - ssh-add <(echo "$ARCH_PRIVATE_KEY")
|
||||
# # setup git, because we'll commit
|
||||
# - git config --global user.name "$COMMITER_NAME"
|
||||
# - git config --global user.email "$COMMITER_EMAIL"
|
||||
# # finally run the script
|
||||
# - scripts/release-aur-git.sh
|
||||
# rules:
|
||||
# - if: $CI_COMMIT_BRANCH
|
||||
# when: never
|
||||
# - if: $CI_COMMIT_TAG =~ /^v*/
|
||||
|
||||
# pages:
|
||||
# image: python:3.8-alpine
|
||||
# stage: deploy
|
||||
# script:
|
||||
# - pip install -U sphinx
|
||||
# - mkdir -p public/{es,en}
|
||||
# - sphinx-build -b html ./docs/es/source/ public/es
|
||||
# - sphinx-build -b html ./docs/en/source/ public/en
|
||||
# artifacts:
|
||||
# paths:
|
||||
# - public
|
||||
# rules:
|
||||
# - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
|
||||
deploy:arch-git:
|
||||
stage: aur
|
||||
image: categulario/makepkg
|
||||
script:
|
||||
# setup ssh
|
||||
- mkdir -p ~/.ssh
|
||||
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
|
||||
- eval $(ssh-agent -s)
|
||||
- ssh-add <(echo "$ARCH_PRIVATE_KEY")
|
||||
# setup git, because we'll commit
|
||||
- git config --global user.name "$COMMITER_NAME"
|
||||
- git config --global user.email "$COMMITER_EMAIL"
|
||||
# clone the repo
|
||||
- git clone $GIT_REPO_URL tiempo-git
|
||||
# finally run the script
|
||||
- scripts/release-aur-git.sh
|
||||
- mv PKGBUILD tiempo-git/
|
||||
- mv .SRCINFO tiempo-git/
|
||||
# and commit
|
||||
- cd tiempo-git
|
||||
- git add .
|
||||
- git commit -m "Release version $VERSION"
|
||||
- git push
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: never
|
||||
- if: $CI_COMMIT_TAG =~ /^v*/
|
||||
|
|
52
CHANGELOG.md
52
CHANGELOG.md
|
@ -1,5 +1,57 @@
|
|||
# Changes
|
||||
|
||||
## 1.6
|
||||
|
||||
- `t now` displays a nicer message if no entries are running.
|
||||
- `chart` formatter now shows the sheet name(s) that are being displayed.
|
||||
- any-linux package now includes install script.
|
||||
|
||||
## 1.5.3
|
||||
|
||||
- added `--flat` option to `list` that displays only available sheet names one
|
||||
per line
|
||||
- completions for bash, fish and zsh added to the package
|
||||
- binary package structure changed
|
||||
|
||||
## 1.5
|
||||
|
||||
- First release to include the man page. Documentation is now published in
|
||||
https://tiempo.categulario.xyz
|
||||
|
||||
## 1.4.1
|
||||
|
||||
- Fix config not being passed to custom formatters
|
||||
|
||||
## 1.4.0
|
||||
|
||||
- Brand new `chart` formatter that shows your work history
|
||||
- Fix bash-completions file
|
||||
- Improve some output messages
|
||||
- Fix timezone handling (in old databases) for --interactive
|
||||
- Tell how to silence the timetrap warning as part of the warning :(
|
||||
- Allow to set per-command default formatters
|
||||
|
||||
## 1.3.2
|
||||
|
||||
- Sort entries by most recent last in --interactive. Since most of the time you
|
||||
are likely recovering a recent entry you'll find it faster.
|
||||
|
||||
## 1.3.1
|
||||
|
||||
- Set correctly the description and dependencies for .deb package
|
||||
|
||||
## 1.3.0
|
||||
|
||||
- Archive entries by the total sum of hours passing t-archive the --time
|
||||
argument.
|
||||
|
||||
## 1.2.5
|
||||
|
||||
- From now on AUR packages (tiempo-git and tiempo-bin) are updated from CI
|
||||
(yay!)
|
||||
- Upgrade dependency rusqlite to 0.28 because 0.25 was yanked (perhaps a
|
||||
security issue)
|
||||
|
||||
## 1.2.0
|
||||
|
||||
- Add `--interactive` to `resume` subcommand
|
||||
|
|
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
16
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[[bin]]
|
||||
name = "t"
|
||||
path = "src/main.rs"
|
||||
path = "src/bin/t.rs"
|
||||
|
||||
[package]
|
||||
name = "tiempo"
|
||||
|
@ -9,15 +9,15 @@ authors = ["Abraham Toriz <categulario@gmail.com>"]
|
|||
edition = "2021"
|
||||
description = "A command line time tracker"
|
||||
license = "GPL-3.0"
|
||||
documentation = "https://gitlab.com/categulario/tiempo-rs"
|
||||
documentation = "https://tiempo.categulario.xyz"
|
||||
homepage = "https://gitlab.com/categulario/tiempo-rs"
|
||||
version = "1.2.3"
|
||||
version = "1.6.0"
|
||||
default-run = "t"
|
||||
|
||||
[dependencies]
|
||||
clap = "2"
|
||||
thiserror = "1"
|
||||
directories = "3"
|
||||
serde_yaml = "0.8"
|
||||
serde_yaml = "0.9"
|
||||
toml = "0.5"
|
||||
itertools = "0.10"
|
||||
textwrap = "0.14"
|
||||
|
@ -31,12 +31,16 @@ hostname = "0.3"
|
|||
atty = "0.2"
|
||||
timeago = "0.3"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "3"
|
||||
features = ["cargo"]
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.rusqlite]
|
||||
version = "0.25.3"
|
||||
version = "0.28.0"
|
||||
features = ["chrono", "bundled"]
|
||||
|
||||
[dependencies.serde]
|
||||
|
|
|
@ -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/*
|
374
README.md
374
README.md
|
@ -1,277 +1,46 @@
|
|||
# Tiempo
|
||||
|
||||
A [timetrap](https://github.com/samg/timetrap/) compatible command line time
|
||||
tracking application.
|
||||
tracking application. [Read the fine manual](https://tiempo.categulario.xyz).
|
||||
|
||||
## Installation
|
||||
|
||||
If you're a [rust](https://rust-lang.org) programmer you can do
|
||||
### For Archlinux (and derivatives) users
|
||||
|
||||
There are both binary and source packages in the AUR:
|
||||
|
||||
* [tiempo-bin](https://aur.archlinux.org/packages/tiempo-bin)
|
||||
* [tiempo-git](https://aur.archlinux.org/packages/tiempo-git)
|
||||
|
||||
### For all other linux users
|
||||
|
||||
Go to [gitlab releases page](https://gitlab.com/categulario/tiempo-rs/-/releases)
|
||||
and grab the latest binary for your linux. There is a `.deb` file for Debian and
|
||||
Ubuntu as well as a binary for any `x86_64` Linux.
|
||||
|
||||
In the case of the tar archive you just need to run the included `install.sh`
|
||||
script.
|
||||
|
||||
### For Rust developers
|
||||
|
||||
You have `cargo`! you can run:
|
||||
|
||||
cargo install tiempo
|
||||
|
||||
Ir if you use archlinux, there's a `tiempo-git` package in the AUR.
|
||||
However that will not install the beautiful man page. Although you can still see
|
||||
it at https://tiempo.categulario.xyz .
|
||||
|
||||
## Tutorial
|
||||
### For everyone else
|
||||
|
||||
First of all, you can abbreviate all commands to their first letter, so `t in`
|
||||
and `t i` are equivalent.
|
||||
You need to compile `tiempo` by yourself. But don't worry! It is not that hard.
|
||||
Just clone [the repository](https://gitlab.com/categulario/tiempo-rs), make sure
|
||||
you have [rust installed](https://rustup.rs) and run:
|
||||
|
||||
### Managing entries
|
||||
cargo build --release
|
||||
|
||||
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.
|
||||
|
||||
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). Once this tools is finished 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.
|
||||
* 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.
|
||||
inside the repository. The binary will be named `t` (or `t.exe` if you use
|
||||
windows) and it is located inside the `target/release` directory that was
|
||||
created during compilation.
|
||||
|
||||
## How to build
|
||||
|
||||
|
@ -307,34 +76,89 @@ and when I want to test some commands against such config file I just source it:
|
|||
|
||||
### Documentation
|
||||
|
||||
The docs are written using [sphinx](https://www.sphinx-doc.org/en/master/), so
|
||||
first you need to install it somehow. Two options I can offer are:
|
||||
The docs are written using [sphinx](https://www.sphinx-doc.org/en/master/). To
|
||||
install the required dependencies enter the `docs` directory and create a virual
|
||||
environment:
|
||||
|
||||
* using your computer's package manager. Install a package with a name similar
|
||||
to `python-sphinx`.
|
||||
* using [pipenv](https://duckduckgo.com/?t=ffab&q=pipenv&ia=web). Just ensure
|
||||
you have python 3.9 on your computer, enter the `docs` directory and do
|
||||
`pipenv install`.
|
||||
virtualenv .venv
|
||||
|
||||
To build the docs just enter the `docs` directory and some of the language
|
||||
directories (currently `es` or `en`) and run:
|
||||
then activate it and install the dependencies:
|
||||
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
To build the docs just do:
|
||||
|
||||
make html
|
||||
|
||||
for the html version (output located at `docs/<lang>/build/html`), or
|
||||
for the html version (output located at `docs/build/html`), or
|
||||
|
||||
make man
|
||||
|
||||
for the man page (output located at `docs/<lang>/build/man/tiempo.1`). If you
|
||||
are using pipenv just prefix the commands with `pipenv run` or run `pipenv
|
||||
shell` before running any command.
|
||||
for the man page (output located at `docs/build/man/tiempo.1`). To test the man
|
||||
page you can do:
|
||||
|
||||
The contents of the docs are located in `docs/<lang>/source/index.rst`,
|
||||
man -l build/man/tiempo.1
|
||||
|
||||
To get a live-reloaded server with the html docs do:
|
||||
|
||||
sphinx-autobuild source/ build/html/
|
||||
|
||||
The contents of the man page are located in `docs/source/index.rst`,
|
||||
formatted as
|
||||
[reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html).
|
||||
|
||||
### Building the packages like in CI but locally
|
||||
|
||||
First pull the image:
|
||||
|
||||
podman pull tiempo-build-env
|
||||
|
||||
Or build it yourself from this repo:
|
||||
|
||||
podman build -t tiempo-build-env .
|
||||
|
||||
Then build the artifacts:
|
||||
|
||||
./scripts/podman-build.sh
|
||||
|
||||
To build the archlinux PKGBUILDs (depends on the artifacts folder created by the
|
||||
previous command):
|
||||
|
||||
./scripts/podman-build-aur-bin.sh
|
||||
./scripts/podman-build-aur-git.sh
|
||||
|
||||
Both of the previous commands produce PKGBUILDs in the current directory so if
|
||||
you run both sequentially you'll lose the PKGBUILD of the first command.
|
||||
|
||||
## Special Thanks
|
||||
|
||||
To [timetrap](https://github.com/samg/timetrap) for existing, to
|
||||
[samg](https://github.com/samg) for creating it. It is the tool I was looking
|
||||
for and whose design I took as reference, keeping compatibility when possible.
|
||||
|
||||
## What I'm working on
|
||||
|
||||
(more or less ordered by priority)
|
||||
|
||||
* fix the pipeline because it's broken. The last release and its artifacts are
|
||||
wrong.
|
||||
* add an install/uninstall script to the any-linux package.
|
||||
* finish the `summary` formatter.
|
||||
* match formatters by prefix (so there's no need to type all of its name if the
|
||||
prefix is unambiguous).
|
||||
* let resume --interactive receive a string as the text for a new entry.
|
||||
* add a command that opens the doc in the browser just like fish
|
||||
* compile a package for windows in CI
|
||||
|
||||
## Release checklist
|
||||
|
||||
(mostly to remind myself)
|
||||
|
||||
* [ ] Ensure tests pass and that clippy doesn't complain
|
||||
* [ ] Add documentation about the new features
|
||||
* [ ] Create an entry in `CHANGELOG.md` with the target version, commit it
|
||||
* [ ] run `vbump`
|
||||
* [ ] git push && git push --tags && cargo publish
|
||||
* [ ] wait for release and then test the releases (aur bin and git and
|
||||
packaged).
|
||||
|
|
|
@ -5,7 +5,7 @@ _tiempo ()
|
|||
cmd="${COMP_WORDS[1]}"
|
||||
|
||||
if [[ ( $cmd = s* || $cmd = d* || $cmd = k* ) && "$COMP_CWORD" = 2 ]]; then
|
||||
COMPREPLY=($(compgen -W "$(echo "select distinct sheet from entries where sheet not like '\_%';" | z b)" $cur))
|
||||
COMPREPLY=($(compgen -W "$(t l --all --flat)" $cur))
|
||||
return
|
||||
elif [[ "$COMP_CWORD" = 1 ]]; then
|
||||
CMDS="archive backend configure display edit in kill list now out resume sheet week month"
|
||||
|
@ -13,4 +13,4 @@ _tiempo ()
|
|||
fi
|
||||
}
|
||||
|
||||
complete -F _tiempo 'z'
|
||||
complete -F _tiempo 't'
|
|
@ -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)"
|
|
@ -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 "$@"
|
|
@ -0,0 +1 @@
|
|||
/build/
|
12
docs/Pipfile
12
docs/Pipfile
|
@ -1,12 +0,0 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
sphinx = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
|
@ -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": {}
|
||||
}
|
|
@ -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
|
|
@ -1,12 +0,0 @@
|
|||
Advanced Usage
|
||||
==============
|
||||
|
||||
Subcommands
|
||||
-----------
|
||||
|
||||
bla
|
||||
|
||||
Settings
|
||||
--------
|
||||
|
||||
bla
|
|
@ -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/
|
|
@ -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']
|
|
@ -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/
|
|
@ -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)
|
|
@ -1,12 +0,0 @@
|
|||
Advanced Usage
|
||||
==============
|
||||
|
||||
Subcommands
|
||||
-----------
|
||||
|
||||
bla
|
||||
|
||||
Settings
|
||||
--------
|
||||
|
||||
bla
|
|
@ -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/
|
|
@ -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']
|
|
@ -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/
|
|
@ -10,8 +10,6 @@ if "%SPHINXBUILD%" == "" (
|
|||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
|
@ -25,6 +23,8 @@ if errorlevel 9009 (
|
|||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
sphinx
|
||||
sphinx-autobuild
|
||||
tomlkit
|
||||
furo
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -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"
|
File diff suppressed because it is too large
Load Diff
|
@ -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/
|
|
@ -1,28 +1,48 @@
|
|||
# Create a debian package for this project.
|
||||
#
|
||||
# This script is intended for use in a CI environment, however it can be run in
|
||||
# your development machine if you pass these variables with the apropiate
|
||||
# values:
|
||||
#
|
||||
# * CI_COMMIT_TAG=v1.2.3
|
||||
#
|
||||
# For example you can build and install this package in the very container it is
|
||||
# done in CI using these commands:
|
||||
#
|
||||
# podman run -it --rm --volume=./:/src --env CI_COMMIT_TAG=v1.2.3 docker.io/categulario/rust-cli-image:latest
|
||||
# cd src && cargo build --release && ./debpackage.sh
|
||||
# apt install ./debian-package/tiempo_1.2.3_amd64.deb
|
||||
# t --help
|
||||
COPYRIGHT_YEARS="2018 - "$(date "+%Y")
|
||||
DPKG_STAGING="debian-package"
|
||||
DPKG_DIR="${DPKG_STAGING}/dpkg"
|
||||
|
||||
PROJECT_MANTAINER="Abraham Toriz Cruz"
|
||||
PROJECT_HOMEPAGE="https://categulario.gitlab.io/tiempo/en"
|
||||
PROJECT_HOMEPAGE="https://gitlab.com/categulario/tiempo-rs"
|
||||
PROJECT_NAME=tiempo
|
||||
PROJECT_VERSION=${CI_COMMIT_TAG:1}
|
||||
PROJECT_BINARY=t
|
||||
PROJECT_DESCRIPTION="A command line time tracking application"
|
||||
|
||||
mkdir -p "${DPKG_DIR}"
|
||||
|
||||
DPKG_BASENAME=${PROJECT_NAME}
|
||||
DPKG_CONFLICTS=
|
||||
DPKG_VERSION=${PROJECT_VERSION}
|
||||
DPKG_VERSION=${CI_COMMIT_TAG:1}
|
||||
DPKG_ARCH=amd64
|
||||
DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb"
|
||||
DPKG_NAME="${DPKG_BASENAME}_${CI_COMMIT_TAG}_${DPKG_ARCH}.deb"
|
||||
DPKG_DEPENDS=
|
||||
DPKG_SECTION=utils
|
||||
|
||||
# Binary
|
||||
install -Dm755 "target/release/$PROJECT_BINARY" "${DPKG_DIR}/usr/bin/$PROJECT_BINARY"
|
||||
# README and LICENSE
|
||||
install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md"
|
||||
install -Dm644 "LICENSE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE"
|
||||
install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
|
||||
gzip -n --best "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/changelog"
|
||||
install -Dm644 "CHANGELOG.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/CHANGELOG.md"
|
||||
install -Dm644 "docs/build/man/$PROJECT_NAME.1.gz" "${DPKG_DIR}/usr/share/man/man1/$PROJECT_NAME.1.gz"
|
||||
install -Dm644 "completions/bash/t" "${DPKG_DIR}/usr/share/bash-completion/completions/t"
|
||||
install -Dm644 "completions/fish/t.fish" "${DPKG_DIR}/usr/share/fish/vendor_completions.d/t.fish"
|
||||
install -Dm644 "completions/zsh/_t" "${DPKG_DIR}/usr/share/zsh/site-functions/_t"
|
||||
|
||||
cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" <<EOF
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
|
@ -41,16 +61,15 @@ mkdir -p "${DPKG_DIR}/DEBIAN"
|
|||
cat > "${DPKG_DIR}/DEBIAN/control" <<EOF
|
||||
Package: ${DPKG_BASENAME}
|
||||
Version: ${DPKG_VERSION}
|
||||
Section: graphics
|
||||
Section: ${DPKG_SECTION}
|
||||
Priority: optional
|
||||
Maintainer: ${PROJECT_MANTAINER}
|
||||
Homepage: ${PROJECT_HOMEPAGE}
|
||||
Architecture: ${DPKG_ARCH}
|
||||
Provides: ${PROJECT_NAME}
|
||||
Depends: libgtk-3-0
|
||||
Depends: ${DPKG_DEPENDS}
|
||||
Conflicts: ${DPKG_CONFLICTS}
|
||||
Description: A simple infinite-canvas free-hand vector drawing application
|
||||
A simple infinite-canvas free-hand vector drawing application
|
||||
Description: ${PROJECT_DESCRIPTION}
|
||||
EOF
|
||||
DPKG_PATH="${DPKG_STAGING}/${DPKG_NAME}"
|
||||
# build dpkg
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,46 +1,44 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# some useful variables
|
||||
VERSION=${CI_COMMIT_TAG:1}
|
||||
PROJECT_NAME=tiempo
|
||||
PROJECT_BINARY=t
|
||||
ARCHIVENAME=$PROJECT_NAME-$VERSION-x86_64.tar.gz
|
||||
|
||||
# clone the repo
|
||||
git clone $BIN_REPO_URL $PROJECT_NAME-bin
|
||||
|
||||
# enter it
|
||||
cd $PROJECT_NAME-bin
|
||||
ARCHIVENAME=$PROJECT_NAME-$CI_COMMIT_TAG-x86_64.tar.gz
|
||||
|
||||
# get the sum from the artifacts
|
||||
SUM=( `cat ../$ARCHIVENAME.sum` )
|
||||
SUM=( `cat artifacts/$ARCHIVENAME.sum` )
|
||||
|
||||
# Generate the PKGBUILD
|
||||
echo "# Maintainer: Abraham Toriz <categulario at gmail dot com>
|
||||
pkgname=$PROJECT_NAME-bin
|
||||
pkgver=$VERSION
|
||||
pkgrel=1
|
||||
pkgdesc='A free-hand vector drawing application with infinite canvas'
|
||||
pkgdesc='A command line time tracking application'
|
||||
arch=('x86_64')
|
||||
url='https://gitlab.com/categulario/tiempo-rs'
|
||||
license=('GPL3')
|
||||
depends=('gtk3')
|
||||
depends=()
|
||||
optdepends=('sqlite: for manually editing the database')
|
||||
provides=('$PROJECT_NAME')
|
||||
conflicts=('$PROJECT_NAME')
|
||||
source=(\"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/generic/v$VERSION/v$VERSION/$PROJECT_NAME-\${pkgver}-x86_64.tar.gz\")
|
||||
source=(\"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/generic/$CI_COMMIT_TAG/$CI_COMMIT_TAG/$PROJECT_NAME-v\${pkgver}-x86_64.tar.gz\")
|
||||
sha256sums=('$SUM')
|
||||
|
||||
package() {
|
||||
cd \"\$srcdir/build\"
|
||||
install -Dm755 $PROJECT_BINARY \"\$pkgdir\"/usr/bin/$PROJECT_BINARY
|
||||
install -Dm755 bin/$PROJECT_BINARY \"\$pkgdir\"/usr/bin/$PROJECT_BINARY
|
||||
|
||||
install -Dm644 README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md
|
||||
install -Dm644 LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE
|
||||
install -Dm644 CHANGELOG.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/CHANGELOG.md
|
||||
install -Dm644 share/doc/$PROJECT_NAME/README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md
|
||||
install -Dm644 share/doc/$PROJECT_NAME/LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE
|
||||
install -Dm644 share/doc/$PROJECT_NAME/CHANGELOG.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/CHANGELOG.md
|
||||
install -Dm644 share/man/man1/$PROJECT_NAME.1.gz \"\$pkgdir\"/usr/share/man/man1/$PROJECT_NAME.1.gz
|
||||
install -Dm644 share/bash-completion/completions/t \"\$pkgdir\"/usr/share/bash-completion/completions/t
|
||||
install -Dm644 share/fish/vendor_completions.d/t.fish \"\$pkgdir\"/usr/share/fish/vendor_completions.d/t.fish
|
||||
install -Dm644 share/zsh/site-functions/_t \"\$pkgdir\"/usr/share/zsh/site-functions/_t
|
||||
}
|
||||
" | tee PKGBUILD > /dev/null
|
||||
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
git add .
|
||||
git commit -m "Release version $VERSION"
|
||||
git push
|
||||
|
|
|
@ -5,22 +5,17 @@ VERSION=${CI_COMMIT_TAG:1}
|
|||
PROJECT_NAME=tiempo
|
||||
PROJECT_BINARY=t
|
||||
|
||||
# clone the repo
|
||||
git clone $GIT_REPO_URL $PROJECT_NAME-git
|
||||
|
||||
# enter it
|
||||
cd $PROJECT_NAME-git
|
||||
|
||||
echo "# Maintainer: Abraham Toriz <categulario at gmail dot com>
|
||||
pkgname=$PROJECT_NAME-git
|
||||
pkgver=$VERSION
|
||||
pkgrel=1
|
||||
pkgdesc='Simple Gtk drawing application'
|
||||
pkgdesc='A command line time tracking application'
|
||||
arch=('i686' 'x86_64')
|
||||
url='https://gitlab.com/categulario/tiempo-rs'
|
||||
license=('GPL3')
|
||||
depends=('gtk3')
|
||||
makedepends=('cargo' 'git')
|
||||
depends=()
|
||||
optdepends=('sqlite: for manually editing the database')
|
||||
makedepends=('cargo' 'git' 'python-sphinx' 'python-tomlkit' 'gzip' 'make')
|
||||
provides=('$PROJECT_NAME')
|
||||
conflicts=('$PROJECT_NAME')
|
||||
source=('$PROJECT_NAME-git::git+https://gitlab.com/categulario/tiempo-rs')
|
||||
|
@ -34,18 +29,23 @@ pkgver() {
|
|||
build() {
|
||||
cd \"\$pkgname\"
|
||||
cargo build --release --locked
|
||||
cd docs
|
||||
make man
|
||||
gzip build/man/$PROJECT_NAME.1
|
||||
}
|
||||
|
||||
package() {
|
||||
cd \"\$pkgname\"
|
||||
install -Dm755 target/release/$PROJECT_NAME \"\$pkgdir\"/usr/bin/$PROJECT_NAME
|
||||
install -Dm755 target/release/$PROJECT_BINARY \"\$pkgdir\"/usr/bin/$PROJECT_BINARY
|
||||
|
||||
install -Dm644 README.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/README.md
|
||||
install -Dm644 LICENSE \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/LICENSE
|
||||
install -Dm644 CHANGELOG.md \"\$pkgdir\"/usr/share/doc/$PROJECT_NAME/CHANGELOG.md
|
||||
|
||||
install -Dm644 docs/build/man/$PROJECT_NAME.1.gz \"\$pkgdir\"/usr/share/man/man1/$PROJECT_NAME.1.gz
|
||||
install -Dm644 completions/bash/t \"\$pkgdir\"/usr/share/bash-completion/completions/t
|
||||
install -Dm644 completions/fish/t.fish \"\$pkgdir\"/usr/share/fish/vendor_completions.d/t.fish
|
||||
install -Dm644 completions/zsh/_t \"\$pkgdir\"/usr/share/zsh/site-functions/_t
|
||||
}" | tee PKGBUILD > /dev/null
|
||||
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
git add .
|
||||
git commit -m "Release version $VERSION"
|
||||
git push
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,112 +1,36 @@
|
|||
use std::convert::TryInto;
|
||||
use std::process::exit;
|
||||
use std::io;
|
||||
|
||||
use clap::{
|
||||
App, Arg, SubCommand, AppSettings, ArgMatches, crate_version, crate_authors,
|
||||
crate_description, crate_name,
|
||||
Command, Arg, SubCommand, command, value_parser,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use tiempo::error;
|
||||
use tiempo::database::SqliteDatabase;
|
||||
use tiempo::env::Env;
|
||||
use tiempo::config::Config;
|
||||
use tiempo::commands::{
|
||||
Command, Facts, r#in::InCommand, display::DisplayCommand,
|
||||
sheet::SheetCommand, today::TodayCommand, yesterday::YesterdayCommand,
|
||||
week::WeekCommand, month::MonthCommand, list::ListCommand, out::OutCommand,
|
||||
resume::ResumeCommand, backend::BackendCommand, kill::KillCommand,
|
||||
now::NowCommand, edit::EditCommand, archive::ArchiveCommand,
|
||||
configure::ConfigureCommand,
|
||||
};
|
||||
use tiempo::io::Streams;
|
||||
|
||||
lazy_static! {
|
||||
// https://regex101.com/r/V9zYQu/1/
|
||||
pub static ref NUMBER_RE: Regex = Regex::new(r"^\d+$").unwrap();
|
||||
}
|
||||
|
||||
fn is_number(v: String) -> Result<(), String>{
|
||||
if NUMBER_RE.is_match(&v) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("'{}' is not a valid number", v))
|
||||
}
|
||||
}
|
||||
|
||||
fn error_trap(matches: ArgMatches) -> error::Result<()> {
|
||||
let env = Env::read();
|
||||
let facts = Facts {
|
||||
config: Config::read(env.timetrap_config_file.as_deref())?,
|
||||
env,
|
||||
now: Utc::now(),
|
||||
};
|
||||
|
||||
if let Some(_matches) = matches.subcommand_matches("backend") {
|
||||
return BackendCommand::handle(&facts.config);
|
||||
}
|
||||
|
||||
let mut streams = Streams {
|
||||
db: SqliteDatabase::from_path_or_create(&facts.config.database_file)?,
|
||||
r#in: io::BufReader::new(io::stdin()),
|
||||
out: io::stdout(),
|
||||
err: io::stderr(),
|
||||
};
|
||||
|
||||
match matches.subcommand() {
|
||||
("in", Some(matches)) => InCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("out", Some(matches)) => OutCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("resume", Some(matches)) => ResumeCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
|
||||
("display", Some(matches)) => DisplayCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("today", Some(matches)) => TodayCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("yesterday", Some(matches)) => YesterdayCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("week", Some(matches)) => WeekCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("month", Some(matches)) => MonthCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
|
||||
("sheet", Some(matches)) => SheetCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("list", Some(matches)) => ListCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("kill", Some(matches)) => KillCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("now", Some(matches)) => NowCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("edit", Some(matches)) => EditCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("archive", Some(matches)) => ArchiveCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
("configure", Some(matches)) => ConfigureCommand::handle(matches.try_into()?, &mut streams, &facts),
|
||||
|
||||
(cmd, _) => Err(error::Error::UnimplementedCommand(cmd.into())),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Lets first declar some args that repeat here and there
|
||||
pub fn make_cli() -> Command<'static> {
|
||||
// Let's first declare some args that repeat here and there
|
||||
let start_arg = Arg::with_name("start")
|
||||
.long("start").short("s")
|
||||
.long("start").short('s')
|
||||
.takes_value(true).value_name("TIME")
|
||||
.help("Include entries that start on this date or later");
|
||||
|
||||
let end_arg = Arg::with_name("end")
|
||||
.long("end").short("e")
|
||||
.long("end").short('e')
|
||||
.takes_value(true).value_name("TIME")
|
||||
.help("Include entries that start on this date or earlier");
|
||||
|
||||
let ids_arg = Arg::with_name("ids")
|
||||
.short("v").long("ids")
|
||||
.short('v').long("ids")
|
||||
.help("Print database ids (for use with edit)");
|
||||
|
||||
let grep_arg = Arg::with_name("grep")
|
||||
.long("grep").short("g")
|
||||
.long("grep").short('g')
|
||||
.takes_value(true).value_name("REGEXP")
|
||||
.help("Include entries where the note matches this regexp.");
|
||||
.help("Only include entries whose note matches this regular expression");
|
||||
|
||||
let format_arg = Arg::with_name("format")
|
||||
.short("f").long("format")
|
||||
.short('f').long("format")
|
||||
.takes_value(true).value_name("FORMAT")
|
||||
.help(
|
||||
"The output format. Valid built-in formats are ical, csv, json, \
|
||||
ids, and text. Documentation on defining custom formats can be \
|
||||
found at https://gitlab.com/categulario/tiempo"
|
||||
"The output format. Valid built-in formats are chart, text, ical, \
|
||||
csv, json and ids. Documentation on defining custom formats can be \
|
||||
found at https://tiempo.categulario.xyz or the man page included \
|
||||
with the installation."
|
||||
);
|
||||
|
||||
let sheet_arg = Arg::with_name("sheet")
|
||||
|
@ -124,22 +48,18 @@ fn main() {
|
|||
let id_arg = Arg::with_name("id")
|
||||
.long("id")
|
||||
.takes_value(true).value_name("ID")
|
||||
.validator(is_number);
|
||||
.value_parser(value_parser!(u64));
|
||||
|
||||
let interactive_arg = Arg::with_name("interactive")
|
||||
.short("i")
|
||||
.short('i')
|
||||
.long("interactive")
|
||||
.takes_value(false)
|
||||
.conflicts_with("id")
|
||||
.help("Choose an entry of the (unique) last N interactively");
|
||||
|
||||
// Now declar this app's cli
|
||||
let matches = App::new("Tiempo")
|
||||
.name(crate_name!())
|
||||
.setting(AppSettings::SubcommandRequired)
|
||||
.version(crate_version!())
|
||||
.author(crate_authors!())
|
||||
.about(crate_description!())
|
||||
let cli = command!()
|
||||
.subcommand_required(true)
|
||||
|
||||
.subcommand(SubCommand::with_name("archive")
|
||||
.visible_alias("a")
|
||||
|
@ -149,9 +69,14 @@ fn main() {
|
|||
.arg(grep_arg.clone())
|
||||
.arg(sheet_arg.clone().help("Archive entries from this sheet instead of the current one"))
|
||||
.arg(Arg::with_name("fake")
|
||||
.short("f").long("fake")
|
||||
.short('f').long("fake")
|
||||
.help("Don't actually archive the entries, just display them")
|
||||
)
|
||||
.arg(Arg::with_name("time")
|
||||
.short('t').long("time")
|
||||
.takes_value(true).value_name("HOURS")
|
||||
.help("Time in hours to archive. Archived time will be equal or less than this.")
|
||||
)
|
||||
)
|
||||
|
||||
.subcommand(SubCommand::with_name("backend")
|
||||
|
@ -166,7 +91,7 @@ fn main() {
|
|||
.long("round-in-seconds")
|
||||
.takes_value(true)
|
||||
.value_name("SECONDS")
|
||||
.validator(is_number)
|
||||
.value_parser(value_parser!(u64))
|
||||
.help("The duration of time to use for rounding with the -r flag. Default: 900 (15 m)"))
|
||||
.arg(Arg::with_name("database_file")
|
||||
.long("database-file")
|
||||
|
@ -210,13 +135,13 @@ fn main() {
|
|||
.long("week-start")
|
||||
.takes_value(true)
|
||||
.value_name("DAY")
|
||||
.possible_values(&["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"])
|
||||
.possible_values(["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"])
|
||||
.help("The day of the week to use as the start of the week for t week. Default: monday"))
|
||||
.arg(Arg::with_name("interactive_entries")
|
||||
.long("interactive-entries")
|
||||
.takes_value(true)
|
||||
.value_name("N")
|
||||
.validator(is_number)
|
||||
.value_parser(value_parser!(u64))
|
||||
.help("How many unique previous notes to show when selecting interactively"))
|
||||
)
|
||||
|
||||
|
@ -271,10 +196,10 @@ fn main() {
|
|||
.arg(grep_arg.clone())
|
||||
.arg(sheet_arg.clone())
|
||||
.arg(Arg::with_name("month")
|
||||
.long("month").short("m")
|
||||
.long("month").short('m')
|
||||
.takes_value(true).value_name("TIME")
|
||||
.aliases(&["s", "start"])
|
||||
.possible_values(&[
|
||||
.possible_values([
|
||||
"this", "current", "last", "jan", "january", "feb",
|
||||
"february", "mar", "march", "apr", "april", "may", "jun",
|
||||
"june", "jul", "july", "aug", "august", "sep", "september",
|
||||
|
@ -322,8 +247,11 @@ fn main() {
|
|||
.visible_alias("l")
|
||||
.about("List existing sheets")
|
||||
.arg(Arg::with_name("all")
|
||||
.short("a").long("all")
|
||||
.short('a').long("all")
|
||||
.help("List archive sheets also"))
|
||||
.arg(Arg::with_name("flat")
|
||||
.short('f').long("flat")
|
||||
.help("show only the sheet names"))
|
||||
)
|
||||
|
||||
.subcommand(SubCommand::with_name("kill")
|
||||
|
@ -333,12 +261,12 @@ fn main() {
|
|||
.arg(Arg::with_name("sheet")
|
||||
.takes_value(true).value_name("SHEET")
|
||||
.conflicts_with_all(&["id", "last"])
|
||||
.required_unless_one(&["id", "last"])
|
||||
.required_unless_one(["id", "last"])
|
||||
.help(
|
||||
"Delete an entire sheet by its name"
|
||||
))
|
||||
.arg(Arg::with_name("last")
|
||||
.short("l").long("last")
|
||||
.short('l').long("last")
|
||||
.takes_value(false)
|
||||
.help("Delete the last entry of the current sheet"))
|
||||
)
|
||||
|
@ -356,12 +284,12 @@ fn main() {
|
|||
.arg(end_arg.clone().help("Set this as the end time"))
|
||||
.arg(
|
||||
Arg::with_name("append")
|
||||
.long("append").short("z")
|
||||
.long("append").short('z')
|
||||
.help("Append to the current note instead of replacing it. The delimiter between appended notes is configurable (see configure)")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("move")
|
||||
.short("m").long("move")
|
||||
.short('m').long("move")
|
||||
.takes_value(true)
|
||||
.value_name("SHEET")
|
||||
.help("Move entry to another sheet")
|
||||
|
@ -372,12 +300,7 @@ fn main() {
|
|||
.value_name("NOTE")
|
||||
.help("The note text. It will replace the previous one unless --append is given")
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
.get_matches();
|
||||
|
||||
if let Err(e) = error_trap(matches) {
|
||||
eprintln!("{}", e);
|
||||
exit(1);
|
||||
}
|
||||
cli
|
||||
}
|
|
@ -64,7 +64,7 @@ impl Default for Facts {
|
|||
}
|
||||
|
||||
pub trait Command<'a> {
|
||||
type Args: TryFrom<&'a ArgMatches<'a>>;
|
||||
type Args: TryFrom<&'a ArgMatches>;
|
||||
|
||||
fn handle<D: Database, I: BufRead, O: Write, E: Write>(args: Self::Args, streams: &mut Streams<D, I, O, E>, facts: &Facts) -> Result<()>;
|
||||
}
|
||||
|
|
|
@ -2,29 +2,31 @@ use std::convert::TryFrom;
|
|||
use std::io::{BufRead, Write};
|
||||
|
||||
use clap::ArgMatches;
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::commands::{Command, Facts};
|
||||
use crate::timeparse::parse_time;
|
||||
use crate::timeparse::{parse_time, parse_hours};
|
||||
use crate::old::{entries_or_warning, time_or_warning};
|
||||
use crate::formatters::text;
|
||||
use crate::regex::parse_regex;
|
||||
use crate::interactive::ask;
|
||||
use crate::io::Streams;
|
||||
use crate::models::Entry;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Args {
|
||||
start: Option<DateTime<Utc>>,
|
||||
end: Option<DateTime<Utc>>,
|
||||
grep: Option<Regex>,
|
||||
hours: Option<u16>,
|
||||
fake: bool,
|
||||
sheet: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
|
||||
|
@ -32,12 +34,29 @@ impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
|||
start: matches.value_of("start").map(parse_time).transpose()?,
|
||||
end: matches.value_of("end").map(parse_time).transpose()?,
|
||||
grep: matches.value_of("grep").map(parse_regex).transpose()?,
|
||||
hours: matches.value_of("time").map(parse_hours).transpose()?,
|
||||
fake: matches.is_present("fake"),
|
||||
sheet: matches.value_of("sheet").map(|s| s.to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the given Entry such that it only lasts the given `time`, and return
|
||||
/// the data needed to create a new entry with mostly the same attributes such
|
||||
/// that it accounts for the time substracted from the original.
|
||||
fn split_entry(entry: &mut Entry, time: Duration) -> (DateTime<Utc>, Option<DateTime<Utc>>, Option<String>, String) {
|
||||
let Entry {
|
||||
id: _, note, start, end, sheet,
|
||||
} = entry.clone();
|
||||
|
||||
let old_entry_end = start + time;
|
||||
let new_entry_start = old_entry_end;
|
||||
|
||||
entry.end = Some(old_entry_end);
|
||||
|
||||
(new_entry_start, end, note, sheet)
|
||||
}
|
||||
|
||||
pub struct ArchiveCommand {}
|
||||
|
||||
impl<'a> Command<'a> for ArchiveCommand {
|
||||
|
@ -50,32 +69,111 @@ impl<'a> Command<'a> for ArchiveCommand {
|
|||
O: Write,
|
||||
E: Write,
|
||||
{
|
||||
let mut entries = {
|
||||
let start = args.start.map(|s| time_or_warning(s, &streams.db)).transpose()?.map(|s| s.0);
|
||||
let end = args.end.map(|e| time_or_warning(e, &streams.db)).transpose()?.map(|e| e.0);
|
||||
let current_sheet = streams.db.current_sheet()?;
|
||||
let sheet = args.sheet.unwrap_or(current_sheet);
|
||||
// Get all entries from the database that match the filter criteria
|
||||
// given from the command line: start time, end time and sheet.
|
||||
let entries = {
|
||||
let started_after = args.start.map(|s| time_or_warning(s, &streams.db)).transpose()?.map(|s| s.0);
|
||||
let started_before = args.end.map(|e| time_or_warning(e, &streams.db)).transpose()?.map(|e| e.0);
|
||||
let sheet = args.sheet.map(Ok).unwrap_or_else(|| streams.db.current_sheet())?;
|
||||
|
||||
streams.db.entries_by_sheet(&sheet, start, end)?
|
||||
};
|
||||
let mut entries = streams.db.entries_by_sheet(&sheet, started_after, started_before)?;
|
||||
|
||||
// only archive those entries that are finished.
|
||||
entries.retain(|e| e.end.is_some());
|
||||
|
||||
if let Some(re) = args.grep {
|
||||
entries.retain(|e| re.is_match(&e.note.clone().unwrap_or_default()));
|
||||
}
|
||||
|
||||
entries
|
||||
};
|
||||
|
||||
// If the user requested to archive entries by a total time then not all
|
||||
// entries will be archived, and instead just those oldest ones that
|
||||
// accumulate the given time will be. If the total time of the filtered
|
||||
// entries is more than the requested time the last one will be split
|
||||
// into two pieces.
|
||||
let (time, entries, new, extra_msg) = if let Some(hours) = args.hours {
|
||||
let requested_time = Duration::hours(hours as i64);
|
||||
// archive the maximum amount of consecutive entries whose
|
||||
// accumulated time is not bigger that `time`.
|
||||
let mut selected_entries = Vec::with_capacity(entries.len());
|
||||
let mut accumulated_time = Duration::seconds(0);
|
||||
let mut new = None;
|
||||
|
||||
for entry in entries {
|
||||
// Can unwrap because only entries with an end time get this far
|
||||
let timespan = entry.timespan().unwrap();
|
||||
|
||||
if accumulated_time < requested_time {
|
||||
if accumulated_time + timespan > requested_time {
|
||||
// should split the last entry
|
||||
let missing_time = requested_time - accumulated_time;
|
||||
let mut entry = entry;
|
||||
let parts = split_entry(&mut entry, missing_time);
|
||||
new.replace(parts);
|
||||
selected_entries.push(entry);
|
||||
accumulated_time = accumulated_time + missing_time;
|
||||
} else {
|
||||
// fits perfectly, just add it
|
||||
selected_entries.push(entry);
|
||||
accumulated_time = accumulated_time + timespan;
|
||||
}
|
||||
} else {
|
||||
// accumulated_time is equal or higher than requested_time,
|
||||
// no more entries are admitted
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let msg = if new.is_some() {
|
||||
String::from("\nAdditionally an entry will be split so that the total archived time is exact.")
|
||||
} else if accumulated_time < requested_time {
|
||||
let requested_time_str = text::format_hours(requested_time);
|
||||
let missing_time_str = text::format_hours(requested_time - accumulated_time);
|
||||
format!("\nThere were not enough entries to fulfill the requested time of {requested_time_str} (difference: {missing_time_str}).")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
(text::format_hours(accumulated_time), selected_entries, new, msg)
|
||||
} else {
|
||||
(text::format_hours(
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|e| e.end.map(|end| end - e.start))
|
||||
.fold(Duration::seconds(0), |acc, new| {
|
||||
acc + new
|
||||
})
|
||||
), entries, None, String::from(""))
|
||||
};
|
||||
|
||||
let n = entries.len();
|
||||
let n_entries = if n == 1 {
|
||||
String::from("1 entry")
|
||||
} else {
|
||||
format!("{n} entries")
|
||||
};
|
||||
|
||||
if args.fake {
|
||||
let (entries, _) = entries_or_warning(entries, &streams.db)?;
|
||||
|
||||
writeln!(streams.out, "These entries would be archived:\n")?;
|
||||
|
||||
text::print_formatted(
|
||||
entries,
|
||||
&mut streams.out,
|
||||
facts,
|
||||
true,
|
||||
)?;
|
||||
} else if ask(streams, &format!("Archive {} entries?", entries.len()))? {
|
||||
} else if ask(streams, &format!("A total of {n_entries} accounting for {time} will be archived.{extra_msg}\nProceed?"))? {
|
||||
for entry in entries {
|
||||
streams.db.entry_update(entry.id, entry.start, entry.end, entry.note, &format!("_{}", entry.sheet))?;
|
||||
}
|
||||
|
||||
if let Some((start, end, note, sheet)) = new {
|
||||
streams.db.entry_insert(start, end, note, &sheet)?;
|
||||
}
|
||||
} else {
|
||||
writeln!(streams.out, "Ok, they're still there")?;
|
||||
}
|
||||
|
@ -83,3 +181,262 @@ impl<'a> Command<'a> for ArchiveCommand {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use chrono::TimeZone;
|
||||
|
||||
use crate::models::Entry;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn archive_archives() {
|
||||
let args: Args = Default::default();
|
||||
let mut streams = Streams::fake(b"y\n");
|
||||
let facts = Facts::new();
|
||||
|
||||
streams.db.set_current_sheet("foo").unwrap();
|
||||
streams.db.entry_insert(facts.now - Duration::hours(3), Some(facts.now - Duration::minutes(90)), Some("first".into()), "foo").unwrap();
|
||||
streams.db.entry_insert(facts.now - Duration::minutes(90), Some(facts.now), Some("second".into()), "foo").unwrap();
|
||||
|
||||
ArchiveCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
let remaining = streams.db.entries_by_sheet("foo", None, None).unwrap();
|
||||
let archived = streams.db.entries_by_sheet("_foo", None, None).unwrap();
|
||||
|
||||
assert_eq!(String::from_utf8_lossy(&streams.out), "A total of 2 entries accounting for 3h will be archived.\nProceed? [y/N] ");
|
||||
|
||||
// First entry gets archived whole, second entry gets split in two:
|
||||
// - a 30 minute piece to complete the requested 2 hour span
|
||||
// - a 1 hour piece that remains unarchived
|
||||
assert_eq!(archived, vec![
|
||||
Entry {
|
||||
id: 1,
|
||||
note: Some("first".into()),
|
||||
start: facts.now - Duration::hours(3),
|
||||
end: Some(facts.now - Duration::minutes(90)),
|
||||
sheet: "_foo".into(),
|
||||
},
|
||||
Entry {
|
||||
id: 2,
|
||||
note: Some("second".into()),
|
||||
start: facts.now - Duration::minutes(90),
|
||||
end: Some(facts.now),
|
||||
sheet: "_foo".into(),
|
||||
},
|
||||
]);
|
||||
assert_eq!(remaining, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_running_entry_is_archived() {
|
||||
let args: Args = Default::default();
|
||||
let mut streams = Streams::fake(b"y\n");
|
||||
let facts = Facts::new();
|
||||
|
||||
streams.db.set_current_sheet("foo").unwrap();
|
||||
streams.db.entry_insert(facts.now - Duration::hours(3), Some(facts.now - Duration::minutes(90)), Some("first".into()), "foo").unwrap();
|
||||
streams.db.entry_insert(facts.now - Duration::minutes(90), None, Some("running".into()), "foo").unwrap();
|
||||
|
||||
ArchiveCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
let remaining = streams.db.entries_by_sheet("foo", None, None).unwrap();
|
||||
let archived = streams.db.entries_by_sheet("_foo", None, None).unwrap();
|
||||
|
||||
assert_eq!(String::from_utf8_lossy(&streams.out), "A total of 1 entry accounting for 1h 30m will be archived.\nProceed? [y/N] ");
|
||||
|
||||
// First entry gets archived whole, second entry gets split in two:
|
||||
// - a 30 minute piece to complete the requested 2 hour span
|
||||
// - a 1 hour piece that remains unarchived
|
||||
assert_eq!(archived, vec![
|
||||
Entry {
|
||||
id: 1,
|
||||
note: Some("first".into()),
|
||||
start: facts.now - Duration::hours(3),
|
||||
end: Some(facts.now - Duration::minutes(90)),
|
||||
sheet: "_foo".into(),
|
||||
},
|
||||
]);
|
||||
assert_eq!(remaining, vec![
|
||||
Entry {
|
||||
id: 2,
|
||||
note: Some("running".into()),
|
||||
start: facts.now - Duration::minutes(90),
|
||||
end: None,
|
||||
sheet: "foo".into(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entries_are_split_properly() {
|
||||
let mut old_entry = Entry {
|
||||
id: 1,
|
||||
start: Utc.with_ymd_and_hms(2022, 7, 29, 10, 0, 0).unwrap(),
|
||||
end: Some(Utc.with_ymd_and_hms(2022, 7, 29, 11, 0, 0).unwrap()),
|
||||
note: Some("an entry".to_string()),
|
||||
sheet: "foo".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(split_entry(&mut old_entry, Duration::minutes(25)), (
|
||||
Utc.with_ymd_and_hms(2022, 7, 29, 10, 25, 0).unwrap(),
|
||||
Some(Utc.with_ymd_and_hms(2022, 7, 29, 11, 0, 0).unwrap()),
|
||||
Some("an entry".to_string()),
|
||||
"foo".to_string(),
|
||||
));
|
||||
assert_eq!(old_entry, Entry {
|
||||
id: 1,
|
||||
start: Utc.with_ymd_and_hms(2022, 7, 29, 10, 0, 0).unwrap(),
|
||||
end: Some(Utc.with_ymd_and_hms(2022, 7, 29, 10, 25, 0).unwrap()),
|
||||
note: Some("an entry".to_string()),
|
||||
sheet: "foo".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_by_hours() {
|
||||
let args = Args {
|
||||
hours: Some(2),
|
||||
..Default::default()
|
||||
};
|
||||
let mut streams = Streams::fake(b"y\n");
|
||||
let facts = Facts::new();
|
||||
|
||||
streams.db.set_current_sheet("foo").unwrap();
|
||||
streams.db.entry_insert(facts.now - Duration::hours(3), Some(facts.now - Duration::minutes(90)), Some("first".into()), "foo").unwrap();
|
||||
streams.db.entry_insert(facts.now - Duration::minutes(90), Some(facts.now), Some("second".into()), "foo").unwrap();
|
||||
|
||||
ArchiveCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
let remaining = streams.db.entries_by_sheet("foo", None, None).unwrap();
|
||||
let archived = streams.db.entries_by_sheet("_foo", None, None).unwrap();
|
||||
|
||||
assert_str_eq!(String::from_utf8_lossy(&streams.out), "A total of 2 entries accounting for 2h will be archived.
|
||||
Additionally an entry will be split so that the total archived time is exact.
|
||||
Proceed? [y/N] ");
|
||||
|
||||
// First entry gets archived whole, second entry gets split in two:
|
||||
// - a 30 minute piece to complete the requested 2 hour span
|
||||
// - a 1 hour piece that remains unarchived
|
||||
assert_eq!(archived, vec![
|
||||
Entry {
|
||||
id: 1,
|
||||
note: Some("first".into()),
|
||||
start: facts.now - Duration::hours(3),
|
||||
end: Some(facts.now - Duration::minutes(90)),
|
||||
sheet: "_foo".into(),
|
||||
},
|
||||
Entry {
|
||||
id: 2,
|
||||
note: Some("second".into()),
|
||||
start: facts.now - Duration::minutes(90),
|
||||
end: Some(facts.now - Duration::hours(1)),
|
||||
sheet: "_foo".into(),
|
||||
},
|
||||
]);
|
||||
assert_eq!(remaining, vec![
|
||||
Entry {
|
||||
id: 3,
|
||||
note: Some("second".into()),
|
||||
start: facts.now - Duration::hours(1),
|
||||
end: Some(facts.now),
|
||||
sheet: "foo".into(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_enough_entries_to_archive_time() {
|
||||
let args = Args {
|
||||
hours: Some(2),
|
||||
..Default::default()
|
||||
};
|
||||
let mut streams = Streams::fake(b"y\n");
|
||||
let facts = Facts::new();
|
||||
|
||||
streams.db.set_current_sheet("foo").unwrap();
|
||||
streams.db.entry_insert(facts.now - Duration::hours(3), Some(facts.now - Duration::minutes(90)), Some("first".into()), "foo").unwrap();
|
||||
|
||||
ArchiveCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
let remaining = streams.db.entries_by_sheet("foo", None, None).unwrap();
|
||||
let archived = streams.db.entries_by_sheet("_foo", None, None).unwrap();
|
||||
|
||||
assert_str_eq!(String::from_utf8_lossy(&streams.out), "A total of 1 entry accounting for 1h 30m will be archived.
|
||||
There were not enough entries to fulfill the requested time of 2h (difference: 30m).
|
||||
Proceed? [y/N] ");
|
||||
|
||||
// First entry gets archived whole, second entry gets split in two:
|
||||
// - a 30 minute piece to complete the requested 2 hour span
|
||||
// - a 1 hour piece that remains unarchived
|
||||
assert_eq!(archived, vec![
|
||||
Entry {
|
||||
id: 1,
|
||||
note: Some("first".into()),
|
||||
start: facts.now - Duration::hours(3),
|
||||
end: Some(facts.now - Duration::minutes(90)),
|
||||
sheet: "_foo".into(),
|
||||
},
|
||||
]);
|
||||
assert_eq!(remaining, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fake_and_split_work_well_together() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let args = Args {
|
||||
hours: Some(2),
|
||||
fake: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut streams = Streams::fake(b"y\n");
|
||||
let facts = Facts::new();
|
||||
let time_a = Utc.with_ymd_and_hms(2022, 8, 1, 10, 0, 0).unwrap();
|
||||
let time_b = time_a + Duration::minutes(90);
|
||||
let time_d = time_a + Duration::hours(3);
|
||||
|
||||
streams.db.set_current_sheet("foo").unwrap();
|
||||
streams.db.entry_insert(time_a, Some(time_b), Some("first".into()), "foo").unwrap();
|
||||
streams.db.entry_insert(time_b, Some(time_d), Some("second".into()), "foo").unwrap();
|
||||
|
||||
ArchiveCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
let remaining = streams.db.entries_by_sheet("foo", None, None).unwrap();
|
||||
let archived = streams.db.entries_by_sheet("_foo", None, None).unwrap();
|
||||
|
||||
assert_str_eq!(String::from_utf8_lossy(&streams.out), "These entries would be archived:
|
||||
|
||||
Timesheet: foo
|
||||
ID Day Start End Duration Notes
|
||||
1 Mon Aug 01, 2022 04:00:00 - 05:30:00 1:30:00 first
|
||||
2 05:30:00 - 06:00:00 0:30:00 second
|
||||
2:00:00
|
||||
------------------------------------------------------------
|
||||
Total 2:00:00
|
||||
");
|
||||
|
||||
// First entry gets archived whole, second entry gets split in two:
|
||||
// - a 30 minute piece to complete the requested 2 hour span
|
||||
// - a 1 hour piece that remains unarchived
|
||||
assert_eq!(archived, vec![]);
|
||||
assert_eq!(remaining, vec![
|
||||
Entry {
|
||||
id: 1,
|
||||
note: Some("first".into()),
|
||||
start: time_a,
|
||||
end: Some(time_b),
|
||||
sheet: "foo".into(),
|
||||
},
|
||||
Entry {
|
||||
id: 2,
|
||||
note: Some("second".into()),
|
||||
start: time_b,
|
||||
end: Some(time_d),
|
||||
sheet: "foo".into(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,14 +53,14 @@ impl Args {
|
|||
fn yes_no_none(matches: &ArgMatches, opt: &str) -> Option<bool> {
|
||||
if matches.is_present(opt) {
|
||||
Some(true)
|
||||
} else if matches.is_present(&format!("no_{}", opt)) {
|
||||
} else if matches.is_present(format!("no_{}", opt)) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
|
||||
|
|
|
@ -98,7 +98,7 @@ pub struct Args {
|
|||
sheet: Option<Sheet>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
|
||||
|
@ -130,7 +130,7 @@ impl<'a> Command<'a> for DisplayCommand {
|
|||
args.end,
|
||||
args.sheet,
|
||||
streams,
|
||||
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
|
||||
args.format.unwrap_or_else(|| facts.config.commands.display.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
|
||||
args.ids,
|
||||
args.grep,
|
||||
facts
|
||||
|
@ -141,10 +141,10 @@ impl<'a> Command<'a> for DisplayCommand {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::TimeZone;
|
||||
use pretty_assertions::assert_eq;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
|
||||
use crate::database::SqliteDatabase;
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -170,7 +170,7 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&streams.err),
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -178,14 +178,14 @@ mod tests {
|
|||
fn filter_by_start() {
|
||||
let args = Args {
|
||||
format: Some(Formatter::Csv),
|
||||
start: Some(Utc.ymd(2021, 6, 30).and_hms(10, 5, 0)),
|
||||
start: Some(Utc.with_ymd_and_hms(2021, 6, 30, 10, 5, 0).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut streams = Streams::fake(b"");
|
||||
let facts = Facts::new();
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
|
@ -204,8 +204,8 @@ mod tests {
|
|||
let mut streams = Streams::fake(b"");
|
||||
let facts = Facts::new();
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("adios".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("adios".into()), "default").unwrap();
|
||||
|
||||
entries_for_display(None, None, None, &mut streams, Formatter::Csv, true, Some("io".parse().unwrap()), &facts).unwrap();
|
||||
|
||||
|
@ -229,13 +229,13 @@ mod tests {
|
|||
let facts = Facts::new();
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "sheet2").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap()), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap()), None, "sheet2").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 13, 0, 0).unwrap()), None, "sheet1").unwrap();
|
||||
|
||||
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), "Timesheet: sheet1
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Timesheet: sheet1
|
||||
Day Start End Duration Notes
|
||||
Wed Jun 30, 2021 04:00:00 - 05:00:00 1:00:00
|
||||
06:00:00 - 07:00:00 1:00:00
|
||||
|
@ -264,9 +264,9 @@ Timesheet: sheet2
|
|||
let facts = Facts::new();
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0)), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0)), None, "_sheet2").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(12, 0, 0), Some(Utc.ymd(2021, 6, 30).and_hms(13, 0, 0)), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap()), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap()), None, "_sheet2").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 13, 0, 0).unwrap()), None, "sheet1").unwrap();
|
||||
|
||||
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
|
@ -296,8 +296,8 @@ Timesheet: sheet1
|
|||
|
||||
let args = Args {
|
||||
format: Some(Formatter::Csv),
|
||||
start: Some(Utc.ymd(2021, 6, 29).and_hms(12, 0, 0)),
|
||||
end: Some(Utc.ymd(2021, 6, 29).and_hms(13, 0, 0)),
|
||||
start: Some(Utc.with_ymd_and_hms(2021, 6, 29, 12, 0, 0).unwrap()),
|
||||
end: Some(Utc.with_ymd_and_hms(2021, 6, 29, 13, 0, 0).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut streams = Streams::fake(b"").with_db(
|
||||
|
@ -317,7 +317,7 @@ Timesheet: sheet1
|
|||
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&streams.err),
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -332,8 +332,33 @@ Timesheet: sheet1
|
|||
..Default::default()
|
||||
});
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
|
||||
assert_eq!(String::from_utf8_lossy(&streams.err), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respect_command_default_formatter() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let args = Default::default();
|
||||
let mut streams = Streams::fake(b"");
|
||||
let facts = Facts::new().with_config(Config {
|
||||
commands: CommandsSettings {
|
||||
display: BaseCommandSettings {
|
||||
default_formatter: Some(Formatter::Ids),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
DisplayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ impl Args {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
|
||||
|
@ -142,7 +142,7 @@ mod tests {
|
|||
note: Some("new note".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
|
||||
let an_hour_ago = now - Duration::hours(1);
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
|
@ -175,7 +175,7 @@ mod tests {
|
|||
note: Some("new note".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
|
||||
let an_hour_ago = now - Duration::hours(1);
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
|
@ -198,7 +198,7 @@ mod tests {
|
|||
fn edit_start() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
|
||||
let args = Args {
|
||||
start: Some(now - Duration::minutes(30)),
|
||||
..Default::default()
|
||||
|
@ -224,7 +224,7 @@ mod tests {
|
|||
fn edit_end() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
|
||||
let args = Args {
|
||||
end: Some(now - Duration::minutes(30)),
|
||||
..Default::default()
|
||||
|
@ -255,7 +255,7 @@ mod tests {
|
|||
append: true,
|
||||
..Default::default()
|
||||
};
|
||||
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
|
||||
let an_hour_ago = now - Duration::hours(1);
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
|
@ -277,7 +277,7 @@ mod tests {
|
|||
fn edit_move() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
|
||||
let args = Args {
|
||||
r#move: Some("new sheet".to_owned()),
|
||||
..Default::default()
|
||||
|
@ -309,7 +309,7 @@ mod tests {
|
|||
append: true,
|
||||
..Default::default()
|
||||
};
|
||||
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
|
||||
let an_hour_ago = now - Duration::hours(1);
|
||||
let facts = Facts::new().with_now(now).with_config(Config {
|
||||
append_notes_delimiter: ";".to_owned(),
|
||||
|
@ -346,8 +346,8 @@ mod tests {
|
|||
let mut streams = Streams::fake(b"").with_db(
|
||||
SqliteDatabase::from_path(&database_file).unwrap()
|
||||
);
|
||||
let now = Utc.ymd(2021, 8, 3).and_hms(20, 29, 0);
|
||||
let new_end = Utc.ymd(2021, 6, 29).and_hms(14, 26, 52);
|
||||
let now = Utc.with_ymd_and_hms(2021, 8, 3, 20, 29, 0).unwrap();
|
||||
let new_end = Utc.with_ymd_and_hms(2021, 6, 29, 14, 26, 52).unwrap();
|
||||
let args = Args {
|
||||
end: Some(new_end),
|
||||
..Default::default()
|
||||
|
@ -365,7 +365,7 @@ mod tests {
|
|||
");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&streams.err),
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
|
||||
);
|
||||
|
||||
std::mem::drop(streams.db);
|
||||
|
|
|
@ -18,7 +18,7 @@ pub struct Args {
|
|||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
|
||||
|
@ -177,7 +177,7 @@ mod tests {
|
|||
assert_eq!(&String::from_utf8_lossy(&streams.out), "Checked into sheet \"default\".\n");
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.err),
|
||||
"[WARNING] You are using the old timetrap format, it is advised that \
|
||||
you update your database using t migrate\n");
|
||||
you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -17,7 +17,7 @@ pub enum Args {
|
|||
Last,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(args: &'a ArgMatches) -> Result<Args> {
|
||||
|
|
|
@ -19,14 +19,16 @@ use super::{Command, Facts};
|
|||
#[derive(Default)]
|
||||
pub struct Args {
|
||||
all: bool,
|
||||
flat: bool,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &ArgMatches) -> Result<Args> {
|
||||
Ok(Args {
|
||||
all: matches.is_present("all"),
|
||||
flat: matches.is_present("flat"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -43,15 +45,13 @@ impl<'a> Command<'a> for ListCommand {
|
|||
O: Write,
|
||||
E: Write,
|
||||
{
|
||||
let today = facts.now.with_timezone(&Local).date().and_hms(0, 0, 0).with_timezone(&Utc);
|
||||
let today = facts.now.with_timezone(&Local).date_naive().and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Utc).unwrap();
|
||||
let entries = if args.all {
|
||||
streams.db.entries_full(None, None)?
|
||||
} else {
|
||||
streams.db.entries_all_visible(None, None)?
|
||||
};
|
||||
|
||||
let (mut entries, needs_warning) = entries_or_warning(entries, &streams.db)?;
|
||||
|
||||
let current = streams.db.current_sheet()?;
|
||||
let last = streams.db.last_sheet()?;
|
||||
|
||||
|
@ -62,6 +62,16 @@ impl<'a> Command<'a> for ListCommand {
|
|||
|
||||
entries.sort_unstable_by_key(|e| e.sheet.clone());
|
||||
|
||||
if args.flat {
|
||||
let sheets: Vec<_> = entries
|
||||
.into_iter()
|
||||
.map(|e| e.sheet)
|
||||
.unique()
|
||||
.collect();
|
||||
|
||||
streams.out.write_all(sheets.join("\n").as_bytes())?;
|
||||
streams.out.write_all(b"\n")?;
|
||||
} else {
|
||||
let mut total_running = Duration::seconds(0);
|
||||
let mut total_today = Duration::seconds(0);
|
||||
let mut total = Duration::seconds(0);
|
||||
|
@ -143,6 +153,7 @@ impl<'a> Command<'a> for ListCommand {
|
|||
]);
|
||||
|
||||
streams.out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
|
||||
}
|
||||
|
||||
warn_if_needed(&mut streams.err, needs_warning, &facts.env)?;
|
||||
|
||||
|
@ -153,7 +164,7 @@ impl<'a> Command<'a> for ListCommand {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{Utc, TimeZone};
|
||||
use pretty_assertions::assert_eq;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
|
||||
use crate::database::{SqliteDatabase, Database};
|
||||
|
||||
|
@ -169,18 +180,18 @@ mod tests {
|
|||
streams.db.set_current_sheet("sheet2").unwrap();
|
||||
streams.db.set_last_sheet("sheet4").unwrap();
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(1, 0, 0)), None, "_archived").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(10,13, 55)), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(7, 39, 18)), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(13, 52, 45)), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), None, None, "sheet4").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 1, 0, 0).unwrap()), None, "_archived").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 10,13, 55).unwrap()), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 7, 39, 18).unwrap()), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap()), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), None, None, "sheet4").unwrap();
|
||||
|
||||
let now = Utc.ymd(2021, 1, 1).and_hms(13, 52, 45);
|
||||
let now = Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
ListCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
|
||||
|
||||
sheet1 0:00:00 0:00:00 10:13:55
|
||||
* sheet2 0:00:00 0:00:00 0:00:00
|
||||
|
@ -195,11 +206,12 @@ mod tests {
|
|||
|
||||
let args = Args {
|
||||
all: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
ListCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
|
||||
|
||||
_archived 0:00:00 0:00:00 1:00:00
|
||||
sheet1 0:00:00 0:00:00 10:13:55
|
||||
|
@ -218,21 +230,59 @@ mod tests {
|
|||
SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap()
|
||||
);
|
||||
|
||||
let now = Local.ymd(2021, 7, 16).and_hms(11, 30, 45);
|
||||
let now = Local.with_ymd_and_hms(2021, 7, 16, 11, 30, 45).unwrap();
|
||||
let facts = Facts::new().with_now(now.with_timezone(&Utc));
|
||||
|
||||
ListCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Today Total Time
|
||||
|
||||
* default 0:10:24 0:10:26 0:10:26
|
||||
--------------------------------------------
|
||||
0:10:24 0:10:26 0:10:26
|
||||
");
|
||||
|
||||
assert_eq!(
|
||||
assert_str_eq!(
|
||||
String::from_utf8_lossy(&streams.err),
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_display() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let mut streams = Streams::fake(b"");
|
||||
|
||||
streams.db.set_current_sheet("sheet2").unwrap();
|
||||
streams.db.set_last_sheet("sheet4").unwrap();
|
||||
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 1, 0, 0).unwrap()), None, "_archived").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 10,13, 55).unwrap()), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 7, 39, 18).unwrap()), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap()), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), None, None, "sheet4").unwrap();
|
||||
|
||||
let now = Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
let args = Args {
|
||||
flat: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
ListCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "sheet1\nsheet2\nsheet3\nsheet4\n");
|
||||
|
||||
let facts = Facts::new().with_now(now);
|
||||
let args = Args {
|
||||
flat: true,
|
||||
all: true,
|
||||
};
|
||||
streams.out.clear();
|
||||
|
||||
ListCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "_archived\nsheet1\nsheet2\nsheet3\nsheet4\n");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,31 +16,27 @@ use super::{Command, Facts, display::{Sheet, entries_for_display}};
|
|||
|
||||
/// Given a local datetime, returns the time when the month it belongs started
|
||||
fn beginning_of_month(time: DateTime<Local>) -> DateTime<Utc> {
|
||||
time.date().with_day(1).unwrap().and_hms(0, 0, 0).with_timezone(&Utc)
|
||||
time.date_naive().with_day(1).unwrap().and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Utc).unwrap()
|
||||
}
|
||||
|
||||
/// Given a datetime compute the time where the previous_month started in UTC
|
||||
fn beginning_of_previous_month(time: DateTime<Local>) -> DateTime<Utc> {
|
||||
match time.month() {
|
||||
1 => {
|
||||
Local.ymd(time.year()-1, 12, 1).and_hms(0, 0, 0).with_timezone(&Utc)
|
||||
Local.with_ymd_and_hms(time.year()-1, 12, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
|
||||
}
|
||||
n => Local.ymd(time.year(), n-1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
|
||||
n => Local.with_ymd_and_hms(time.year(), n-1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum MonthSpec {
|
||||
Last,
|
||||
#[default]
|
||||
This,
|
||||
Month(u32),
|
||||
}
|
||||
|
||||
impl Default for MonthSpec {
|
||||
fn default() -> MonthSpec {
|
||||
MonthSpec::This
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MonthSpec {
|
||||
type Err = Error;
|
||||
|
||||
|
@ -74,7 +70,7 @@ pub struct Args {
|
|||
sheet: Option<Sheet>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
|
||||
|
@ -110,21 +106,21 @@ impl<'a> Command<'a> for MonthCommand {
|
|||
if month < now.month() {
|
||||
// the specified month is in the current year
|
||||
(
|
||||
Local.ymd(now.year(), month, 1).and_hms(0, 0, 0).with_timezone(&Utc),
|
||||
Local.with_ymd_and_hms(now.year(), month, 1, 0, 0, 0).unwrap().with_timezone(&Utc),
|
||||
if month < 12 {
|
||||
Local.ymd(now.year(), month+1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
|
||||
Local.with_ymd_and_hms(now.year(), month+1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
|
||||
} else {
|
||||
Local.ymd(now.year()+1, 1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
|
||||
Local.with_ymd_and_hms(now.year()+1, 1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// use previous year
|
||||
(
|
||||
Local.ymd(now.year() - 1, month, 1).and_hms(0, 0, 0).with_timezone(&Utc),
|
||||
Local.with_ymd_and_hms(now.year() - 1, month, 1, 0, 0, 0).unwrap().with_timezone(&Utc),
|
||||
if month < 12 {
|
||||
Local.ymd(now.year() - 1, month + 1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
|
||||
Local.with_ymd_and_hms(now.year() - 1, month + 1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
|
||||
} else {
|
||||
Local.ymd(now.year(), 1, 1).and_hms(0, 0, 0).with_timezone(&Utc)
|
||||
Local.with_ymd_and_hms(now.year(), 1, 1, 0, 0, 0).unwrap().with_timezone(&Utc)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -136,7 +132,7 @@ impl<'a> Command<'a> for MonthCommand {
|
|||
Some(end),
|
||||
args.sheet,
|
||||
streams,
|
||||
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
|
||||
args.format.unwrap_or_else(|| facts.config.commands.month.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
|
||||
args.ids,
|
||||
args.grep,
|
||||
facts
|
||||
|
@ -146,7 +142,7 @@ impl<'a> Command<'a> for MonthCommand {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -156,14 +152,40 @@ mod tests {
|
|||
|
||||
let args = Default::default();
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.ymd(2021, 6, 30).and_hms(11, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_config(Config {
|
||||
default_formatter: Formatter::Ids,
|
||||
..Default::default()
|
||||
}).with_now(now);
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
MonthCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
|
||||
assert_eq!(String::from_utf8_lossy(&streams.err), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respect_command_default_formatter() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let args = Default::default();
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_config(Config {
|
||||
commands: CommandsSettings {
|
||||
month: BaseCommandSettings {
|
||||
default_formatter: Some(Formatter::Ids),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}).with_now(now);
|
||||
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
MonthCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use super::{Command, Facts};
|
|||
pub struct Args {
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(_matches: &'a ArgMatches) -> Result<Args> {
|
||||
|
@ -40,6 +40,9 @@ impl<'a> Command<'a> for NowCommand {
|
|||
|
||||
let (entries, needs_warning) = entries_or_warning(entries, &streams.db)?;
|
||||
|
||||
if entries.is_empty() {
|
||||
streams.out.write_all(b"No running entries\n")?;
|
||||
} else {
|
||||
let current = streams.db.current_sheet()?;
|
||||
let last = streams.db.last_sheet()?;
|
||||
|
||||
|
@ -71,11 +74,12 @@ impl<'a> Command<'a> for NowCommand {
|
|||
}.to_string(),
|
||||
entry.sheet,
|
||||
format_duration(facts.now - entry.start),
|
||||
entry.note.unwrap_or_else(|| "".to_string())
|
||||
entry.note.unwrap_or_default(),
|
||||
]);
|
||||
}
|
||||
|
||||
streams.out.write_all(tabs.print(facts.env.stdout_is_tty).as_bytes())?;
|
||||
}
|
||||
|
||||
warn_if_needed(&mut streams.err, needs_warning, &facts.env)?;
|
||||
|
||||
|
@ -86,7 +90,7 @@ impl<'a> Command<'a> for NowCommand {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{Utc, TimeZone, Local};
|
||||
use pretty_assertions::assert_eq;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
|
||||
use crate::database::{SqliteDatabase, Database};
|
||||
|
||||
|
@ -97,21 +101,21 @@ mod tests {
|
|||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.ymd(2021, 1, 1).and_hms(13, 52, 45);
|
||||
let now = Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
streams.db.set_current_sheet("sheet2").unwrap();
|
||||
streams.db.set_last_sheet("sheet4").unwrap();
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(1, 0, 0)), None, "_archived").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(10,13, 55)), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(7, 39, 18)), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), Some(Utc.ymd(2021, 1, 1).and_hms(13, 52, 45)), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 1, 1).and_hms(12, 0, 0), None, Some("some".to_string()), "sheet4").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 1, 0, 0).unwrap()), None, "_archived").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 10,13, 55).unwrap()), None, "sheet1").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 7, 39, 18).unwrap()), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap()), None, "sheet3").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 1, 1, 12, 0, 0).unwrap(), None, Some("some".to_string()), "sheet4").unwrap();
|
||||
|
||||
NowCommand::handle(Default::default(), &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Activity
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Activity
|
||||
|
||||
- sheet4 1:52:45 some
|
||||
");
|
||||
|
@ -123,19 +127,33 @@ mod tests {
|
|||
SqliteDatabase::from_path("assets/test_list_old_database.db").unwrap()
|
||||
);
|
||||
|
||||
let now = Local.ymd(2021, 7, 16).and_hms(11, 30, 45);
|
||||
let now = Local.with_ymd_and_hms(2021, 7, 16, 11, 30, 45).unwrap();
|
||||
let facts = Facts::new().with_now(now.with_timezone(&Utc));
|
||||
|
||||
NowCommand::handle(Default::default(), &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Activity
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), " Timesheet Running Activity
|
||||
|
||||
* default 0:10:24 que
|
||||
");
|
||||
|
||||
assert_eq!(
|
||||
assert_str_eq!(
|
||||
String::from_utf8_lossy(&streams.err),
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate\n"
|
||||
"[WARNING] You are using the old timetrap format, it is advised that you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_no_running_entries_display_nicer_message() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.with_ymd_and_hms(2021, 1, 1, 13, 52, 45).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
NowCommand::handle(Default::default(), &mut streams, &facts).unwrap();
|
||||
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "No running entries
|
||||
");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ pub struct Args {
|
|||
at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
|
||||
|
@ -114,6 +114,6 @@ mod tests {
|
|||
assert_eq!(&String::from_utf8_lossy(&streams.out), "Checked out of sheet \"default\".\n");
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.err),
|
||||
"[WARNING] You are using the old timetrap format, it is advised that \
|
||||
you update your database using t migrate\n");
|
||||
you update your database using t migrate. To supress this warning set TIEMPO_SUPRESS_TIMETRAP_WARNING=1\n");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,25 +12,21 @@ use crate::io::Streams;
|
|||
use crate::interactive::note_from_last_entries;
|
||||
use super::{Command, Facts, r#in, sheet};
|
||||
|
||||
#[derive(Default)]
|
||||
enum SelectedEntry {
|
||||
Id(u64),
|
||||
Interactive,
|
||||
#[default]
|
||||
NotSpecified,
|
||||
}
|
||||
|
||||
impl Default for SelectedEntry {
|
||||
fn default() -> Self {
|
||||
SelectedEntry::NotSpecified
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Args {
|
||||
entry: SelectedEntry,
|
||||
at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
|
||||
|
|
|
@ -14,7 +14,7 @@ pub struct Args {
|
|||
pub sheet: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::convert::TryFrom;
|
|||
use std::io::{BufRead, Write};
|
||||
|
||||
use clap::ArgMatches;
|
||||
use chrono::{DateTime, Utc, Local};
|
||||
use chrono::{DateTime, Utc, Local, Timelike};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::error::{Result, Error};
|
||||
|
@ -23,7 +23,7 @@ pub struct Args {
|
|||
sheet: Option<Sheet>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
|
||||
|
@ -49,14 +49,20 @@ impl<'a> Command<'a> for TodayCommand {
|
|||
O: Write,
|
||||
E: Write,
|
||||
{
|
||||
let start = Some(facts.now.with_timezone(&Local).date().and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
let start = Some(facts.now
|
||||
.with_timezone(&Local)
|
||||
.with_hour(0).unwrap()
|
||||
.with_minute(0).unwrap()
|
||||
.with_second(0).unwrap()
|
||||
.with_nanosecond(0).unwrap()
|
||||
.with_timezone(&Utc));
|
||||
|
||||
entries_for_display(
|
||||
start,
|
||||
args.end,
|
||||
args.sheet,
|
||||
streams,
|
||||
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
|
||||
args.format.unwrap_or_else(|| facts.config.commands.today.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
|
||||
args.ids,
|
||||
args.grep,
|
||||
facts
|
||||
|
@ -68,7 +74,7 @@ impl<'a> Command<'a> for TodayCommand {
|
|||
mod tests {
|
||||
use chrono::TimeZone;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -81,10 +87,35 @@ mod tests {
|
|||
let facts = Facts::new().with_config(Config {
|
||||
default_formatter: Formatter::Ids,
|
||||
..Default::default()
|
||||
}).with_now(Utc.ymd(2021, 6, 30).and_hms(11, 0, 0));
|
||||
}).with_now(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap());
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
TodayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
|
||||
assert_eq!(String::from_utf8_lossy(&streams.err), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respect_command_default_formatter() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let args = Default::default();
|
||||
let mut streams = Streams::fake(b"");
|
||||
let facts = Facts::new().with_config(Config {
|
||||
commands: CommandsSettings {
|
||||
today: BaseCommandSettings {
|
||||
default_formatter: Some(Formatter::Ids),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}).with_now(Utc.with_ymd_and_hms(2021, 6, 30, 11, 0, 0).unwrap());
|
||||
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
TodayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::convert::TryFrom;
|
|||
use std::io::{BufRead, Write};
|
||||
|
||||
use clap::ArgMatches;
|
||||
use chrono::{DateTime, Utc, Local, Duration, Weekday, Datelike};
|
||||
use chrono::{DateTime, Utc, Local, Duration, Weekday, Datelike, Timelike};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::error::{Result, Error};
|
||||
|
@ -56,7 +56,12 @@ fn prev_day(now: DateTime<Local>, week_start: WeekDay) -> DateTime<Utc> {
|
|||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
begining.date().and_hms(0, 0, 0).with_timezone(&Utc)
|
||||
begining
|
||||
.with_hour(0).unwrap()
|
||||
.with_minute(0).unwrap()
|
||||
.with_second(0).unwrap()
|
||||
.with_nanosecond(0).unwrap()
|
||||
.with_timezone(&Utc)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -68,7 +73,7 @@ pub struct Args {
|
|||
sheet: Option<Sheet>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
|
||||
|
@ -101,7 +106,7 @@ impl<'a> Command<'a> for WeekCommand {
|
|||
args.end,
|
||||
args.sheet,
|
||||
streams,
|
||||
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
|
||||
args.format.unwrap_or_else(|| facts.config.commands.week.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
|
||||
args.ids,
|
||||
args.grep,
|
||||
facts
|
||||
|
@ -113,22 +118,22 @@ impl<'a> Command<'a> for WeekCommand {
|
|||
mod tests {
|
||||
use chrono::TimeZone;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_prev_day() {
|
||||
// starting a saturday
|
||||
let now = Local.ymd(2021, 7, 10).and_hms(18, 31, 0);
|
||||
let now = Local.with_ymd_and_hms(2021, 7, 10, 18, 31, 0).unwrap();
|
||||
|
||||
assert_eq!(prev_day(now, WeekDay::Monday), Local.ymd(2021, 7, 5).and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Tuesday), Local.ymd(2021, 7, 6).and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Wednesday), Local.ymd(2021, 7, 7).and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Thursday), Local.ymd(2021, 7, 8).and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Friday), Local.ymd(2021, 7, 9).and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Saturday), Local.ymd(2021, 7, 10).and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Sunday), Local.ymd(2021, 7, 4).and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Monday), Local.with_ymd_and_hms(2021, 7, 5, 0, 0, 0).unwrap().with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Tuesday), Local.with_ymd_and_hms(2021, 7, 6, 0, 0, 0).unwrap().with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Wednesday), Local.with_ymd_and_hms(2021, 7, 7, 0, 0, 0).unwrap().with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Thursday), Local.with_ymd_and_hms(2021, 7, 8, 0, 0, 0).unwrap().with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Friday), Local.with_ymd_and_hms(2021, 7, 9, 0, 0, 0).unwrap().with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Saturday), Local.with_ymd_and_hms(2021, 7, 10, 0, 0, 0).unwrap().with_timezone(&Utc));
|
||||
assert_eq!(prev_day(now, WeekDay::Sunday), Local.with_ymd_and_hms(2021, 7, 4, 0, 0, 0).unwrap().with_timezone(&Utc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -137,14 +142,40 @@ mod tests {
|
|||
|
||||
let args = Default::default();
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.ymd(2021, 7, 1).and_hms(10, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 7, 1, 10, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_config(Config {
|
||||
default_formatter: Formatter::Ids,
|
||||
..Default::default()
|
||||
}).with_now(now);
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
WeekCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
|
||||
assert_eq!(String::from_utf8_lossy(&streams.err), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respect_command_default_formatter() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let args = Default::default();
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.with_ymd_and_hms(2021, 7, 1, 10, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_config(Config {
|
||||
commands: CommandsSettings {
|
||||
week: BaseCommandSettings {
|
||||
default_formatter: Some(Formatter::Ids),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}).with_now(now);
|
||||
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
WeekCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ pub struct Args {
|
|||
sheet: Option<Sheet>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
impl<'a> TryFrom<&'a ArgMatches> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Args> {
|
||||
|
@ -46,16 +46,16 @@ impl<'a> Command<'a> for YesterdayCommand {
|
|||
O: Write,
|
||||
E: Write,
|
||||
{
|
||||
let today = facts.now.with_timezone(&Local).date();
|
||||
let start = Some((today - Duration::days(1)).and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
let end = Some(today.and_hms(0, 0, 0).with_timezone(&Utc));
|
||||
let today = facts.now.with_timezone(&Local).date_naive();
|
||||
let start = Some((today - Duration::days(1)).and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Utc).unwrap());
|
||||
let end = Some(today.and_hms_opt(0, 0, 0).unwrap().and_local_timezone(Utc).unwrap());
|
||||
|
||||
entries_for_display(
|
||||
start,
|
||||
end,
|
||||
args.sheet,
|
||||
streams,
|
||||
args.format.unwrap_or_else(|| facts.config.default_formatter.clone()),
|
||||
args.format.unwrap_or_else(|| facts.config.commands.yesterday.default_formatter.as_ref().unwrap_or(&facts.config.default_formatter).clone()),
|
||||
args.ids,
|
||||
args.grep,
|
||||
facts
|
||||
|
@ -68,7 +68,7 @@ mod tests {
|
|||
use chrono::{Duration, TimeZone};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, CommandsSettings, BaseCommandSettings};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -79,20 +79,20 @@ mod tests {
|
|||
..Default::default()
|
||||
};
|
||||
let mut streams = Streams::fake(b"");
|
||||
let two_days_ago = Local::now().date() - Duration::days(2);
|
||||
let yesterday = Local::now().date() - Duration::days(1);
|
||||
let today = Local::now().date();
|
||||
let two_days_ago = Local::now().date_naive() - Duration::days(2);
|
||||
let yesterday = Local::now().date_naive() - Duration::days(1);
|
||||
let today = Local::now().date_naive();
|
||||
let facts = Facts::new();
|
||||
|
||||
streams.db.entry_insert(two_days_ago.and_hms(1, 2, 3).with_timezone(&Utc), None, None, "default").unwrap();
|
||||
streams.db.entry_insert(yesterday.and_hms(1, 2, 3).with_timezone(&Utc), None, Some("This!".into()), "default").unwrap();
|
||||
streams.db.entry_insert(today.and_hms(1, 2, 3).with_timezone(&Utc), None, None, "default").unwrap();
|
||||
streams.db.entry_insert(two_days_ago.and_hms_opt(1, 2, 3).unwrap().and_local_timezone(Utc).unwrap(), None, None, "default").unwrap();
|
||||
streams.db.entry_insert(yesterday.and_hms_opt(1, 2, 3).unwrap().and_local_timezone(Utc).unwrap(), None, Some("This!".into()), "default").unwrap();
|
||||
streams.db.entry_insert(today.and_hms_opt(1, 2, 3).unwrap().and_local_timezone(Utc).unwrap(), None, None, "default").unwrap();
|
||||
|
||||
YesterdayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), &format!("start,end,note,sheet
|
||||
{},,This!,default
|
||||
", yesterday.and_hms(1, 2, 3).with_timezone(&Utc).to_rfc3339_opts(chrono::SecondsFormat::Micros, true)));
|
||||
", yesterday.and_hms_opt(1, 2, 3).unwrap().and_local_timezone(Utc).unwrap().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&streams.err),
|
||||
|
@ -106,14 +106,40 @@ mod tests {
|
|||
|
||||
let args = Default::default();
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.ymd(2021, 7, 1).and_hms(10, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2021, 7, 1, 10, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_config(Config {
|
||||
default_formatter: Formatter::Ids,
|
||||
..Default::default()
|
||||
}).with_now(now);
|
||||
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
YesterdayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
assert_eq!(&String::from_utf8_lossy(&streams.out), "1 2\n");
|
||||
assert_eq!(String::from_utf8_lossy(&streams.err), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respect_command_default_formatter() {
|
||||
std::env::set_var("TZ", "CST+6");
|
||||
|
||||
let args = Default::default();
|
||||
let mut streams = Streams::fake(b"");
|
||||
let now = Utc.with_ymd_and_hms(2021, 7, 1, 10, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_config(Config {
|
||||
commands: CommandsSettings {
|
||||
yesterday: BaseCommandSettings {
|
||||
default_formatter: Some(Formatter::Ids),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}).with_now(now);
|
||||
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 0, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
streams.db.entry_insert(Utc.with_ymd_and_hms(2021, 6, 30, 10, 10, 0).unwrap(), None, Some("hola".into()), "default").unwrap();
|
||||
|
||||
YesterdayCommand::handle(args, &mut streams, &facts).unwrap();
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@ use std::collections::HashMap;
|
|||
use directories::{UserDirs, ProjectDirs};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use toml::to_string;
|
||||
use chrono::Weekday;
|
||||
|
||||
use crate::{error::{Result, Error::{self, *}}, formatters::Formatter};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum WeekDay {
|
||||
Monday,
|
||||
Tuesday,
|
||||
|
@ -38,7 +39,73 @@ impl FromStr for WeekDay {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
impl From<Weekday> for WeekDay {
|
||||
fn from(wd: Weekday) -> WeekDay {
|
||||
match wd {
|
||||
Weekday::Mon => WeekDay::Monday,
|
||||
Weekday::Tue => WeekDay::Tuesday,
|
||||
Weekday::Wed => WeekDay::Wednesday,
|
||||
Weekday::Thu => WeekDay::Thursday,
|
||||
Weekday::Fri => WeekDay::Friday,
|
||||
Weekday::Sat => WeekDay::Saturday,
|
||||
Weekday::Sun => WeekDay::Sunday,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct ChartFormatterSettings {
|
||||
/// This setting is used to highlight hours that go beyond the daily goal.
|
||||
/// If unset all hours will look the same.
|
||||
pub daily_goal_hours: u32,
|
||||
|
||||
/// If set, weekly hour count will be highlighted in green if equal or
|
||||
/// higher than the weekly goal or in red if lower. If not set number will
|
||||
/// be displayed in the default color.
|
||||
pub weekly_goal_hours: u32,
|
||||
|
||||
/// This is the amount of minutes that each character represents in the
|
||||
/// chart
|
||||
pub character_equals_minutes: usize,
|
||||
}
|
||||
|
||||
impl Default for ChartFormatterSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
daily_goal_hours: 0,
|
||||
weekly_goal_hours: 0,
|
||||
character_equals_minutes: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct FormattersSettings {
|
||||
pub chart: ChartFormatterSettings,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct BaseCommandSettings {
|
||||
pub default_formatter: Option<Formatter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct CommandsSettings {
|
||||
pub display: BaseCommandSettings,
|
||||
pub month: BaseCommandSettings,
|
||||
pub today: BaseCommandSettings,
|
||||
pub week: BaseCommandSettings,
|
||||
pub yesterday: BaseCommandSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
#[serde(skip)]
|
||||
|
@ -87,6 +154,12 @@ pub struct Config {
|
|||
/// and kill)
|
||||
pub interactive_entries: usize,
|
||||
|
||||
/// Individual settings for each formatter
|
||||
pub formatters: FormattersSettings,
|
||||
|
||||
/// Settings for each command
|
||||
pub commands: CommandsSettings,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
@ -286,6 +359,8 @@ impl Default for Config {
|
|||
note_editor: None,
|
||||
week_start: WeekDay::Monday,
|
||||
interactive_entries: 5,
|
||||
formatters: Default::default(),
|
||||
commands: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
148
src/database.rs
148
src/database.rs
|
@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
|
|||
use crate::error::{Error, Result};
|
||||
use crate::models::{Entry, Meta};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum DBVersion {
|
||||
Timetrap,
|
||||
Version(u16),
|
||||
|
@ -176,7 +176,7 @@ pub trait Database {
|
|||
}
|
||||
|
||||
fn last_entry_of_sheet(&self, sheet: &str) -> Result<Option<Entry>> {
|
||||
Ok(self.entry_query("select * from entries where sheet=?1 order by id desc limit 1", &[&sheet])?.into_iter().next())
|
||||
Ok(self.entry_query("select * from entries where sheet=?1 order by start desc limit 1", &[&sheet])?.into_iter().next())
|
||||
}
|
||||
|
||||
fn delete_entry_by_id(&mut self, id: u64) -> Result<()> {
|
||||
|
@ -279,6 +279,7 @@ impl Database for SqliteDatabase {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::let_and_return)]
|
||||
fn entry_query(&self, query: &str, params: &[&dyn ToSql]) -> Result<Vec<Entry>> {
|
||||
let mut stmt = self.connection.prepare(query)?;
|
||||
|
||||
|
@ -323,7 +324,8 @@ impl Database for SqliteDatabase {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::TimeZone;
|
||||
use chrono::{TimeZone, NaiveDate};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -332,21 +334,21 @@ mod tests {
|
|||
let mut db = SqliteDatabase::from_memory().unwrap();
|
||||
db.init().unwrap();
|
||||
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "OOO").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "OOO").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "OOO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "OOO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "OOO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "OOO").unwrap();
|
||||
|
||||
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
|
||||
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
|
||||
let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
|
||||
let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
|
||||
|
||||
// filter by start and end
|
||||
assert_eq!(
|
||||
db.entries_by_sheet("XXX", Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -354,8 +356,8 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_by_sheet("XXX", Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -363,8 +365,8 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_by_sheet("XXX", None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -372,9 +374,9 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_by_sheet("XXX", None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -384,21 +386,21 @@ mod tests {
|
|||
let mut db = SqliteDatabase::from_memory().unwrap();
|
||||
db.init().unwrap();
|
||||
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "_OO").unwrap();
|
||||
|
||||
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
|
||||
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
|
||||
let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
|
||||
let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
|
||||
|
||||
// filter by start and end
|
||||
assert_eq!(
|
||||
db.entries_all_visible(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -406,8 +408,8 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_all_visible(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -415,8 +417,8 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_all_visible(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -424,9 +426,9 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_all_visible(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -436,22 +438,22 @@ mod tests {
|
|||
let mut db = SqliteDatabase::from_memory().unwrap();
|
||||
db.init().unwrap();
|
||||
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(1, 0, 0), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(2, 0, 0), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.ymd(2021, 7, 7).and_hms(3, 0, 0), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(), None, None, "_OO").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "XXX").unwrap();
|
||||
db.entry_insert(Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(), None, None, "_OO").unwrap();
|
||||
|
||||
let start = Utc.ymd(2021, 7, 7).and_hms(1, 30, 0);
|
||||
let end = Utc.ymd(2021, 7, 7).and_hms(2, 30, 0);
|
||||
let start = Utc.with_ymd_and_hms(2021, 7, 7, 1, 30, 0).unwrap();
|
||||
let end = Utc.with_ymd_and_hms(2021, 7, 7, 2, 30, 0).unwrap();
|
||||
|
||||
// filter by start and end
|
||||
assert_eq!(
|
||||
db.entries_full(Some(start), Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -459,10 +461,10 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_full(Some(start), None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -470,10 +472,10 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_full(None, Some(end)).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -481,13 +483,41 @@ mod tests {
|
|||
assert_eq!(
|
||||
db.entries_full(None, None).unwrap().into_iter().map(|e| e.start).collect::<Vec<_>>(),
|
||||
vec![
|
||||
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(1, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(2, 0, 0),
|
||||
Utc.ymd(2021, 7, 7).and_hms(3, 0, 0),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 1, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 2, 0, 0).unwrap(),
|
||||
Utc.with_ymd_and_hms(2021, 7, 7, 3, 0, 0).unwrap(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Due to the "archive by time" feature it can happen that an entry is
|
||||
/// split in two: the first (old) entry is archived with an updated end time
|
||||
/// and the second (new) entry is created with the remaining time of the
|
||||
/// original entry. In this case the last entry of the sheet is not the one
|
||||
/// with the largest id, but the one with the latest start time.
|
||||
#[test]
|
||||
fn last_entry_of_sheet_considers_split_entries() {
|
||||
let mut db = SqliteDatabase::from_memory().unwrap();
|
||||
db.init().unwrap();
|
||||
|
||||
let sometime = NaiveDate::from_ymd_opt(2022, 7, 27).unwrap();
|
||||
|
||||
db.entry_insert(sometime.and_hms_opt(11, 0, 0).unwrap().and_local_timezone(Utc).unwrap(), Some(sometime.and_hms_opt(12, 0, 0).unwrap().and_local_timezone(Utc).unwrap()), Some("latest".into()), "foo").unwrap();
|
||||
db.entry_insert(sometime.and_hms_opt(10, 0, 0).unwrap().and_local_timezone(Utc).unwrap(), Some(sometime.and_hms_opt(11, 0, 0).unwrap().and_local_timezone(Utc).unwrap()), Some("oldest".into()), "foo").unwrap();
|
||||
|
||||
// filter by start and end
|
||||
assert_eq!(
|
||||
db.last_entry_of_sheet("foo").unwrap().unwrap(),
|
||||
Entry {
|
||||
id: 1,
|
||||
note: Some("latest".into()),
|
||||
start: sometime.and_hms_opt(11, 0, 0).unwrap().and_local_timezone(Utc).unwrap(),
|
||||
end: Some(sometime.and_hms_opt(12, 0, 0).unwrap().and_local_timezone(Utc).unwrap()),
|
||||
sheet: "foo".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use std::process::{Command, Stdio};
|
||||
use std::io::{Read, Write, Seek, SeekFrom};
|
||||
use std::io::{Read, Write, Seek};
|
||||
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
|
@ -20,7 +20,7 @@ pub fn get_string(note_editor: Option<&str>, prev_contents: Option<String>) -> R
|
|||
};
|
||||
|
||||
let parts: Vec<_> = note_editor.split(' ').filter(|p| !p.is_empty()).collect();
|
||||
let editor = if let Some(name) = parts.get(0) {
|
||||
let editor = if let Some(name) = parts.first() {
|
||||
name.to_owned()
|
||||
} else {
|
||||
return Err(EditorIsEmpty);
|
||||
|
@ -38,7 +38,7 @@ pub fn get_string(note_editor: Option<&str>, prev_contents: Option<String>) -> R
|
|||
|
||||
if let Some(contents) = prev_contents {
|
||||
tmpfile.write_all(contents.as_bytes())?;
|
||||
tmpfile.seek(SeekFrom::Start(0))?;
|
||||
tmpfile.rewind()?;
|
||||
}
|
||||
|
||||
c.arg(tmpfile.as_ref());
|
||||
|
|
|
@ -19,6 +19,9 @@ pub enum Error {
|
|||
#[error("The subcommand '{0}' is not implemented")]
|
||||
UnimplementedCommand(String),
|
||||
|
||||
#[error("A subcommand was not specified and no default command is set")]
|
||||
MissingSubcommand,
|
||||
|
||||
/// Sometimes a specific variant for an error is not necessary if the error
|
||||
/// can only happen in one place in the code. This is what the generic error
|
||||
/// is for and nothing else.
|
||||
|
@ -115,6 +118,9 @@ query it using t backend or the sqlite3 command provided by your system.")]
|
|||
#[error("Could not understand '{0}' as a week day. Try 'monday' or 'TuesDay'")]
|
||||
InvalidWeekDaySpec(String),
|
||||
|
||||
#[error("Could not understand '{0}' as a number of hours")]
|
||||
InvalidHours(String),
|
||||
|
||||
#[error("An error ocurred while trying to read entries from the database.
|
||||
|
||||
In the row with id {id} the data at column '{col}' has a value that is not a
|
||||
|
@ -210,7 +216,7 @@ which where taken from your config file located at
|
|||
|
||||
{config_at}
|
||||
|
||||
Perhaps is mispelled?", format_paths(.paths))]
|
||||
Perhaps it is mispelled?", format_paths(.paths))]
|
||||
FormatterNotFound {
|
||||
name: String,
|
||||
paths: Vec<PathBuf>,
|
||||
|
|
|
@ -13,24 +13,21 @@ pub mod json;
|
|||
pub mod ids;
|
||||
pub mod ical;
|
||||
pub mod custom;
|
||||
pub mod chart;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Formatter {
|
||||
#[default]
|
||||
Text,
|
||||
Csv,
|
||||
Json,
|
||||
Ids,
|
||||
Ical,
|
||||
Chart,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl Default for Formatter {
|
||||
fn default() -> Formatter {
|
||||
Formatter::Text
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatter {
|
||||
/// Prints the given entries to the specified output device.
|
||||
///
|
||||
|
@ -49,6 +46,7 @@ impl Formatter {
|
|||
Formatter::Json => json::print_formatted(entries, out)?,
|
||||
Formatter::Ids => ids::print_formatted(entries, out)?,
|
||||
Formatter::Ical => ical::print_formatted(entries, out, facts.now)?,
|
||||
Formatter::Chart => chart::print_formatted(entries, out, facts)?,
|
||||
Formatter::Custom(name) => custom::print_formatted(name, entries, out, err, facts)?,
|
||||
}
|
||||
|
||||
|
@ -66,6 +64,7 @@ impl FromStr for Formatter {
|
|||
"json" => Formatter::Json,
|
||||
"ids" => Formatter::Ids,
|
||||
"ical" => Formatter::Ical,
|
||||
"chart" => Formatter::Chart,
|
||||
custom_format => Formatter::Custom(custom_format.into()),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
");
|
||||
}
|
||||
}
|
|
@ -10,9 +10,9 @@ pub fn print_formatted<W: Write>(entries: Vec<Entry>, out: &mut W, ids: bool) ->
|
|||
let mut wtr = Writer::from_writer(out);
|
||||
|
||||
if ids {
|
||||
wtr.write_record(&["id", "start", "end", "note", "sheet"])?;
|
||||
wtr.write_record(["id", "start", "end", "note", "sheet"])?;
|
||||
} else {
|
||||
wtr.write_record(&["start", "end", "note", "sheet"])?;
|
||||
wtr.write_record(["start", "end", "note", "sheet"])?;
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
|
@ -46,8 +46,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_print_formatted() {
|
||||
let entries = vec![
|
||||
Entry::new_sample(1, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), Some(Utc.ymd(2021, 6, 30).and_hms(19, 0, 0))),
|
||||
Entry::new_sample(2, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), None),
|
||||
Entry::new_sample(1, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 19, 0, 0).unwrap())),
|
||||
Entry::new_sample(2, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), None),
|
||||
];
|
||||
let mut out = Vec::new();
|
||||
|
||||
|
@ -62,8 +62,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_print_formatted_ids() {
|
||||
let entries = vec![
|
||||
Entry::new_sample(1, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), Some(Utc.ymd(2021, 6, 30).and_hms(19, 0, 0))),
|
||||
Entry::new_sample(2, Utc.ymd(2021, 6, 30).and_hms(18, 12, 34), None),
|
||||
Entry::new_sample(1, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), Some(Utc.with_ymd_and_hms(2021, 6, 30, 19, 0, 0).unwrap())),
|
||||
Entry::new_sample(2, Utc.with_ymd_and_hms(2021, 6, 30, 18, 12, 34).unwrap(), None),
|
||||
];
|
||||
let mut out = Vec::new();
|
||||
|
||||
|
|
|
@ -32,15 +32,11 @@ fn is_valid_formatter_name(formatter: &str) -> bool {
|
|||
}
|
||||
|
||||
fn get_formatter_config(config: &Config, formatter: &str) -> String {
|
||||
if let Some(formatters) = config.extra.get("formatters") {
|
||||
if let Some(config_for_formatter) = formatters.get(formatter) {
|
||||
if let Some(config_for_formatter) = config.formatters.extra.get(formatter) {
|
||||
config_for_formatter.to_string()
|
||||
} else {
|
||||
String::from("{}")
|
||||
}
|
||||
} else {
|
||||
String::from("{}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_formatted<O, E>(formatter: &str, entries: Vec<Entry>, out: &mut O, err: &mut E, facts: &Facts) -> Result<()>
|
||||
|
@ -114,6 +110,8 @@ where
|
|||
mod tests {
|
||||
use crate::config::Config;
|
||||
|
||||
use pretty_assertions::assert_str_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
@ -158,7 +156,7 @@ t config --formatter-search-paths <path>..");
|
|||
});
|
||||
let err = print_formatted("pollo", Vec::new(), &mut out, &mut err, &facts).unwrap_err();
|
||||
|
||||
assert_eq!(err.to_string(), "\
|
||||
assert_str_eq!(err.to_string(), "\
|
||||
Could not find a formatter with name 'pollo' in any of the following paths:
|
||||
|
||||
- /not/a/path
|
||||
|
@ -168,6 +166,6 @@ which where taken from your config file located at
|
|||
|
||||
/etc/tiempo/config.toml
|
||||
|
||||
Perhaps is mispelled?");
|
||||
Perhaps it is mispelled?");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,24 @@ pub fn format_duration(dur: Duration) -> String {
|
|||
format!("{}:{:02}:{:02}", dur.num_hours(), dur.num_minutes() % 60, dur.num_seconds() % 60)
|
||||
}
|
||||
|
||||
pub fn format_hours(dur: Duration) -> String {
|
||||
let hours = dur.num_hours();
|
||||
let minutes = dur.num_minutes() % 60;
|
||||
let seconds = dur.num_seconds() % 60;
|
||||
|
||||
let time = [
|
||||
if hours > 0 { Some(format!("{hours}h")) } else { None },
|
||||
if minutes > 0 { Some(format!("{minutes}m")) } else { None },
|
||||
if seconds > 0 { Some(format!("{seconds}s")) } else { None },
|
||||
].into_iter().flatten().collect::<Vec<_>>().join(" ");
|
||||
|
||||
if time.is_empty() {
|
||||
String::from("less than one second")
|
||||
} else {
|
||||
time
|
||||
}
|
||||
}
|
||||
|
||||
fn format_start(t: NaiveTime) -> String {
|
||||
format!("{:02}:{:02}:{:02}", t.hour(), t.minute(), t.second())
|
||||
}
|
||||
|
@ -63,7 +81,7 @@ pub fn print_formatted<W: Write>(entries: Vec<Entry>, out: &mut W, facts: &Facts
|
|||
]);
|
||||
}
|
||||
|
||||
let entries_by_date = group.group_by(|e| e.start.with_timezone(&Local).date());
|
||||
let entries_by_date = group.group_by(|e| e.start.with_timezone(&Local).date_naive());
|
||||
let mut total = Duration::seconds(0);
|
||||
|
||||
for (date, entries) in entries_by_date.into_iter() {
|
||||
|
@ -140,13 +158,13 @@ mod tests {
|
|||
std::env::set_var("TZ", "CST+6");
|
||||
let mut output = Vec::new();
|
||||
let entries = vec![
|
||||
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
|
||||
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(18, 0, 0))),
|
||||
Entry::new_sample(3, Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0))),
|
||||
Entry::new_sample(4, Utc.ymd(2008, 10, 5).and_hms(18, 0, 0), None),
|
||||
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
|
||||
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 18, 0, 0).unwrap())),
|
||||
Entry::new_sample(3, Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap())),
|
||||
Entry::new_sample(4, Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap(), None),
|
||||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
print_formatted(entries, &mut output, &facts, false).unwrap();
|
||||
|
@ -169,10 +187,13 @@ mod tests {
|
|||
std::env::set_var("TZ", "CST+6");
|
||||
let mut output = Vec::new();
|
||||
let entries = vec![
|
||||
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms_milli(12, 0, 0, 432), Some(Utc.ymd(2008, 10, 3).and_hms_milli(14, 0, 0, 312))),
|
||||
Entry::new_sample(
|
||||
1,
|
||||
Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap().with_nanosecond(432_000_000).unwrap(),
|
||||
Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap().with_nanosecond(312_000_000).unwrap())),
|
||||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
print_formatted(entries, &mut output, &facts, false).unwrap();
|
||||
|
@ -191,11 +212,11 @@ mod tests {
|
|||
std::env::set_var("TZ", "CST+6");
|
||||
let mut output = Vec::new();
|
||||
let entries = vec![
|
||||
Entry::new_sample(1, Utc.ymd(2008, 10, 1).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
|
||||
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
|
||||
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 1, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
|
||||
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
|
||||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
print_formatted(entries, &mut output, &facts, false).unwrap();
|
||||
|
@ -216,13 +237,13 @@ mod tests {
|
|||
std::env::set_var("TZ", "CST+6");
|
||||
let mut output = Vec::new();
|
||||
let entries = vec![
|
||||
Entry::new_sample(1, Utc.ymd(2008, 10, 3).and_hms(12, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(14, 0, 0))),
|
||||
Entry::new_sample(2, Utc.ymd(2008, 10, 3).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 3).and_hms(18, 0, 0))),
|
||||
Entry::new_sample(3, Utc.ymd(2008, 10, 5).and_hms(16, 0, 0), Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0))),
|
||||
Entry::new_sample(4, Utc.ymd(2008, 10, 5).and_hms(18, 0, 0), None),
|
||||
Entry::new_sample(1, Utc.with_ymd_and_hms(2008, 10, 3, 12, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 14, 0, 0).unwrap())),
|
||||
Entry::new_sample(2, Utc.with_ymd_and_hms(2008, 10, 3, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 3, 18, 0, 0).unwrap())),
|
||||
Entry::new_sample(3, Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(), Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap())),
|
||||
Entry::new_sample(4, Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap(), None),
|
||||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
print_formatted(entries, &mut output, &facts, true).unwrap();
|
||||
|
@ -248,13 +269,13 @@ mod tests {
|
|||
Entry {
|
||||
id: 60000,
|
||||
sheet: "default".into(),
|
||||
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
|
||||
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
|
||||
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
||||
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
||||
note: Some(LONG_NOTE.into()),
|
||||
},
|
||||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
print_formatted(entries, &mut output, &facts, true).unwrap();
|
||||
|
@ -283,13 +304,13 @@ mod tests {
|
|||
Entry {
|
||||
id: 1,
|
||||
sheet: "default".into(),
|
||||
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
|
||||
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
|
||||
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
||||
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
||||
note: Some("first line\nand a second line".into()),
|
||||
},
|
||||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
print_formatted(entries, &mut output, &facts, false).unwrap();
|
||||
|
@ -312,13 +333,13 @@ mod tests {
|
|||
Entry {
|
||||
id: 1,
|
||||
sheet: "default".into(),
|
||||
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
|
||||
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
|
||||
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
||||
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
||||
note: Some("quiúbole".into()),
|
||||
},
|
||||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
print_formatted(entries, &mut output, &facts, false).unwrap();
|
||||
|
@ -341,20 +362,20 @@ mod tests {
|
|||
Entry {
|
||||
id: 1,
|
||||
sheet: "sheet1".to_string(),
|
||||
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
|
||||
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
|
||||
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
||||
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
||||
note: Some("quiúbole".to_string()),
|
||||
},
|
||||
Entry {
|
||||
id: 2,
|
||||
sheet: "sheet2".to_string(),
|
||||
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
|
||||
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
|
||||
start: Utc.with_ymd_and_hms(2008, 10, 5, 16, 0, 0).unwrap(),
|
||||
end: Some(Utc.with_ymd_and_hms(2008, 10, 5, 18, 0, 0).unwrap()),
|
||||
note: Some("quiúbole".to_string()),
|
||||
},
|
||||
];
|
||||
|
||||
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
|
||||
let now = Utc.with_ymd_and_hms(2008, 10, 5, 20, 0, 0).unwrap();
|
||||
let facts = Facts::new().with_now(now);
|
||||
|
||||
print_formatted(entries, &mut output, &facts, false).unwrap();
|
||||
|
@ -376,4 +397,13 @@ Timesheet: sheet2
|
|||
Grand total 4:00:00
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hour_formatting() {
|
||||
assert_eq!(format_hours(Duration::hours(3) + Duration::minutes(30)), "3h 30m");
|
||||
assert_eq!(format_hours(Duration::hours(3) + Duration::minutes(1)), "3h 1m");
|
||||
assert_eq!(format_hours(Duration::hours(3) + Duration::minutes(5) + Duration::seconds(59)), "3h 5m 59s");
|
||||
assert_eq!(format_hours(Duration::hours(3)), "3h");
|
||||
assert_eq!(format_hours(Duration::milliseconds(543)), "less than one second")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::commands::Facts;
|
|||
use crate::models::Entry;
|
||||
use crate::tabulate::{Tabulate, Col, Align};
|
||||
use crate::formatters::text::format_duration;
|
||||
use crate::old::entries_or_warning;
|
||||
|
||||
fn read_line<I: BufRead>(mut r#in: I) -> io::Result<String> {
|
||||
let mut pre_n = String::new();
|
||||
|
@ -65,6 +66,7 @@ where
|
|||
E: Write,
|
||||
{
|
||||
let entries = streams.db.entries_by_sheet(current_sheet, None, None)?;
|
||||
let entries = entries_or_warning(entries, &streams.db)?.0;
|
||||
let mut uniques = HashMap::new();
|
||||
|
||||
struct GroupedEntry {
|
||||
|
@ -76,9 +78,7 @@ where
|
|||
// From all possible entries belonging to this sheet keep only those with a
|
||||
// note
|
||||
let entries_with_notes = entries
|
||||
// Iterate all entries
|
||||
.into_iter()
|
||||
// preserve only those with a text note
|
||||
.filter_map(|e| e.note.map(|n| GroupedEntry {
|
||||
note: n,
|
||||
last_start: e.start,
|
||||
|
@ -120,7 +120,7 @@ where
|
|||
table.feed(vec!["#", "Note", "Total time", "Last started"]);
|
||||
table.separator(' ');
|
||||
|
||||
for (i, entry) in uniques.iter().take(facts.config.interactive_entries).enumerate() {
|
||||
for (i, entry) in uniques.iter().take(facts.config.interactive_entries).enumerate().rev() {
|
||||
let i = i + 1;
|
||||
let ago = formatter.convert_chrono(entry.last_start, facts.now);
|
||||
|
||||
|
@ -185,7 +185,7 @@ are you sure you want to delete entry {id} with note
|
|||
|
||||
\"{note}\"
|
||||
|
||||
{duration})"))? {
|
||||
{duration}?"))? {
|
||||
streams.db.delete_entry_by_id(entry.id)?;
|
||||
writeln!(streams.out, "Gone")?;
|
||||
} else {
|
||||
|
@ -216,15 +216,15 @@ mod tests {
|
|||
streams.db.entry_insert(one_hour_ago, Some(facts.now), Some("second task".into()), "default").unwrap();
|
||||
|
||||
// call the command interactively
|
||||
note_from_last_entries(&mut streams, &facts, "default").unwrap();
|
||||
assert_eq!(note_from_last_entries(&mut streams, &facts, "default").unwrap().unwrap(), "second task");
|
||||
|
||||
// check the output
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Latest entries of sheet 'default':
|
||||
|
||||
# Note Total time Last started
|
||||
|
||||
1 second task 1:00:00 1 hour ago
|
||||
2 first task 1:00:00 2 hours ago
|
||||
1 second task 1:00:00 1 hour ago
|
||||
|
||||
enter number or q to cancel
|
||||
>> ");
|
||||
|
@ -251,17 +251,17 @@ enter number or q to cancel
|
|||
streams.db.entry_insert(facts.now - Duration::minutes(4), Some(facts.now - Duration::minutes(3)), Some("task 6".into()), "default").unwrap();
|
||||
|
||||
// call the command interactively
|
||||
note_from_last_entries(&mut streams, &facts, "default").unwrap();
|
||||
assert_eq!(note_from_last_entries(&mut streams, &facts, "default").unwrap().unwrap(), "task 6");
|
||||
|
||||
// check the output
|
||||
assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Latest entries of sheet 'default':
|
||||
|
||||
# Note Total time Last started
|
||||
|
||||
1 task 6 0:01:00 4 minutes ago
|
||||
2 task 5 0:01:00 5 minutes ago
|
||||
3 task 4 0:01:00 6 minutes ago
|
||||
4 task 3 0:01:00 7 minutes ago
|
||||
3 task 4 0:01:00 6 minutes ago
|
||||
2 task 5 0:01:00 5 minutes ago
|
||||
1 task 6 0:01:00 4 minutes ago
|
||||
|
||||
enter number or q to cancel
|
||||
>> ");
|
||||
|
|
|
@ -15,3 +15,4 @@ pub mod old;
|
|||
pub mod interactive;
|
||||
pub mod env;
|
||||
pub mod io;
|
||||
pub mod cli;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Entry {
|
||||
pub id: u64,
|
||||
pub note: Option<String>,
|
||||
|
@ -27,4 +27,23 @@ impl Entry {
|
|||
sheet: "default".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_sheet(self, sheet: &str) -> Entry {
|
||||
Entry {
|
||||
sheet: sheet.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timespan(&self) -> Option<Duration> {
|
||||
self.end.map(|e| e - self.start)
|
||||
}
|
||||
|
||||
// returns the number of hours of this entry as decimal. If entry is
|
||||
// unfinished return its elapsed time so far.
|
||||
pub fn hours(&self, now: DateTime<Utc>) -> f64 {
|
||||
let d = self.end.unwrap_or(now) - self.start;
|
||||
|
||||
d.num_hours() as f64 + (d.num_minutes() % 60) as f64 / 60.0 + (d.num_seconds() % 60) as f64 / 3600.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,7 +78,8 @@ pub fn warn_if_needed<E: Write>(err: &mut E, needs_warning: bool, env: &Env) ->
|
|||
writeln!(
|
||||
err,
|
||||
"{} You are using the old timetrap format, it is advised that \
|
||||
you update your database using t migrate",
|
||||
you update your database using t migrate. To supress this warning \
|
||||
set TIEMPO_SUPRESS_TIMETRAP_WARNING=1",
|
||||
if env.stderr_is_tty {
|
||||
Yellow.bold().paint("[WARNING]")
|
||||
} else {
|
||||
|
|
|
@ -2,15 +2,49 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use ansi_term::Style;
|
||||
use regex::Regex;
|
||||
|
||||
lazy_static! {
|
||||
// https://en.wikipedia.org/wiki/ANSI_escape_code#DOS,_OS/2,_and_Windows
|
||||
//
|
||||
// For Control Sequence Introducer, or CSI, commands, the ESC [ is followed
|
||||
// by any number (including none) of "parameter bytes" in the range
|
||||
// 0x30–0x3F (ASCII 0–9:;<=>?), then by any number of "intermediate bytes"
|
||||
// in the range 0x20–0x2F (ASCII space and !"#$%&'()*+,-./), then finally
|
||||
// by a single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~)
|
||||
//
|
||||
// The lazy regex bellow doesn't cover all of that. It just works on ansi
|
||||
// colors.
|
||||
pub static ref ANSI_REGEX: Regex = Regex::new("\x1b\\[[\\d;]*m").unwrap();
|
||||
}
|
||||
|
||||
/// An abstract way of getting the visual size of a string in a terminal
|
||||
pub trait VisualSize {
|
||||
fn size(&self) -> usize;
|
||||
}
|
||||
|
||||
impl VisualSize for &str {
|
||||
fn size(&self) -> usize {
|
||||
let s = ANSI_REGEX.replace_all(self, "");
|
||||
|
||||
s.chars().count()
|
||||
}
|
||||
}
|
||||
|
||||
impl VisualSize for String {
|
||||
fn size(&self) -> usize {
|
||||
self.as_str().size()
|
||||
}
|
||||
}
|
||||
|
||||
fn lpad(s: &str, len: usize) -> String {
|
||||
let padding = " ".repeat(len.saturating_sub(s.chars().count()));
|
||||
let padding = " ".repeat(len.saturating_sub(s.size()));
|
||||
|
||||
padding + s
|
||||
}
|
||||
|
||||
fn rpad(s: &str, len: usize) -> String {
|
||||
let padding = " ".repeat(len.saturating_sub(s.chars().count()));
|
||||
let padding = " ".repeat(len.saturating_sub(s.size()));
|
||||
|
||||
s.to_string() + &padding
|
||||
}
|
||||
|
@ -110,10 +144,10 @@ impl Tabulate {
|
|||
for (col, ((w, d), c)) in self.widths.iter_mut().zip(data.iter()).zip(self.cols.iter()).enumerate() {
|
||||
for (r1, dl) in d.as_ref().split('\n').enumerate() {
|
||||
for (r2, l) in constrained_lines(dl, c.max_width.unwrap_or(usize::MAX)).into_iter().enumerate() {
|
||||
let count = l.chars().count();
|
||||
let width = l.as_ref().size();
|
||||
|
||||
if count > *w {
|
||||
*w = count;
|
||||
if width > *w {
|
||||
*w = width;
|
||||
}
|
||||
|
||||
if let Some(line) = lines.get_mut(r1 + r2) {
|
||||
|
@ -189,6 +223,7 @@ impl Tabulate {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use ansi_term::Color::Fixed;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -425,4 +460,11 @@ foo key
|
|||
{} foo
|
||||
", Style::new().dimmed().paint("key")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sizes_of_things() {
|
||||
assert_eq!("🥦".size(), 1);
|
||||
assert_eq!("á".size(), 1);
|
||||
assert_eq!(Fixed(10).paint("hola").to_string().size(), 4);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,10 +14,8 @@ fn date_from_parts<T: TimeZone>(
|
|||
timezone: T, input: &str, year: i32, month: u32, day: u32, hour: u32,
|
||||
minute: u32, second: u32
|
||||
) -> Result<DateTime<Utc>> {
|
||||
let try_date = timezone.ymd_opt(
|
||||
year, month, day
|
||||
).and_hms_opt(
|
||||
hour, minute, second
|
||||
let try_date = timezone.with_ymd_and_hms(
|
||||
year, month, day, hour, minute, second
|
||||
);
|
||||
|
||||
match try_date {
|
||||
|
@ -36,9 +34,9 @@ fn offset_from_parts(east: bool, hours: i32, minutes: i32) -> FixedOffset {
|
|||
second += minutes * 60;
|
||||
|
||||
if east {
|
||||
FixedOffset::east(second)
|
||||
FixedOffset::east_opt(second).unwrap()
|
||||
} else {
|
||||
FixedOffset::west(second)
|
||||
FixedOffset::west_opt(second).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,9 +92,9 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
|||
|
||||
// first try to parse as a full datetime with optional timezone
|
||||
if let Some(caps) = DATETIME_REGEX.captures(input) {
|
||||
let year: i32 = (&caps["year"]).parse().unwrap();
|
||||
let month: u32 = (&caps["month"]).parse().unwrap();
|
||||
let day: u32 = (&caps["day"]).parse().unwrap();
|
||||
let year: i32 = caps["year"].parse().unwrap();
|
||||
let month: u32 = caps["month"].parse().unwrap();
|
||||
let day: u32 = caps["day"].parse().unwrap();
|
||||
let hour: u32 = caps.name("hour").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
|
||||
let minute: u32 = caps.name("minute").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
|
||||
let second: u32 = caps.name("second").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
|
||||
|
@ -107,8 +105,8 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
|||
Utc, input, year, month, day, hour, minute, second,
|
||||
)
|
||||
} else {
|
||||
let hours: i32 = (&caps["ohour"]).parse().unwrap();
|
||||
let minutes: i32 = (&caps["omin"]).parse().unwrap();
|
||||
let hours: i32 = caps["ohour"].parse().unwrap();
|
||||
let minutes: i32 = caps["omin"].parse().unwrap();
|
||||
let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes);
|
||||
|
||||
date_from_parts(
|
||||
|
@ -124,7 +122,7 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
|||
}
|
||||
|
||||
if let Some(caps) = HOUR_REGEX.captures(input) {
|
||||
let hour: u32 = (&caps["hour"]).parse().unwrap();
|
||||
let hour: u32 = caps["hour"].parse().unwrap();
|
||||
let minute: u32 = caps.name("minute").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
|
||||
let second: u32 = caps.name("second").map(|t| t.as_str().parse().unwrap()).unwrap_or(0);
|
||||
|
||||
|
@ -136,8 +134,8 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
|||
Utc, input, year, month, day, hour, minute, second,
|
||||
)
|
||||
} else {
|
||||
let hours: i32 = (&caps["ohour"]).parse().unwrap();
|
||||
let minutes: i32 = (&caps["omin"]).parse().unwrap();
|
||||
let hours: i32 = caps["ohour"].parse().unwrap();
|
||||
let minutes: i32 = caps["omin"].parse().unwrap();
|
||||
let fo = offset_from_parts(&caps["sign"] == "+", hours, minutes);
|
||||
let (year, month, day) = date_parts(fo.from_utc_datetime(&Utc::now().naive_utc()));
|
||||
|
||||
|
@ -157,56 +155,60 @@ pub fn parse_time(input: &str) -> Result<DateTime<Utc>> {
|
|||
Err(Error::DateTimeParseError(input.into()))
|
||||
}
|
||||
|
||||
pub fn parse_hours(input: &str) -> Result<u16> {
|
||||
input.parse().map_err(|_| Error::InvalidHours(input.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{TimeZone, Duration};
|
||||
use chrono::{TimeZone, Duration, Timelike};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_datetime_string() {
|
||||
assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 0));
|
||||
assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 12));
|
||||
assert_eq!(parse_time("2021-05-21 11:36").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap());
|
||||
assert_eq!(parse_time("2021-05-21 11:36:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
|
||||
|
||||
assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 0));
|
||||
assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Local.ymd(2021, 5, 21).and_hms(11, 36, 12));
|
||||
assert_eq!(parse_time("2021-05-21T11:36").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 0).unwrap());
|
||||
assert_eq!(parse_time("2021-05-21T11:36:12").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_date() {
|
||||
assert_eq!(parse_time("2021-05-21").unwrap(), Local.ymd(2021, 5, 21).and_hms(0, 0, 0));
|
||||
assert_eq!(parse_time("2021-05-21").unwrap(), Local.with_ymd_and_hms(2021, 5, 21, 0, 0, 0).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_hour() {
|
||||
let localdate = Local::now().date();
|
||||
let localdate = Local::now().with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
|
||||
|
||||
assert_eq!(parse_time("11:36").unwrap(), localdate.and_hms(11, 36, 0));
|
||||
assert_eq!(parse_time("11:36:35").unwrap(), localdate.and_hms(11, 36, 35));
|
||||
assert_eq!(parse_time("11:36").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc));
|
||||
assert_eq!(parse_time("11:36:35").unwrap(), localdate.with_hour(11).unwrap().with_minute(36).unwrap().with_second(35).unwrap().with_timezone(&Utc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_hour_with_timezone() {
|
||||
let hours: i32 = 3600;
|
||||
let todayutc = Utc::now().date();
|
||||
let todayutc = Utc::now().date_naive();
|
||||
|
||||
assert_eq!(parse_time("11:36Z").unwrap(), todayutc.and_hms(11, 36, 0));
|
||||
assert_eq!(parse_time("11:36:35z").unwrap(), todayutc.and_hms(11, 36, 35));
|
||||
assert_eq!(parse_time("11:36Z").unwrap(), todayutc.and_hms_opt(11, 36, 0).unwrap().and_local_timezone(Utc).unwrap());
|
||||
assert_eq!(parse_time("11:36:35z").unwrap(), todayutc.and_hms_opt(11, 36, 35).unwrap().and_local_timezone(Utc).unwrap());
|
||||
|
||||
let offset = FixedOffset::west(5 * hours);
|
||||
let todayoffset = offset.from_utc_datetime(&Utc::now().naive_utc()).date();
|
||||
assert_eq!(parse_time("11:36-5:00").unwrap(), todayoffset.and_hms(11, 36, 0));
|
||||
let offset = FixedOffset::west_opt(5 * hours).unwrap();
|
||||
let today_at_offset = offset.from_utc_datetime(&Utc::now().naive_utc()).with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
|
||||
assert_eq!(parse_time("11:36-5:00").unwrap(), today_at_offset.with_hour(11).unwrap().with_minute(36).unwrap().with_timezone(&Utc));
|
||||
|
||||
let offset = FixedOffset::east(5 * hours);
|
||||
let todayoffset = offset.from_utc_datetime(&Utc::now().naive_utc()).date();
|
||||
assert_eq!(parse_time("11:36:35+5:00").unwrap(), todayoffset.and_hms(11, 36, 35));
|
||||
let offset = FixedOffset::east_opt(5 * hours).unwrap();
|
||||
let today_at_offset = offset.from_utc_datetime(&Utc::now().naive_utc()).with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap().with_nanosecond(0).unwrap();
|
||||
assert_eq!(parse_time("11:36:35+5:00").unwrap(), today_at_offset.with_hour(11).unwrap().with_minute(36).unwrap().with_second(35).unwrap().with_timezone(&Utc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_specified_timezone() {
|
||||
assert_eq!(parse_time("2021-05-21T11:36:12Z").unwrap(), Utc.ymd(2021, 5, 21).and_hms(11, 36, 12));
|
||||
assert_eq!(parse_time("2021-05-21T11:36:12-3:00").unwrap(), Utc.ymd(2021, 5, 21).and_hms(14, 36, 12));
|
||||
assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.ymd(2021, 5, 21).and_hms(8, 36, 12));
|
||||
assert_eq!(parse_time("2021-05-21T11:36:12Z").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 11, 36, 12).unwrap());
|
||||
assert_eq!(parse_time("2021-05-21T11:36:12-3:00").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 14, 36, 12).unwrap());
|
||||
assert_eq!(parse_time("2021-05-21T11:36:12+3:00").unwrap(), Utc.with_ymd_and_hms(2021, 5, 21, 8, 36, 12).unwrap());
|
||||
}
|
||||
|
||||
fn time_diff(t1: DateTime<Utc>, t2: DateTime<Local>) {
|
||||
|
@ -222,7 +224,7 @@ mod tests {
|
|||
time_diff(parse_time("two hours ago").unwrap(), Local::now() - Duration::hours(2));
|
||||
time_diff(parse_time("ten hours ago").unwrap(), Local::now() - Duration::hours(10));
|
||||
time_diff(parse_time("twenty one hours ago").unwrap(), Local::now() - Duration::hours(21));
|
||||
time_diff(dbg!(parse_time("15 hours ago").unwrap()), dbg!(Local::now() - Duration::hours(15)));
|
||||
time_diff(parse_time("15 hours ago").unwrap(), Local::now() - Duration::hours(15));
|
||||
|
||||
// minutes
|
||||
time_diff(parse_time("a minute ago").unwrap(), Local::now() - Duration::minutes(1));
|
||||
|
@ -231,7 +233,7 @@ mod tests {
|
|||
time_diff(parse_time("forty one minutes ago").unwrap(), Local::now() - Duration::minutes(41));
|
||||
time_diff(parse_time("1 minute ago").unwrap(), Local::now() - Duration::minutes(1));
|
||||
time_diff(parse_time("23 minutes ago").unwrap(), Local::now() - Duration::minutes(23));
|
||||
time_diff(parse_time("half an hour ago").unwrap(), dbg!(Local::now() - Duration::minutes(30)));
|
||||
time_diff(parse_time("half an hour ago").unwrap(), Local::now() - Duration::minutes(30));
|
||||
|
||||
// mixed
|
||||
time_diff(parse_time("an hour 10 minutes ago").unwrap(), Local::now() - Duration::minutes(70));
|
||||
|
|
Loading…
Reference in New Issue