17 Commits

Author SHA1 Message Date
f6435fcd88 update the pre-built binary names to match the README. 2026-08-01 04:21:05 +01:00
38975cd9d6 update not regarding --count and --count-flag update
add ListCmd to README.
2026-08-01 04:20:51 +01:00
18f56b4ecb mark --count and --count-prompt as ME.
use AllKinds to build the selection prompt.

remove --kind flag validation from PreRunE (its redundant).
2026-08-01 04:20:27 +01:00
dd49700eb1 add list subcommand
add renderKindList()
2026-08-01 04:19:18 +01:00
af06fab1f1 add AllKinds to the public API. Use it as a single source of truth. 2026-08-01 04:18:48 +01:00
github-actions[bot]
c55ab96a4f chore: auto-update Go modules 2026-07-27 00:53:05 +00:00
2259676027 add drawUnique test 2026-07-24 16:52:49 +01:00
12ca7d8fc2 pluralise draw only if entries>1 2026-07-24 16:21:18 +01:00
1310447664 add --kind flag, it allows you to configure the lottery kind directly, skipping the selection prompt
update the README with more examples.
2026-07-24 13:11:56 +01:00
3f0af6e5ec render multiple draws into a single card 2026-07-24 12:48:06 +01:00
5a20a3cf2b md fix 2026-07-24 01:40:36 +01:00
b364276d5a add --count and --count-prompt flags
add support for env vars.

add count prompt logic to the root command

update the README.
2026-07-24 01:39:12 +01:00
2d1a024d65 upd selectionprompt img 2026-07-24 00:02:33 +01:00
ad882d0fc0 lint fixes
make "Set For Life" easier to read.
2026-07-23 23:57:16 +01:00
fb13566543 upd README. 2026-07-23 23:49:48 +01:00
cdee9113de add README + img/ 2026-07-23 23:41:19 +01:00
06f47b1f70 add taskfile 2026-07-23 23:40:58 +01:00
14 changed files with 642 additions and 109 deletions

View File

@@ -17,6 +17,7 @@ before:
builds: builds:
- main: ./cmd/lottery/ - main: ./cmd/lottery/
binary: lottery
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: goos:

83
README.md Normal file
View File

@@ -0,0 +1,83 @@
![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white)
![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black)
![macOS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)
# lottery
Play National Lottery games from your terminal.
![Selection Prompt](./img/selectionprompt.png)
![Draw](./img/draw.png)
## Install
```console
go install github.com/onyx-and-iris/lottery-cli/cmd/lottery@latest
```
## Configuration
*flags*
- --kind/-k: The kind of lottery.
- --count/-c: Number of draws to generate.
- --count-prompt/-C: Prompt for the number of draws to generate.
> Note. --count and --count-prompt flags are mutually exclusive.
*environment variables*
```bash
#!/usr/bin/env bash
export LOTTERY_KIND=lotto
export LOTTERY_COUNT=3
export LOTTERY_COUNT_PROMPT=false
```
## Use
### RootCmd
Run with the selection prompt without prompting for a count:
```console
lottery
```
Run with the selection prompt but also prompt for a count:
```console
lottery --count-prompt
```
Run with the selection prompt but pass in the count directly:
```console
lottery --count=4
```
Run a single draw directly:
```console
lottery --kind=euromillions
```
Run multiple draws directly:
```console
lottery --kind=euromillions --count=3
```
### ListCmd
List the available lotteries:
```console
lottery list
```
## Special Thanks
- [spf13](https://github.com/spf13) for the [cobra](https://github.com/spf13/cobra) and [viper](https://github.com/spf13/viper) packages.
- [Charm](https://github.com/charmbracelet) developers for the [fang](https://github.com/charmbracelet/fang), [lipgloss](https://github.com/charmbracelet/lipgloss) and [huh](https://github.com/charmbracelet/huh) packages.

65
Taskfile.yaml Normal file
View File

@@ -0,0 +1,65 @@
version: '3'
vars:
PROGRAM: lottery
SHELL: '{{if eq .OS "Windows_NT"}}powershell{{end}}'
BIN_DIR: bin
VERSION:
sh: 'git describe --tags $(git rev-list --tags --max-count=1)'
WINDOWS: '{{.BIN_DIR}}/{{.PROGRAM}}_windows_amd64.exe'
LINUX: '{{.BIN_DIR}}/{{.PROGRAM}}_linux_amd64'
MACOS: '{{.BIN_DIR}}/{{.PROGRAM}}_darwin_amd64'
tasks:
default:
desc: Build the lottery project
cmds:
- task: build
build:
desc: Build the lottery project
deps: [vet]
cmds:
- task: build-windows
- task: build-linux
- task: build-macos
vet:
desc: Vet the code
deps: [fmt]
cmds:
- go vet ./...
fmt:
desc: Fmt the code
cmds:
- go fmt ./...
build-windows:
desc: Build the lottery project for Windows
cmds:
- GOOS=windows GOARCH=amd64 go build -o {{.WINDOWS}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
internal: true
build-linux:
desc: Build the lottery project for Linux
cmds:
- GOOS=linux GOARCH=amd64 go build -o {{.LINUX}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
internal: true
build-macos:
desc: Build the lottery project for macOS
cmds:
- GOOS=darwin GOARCH=amd64 go build -o {{.MACOS}} -ldflags="-X main.version={{.VERSION}}" ./cmd/{{.PROGRAM}}/
internal: true
test:
desc: Run tests
cmds:
- go test ./...
clean:
desc: Clean the build artifacts
cmds:
- '{{.SHELL}} rm -r {{.BIN_DIR}}'

24
cmd/lottery/list.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
"github.com/onyx-and-iris/lottery-cli"
)
// listCmd represents the list command.
var listCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List all available lottery kinds.",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Println(renderKindList(lottery.AllKinds()))
return nil
},
}
func init() {
rootCmd.AddCommand(listCmd)
}

View File

@@ -9,8 +9,10 @@ import (
"charm.land/huh/v2" "charm.land/huh/v2"
"github.com/charmbracelet/fang" "github.com/charmbracelet/fang"
"github.com/onyx-and-iris/lottery-cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/onyx-and-iris/lottery-cli"
) )
var version string var version string
@@ -27,32 +29,32 @@ func versionFromBuild() string {
return strings.Split(info.Main.Version, "-")[0] return strings.Split(info.Main.Version, "-")[0]
} }
var cmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "lottery", Use: "lottery",
Short: "A CLI for National Lottery games.", Short: "A CLI for National Lottery games.",
RunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
var selected string cmd.MarkFlagsMutuallyExclusive("count", "count-prompt")
form := huh.NewForm( return nil
huh.NewGroup( },
huh.NewSelect[string](). RunE: func(cmd *cobra.Command, args []string) error {
Title("Pick a lottery."). kindStr := viper.GetString("kind")
Options( if kindStr == "" {
huh.NewOption("Lotto", "lotto"), form := huh.NewForm(
huh.NewOption("EuroMillions", "euromillions"), huh.NewGroup(
huh.NewOption("SetForLife", "setforlife"), huh.NewSelect[string]().
huh.NewOption("Thunderball", "thunderball"), Title("Pick a lottery.").
huh.NewOption("Powerball", "powerball"), Options(kindPromptOptions()...).
). Value(&kindStr),
Value(&selected), ),
), )
) err := form.Run()
err := form.Run() if err != nil {
if err != nil { return err
return err }
} }
kind, err := lottery.ParseKind(selected) kind, err := lottery.ParseKind(kindStr)
if err != nil { if err != nil {
return err return err
} }
@@ -61,15 +63,83 @@ var cmd = &cobra.Command{
if err != nil { if err != nil {
return err return err
} }
l.Draw()
fmt.Println(renderDraw(l)) if countPrompt := viper.GetBool("count-prompt"); countPrompt {
var count string
if err := huh.NewInput().
Title("How many draws would you like to generate?").
Value(&count).Run(); err != nil {
return err
}
viper.Set("count", count)
}
count := viper.GetInt("count")
includeDrawHeading := count > 1
renders := make([]string, 0, count)
drawTitle := "Lottery"
for i := range count {
l.Draw()
title, entry := renderDrawEntry(l, i+1, includeDrawHeading)
drawTitle = title
renders = append(renders, entry)
}
if len(renders) > 0 {
fmt.Println(renderDrawCollection(drawTitle, renders))
}
return nil return nil
}, },
} }
func kindPromptLabel(kind lottery.Kind) string {
switch kind {
case lottery.KindLotto:
return "Lotto"
case lottery.KindEuroMillions:
return "EuroMillions"
case lottery.KindSetForLife:
return "Set For Life"
case lottery.KindThunderball:
return "Thunderball"
case lottery.KindPowerball:
return "Powerball"
default:
return string(kind)
}
}
func kindPromptOptions() []huh.Option[string] {
kinds := lottery.AllKinds()
options := make([]huh.Option[string], 0, len(kinds))
for _, kind := range kinds {
options = append(options, huh.NewOption(kindPromptLabel(kind), string(kind)))
}
return options
}
func init() {
rootCmd.Flags().StringP("kind", "k", "", "Lottery kind to generate draws for.")
rootCmd.Flags().IntP("count", "c", 1, "Number of draws to generate.")
rootCmd.Flags().BoolP("count-prompt", "C", false, "Prompt for the number of draws to generate.")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.SetEnvPrefix("LOTTERY")
viper.AutomaticEnv()
if err := viper.BindPFlags(rootCmd.Flags()); err != nil {
panic(err)
}
}
func main() { func main() {
if err := fang.Execute(context.Background(), cmd, fang.WithVersion(versionFromBuild())); err != nil { if err := fang.Execute(
context.Background(),
rootCmd,
fang.WithVersion(versionFromBuild()),
); err != nil {
os.Exit(1) os.Exit(1)
} }
} }

View File

@@ -5,9 +5,28 @@ import (
"strings" "strings"
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"github.com/onyx-and-iris/lottery-cli" "github.com/onyx-and-iris/lottery-cli"
) )
// nolint:misspell
var (
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
drawHeadingStyle = lipgloss.NewStyle().
Bold(true).
Underline(true).
Foreground(lipgloss.Color("14"))
labelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
numbersStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10"))
specialStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11"))
separatorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
cardStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("8")).
Padding(0, 1).
MarginTop(1)
)
func formatNumberList(numbers []int) string { func formatNumberList(numbers []int) string {
parts := make([]string, len(numbers)) parts := make([]string, len(numbers))
for i, n := range numbers { for i, n := range numbers {
@@ -16,46 +35,140 @@ func formatNumberList(numbers []int) string {
return strings.Join(parts, " ") return strings.Join(parts, " ")
} }
func renderDraw(l lottery.Lottery) string { func drawTitleAndLines(l lottery.Lottery) (string, []string) {
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12"))
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
numbersStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("10"))
specialStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("11"))
var title string var title string
var lines []string var lines []string
switch game := l.(type) { switch game := l.(type) {
case *lottery.Lotto: case *lottery.Lotto:
title = "Lotto" title = "Lotto"
lines = append(lines, labelStyle.Render("Numbers:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) lines = append(
lines,
labelStyle.Render(
"Numbers:",
)+" "+numbersStyle.Render(
formatNumberList(game.Numbers[:]),
),
)
case *lottery.EuroMillions: case *lottery.EuroMillions:
title = "EuroMillions" title = "EuroMillions"
lines = append(lines, labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) lines = append(
lines = append(lines, labelStyle.Render("Lucky Stars:")+" "+specialStyle.Render(formatNumberList(game.LuckyStars[:]))) lines,
labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:])),
)
lines = append(
lines,
labelStyle.Render(
"Lucky Stars:",
)+" "+specialStyle.Render(
formatNumberList(game.LuckyStars[:]),
),
)
case *lottery.SetForLife: case *lottery.SetForLife:
title = "Set For Life" title = "Set For Life"
lines = append(lines, labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) lines = append(
lines = append(lines, labelStyle.Render("Life Ball:")+" "+specialStyle.Render(strconv.Itoa(game.LifeBall))) lines,
labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:])),
)
lines = append(
lines,
labelStyle.Render("Life Ball:")+" "+specialStyle.Render(strconv.Itoa(game.LifeBall)),
)
case *lottery.Thunderball: case *lottery.Thunderball:
title = "Thunderball" title = "Thunderball"
lines = append(lines, labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) lines = append(
lines = append(lines, labelStyle.Render("Thunderball:")+" "+specialStyle.Render(strconv.Itoa(game.Thunderball))) lines,
labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:])),
)
lines = append(
lines,
labelStyle.Render(
"Thunderball:",
)+" "+specialStyle.Render(
strconv.Itoa(game.Thunderball),
),
)
case *lottery.Powerball: case *lottery.Powerball:
title = "Powerball" title = "Powerball"
lines = append(lines, labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:]))) lines = append(
lines = append(lines, labelStyle.Render("Powerball:")+" "+specialStyle.Render(strconv.Itoa(game.Powerball))) lines,
labelStyle.Render("Main:")+" "+numbersStyle.Render(formatNumberList(game.Numbers[:])),
)
lines = append(
lines,
labelStyle.Render("Powerball:")+" "+specialStyle.Render(strconv.Itoa(game.Powerball)),
)
default: default:
title = "Lottery" title = "Lottery"
lines = append(lines, "Unknown lottery type") lines = append(lines, "Unknown lottery type")
} }
body := strings.Join(lines, "\n") return title, lines
card := lipgloss.NewStyle(). }
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("8")). func renderDrawEntry(l lottery.Lottery, drawNumber int, includeHeading bool) (string, string) {
Padding(0, 1). title, lines := drawTitleAndLines(l)
MarginTop(1)
entry := make([]string, 0, len(lines)+1)
return card.Render(titleStyle.Render(title+" draw") + "\n" + body) if includeHeading {
entry = append(entry, drawHeadingStyle.Render("Draw "+strconv.Itoa(drawNumber)))
}
entry = append(entry, lines...)
return title, strings.Join(entry, "\n")
}
func maxLineWidth(blocks []string) int {
maxWidth := 0
for _, block := range blocks {
for line := range strings.SplitSeq(block, "\n") {
width := lipgloss.Width(line)
if width > maxWidth {
maxWidth = width
}
}
}
if maxWidth < 12 {
return 12
}
return maxWidth
}
// renderDrawCollection renders a collection of draw entries into a single string with a title and separator lines.
func renderDrawCollection(title string, entries []string) string {
if len(entries) == 0 {
return ""
}
separator := separatorStyle.Render(strings.Repeat("─", maxLineWidth(entries)))
body := strings.Join(entries, "\n"+separator+"\n")
if len(entries) == 1 {
title = titleStyle.Render(title + " draw")
} else {
title = titleStyle.Render(title + " draws")
}
return cardStyle.Render(title + "\n" + body)
}
// renderKindList renders a list of lottery kinds into a formatted string.
func renderKindList(kinds []lottery.Kind) string {
lines := make([]string, 0, len(kinds)+1)
lines = append(
lines,
labelStyle.Render("Total:")+" "+numbersStyle.Render(strconv.Itoa(len(kinds))),
)
for _, kind := range kinds {
lines = append(
lines,
specialStyle.Render("•")+
" "+numbersStyle.Render(kindPromptLabel(kind))+
" "+labelStyle.Render("("+string(kind)+")"),
)
}
return cardStyle.Render(
titleStyle.Render("Available lottery kinds") + "\n" + strings.Join(lines, "\n"),
)
} }

37
go.mod
View File

@@ -7,38 +7,47 @@ require (
charm.land/lipgloss/v2 v2.0.5 charm.land/lipgloss/v2 v2.0.5
github.com/charmbracelet/fang v1.0.0 github.com/charmbracelet/fang v1.0.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
) )
require ( require (
charm.land/bubbles/v2 v2.0.0 // indirect charm.land/bubbles/v2 v2.1.1 // indirect
charm.land/bubbletea/v2 v2.0.2 // indirect charm.land/bubbletea/v2 v2.0.8 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/catppuccin/go v0.2.0 // indirect github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect github.com/charmbracelet/x/exp/charmtone v0.0.0-20260726004341-482a56510f1b // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/exp/strings v0.1.0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-runewidth v0.0.27 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/mango v0.1.0 // indirect github.com/muesli/mango v0.2.0 // indirect
github.com/muesli/mango-cobra v1.2.0 // indirect github.com/muesli/mango-cobra v1.3.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/mango-pflag v0.2.0 // indirect
github.com/muesli/roff v0.1.0 // indirect github.com/muesli/roff v0.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.19.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
) )

87
go.sum
View File

@@ -1,7 +1,7 @@
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.1.1 h1:7r55WzBxpo/R3z98hGmY7KKPd3ET6vsf0Fb9sDHOV60=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbles/v2 v2.1.1/go.mod h1:GE6M31gaWZVXzGw73OeuTTgy4lX+OtkH0E5ymnNsHxo=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= charm.land/bubbletea/v2 v2.0.8 h1:SxTJMhCAI3lbPmy4SgX5LWZ24AdINr4I6UEqzZvYJuY=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= charm.land/bubbletea/v2 v2.0.8/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY=
@@ -12,28 +12,28 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ= github.com/charmbracelet/fang v1.0.0 h1:jESBY40agJOlLYnnv9jE0mLqDGTxEk0hkOnx7YGyRlQ=
github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo= github.com/charmbracelet/fang v1.0.0/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac h1:BP8qMDGjmOejoVTklEXXTHI0OwMIt8JHPSPHxscFFwA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= github.com/charmbracelet/ultraviolet v0.0.0-20260720091822-7cc6674724ac/go.mod h1:psnCZIfwwxVs6v6DhUc6NJ8AQ3ejvs2ejKwoOMeVmUk=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= github.com/charmbracelet/x/exp/charmtone v0.0.0-20260726004341-482a56510f1b h1:KXyKTg/2+ktNqnc/UZ6hpwfhjWK3EIPjenZNhmCK4WY=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= github.com/charmbracelet/x/exp/charmtone v0.0.0-20260726004341-482a56510f1b/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8= github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= 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/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 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
@@ -53,46 +53,75 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 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/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec=
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E=
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= github.com/muesli/mango-pflag v0.2.0 h1:QViokgKDZQCzKhYe1zH8D+UlPJzBSGoP9yx0hBG0t5k=
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y=
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 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= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 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/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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

BIN
img/draw.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
img/selectionprompt.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -1,6 +1,10 @@
package lottery package lottery
import "fmt" import (
"fmt"
"slices"
"strings"
)
type Kind string type Kind string
@@ -12,19 +16,43 @@ const (
KindPowerball Kind = "powerball" KindPowerball Kind = "powerball"
) )
func ParseKind(kind string) (Kind, error) { var allKinds = []Kind{
switch kind { KindLotto,
case "lotto": KindEuroMillions,
return KindLotto, nil KindSetForLife,
case "euromillions": KindThunderball,
return KindEuroMillions, nil KindPowerball,
case "setforlife": }
return KindSetForLife, nil
case "thunderball": var allKindsText = []string{
return KindThunderball, nil string(KindLotto),
case "powerball": string(KindEuroMillions),
return KindPowerball, nil string(KindSetForLife),
default: string(KindThunderball),
return "", fmt.Errorf("invalid lottery kind: %s", kind) string(KindPowerball),
} }
var allKindsCSV = strings.Join(allKindsText, ", ")
// AllKinds returns the complete, ordered list of supported lottery kinds.
//
// The returned slice is a copy and can be modified safely by callers.
func AllKinds() []Kind {
kinds := make([]Kind, len(allKinds))
copy(kinds, allKinds)
return kinds
}
// ParseKind parses a string into a Kind, returning an error if the string is not a valid kind.
func ParseKind(kind string) (Kind, error) {
parsed := Kind(kind)
if slices.Contains(allKinds, parsed) {
return parsed, nil
}
return "", fmt.Errorf(
"invalid lottery kind: %s, must be one of: %s",
kind,
allKindsCSV,
)
} }

71
kinds_test.go Normal file
View File

@@ -0,0 +1,71 @@
package lottery
import (
"strings"
"testing"
)
func TestAllKindsReturnsExpectedOrder(t *testing.T) {
t.Parallel()
got := AllKinds()
want := []Kind{
KindLotto,
KindEuroMillions,
KindSetForLife,
KindThunderball,
KindPowerball,
}
if len(got) != len(want) {
t.Fatalf("expected %d kinds, got %d", len(want), len(got))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("kind at index %d: expected %q, got %q", i, want[i], got[i])
}
}
}
func TestAllKindsReturnsCopy(t *testing.T) {
t.Parallel()
first := AllKinds()
first[0] = "mutated"
again := AllKinds()
if again[0] != KindLotto {
t.Fatalf("expected AllKinds to return a defensive copy")
}
}
func TestParseKindAcceptsAllKinds(t *testing.T) {
t.Parallel()
for _, kind := range AllKinds() {
parsed, err := ParseKind(string(kind))
if err != nil {
t.Fatalf("expected no error for %q, got %v", kind, err)
}
if parsed != kind {
t.Fatalf("expected %q, got %q", kind, parsed)
}
}
}
func TestParseKindIncludesAllKindsInError(t *testing.T) {
t.Parallel()
_, err := ParseKind("invalid")
if err == nil {
t.Fatalf("expected an error for invalid kind")
}
message := err.Error()
for _, kind := range AllKinds() {
if !strings.Contains(message, string(kind)) {
t.Fatalf("expected error message to contain kind %q: %s", kind, message)
}
}
}

View File

@@ -8,8 +8,8 @@ import (
// drawUnique returns count unique numbers in the range [1, max]. // drawUnique returns count unique numbers in the range [1, max].
// It uses the rand.Perm function to generate a random permutation of numbers and selects the first count numbers from it. // It uses the rand.Perm function to generate a random permutation of numbers and selects the first count numbers from it.
// The result is sorted before being returned. // The result is sorted before being returned.
func drawUnique(count, max int) []int { func drawUnique(count, maxNum int) []int {
perm := rand.Perm(max) perm := rand.Perm(maxNum)
result := make([]int, count) result := make([]int, count)
for i := range result { for i := range result {
result[i] = perm[i] + 1 result[i] = perm[i] + 1

40
util_test.go Normal file
View File

@@ -0,0 +1,40 @@
package lottery
import "testing"
func TestDrawUnique(t *testing.T) {
for _, tc := range []struct {
name string
count int
maxNum int
}{
{"small", 3, 10},
{"exact", 5, 5},
{"larger", 10, 50},
} {
t.Run(tc.name, func(t *testing.T) {
for range 100 {
nums := drawUnique(tc.count, tc.maxNum)
if len(nums) != tc.count {
t.Fatalf("got len=%d, want %d", len(nums), tc.count)
}
seen := make(map[int]struct{}, len(nums))
prev := 0
for _, n := range nums {
if n < 1 || n > tc.maxNum {
t.Fatalf("value %d out of range [1,%d]", n, tc.maxNum)
}
if n < prev {
t.Fatalf("values not sorted: %v", nums)
}
if _, ok := seen[n]; ok {
t.Fatalf("duplicate value %d in %v", n, nums)
}
prev = n
seen[n] = struct{}{}
}
}
})
}
}