├── .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 |
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 | [](https://github.com/caarlos0/timer/releases/latest)
101 |
102 | [](LICENSE.md)
103 |
104 | [](https://github.com/caarlos0/timer/actions?query=workflow%3Abuild)
105 |
106 | [](https://goreportcard.com/report/github.com/caarlos0/timer)
107 |
108 | [](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 |
--------------------------------------------------------------------------------