├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── lock.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── go.mod
├── go.sum
├── goreleaser.yml
├── internal
├── cmd
│ ├── common.go
│ ├── completion.go
│ ├── edit.go
│ ├── fromjson.go
│ ├── list.go
│ ├── log.go
│ ├── man.go
│ ├── paths.go
│ ├── report.go
│ ├── root.go
│ └── tojson.go
├── model
│ └── model.go
├── store
│ └── store.go
└── ui
│ ├── common.go
│ ├── json.go
│ ├── main.go
│ ├── markdown.go
│ └── project_timer.go
├── main.go
├── scripts
├── completions.sh
└── manpages.sh
└── static
└── undraw_Dev_focus_re_6iwt.svg
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [caarlos0]
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gomod"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | time: "08:00"
8 | labels:
9 | - "dependencies"
10 | - "automerge"
11 | - package-ecosystem: "github-actions"
12 | directory: "/"
13 | schedule:
14 | interval: "daily"
15 | time: "08:00"
16 | labels:
17 | - "dependencies"
18 | - "automerge"
19 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | tags:
8 | - "v*"
9 | pull_request:
10 |
11 | permissions:
12 | contents: write
13 | id-token: write
14 | packages: write
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0
24 | - name: Set up Go
25 | uses: actions/setup-go@v5
26 | with:
27 | go-version: stable
28 | - run: |
29 | go mod tidy
30 | go test -v ./...
31 | go build -o tt .
32 | - uses: sigstore/cosign-installer@v3.8.2
33 | - uses: goreleaser/goreleaser-action@v6
34 | if: success() && startsWith(github.ref, 'refs/tags/')
35 | with:
36 | distribution: goreleaser-pro
37 | version: latest
38 | args: release --clean
39 | env:
40 | GITHUB_TOKEN: ${{ secrets.GH_PAT }}
41 | FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
42 | GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
43 | COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
44 | AUR_KEY: ${{ secrets.AUR_KEY }}
45 | dependabot:
46 | needs: [build]
47 | runs-on: ubuntu-latest
48 | permissions:
49 | pull-requests: write
50 | contents: write
51 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}}
52 | steps:
53 | - id: metadata
54 | uses: dependabot/fetch-metadata@v2
55 | with:
56 | github-token: "${{ secrets.GITHUB_TOKEN }}"
57 | - run: |
58 | gh pr review --approve "$PR_URL"
59 | gh pr merge --squash --auto "$PR_URL"
60 | env:
61 | PR_URL: ${{github.event.pull_request.html_url}}
62 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
63 |
--------------------------------------------------------------------------------
/.github/workflows/lock.yml:
--------------------------------------------------------------------------------
1 | name: lock-inactive
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | schedule:
8 | - cron: '0 * * * *'
9 |
10 | jobs:
11 | lock:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: dessant/lock-threads@v5
15 | with:
16 | github-token: ${{ github.token }}
17 | issue-lock-inactive-days: 30
18 | pr-lock-inactive-days: 30
19 | issue-lock-comment: >
20 | This issue has been automatically locked since there
21 | has not been any recent activity after it was closed.
22 | Please open a new issue for related bugs.
23 | pr-lock-comment: >
24 | This pull request has been automatically locked since there
25 | has not been any recent activity after it was closed.
26 | Please open a new issue for related bugs.
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | tasktimer.log
2 | dist
3 | tt
4 | completions/
5 | tasktimer
6 | manpages
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Carlos Alexandro Becker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
tasktimer
4 | Task Timer (tt
) is a dead simple TUI task timer.
5 |
6 |
7 | ---
8 |
9 | ## Usage
10 |
11 | To get started, just run `tt`:
12 |
13 | ```sh
14 | tt
15 | ```
16 |
17 | You'll be presented with something like this:
18 |
19 |
20 |
21 | You can just type a small description of what you're working on and press
22 | ENTER to start timing.
23 |
24 | At any time, press ENTER again to stop the
25 | current timer or type a new task description and press ENTER
26 | to stop the previous task and start the new one.
27 |
28 | Each task will have its own timer, and the sum of all tasks will be displayed
29 | in the header:
30 |
31 |
32 |
33 | At any time, press CTRL+c to stop the current
34 | timer (if any) and exit.
35 |
36 | You can also press ESC stop the current task and blur the input
37 | field and navigate around a long list of tasks using the
38 | arrow keys/page up/page down/etc.
39 |
40 | Note: there is no concept of "resuming tasks", you can however create several tasks with the same description.
41 |
42 | ## Report
43 |
44 | You can extract a markdown file by running:
45 |
46 | ```sh
47 | tt report
48 | ```
49 |
50 | It will output the given project (via `-p PROJECT`) to `STDOUT`. You can
51 | then save it to a file, pipe to another software or do whatever you like:
52 |
53 |
54 |
55 | ## Edit
56 |
57 | Let's say you forgot the timer running... you can edit it using the edit command:
58 |
59 | ```sh
60 | tt edit
61 | ```
62 |
63 |
64 |
65 | The project will be exporter to a JSON file and will open with your `$EDITOR`.
66 | Once you close it, it will be imported over the old one.
67 |
68 | You can also backup/edit/restore using `tt to-json` and `tt from-json`.
69 |
70 | ## Help
71 |
72 | At any time, check `--help` to see the available options.
73 |
74 | ## Install
75 |
76 | **homebrew**:
77 |
78 | ```sh
79 | brew install caarlos0/tap/tt
80 | ```
81 |
82 | **apt**:
83 |
84 | ```sh
85 | echo 'deb [trusted=yes] https://repo.caarlos0.dev/apt/ /' | sudo tee /etc/apt/sources.list.d/caarlos0.list
86 | sudo apt update
87 | sudo apt install tt
88 | ```
89 |
90 | **yum**:
91 |
92 | ```sh
93 | echo '[caarlos0]
94 | name=caarlos0
95 | baseurl=https://repo.caarlos0.dev/yum/
96 | enabled=1
97 | gpgcheck=0' | sudo tee /etc/yum.repos.d/caarlos0.repo
98 | sudo yum install tt
99 | ```
100 |
101 | **arch linux**:
102 |
103 | ```sh
104 | yay -S tasktimer-bin
105 | ```
106 |
107 | **deb/rpm/apk**:
108 |
109 | Download the `.apk`, `.deb` or `.rpm` from the [releases page][releases] and install with the appropriate commands.
110 |
111 | **manually**:
112 |
113 | Download the pre-compiled binaries from the [releases page][releases] or clone the repo build from source.
114 |
115 | ## FAQ
116 |
117 | ### Where are data and logs stored?
118 |
119 | Depends on the OS, but you can see yours running:
120 |
121 | ```sh
122 | tt paths
123 | ```
124 |
125 | ## Stargazers over time
126 |
127 | [](https://starchart.cc/caarlos0/tasktimer)
128 |
129 | [Badger]: https://github.com/dgraph-io/badger
130 | [releases]: https://github.com/caarlos0/tasktimer/releases
131 |
132 | # Badges
133 |
134 | [](https://github.com/caarlos0/tasktimer/releases/latest)
135 |
136 | [](LICENSE.md)
137 |
138 | [](https://github.com/caarlos0/tasktimer/actions?query=workflow%3Abuild)
139 |
140 | [](https://goreportcard.com/report/github.com/caarlos0/tasktimer)
141 |
142 | [](https://github.com/goreleaser)
143 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/caarlos0/tasktimer
2 |
3 | go 1.19
4 | toolchain go1.24.1
5 |
6 | require (
7 | github.com/caarlos0/timea.go v1.2.0
8 | github.com/charmbracelet/bubbles v0.20.0
9 | github.com/charmbracelet/bubbletea v1.3.4
10 | github.com/charmbracelet/glamour v0.9.1
11 | github.com/charmbracelet/lipgloss v1.1.0
12 | github.com/dgraph-io/badger/v3 v3.2103.5
13 | github.com/mattn/go-isatty v0.0.20
14 | github.com/muesli/go-app-paths v0.2.2
15 | github.com/muesli/mango-cobra v1.2.0
16 | github.com/muesli/roff v0.1.0
17 | github.com/spf13/cobra v1.9.1
18 | )
19 |
20 | require (
21 | github.com/alecthomas/chroma/v2 v2.14.0 // indirect
22 | github.com/atotto/clipboard v0.1.4 // indirect
23 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
24 | github.com/aymerick/douceur v0.2.0 // indirect
25 | github.com/cespare/xxhash v1.1.0 // indirect
26 | github.com/cespare/xxhash/v2 v2.1.2 // indirect
27 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
28 | github.com/charmbracelet/x/ansi v0.8.0 // indirect
29 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
30 | github.com/charmbracelet/x/term v0.2.1 // indirect
31 | github.com/dgraph-io/ristretto v0.1.1 // indirect
32 | github.com/dlclark/regexp2 v1.11.0 // indirect
33 | github.com/dustin/go-humanize v1.0.1 // indirect
34 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
35 | github.com/gogo/protobuf v1.3.2 // indirect
36 | github.com/golang/glog v1.2.4 // indirect
37 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
38 | github.com/golang/protobuf v1.5.2 // indirect
39 | github.com/golang/snappy v0.0.4 // indirect
40 | github.com/google/flatbuffers v22.10.26+incompatible // indirect
41 | github.com/gorilla/css v1.0.1 // indirect
42 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
43 | github.com/klauspost/compress v1.15.12 // indirect
44 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
45 | github.com/mattn/go-localereader v0.0.1 // indirect
46 | github.com/mattn/go-runewidth v0.0.16 // indirect
47 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect
48 | github.com/mitchellh/go-homedir v1.1.0 // indirect
49 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
50 | github.com/muesli/cancelreader v0.2.2 // indirect
51 | github.com/muesli/mango v0.2.0 // indirect
52 | github.com/muesli/mango-pflag v0.1.0 // indirect
53 | github.com/muesli/reflow v0.3.0 // indirect
54 | github.com/muesli/termenv v0.16.0 // indirect
55 | github.com/pkg/errors v0.9.1 // indirect
56 | github.com/rivo/uniseg v0.4.7 // indirect
57 | github.com/sahilm/fuzzy v0.1.1 // indirect
58 | github.com/spf13/pflag v1.0.6 // indirect
59 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
60 | github.com/yuin/goldmark v1.7.8 // indirect
61 | github.com/yuin/goldmark-emoji v1.0.5 // indirect
62 | go.opencensus.io v0.24.0 // indirect
63 | golang.org/x/net v0.36.0 // indirect
64 | golang.org/x/sync v0.12.0 // indirect
65 | golang.org/x/sys v0.31.0 // indirect
66 | golang.org/x/term v0.30.0 // indirect
67 | golang.org/x/text v0.23.0 // indirect
68 | google.golang.org/protobuf v1.33.0 // indirect
69 | )
70 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3 | github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
5 | github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
6 | github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
7 | github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
8 | github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
9 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
10 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
11 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
12 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
13 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
14 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
15 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
16 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
17 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
18 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
19 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
20 | github.com/caarlos0/timea.go v1.2.0 h1:JkjyWSUheN4nGO/OmYVGKbEv4ozHP/zuTZWD5Ih3Gog=
21 | github.com/caarlos0/timea.go v1.2.0/go.mod h1:p4uopjR7K+y0Oxh7j0vLh3vSo58jjzOgXHKcyKwQjuY=
22 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
23 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
24 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
25 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
26 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
27 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
28 | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
29 | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
30 | github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
31 | github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
32 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
33 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
34 | github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
35 | github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
36 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
37 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
38 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
39 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
40 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
41 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
42 | github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
43 | github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
44 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
45 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
46 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
47 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
48 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
49 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
50 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
51 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
52 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
53 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
54 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
55 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
56 | github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
57 | github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
58 | github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
59 | github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
60 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
61 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
62 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
63 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
64 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
65 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
66 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
67 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
68 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
69 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
70 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
71 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
72 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
73 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
74 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
75 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
76 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
77 | github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
78 | github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
79 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
80 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
81 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
82 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
83 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
84 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
85 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
86 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
87 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
88 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
89 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
90 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
91 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
92 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
93 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
94 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
95 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
96 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
97 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
98 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
99 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
100 | github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
101 | github.com/google/flatbuffers v22.10.26+incompatible h1:z1QiaMyPu1x3Z6xf2u1dsLj1ZxicdGSeaLpCuIsQNZM=
102 | github.com/google/flatbuffers v22.10.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
103 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
104 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
105 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
106 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
107 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
108 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
109 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
110 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
111 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
112 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
113 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
114 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
115 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
116 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
117 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
118 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
119 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
120 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
121 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
122 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
123 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
124 | github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
125 | github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
126 | github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
127 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
128 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
129 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
130 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
131 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
132 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
133 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
134 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
135 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
136 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
137 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
138 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
139 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
140 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
141 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
142 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
143 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
144 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
145 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
146 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
147 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
148 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
149 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
150 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
151 | github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI=
152 | github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
153 | github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
154 | github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
155 | github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
156 | github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
157 | github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
158 | github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
159 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
160 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
161 | github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
162 | github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
163 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
164 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
165 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
166 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
167 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
168 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
169 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
170 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
171 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
172 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
173 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
174 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
175 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
176 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
177 | github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
178 | github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
179 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
180 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
181 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
182 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
183 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
184 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
185 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
186 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
187 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
188 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
189 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
190 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
191 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
192 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
193 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
194 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
195 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
196 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
197 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
198 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
199 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
200 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
201 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
202 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
203 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
204 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
205 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
206 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
207 | github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
208 | github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
209 | github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
210 | github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
211 | github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
212 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
213 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
214 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
215 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
216 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
217 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
218 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
219 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
220 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
221 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
222 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
223 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
224 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
225 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
226 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
227 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
228 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
229 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
230 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
231 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
232 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
233 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
234 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
235 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
236 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
237 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
238 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
239 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
240 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
241 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
242 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
243 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
244 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
245 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
246 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
247 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
248 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
249 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
250 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
251 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
252 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
253 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
254 | golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
255 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
256 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
257 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
258 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
259 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
260 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
261 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
262 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
263 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
264 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
265 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
266 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
267 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
268 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
269 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
270 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
271 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
272 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
273 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
274 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
275 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
276 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
277 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
278 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
279 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
280 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
281 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
282 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
283 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
284 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
285 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
286 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
287 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
288 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
289 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
290 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
291 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
292 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
293 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
294 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
295 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
296 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
297 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
298 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
299 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
300 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
301 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
302 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
303 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
304 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
305 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
306 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
307 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
308 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
309 |
--------------------------------------------------------------------------------
/goreleaser.yml:
--------------------------------------------------------------------------------
1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
2 | version: 2
3 | project_name: tt
4 | variables:
5 | homepage: https://github.com/caarlos0/tasktimer
6 | repository: https://github.com/caarlos0/tasktimer
7 | description: Task Timer (tt) is a dead simple TUI task timer
8 | aur_project_name: tasktimer
9 | includes:
10 | - from_url:
11 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/build.yml
12 | - from_url:
13 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/package_with_completions_and_manpages.yml
14 | - from_url:
15 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/release.yml
16 | - from_url:
17 | url: https://raw.githubusercontent.com/caarlos0/goreleaserfiles/main/cosign_checksum.yml
18 |
19 | furies:
20 | - account: caarlos0
21 |
--------------------------------------------------------------------------------
/internal/cmd/common.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "io"
5 | "os"
6 | "path/filepath"
7 |
8 | tea "github.com/charmbracelet/bubbletea"
9 | "github.com/dgraph-io/badger/v3"
10 | gap "github.com/muesli/go-app-paths"
11 | )
12 |
13 | func paths(project string) (string, string, error) {
14 | home := gap.NewScope(gap.User, "tasktimer")
15 |
16 | logfile, err := home.LogPath(project + ".log")
17 | if err != nil {
18 | return "", "", err
19 | }
20 |
21 | dbfile, err := home.DataPath(project + ".db")
22 | if err != nil {
23 | return "", "", err
24 | }
25 |
26 | return logfile, dbfile, nil
27 | }
28 |
29 | func setup(project string) (*badger.DB, io.Closer, error) {
30 | logfile, dbfile, err := paths(project)
31 | if err != nil {
32 | return nil, nil, err
33 | }
34 |
35 | if err := os.MkdirAll(filepath.Dir(logfile), 0o754); err != nil {
36 | return nil, nil, err
37 | }
38 |
39 | f, err := tea.LogToFile(logfile, "tasktimer")
40 | if err != nil {
41 | return nil, nil, err
42 | }
43 |
44 | // TODO: maybe sync writes?
45 | options := badger.DefaultOptions(dbfile).
46 | WithLogger(badgerStdLoggerAdapter{}).
47 | WithLoggingLevel(badger.ERROR)
48 | db, err := badger.Open(options)
49 | return db, f, err
50 | }
51 |
--------------------------------------------------------------------------------
/internal/cmd/completion.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import "github.com/spf13/cobra"
4 |
5 | type completionCmd struct {
6 | cmd *cobra.Command
7 | }
8 |
9 | func newCompletionCmd() *completionCmd {
10 | cmd := &cobra.Command{
11 | Use: "completion [bash|zsh|fish]",
12 | Short: "Print shell autocompletion scripts for tt",
13 | Long: `To load completions:
14 | Bash:
15 | $ source <(tt completion bash)
16 | # To load completions for each session, execute once:
17 | Linux:
18 | $ tt completion bash > /etc/bash_completion.d/tt
19 | MacOS:
20 | $ tt completion bash > /usr/local/etc/bash_completion.d/tt
21 | Zsh:
22 | # If shell completion is not already enabled in your environment you will need
23 | # to enable it. You can execute the following once:
24 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc
25 | # To load completions for each session, execute once:
26 | $ tt completion zsh > "${fpath[1]}/_tt"
27 | # You will need to start a new shell for this setup to take effect.
28 | Fish:
29 | $ tt completion fish | source
30 | # To load completions for each session, execute once:
31 | $ tt completion fish > ~/.config/fish/completions/tt.fish
32 | `,
33 | SilenceUsage: true,
34 | DisableFlagsInUseLine: true,
35 | Hidden: true,
36 | ValidArgs: []string{"bash", "zsh", "fish"},
37 | Args: cobra.ExactValidArgs(1),
38 | RunE: func(cmd *cobra.Command, args []string) error {
39 | var err error
40 | switch args[0] {
41 | case "bash":
42 | err = cmd.Root().GenBashCompletion(cmd.OutOrStdout())
43 | case "zsh":
44 | err = cmd.Root().GenZshCompletion(cmd.OutOrStdout())
45 | case "fish":
46 | err = cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true)
47 | }
48 | return err
49 | },
50 | }
51 | return &completionCmd{cmd: cmd}
52 | }
53 |
--------------------------------------------------------------------------------
/internal/cmd/edit.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "strings"
9 | "time"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | type editCmd struct {
15 | cmd *cobra.Command
16 | }
17 |
18 | func newEditCmd() *editCmd {
19 | cmd := &cobra.Command{
20 | Use: "edit",
21 | Short: "Syntactic sugar for to-json | $EDITOR | from-json",
22 | Aliases: []string{"e"},
23 | Args: cobra.NoArgs,
24 | RunE: func(cmd *cobra.Command, args []string) error {
25 | tmp := filepath.Join(os.TempDir(), fmt.Sprintf("tt-%d.json", time.Now().Unix()))
26 |
27 | if err := newToJSONCmd().cmd.RunE(cmd, []string{tmp}); err != nil {
28 | return err
29 | }
30 |
31 | editor := strings.Fields(os.Getenv("EDITOR"))
32 | if len(editor) == 0 {
33 | return fmt.Errorf("no $EDITOR set")
34 | }
35 |
36 | editorCmd := editor[0]
37 | var editorArgs []string
38 | if len(editor) > 1 {
39 | editorArgs = append(editorArgs, editor[1:]...)
40 | }
41 | editorArgs = append(editorArgs, tmp)
42 |
43 | edit := exec.Command(editorCmd, editorArgs...)
44 | edit.Stderr = os.Stderr
45 | edit.Stdout = os.Stdout
46 | edit.Stdin = os.Stdin
47 | if err := edit.Run(); err != nil {
48 | return err
49 | }
50 |
51 | return newFromJSONCmd().cmd.RunE(cmd, []string{tmp})
52 | },
53 | }
54 |
55 | return &editCmd{cmd: cmd}
56 | }
57 |
--------------------------------------------------------------------------------
/internal/cmd/fromjson.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "os"
9 |
10 | "github.com/caarlos0/tasktimer/internal/model"
11 | "github.com/caarlos0/tasktimer/internal/store"
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | type fromJSONCmd struct {
16 | cmd *cobra.Command
17 | }
18 |
19 | func newFromJSONCmd() *fromJSONCmd {
20 | cmd := &cobra.Command{
21 | Use: "from-json",
22 | Short: "Imports a JSON into a project - WARNING: it will wipe the project first, use with care!",
23 | Args: cobra.ExactArgs(1),
24 | RunE: func(cmd *cobra.Command, args []string) error {
25 | project := cmd.Parent().Flag("project").Value.String()
26 | db, f, err := setup(project)
27 | if err != nil {
28 | return err
29 | }
30 | defer db.Close()
31 | defer f.Close()
32 |
33 | input, err := os.ReadFile(args[0])
34 | if err != nil {
35 | return fmt.Errorf("failed to read %s: %w", args[0], err)
36 | }
37 |
38 | var tasks []model.ExportedTask
39 | if err := json.Unmarshal(input, &tasks); err != nil {
40 | return fmt.Errorf("input json is not in the correct format: %w", err)
41 | }
42 |
43 | tmp, err := ioutil.TempFile("", "tasktimer-"+project)
44 | if err != nil {
45 | return fmt.Errorf("failed to create backup file: %w", err)
46 | }
47 | if _, err := db.Backup(tmp, 0); err != nil {
48 | return fmt.Errorf("failed to backup to %s: %w", tmp.Name(), err)
49 | }
50 |
51 | log.Printf("backup made to %s\n", tmp.Name())
52 |
53 | if err := db.DropAll(); err != nil {
54 | return fmt.Errorf("failed to clear database: %w", err)
55 | }
56 |
57 | return store.LoadTasks(db, tasks)
58 | },
59 | }
60 |
61 | return &fromJSONCmd{cmd: cmd}
62 | }
63 |
--------------------------------------------------------------------------------
/internal/cmd/list.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | "github.com/charmbracelet/glamour"
11 | "github.com/mattn/go-isatty"
12 | gap "github.com/muesli/go-app-paths"
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | type listCmd struct {
17 | cmd *cobra.Command
18 | }
19 |
20 | func newListCmd() *listCmd {
21 | cmd := &cobra.Command{
22 | Use: "list",
23 | Short: "List all projects",
24 | Args: cobra.NoArgs,
25 | RunE: func(cmd *cobra.Command, args []string) error {
26 | home := gap.NewScope(gap.User, "tasktimer")
27 | datas, err := home.DataDirs()
28 | if err != nil {
29 | return err
30 | }
31 |
32 | var buf bytes.Buffer
33 | for _, data := range datas {
34 | if _, err := os.Stat(data); err != nil && os.IsNotExist(err) {
35 | continue
36 | }
37 | if err := filepath.Walk(data, func(path string, info os.FileInfo, err error) error {
38 | if err != nil {
39 | return err
40 | }
41 | if filepath.Ext(path) == ".db" {
42 | _, _ = fmt.Fprintln(&buf, "- "+strings.Replace(filepath.Base(path), ".db", "", 1))
43 | return filepath.SkipDir
44 | }
45 | return nil
46 | }); err != nil {
47 | return err
48 | }
49 | }
50 |
51 | if isatty.IsTerminal(os.Stdout.Fd()) {
52 | rendered, err := glamour.RenderWithEnvironmentConfig(buf.String())
53 | if err != nil {
54 | return err
55 | }
56 | fmt.Print(rendered)
57 | return nil
58 | }
59 |
60 | fmt.Print(buf.String())
61 | return nil
62 | },
63 | }
64 |
65 | return &listCmd{cmd: cmd}
66 | }
67 |
--------------------------------------------------------------------------------
/internal/cmd/log.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "log"
5 | )
6 |
7 | type badgerStdLoggerAdapter struct{}
8 |
9 | func (b badgerStdLoggerAdapter) Errorf(s string, i ...interface{}) {
10 | log.Printf("[ERR] "+s, i...)
11 | }
12 |
13 | func (b badgerStdLoggerAdapter) Warningf(s string, i ...interface{}) {
14 | log.Printf("[WARN] "+s, i...)
15 | }
16 |
17 | func (b badgerStdLoggerAdapter) Infof(s string, i ...interface{}) {
18 | log.Printf("[INFO] "+s, i...)
19 | }
20 |
21 | func (b badgerStdLoggerAdapter) Debugf(s string, i ...interface{}) {
22 | log.Printf("[DEBUG] "+s, i...)
23 | }
24 |
--------------------------------------------------------------------------------
/internal/cmd/man.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | mcobra "github.com/muesli/mango-cobra"
8 | "github.com/muesli/roff"
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | type manCmd struct {
13 | cmd *cobra.Command
14 | }
15 |
16 | func newManCmd() *manCmd {
17 | root := &manCmd{}
18 | cmd := &cobra.Command{
19 | Use: "man",
20 | Short: "Generates tt's command line manpages",
21 | SilenceUsage: true,
22 | DisableFlagsInUseLine: true,
23 | Hidden: true,
24 | Args: cobra.NoArgs,
25 | RunE: func(cmd *cobra.Command, args []string) error {
26 | manPage, err := mcobra.NewManPage(1, root.cmd.Root())
27 | if err != nil {
28 | return err
29 | }
30 |
31 | _, err = fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument()))
32 | return err
33 | },
34 | }
35 |
36 | root.cmd = cmd
37 | return root
38 | }
39 |
--------------------------------------------------------------------------------
/internal/cmd/paths.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | type pathsCmd struct {
10 | cmd *cobra.Command
11 | }
12 |
13 | func newPathsCmd() *pathsCmd {
14 | cmd := &cobra.Command{
15 | Use: "paths",
16 | Short: "Print the paths being used for logs, data et al",
17 | Args: cobra.NoArgs,
18 | RunE: func(cmd *cobra.Command, args []string) error {
19 | project := cmd.Parent().Flag("project").Value.String()
20 | logfile, dbfile, err := paths(project)
21 | if err != nil {
22 | return err
23 | }
24 | fmt.Println("Database path:", dbfile)
25 | fmt.Println("Log path: ", logfile)
26 | return nil
27 | },
28 | }
29 | return &pathsCmd{cmd: cmd}
30 | }
31 |
--------------------------------------------------------------------------------
/internal/cmd/report.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/caarlos0/tasktimer/internal/ui"
9 | "github.com/charmbracelet/glamour"
10 | "github.com/mattn/go-isatty"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | type reportCmd struct {
15 | cmd *cobra.Command
16 | }
17 |
18 | func newRerportCmd() *reportCmd {
19 | cmd := &cobra.Command{
20 | Use: "report",
21 | Aliases: []string{"r"},
22 | Short: "Print a markdown report of the given project to STDOUT",
23 | Args: cobra.NoArgs,
24 | RunE: func(cmd *cobra.Command, args []string) error {
25 | project := cmd.Parent().Flag("project").Value.String()
26 | db, f, err := setup(project)
27 | if err != nil {
28 | return err
29 | }
30 | defer db.Close()
31 | defer f.Close()
32 |
33 | var buf bytes.Buffer
34 | if err := ui.WriteProjectMarkdown(db, project, &buf); err != nil {
35 | return err
36 | }
37 |
38 | md := buf.String()
39 |
40 | if isatty.IsTerminal(os.Stdout.Fd()) {
41 | rendered, err := glamour.RenderWithEnvironmentConfig(md)
42 | if err != nil {
43 | return err
44 | }
45 | md = rendered
46 | }
47 |
48 | fmt.Print(md)
49 | return nil
50 | },
51 | }
52 |
53 | return &reportCmd{cmd: cmd}
54 | }
55 |
--------------------------------------------------------------------------------
/internal/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/caarlos0/tasktimer/internal/ui"
5 | tea "github.com/charmbracelet/bubbletea"
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | func Execute(version string, exit func(int), args []string) {
10 | newRootCmd(version, exit).Execute(args)
11 | }
12 |
13 | type rootCmd struct {
14 | cmd *cobra.Command
15 | project string
16 | exit func(int)
17 | }
18 |
19 | func (c rootCmd) Execute(args []string) {
20 | c.cmd.SetArgs(args)
21 | if err := c.cmd.Execute(); err != nil {
22 | c.exit(1)
23 | }
24 | }
25 |
26 | func newRootCmd(version string, exit func(int)) *rootCmd {
27 | root := &rootCmd{
28 | exit: exit,
29 | }
30 | cmd := &cobra.Command{
31 | Use: "tt",
32 | Short: "Task Timer (tt) is a dead simple TUI task timer",
33 | Version: version,
34 | SilenceUsage: true,
35 | RunE: func(cmd *cobra.Command, args []string) error {
36 | db, f, err := setup(root.project)
37 | if err != nil {
38 | return err
39 | }
40 | defer db.Close()
41 | defer f.Close()
42 |
43 | p := tea.NewProgram(ui.Init(db, root.project))
44 | p.EnterAltScreen()
45 | defer p.ExitAltScreen()
46 | return p.Start()
47 | },
48 | }
49 |
50 | cmd.PersistentFlags().StringVarP(&root.project, "project", "p", "default", "Project name")
51 |
52 | cmd.AddCommand(
53 | newRerportCmd().cmd,
54 | newCompletionCmd().cmd,
55 | newPathsCmd().cmd,
56 | newToJSONCmd().cmd,
57 | newFromJSONCmd().cmd,
58 | newListCmd().cmd,
59 | newEditCmd().cmd,
60 | newManCmd().cmd,
61 | )
62 |
63 | root.cmd = cmd
64 | return root
65 | }
66 |
--------------------------------------------------------------------------------
/internal/cmd/tojson.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/caarlos0/tasktimer/internal/ui"
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | type toJSONCmd struct {
11 | cmd *cobra.Command
12 | }
13 |
14 | func newToJSONCmd() *toJSONCmd {
15 | cmd := &cobra.Command{
16 | Use: "to-json",
17 | Short: "Exports the database as JSON",
18 | Args: cobra.MaximumNArgs(1),
19 | RunE: func(cmd *cobra.Command, args []string) error {
20 | project := cmd.Parent().Flag("project").Value.String()
21 | db, f, err := setup(project)
22 | if err != nil {
23 | return err
24 | }
25 | defer db.Close()
26 | defer f.Close()
27 |
28 | if len(args) > 0 {
29 | f, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0o666)
30 | if err != nil {
31 | return err
32 | }
33 | defer f.Close()
34 | return ui.WriteProjectJSON(db, project, f)
35 | }
36 |
37 | return ui.WriteProjectJSON(db, project, os.Stdout)
38 | },
39 | }
40 |
41 | return &toJSONCmd{cmd: cmd}
42 | }
43 |
--------------------------------------------------------------------------------
/internal/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "time"
7 | )
8 |
9 | type Task struct {
10 | ID uint64 `json:"id"`
11 | Title string `json:"desc"`
12 | StartAt time.Time `json:"start"`
13 | EndAt time.Time `json:"end"`
14 | }
15 |
16 | func (t Task) Bytes() []byte {
17 | bts, err := json.Marshal(&t)
18 | if err != nil {
19 | log.Fatalln(err)
20 | }
21 | return bts
22 | }
23 |
24 | type ExportedTask struct {
25 | Title string `json:"desc"`
26 | StartAt time.Time `json:"start"`
27 | EndAt time.Time `json:"end"`
28 | }
29 |
30 | func (t ExportedTask) Bytes() []byte {
31 | bts, err := json.Marshal(&t)
32 | if err != nil {
33 | log.Fatalln(err)
34 | }
35 | return bts
36 | }
37 |
--------------------------------------------------------------------------------
/internal/store/store.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "log"
7 | "sort"
8 | "strconv"
9 | "time"
10 |
11 | "github.com/caarlos0/tasktimer/internal/model"
12 | "github.com/dgraph-io/badger/v3"
13 | )
14 |
15 | var (
16 | prefix = []byte("tasks.")
17 | sequenceID = []byte("tasks_seq")
18 | )
19 |
20 | func GetTaskList(db *badger.DB) ([]model.Task, error) {
21 | var tasks []model.Task
22 | if err := db.View(func(txn *badger.Txn) error {
23 | it := txn.NewIterator(badger.DefaultIteratorOptions)
24 | defer it.Close()
25 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
26 | item := it.Item()
27 | err := item.Value(func(v []byte) error {
28 | var task model.Task
29 | if err := json.Unmarshal(v, &task); err != nil {
30 | return err
31 | }
32 | tasks = append(tasks, task)
33 | return nil
34 | })
35 | if err != nil {
36 | return err
37 | }
38 | }
39 | return nil
40 | }); err != nil {
41 | return tasks, err
42 | }
43 | sort.Slice(tasks, func(i, j int) bool {
44 | return tasks[i].StartAt.After(tasks[j].StartAt)
45 | })
46 | return tasks, nil
47 | }
48 |
49 | func CloseTasks(db *badger.DB) error {
50 | return db.Update(func(txn *badger.Txn) error {
51 | it := txn.NewIterator(badger.DefaultIteratorOptions)
52 | defer it.Close()
53 | for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
54 | item := it.Item()
55 | k := item.Key()
56 | err := item.Value(func(v []byte) error {
57 | var task model.Task
58 | if err := json.Unmarshal(v, &task); err != nil {
59 | return err
60 | }
61 | if !task.EndAt.IsZero() {
62 | return nil
63 | }
64 | task.EndAt = time.Now().Truncate(time.Second)
65 | log.Println("closing", task.Title)
66 | return txn.Set(k, task.Bytes())
67 | })
68 | if err != nil {
69 | return err
70 | }
71 | }
72 | return nil
73 | })
74 | }
75 |
76 | func CreateTask(db *badger.DB, t string) error {
77 | if t == "" {
78 | return nil
79 | }
80 |
81 | return db.Update(func(txn *badger.Txn) error {
82 | seq, err := db.GetSequence(sequenceID, 100)
83 | if err != nil {
84 | return err
85 | }
86 | defer seq.Release()
87 | s, err := seq.Next()
88 | if err != nil {
89 | return err
90 | }
91 |
92 | id := string(prefix) + strconv.FormatUint(s, 10)
93 | log.Println("creating task:", id, "->", t)
94 | return txn.Set([]byte(id), model.Task{
95 | ID: s,
96 | Title: t,
97 | StartAt: time.Now().Truncate(time.Second),
98 | }.Bytes())
99 | })
100 | }
101 |
102 | func LoadTasks(db *badger.DB, tasks []model.ExportedTask) error {
103 | sort.Slice(tasks, func(i, j int) bool {
104 | return tasks[i].StartAt.Before(tasks[j].StartAt)
105 | })
106 | return db.Update(func(txn *badger.Txn) error {
107 | seq, err := db.GetSequence(sequenceID, 100)
108 | if err != nil {
109 | return err
110 | }
111 | defer seq.Release()
112 |
113 | for _, t := range tasks {
114 | s, err := seq.Next()
115 | if err != nil {
116 | return err
117 | }
118 | id := string(prefix) + strconv.FormatUint(s, 10)
119 | log.Println("creating task:", id, "->", t)
120 | if err := txn.Set([]byte(id), model.Task{
121 | ID: s,
122 | Title: t.Title,
123 | StartAt: t.StartAt,
124 | EndAt: t.EndAt,
125 | }.Bytes()); err != nil {
126 | return fmt.Errorf("failed to create task: %w", err)
127 | }
128 | }
129 |
130 | return nil
131 | })
132 | }
133 |
--------------------------------------------------------------------------------
/internal/ui/common.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/charmbracelet/bubbles/list"
5 | "github.com/charmbracelet/lipgloss"
6 | )
7 |
8 | // light palette: https://colorhunt.co/palette/201882
9 | // dark palette: https://colorhunt.co/palette/273948
10 | var (
11 | defaultStyles = list.NewDefaultItemStyles()
12 |
13 | activeColor = defaultStyles.SelectedTitle.GetForeground()
14 | secondaryColor = defaultStyles.NormalTitle.GetForeground()
15 |
16 | errorColor = lipgloss.AdaptiveColor{
17 | Light: "#e94560",
18 | Dark: "#f05945",
19 | }
20 |
21 | secondaryForeground = lipgloss.NewStyle().Foreground(secondaryColor)
22 | boldStyle = lipgloss.NewStyle().Bold(true)
23 | activeForegroundBold = lipgloss.NewStyle().Bold(true).Foreground(activeColor)
24 | errorFaintForeground = lipgloss.NewStyle().Foreground(errorColor).Faint(true)
25 | errorForegroundPadded = lipgloss.NewStyle().Padding(4).Foreground(errorColor)
26 | separator = secondaryForeground.Render(" • ")
27 | listStyle = lipgloss.NewStyle().Margin(6, 2, 0, 2)
28 | )
29 |
--------------------------------------------------------------------------------
/internal/ui/json.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 |
7 | "github.com/caarlos0/tasktimer/internal/model"
8 | "github.com/caarlos0/tasktimer/internal/store"
9 | "github.com/dgraph-io/badger/v3"
10 | )
11 |
12 | // WriteProjectJSON writes the project task list in JSON format to the given
13 | // io.Writer.
14 | func WriteProjectJSON(db *badger.DB, project string, w io.Writer) error {
15 | tasks, err := store.GetTaskList(db)
16 | if err != nil {
17 | return err
18 | }
19 |
20 | var expTasks []model.ExportedTask
21 | for _, t := range tasks {
22 | expTasks = append(expTasks, model.ExportedTask{
23 | Title: t.Title,
24 | StartAt: t.StartAt,
25 | EndAt: t.EndAt,
26 | })
27 | }
28 | bts, err := json.MarshalIndent(expTasks, "", " ")
29 | if err != nil {
30 | return err
31 | }
32 |
33 | _, err = w.Write(bts)
34 |
35 | return err
36 | }
37 |
--------------------------------------------------------------------------------
/internal/ui/main.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "log"
5 | "strings"
6 | "time"
7 |
8 | "github.com/caarlos0/tasktimer/internal/model"
9 | "github.com/caarlos0/tasktimer/internal/store"
10 | timeago "github.com/caarlos0/timea.go"
11 | "github.com/charmbracelet/bubbles/key"
12 | "github.com/charmbracelet/bubbles/list"
13 | "github.com/charmbracelet/bubbles/spinner"
14 | "github.com/charmbracelet/bubbles/textinput"
15 | tea "github.com/charmbracelet/bubbletea"
16 | "github.com/dgraph-io/badger/v3"
17 | )
18 |
19 | type keymap struct {
20 | Esc key.Binding
21 | Enter key.Binding
22 | CtrlC key.Binding
23 | R key.Binding
24 | }
25 |
26 | func Init(db *badger.DB, project string) tea.Model {
27 | input := textinput.NewModel()
28 | input.Prompt = "❯ "
29 | input.Placeholder = "New task description..."
30 | input.Focus()
31 | input.CharLimit = 250
32 | input.Width = 50
33 |
34 | keymap := &keymap{
35 | Esc: key.NewBinding(
36 | key.WithKeys("esc"),
37 | key.WithHelp("esc", "cancel"),
38 | ),
39 | Enter: key.NewBinding(
40 | key.WithKeys("enter"),
41 | key.WithHelp("enter", "start/stop timer"),
42 | ),
43 | CtrlC: key.NewBinding(
44 | key.WithKeys("ctrl+c"),
45 | key.WithHelp("ctrl+c", "exit"),
46 | ),
47 | R: key.NewBinding(
48 | key.WithKeys("r"),
49 | key.WithHelp("r", "restart"),
50 | ),
51 | }
52 |
53 | l := list.NewModel([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
54 | l.Title = "tasks"
55 | l.SetSpinner(spinner.Pulse)
56 | l.DisableQuitKeybindings()
57 | l.AdditionalShortHelpKeys = func() []key.Binding {
58 | return []key.Binding{
59 | keymap.Esc,
60 | keymap.Enter,
61 | keymap.CtrlC,
62 | keymap.R,
63 | }
64 | }
65 |
66 | return mainModel{
67 | list: l,
68 | timer: projectTimerModel{},
69 | db: db,
70 | input: input,
71 | project: project,
72 | keymap: keymap,
73 | }
74 | }
75 |
76 | type mainModel struct {
77 | input textinput.Model
78 | list list.Model
79 | timer projectTimerModel
80 | db *badger.DB
81 | project string
82 | err error
83 | keymap *keymap
84 | }
85 |
86 | func (m mainModel) Init() tea.Cmd {
87 | return tea.Batch(
88 | m.list.StartSpinner(),
89 | enqueueTaskListUpdate,
90 | textinput.Blink,
91 | )
92 | }
93 |
94 | func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
95 | var cmds []tea.Cmd
96 | var cmd tea.Cmd
97 | var newMsg tea.Msg
98 |
99 | m.list.DisableQuitKeybindings()
100 | m.list.KeyMap.CursorUp.SetEnabled(!m.input.Focused() && !m.list.SettingFilter())
101 | m.list.KeyMap.CursorDown.SetEnabled(!m.input.Focused() && !m.list.SettingFilter())
102 | m.list.KeyMap.Filter.SetEnabled(!m.input.Focused() && !m.list.SettingFilter())
103 | m.keymap.Esc.SetEnabled(m.input.Focused())
104 |
105 | switch msg := msg.(type) {
106 | case errMsg:
107 | log.Println("errMsg")
108 | m.err = msg.error
109 | case tea.WindowSizeMsg:
110 | log.Println("tea.WindowSizeMsg")
111 | top, right, bottom, left := listStyle.GetMargin()
112 | m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom)
113 | case updateTaskListMsg:
114 | log.Println("updateTaskListMsg")
115 | cmds = append(cmds, m.list.StartSpinner(), updateTaskListCmd(m.db))
116 | case taskListUpdatedMsg:
117 | log.Println("taskListUpdatedMsg")
118 | items := make([]list.Item, 0, len(msg.tasks))
119 | for _, t := range msg.tasks {
120 | items = append(items, item{
121 | title: t.Title,
122 | start: t.StartAt,
123 | end: t.EndAt,
124 | })
125 | }
126 |
127 | m.list.StopSpinner()
128 | m.list.ResetSelected()
129 | m.list.ResetFilter()
130 | cmds = append(cmds, m.list.SetItems(items), updateProjectTimerCmd(msg.tasks))
131 | case tea.KeyMsg:
132 | if key.Matches(msg, m.keymap.CtrlC) {
133 | log.Println("tea.KeyMsg -> ctrl+c")
134 | return m, tea.Sequentially(closeTasksCmd(m.db), tea.Quit)
135 | }
136 |
137 | if m.list.SettingFilter() {
138 | log.Println("tea.KeyMsg -> settingFilter")
139 | break
140 | }
141 |
142 | if m.input.Focused() {
143 | if key.Matches(msg, m.keymap.Esc) {
144 | log.Println("tea.KeyMsg -> input.Focused -> esc")
145 | m.input.Blur()
146 | cmds = append(cmds, tea.Sequentially(
147 | closeTasksCmd(m.db),
148 | updateTaskListCmd(m.db)),
149 | )
150 | }
151 | if key.Matches(msg, m.keymap.Enter) {
152 | log.Println("tea.KeyMsg -> input.Focused -> enter")
153 | cmds = append(cmds, tea.Sequentially(
154 | closeTasksCmd(m.db),
155 | createTaskCmd(m.db, strings.TrimSpace(m.input.Value())),
156 | ))
157 | m.input.SetValue("")
158 | }
159 |
160 | // delegate keypresses to input
161 | log.Println("tea.KeyMsg -> input.Focused")
162 | m.input, cmd = m.input.Update(msg)
163 | cmds = append(cmds, cmd)
164 | newMsg = doNotPropagateMsg{}
165 | } else {
166 | if key.Matches(msg, m.keymap.Esc) {
167 | log.Println("tea.KeyMsg -> !input.Focused -> esc")
168 | newMsg = doNotPropagateMsg{}
169 | }
170 | if key.Matches(msg, m.keymap.Enter) {
171 | log.Println("tea.KeyMsg -> !input.Focused -> enter")
172 | m.input.Focus()
173 | cmds = append(cmds, textinput.Blink)
174 | }
175 | if key.Matches(msg, m.keymap.R) {
176 | log.Println("tea.KeyMsg -> !input.Focused -> R")
177 | m.input.SetValue(m.list.SelectedItem().FilterValue())
178 | m.input.Focus()
179 | cmds = append(cmds, textinput.Blink)
180 | newMsg = doNotPropagateMsg{};
181 | }
182 | }
183 | }
184 |
185 | if newMsg != nil {
186 | log.Println("tea.KeyMsg -> override original msg")
187 | msg = newMsg
188 | }
189 |
190 | m.timer, cmd = m.timer.Update(msg)
191 | cmds = append(cmds, cmd)
192 | m.input, cmd = m.input.Update(msg)
193 | cmds = append(cmds, cmd)
194 | m.list, cmd = m.list.Update(msg)
195 | cmds = append(cmds, cmd)
196 | return m, tea.Batch(cmds...)
197 | }
198 |
199 | func (m mainModel) View() string {
200 | if m.err != nil {
201 | return "\n" +
202 | errorFaintForeground.Render("Oops, something went wrong:") +
203 | "\n\n" +
204 | errorForegroundPadded.Render(m.err.Error()) +
205 | "\n\n" +
206 | errorFaintForeground.Render("Check the logs for more details...")
207 | }
208 | return secondaryForeground.Render("project: ") +
209 | activeForegroundBold.Render(m.project) +
210 | separator + m.timer.View() + "\n\n" +
211 | m.input.View() + "\n\n" +
212 | m.list.View() + "\n"
213 | }
214 |
215 | // msgs
216 |
217 | type doNotPropagateMsg struct{}
218 |
219 | type updateTaskListMsg struct{}
220 |
221 | type taskListUpdatedMsg struct {
222 | tasks []model.Task
223 | }
224 |
225 | type errMsg struct{ error }
226 |
227 | func (e errMsg) Error() string { return e.error.Error() }
228 |
229 | // cmds
230 |
231 | func closeTasksCmd(db *badger.DB) tea.Cmd {
232 | return func() tea.Msg {
233 | log.Println("closeTasksCmd")
234 | if err := store.CloseTasks(db); err != nil {
235 | return errMsg{err}
236 | }
237 | return nil
238 | }
239 | }
240 |
241 | func createTaskCmd(db *badger.DB, t string) tea.Cmd {
242 | return func() tea.Msg {
243 | log.Println("createTaskCmd")
244 | if err := store.CreateTask(db, t); err != nil {
245 | return errMsg{err}
246 | }
247 | return updateTaskListMsg{}
248 | }
249 | }
250 |
251 | func enqueueTaskListUpdate() tea.Msg {
252 | return updateTaskListMsg{}
253 | }
254 |
255 | func updateTaskListCmd(db *badger.DB) tea.Cmd {
256 | return func() tea.Msg {
257 | log.Println("updateTaskListCmd")
258 | tasks, err := store.GetTaskList(db)
259 | if err != nil {
260 | return errMsg{err}
261 | }
262 | return taskListUpdatedMsg{tasks}
263 | }
264 | }
265 |
266 | // models
267 |
268 | type item struct {
269 | title string
270 | start, end time.Time
271 | }
272 |
273 | func (i item) Title() string {
274 | if i.end.IsZero() {
275 | return boldStyle.Render(i.title)
276 | }
277 | return i.title
278 | }
279 |
280 | func (i item) Description() string {
281 | end := time.Now()
282 | if !i.end.IsZero() {
283 | end = i.end
284 | }
285 | ago := timeago.Of(i.start, timeago.Options{
286 | Precision: timeago.MinutePrecision,
287 | })
288 | return ago + " - " + end.Sub(i.start).Round(time.Second).String()
289 | }
290 |
291 | func (i item) FilterValue() string { return i.title }
292 |
--------------------------------------------------------------------------------
/internal/ui/markdown.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "time"
7 |
8 | "github.com/caarlos0/tasktimer/internal/store"
9 | "github.com/dgraph-io/badger/v3"
10 | )
11 |
12 | // WriteProjectMarkdown writes the project task list in markdown format to the given
13 | // io.Writer.
14 | func WriteProjectMarkdown(db *badger.DB, project string, w io.Writer) error {
15 | tasks, err := store.GetTaskList(db)
16 | if err != nil {
17 | return err
18 | }
19 |
20 | if len(tasks) == 0 {
21 | return fmt.Errorf("project %s has no tasks", project)
22 | }
23 |
24 | _, _ = fmt.Fprintln(w, "# "+project+"\n")
25 | _, _ = fmt.Fprintf(
26 | w,
27 | "> Total time **%s**, timed between **%s** and **%s**\n\n",
28 | sumTasksTimes(tasks, time.Time{}).Round(time.Second).String(),
29 | tasks[len(tasks)-1].StartAt.Format("2006-01-02"),
30 | tasks[0].EndAt.Format("2006-01-02"),
31 | )
32 |
33 | for _, task := range tasks {
34 | _, _ = fmt.Fprintf(
35 | w,
36 | "- **#%d** %s - _%s_ - _%s_\n",
37 | task.ID+1,
38 | task.Title,
39 | task.StartAt.Format("2006-01-02 15:04:05"),
40 | task.EndAt.Sub(task.StartAt).Round(time.Second),
41 | )
42 | }
43 |
44 | return nil
45 | }
46 |
--------------------------------------------------------------------------------
/internal/ui/project_timer.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/caarlos0/tasktimer/internal/model"
7 | tea "github.com/charmbracelet/bubbletea"
8 | )
9 |
10 | type projectTimerModel struct {
11 | tasks []model.Task
12 | }
13 |
14 | func (m projectTimerModel) Init() tea.Cmd {
15 | return nil
16 | }
17 |
18 | func (m projectTimerModel) Update(msg tea.Msg) (projectTimerModel, tea.Cmd) {
19 | switch msg := msg.(type) {
20 | case projectTimerUpdateMsg:
21 | m.tasks = msg.tasks
22 | }
23 | return m, nil
24 | }
25 |
26 | func (m projectTimerModel) View() string {
27 | return secondaryForeground.Render("total: ") +
28 | activeForegroundBold.Render(sumTasksTimes(m.tasks, time.Time{}).Round(time.Second).String()) +
29 | separator +
30 | secondaryForeground.Render("today: ") +
31 | activeForegroundBold.Render(sumTasksTimes(m.tasks, todayAtMidnight()).Round(time.Second).String())
32 | }
33 |
34 | // msgs and cmds
35 |
36 | type projectTimerUpdateMsg struct {
37 | tasks []model.Task
38 | }
39 |
40 | func updateProjectTimerCmd(tasks []model.Task) tea.Cmd {
41 | return func() tea.Msg {
42 | return projectTimerUpdateMsg{tasks}
43 | }
44 | }
45 |
46 | func sumTasksTimes(tasks []model.Task, since time.Time) time.Duration {
47 | d := time.Duration(0)
48 | for _, t := range tasks {
49 | if t.StartAt.Before(since) {
50 | continue
51 | }
52 |
53 | z := t.EndAt
54 | if z.IsZero() {
55 | z = time.Now()
56 | }
57 | d += z.Sub(t.StartAt)
58 | }
59 | return d
60 | }
61 |
62 | func todayAtMidnight() time.Time {
63 | return time.Now().Truncate(time.Hour * 24)
64 | }
65 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/caarlos0/tasktimer/internal/cmd"
7 | )
8 |
9 | var version = "dev"
10 |
11 | func main() {
12 | cmd.Execute(
13 | version,
14 | os.Exit,
15 | os.Args[1:],
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/scripts/completions.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | rm -rf completions
4 | mkdir completions
5 | go build -o tt .
6 | for sh in bash zsh fish; do
7 | ./tt completion "$sh" >"completions/tt.$sh"
8 | done
9 |
--------------------------------------------------------------------------------
/scripts/manpages.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | rm -rf manpages
4 | mkdir manpages
5 | go run . man | gzip -c >manpages/tt.1.gz
6 |
--------------------------------------------------------------------------------
/static/undraw_Dev_focus_re_6iwt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------