Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The tool supports four main modes: calculate from **now**, from a target **wake*

## Features

- Interactive TUI mode when run without flags
- Calculate wake times from the current time
- Calculate bedtimes for a target wake time
- Calculate wake times for a target sleep time
Expand Down Expand Up @@ -52,7 +53,21 @@ go build -o sleepycli .
go run . --help
```

## Usage
## Interactive mode

Run `sleepycli` without any flags to launch the interactive TUI:

```bash
sleepycli
```

- Navigate with `↑` / `↓`
- Press `enter` on a mode to select it and type your time
- Adjust settings (buffer, cycle range) from the settings section
- Press `enter` to confirm — the result is printed to your terminal
- Press `q` to quit without output

## CLI flags

```bash
sleepycli [mode] [options]
Expand Down Expand Up @@ -109,27 +124,27 @@ sleepycli --sleep 23:00 --cycles-min 3 --cycles-max 7
#### `--wake 07:00`

```text
To wake up at 07:00:
───────────────────
To wake up at 07:00
──────────────────────────────
Assuming 15 min to fall asleep

- 6 cycles, go to sleep at 21:45 (9h 00m)
- 5 cycles, go to sleep at 23:15 (7h 30m)
- 4 cycles, go to sleep at 00:45 (6h 00m)
6 cyclessleep at 21:45 (9h 00m)
5 cyclessleep at 23:15 (7h 30m)
4 cyclessleep at 00:45 (6h 00m)
```

#### `--from 22:00 --to 07:00`

```text
Between 22:00 and 07:00:
───────────────────────
Between 22:00 and 07:00
──────────────────────────────
Assuming 15 min to fall asleep

- 5 complete cycles (7h 30m)
- 75 minutes remaining
5 complete cycles (7h 30m)
75 minutes remaining
```

## Flags
## Flag reference

| Flag | Description | Default |
|---|---|---:|
Expand Down
30 changes: 29 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,32 @@ module github.com/TyostoKarry/sleepycli

go 1.26.1

require github.com/spf13/pflag v1.0.10
require (
charm.land/bubbletea/v2 v2.0.2
github.com/charmbracelet/lipgloss v1.1.0
github.com/spf13/pflag v1.0.10
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
)
51 changes: 51 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,53 @@
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f h1:UytXHv0UxnsDFmL/7Z9Q5SBYPwSuRLXHbwx+6LycZ2w=
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
1 change: 1 addition & 0 deletions internal/help/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func writeHelp(w io.Writer, name string) error {

fmt.Fprintf(&b, "%s — sleep cycle calculator\n\n", name)
b.WriteString("Usage:\n")
fmt.Fprintf(&b, " %s Launch interactive mode\n", name)
fmt.Fprintf(&b, " %s [mode] [options]\n\n", name)

b.WriteString("Choose exactly one mode:\n")
Expand Down
64 changes: 64 additions & 0 deletions internal/render/render.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package render

import (
"fmt"
"strings"
"time"

"github.com/TyostoKarry/sleepycli/internal/cycle"
"github.com/TyostoKarry/sleepycli/internal/styles"
)

func FormatDuration(cycleCount int) string {
duration := time.Duration(cycleCount) * cycle.CycleDuration
hours := int(duration.Hours())
minutes := int(duration.Minutes()) % 60
return fmt.Sprintf("%dh %02dm", hours, minutes)
}

func WakeTimes(base time.Time, buffer time.Duration, minCycles, maxCycles int, header string) string {
wakeTimes := cycle.CalculateWakeTimes(base, buffer, minCycles, maxCycles)
var sb strings.Builder
sb.WriteString(styles.Result.Render(header) + "\n")
sb.WriteString(styles.Separator.Render(strings.Repeat("─", 30)) + "\n")
sb.WriteString(styles.Dim.Render(fmt.Sprintf("Assuming %d min to fall asleep", int(buffer.Minutes()))) + "\n\n")
for i := len(wakeTimes) - 1; i >= 0; i-- {
c := minCycles + i
fmt.Fprintf(&sb, " %s cycles → wake at %s %s\n",
styles.Result.Render(fmt.Sprintf("%d", c)),
styles.Result.Render(wakeTimes[i].Format("15:04")),
styles.Dim.Render("("+FormatDuration(c)+")"))
}
return sb.String()
}

func Bedtimes(base time.Time, buffer time.Duration, minCycles, maxCycles int, header string) string {
bedTimes := cycle.CalculateBedtimes(base, buffer, minCycles, maxCycles)
var sb strings.Builder
sb.WriteString(styles.Result.Render(header) + "\n")
sb.WriteString(styles.Separator.Render(strings.Repeat("─", 30)) + "\n")
sb.WriteString(styles.Dim.Render(fmt.Sprintf("Assuming %d min to fall asleep", int(buffer.Minutes()))) + "\n\n")
for i := len(bedTimes) - 1; i >= 0; i-- {
c := minCycles + i
fmt.Fprintf(&sb, " %s cycles → sleep at %s %s\n",
styles.Result.Render(fmt.Sprintf("%d", c)),
styles.Result.Render(bedTimes[i].Format("15:04")),
styles.Dim.Render("("+FormatDuration(c)+")"))
}
return sb.String()
}

func Window(from, to string, fromTime, toTime time.Time, bufferMinutes int) string {
buffer := time.Duration(bufferMinutes) * time.Minute
cycles, remainder := cycle.CalculateCyclesInWindow(fromTime, toTime, buffer)
var sb strings.Builder
sb.WriteString(styles.Result.Render(fmt.Sprintf("Between %s and %s", from, to)) + "\n")
sb.WriteString(styles.Separator.Render(strings.Repeat("─", 30)) + "\n")
sb.WriteString(styles.Dim.Render(fmt.Sprintf("Assuming %d min to fall asleep", bufferMinutes)) + "\n\n")
fmt.Fprintf(&sb, " %s complete cycles %s\n",
styles.Result.Render(fmt.Sprintf("%d", cycles)),
styles.Dim.Render("("+FormatDuration(cycles)+")"))
fmt.Fprintf(&sb, " %s minutes remaining\n",
styles.Result.Render(fmt.Sprintf("%d", int(remainder.Minutes()))))
return sb.String()
}
10 changes: 10 additions & 0 deletions internal/styles/styles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package styles

import "github.com/charmbracelet/lipgloss"

var (
Result = lipgloss.NewStyle().Foreground(lipgloss.Color("120"))
Dim = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
Separator = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
Error = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
)
Loading