├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── scripts ├── manpages.sh └── completions.sh ├── timer.tape ├── LICENSE.md ├── goreleaser.yml ├── go.mod ├── README.md ├── go.sum └── main.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [caarlos0] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | timer 2 | manpages/ 3 | completions/ 4 | timer.exe 5 | dist 6 | -------------------------------------------------------------------------------- /scripts/manpages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | rm -rf manpages 4 | mkdir manpages 5 | go run . man | gzip -c >manpages/timer.1.gz 6 | -------------------------------------------------------------------------------- /timer.tape: -------------------------------------------------------------------------------- 1 | Output timer.gif 2 | 3 | Set Padding 32 4 | Set FontSize 18 5 | Set Height 300 6 | 7 | Type "timer 5s -n Demo" 8 | Enter 9 | Sleep 10s 10 | -------------------------------------------------------------------------------- /scripts/completions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | rm -rf completions 4 | mkdir completions 5 | go build -o timer . 6 | for sh in bash zsh fish; do 7 | ./timer completion "$sh" >"completions/timer.$sh" 8 | done 9 | -------------------------------------------------------------------------------- /.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 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | time: "08:00" 15 | labels: 16 | - "dependencies" 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 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 | -------------------------------------------------------------------------------- /goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json 2 | version: 2 3 | variables: 4 | homepage: https://github.com/caarlos0/timer 5 | repository: https://github.com/caarlos0/timer 6 | description: Timer is like sleep, but reports progress. 7 | 8 | includes: 9 | - from_url: 10 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/build.yml 11 | - from_url: 12 | url: https://raw.githubusercontent.com/caarlos0/goreleaserfiles/main/windows.yml 13 | - from_url: 14 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/package_with_completions_and_manpages.yml 15 | - from_url: 16 | url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/release.yml 17 | - from_url: 18 | url: https://raw.githubusercontent.com/caarlos0/goreleaserfiles/main/cosign_checksum.yml 19 | 20 | furies: 21 | - account: caarlos0 22 | 23 | snapcrafts: 24 | - publish: false 25 | summary: "{{.Var.description}}" 26 | description: "{{.Var.description}}" 27 | grade: stable 28 | license: MIT 29 | confinement: strict 30 | apps: 31 | org-stats: 32 | command: timer 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/caarlos0/timer 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 9 | github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 10 | github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 11 | github.com/muesli/mango-cobra v1.2.0 12 | github.com/muesli/roff v0.1.0 13 | github.com/spf13/cobra v1.9.1 14 | ) 15 | 16 | require ( 17 | github.com/charmbracelet/colorprofile v0.3.1 // indirect 18 | github.com/charmbracelet/harmonica v0.2.0 // indirect 19 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 20 | github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect 21 | github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect 22 | github.com/charmbracelet/x/term v0.2.1 // indirect 23 | github.com/charmbracelet/x/windows v0.2.1 // indirect 24 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 25 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 26 | github.com/mattn/go-runewidth v0.0.16 // indirect 27 | github.com/muesli/cancelreader v0.2.2 // indirect 28 | github.com/muesli/mango v0.2.0 // indirect 29 | github.com/muesli/mango-pflag v0.1.0 // indirect 30 | github.com/rivo/uniseg v0.4.7 // indirect 31 | github.com/spf13/pflag v1.0.6 // indirect 32 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 33 | golang.org/x/sync v0.13.0 // indirect 34 | golang.org/x/sys v0.32.0 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /.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 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - uses: actions/setup-go@v5 24 | with: 25 | go-version: stable 26 | - uses: cachix/install-nix-action@v31 27 | with: 28 | github_access_token: ${{ secrets.GH_PAT }} 29 | - name: test 30 | run: | 31 | go mod tidy 32 | go test -v ./... 33 | go build -o timer . 34 | - uses: sigstore/cosign-installer@v3.8.2 35 | - uses: samuelmeuli/action-snapcraft@v3 36 | - uses: goreleaser/goreleaser-action@v6 37 | if: success() && startsWith(github.ref, 'refs/tags/') 38 | with: 39 | distribution: goreleaser-pro 40 | version: nightly 41 | args: release --clean 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 44 | FURY_TOKEN: ${{ secrets.FURY_TOKEN }} 45 | GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 46 | AUR_KEY: ${{ secrets.AUR_KEY }} 47 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} 48 | dependabot: 49 | needs: [build] 50 | runs-on: ubuntu-latest 51 | permissions: 52 | pull-requests: write 53 | contents: write 54 | if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}} 55 | steps: 56 | - id: metadata 57 | uses: dependabot/fetch-metadata@v2 58 | with: 59 | github-token: "${{ secrets.GITHUB_TOKEN }}" 60 | - run: | 61 | gh pr review --approve "$PR_URL" 62 | gh pr merge --squash --auto "$PR_URL" 63 | env: 64 | PR_URL: ${{github.event.pull_request.html_url}} 65 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Made with VHS 3 | 4 | 5 | 6 |
7 |

timer

8 |

A sleep with progress.

9 |

10 | 11 | --- 12 | 13 | Timer is a small CLI, similar to the `sleep` everyone already knows and love, 14 | with a couple of extra features: 15 | 16 | - a progress bar indicating the progression of said timer 17 | - a timer showing how much time is left 18 | - named timers 19 | 20 | ## Usage 21 | 22 | ```sh 23 | timer 24 | timer -n 25 | man timer 26 | timer --help 27 | ``` 28 | 29 | It is possible to pass a time unit for ``. 30 | 31 | Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". 32 | If no unit is passed, it defaults to seconds ("s"). 33 | 34 | If you want to show the start time in 24-hour format, use `--format 24h`. For 35 | example: 36 | ```sh 37 | timer 5s --format 24h -n Demo 38 | ``` 39 | Currently, the two formats supported by the `--format` option are: 40 | - `kitchen`: the default, example: `9:16PM`. 41 | - `24h`: 24-hour time format, example: `21:16`. 42 | 43 | ## Install 44 | 45 | **homebrew**: 46 | 47 | ```sh 48 | brew install caarlos0/tap/timer 49 | ``` 50 | 51 | **macports**: 52 | 53 | ```sh 54 | sudo port install timer 55 | ``` 56 | 57 | **snap**: 58 | 59 | ```sh 60 | snap install timer 61 | ``` 62 | 63 | **apt**: 64 | 65 | ```sh 66 | echo 'deb [trusted=yes] https://repo.caarlos0.dev/apt/ /' | sudo tee /etc/apt/sources.list.d/caarlos0.list 67 | sudo apt update 68 | sudo apt install timer 69 | ``` 70 | 71 | **yum**: 72 | 73 | ```sh 74 | echo '[caarlos0] 75 | name=caarlos0 76 | baseurl=https://repo.caarlos0.dev/yum/ 77 | enabled=1 78 | gpgcheck=0' | sudo tee /etc/yum.repos.d/caarlos0.repo 79 | sudo yum install timer 80 | ``` 81 | 82 | **arch linux**: 83 | 84 | ```sh 85 | yay -S timer-bin 86 | ``` 87 | 88 | **deb/rpm/apk**: 89 | 90 | Download the `.apk`, `.deb` or `.rpm` from the [releases page][releases] and install with the appropriate commands. 91 | 92 | **manually**: 93 | 94 | Download the pre-compiled binaries from the [releases page][releases] or clone the repo build from source. 95 | 96 | [releases]: https://github.com/caarlos0/timer/releases 97 | 98 | # Badges 99 | 100 | [![Release](https://img.shields.io/github/release/caarlos0/timer.svg?style=for-the-badge)](https://github.com/caarlos0/timer/releases/latest) 101 | 102 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](LICENSE.md) 103 | 104 | [![Build](https://img.shields.io/github/actions/workflow/status/caarlos0/timer/build.yml?style=for-the-badge)](https://github.com/caarlos0/timer/actions?query=workflow%3Abuild) 105 | 106 | [![Go Report Card](https://goreportcard.com/badge/github.com/caarlos0/timer?style=for-the-badge)](https://goreportcard.com/report/github.com/caarlos0/timer) 107 | 108 | [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?style=for-the-badge)](https://github.com/goreleaser) 109 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 2 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 3 | github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE= 4 | github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= 5 | github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40= 6 | github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3/go.mod h1:ZFDg5oPjyRYrPAa3iFrtP1DO8xy+LUQxd9JFHEcuwJY= 7 | github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= 8 | github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= 9 | github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= 10 | github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 11 | github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 h1:kU4FK3BrF9yfW4P1tT1+Ag9KVckKhkUuLTwj//7SzaA= 12 | github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc= 13 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 14 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 15 | github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg= 16 | github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81/go.mod h1:poPFOXFTsJsnLbkV3H2KxAAXT7pdjxxLujLocWjkyzM= 17 | github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw= 18 | github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 19 | github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 h1:fsWj8NF5njyMVzELc7++HsvRDvgz3VcgGAUgWBDWWWM= 20 | github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197/go.mod h1:xseGeVftoP9rVI+/8WKYrJFH6ior6iERGvklwwHz5+s= 21 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 22 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 23 | github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I= 24 | github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M= 25 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 26 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 27 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 28 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 29 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 30 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 31 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 32 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 33 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 34 | github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= 35 | github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= 36 | github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= 37 | github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= 38 | github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= 39 | github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= 40 | github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= 41 | github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= 42 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 43 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 44 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 45 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 46 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 47 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 48 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 49 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 50 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 51 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 52 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 53 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 54 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 55 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 56 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 57 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 59 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/charmbracelet/bubbles/v2/key" 11 | "github.com/charmbracelet/bubbles/v2/progress" 12 | "github.com/charmbracelet/bubbles/v2/timer" 13 | tea "github.com/charmbracelet/bubbletea/v2" 14 | "github.com/charmbracelet/lipgloss/v2" 15 | mcobra "github.com/muesli/mango-cobra" 16 | "github.com/muesli/roff" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | type model struct { 21 | name string 22 | altscreen bool 23 | startTimeFormat string 24 | duration time.Duration 25 | passed time.Duration 26 | start time.Time 27 | timer timer.Model 28 | progress progress.Model 29 | quitting bool 30 | interrupting bool 31 | } 32 | 33 | func (m model) Init() tea.Cmd { 34 | return m.timer.Init() 35 | } 36 | 37 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 38 | switch msg := msg.(type) { 39 | case timer.TickMsg: 40 | var cmds []tea.Cmd 41 | var cmd tea.Cmd 42 | 43 | m.passed += m.timer.Interval 44 | pct := m.passed.Milliseconds() * 100 / m.duration.Milliseconds() 45 | cmds = append(cmds, m.progress.SetPercent(float64(pct)/100)) 46 | 47 | m.timer, cmd = m.timer.Update(msg) 48 | cmds = append(cmds, cmd) 49 | return m, tea.Batch(cmds...) 50 | 51 | case tea.WindowSizeMsg: 52 | m.progress.SetWidth(msg.Width - padding*2 - 4) 53 | winHeight = msg.Height 54 | if !m.altscreen && m.progress.Width() > maxWidth { 55 | m.progress.SetWidth(maxWidth) 56 | } 57 | return m, nil 58 | 59 | case timer.StartStopMsg: 60 | var cmd tea.Cmd 61 | m.timer, cmd = m.timer.Update(msg) 62 | return m, cmd 63 | 64 | case timer.TimeoutMsg: 65 | m.quitting = true 66 | return m, tea.Quit 67 | 68 | case progress.FrameMsg: 69 | var cmd tea.Cmd 70 | m.progress, cmd = m.progress.Update(msg) 71 | return m, cmd 72 | 73 | case tea.KeyMsg: 74 | if key.Matches(msg, quitKeys) { 75 | m.quitting = true 76 | return m, tea.Quit 77 | } 78 | if key.Matches(msg, intKeys) { 79 | m.interrupting = true 80 | return m, tea.Quit 81 | } 82 | } 83 | 84 | return m, nil 85 | } 86 | 87 | func (m model) View() string { 88 | if m.quitting || m.interrupting { 89 | return "" 90 | } 91 | 92 | var startTimeFormat string 93 | switch strings.ToLower(m.startTimeFormat) { 94 | case "24h": 95 | startTimeFormat = "15:04" // See: https://golang.cafe/blog/golang-time-format-example.html 96 | default: 97 | startTimeFormat = time.Kitchen 98 | } 99 | result := boldStyle.Render(m.start.Format(startTimeFormat)) 100 | if m.name != "" { 101 | result += ": " + italicStyle.Render(m.name) 102 | } 103 | result += " - " + boldStyle.Render(m.timer.View()) + "\n" + m.progress.View() 104 | if m.altscreen { 105 | return altscreenStyle. 106 | MarginTop((winHeight - 2) / 2). 107 | Render(result) 108 | } 109 | return result 110 | } 111 | 112 | var ( 113 | name string 114 | altscreen bool 115 | startTimeFormat string 116 | winHeight int 117 | version = "dev" 118 | quitKeys = key.NewBinding(key.WithKeys("esc", "q")) 119 | intKeys = key.NewBinding(key.WithKeys("ctrl+c")) 120 | altscreenStyle = lipgloss.NewStyle().MarginLeft(padding) 121 | boldStyle = lipgloss.NewStyle().Bold(true) 122 | italicStyle = lipgloss.NewStyle().Italic(true) 123 | ) 124 | 125 | const ( 126 | padding = 2 127 | maxWidth = 80 128 | ) 129 | 130 | var rootCmd = &cobra.Command{ 131 | Use: "timer", 132 | Short: "timer is like sleep, but with progress report", 133 | Version: version, 134 | SilenceUsage: true, 135 | Args: cobra.ExactArgs(1), 136 | RunE: func(cmd *cobra.Command, args []string) error { 137 | addSuffixIfArgIsNumber(&(args[0]), "s") 138 | duration, err := time.ParseDuration(args[0]) 139 | if err != nil { 140 | return err 141 | } 142 | var opts []tea.ProgramOption 143 | if altscreen { 144 | opts = append(opts, tea.WithAltScreen()) 145 | } 146 | interval := time.Second 147 | if duration < time.Minute { 148 | interval = 100 * time.Millisecond 149 | } 150 | m, err := tea.NewProgram(model{ 151 | duration: duration, 152 | timer: timer.New(duration, timer.WithInterval(interval)), 153 | progress: progress.New(progress.WithDefaultGradient()), 154 | name: name, 155 | altscreen: altscreen, 156 | startTimeFormat: startTimeFormat, 157 | start: time.Now(), 158 | }, opts...).Run() 159 | if err != nil { 160 | return err 161 | } 162 | if m.(model).interrupting { 163 | return fmt.Errorf("interrupted") 164 | } 165 | if name != "" { 166 | cmd.Printf("%s ", name) 167 | } 168 | cmd.Printf("finished!\n") 169 | return nil 170 | }, 171 | } 172 | 173 | var manCmd = &cobra.Command{ 174 | Use: "man", 175 | Short: "Generates man pages", 176 | SilenceUsage: true, 177 | DisableFlagsInUseLine: true, 178 | Hidden: true, 179 | Args: cobra.NoArgs, 180 | RunE: func(_ *cobra.Command, _ []string) error { 181 | manPage, err := mcobra.NewManPage(1, rootCmd) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | _, err = fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument())) 187 | return err 188 | }, 189 | } 190 | 191 | func init() { 192 | rootCmd.Flags().StringVarP(&name, "name", "n", "", "timer name") 193 | rootCmd.Flags().BoolVarP(&altscreen, "fullscreen", "f", false, "fullscreen") 194 | rootCmd.Flags().StringVarP(&startTimeFormat, "format", "", "", "Specify start time format, possible values: 24h, kitchen") 195 | 196 | rootCmd.AddCommand(manCmd) 197 | } 198 | 199 | func main() { 200 | if err := rootCmd.Execute(); err != nil { 201 | os.Exit(1) 202 | } 203 | } 204 | 205 | func addSuffixIfArgIsNumber(s *string, suffix string) { 206 | _, err := strconv.ParseFloat(*s, 64) 207 | if err == nil { 208 | *s = *s + suffix 209 | } 210 | } 211 | --------------------------------------------------------------------------------