diff --git a/Makefile b/Makefile index 2e82291..203455b 100644 --- a/Makefile +++ b/Makefile @@ -7,12 +7,12 @@ clean: go clean build: - go build -o ./bin/cli ./cmd/cli + go build -o ./bin/pm ./cmd/pm go build -o ./bin/tui ./cmd/tui go build -o ./bin/web ./cmd/web cli: - go run ./cmd/cli + go run ./cmd/pm tui: go run ./cmd/tui diff --git a/README.md b/README.md index b209c25..b9455da 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ LazyPM is a lightweight project management system that provides three interfaces ## Features - Issue tracking (bugs, features, tasks, epics, chores) -- Status management (open, in-progress, blocked, deferred, closed) +- Status management (open, in-progress, closed) - Dependency tracking between issues - Labels and comments - Statistics and reporting @@ -22,12 +22,19 @@ LazyPM is a lightweight project management system that provides three interfaces ## Quick Start +### Prerequisites + +- Go 1.25.6+ +- Make + +### Installation + ```bash # Install dependencies go mod tidy ``` -### Install make +### Make Installation Linux: ```bash @@ -43,41 +50,64 @@ winget install --id chocolatey.chocolatey --source winget choco install make ``` -### Available make commands +## Build Commands ```bash -# Build all interfaces -make build +# Build all binaries +make build # Creates bin/pm, bin/tui, bin/web -# Run CLI -make cli - -# Run TUI -make tui - -# Run Web interface -make web +# Run specific interfaces +make cli # Run CLI interface +make tui # Run TUI interface (interactive) +make web # Run Web server (localhost:8080) # Development with hot reload -make dev +make dev # Watch templ files and auto-reload web +make tw # Watch Tailwind CSS changes +make watch # Run both dev and tw in parallel + +# Maintenance +make tidy # Run go mod tidy +make clean # Clean build artifacts + +# Install +make install-cli # Install CLI with shell completions +make completions # Generate shell completion scripts ``` ## Project Structure ``` -├── cmd/ # Entry points (cli, tui, web) -├── internal/ # Core services and models -├── pkg/ # Public packages (CLI, TUI, Web) -└── .pm/ # Local data storage +├── bin/ # Compiled binaries +├── cmd/ # Entry points +│ ├── pm/ # CLI main.go +│ ├── tui/ # TUI main.go +│ └── web/ # Web server main.go +├── internal/ # Core implementation +│ ├── models/ # Data models (beads types) +│ ├── service/ # Business logic (beads, statistics) +│ └── storage/ # Data persistence +├── pkg/ # Public packages +│ ├── cli/ # CLI commands and REPL +│ ├── tui/ # TUI views and components +│ └── web/ # Web handlers, templates, assets +└── .pm/ # Local data storage (gitignored) ``` -## Requirements - -- Go 1.25.6+ -- SQLite - -## Dependencies +## Technology Stack +- [Go](https://golang.org/) 1.25.6 - Backend language - [Cobra](https://github.com/spf13/cobra) - CLI framework -- [templ](https://github.com/a-h/templ) - HTML templating -- [beads](https://github.com/steveyegge/beads) - Issue tracking engine +- [Bubbletea](https://github.com/charmbracelet/bubbletea) - TUI framework +- [Lipgloss](https://github.com/charmbracelet/lipgloss) - Terminal styling +- [Templ](https://github.com/a-h/templ) - HTML templating +- [Tailwind CSS v4](https://tailwindcss.com/) + DaisyUI - Web styling +- [Beads](https://github.com/steveyegge/beads) - Issue tracking engine + +## Configuration + +LazyPM stores data in a local `.pm` directory: +- `.pm/db.db` - SQLite database for issues +- `.pm/stats.json` - Statistics storage + +The directory is automatically created on first run. \ No newline at end of file diff --git a/cmd/beads_viewer/main.go b/cmd/beads_viewer/main.go deleted file mode 100644 index 35cc2c8..0000000 --- a/cmd/beads_viewer/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "log" - - "github.com/Dicklesworthstone/beads_viewer/pkg/model" - "github.com/Dicklesworthstone/beads_viewer/pkg/ui" - "github.com/LazyBachelor/LazyPM/internal/service" - tea "github.com/charmbracelet/bubbletea" -) - -func main() { - config := service.Config{ - StatisticsStoragePath: "./.pm/stats.json", - BeadsDBPath: "./.pm/db.db", - IssuePrefix: "pm", - } - - svc, close, err := service.NewServices(context.Background(), config) - if err != nil { - log.Fatal(err) - } - defer close() - - beadsIssues, err := svc.Beads.AllIssues(context.Background()) - if err != nil { - panic(err) - } - - modelIssues := make([]model.Issue, 0, len(beadsIssues)) - for _, issue := range beadsIssues { - jsonData, err := json.Marshal(issue) - if err != nil { - panic(err) - } - - var modelIssue model.Issue - if err := json.Unmarshal(jsonData, &modelIssue); err != nil { - panic(err) - } - modelIssues = append(modelIssues, modelIssue) - } - - model := ui.NewModel(modelIssues, nil, config.BeadsDBPath) - - if err := tea.NewProgram(model, tea.WithAltScreen(), - tea.WithMouseAllMotion()); err != nil { - panic(err) - } - -} diff --git a/cmd/survey/survey.go b/cmd/survey/survey.go index 71cd91f..bc58ef9 100644 --- a/cmd/survey/survey.go +++ b/cmd/survey/survey.go @@ -59,7 +59,7 @@ func main() { err = web.Run(ctx, config) case "tui": fmt.Println("Starting TUI Interface...") - err = tui.Run(ctx, config) + _, err = tui.Run(ctx, config) default: fmt.Println("Invalid selection. Exiting.") } diff --git a/cmd/tui/main.go b/cmd/tui/main.go index 45b0974..f53021f 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -13,7 +13,7 @@ func main() { IssuePrefix: "pm", } - if err := tui.Run(context.Background(), config); err != nil { + if _, err := tui.Run(context.Background(), config); err != nil { panic(err) } } diff --git a/go.mod b/go.mod index fad1402..458060d 100644 --- a/go.mod +++ b/go.mod @@ -4,83 +4,76 @@ go 1.25.6 require ( github.com/BurntSushi/toml v1.6.0 - github.com/Dicklesworthstone/beads_viewer v0.14.3 - github.com/NYTimes/gziphandler v1.1.1 - github.com/a-h/templ v0.3.977 + github.com/google/uuid v1.6.0 + github.com/muesli/reflow v0.3.0 + github.com/steveyegge/beads v0.49.6 +) + +// Terminal dependencies +require ( github.com/c-bata/go-prompt v0.2.6 + github.com/charmbracelet/bubbles v0.21.1 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/fang v0.4.4 github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 - github.com/google/uuid v1.6.0 - github.com/muesli/reflow v0.3.0 - github.com/rs/cors v1.11.1 github.com/spf13/cobra v1.10.2 - github.com/steveyegge/beads v0.49.3 - golang.org/x/term v0.39.0 + golang.org/x/term v0.40.0 +) + +// Web dependencies +require ( + github.com/NYTimes/gziphandler v1.1.1 + github.com/a-h/templ v0.3.977 + github.com/rs/cors v1.11.1 ) require ( charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect - git.sr.ht/~sbinet/gg v0.7.0 // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect - github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect - github.com/alecthomas/chroma/v2 v2.23.1 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/aymerick/douceur v0.2.0 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/charmbracelet/bubbles v0.21.1 // indirect github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/glamour v0.10.0 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect - github.com/charmbracelet/x/ansi v0.11.5 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260209111912-3cca7cf7b09b // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20260203065841-a1c614051099 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20260209194814-eeb2896ac759 // indirect github.com/charmbracelet/x/exp/strings v0.1.0 // 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/cli/browser v1.3.0 // 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/dlclark/regexp2 v1.11.5 // indirect + github.com/clipperhouse/displaywidth v0.10.0 // indirect + github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect - github.com/gorilla/css v1.0.1 // indirect - github.com/haatos/goshipit v0.0.0-20260102021700-fcb988ab74c5 // indirect + github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/mattn/go-tty v0.0.3 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mattn/go-tty v0.0.7 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/mango v0.1.0 // indirect - github.com/muesli/mango-cobra v1.2.0 // indirect - github.com/muesli/mango-pflag v0.1.0 // indirect + github.com/muesli/mango v0.2.0 // indirect + github.com/muesli/mango-cobra v1.3.0 // indirect + github.com/muesli/mango-pflag v0.2.0 // indirect github.com/muesli/roff v0.1.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/natefinch/atomic v1.0.1 // indirect github.com/ncruces/go-sqlite3 v0.30.5 // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect @@ -91,24 +84,15 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tetratelabs/wazero v1.11.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.16 // indirect - github.com/yuin/goldmark-emoji v1.0.6 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/image v0.35.0 // indirect - golang.org/x/mod v0.32.0 // indirect - golang.org/x/net v0.49.0 // indirect + golang.org/x/exp v0.0.0-20260209203927-2842357ff358 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.41.0 // indirect - gonum.org/v1/gonum v0.17.0 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.67.7 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.44.3 // indirect ) tool ( diff --git a/go.sum b/go.sum index 14f3663..6d444a0 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,7 @@ charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k= charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= -git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= -git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= -git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM= -git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Dicklesworthstone/beads_viewer v0.14.3 h1:DxeICQESYvrH+8cn/Pymlb7l9Xm5XL6BTyi6KWiY3H8= -github.com/Dicklesworthstone/beads_viewer v0.14.3/go.mod h1:5oEV2h+PVmBTdQpOijnlWMlG/qi+zheXbgOUlqKmWKI= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= @@ -17,16 +10,6 @@ github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ6 github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg= github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= -github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= -github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= -github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= -github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -35,8 +18,6 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE 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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= @@ -51,28 +32,24 @@ github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY= github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo= -github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= -github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ= -github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0= -github.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4= -github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/ultraviolet v0.0.0-20260209111912-3cca7cf7b09b h1:jyHmbVXscPtC1S4Cg2OW1Zq3bwTJoY6/q40Ahi8CqaA= +github.com/charmbracelet/ultraviolet v0.0.0-20260209111912-3cca7cf7b09b/go.mod h1:42rCfhmE+4ZM7twEctghIzlIWyPj6FCDTBiMepHE2Ss= +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/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= 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/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260209194814-eeb2896ac759 h1:U0li+kYsNefPhJPP9SfwQJNbQ58iXWRtidnj1Rqf//g= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260209194814-eeb2896ac759/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/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= -github.com/charmbracelet/x/exp/slice v0.0.0-20260203065841-a1c614051099 h1:H09krOypYvXsPG4kYl+9J3tCRhST8WI3WEpXJELw+eg= -github.com/charmbracelet/x/exp/slice v0.0.0-20260203065841-a1c614051099/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= 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= @@ -85,20 +62,16 @@ github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGl github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -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/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= +github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -111,28 +84,14 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 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/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= -github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/haatos/goshipit v0.0.0-20260102021700-fcb988ab74c5 h1:0nJIrglhp2shqGH+sM4dYcADM5w8gCT2ilE6mxdZcio= -github.com/haatos/goshipit v0.0.0-20260102021700-fcb988ab74c5/go.mod h1:LFP8N8y5ORkifb+LZuOVNZYlJuV3WqdXCjxX5pGUaNI= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 h1:sb7SfxZfN+U9OHC61tcS98Ge0zY9uEkW5CP6KB4YVHg= +github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320/go.mod h1:LFP8N8y5ORkifb+LZuOVNZYlJuV3WqdXCjxX5pGUaNI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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= @@ -157,22 +116,21 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 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/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= -github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= +github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= 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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 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/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= -github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= -github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= -github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= -github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= -github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= +github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ= +github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= +github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec= +github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E= +github.com/muesli/mango-pflag v0.2.0 h1:QViokgKDZQCzKhYe1zH8D+UlPJzBSGoP9yx0hBG0t5k= +github.com/muesli/mango-pflag v0.2.0/go.mod h1:X9LT1p/pbGA1wjvEbtwnixujKErkP0jVmrxwrw3fL0Y= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= @@ -183,8 +141,6 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0 github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/ncruces/go-sqlite3 v0.30.5 h1:6usmTQ6khriL8oWilkAZSJM/AIpAlVL2zFrlcpDldCE= github.com/ncruces/go-sqlite3 v0.30.5/go.mod h1:0I0JFflTKzfs3Ogfv8erP7CCoV/Z8uxigVDNOR0AQ5E= -github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= -github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -193,8 +149,6 @@ github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -219,8 +173,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 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/steveyegge/beads v0.49.3 h1:cMV1p5yhQZvHztnL3OwZRrwpHamPfIiJItcqJ+RSsmI= -github.com/steveyegge/beads v0.49.3/go.mod h1:CRsyKS7RrUeiv/+rCbq3PtOOo01rFlRsWOq+IAeGkkU= +github.com/steveyegge/beads v0.49.6 h1:ac/SJBYuz+hUww07pbjLfhXw8RGNyIkslTbutqHzWYQ= +github.com/steveyegge/beads v0.49.6/go.mod h1:yYUYUsF8GbLEylNiJMu2BSwAOCgRLrywfGOO8oKPmoA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -233,97 +187,35 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= -github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= -github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= -golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= -golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c= +golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= -modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= -modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= -modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI= -modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= -modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index c85a988..c3f515c 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -1,3 +1,4 @@ +// Package cli provides the command-line interface for the PM System. package cli import ( @@ -7,7 +8,7 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/cli/commands" ) -// CLIConfig is an alias for service.Config, which contains all the necessary +// CLIConfig is an alias for service.Config, used to configure the CLI. type CLIConfig = service.Config // Run initializes the services and executes the CLI commands. diff --git a/pkg/cli/commands/close.go b/pkg/cli/commands/close.go new file mode 100644 index 0000000..b4ff882 --- /dev/null +++ b/pkg/cli/commands/close.go @@ -0,0 +1,63 @@ +package commands + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" +) + +// closeCmd represents the close command, +// which allows users to close an existing issue by its ID. +var closeCmd = &cobra.Command{ + Use: "close [id]", + Short: "Close an existing issue", + Long: `Close an existing issue by its ID.`, + Example: `pm close pm-abc`, + + Args: cobra.ExactArgs(1), + RunE: runCloseCmd, + + ValidArgsFunction: completeIssues, +} + +// runCloseCmd executes the close command logic, +// which closes an issue by its ID after confirming with the user. +func runCloseCmd(cmd *cobra.Command, args []string) error { + closeID := args[0] + + if closeID == "" { + return fmt.Errorf("issue ID cannot be empty") + } + + // Fetch the issue to ensure it exists before closing. + issue, err := svc.Beads.GetIssue(cmd.Context(), closeID) + if err != nil { + return fmt.Errorf("error fetching issue: %w", err) + } + + if issue == nil { + return fmt.Errorf("issue with ID %s not found", closeID) + } + + // Ask for closing reason + if err = huh.NewInput().Value(&issue.CloseReason). + Title("Reason for closing the issue?").WithTheme(huh.ThemeBase()).Run(); err != nil { + return fmt.Errorf("error getting close reason: %w", err) + } + + // Close the issue. + err = svc.Beads.CloseIssue(cmd.Context(), closeID, issue.CloseReason, "", "") + if err != nil { + return fmt.Errorf("error closing issue: %w", err) + } + + cmd.Println("Closed issue with ID:", closeID) + + return nil +} + +// init function to set up the close command and its flags. +func init() { + rootCmd.AddCommand(closeCmd) +} diff --git a/pkg/cli/commands/completion.go b/pkg/cli/commands/completion.go index ff13107..77d3fcf 100644 --- a/pkg/cli/commands/completion.go +++ b/pkg/cli/commands/completion.go @@ -8,6 +8,20 @@ import ( "github.com/spf13/cobra" ) +// Variables for completion options and functions. +var ( + typeOptions = []string{"bug", "feature", "task"} + statusOptions = []string{"open", "closed", "in_progress"} + priorityRange = []string{"0", "1", "2", "3", "4"} +) + +// completionFunc returns a function that provides shell completion for the given options. +func completionFunc(options []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return options, cobra.ShellCompDirectiveDefault + } +} + // completeIssues provides shell completion for issue IDs and titles. func completeIssues(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { issues, _ := GetIssueCompletions(cmd.Context(), toComplete) diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index 289c72f..538ed27 100644 --- a/pkg/cli/commands/create.go +++ b/pkg/cli/commands/create.go @@ -5,17 +5,13 @@ import ( "strings" "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) -// Variables to hold flag values for the create command. -var ( - createDescription string - createStatus string - createType string - createPriority int -) +// createFlags holds the flag values for the create command +var createFlags Flags const ( createCmdExample = `pm create New issue -d "Description" -s open -t task -p 3 @@ -29,25 +25,32 @@ var createCmd = &cobra.Command{ Long: `Create a new issue with the specified details.`, Example: createCmdExample, + Args: cobra.MinimumNArgs(0), Aliases: []string{"add"}, - Args: cobra.MinimumNArgs(1), RunE: runCreateCmd, } // runCreateCmd executes the create command logic, func runCreateCmd(cmd *cobra.Command, args []string) error { - createTitle := strings.Join(args, " ") + createFlags.title = strings.Join(args, " ") - if createTitle == "" { + // Run interactive if flag is set + if createFlags.interactive { + if err := runCreateInteractive(); err != nil { + return err + } + } + + if createFlags.title == "" { return fmt.Errorf("issue title cannot be empty") } issue := &models.Issue{ - Title: createTitle, - Description: createDescription, - Status: models.Status(createStatus), - IssueType: models.IssueType(createType), - Priority: createPriority, + Title: createFlags.title, + Description: createFlags.description, + Status: models.Status(createFlags.status), + IssueType: models.IssueType(createFlags.issueType), + Priority: createFlags.priority, } // Create the issue using the service layer. @@ -56,52 +59,60 @@ func runCreateCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("error creating issue: %w", err) } - // Build the output string with the created issue details. - str := fmt.Sprintf("Created issue with ID: %s\n", issue.ID) - - if issue.Title != "" { - str += fmt.Sprintf("Title: %s\n", issue.Title) - } - - if issue.Description != "" { - str += fmt.Sprintf("Description: %s\n", issue.Description) - } - - if issue.Status != "" { - str += fmt.Sprintf("Status: %s\n", issue.Status) - } - - if issue.IssueType != "" { - str += fmt.Sprintf("Type: %s\n", issue.IssueType) - } - - if issue.Priority != 0 { - str += fmt.Sprintf("Priority: %d\n", issue.Priority) - } - - cmd.Print(str) + // Display the created issue details to the user. + cmd.Printf("Created issue:\n%s", models.IssueString(*issue)) return nil } +// runCreateInteractive runs the interactive mode for creating issues, +// allowing users to input issue details through a form. +func runCreateInteractive() error { + form := huh.NewForm( + + huh.NewGroup( + huh.NewInput().Value(&createFlags.title).Title("Title"), + huh.NewText().Value(&createFlags.description).Title("Description")), + + huh.NewGroup( + huh.NewSelect[string]().Title("Status"). + Options( + huh.NewOption("Open", "open"), + huh.NewOption("Closed", "closed"), + huh.NewOption("In Progress", "in_progress"), + ).Value(&createFlags.status), + + huh.NewSelect[string]().Title("Type"). + Options( + huh.NewOption("Bug", "bug"), + huh.NewOption("Feature", "feature"), + huh.NewOption("Task", "task"), + ).Value(&createFlags.issueType), + + huh.NewSelect[int]().Title("Priority"). + Options( + huh.NewOption("0", 0), + huh.NewOption("1", 1), + huh.NewOption("2", 2), + huh.NewOption("3", 3), + huh.NewOption("4", 4), + ).Value(&createFlags.priority), + )).WithTheme(huh.ThemeBase16()) + + return form.Run() +} + // init function to set up the create command and its flags. func init() { - createCmd.Flags().StringVarP(&createDescription, "desc", "d", "", "Issue description") - createCmd.Flags().StringVarP(&createStatus, "status", "s", "open", "Issue status(open, closed, in_progress)") - createCmd.Flags().StringVarP(&createType, "type", "t", "task", "Issue type(bug, feature, task)") - createCmd.Flags().IntVarP(&createPriority, "priority", "p", 0, "Issue priority(0-5)") + createCmd.Flags().BoolVarP(&createFlags.interactive, "interactive", "i", false, "Create issue interactively") + createCmd.Flags().StringVarP(&createFlags.description, "desc", "d", "", "Issue description") + createCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress)") + createCmd.Flags().StringVarP(&createFlags.issueType, "type", "t", "task", "Issue type(bug, feature, task)") + createCmd.Flags().IntVarP(&createFlags.priority, "priority", "p", 0, "Issue priority(0-4)") - createCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"bug", "feature", "task"}, cobra.ShellCompDirectiveDefault - }) - - createCmd.RegisterFlagCompletionFunc("status", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"open", "closed", "in_progress"}, cobra.ShellCompDirectiveDefault - }) - - createCmd.RegisterFlagCompletionFunc("priority", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"0", "1", "2", "3", "4", "5"}, cobra.ShellCompDirectiveDefault - }) + createCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions)) + createCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions)) + createCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange)) rootCmd.AddCommand(createCmd) } diff --git a/pkg/cli/commands/delete.go b/pkg/cli/commands/delete.go index ce2cc40..b96e659 100644 --- a/pkg/cli/commands/delete.go +++ b/pkg/cli/commands/delete.go @@ -1,15 +1,21 @@ package commands import ( + "context" "fmt" "strings" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/charmbracelet/huh" "github.com/spf13/cobra" ) // Variables for delete command flag. -var confirmDelete bool +var ( + confirmDelete bool + deleteIDs []string + deleteInteractive bool +) // deleteCmd represents the delete command. var deleteCmd = &cobra.Command{ @@ -18,8 +24,9 @@ var deleteCmd = &cobra.Command{ Long: `Delete an existing issue by its ID.`, Example: `pm delete pm-abc`, + ValidArgsFunction: completeIssues, + Aliases: []string{"del", "remove", "rm"}, - Args: cobra.ExactArgs(1), RunE: runDeleteCmd, } @@ -28,6 +35,17 @@ var deleteCmd = &cobra.Command{ func runDeleteCmd(cmd *cobra.Command, args []string) error { deleteID := strings.Join(args, " ") + if deleteInteractive { + if err := runDeleteInteractive(cmd.Context()); err != nil { + return err + } + return nil + } + + if deleteID == "" { + return fmt.Errorf("issue ID cannot be empty") + } + // Fetch the issue to ensure it exists before deletion. issue, err := svc.Beads.GetIssue(cmd.Context(), deleteID) if err != nil { @@ -62,8 +80,49 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { return nil } +// runDeleteInteractive runs the interactive mode for deleting issues, +// allowing users to select multiple issues for deletion. +func runDeleteInteractive(ctx context.Context) error { + options := []huh.Option[string]{} + + issues, err := svc.Beads.SearchIssues(ctx, "", models.IssueFilter{}) + if err != nil { + return fmt.Errorf("error fetching issues: %w", err) + } + + for _, issue := range issues { + desc := fmt.Sprintf("%s: %s", issue.ID, issue.Title) + options = append(options, huh.NewOption(desc, issue.ID)) + } + + form := huh.NewForm( + huh.NewGroup( + huh.NewMultiSelect[string](). + Options(options...).Value(&deleteIDs). + Title("Select issues to delete"))).WithTheme(huh.ThemeBase()) + + if err := form.Run(); err != nil { + return fmt.Errorf("error running interactive form: %w", err) + } + + if len(deleteIDs) == 0 { + return fmt.Errorf("no issues selected for deletion") + } + + for _, id := range deleteIDs { + err := svc.Beads.DeleteIssue(ctx, id) + if err != nil { + return fmt.Errorf("error deleting issue with ID %s: %w", id, err) + } + fmt.Printf("Deleted issue with ID: %s\n", id) + } + + return nil +} + // init function to set up the delete command and its flags. func init() { + deleteCmd.Flags().BoolVarP(&deleteInteractive, "interactive", "i", false, "Delete issues interactively") deleteCmd.Flags().BoolVarP(&confirmDelete, "yes", "y", true, "Confirm deletion without prompt") rootCmd.AddCommand(deleteCmd) diff --git a/pkg/cli/commands/ls.go b/pkg/cli/commands/ls.go index 5bd841c..bd65899 100644 --- a/pkg/cli/commands/ls.go +++ b/pkg/cli/commands/ls.go @@ -8,30 +8,23 @@ import ( ) // Variables for get-issues command flags. -var ( - titleFlag string - descriptionFlag string - statusFlag string - typeFlag string - priorityFlag int - limit int = 25 -) +var listFlags Flags const ( - lsExamples = `pm ls [id|title|description] -pm ls --status open --type bug -pm ls --title "New feature" --desc "feature description" -pm ls -p 1 -l 10` + lsExamples = `pm list [id|title|description] +pm list --status open --type bug +pm list --title "New feature" --desc "feature description" +pm list -p 1 -l 10` ) // getIssuesCmd represents the get issues command. var getIssuesCmd = &cobra.Command{ - Use: "ls [search query]", + Use: "list [search query]", Short: "List all issues", Long: `List all issues in the project management system.`, Example: lsExamples, - Aliases: []string{"list", "search"}, + Aliases: []string{"ls", "search"}, Args: cobra.MinimumNArgs(0), RunE: runGetIssuesCmd, } @@ -42,23 +35,23 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error { queryArg := strings.Join(args, " ") filter := models.IssueFilter{ - TitleSearch: titleFlag, - DescriptionContains: descriptionFlag, - Limit: limit, + TitleSearch: listFlags.title, + DescriptionContains: listFlags.description, + Limit: listFlags.limit, } // Only set filter fields if the corresponding flags // were explicitly provided by the user. if cmd.Flags().Changed("status") { - s := models.Status(statusFlag) + s := models.Status(listFlags.status) filter.Status = &s } if cmd.Flags().Changed("type") { - t := models.IssueType(typeFlag) + t := models.IssueType(listFlags.issueType) filter.IssueType = &t } if cmd.Flags().Changed("priority") { - filter.Priority = &priorityFlag + filter.Priority = &listFlags.priority } // Fetch issues based on the search query and filters. @@ -76,24 +69,17 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error { // init function to set up the get issues command and its flags. func init() { - getIssuesCmd.Flags().StringVar(&titleFlag, "title", "", "Filter issues by title") - getIssuesCmd.Flags().StringVarP(&descriptionFlag, "desc", "d", "", "Filter issues by description") - getIssuesCmd.Flags().StringVarP(&statusFlag, "status", "s", "", "Filter issues by status (open, closed, in_progress)") - getIssuesCmd.Flags().StringVarP(&typeFlag, "type", "t", "", "Filter issues by type (bug, feature, task)") - getIssuesCmd.Flags().IntVarP(&priorityFlag, "priority", "p", 0, "Filter issues by priority (0-5)") - getIssuesCmd.Flags().IntVarP(&limit, "limit", "l", 25, "Limit the number of issues returned") + getIssuesCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title") + getIssuesCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description") + getIssuesCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)") + getIssuesCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)") + getIssuesCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)") - getIssuesCmd.RegisterFlagCompletionFunc("status", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"open", "closed", "in_progress"}, cobra.ShellCompDirectiveDefault - }) + getIssuesCmd.Flags().IntVarP(&listFlags.limit, "limit", "l", 25, "Limit the number of issues returned") - getIssuesCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"bug", "feature", "task"}, cobra.ShellCompDirectiveDefault - }) - - getIssuesCmd.RegisterFlagCompletionFunc("priority", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"0", "1", "2", "3", "4", "5"}, cobra.ShellCompDirectiveDefault - }) + getIssuesCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions)) + getIssuesCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions)) + getIssuesCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange)) rootCmd.AddCommand(getIssuesCmd) } diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index 418d7ae..9e18856 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -14,6 +14,18 @@ import ( // Must be called before executing any commands to ensure services are available. var svc *service.Services +// Flags struct to hold command-line flag values for issues. +type Flags struct { + interactive bool + limit int + + title string + description string + status string + issueType string + priority int +} + // rootCmd is the base command for the CLI application. var rootCmd = &cobra.Command{ Short: "Project Management CLI", @@ -56,7 +68,7 @@ func ExecuteArgsString(args []string) (string, error) { // init function to set up the command hierarchy and options. func init() { - rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.CompletionOptions.DisableDefaultCmd = false rootCmd.AddGroup(&cobra.Group{ID: "help", Title: "Helping Commands"}) rootCmd.SetCompletionCommandGroupID("help") rootCmd.SetHelpCommandGroupID("help") diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go new file mode 100644 index 0000000..099a08c --- /dev/null +++ b/pkg/cli/commands/update.go @@ -0,0 +1,100 @@ +package commands + +import ( + "fmt" + + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/spf13/cobra" +) + +var updateFlags Flags + +var updateCmd = &cobra.Command{ + Use: "update [issue ID]", + Short: "Update an existing issue", + Long: `Update an existing issue by its ID with the specified details.`, + Example: `pm update pm-001 --title "New title" -d "Description" -s in_progress --type task -p 3`, + RunE: runUpdateCmd, + Aliases: []string{"edit"}, + Args: cobra.ExactArgs(1), + ValidArgsFunction: completeIssues, +} + +func runUpdateCmd(cmd *cobra.Command, args []string) error { + issueID := args[0] + + issue, err := svc.Beads.GetIssue(cmd.Context(), issueID) + if err != nil { + return fmt.Errorf("error getting issue: %w", err) + } + + if issue == nil { + return fmt.Errorf("issue with ID %s not found", issueID) + } + + updates, err := getUpdateValues(cmd) + if err != nil { + return fmt.Errorf("error getting update values: %w", err) + } + + err = svc.Beads.UpdateIssue(cmd.Context(), issueID, updates, "test_actor") + if err != nil { + return fmt.Errorf("error updating issue: %w", err) + } + + updatedIssue, err := svc.Beads.GetIssue(cmd.Context(), issueID) + if err != nil { + return fmt.Errorf("error getting updated issue: %w", err) + } + + cmd.Printf("Updated issue to:\n%s", models.IssueString(*updatedIssue)) + + return nil +} + +func init() { + updateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title") + updateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description") + updateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)") + updateCmd.Flags().StringVarP(&updateFlags.issueType, "type", "t", "", "New issue type(bug, feature, task)") + updateCmd.Flags().IntVarP(&updateFlags.priority, "priority", "p", 0, "New issue priority(0-5)") + + updateCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions)) + updateCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions)) + updateCmd.RegisterFlagCompletionFunc("priority", completionFunc(priorityRange)) + + rootCmd.AddCommand(updateCmd) +} + +func getUpdateValues(cmd *cobra.Command) (map[string]interface{}, error) { + updates := make(map[string]interface{}) + + if cmd.Flags().Changed("title") { + if updateFlags.title == "" { + return updates, fmt.Errorf("issue title cannot be empty") + } + updates["title"] = updateFlags.title + } + + if cmd.Flags().Changed("desc") { + updates["description"] = updateFlags.description + } + + if cmd.Flags().Changed("status") { + updates["status"] = updateFlags.status + } + + if cmd.Flags().Changed("type") { + updates["issue_type"] = updateFlags.issueType + } + + if cmd.Flags().Changed("priority") { + updates["priority"] = updateFlags.priority + } + + if len(updates) == 0 { + return updates, fmt.Errorf("no updates specified") + } + + return updates, nil +} diff --git a/pkg/cli/repl/completer.go b/pkg/cli/repl/completer.go index 9cb119a..99d1b21 100644 --- a/pkg/cli/repl/completer.go +++ b/pkg/cli/repl/completer.go @@ -6,23 +6,28 @@ import ( "github.com/c-bata/go-prompt" ) +// completer provides suggestions for the REPL input based on the current input text. func completer(d prompt.Document) []prompt.Suggest { - text := d.TextBeforeCursor() - words := strings.Fields(text) + text := d.TextBeforeCursor() // Gets the text before the cursor as a string. + words := strings.Fields(text) // Split the string into words as []string. + // If there are no words, return no suggestions. if len(words) == 0 { return nil } + // If the first word is not "pm", only provide root-level suggestions. if words[0] != "pm" { return filterByPrefix(rootSuggestions, words[0]) } + // If the first word is "pm", provide command and flag suggestions based on the context. pmWords := words[1:] if len(words) == 1 || (len(pmWords) == 1 && !strings.HasSuffix(text, " ")) { return commandSuggestions(pmWords) } + // If the last word starts with a "-", provide flag suggestions for the current command. if len(pmWords) >= 1 { return flagSuggestions(pmWords[0], pmWords, text) } diff --git a/pkg/cli/repl/executor.go b/pkg/cli/repl/executor.go index 0ec939f..c0c50a7 100644 --- a/pkg/cli/repl/executor.go +++ b/pkg/cli/repl/executor.go @@ -7,6 +7,9 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/cli/commands" ) +// execute processes the input command and returns the output +// or an error if it occurs. It handles what type of command is being executed, +// whether it's a PM command or a shell command, and routes it accordingly. func execute(input string) (string, error) { if input == "" { return "", nil @@ -26,6 +29,8 @@ func execute(input string) (string, error) { return executeShellCommand(input) } +// executeShellCommand executes a shell command +// and returns its output or an error if it occurs. func executeShellCommand(input string) (string, error) { parts := strings.Fields(input) if len(parts) == 0 { @@ -37,6 +42,8 @@ func executeShellCommand(input string) (string, error) { return string(output), err } +// executePMCommand executes a PM command using the commands package +// and returns its output or an error if it occurs. func executePMCommand(input string) (string, error) { parts := strings.Fields(input) if len(parts) == 0 { diff --git a/pkg/cli/repl/options.go b/pkg/cli/repl/options.go index 7ba4e63..0d0918c 100644 --- a/pkg/cli/repl/options.go +++ b/pkg/cli/repl/options.go @@ -5,13 +5,8 @@ import "github.com/c-bata/go-prompt" const PromptPrefix = "> " const OptionMaxSuggestions = 5 -const ( - ReplHelp = `Type 'pm help' for available PM commands. -You can also run shell commands directly. Type 'exit' or 'quit' to leave.` - - ReplTitle = "Welcome to Project Management CLI! " + ReplHelp -) - +// promptOptions returns a slice of prompt.Option +// to configure the behavior and appearance of the REPL prompt. func promptOptions(history []string) []prompt.Option { return []prompt.Option{ prompt.OptionPrefixTextColor(prompt.Cyan), diff --git a/pkg/cli/repl/repl.go b/pkg/cli/repl/repl.go index b00fa75..cba9569 100644 --- a/pkg/cli/repl/repl.go +++ b/pkg/cli/repl/repl.go @@ -1,3 +1,4 @@ +// Package repl implements the Read-Eval-Print Loop (REPL) for the PM CLI. package repl import ( @@ -14,45 +15,63 @@ import ( "golang.org/x/term" ) +const ( + ReplHelp = `Type 'pm help' for available PM commands. +You can also run shell commands directly. Type 'exit' or 'quit' to leave.` + + ReplTitle = "Welcome to Project Management CLI! " + ReplHelp +) + +// RunREPL starts the interactive Read-Eval-Print Loop for the PM CLI. func RunREPL(ctx context.Context, config cli.CLIConfig) error { + // Set terminal to raw mode to capture input properly in the REPL. + // This allows us to handle input character by character and provide a better user experience. + // We also ensure that the terminal state is restored when the REPL exits, even if an error occurs. oldState, err := term.GetState(int(os.Stdin.Fd())) if err != nil { return fmt.Errorf("failed to get terminal state: %w", err) } defer term.Restore(int(os.Stdin.Fd()), oldState) + // Initialize services for beads, config and stats. svc, cleanup, err := service.NewServices(ctx, config) if err != nil { return fmt.Errorf("failed to initialize services: %w", err) } defer cleanup() + // Make sure to set services, to ensure they are available. commands.SetServices(svc) - fmt.Println(styles.TitleStyle.Render(ReplTitle)) + fmt.Println(styles.TitleStyle.Render(ReplTitle)) // Print REPL title. + // history keeps track of command history. + // This enables navigating through previous commands. var history []string + // Start the REPL loop, which continues until the user types "exit" or "quit". for { + // Prompt the user for input, and provide suggestions. input := prompt.Input( PromptPrefix, completer, promptOptions(history)..., ) + // Trim whitespace from the input to ensure consistent command processing. input = strings.TrimSpace(input) + // If the user types "exit" or "quit", break the loop and exit the REPL. if input == "exit" || input == "quit" { fmt.Println("Goodbye!") break } + // Add the input to the history for future navigation. history = append(history, input) - // Ignore errors for now, gives better ux - output, _ := execute(input) - - fmt.Println(styles.CommandStyle.Render(output)) + output, _ := execute(input) // Ignore errors for now, gives better ux + fmt.Println(styles.CommandStyle.Render(output)) // Print the output of the command in a styled format. } return nil diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index defefe2..e99c681 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -9,6 +9,7 @@ import ( "github.com/muesli/reflow/truncate" ) +// rootSuggestions is a list of prompt suggestions for root-level commands. var rootSuggestions = []prompt.Suggest{ {Text: "pm", Description: "Project Management System"}, {Text: "exit", Description: "Exit pm CLI"}, @@ -17,21 +18,36 @@ var rootSuggestions = []prompt.Suggest{ {Text: "git", Description: "Version control system"}, } +// commandSuggestions is a list of prompt suggestions for PM commands. var baseSuggestions = []prompt.Suggest{ {Text: "help", Description: "Show help information"}, {Text: "delete", Description: "Delete an issue by ID"}, + {Text: "close", Description: "Close an issue by ID"}, {Text: "create", Description: "Create a new issue with title"}, + {Text: "update", Description: "Update an existing issue by ID"}, {Text: "describe", Description: "Get issue details by ID"}, {Text: "list", Description: "List all issues"}, } +// createFlags is a list of prompt suggestions for the create command flags. var createFlags = []prompt.Suggest{ + {Text: "--interactive", Description: "Create issue interactively"}, {Text: "--desc", Description: "Issue description"}, {Text: "--status", Description: "Issue status (open, closed, in_progress)"}, {Text: "--type", Description: "Issue type (bug, feature, task)"}, {Text: "--priority", Description: "Issue priority (0-5)"}, } +// updateFlags is a list of prompt suggestions for the update command flags. +var updateFlags = []prompt.Suggest{ + {Text: "--title", Description: "New issue title"}, + {Text: "--desc", Description: "New issue description"}, + {Text: "--status", Description: "New issue status (open, closed, in_progress)"}, + {Text: "--type", Description: "New issue type (bug, feature, task)"}, + {Text: "--priority", Description: "New issue priority (0-5)"}, +} + +// listFlags is a list of prompt suggestions for the list command flags. var listFlags = []prompt.Suggest{ {Text: "--title", Description: "Filter by title"}, {Text: "--desc", Description: "Filter by description"}, @@ -41,27 +57,63 @@ var listFlags = []prompt.Suggest{ {Text: "--limit", Description: "Limit number of results"}, } +var deleteFlags = []prompt.Suggest{ + {Text: "--yes", Description: "Confirm deletion without prompt"}, + {Text: "--interactive", Description: "Select issues to delete interactively"}, +} + +// statusValues is a list of prompt suggestions for status types var statusValues = []prompt.Suggest{ {Text: "open", Description: "Open status"}, {Text: "closed", Description: "Closed status"}, {Text: "in_progress", Description: "In progress status"}, } +// typeValues is a list of prompt suggestions for issue types var typeValues = []prompt.Suggest{ {Text: "bug", Description: "Bug issue type"}, {Text: "feature", Description: "Feature issue type"}, {Text: "task", Description: "Task issue type"}, } +// priorityValues is a list of prompt suggestions for issue priority levels var priorityValues = []prompt.Suggest{ {Text: "0", Description: "Lowest priority"}, {Text: "1", Description: "Low priority"}, {Text: "2", Description: "Medium-low priority"}, {Text: "3", Description: "Medium priority"}, {Text: "4", Description: "High priority"}, - {Text: "5", Description: "Highest priority"}, } +// isIDCommand maps command names to a boolean indicating whether they expect an issue ID as an argument. +var isIDCommand = map[string]bool{ + "describe": true, + "delete": true, + "del": true, + "rm": true, + "remove": true, + "get": true, + "read": true, + "close": true, + "update": true, + "edit": true, +} + +var commandFlags = map[string][]prompt.Suggest{ + "create": createFlags, + "add": createFlags, + "update": updateFlags, + "edit": updateFlags, + "list": listFlags, + "ls": listFlags, + "search": listFlags, + "delete": deleteFlags, + "del": deleteFlags, + "rm": deleteFlags, + "remove": deleteFlags, +} + +// commandSuggestions returns a list of prompt suggestions based on the current input words. func commandSuggestions(words []string) []prompt.Suggest { if len(words) == 0 { return baseSuggestions @@ -69,6 +121,7 @@ func commandSuggestions(words []string) []prompt.Suggest { return filterByPrefix(baseSuggestions, words[0]) } +// flagSuggestions returns a list of prompt suggestions for command flags based on the current input. func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { lastWord, prevWord := parseWords(words, text) @@ -76,29 +129,19 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { return filterByPrefix(values, lastWord) } - if cmd == "describe" || cmd == "delete" || cmd == "del" || cmd == "rm" || cmd == "remove" || cmd == "get" || cmd == "read" { - // For ID-oriented commands, pass the current partial argument (lastWord) - // so issueIDSuggestions can distinguish between completing the command - // name and completing the ID itself. - return issueIDSuggestions(lastWord, len(words) >= 2) + flags := commandFlags[cmd] + + if isIDCommand[cmd] { + if len(words) < 2 && !strings.HasPrefix(lastWord, "-") { + return issueIDSuggestions(lastWord, true) + } + return filterByPrefix(flags, lastWord) } - var flags []prompt.Suggest - switch cmd { - case "create", "add": - flags = createFlags - case "list", "ls", "search": - flags = listFlags - default: - return nil - } - - if lastWord == "" { - return flags - } return filterByPrefix(flags, lastWord) } +// issueIDSuggestions returns a list of prompt suggestions for issue IDs based on the current partial input. func issueIDSuggestions(partial string, hasCommand bool) []prompt.Suggest { // Only show suggestions if we've typed the command already if !hasCommand { @@ -117,6 +160,7 @@ func issueIDSuggestions(partial string, hasCommand bool) []prompt.Suggest { return suggestions } +// parseWords extracts the last and previous words from the input for flag suggestion logic. func parseWords(words []string, text string) (lastWord, prevWord string) { if len(words) > 0 && !strings.HasSuffix(text, " ") { lastWord = words[len(words)-1] @@ -129,6 +173,7 @@ func parseWords(words []string, text string) (lastWord, prevWord string) { return } +// getFlagValues returns a list of prompt suggestions for flag values based on the given flag. func getFlagValues(flag string) []prompt.Suggest { switch flag { case "-s", "--status": @@ -141,6 +186,7 @@ func getFlagValues(flag string) []prompt.Suggest { return nil } +// filterByPrefix filters a list of prompt suggestions based on a given prefix. func filterByPrefix(suggestions []prompt.Suggest, prefix string) []prompt.Suggest { if prefix == "" { return suggestions diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go new file mode 100644 index 0000000..53b3dfc --- /dev/null +++ b/pkg/tui/styles/styles.go @@ -0,0 +1,80 @@ +package styles + +import "github.com/charmbracelet/lipgloss" + +var ( + Primary = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} + Secondary = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} + + Success = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} + Warning = lipgloss.AdaptiveColor{Light: "#F59E0B", Dark: "#F59E0B"} + Error = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"} + + PrimaryText = lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#E0E0E0"} + SecondaryText = lipgloss.AdaptiveColor{Light: "#666666", Dark: "#999999"} + FaintText = lipgloss.AdaptiveColor{Light: "#999999", Dark: "#666666"} + + PrimaryBorder = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} + SecondaryBorder = lipgloss.AdaptiveColor{Light: "#CCCCCC", Dark: "#444444"} + + SelectedBackground = lipgloss.AdaptiveColor{Light: "#E8E8E8", Dark: "#333333"} +) + +const ( + ListViewRatio = 70 // Percentage of total width allocated to the list view + LabelWidth = 14 + MarginBottomSmall = 1 +) + +var DefaultBorder = lipgloss.ThickBorder() + +var ( + HeaderStyle = lipgloss.NewStyle().Foreground(Primary).Padding(0, 1).Bold(true) + HeaderTitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0) +) + +var ContainerStyle = lipgloss.NewStyle(). + Border(DefaultBorder, true, false, false, false). + BorderForeground(SecondaryBorder). + Padding(1) + +var DetailsContainerStyle = lipgloss.NewStyle(). + Border(DefaultBorder, true, false, false, true). + BorderForeground(SecondaryBorder). + Padding(1) + +var ( + RowStyle = lipgloss.NewStyle().MarginBottom(MarginBottomSmall) + TitleStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) + LabelStyle = lipgloss.NewStyle().Foreground(SecondaryText) + ValueStyle = lipgloss.NewStyle().Foreground(PrimaryText) + IssueTypeStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) +) + +var ( + FilterStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true).Padding(0, 1) + FilterInputStyle = lipgloss.NewStyle().Foreground(PrimaryText).Padding(0, 1) + FilterPromptStyle = lipgloss.NewStyle().Foreground(Primary).Bold(true) +) + +func StatusStyle(status string) lipgloss.Style { + style := lipgloss.NewStyle().Bold(true) + switch status { + case "open": + return style.Foreground(Secondary) + case "closed": + return style.Foreground(FaintText) + case "in_progress": + return style.Foreground(Warning) + default: + return style.Foreground(SecondaryText) + } +} + +func HighlightKey(key string) string { + return lipgloss.NewStyle(). + Foreground(Primary). + Bold(true). + Padding(0, 1). + Render(key) +} diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 19db02a..b0feda5 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -4,17 +4,22 @@ import ( "context" "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/pkg/tui/views" + tea "github.com/charmbracelet/bubbletea" ) type TUIConfig = service.Config -func Run(ctx context.Context, config TUIConfig) error { - _, cleanup, err := service.NewServices(ctx, config) +func Run(ctx context.Context, config TUIConfig) (tea.Model, error) { + svc, cleanup, err := service.NewServices(ctx, config) if err != nil { - return err + return nil, err } + defer cleanup() - return nil + app := tea.NewProgram(views.NewDashboardView(svc), + tea.WithAltScreen(), tea.WithMouseAllMotion()) + return app.Run() } diff --git a/pkg/tui/views/dashboard/header.go b/pkg/tui/views/dashboard/header.go new file mode 100644 index 0000000..fcd2b7e --- /dev/null +++ b/pkg/tui/views/dashboard/header.go @@ -0,0 +1,32 @@ +package dashboard + +import ( + "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/charmbracelet/lipgloss" +) + +type Header struct { + title string +} + +func NewHeader(title string) Header { + return Header{ + title: title, + } +} + +func (h Header) View(width int) string { + title := styles.HeaderTitleStyle.Render(h.title) + + return lipgloss.PlaceHorizontal( + width, + lipgloss.Left, + title, + lipgloss.WithWhitespaceChars("─"), + lipgloss.WithWhitespaceForeground(styles.Primary), + ) +} + +func (h Header) Height() int { + return 1 +} diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go new file mode 100644 index 0000000..c030ab1 --- /dev/null +++ b/pkg/tui/views/dashboard/help_bar.go @@ -0,0 +1,94 @@ +package dashboard + +import ( + "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/charmbracelet/lipgloss" +) + +type HelpBar struct { + keyMap DashboardKeyMap + showAll bool + width int +} + +func NewHelpBar(keyMap DashboardKeyMap) HelpBar { + return HelpBar{keyMap: keyMap} +} + +func (h *HelpBar) SetWidth(width int) { + h.width = width +} + +func (h HelpBar) View() string { + if h.width == 0 { + return "" + } + if h.showAll { + return h.fullHelp() + } + return h.shortHelp() +} + +func (h HelpBar) shortHelp() string { + keys := []string{ + styles.HighlightKey("↑/k") + " up", + styles.HighlightKey("↓/j") + " down", + styles.HighlightKey("q") + " quit", + styles.HighlightKey("?") + " help", + } + content := lipgloss.JoinHorizontal(lipgloss.Left, keys...) + return lipgloss.NewStyle(). + Border(lipgloss.Border{Top: "─"}, true, false, false, false). + BorderForeground(styles.SecondaryBorder). + Padding(0, 1). + Width(h.width). + Render(content) +} + +func (h HelpBar) fullHelp() string { + keyStyle := lipgloss.NewStyle().Width(8).Align(lipgloss.Right) + descStyle := lipgloss.NewStyle().Width(12) + + renderHelpItem := func(key, desc string) string { + return lipgloss.JoinHorizontal( + lipgloss.Left, + keyStyle.Render(styles.HighlightKey(key)), + " ", + descStyle.Render(desc), + ) + } + + renderRow := func(leftKey, leftDesc, rightKey, rightDesc string) string { + leftItem := renderHelpItem(leftKey, leftDesc) + rightItem := renderHelpItem(rightKey, rightDesc) + return lipgloss.JoinHorizontal(lipgloss.Left, leftItem, " ", rightItem) + } + + rows := []string{ + renderRow("↑/k", "up", "enter", "view issue"), + renderRow("↓/j", "down", "b", "back to list"), + renderRow("?", "help", "q", "quit"), + } + content := lipgloss.JoinVertical(lipgloss.Left, rows...) + return lipgloss.NewStyle(). + Border(lipgloss.Border{Top: "─"}, true, false, false, false). + BorderForeground(styles.SecondaryBorder). + Padding(0, 1). + Width(h.width). + Render(content) +} + +func (h HelpBar) Height() int { + if h.width == 0 { + return 0 + } + return lipgloss.Height(h.View()) +} + +func (h HelpBar) IsExpanded() bool { + return h.showAll +} + +func (h *HelpBar) ToggleHelp() { + h.showAll = !h.showAll +} diff --git a/pkg/tui/views/dashboard/issue_detail.go b/pkg/tui/views/dashboard/issue_detail.go new file mode 100644 index 0000000..8706863 --- /dev/null +++ b/pkg/tui/views/dashboard/issue_detail.go @@ -0,0 +1,95 @@ +package dashboard + +import ( + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/charmbracelet/bubbles/viewport" + "github.com/charmbracelet/lipgloss" +) + +type IssueDetail struct { + viewport viewport.Model + issue models.Issue + focused bool +} + +func NewIssueDetail() IssueDetail { + vp := viewport.New(0, 0) + return IssueDetail{ + viewport: vp, + } +} + +func (i *IssueDetail) SetIssue(issue models.Issue) { + i.issue = issue + i.refreshContent() +} + +func (i *IssueDetail) SetSize(width, height int) { + i.viewport.Height = height + i.viewport.Width = width + i.refreshContent() +} + +func (i *IssueDetail) SetFocused(focused bool) { + i.focused = focused +} + +func (i *IssueDetail) refreshContent() { + + titleRow := styles.RowStyle.Render( + styles.TitleStyle.Render(i.issue.Title), + ) + + idRow := styles.RowStyle.Render( + styles.LabelStyle.Render("ID:") + styles.ValueStyle.Render(i.issue.ID), + ) + + typeRow := styles.RowStyle.Render( + styles.LabelStyle.Render("Type:") + styles.ValueStyle.Render(string(i.issue.IssueType)), + ) + + statusRow := styles.RowStyle.Render( + styles.LabelStyle.Render("Status:") + styles.StatusStyle(string(i.issue.Status)).Render(string(i.issue.Status)), + ) + + descLabel := styles.LabelStyle.Render("Description:") + descContent := styles.ValueStyle.Render(i.issue.Description) + + content := lipgloss.JoinVertical(lipgloss.Left, + titleRow, + idRow, + typeRow, + statusRow, + descLabel, + descContent, + ) + + i.viewport.SetContent(content) +} + +func (i IssueDetail) View() string { + content := i.viewport.View() + + if i.focused { + return styles.DetailsContainerStyle. + BorderForeground(styles.PrimaryBorder). + Width(i.viewport.Width). + Height(i.viewport.Height). + MaxHeight(i.viewport.Height). + Render(content) + } + return styles.DetailsContainerStyle. + Width(i.viewport.Width). + Height(i.viewport.Height). + MaxHeight(i.viewport.Height). + Render(content) +} + +func (i *IssueDetail) ScrollUp(lines int) { + i.viewport.ScrollUp(lines) +} + +func (i *IssueDetail) ScrollDown(lines int) { + i.viewport.ScrollDown(lines) +} diff --git a/pkg/tui/views/dashboard/issue_list.go b/pkg/tui/views/dashboard/issue_list.go new file mode 100644 index 0000000..18504c1 --- /dev/null +++ b/pkg/tui/views/dashboard/issue_list.go @@ -0,0 +1,256 @@ +package dashboard + +import ( + "context" + "io" + + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/reflow/truncate" +) + +type IssueList struct { + list list.Model + svc *service.Services + width int + height int +} + +type ListIssue struct { + models.Issue +} + +func (l ListIssue) Title() string { return l.Issue.Title } +func (l ListIssue) Description() string { return l.Issue.Description } +func (l ListIssue) FilterValue() string { return l.Issue.ID + " " + l.Issue.Title } + +type TableColumn struct { + width uint + label string + key string +} + +func getTableColumns(width int) []TableColumn { + switch { + case width < 45: + return []TableColumn{ + {width: 10, label: "ID", key: "id"}, + {width: uint(width - 10), label: "TITLE", key: "title"}, + } + case width < 60: + return []TableColumn{ + {width: 10, label: "ID", key: "id"}, + {width: 20, label: "TITLE", key: "title"}, + {width: 15, label: "STATUS", key: "status"}, + } + default: + return []TableColumn{ + {width: 12, label: "ID", key: "id"}, + {width: 20, label: "TITLE", key: "title"}, + {width: 15, label: "STATUS", key: "status"}, + {width: 10, label: "TYPE", key: "type"}, + } + } +} + +func renderHeaders(cols []TableColumn) string { + var parts []string + headerStyle := lipgloss.NewStyle().Foreground(styles.FaintText).Bold(true) + + for _, col := range cols { + colWidth := col.width + if colWidth == 0 { + colWidth = 1 + } + style := lipgloss.NewStyle().Width(int(colWidth)) + headerText := headerStyle.Render(truncate.StringWithTail(col.label, colWidth, "...")) + parts = append(parts, style.Render(headerText)) + } + + return lipgloss.JoinHorizontal(lipgloss.Left, parts...) +} + +func NewIssueList(svc *service.Services, width, height int) IssueList { + issues, err := svc.Beads.AllIssues(context.Background()) + if err != nil { + return IssueList{} + } + + listIssues := []ListIssue{} + for _, issue := range issues { + listIssues = append(listIssues, ListIssue{Issue: issue}) + } + + items := make([]list.Item, len(listIssues)) + for i, issue := range listIssues { + items[i] = issue + } + + l := list.New(items, NewIssueListDelegate(width), width, height) + l.SetShowTitle(false) + l.SetShowHelp(false) + l.SetShowStatusBar(false) + l.SetFilteringEnabled(true) + l.FilterInput.PromptStyle = styles.FilterPromptStyle + l.FilterInput.Cursor.Style = styles.FilterStyle + l.FilterInput.TextStyle = styles.FilterInputStyle + l.FilterInput.Prompt = "🔍 " + + return IssueList{ + list: l, + svc: svc, + width: width, + height: height, + } +} + +func (l *IssueList) Update(msg tea.Msg) (tea.Cmd, bool) { + var cmd tea.Cmd + oldIndex := l.list.Index() + l.list, cmd = l.list.Update(msg) + changed := l.list.Index() != oldIndex + return cmd, changed +} + +func (l *IssueList) SetSize(width, height int) { + l.width = width + l.height = height + + l.list.SetSize(width, height) + l.list.SetDelegate(NewIssueListDelegate(width)) +} + +func (l IssueList) View() string { + return l.renderResponsive() +} + +func (l IssueList) renderResponsive() string { + cols := getTableColumns(l.width) + header := renderHeaders(cols) + + var content []string + + if l.list.FilterState() == list.Filtering { + filterText := l.list.FilterInput.Value() + filterView := styles.FilterStyle.Render("🔍 " + filterText) + content = append(content, filterView) + } + + itemsView := l.renderFilteredItems() + content = append(content, header, itemsView) + + return styles.ContainerStyle. + Width(l.width). + MaxWidth(l.width). + MaxHeight(l.height). + Render(lipgloss.JoinVertical(lipgloss.Left, content...)) +} + +func (l IssueList) renderFilteredItems() string { + var items []string + + var visibleItems []list.Item + if l.list.FilterState() == list.Filtering || l.list.FilterState() == list.FilterApplied { + visibleItems = l.list.VisibleItems() + } else { + visibleItems = l.list.Items() + } + + itemsPerPage := l.list.Paginator.ItemsOnPage(len(visibleItems)) + start := l.list.Paginator.Page * itemsPerPage + end := min(start+itemsPerPage, len(visibleItems)) + + cursor := l.list.Index() + + for i := start; i < end && i < len(visibleItems); i++ { + isSelected := i == cursor + if issue, ok := visibleItems[i].(ListIssue); ok { + cols := getTableColumns(l.width) + row := renderRow(issue, isSelected, cols) + items = append(items, row) + } + } + + return lipgloss.JoinVertical(lipgloss.Left, items...) +} + +func (l IssueList) SelectedItem() ListIssue { + if item, ok := l.list.SelectedItem().(ListIssue); ok { + return item + } + return ListIssue{} +} + +func (l IssueList) Index() int { + return l.list.Index() +} + +func (l IssueList) FilterState() list.FilterState { + return l.list.FilterState() +} + +type IssueListDelegate struct { + width int +} + +func NewIssueListDelegate(width int) IssueListDelegate { + return IssueListDelegate{width: width} +} + +func (d IssueListDelegate) Height() int { return 1 } +func (d IssueListDelegate) Spacing() int { return 0 } +func (d IssueListDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil } + +func (d IssueListDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + issue, ok := listItem.(ListIssue) + if !ok { + return + } + + isSelected := index == m.Index() + cols := getTableColumns(d.width) + + row := renderRow(issue, isSelected, cols) + io.WriteString(w, row) +} + +func renderRow(issue ListIssue, isSelected bool, cols []TableColumn) string { + var parts []string + + for _, col := range cols { + value := getColumnValue(col, issue) + colWidth := col.width + if colWidth == 0 { + colWidth = 1 + } + + style := lipgloss.NewStyle().Width(int(colWidth)) + if isSelected { + style = style.Background(styles.SelectedBackground).Bold(true) + } + + truncated := truncate.StringWithTail(value, colWidth, "...") + parts = append(parts, style.Render(truncated)) + } + + return lipgloss.JoinHorizontal(lipgloss.Left, parts...) +} + +func getColumnValue(col TableColumn, issue ListIssue) string { + switch col.key { + case "id": + return issue.ID + case "title": + return issue.Title() + case "status": + return string(issue.Issue.Status) + case "type": + return string(issue.Issue.IssueType) + default: + return "" + } +} diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go new file mode 100644 index 0000000..dfdeb03 --- /dev/null +++ b/pkg/tui/views/dashboard/keys.go @@ -0,0 +1,63 @@ +package dashboard + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +type DashboardKeyMap struct { + Help key.Binding + Quit key.Binding + SelectIssue key.Binding + BackToList key.Binding + ScrollUp key.Binding + ScrollDown key.Binding +} + +var defaultDashboardKeyMap = DashboardKeyMap{ + Help: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "help"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "ctrl+c"), + key.WithHelp("q", "quit"), + ), + SelectIssue: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "view issue"), + ), + BackToList: key.NewBinding( + key.WithKeys("b"), + key.WithHelp("b", "back to list"), + ), + ScrollUp: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "up"), + ), + ScrollDown: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "down"), + ), +} + +func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { + var cmd tea.Cmd + + switch { + case key.Matches(msg, d.keyMap.Help): + d.helpBar.ToggleHelp() + case key.Matches(msg, d.keyMap.Quit): + return tea.Quit + case d.IsFocusedOnList() && key.Matches(msg, d.keyMap.SelectIssue): + d.FocusDetail() + case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.BackToList): + d.FocusList() + case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollUp): + d.issueDetail.ScrollUp(1) + case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): + d.issueDetail.ScrollDown(1) + } + + return cmd +} diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go new file mode 100644 index 0000000..702cb8f --- /dev/null +++ b/pkg/tui/views/dashboard/model.go @@ -0,0 +1,69 @@ +package dashboard + +import ( + "github.com/LazyBachelor/LazyPM/internal/service" + tea "github.com/charmbracelet/bubbletea" +) + +type Model struct { + header Header + issueList IssueList + issueDetail IssueDetail + helpBar HelpBar + keyMap DashboardKeyMap + svc *service.Services + width int + height int + focusedPane int // 0 = list, 1 = detail +} + +func NewDashboard(svc *service.Services) *Model { + m := &Model{ + header: NewHeader("Project Manager Dashboard"), + keyMap: defaultDashboardKeyMap, + svc: svc, + width: 80, + height: 24, + focusedPane: 0, + } + + m.issueList = NewIssueList(svc, 0, 0) + m.issueDetail = NewIssueDetail() + m.helpBar = NewHelpBar(m.keyMap) + + if selected := m.issueList.SelectedItem(); selected.ID != "" { + m.issueDetail.SetIssue(selected.Issue) + } + + return m +} + +func (m *Model) Init() tea.Cmd { + return nil +} + +func (m *Model) IsFocusedOnList() bool { + return m.focusedPane == 0 +} + +func (m *Model) IsFocusedOnDetail() bool { + return m.focusedPane == 1 +} + +func (m *Model) FocusList() { + m.focusedPane = 0 + m.issueDetail.SetFocused(false) +} + +func (m *Model) FocusDetail() { + m.focusedPane = 1 + m.issueDetail.SetFocused(true) +} + +func (m *Model) ToggleFocus() { + if m.focusedPane == 0 { + m.FocusDetail() + } else { + m.FocusList() + } +} diff --git a/pkg/tui/views/dashboard/update.go b/pkg/tui/views/dashboard/update.go new file mode 100644 index 0000000..7fe15cc --- /dev/null +++ b/pkg/tui/views/dashboard/update.go @@ -0,0 +1,35 @@ +package dashboard + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/bubbles/list" +) + +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if m.issueList.FilterState() == list.Filtering { + cmd, _ := m.issueList.Update(msg) + return m, cmd + } + + cmd := m.handleKeyMsg(msg) + if cmd != nil { + return m, cmd + } + + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, nil + } + + cmd, changed := m.issueList.Update(msg) + if changed { + if selected := m.issueList.SelectedItem(); selected.ID != "" { + m.issueDetail.SetIssue(selected.Issue) + } + } + + return m, cmd +} diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go new file mode 100644 index 0000000..18d207e --- /dev/null +++ b/pkg/tui/views/dashboard/view.go @@ -0,0 +1,36 @@ +package dashboard + +import ( + "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/charmbracelet/lipgloss" +) + +func (m *Model) View() string { + if m.width == 0 || m.height == 0 { + return "Loading..." + } + + m.helpBar.SetWidth(m.width) + + header := m.header.View(m.width) + headerHeight := m.header.Height() + + bottomView := m.helpBar.View() + bottomHeight := m.helpBar.Height() + + contentHeight := m.height - headerHeight - bottomHeight + + totalContentWidth := m.width - 1 + listWidth := totalContentWidth * styles.ListViewRatio / 100 + detailWidth := totalContentWidth - listWidth + + m.issueList.SetSize(listWidth, contentHeight) + m.issueDetail.SetSize(detailWidth, contentHeight) + + listView := m.issueList.View() + detailView := m.issueDetail.View() + + content := lipgloss.JoinHorizontal(lipgloss.Left, listView, detailView) + + return lipgloss.JoinVertical(lipgloss.Left, header, content, bottomView) +} diff --git a/pkg/tui/views/views.go b/pkg/tui/views/views.go new file mode 100644 index 0000000..c7bc138 --- /dev/null +++ b/pkg/tui/views/views.go @@ -0,0 +1,10 @@ +package views + +import ( + "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard" +) + +func NewDashboardView(svc *service.Services) *dashboard.Model { + return dashboard.NewDashboard(svc) +}