├── .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 | header image 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 | image 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 | image 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 | image 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 | image 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 | [![Stargazers over time](https://starchart.cc/caarlos0/tasktimer.svg)](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 | [![Release](https://img.shields.io/github/release/caarlos0/tasktimer.svg?style=for-the-badge)](https://github.com/caarlos0/tasktimer/releases/latest) 135 | 136 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](LICENSE.md) 137 | 138 | [![Build](https://img.shields.io/github/workflow/status/caarlos0/tasktimer/build?style=for-the-badge)](https://github.com/caarlos0/tasktimer/actions?query=workflow%3Abuild) 139 | 140 | [![Go Report Card](https://goreportcard.com/badge/github.com/caarlos0/tasktimer?style=for-the-badge)](https://goreportcard.com/report/github.com/caarlos0/tasktimer) 141 | 142 | [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?style=for-the-badge)](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 | --------------------------------------------------------------------------------