From 008bfd1efb481a8e34ac10f5bd892090a4281938 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Feb 2026 15:22:32 +0100 Subject: [PATCH 01/60] Add basic issue view, styling and ux need improvement --- cmd/tui/main.go | 2 +- go.mod | 2 +- pkg/tui/styles/styles.go | 30 +++++++++++++ pkg/tui/tui.go | 13 ++++-- pkg/tui/views/dashboard/deligate.go | 38 ++++++++++++++++ pkg/tui/views/dashboard/keys.go | 56 ++++++++++++++++++++++++ pkg/tui/views/dashboard/model.go | 29 +++++++++++++ pkg/tui/views/dashboard/update.go | 60 ++++++++++++++++++++++++++ pkg/tui/views/dashboard/view.go | 63 +++++++++++++++++++++++++++ pkg/tui/views/issue/view.go | 67 +++++++++++++++++++++++++++++ pkg/tui/views/views.go | 10 +++++ 11 files changed, 364 insertions(+), 6 deletions(-) create mode 100644 pkg/tui/styles/styles.go create mode 100644 pkg/tui/views/dashboard/deligate.go create mode 100644 pkg/tui/views/dashboard/keys.go create mode 100644 pkg/tui/views/dashboard/model.go create mode 100644 pkg/tui/views/dashboard/update.go create mode 100644 pkg/tui/views/dashboard/view.go create mode 100644 pkg/tui/views/issue/view.go create mode 100644 pkg/tui/views/views.go diff --git a/cmd/tui/main.go b/cmd/tui/main.go index 9d2e455..6871019 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -14,7 +14,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 abdfeb5..362b55b 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( 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/charmbracelet/bubbles v0.21.1 github.com/charmbracelet/bubbletea v1.3.10 github.com/google/uuid v1.6.0 github.com/rs/cors v1.11.1 @@ -24,7 +25,6 @@ require ( 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/huh v0.8.0 // indirect diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go new file mode 100644 index 0000000..e7901b3 --- /dev/null +++ b/pkg/tui/styles/styles.go @@ -0,0 +1,30 @@ +package styles + +import "github.com/charmbracelet/lipgloss" + +var ( + AppStyle = lipgloss.NewStyle().Padding(3, 3) + + TitleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("12")). + Padding(0, 1).Bold(true).Border(lipgloss.NormalBorder()) + + SelectedItemStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, false, true). + Foreground(lipgloss.Color("2")). + Padding(0, 0, 0, 1) + + ItemStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("7")). + Padding(0, 0, 0, 2) + + IssueStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("8")). + Padding(1) + + FocusedIssueStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("2")). + Padding(1) +) 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/deligate.go b/pkg/tui/views/dashboard/deligate.go new file mode 100644 index 0000000..f446e59 --- /dev/null +++ b/pkg/tui/views/dashboard/deligate.go @@ -0,0 +1,38 @@ +package dashboard + +import ( + "fmt" + "io" + + "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +type IssueListDelegate struct{} + +func (d IssueListDelegate) Height() int { return 2 } +func (d IssueListDelegate) Spacing() int { return 1 } +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 + } + + id := issue.ID + title := issue.Title() + description := issue.Description() + + str := fmt.Sprintf("ID: %s\t%s\nDescription:\t%s", id, title, description) + + fn := styles.ItemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return styles.SelectedItemStyle.Render(s...) + } + } + + fmt.Fprint(w, fn(str)) +} diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go new file mode 100644 index 0000000..70026b3 --- /dev/null +++ b/pkg/tui/views/dashboard/keys.go @@ -0,0 +1,56 @@ +package dashboard + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +type DashboardKeyMap struct { + Quit key.Binding + SelectIssue key.Binding + BackToList key.Binding + ScrollUp key.Binding + ScrollDown key.Binding +} + +var defaultDashboardKeyMap = DashboardKeyMap{ + 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", "scroll up"), + ), + ScrollDown: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "scroll down"), + ), +} + +func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { + var cmd tea.Cmd + + switch { + case key.Matches(msg, d.keyMap.Quit): + return tea.Quit + case !d.focusedOnIssue && key.Matches(msg, d.keyMap.SelectIssue): + d.focusedOnIssue = true + case d.focusedOnIssue && key.Matches(msg, d.keyMap.BackToList): + d.focusedOnIssue = false + case d.focusedOnIssue && key.Matches(msg, d.keyMap.ScrollUp): + d.issueView.Viewport.LineUp(1) + case d.focusedOnIssue && key.Matches(msg, d.keyMap.ScrollDown): + d.issueView.Viewport.LineDown(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..fed9456 --- /dev/null +++ b/pkg/tui/views/dashboard/model.go @@ -0,0 +1,29 @@ +package dashboard + +import ( + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/pkg/tui/views/issue" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +type Model struct { + issueView issue.Model + issueList list.Model + keyMap DashboardKeyMap + svc *service.Services + focusedOnIssue bool +} + +func (d Model) Init() tea.Cmd { + return nil +} + +type ListIssue struct { + models.Issue +} + +func (i ListIssue) Title() string { return i.Issue.Title } +func (i ListIssue) Description() string { return i.Issue.Description } +func (i ListIssue) FilterValue() string { return i.Issue.ID + " " + i.Issue.Title } diff --git a/pkg/tui/views/dashboard/update.go b/pkg/tui/views/dashboard/update.go new file mode 100644 index 0000000..b8dd033 --- /dev/null +++ b/pkg/tui/views/dashboard/update.go @@ -0,0 +1,60 @@ +package dashboard + +import ( + "fmt" + + "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" +) + +func (d Model) Update(m tea.Msg) (tea.Model, tea.Cmd) { + switch msg := m.(type) { + case tea.KeyMsg: + if d.issueList.FilterState() == list.Filtering { + break + } + cmd := d.handleKeyMsg(msg) + if cmd != nil { + return d, cmd + } + case tea.WindowSizeMsg: + d.updateSizes(msg.Width, msg.Height) + } + + var cmd tea.Cmd + oldIndex := d.issueList.Index() + d.issueList, cmd = d.issueList.Update(m) + + if d.issueList.Index() != oldIndex { + d.updateIssueView() + } + + return d, cmd +} + +func (d *Model) updateIssueView() { + if item, ok := d.issueList.SelectedItem().(ListIssue); ok { + d.issueView.ID = item.ID + d.issueView.Title = item.Issue.Title + d.issueView.Description = item.Issue.Description + d.issueView.Status = string(item.Issue.Status) + d.issueView.IssueType = string(item.Issue.IssueType) + + content := fmt.Sprintf("ID: %s\nTitle: %s\nDescription: %s\nStatus: %s\nType: %s", + d.issueView.ID, d.issueView.Title, d.issueView.Description, d.issueView.Status, d.issueView.IssueType) + + d.issueView.Viewport.SetContent(content) + } +} + +func (d *Model) updateSizes(width, height int) { + listWidth := width / 2 + issueWidth := width - listWidth + + w, h := styles.AppStyle.GetFrameSize() + d.issueList.SetSize(listWidth-w, height-h) + d.issueView.SetSize(issueWidth, height-h) + d.issueView.Viewport.Width = issueWidth + d.issueView.Viewport.Height = height - h +} diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go new file mode 100644 index 0000000..31f7b71 --- /dev/null +++ b/pkg/tui/views/dashboard/view.go @@ -0,0 +1,63 @@ +package dashboard + +import ( + "context" + + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/lipgloss" + + "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/LazyBachelor/LazyPM/pkg/tui/styles" + "github.com/LazyBachelor/LazyPM/pkg/tui/views/issue" +) + +func NewDashboard(svc *service.Services) Model { + + issues, err := svc.Beads.AllIssues(context.Background()) + + listIssues := []ListIssue{} + for _, issue := range issues { + listIssues = append(listIssues, ListIssue{Issue: issue}) + } + + if err != nil { + listIssues = []ListIssue{} + } + + items := make([]list.Item, len(listIssues)) + for i, issue := range listIssues { + items[i] = issue + } + + issueList := list.New(items, IssueListDelegate{}, 0, 0) + + issueList.Title = "Issues" + issueList.Styles.Title = styles.TitleStyle + + issueView := issue.NewIssueView(issue.Model{}) + + m := Model{ + issueView: issueView, + issueList: issueList, + keyMap: defaultDashboardKeyMap, + svc: svc, + } + + m.updateIssueView() + + return m +} + +func (d Model) View() string { + issueView := d.issueView.View() + + if d.focusedOnIssue { + issueView = styles.FocusedIssueStyle.Render(issueView) + } else { + issueView = styles.IssueStyle.Render(issueView) + } + + str := lipgloss.JoinHorizontal(lipgloss.Left, styles.AppStyle.Render(d.issueList.View()), issueView) + + return str +} diff --git a/pkg/tui/views/issue/view.go b/pkg/tui/views/issue/view.go new file mode 100644 index 0000000..ff80d86 --- /dev/null +++ b/pkg/tui/views/issue/view.go @@ -0,0 +1,67 @@ +package issue + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" +) + +type Model struct { + Viewport viewport.Model + ID string + Title string + Description string + Status string + IssueType string + Width, Height int + ready bool +} + +func NewIssueView(issue Model) Model { + return Model{ + ID: issue.ID, + Title: issue.Title, + Description: issue.Description, + Status: issue.Status, + IssueType: issue.IssueType, + Width: issue.Width, + Height: issue.Height, + Viewport: viewport.New(issue.Width, issue.Height), + ready: false, + } +} + +func (m *Model) SetSize(width, height int) { + m.Width = width + m.Height = height +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + case tea.WindowSizeMsg: + m.SetSize(msg.Width, msg.Height) + m.Viewport.Width = m.Width + m.Viewport.Height = m.Height + m.ready = true + } + + if !m.ready { + return m, nil + } + + content := fmt.Sprintf("ID: %s\nTitle: %s\nDescription: %s\nStatus: %s\nType: %s", + m.ID, m.Title, m.Description, m.Status, m.IssueType) + + m.Viewport.SetContent(content) + return m, nil +} + +func (m Model) View() string { + return fmt.Sprintf("%s", m.Viewport.View()) +} diff --git a/pkg/tui/views/views.go b/pkg/tui/views/views.go new file mode 100644 index 0000000..a246942 --- /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) +} From 3a9b8a8bd1bfa57275759c12d3f75b1dc00b79d4 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Feb 2026 19:44:28 +0100 Subject: [PATCH 02/60] make survery use new tui --- cmd/survey.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/survey.go b/cmd/survey.go index 47da94a..392c781 100644 --- a/cmd/survey.go +++ b/cmd/survey.go @@ -1,13 +1,14 @@ package main import ( + "context" + "fmt" + "os" + "github.com/LazyBachelor/LazyPM/pkg" "github.com/LazyBachelor/LazyPM/pkg/cli" "github.com/LazyBachelor/LazyPM/pkg/tui" "github.com/LazyBachelor/LazyPM/pkg/web" - "context" - "fmt" - "os" ) func main() { @@ -23,7 +24,7 @@ func main() { switch os.Args[1] { case "tui": - err = tui.Run(ctx, config) + _, err = tui.Run(ctx, config) case "cli": err = cli.Run(ctx, config) case "web": From a38c4ec7b159ae078623f08ac205db914359002f Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Feb 2026 19:45:13 +0100 Subject: [PATCH 03/60] use custom help menu --- go.mod | 2 +- pkg/tui/views/dashboard/keys.go | 27 +++++++++++++++++++++++---- pkg/tui/views/dashboard/model.go | 7 ++----- pkg/tui/views/dashboard/view.go | 12 +++++++++++- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 362b55b..5104136 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/a-h/templ v0.3.977 github.com/charmbracelet/bubbles v0.21.1 github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/google/uuid v1.6.0 github.com/rs/cors v1.11.1 github.com/spf13/cobra v1.10.2 @@ -28,7 +29,6 @@ require ( github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/glamour v0.10.0 // indirect github.com/charmbracelet/huh v0.8.0 // indirect - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect github.com/charmbracelet/x/ansi v0.11.5 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20260203065841-a1c614051099 // indirect diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 70026b3..6a832ea 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -6,6 +6,7 @@ import ( ) type DashboardKeyMap struct { + Help key.Binding Quit key.Binding SelectIssue key.Binding BackToList key.Binding @@ -14,6 +15,10 @@ type DashboardKeyMap struct { } var defaultDashboardKeyMap = DashboardKeyMap{ + Help: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "help"), + ), Quit: key.NewBinding( key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit"), @@ -28,18 +33,32 @@ var defaultDashboardKeyMap = DashboardKeyMap{ ), ScrollUp: key.NewBinding( key.WithKeys("up", "k"), - key.WithHelp("↑/k", "scroll up"), + key.WithHelp("↑/k", "up"), ), ScrollDown: key.NewBinding( key.WithKeys("down", "j"), - key.WithHelp("↓/j", "scroll down"), + key.WithHelp("↓/j", "down"), ), } +func (m DashboardKeyMap) ShortHelp() []key.Binding { + return []key.Binding{m.ScrollDown, m.ScrollUp, m.Quit, m.Help} +} + +func (m DashboardKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {m.SelectIssue, m.BackToList}, + {m.ScrollUp, m.ScrollDown}, + {m.Help, m.Quit}, + } +} + func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { var cmd tea.Cmd switch { + case key.Matches(msg, d.keyMap.Help): + d.help.ShowAll = !d.help.ShowAll case key.Matches(msg, d.keyMap.Quit): return tea.Quit case !d.focusedOnIssue && key.Matches(msg, d.keyMap.SelectIssue): @@ -47,9 +66,9 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { case d.focusedOnIssue && key.Matches(msg, d.keyMap.BackToList): d.focusedOnIssue = false case d.focusedOnIssue && key.Matches(msg, d.keyMap.ScrollUp): - d.issueView.Viewport.LineUp(1) + d.issueView.Viewport.ScrollUp(1) case d.focusedOnIssue && key.Matches(msg, d.keyMap.ScrollDown): - d.issueView.Viewport.LineDown(1) + d.issueView.Viewport.ScrollDown(1) } return cmd diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index fed9456..d327bd1 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -4,11 +4,12 @@ import ( "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/tui/views/issue" + "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" ) type Model struct { + help help.Model issueView issue.Model issueList list.Model keyMap DashboardKeyMap @@ -16,10 +17,6 @@ type Model struct { focusedOnIssue bool } -func (d Model) Init() tea.Cmd { - return nil -} - type ListIssue struct { models.Issue } diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 31f7b71..6700adf 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -3,7 +3,9 @@ package dashboard import ( "context" + "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/LazyBachelor/LazyPM/internal/service" @@ -33,10 +35,12 @@ func NewDashboard(svc *service.Services) Model { issueList.Title = "Issues" issueList.Styles.Title = styles.TitleStyle + issueList.SetShowHelp(false) issueView := issue.NewIssueView(issue.Model{}) m := Model{ + help: help.New(), issueView: issueView, issueList: issueList, keyMap: defaultDashboardKeyMap, @@ -48,6 +52,10 @@ func NewDashboard(svc *service.Services) Model { return m } +func (d Model) Init() tea.Cmd { + return nil +} + func (d Model) View() string { issueView := d.issueView.View() @@ -57,7 +65,9 @@ func (d Model) View() string { issueView = styles.IssueStyle.Render(issueView) } - str := lipgloss.JoinHorizontal(lipgloss.Left, styles.AppStyle.Render(d.issueList.View()), issueView) + help := d.help.View(d.keyMap) + + str := lipgloss.JoinHorizontal(lipgloss.Left, styles.AppStyle.Render(d.issueList.View()), issueView) + "\n" + help return str } From 01edc7fe2f752fc296f38e7f9ed5ddfc74a0e6b6 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Feb 2026 19:46:39 +0100 Subject: [PATCH 04/60] gitignore .idea --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 62a01e9..4f41aae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .pm *.db *.ext -bin \ No newline at end of file +bin +.idea \ No newline at end of file From e5add142c597a3eb1420054d1d306bec7bdf568a Mon Sep 17 00:00:00 2001 From: Moira Daniella A Sebastian Date: Wed, 4 Feb 2026 18:21:06 +0100 Subject: [PATCH 05/60] Added title and description for CREATE ISSUE CLI --- pkg/cli/commands/create.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index 7b8704d..bf5c700 100644 --- a/pkg/cli/commands/create.go +++ b/pkg/cli/commands/create.go @@ -1,13 +1,19 @@ package commands import ( - "github.com/LazyBachelor/LazyPM/internal/models" "fmt" "os" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/spf13/cobra" ) +var ( + createTitle string + createDescription string +) + var createCmd = &cobra.Command{ Use: "create", Short: "Create a new issue", @@ -15,10 +21,16 @@ var createCmd = &cobra.Command{ RunE: runCreateCmd, } +func init() { + createCmd.Flags().StringVar(&createTitle, "title", "", "Issue title") + _ = createCmd.MarkFlagRequired("title") + createCmd.Flags().StringVar(&createDescription, "desc", "", "Issue description") +} + func runCreateCmd(cmd *cobra.Command, args []string) error { issue := &models.Issue{ - Title: "test", - Description: "This is a test issue created by the CLI.", + Title: createTitle, + Description: createDescription, Status: models.StatusOpen, IssueType: models.TypeBug, Priority: 0, From 0e8bbada6b41e4929381d50c285a5ec853077451 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Feb 2026 20:19:11 +0100 Subject: [PATCH 06/60] just return to avoid printing error twice --- cmd/cli/main.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 54e7634..d4f103d 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -1,11 +1,10 @@ package main import ( + "context" + "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/cli" - "context" - "fmt" - "os" ) func main() { @@ -16,7 +15,6 @@ func main() { } if err := cli.Run(context.Background(), config); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) + return } } From 296f9c6d83ef560fd2140d9d6bcb66b49aac2cf7 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Feb 2026 20:19:31 +0100 Subject: [PATCH 07/60] disable completon command --- pkg/cli/commands/root.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index 9cfa819..7f3488f 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -21,4 +21,5 @@ func Execute(services *service.Services) error { func init() { rootCmd.AddCommand(createCmd) + rootCmd.CompletionOptions.DisableDefaultCmd = true } From 370bbd626ca227d08c6e25ba4a8ba1d70b354755 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Feb 2026 20:19:53 +0100 Subject: [PATCH 08/60] add other flags and validation --- pkg/cli/commands/create.go | 56 ++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index bf5c700..5cbfba1 100644 --- a/pkg/cli/commands/create.go +++ b/pkg/cli/commands/create.go @@ -2,7 +2,6 @@ package commands import ( "fmt" - "os" "github.com/LazyBachelor/LazyPM/internal/models" @@ -10,37 +9,70 @@ import ( ) var ( - createTitle string createDescription string + createStatus string + createType string + createPriority int ) var createCmd = &cobra.Command{ - Use: "create", + Use: "issue [title]", Short: "Create a new issue", Long: `Create a new issue with the specified details.`, RunE: runCreateCmd, + Args: cobra.ExactArgs(1), } func init() { - createCmd.Flags().StringVar(&createTitle, "title", "", "Issue title") - _ = createCmd.MarkFlagRequired("title") - createCmd.Flags().StringVar(&createDescription, "desc", "", "Issue description") + 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)") } func runCreateCmd(cmd *cobra.Command, args []string) error { + createTitle := args[0] + + if createTitle == "" { + return fmt.Errorf("issue title cannot be empty") + } + issue := &models.Issue{ Title: createTitle, Description: createDescription, - Status: models.StatusOpen, - IssueType: models.TypeBug, - Priority: 0, + Status: models.Status(createStatus), + IssueType: models.IssueType(createType), + Priority: createPriority, } err := svc.Beads.CreateIssue(cmd.Context(), issue, "test_actor") if err != nil { - fmt.Fprintf(os.Stderr, "Error creating issue: %v\n", err) - os.Exit(1) + return fmt.Errorf("error creating issue: %w", err) } - fmt.Printf("Created issue: %s\n", issue.ID) + + 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) + } + + fmt.Print(str) + return nil } From a4054b26ae78175a5201ae609bd356c531267de1 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Feb 2026 20:23:32 +0100 Subject: [PATCH 09/60] cange back use to create --- pkg/cli/commands/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index 5cbfba1..1403271 100644 --- a/pkg/cli/commands/create.go +++ b/pkg/cli/commands/create.go @@ -16,7 +16,7 @@ var ( ) var createCmd = &cobra.Command{ - Use: "issue [title]", + Use: "create [title]", Short: "Create a new issue", Long: `Create a new issue with the specified details.`, RunE: runCreateCmd, From 25c71d17dfabca22ff7d844acb964af628656ccd Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 11:05:36 +0100 Subject: [PATCH 10/60] move help and completion command to group --- pkg/cli/commands/root.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index 7f3488f..ebe772f 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -21,5 +21,8 @@ func Execute(services *service.Services) error { func init() { rootCmd.AddCommand(createCmd) - rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.CompletionOptions.DisableDefaultCmd = false + rootCmd.AddGroup(&cobra.Group{ID: "other", Title: "Helping Commands"}) + rootCmd.SetCompletionCommandGroupID("other") + rootCmd.SetHelpCommandGroupID("other") } From 5950fd2f2c53bd0aa1a286ef9cdd12f35c7b91e6 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 11:07:09 +0100 Subject: [PATCH 11/60] make title be read from multible args instead of one. add completions to crate command --- pkg/cli/commands/create.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index 1403271..ea2e529 100644 --- a/pkg/cli/commands/create.go +++ b/pkg/cli/commands/create.go @@ -2,6 +2,7 @@ package commands import ( "fmt" + "strings" "github.com/LazyBachelor/LazyPM/internal/models" @@ -19,8 +20,11 @@ var createCmd = &cobra.Command{ Use: "create [title]", Short: "Create a new issue", Long: `Create a new issue with the specified details.`, - RunE: runCreateCmd, - Args: cobra.ExactArgs(1), + Example: `pm create New issue -d "Description" -s open -t task -p 3 +pm create Fix bug --desc "Bug description" --status in_progress --type bug --priority 5`, + RunE: runCreateCmd, + Aliases: []string{"add"}, + Args: cobra.MinimumNArgs(1), } func init() { @@ -28,10 +32,22 @@ func init() { 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.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 + }) } func runCreateCmd(cmd *cobra.Command, args []string) error { - createTitle := args[0] + createTitle := strings.Join(args, " ") if createTitle == "" { return fmt.Errorf("issue title cannot be empty") From 64a9c7cd0b5c550e0873a80d99138b1f71b02b1d Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:26:52 +0100 Subject: [PATCH 12/60] use tui config for consistency --- cmd/tui/main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/tui/main.go b/cmd/tui/main.go index 6871019..f53021f 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -3,12 +3,11 @@ package main import ( "context" - "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/tui" ) func main() { - config := service.Config{ + config := tui.TUIConfig{ StatisticsStoragePath: "./.pm/stats.json", BeadsDBPath: "./.pm/db.db", IssuePrefix: "pm", From d6fff28c929627afb32bc308405442b949f39072 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 12:48:09 +0100 Subject: [PATCH 13/60] add ls command for listing all issues with query and filter tags --- pkg/cli/commands/ls.go | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 pkg/cli/commands/ls.go diff --git a/pkg/cli/commands/ls.go b/pkg/cli/commands/ls.go new file mode 100644 index 0000000..76e68aa --- /dev/null +++ b/pkg/cli/commands/ls.go @@ -0,0 +1,91 @@ +package commands + +import ( + "strings" + + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/spf13/cobra" +) + +var ( + titleFlag string + descriptionFlag string + statusFlag string + typeFlag string + priorityFlag int + limit int = 25 +) + +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` +) + +var getIssuesCmd = &cobra.Command{ + Use: "ls [search query]", + Short: "List all issues", + Long: `List all issues in the project management system.`, + Aliases: []string{"list", "search"}, + Example: lsExamples, + Args: cobra.MinimumNArgs(0), + RunE: runGetIssuesCmd, +} + +func runGetIssuesCmd(cmd *cobra.Command, args []string) error { + + queryArg := strings.Join(args, " ") + + filter := models.IssueFilter{ + TitleSearch: titleFlag, + DescriptionContains: descriptionFlag, + Limit: limit, + } + + if cmd.Flags().Changed("status") { + s := models.Status(statusFlag) + filter.Status = &s + } + if cmd.Flags().Changed("type") { + t := models.IssueType(typeFlag) + filter.IssueType = &t + } + if cmd.Flags().Changed("priority") { + filter.Priority = &priorityFlag + } + + issuesPtr, err := svc.Beads.SearchIssues(cmd.Context(), queryArg, filter) + if err != nil { + return err + } + + issues := models.IssuesPtrToIssues(issuesPtr) + + models.PrintIssues(issues) + + return nil +} + +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.RegisterFlagCompletionFunc("status", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"open", "closed", "in_progress"}, cobra.ShellCompDirectiveDefault + }) + + 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 + }) + + rootCmd.AddCommand(getIssuesCmd) +} From 7303f28a881529076a701c52bbd003ba1610c9e7 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 12:49:23 +0100 Subject: [PATCH 14/60] add helper for printing issue list. add helper for converting issue pointer to issue --- internal/models/beads.go | 41 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/models/beads.go b/internal/models/beads.go index d009e86..83ec37b 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -1,6 +1,13 @@ package models -import "github.com/steveyegge/beads" +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/muesli/reflow/truncate" + "github.com/steveyegge/beads" +) type ( Issue = beads.Issue @@ -71,3 +78,35 @@ const ( EventLabelRemoved = beads.EventLabelRemoved EventCompacted = beads.EventCompacted ) + +func IssuesPtrToIssues(issuePtr []*Issue) []Issue { + issues := make([]Issue, 0, len(issuePtr)) + for _, issue := range issuePtr { + issues = append(issues, *issue) + } + return issues +} + +func FormatIssueRow(issue Issue) string { + return fmt.Sprintf( + "%s\t%s\t%s\t%s\t%s\t%d", + truncate.String(issue.ID, 5), + truncate.StringWithTail(issue.Title, 25, "..."), + truncate.StringWithTail(issue.Description, 40, "..."), + issue.Status, + issue.IssueType, + issue.Priority, + ) +} + +func PrintIssues(issues []Issue) { + w := tabwriter.NewWriter(os.Stdout, 8, 10, 5, ' ', 0) + + fmt.Fprintln(w, "ID\tTITLE\tDESCRIPTION\tSTATUS\tTYPE\tPRIORITY") + + for _, issue := range issues { + fmt.Fprintln(w, FormatIssueRow(issue)) + } + + w.Flush() +} From 4f28d81aae1672cc49944eef8711f6cbd1efa79d Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 12:50:01 +0100 Subject: [PATCH 15/60] add read/describe command for viewing issue details --- pkg/cli/commands/read.go | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pkg/cli/commands/read.go diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go new file mode 100644 index 0000000..0116b96 --- /dev/null +++ b/pkg/cli/commands/read.go @@ -0,0 +1,63 @@ +package commands + +import ( + "strings" + + "github.com/spf13/cobra" +) + +var getIssueCmd = &cobra.Command{ + Use: "describe [issue ID]", + Aliases: []string{"get", "read"}, + Short: "Gets issue details", + Long: `Gets issue details by ID`, + RunE: runGetCmd, + Args: cobra.ExactArgs(1), + ValidArgsFunction: completeIssues, +} + +func runGetCmd(cmd *cobra.Command, args []string) error { + issueID := args[0] + + issue, err := svc.Beads.GetIssue(cmd.Context(), issueID) + if err != nil { + return err + } + + cmd.Printf("Title: %s\n", issue.Title) + cmd.Printf("Description: %s\n", issue.Description) + cmd.Printf("Status: %s\n", issue.Status) + cmd.Printf("Type: %s\n", issue.IssueType) + cmd.Printf("Priority: %d\n", issue.Priority) + + return nil +} + +func init() { + rootCmd.AddCommand(getIssueCmd) +} + +func completeIssues(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if svc == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + issues, err := svc.Beads.AllIssues(cmd.Context()) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var completions []string + for _, issue := range issues { + + if strings.HasPrefix(issue.Title, toComplete) { + completions = append(completions, issue.ID) + } + + if strings.HasPrefix(issue.ID, toComplete) { + completions = append(completions, issue.ID) + } + } + + return completions, cobra.ShellCompDirectiveNoFileComp +} From 1f254fb7e8f0ad14a572989c797779d153c70ada Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:24:24 +0100 Subject: [PATCH 16/60] add variable to service config to store rootcmd name --- internal/service/service.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/service/service.go b/internal/service/service.go index 344ac92..c6afc25 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,16 +1,18 @@ package service import ( - "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/LazyBachelor/LazyPM/internal/storage" "context" "time" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/storage" + "github.com/google/uuid" "github.com/steveyegge/beads" ) type Config struct { + RootCmd string WebAddress string BeadsDBPath string IssuePrefix string From ab2fab6194a080cf69039cf36cdafb1dcf2a1b2e Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:25:34 +0100 Subject: [PATCH 17/60] use new rootCmd name var --- cmd/{cli => pm}/main.go | 4 ++-- pkg/cli/commands/root.go | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) rename cmd/{cli => pm}/main.go (78%) diff --git a/cmd/cli/main.go b/cmd/pm/main.go similarity index 78% rename from cmd/cli/main.go rename to cmd/pm/main.go index d4f103d..5ad70e1 100644 --- a/cmd/cli/main.go +++ b/cmd/pm/main.go @@ -3,12 +3,12 @@ package main import ( "context" - "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/cli" ) func main() { - config := service.Config{ + config := cli.CLIConfig{ + RootCmd: "pm", IssuePrefix: "pm", BeadsDBPath: "./.pm/db.db", StatisticsStoragePath: "./.pm/stats.json", diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index ebe772f..edee54c 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -9,20 +9,21 @@ import ( var svc *service.Services var rootCmd = &cobra.Command{ - Use: "pm", Short: "Project Management CLI", Long: `Project Management CLI for managing issues and tasks.`, } func Execute(services *service.Services) error { svc = services + rootCmd.Use = svc.Config.RootCmd return rootCmd.Execute() } func init() { rootCmd.AddCommand(createCmd) + rootCmd.CompletionOptions.DisableDefaultCmd = false - rootCmd.AddGroup(&cobra.Group{ID: "other", Title: "Helping Commands"}) - rootCmd.SetCompletionCommandGroupID("other") - rootCmd.SetHelpCommandGroupID("other") + rootCmd.AddGroup(&cobra.Group{ID: "help", Title: "Helping Commands"}) + rootCmd.SetCompletionCommandGroupID("help") + rootCmd.SetHelpCommandGroupID("help") } From 5d883927766ab6eb2b27f6663b3cf58bcabe59c9 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:26:02 +0100 Subject: [PATCH 18/60] add completion scripts for command line autocomplete --- Makefile | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ee8af5e..2e82291 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +SHELL := /bin/bash + tidy: go mod tidy @@ -27,4 +29,34 @@ tw: watch: @make -j2 dev tw -.PHONY: tidy clean build cli tui web dev tw \ No newline at end of file +completions: + @go build -o ./bin/pm ./cmd/pm + @mkdir -p ./bin + @./bin/pm completion bash > ./bin/pm_bash.sh + @./bin/pm completion zsh > ./bin/pm_zsh.sh + @./bin/pm completion fish > ./bin/pm_fish.sh + @./bin/pm completion powershell > ./bin/pm_powershell.ps1 + +install-bash-temp: completions + @go install ./cmd/pm + @source ./bin/pm_bash.sh + +install-zsh-temp: completions + @go install ./cmd/pm + @source ./bin/pm_zsh.sh + +install-fish-temp: completions + @go install ./cmd/pm + @source ./bin/pm_fish.sh + +install-powershell-temp: completions + @go install ./cmd/pm + @source ./bin/pm_powershell.ps1 + + +install-cli: completions + @go install ./cmd/pm + @sudo cp ./bin/pm_bash.sh /etc/bash_completion.d/pm + + +.PHONY: tidy clean build cli tui web dev tw completions install-bash-temp install-zsh-temp install-fish-temp install-powershell-temp install-cli \ No newline at end of file From ec0a6de601140cbb1fdfdd1be45074ae942438e4 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:48:41 +0100 Subject: [PATCH 19/60] Update pkg/cli/commands/read.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/commands/read.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go index 0116b96..53e1ba7 100644 --- a/pkg/cli/commands/read.go +++ b/pkg/cli/commands/read.go @@ -50,12 +50,10 @@ func completeIssues(cmd *cobra.Command, args []string, toComplete string) ([]str var completions []string for _, issue := range issues { - if strings.HasPrefix(issue.Title, toComplete) { - completions = append(completions, issue.ID) - } - if strings.HasPrefix(issue.ID, toComplete) { completions = append(completions, issue.ID) + } else if strings.HasPrefix(issue.Title, toComplete) { + completions = append(completions, issue.ID) } } From 0c737787b4bc2f1b85f1e8eaa1dd2c916dad0d0d Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:51:19 +0100 Subject: [PATCH 20/60] Update pkg/cli/commands/read.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/commands/read.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go index 53e1ba7..42e292b 100644 --- a/pkg/cli/commands/read.go +++ b/pkg/cli/commands/read.go @@ -9,8 +9,8 @@ import ( var getIssueCmd = &cobra.Command{ Use: "describe [issue ID]", Aliases: []string{"get", "read"}, - Short: "Gets issue details", - Long: `Gets issue details by ID`, + Short: "Get issue details", + Long: `Get issue details by ID`, RunE: runGetCmd, Args: cobra.ExactArgs(1), ValidArgsFunction: completeIssues, From 3981677d98f94070762fea02803ff8e91044bbee Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 13:55:20 +0100 Subject: [PATCH 21/60] chack for nil pointer in helper use helper for converting to issue --- internal/models/beads.go | 6 ++++-- internal/service/beads.go | 7 +------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/models/beads.go b/internal/models/beads.go index 83ec37b..0e9c663 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -81,8 +81,10 @@ const ( func IssuesPtrToIssues(issuePtr []*Issue) []Issue { issues := make([]Issue, 0, len(issuePtr)) - for _, issue := range issuePtr { - issues = append(issues, *issue) + for _, issuePtr := range issuePtr { + if issuePtr != nil { + issues = append(issues, *issuePtr) + } } return issues } diff --git a/internal/service/beads.go b/internal/service/beads.go index e9a191d..06c8373 100644 --- a/internal/service/beads.go +++ b/internal/service/beads.go @@ -37,12 +37,7 @@ func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) { return []models.Issue{}, nil } - issues := make([]models.Issue, 0, len(issuesPtr)) - for _, issuePtr := range issuesPtr { - if issuePtr != nil { - issues = append(issues, *issuePtr) - } - } + issues := models.IssuesPtrToIssues(issuesPtr) return issues, nil } From 9566edb381d2d71ae6ce201b9f933a61bd0c76f6 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 14:27:44 +0100 Subject: [PATCH 22/60] decrease truncate leangth for issueID --- internal/models/beads.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/models/beads.go b/internal/models/beads.go index 0e9c663..bae490a 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -92,7 +92,7 @@ func IssuesPtrToIssues(issuePtr []*Issue) []Issue { func FormatIssueRow(issue Issue) string { return fmt.Sprintf( "%s\t%s\t%s\t%s\t%s\t%d", - truncate.String(issue.ID, 5), + truncate.String(issue.ID, 10), truncate.StringWithTail(issue.Title, 25, "..."), truncate.StringWithTail(issue.Description, 40, "..."), issue.Status, From 5fcaf133a91b00de721debce178836e89d15d779 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Feb 2026 14:28:15 +0100 Subject: [PATCH 23/60] nil check issue so it does not crash when issue not found --- pkg/cli/commands/read.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go index 42e292b..eb054c0 100644 --- a/pkg/cli/commands/read.go +++ b/pkg/cli/commands/read.go @@ -24,6 +24,11 @@ func runGetCmd(cmd *cobra.Command, args []string) error { return err } + if issue == nil { + cmd.Printf("Issue with ID '%s' not found\n", issueID) + return nil + } + cmd.Printf("Title: %s\n", issue.Title) cmd.Printf("Description: %s\n", issue.Description) cmd.Printf("Status: %s\n", issue.Status) From c331ad20010dbdaad0386319603dc9148d25a9cb Mon Sep 17 00:00:00 2001 From: vb Date: Sat, 7 Feb 2026 15:33:09 +0100 Subject: [PATCH 24/60] adding update feature to cli --- pkg/cli/commands/update.go | 126 ++++++++++++++++++++++++++++++++++++ pkg/cli/repl/suggestions.go | 22 +++++++ 2 files changed, 148 insertions(+) create mode 100644 pkg/cli/commands/update.go diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go new file mode 100644 index 0000000..6a262d6 --- /dev/null +++ b/pkg/cli/commands/update.go @@ -0,0 +1,126 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + updateDescription string + updateStatus string + updateType string + updatePriority int + updateTitle string +) + +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 init() { + updateCmd.Flags().StringVar(&updateTitle, "title", "", "New issue title") + updateCmd.Flags().StringVarP(&updateDescription, "desc", "d", "", "New issue description") + updateCmd.Flags().StringVarP(&updateStatus, "status", "s", "", "New issue status(open, closed, in_progress)") + updateCmd.Flags().StringVarP(&updateType, "type", "", "", "New issue type(bug, feature, task)") + updateCmd.Flags().IntVarP(&updatePriority, "priority", "p", -1, "New issue priority(0-5)") + + updateCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"bug", "feature", "task"}, cobra.ShellCompDirectiveDefault + }) + + updateCmd.RegisterFlagCompletionFunc("status", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"open", "closed", "in_progress"}, cobra.ShellCompDirectiveDefault + }) + + updateCmd.RegisterFlagCompletionFunc("priority", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"0", "1", "2", "3", "4", "5"}, cobra.ShellCompDirectiveDefault + }) + + rootCmd.AddCommand(updateCmd) +} + +func runUpdateCmd(cmd *cobra.Command, args []string) error { + issueID := args[0] + + updates := make(map[string]interface{}) + + if cmd.Flags().Changed("title") { + if updateTitle == "" { + return fmt.Errorf("issue title cannot be empty") + } + updates["title"] = updateTitle + } + + if cmd.Flags().Changed("desc") { + updates["description"] = updateDescription + } + + if cmd.Flags().Changed("status") { + updates["status"] = updateStatus + } + + if cmd.Flags().Changed("type") { + updates["issue_type"] = updateType + } + + if cmd.Flags().Changed("priority") { + updates["priority"] = updatePriority + } + + if len(updates) == 0 { + return fmt.Errorf("no updates specified") + } + + 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) + } + + 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) + } + + str := fmt.Sprintf("Updated issue with ID: %s\n", issueID) + + if updatedIssue.Title != "" { + str += fmt.Sprintf("Title: %s\n", updatedIssue.Title) + } + + if updatedIssue.Description != "" { + str += fmt.Sprintf("Description: %s\n", updatedIssue.Description) + } + + if updatedIssue.Status != "" { + str += fmt.Sprintf("Status: %s\n", updatedIssue.Status) + } + + if updatedIssue.IssueType != "" { + str += fmt.Sprintf("Type: %s\n", updatedIssue.IssueType) + } + + if updatedIssue.Priority != 0 { + str += fmt.Sprintf("Priority: %d\n", updatedIssue.Priority) + } + + fmt.Print(str) + + return nil +} diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index defefe2..0d081cc 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -21,6 +21,7 @@ var baseSuggestions = []prompt.Suggest{ {Text: "help", Description: "Show help information"}, {Text: "delete", Description: "Delete 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"}, } @@ -32,6 +33,14 @@ var createFlags = []prompt.Suggest{ {Text: "--priority", Description: "Issue priority (0-5)"}, } +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)"}, +} + var listFlags = []prompt.Suggest{ {Text: "--title", Description: "Filter by title"}, {Text: "--desc", Description: "Filter by description"}, @@ -83,6 +92,19 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { return issueIDSuggestions(lastWord, len(words) >= 2) } + // Update/edit: suggest issue IDs when typing the ID, flags after ID is provided + if cmd == "update" || cmd == "edit" { + if len(words) >= 2 && (lastWord == "" || strings.HasPrefix(lastWord, "-")) { + // ID already provided (trailing space) or typing a flag - suggest flags + flags := updateFlags + if lastWord == "" { + return flags + } + return filterByPrefix(flags, lastWord) + } + return issueIDSuggestions(lastWord, len(words) >= 2) + } + var flags []prompt.Suggest switch cmd { case "create", "add": From 6bcff202b44ffa9eea3a3b25a391a59e72fd86c3 Mon Sep 17 00:00:00 2001 From: vb Date: Sat, 7 Feb 2026 15:39:05 +0100 Subject: [PATCH 25/60] fix --- pkg/cli/repl/suggestions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index 0d081cc..cc8b5d6 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -95,7 +95,7 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { // Update/edit: suggest issue IDs when typing the ID, flags after ID is provided if cmd == "update" || cmd == "edit" { if len(words) >= 2 && (lastWord == "" || strings.HasPrefix(lastWord, "-")) { - // ID already provided (trailing space) or typing a flag - suggest flags + // ID already provided (trailing space) or typing a flag -> suggest update flags flags := updateFlags if lastWord == "" { return flags From 232243926278326483f8f30ca6f3b93e3c66be5f Mon Sep 17 00:00:00 2001 From: viljarb0 <156418063+viljarb0@users.noreply.github.com> Date: Sun, 8 Feb 2026 09:43:54 +0100 Subject: [PATCH 26/60] Update pkg/cli/commands/update.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/commands/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go index 6a262d6..03f5754 100644 --- a/pkg/cli/commands/update.go +++ b/pkg/cli/commands/update.go @@ -116,7 +116,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { str += fmt.Sprintf("Type: %s\n", updatedIssue.IssueType) } - if updatedIssue.Priority != 0 { + if cmd.Flags().Changed("priority") { str += fmt.Sprintf("Priority: %d\n", updatedIssue.Priority) } From 910fad83dc86874e614b53e1f148d8f9037a513d Mon Sep 17 00:00:00 2001 From: viljarb0 <156418063+viljarb0@users.noreply.github.com> Date: Sun, 8 Feb 2026 09:44:12 +0100 Subject: [PATCH 27/60] Update pkg/cli/commands/update.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/commands/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go index 03f5754..81c8cec 100644 --- a/pkg/cli/commands/update.go +++ b/pkg/cli/commands/update.go @@ -120,7 +120,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { str += fmt.Sprintf("Priority: %d\n", updatedIssue.Priority) } - fmt.Print(str) + cmd.Print(str) return nil } From 228ffa3cdb8eee833bbb0bc9e2184952555a2646 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:28:26 +0100 Subject: [PATCH 28/60] fix path in makefile for cli --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 0b9dd86c7395c7d5562a2ed64951de21c26dc3bd Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:28:39 +0100 Subject: [PATCH 29/60] add close command --- pkg/cli/commands/close.go | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 pkg/cli/commands/close.go diff --git a/pkg/cli/commands/close.go b/pkg/cli/commands/close.go new file mode 100644 index 0000000..4bfecaa --- /dev/null +++ b/pkg/cli/commands/close.go @@ -0,0 +1,60 @@ +package commands + +import ( + "fmt" + "strings" + + "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`, + ValidArgsFunction: completeIssues, + + RunE: runCloseCmd, +} + +// 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 := strings.Join(args, " ") + + 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 + huh.NewInput().Value(&issue.CloseReason). + Title("Reason for closing the issue?").WithTheme(huh.ThemeBase()).Run() + + // 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) +} From 84c0ad22862a91b79c2031976fd8f3764adaa0ae Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:29:06 +0100 Subject: [PATCH 30/60] add completionFunc for reducing boilerplate for command implementatinos --- pkg/cli/commands/completion.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/cli/commands/completion.go b/pkg/cli/commands/completion.go index ff13107..af34c20 100644 --- a/pkg/cli/commands/completion.go +++ b/pkg/cli/commands/completion.go @@ -8,6 +8,19 @@ import ( "github.com/spf13/cobra" ) +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) From 59b912325b252a47592fec79f3211450ace1f684 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:29:50 +0100 Subject: [PATCH 31/60] add close issue to repl completions --- pkg/cli/repl/suggestions.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index defefe2..cf8b925 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -20,6 +20,7 @@ var rootSuggestions = []prompt.Suggest{ 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: "describe", Description: "Get issue details by ID"}, {Text: "list", Description: "List all issues"}, @@ -76,7 +77,8 @@ 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" { + if cmd == "describe" || cmd == "delete" || cmd == "del" || + cmd == "rm" || cmd == "remove" || cmd == "get" || cmd == "read" || cmd == "close" { // For ID-oriented commands, pass the current partial argument (lastWord) // so issueIDSuggestions can distinguish between completing the command // name and completing the ID itself. From 0e5e22619be3c25faff03ce580ba239303d35238 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:30:24 +0100 Subject: [PATCH 32/60] add a flags struct other commands can impelemt enable completion command --- pkg/cli/commands/root.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index 418d7ae..296d029 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -14,6 +14,15 @@ import ( // Must be called before executing any commands to ensure services are available. var svc *service.Services +type Flags struct { + interactive bool + 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 +65,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") From f403c9b79d1a8f6c8849afd902cd84a23ad87f08 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:33:43 +0100 Subject: [PATCH 33/60] use flag struct rename usage to list --- pkg/cli/commands/ls.go | 58 ++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 36 deletions(-) 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) } From a24c658473f8d676e54012237612a091a9da120f Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:34:51 +0100 Subject: [PATCH 34/60] refactor for simplicity change to use createFlags var add interactive mode --- pkg/cli/commands/create.go | 120 ++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 54 deletions(-) diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index 289c72f..95e9d53 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,61 @@ 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 } +func runCreateInteractive() error { + form := huh.NewForm( + + huh.NewGroup( + huh.NewInput().Value(&createFlags.title).Title("Title"), + huh.NewText().Value(&createFlags.description).Title("Description"), + ).Title("Issue Details"), + + huh.NewGroup( + huh.NewSelect[string](). + Options( + huh.NewOption("Open", "open"), + huh.NewOption("Closed", "closed"), + huh.NewOption("In Progress", "in_progress"), + ).Value(&createFlags.status).Title("Status"), + + huh.NewSelect[string](). + Options( + huh.NewOption("Bug", "bug"), + huh.NewOption("Feature", "feature"), + huh.NewOption("Task", "task"), + ).Value(&createFlags.issueType).Title("Type"), + + huh.NewSelect[int](). + Options( + huh.NewOption("0", 0), + huh.NewOption("1", 1), + huh.NewOption("2", 2), + huh.NewOption("3", 3), + huh.NewOption("4", 4), + huh.NewOption("5", 5), + ).Value(&createFlags.priority).Title("Priority"), + ).Title("Create New Issue").WithTheme(huh.ThemeBase()), + ) + + 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-5)") - 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) } From 33ea5c70d8fb880bdb8ed3c0fcf4ee69ddcd68fd Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:35:13 +0100 Subject: [PATCH 35/60] add interactive mode --- pkg/cli/commands/delete.go | 57 +++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/pkg/cli/commands/delete.go b/pkg/cli/commands/delete.go index ce2cc40..4884464 100644 --- a/pkg/cli/commands/delete.go +++ b/pkg/cli/commands/delete.go @@ -1,15 +1,19 @@ 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 deleteIDs []string +var deleteInteractive bool // deleteCmd represents the delete command. var deleteCmd = &cobra.Command{ @@ -18,8 +22,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 +33,17 @@ var deleteCmd = &cobra.Command{ func runDeleteCmd(cmd *cobra.Command, args []string) error { deleteID := strings.Join(args, " ") + if deleteInteractive { + if err := runDeleteInteractive(); 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 +78,47 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { return nil } +func runDeleteInteractive() error { + options := []huh.Option[string]{} + + issues, err := svc.Beads.SearchIssues(context.Background(), "", 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]().Value(&deleteIDs). + 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(context.Background(), 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) From 38faee631fa21d82db8d026827b2f473f3d4e7a9 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:35:29 +0100 Subject: [PATCH 36/60] add limit to flags --- pkg/cli/commands/root.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index 296d029..2115fc1 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -16,6 +16,8 @@ var svc *service.Services type Flags struct { interactive bool + limit int + title string description string status string From 3bf80ba85940fd7993b6c5fe5fb87e5ac0546bcf Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Sun, 8 Feb 2026 12:56:09 +0100 Subject: [PATCH 37/60] add comments to the repl package --- pkg/cli/cli.go | 3 ++- pkg/cli/repl/completer.go | 9 +++++++-- pkg/cli/repl/executor.go | 7 +++++++ pkg/cli/repl/options.go | 9 ++------- pkg/cli/repl/repl.go | 29 ++++++++++++++++++++++++----- pkg/cli/repl/suggestions.go | 29 ++++++++++++++++++++++++++--- 6 files changed, 68 insertions(+), 18 deletions(-) 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/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 cf8b925..bc347b2 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,6 +18,7 @@ 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"}, @@ -26,6 +28,7 @@ var baseSuggestions = []prompt.Suggest{ {Text: "list", Description: "List all issues"}, } +// createFlags is a list of prompt suggestions for the create command flags. var createFlags = []prompt.Suggest{ {Text: "--desc", Description: "Issue description"}, {Text: "--status", Description: "Issue status (open, closed, in_progress)"}, @@ -33,6 +36,7 @@ var createFlags = []prompt.Suggest{ {Text: "--priority", Description: "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"}, @@ -42,27 +46,42 @@ var listFlags = []prompt.Suggest{ {Text: "--limit", Description: "Limit number of results"}, } +// 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, +} + +// 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 @@ -70,6 +89,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) @@ -77,8 +97,7 @@ 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" || cmd == "close" { + if isIDCommand[cmd] { // For ID-oriented commands, pass the current partial argument (lastWord) // so issueIDSuggestions can distinguish between completing the command // name and completing the ID itself. @@ -101,6 +120,7 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { 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 { @@ -119,6 +139,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] @@ -131,6 +152,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": @@ -143,6 +165,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 From 431f9e4f5d1969da300e70d0df22ff51b3e81803 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 9 Feb 2026 19:13:46 +0100 Subject: [PATCH 38/60] add comments to fuctions. make sure priority ranges form 0-4 --- pkg/cli/commands/completion.go | 1 + pkg/cli/commands/create.go | 5 +++-- pkg/cli/commands/delete.go | 10 +++++++--- pkg/cli/commands/root.go | 1 + 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/cli/commands/completion.go b/pkg/cli/commands/completion.go index af34c20..77d3fcf 100644 --- a/pkg/cli/commands/completion.go +++ b/pkg/cli/commands/completion.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" ) +// Variables for completion options and functions. var ( typeOptions = []string{"bug", "feature", "task"} statusOptions = []string{"open", "closed", "in_progress"} diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index 95e9d53..84504db 100644 --- a/pkg/cli/commands/create.go +++ b/pkg/cli/commands/create.go @@ -65,6 +65,8 @@ func runCreateCmd(cmd *cobra.Command, args []string) error { 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( @@ -95,7 +97,6 @@ func runCreateInteractive() error { huh.NewOption("2", 2), huh.NewOption("3", 3), huh.NewOption("4", 4), - huh.NewOption("5", 5), ).Value(&createFlags.priority).Title("Priority"), ).Title("Create New Issue").WithTheme(huh.ThemeBase()), ) @@ -109,7 +110,7 @@ func init() { 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-5)") + createCmd.Flags().IntVarP(&createFlags.priority, "priority", "p", 0, "Issue priority(0-4)") createCmd.RegisterFlagCompletionFunc("type", completionFunc(typeOptions)) createCmd.RegisterFlagCompletionFunc("status", completionFunc(statusOptions)) diff --git a/pkg/cli/commands/delete.go b/pkg/cli/commands/delete.go index 4884464..a684e53 100644 --- a/pkg/cli/commands/delete.go +++ b/pkg/cli/commands/delete.go @@ -11,9 +11,11 @@ import ( ) // Variables for delete command flag. -var confirmDelete bool -var deleteIDs []string -var deleteInteractive bool +var ( + confirmDelete bool + deleteIDs []string + deleteInteractive bool +) // deleteCmd represents the delete command. var deleteCmd = &cobra.Command{ @@ -78,6 +80,8 @@ 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() error { options := []huh.Option[string]{} diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index 2115fc1..9e18856 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -14,6 +14,7 @@ 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 From 9d6c3fe797e2b0234b723197d969b943a2f16301 Mon Sep 17 00:00:00 2001 From: vb Date: Sat, 7 Feb 2026 15:33:09 +0100 Subject: [PATCH 39/60] adding update feature to cli --- pkg/cli/commands/update.go | 126 ++++++++++++++++++++++++++++++++++++ pkg/cli/repl/suggestions.go | 22 +++++++ 2 files changed, 148 insertions(+) create mode 100644 pkg/cli/commands/update.go diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go new file mode 100644 index 0000000..6a262d6 --- /dev/null +++ b/pkg/cli/commands/update.go @@ -0,0 +1,126 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + updateDescription string + updateStatus string + updateType string + updatePriority int + updateTitle string +) + +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 init() { + updateCmd.Flags().StringVar(&updateTitle, "title", "", "New issue title") + updateCmd.Flags().StringVarP(&updateDescription, "desc", "d", "", "New issue description") + updateCmd.Flags().StringVarP(&updateStatus, "status", "s", "", "New issue status(open, closed, in_progress)") + updateCmd.Flags().StringVarP(&updateType, "type", "", "", "New issue type(bug, feature, task)") + updateCmd.Flags().IntVarP(&updatePriority, "priority", "p", -1, "New issue priority(0-5)") + + updateCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"bug", "feature", "task"}, cobra.ShellCompDirectiveDefault + }) + + updateCmd.RegisterFlagCompletionFunc("status", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"open", "closed", "in_progress"}, cobra.ShellCompDirectiveDefault + }) + + updateCmd.RegisterFlagCompletionFunc("priority", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"0", "1", "2", "3", "4", "5"}, cobra.ShellCompDirectiveDefault + }) + + rootCmd.AddCommand(updateCmd) +} + +func runUpdateCmd(cmd *cobra.Command, args []string) error { + issueID := args[0] + + updates := make(map[string]interface{}) + + if cmd.Flags().Changed("title") { + if updateTitle == "" { + return fmt.Errorf("issue title cannot be empty") + } + updates["title"] = updateTitle + } + + if cmd.Flags().Changed("desc") { + updates["description"] = updateDescription + } + + if cmd.Flags().Changed("status") { + updates["status"] = updateStatus + } + + if cmd.Flags().Changed("type") { + updates["issue_type"] = updateType + } + + if cmd.Flags().Changed("priority") { + updates["priority"] = updatePriority + } + + if len(updates) == 0 { + return fmt.Errorf("no updates specified") + } + + 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) + } + + 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) + } + + str := fmt.Sprintf("Updated issue with ID: %s\n", issueID) + + if updatedIssue.Title != "" { + str += fmt.Sprintf("Title: %s\n", updatedIssue.Title) + } + + if updatedIssue.Description != "" { + str += fmt.Sprintf("Description: %s\n", updatedIssue.Description) + } + + if updatedIssue.Status != "" { + str += fmt.Sprintf("Status: %s\n", updatedIssue.Status) + } + + if updatedIssue.IssueType != "" { + str += fmt.Sprintf("Type: %s\n", updatedIssue.IssueType) + } + + if updatedIssue.Priority != 0 { + str += fmt.Sprintf("Priority: %d\n", updatedIssue.Priority) + } + + fmt.Print(str) + + return nil +} diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index bc347b2..4c38f8a 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -24,6 +24,7 @@ var baseSuggestions = []prompt.Suggest{ {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"}, } @@ -36,6 +37,14 @@ var createFlags = []prompt.Suggest{ {Text: "--priority", Description: "Issue priority (0-5)"}, } +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"}, @@ -104,6 +113,19 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { return issueIDSuggestions(lastWord, len(words) >= 2) } + // Update/edit: suggest issue IDs when typing the ID, flags after ID is provided + if cmd == "update" || cmd == "edit" { + if len(words) >= 2 && (lastWord == "" || strings.HasPrefix(lastWord, "-")) { + // ID already provided (trailing space) or typing a flag - suggest flags + flags := updateFlags + if lastWord == "" { + return flags + } + return filterByPrefix(flags, lastWord) + } + return issueIDSuggestions(lastWord, len(words) >= 2) + } + var flags []prompt.Suggest switch cmd { case "create", "add": From 7d0877dfb00d3a7dd22b63dcbdf12a8987b1c561 Mon Sep 17 00:00:00 2001 From: vb Date: Sat, 7 Feb 2026 15:39:05 +0100 Subject: [PATCH 40/60] fix --- pkg/cli/repl/suggestions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index 4c38f8a..c62e499 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -116,7 +116,7 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { // Update/edit: suggest issue IDs when typing the ID, flags after ID is provided if cmd == "update" || cmd == "edit" { if len(words) >= 2 && (lastWord == "" || strings.HasPrefix(lastWord, "-")) { - // ID already provided (trailing space) or typing a flag - suggest flags + // ID already provided (trailing space) or typing a flag -> suggest update flags flags := updateFlags if lastWord == "" { return flags From 742daef89268e6309005daafa2eec1e3b007a75f Mon Sep 17 00:00:00 2001 From: viljarb0 <156418063+viljarb0@users.noreply.github.com> Date: Sun, 8 Feb 2026 09:43:54 +0100 Subject: [PATCH 41/60] Update pkg/cli/commands/update.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/commands/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go index 6a262d6..03f5754 100644 --- a/pkg/cli/commands/update.go +++ b/pkg/cli/commands/update.go @@ -116,7 +116,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { str += fmt.Sprintf("Type: %s\n", updatedIssue.IssueType) } - if updatedIssue.Priority != 0 { + if cmd.Flags().Changed("priority") { str += fmt.Sprintf("Priority: %d\n", updatedIssue.Priority) } From c6f5363ae8f56afa1a85a65bfadfca816bd9a654 Mon Sep 17 00:00:00 2001 From: viljarb0 <156418063+viljarb0@users.noreply.github.com> Date: Sun, 8 Feb 2026 09:44:12 +0100 Subject: [PATCH 42/60] Update pkg/cli/commands/update.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/commands/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go index 03f5754..81c8cec 100644 --- a/pkg/cli/commands/update.go +++ b/pkg/cli/commands/update.go @@ -120,7 +120,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { str += fmt.Sprintf("Priority: %d\n", updatedIssue.Priority) } - fmt.Print(str) + cmd.Print(str) return nil } From fa6b57e6c0b706e61a8b0089fff904e49caabe55 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 9 Feb 2026 20:28:32 +0100 Subject: [PATCH 43/60] refactor and clean up update command --- pkg/cli/commands/update.go | 138 +++++++++++++++---------------------- 1 file changed, 54 insertions(+), 84 deletions(-) diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go index 81c8cec..a93bc36 100644 --- a/pkg/cli/commands/update.go +++ b/pkg/cli/commands/update.go @@ -3,16 +3,11 @@ package commands import ( "fmt" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/spf13/cobra" ) -var ( - updateDescription string - updateStatus string - updateType string - updatePriority int - updateTitle string -) +var updateFlags Flags var updateCmd = &cobra.Command{ Use: "update [issue ID]", @@ -25,67 +20,17 @@ var updateCmd = &cobra.Command{ ValidArgsFunction: completeIssues, } -func init() { - updateCmd.Flags().StringVar(&updateTitle, "title", "", "New issue title") - updateCmd.Flags().StringVarP(&updateDescription, "desc", "d", "", "New issue description") - updateCmd.Flags().StringVarP(&updateStatus, "status", "s", "", "New issue status(open, closed, in_progress)") - updateCmd.Flags().StringVarP(&updateType, "type", "", "", "New issue type(bug, feature, task)") - updateCmd.Flags().IntVarP(&updatePriority, "priority", "p", -1, "New issue priority(0-5)") - - updateCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"bug", "feature", "task"}, cobra.ShellCompDirectiveDefault - }) - - updateCmd.RegisterFlagCompletionFunc("status", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"open", "closed", "in_progress"}, cobra.ShellCompDirectiveDefault - }) - - updateCmd.RegisterFlagCompletionFunc("priority", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"0", "1", "2", "3", "4", "5"}, cobra.ShellCompDirectiveDefault - }) - - rootCmd.AddCommand(updateCmd) -} - func runUpdateCmd(cmd *cobra.Command, args []string) error { issueID := args[0] - updates := make(map[string]interface{}) - - if cmd.Flags().Changed("title") { - if updateTitle == "" { - return fmt.Errorf("issue title cannot be empty") - } - updates["title"] = updateTitle - } - - if cmd.Flags().Changed("desc") { - updates["description"] = updateDescription - } - - if cmd.Flags().Changed("status") { - updates["status"] = updateStatus - } - - if cmd.Flags().Changed("type") { - updates["issue_type"] = updateType - } - - if cmd.Flags().Changed("priority") { - updates["priority"] = updatePriority - } - - if len(updates) == 0 { - return fmt.Errorf("no updates specified") - } - issue, err := svc.Beads.GetIssue(cmd.Context(), issueID) - if err != nil { + if err != nil || issue == 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") @@ -98,29 +43,54 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("error getting updated issue: %w", err) } - str := fmt.Sprintf("Updated issue with ID: %s\n", issueID) - - if updatedIssue.Title != "" { - str += fmt.Sprintf("Title: %s\n", updatedIssue.Title) - } - - if updatedIssue.Description != "" { - str += fmt.Sprintf("Description: %s\n", updatedIssue.Description) - } - - if updatedIssue.Status != "" { - str += fmt.Sprintf("Status: %s\n", updatedIssue.Status) - } - - if updatedIssue.IssueType != "" { - str += fmt.Sprintf("Type: %s\n", updatedIssue.IssueType) - } - - if cmd.Flags().Changed("priority") { - str += fmt.Sprintf("Priority: %d\n", updatedIssue.Priority) - } - - cmd.Print(str) + 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 +} From 155d9d79ae992387e01bc6e99fbd6405031a639a Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 9 Feb 2026 20:28:42 +0100 Subject: [PATCH 44/60] refine suggestions a bit --- pkg/cli/repl/suggestions.go | 47 ++++++++++++++----------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index c62e499..ba79385 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -37,6 +37,7 @@ var createFlags = []prompt.Suggest{ {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"}, @@ -88,6 +89,18 @@ var isIDCommand = map[string]bool{ "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, } // commandSuggestions returns a list of prompt suggestions based on the current input words. @@ -106,39 +119,15 @@ func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest { return filterByPrefix(values, lastWord) } + flags := commandFlags[cmd] + if isIDCommand[cmd] { - // 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) - } - - // Update/edit: suggest issue IDs when typing the ID, flags after ID is provided - if cmd == "update" || cmd == "edit" { - if len(words) >= 2 && (lastWord == "" || strings.HasPrefix(lastWord, "-")) { - // ID already provided (trailing space) or typing a flag -> suggest update flags - flags := updateFlags - if lastWord == "" { - return flags - } - return filterByPrefix(flags, lastWord) + if len(words) < 2 && !strings.HasPrefix(lastWord, "-") { + return issueIDSuggestions(lastWord, true) } - return issueIDSuggestions(lastWord, len(words) >= 2) + 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) } From bb096ff19da719dbf18ad70bdd6bab13d7c206f0 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:42:44 +0100 Subject: [PATCH 45/60] Update pkg/cli/repl/suggestions.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/repl/suggestions.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index 579ed2f..ba79385 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -25,7 +25,6 @@ var baseSuggestions = []prompt.Suggest{ {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: "update", Description: "Update an existing issue by ID"}, {Text: "describe", Description: "Get issue details by ID"}, {Text: "list", Description: "List all issues"}, } From c773e61f4e5e87f8bb9c43caf964d9b6af1672e1 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 00:04:13 +0100 Subject: [PATCH 46/60] implement suggestions from copilot --- pkg/cli/commands/close.go | 21 ++++++++++++--------- pkg/cli/commands/create.go | 18 ++++++++---------- pkg/cli/commands/delete.go | 10 +++++----- pkg/cli/commands/update.go | 6 +++++- pkg/cli/repl/suggestions.go | 10 ++++++++++ 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/pkg/cli/commands/close.go b/pkg/cli/commands/close.go index 4bfecaa..b4ff882 100644 --- a/pkg/cli/commands/close.go +++ b/pkg/cli/commands/close.go @@ -2,7 +2,6 @@ package commands import ( "fmt" - "strings" "github.com/charmbracelet/huh" "github.com/spf13/cobra" @@ -11,19 +10,21 @@ import ( // 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`, - ValidArgsFunction: completeIssues, + 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 := strings.Join(args, " ") + closeID := args[0] if closeID == "" { return fmt.Errorf("issue ID cannot be empty") @@ -40,8 +41,10 @@ func runCloseCmd(cmd *cobra.Command, args []string) error { } // Ask for closing reason - huh.NewInput().Value(&issue.CloseReason). - Title("Reason for closing the issue?").WithTheme(huh.ThemeBase()).Run() + 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, "", "") diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index 84504db..538ed27 100644 --- a/pkg/cli/commands/create.go +++ b/pkg/cli/commands/create.go @@ -72,34 +72,32 @@ func runCreateInteractive() error { huh.NewGroup( huh.NewInput().Value(&createFlags.title).Title("Title"), - huh.NewText().Value(&createFlags.description).Title("Description"), - ).Title("Issue Details"), + huh.NewText().Value(&createFlags.description).Title("Description")), huh.NewGroup( - huh.NewSelect[string](). + huh.NewSelect[string]().Title("Status"). Options( huh.NewOption("Open", "open"), huh.NewOption("Closed", "closed"), huh.NewOption("In Progress", "in_progress"), - ).Value(&createFlags.status).Title("Status"), + ).Value(&createFlags.status), - huh.NewSelect[string](). + huh.NewSelect[string]().Title("Type"). Options( huh.NewOption("Bug", "bug"), huh.NewOption("Feature", "feature"), huh.NewOption("Task", "task"), - ).Value(&createFlags.issueType).Title("Type"), + ).Value(&createFlags.issueType), - huh.NewSelect[int](). + 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).Title("Priority"), - ).Title("Create New Issue").WithTheme(huh.ThemeBase()), - ) + ).Value(&createFlags.priority), + )).WithTheme(huh.ThemeBase16()) return form.Run() } diff --git a/pkg/cli/commands/delete.go b/pkg/cli/commands/delete.go index a684e53..b96e659 100644 --- a/pkg/cli/commands/delete.go +++ b/pkg/cli/commands/delete.go @@ -36,7 +36,7 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { deleteID := strings.Join(args, " ") if deleteInteractive { - if err := runDeleteInteractive(); err != nil { + if err := runDeleteInteractive(cmd.Context()); err != nil { return err } return nil @@ -82,10 +82,10 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { // runDeleteInteractive runs the interactive mode for deleting issues, // allowing users to select multiple issues for deletion. -func runDeleteInteractive() error { +func runDeleteInteractive(ctx context.Context) error { options := []huh.Option[string]{} - issues, err := svc.Beads.SearchIssues(context.Background(), "", models.IssueFilter{}) + issues, err := svc.Beads.SearchIssues(ctx, "", models.IssueFilter{}) if err != nil { return fmt.Errorf("error fetching issues: %w", err) } @@ -97,7 +97,7 @@ func runDeleteInteractive() error { form := huh.NewForm( huh.NewGroup( - huh.NewMultiSelect[string]().Value(&deleteIDs). + huh.NewMultiSelect[string](). Options(options...).Value(&deleteIDs). Title("Select issues to delete"))).WithTheme(huh.ThemeBase()) @@ -110,7 +110,7 @@ func runDeleteInteractive() error { } for _, id := range deleteIDs { - err := svc.Beads.DeleteIssue(context.Background(), id) + err := svc.Beads.DeleteIssue(ctx, id) if err != nil { return fmt.Errorf("error deleting issue with ID %s: %w", id, err) } diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go index a93bc36..099a08c 100644 --- a/pkg/cli/commands/update.go +++ b/pkg/cli/commands/update.go @@ -24,10 +24,14 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { issueID := args[0] issue, err := svc.Beads.GetIssue(cmd.Context(), issueID) - if err != nil || issue == nil { + 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) diff --git a/pkg/cli/repl/suggestions.go b/pkg/cli/repl/suggestions.go index ba79385..e99c681 100644 --- a/pkg/cli/repl/suggestions.go +++ b/pkg/cli/repl/suggestions.go @@ -31,6 +31,7 @@ var baseSuggestions = []prompt.Suggest{ // 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)"}, @@ -56,6 +57,11 @@ 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"}, @@ -101,6 +107,10 @@ var commandFlags = map[string][]prompt.Suggest{ "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. From ed907238947834ac19cf2085e867e4a9f6c16e2e Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 10:50:27 +0100 Subject: [PATCH 47/60] core: upgrade deps --- cmd/beads_viewer/main.go | 53 ------------ cmd/survey.go | 4 - go.mod | 78 +++++++---------- go.sum | 182 ++++++++------------------------------- 4 files changed, 68 insertions(+), 249 deletions(-) delete mode 100644 cmd/beads_viewer/main.go 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.go b/cmd/survey.go index 044a13a..5bd99c5 100644 --- a/cmd/survey.go +++ b/cmd/survey.go @@ -5,10 +5,6 @@ import ( "fmt" "os" - "context" - "fmt" - "os" - "github.com/LazyBachelor/LazyPM/pkg" "github.com/LazyBachelor/LazyPM/pkg/cli" "github.com/LazyBachelor/LazyPM/pkg/cli/repl" diff --git a/go.mod b/go.mod index 8dee4d1..5419fc8 100644 --- a/go.mod +++ b/go.mod @@ -3,83 +3,76 @@ module github.com/LazyBachelor/LazyPM go 1.25.6 require ( - 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/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 @@ -90,24 +83,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 dba30f8..d774a58 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,5 @@ 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/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= @@ -15,16 +8,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= @@ -33,8 +16,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= @@ -49,28 +30,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= @@ -83,20 +60,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= @@ -109,28 +82,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= @@ -155,22 +114,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= @@ -181,8 +139,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= @@ -191,8 +147,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= @@ -217,8 +171,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= @@ -231,97 +185,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= From 41bde2d1f413c7dbe661e8dc8c9e3543303591e1 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 17:49:08 +0100 Subject: [PATCH 48/60] move issue views to dashboard --- pkg/tui/views/issue/view.go | 67 ------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 pkg/tui/views/issue/view.go diff --git a/pkg/tui/views/issue/view.go b/pkg/tui/views/issue/view.go deleted file mode 100644 index ff80d86..0000000 --- a/pkg/tui/views/issue/view.go +++ /dev/null @@ -1,67 +0,0 @@ -package issue - -import ( - "fmt" - - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" -) - -type Model struct { - Viewport viewport.Model - ID string - Title string - Description string - Status string - IssueType string - Width, Height int - ready bool -} - -func NewIssueView(issue Model) Model { - return Model{ - ID: issue.ID, - Title: issue.Title, - Description: issue.Description, - Status: issue.Status, - IssueType: issue.IssueType, - Width: issue.Width, - Height: issue.Height, - Viewport: viewport.New(issue.Width, issue.Height), - ready: false, - } -} - -func (m *Model) SetSize(width, height int) { - m.Width = width - m.Height = height -} - -func (m Model) Init() tea.Cmd { - return nil -} - -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - - case tea.WindowSizeMsg: - m.SetSize(msg.Width, msg.Height) - m.Viewport.Width = m.Width - m.Viewport.Height = m.Height - m.ready = true - } - - if !m.ready { - return m, nil - } - - content := fmt.Sprintf("ID: %s\nTitle: %s\nDescription: %s\nStatus: %s\nType: %s", - m.ID, m.Title, m.Description, m.Status, m.IssueType) - - m.Viewport.SetContent(content) - return m, nil -} - -func (m Model) View() string { - return fmt.Sprintf("%s", m.Viewport.View()) -} From 6ee8e678587bef12c2e303d9e5ee8424a6d8fe79 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 17:49:22 +0100 Subject: [PATCH 49/60] move delegate to issue_list --- pkg/tui/views/dashboard/deligate.go | 38 ----------------------------- 1 file changed, 38 deletions(-) delete mode 100644 pkg/tui/views/dashboard/deligate.go diff --git a/pkg/tui/views/dashboard/deligate.go b/pkg/tui/views/dashboard/deligate.go deleted file mode 100644 index f446e59..0000000 --- a/pkg/tui/views/dashboard/deligate.go +++ /dev/null @@ -1,38 +0,0 @@ -package dashboard - -import ( - "fmt" - "io" - - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" -) - -type IssueListDelegate struct{} - -func (d IssueListDelegate) Height() int { return 2 } -func (d IssueListDelegate) Spacing() int { return 1 } -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 - } - - id := issue.ID - title := issue.Title() - description := issue.Description() - - str := fmt.Sprintf("ID: %s\t%s\nDescription:\t%s", id, title, description) - - fn := styles.ItemStyle.Render - if index == m.Index() { - fn = func(s ...string) string { - return styles.SelectedItemStyle.Render(s...) - } - } - - fmt.Fprint(w, fn(str)) -} From 4129cfacb48f3761cd2f24e76fac8fb48fb726d8 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 17:49:39 +0100 Subject: [PATCH 50/60] add style definitions for better modifiability --- pkg/tui/styles/styles.go | 90 +++++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go index e7901b3..53b3dfc 100644 --- a/pkg/tui/styles/styles.go +++ b/pkg/tui/styles/styles.go @@ -3,28 +3,78 @@ package styles import "github.com/charmbracelet/lipgloss" var ( - AppStyle = lipgloss.NewStyle().Padding(3, 3) + Primary = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} + Secondary = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} - TitleStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("12")). - Padding(0, 1).Bold(true).Border(lipgloss.NormalBorder()) + Success = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} + Warning = lipgloss.AdaptiveColor{Light: "#F59E0B", Dark: "#F59E0B"} + Error = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"} - SelectedItemStyle = lipgloss.NewStyle(). - Border(lipgloss.NormalBorder(), false, false, false, true). - Foreground(lipgloss.Color("2")). - Padding(0, 0, 0, 1) + PrimaryText = lipgloss.AdaptiveColor{Light: "#1A1A1A", Dark: "#E0E0E0"} + SecondaryText = lipgloss.AdaptiveColor{Light: "#666666", Dark: "#999999"} + FaintText = lipgloss.AdaptiveColor{Light: "#999999", Dark: "#666666"} - ItemStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("7")). - Padding(0, 0, 0, 2) + PrimaryBorder = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} + SecondaryBorder = lipgloss.AdaptiveColor{Light: "#CCCCCC", Dark: "#444444"} - IssueStyle = lipgloss.NewStyle(). - Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("8")). - Padding(1) - - FocusedIssueStyle = lipgloss.NewStyle(). - Border(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("2")). - Padding(1) + 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) +} From 815977a208a21ff3358e85604cceab7c78eef77b Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 17:49:53 +0100 Subject: [PATCH 51/60] add custom help bar --- pkg/tui/views/dashboard/help_bar.go | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 pkg/tui/views/dashboard/help_bar.go diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go new file mode 100644 index 0000000..6593d8c --- /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.showAll { + return 3 + } + return 1 +} + +func (h HelpBar) IsExpanded() bool { + return h.showAll +} + +func (h *HelpBar) ToggleHelp() { + h.showAll = !h.showAll +} From ca8fc4022f22fd9aa1fad5cc08bbea3f6130b9c4 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 17:50:02 +0100 Subject: [PATCH 52/60] add custom header --- pkg/tui/views/dashboard/header.go | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 pkg/tui/views/dashboard/header.go diff --git a/pkg/tui/views/dashboard/header.go b/pkg/tui/views/dashboard/header.go new file mode 100644 index 0000000..9810188 --- /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 2 +} From 87a1a18f70053fcc0297fa170a523db10ec0fa8c Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 20:10:45 +0100 Subject: [PATCH 53/60] improving issue list --- pkg/tui/views/dashboard/issue_list.go | 257 ++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 pkg/tui/views/dashboard/issue_list.go diff --git a/pkg/tui/views/dashboard/issue_list.go b/pkg/tui/views/dashboard/issue_list.go new file mode 100644 index 0000000..fbabee7 --- /dev/null +++ b/pkg/tui/views/dashboard/issue_list.go @@ -0,0 +1,257 @@ +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()) + + listIssues := []ListIssue{} + for _, issue := range issues { + listIssues = append(listIssues, ListIssue{Issue: issue}) + } + + if err != nil { + listIssues = []ListIssue{} + } + + 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 "" + } +} From ee3e64a88329b57e789e892c25b9671341c0c1ef Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 20:10:58 +0100 Subject: [PATCH 54/60] improving issue details view --- pkg/tui/views/dashboard/issue_detail.go | 95 +++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 pkg/tui/views/dashboard/issue_detail.go 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) +} From 19b32dca1bfef0b8eafa750e5c79563a8c483a9b Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 20:11:36 +0100 Subject: [PATCH 55/60] add dashboard --- pkg/tui/views/dashboard/keys.go | 30 ++++-------- pkg/tui/views/dashboard/model.go | 73 +++++++++++++++++++++++------ pkg/tui/views/dashboard/update.go | 62 ++++++++----------------- pkg/tui/views/dashboard/view.go | 77 ++++++++----------------------- pkg/tui/views/views.go | 2 +- 5 files changed, 106 insertions(+), 138 deletions(-) diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 6a832ea..dfdeb03 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -41,34 +41,22 @@ var defaultDashboardKeyMap = DashboardKeyMap{ ), } -func (m DashboardKeyMap) ShortHelp() []key.Binding { - return []key.Binding{m.ScrollDown, m.ScrollUp, m.Quit, m.Help} -} - -func (m DashboardKeyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {m.SelectIssue, m.BackToList}, - {m.ScrollUp, m.ScrollDown}, - {m.Help, m.Quit}, - } -} - func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { var cmd tea.Cmd switch { case key.Matches(msg, d.keyMap.Help): - d.help.ShowAll = !d.help.ShowAll + d.helpBar.ToggleHelp() case key.Matches(msg, d.keyMap.Quit): return tea.Quit - case !d.focusedOnIssue && key.Matches(msg, d.keyMap.SelectIssue): - d.focusedOnIssue = true - case d.focusedOnIssue && key.Matches(msg, d.keyMap.BackToList): - d.focusedOnIssue = false - case d.focusedOnIssue && key.Matches(msg, d.keyMap.ScrollUp): - d.issueView.Viewport.ScrollUp(1) - case d.focusedOnIssue && key.Matches(msg, d.keyMap.ScrollDown): - d.issueView.Viewport.ScrollDown(1) + 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 index d327bd1..702cb8f 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -1,26 +1,69 @@ package dashboard import ( - "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/service" - "github.com/LazyBachelor/LazyPM/pkg/tui/views/issue" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" ) type Model struct { - help help.Model - issueView issue.Model - issueList list.Model - keyMap DashboardKeyMap - svc *service.Services - focusedOnIssue bool + header Header + issueList IssueList + issueDetail IssueDetail + helpBar HelpBar + keyMap DashboardKeyMap + svc *service.Services + width int + height int + focusedPane int // 0 = list, 1 = detail } -type ListIssue struct { - models.Issue +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 (i ListIssue) Title() string { return i.Issue.Title } -func (i ListIssue) Description() string { return i.Issue.Description } -func (i ListIssue) FilterValue() string { return i.Issue.ID + " " + i.Issue.Title } +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 index b8dd033..b2f7d9a 100644 --- a/pkg/tui/views/dashboard/update.go +++ b/pkg/tui/views/dashboard/update.go @@ -1,60 +1,34 @@ package dashboard import ( - "fmt" - - "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) -func (d Model) Update(m tea.Msg) (tea.Model, tea.Cmd) { - switch msg := m.(type) { +func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { case tea.KeyMsg: - if d.issueList.FilterState() == list.Filtering { - break + if m.issueList.FilterState() == 1 { + cmd, _ := m.issueList.Update(msg) + return m, cmd } - cmd := d.handleKeyMsg(msg) + + cmd := m.handleKeyMsg(msg) if cmd != nil { - return d, cmd + return m, cmd } + case tea.WindowSizeMsg: - d.updateSizes(msg.Width, msg.Height) + m.width = msg.Width + m.height = msg.Height + return m, nil } - var cmd tea.Cmd - oldIndex := d.issueList.Index() - d.issueList, cmd = d.issueList.Update(m) - - if d.issueList.Index() != oldIndex { - d.updateIssueView() + cmd, changed := m.issueList.Update(msg) + if changed { + if selected := m.issueList.SelectedItem(); selected.ID != "" { + m.issueDetail.SetIssue(selected.Issue) + } } - return d, cmd -} - -func (d *Model) updateIssueView() { - if item, ok := d.issueList.SelectedItem().(ListIssue); ok { - d.issueView.ID = item.ID - d.issueView.Title = item.Issue.Title - d.issueView.Description = item.Issue.Description - d.issueView.Status = string(item.Issue.Status) - d.issueView.IssueType = string(item.Issue.IssueType) - - content := fmt.Sprintf("ID: %s\nTitle: %s\nDescription: %s\nStatus: %s\nType: %s", - d.issueView.ID, d.issueView.Title, d.issueView.Description, d.issueView.Status, d.issueView.IssueType) - - d.issueView.Viewport.SetContent(content) - } -} - -func (d *Model) updateSizes(width, height int) { - listWidth := width / 2 - issueWidth := width - listWidth - - w, h := styles.AppStyle.GetFrameSize() - d.issueList.SetSize(listWidth-w, height-h) - d.issueView.SetSize(issueWidth, height-h) - d.issueView.Viewport.Width = issueWidth - d.issueView.Viewport.Height = height - h + return m, cmd } diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 6700adf..18d207e 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -1,73 +1,36 @@ package dashboard import ( - "context" - - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - - "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" - "github.com/LazyBachelor/LazyPM/pkg/tui/views/issue" + "github.com/charmbracelet/lipgloss" ) -func NewDashboard(svc *service.Services) Model { - - issues, err := svc.Beads.AllIssues(context.Background()) - - listIssues := []ListIssue{} - for _, issue := range issues { - listIssues = append(listIssues, ListIssue{Issue: issue}) +func (m *Model) View() string { + if m.width == 0 || m.height == 0 { + return "Loading..." } - if err != nil { - listIssues = []ListIssue{} - } + m.helpBar.SetWidth(m.width) - items := make([]list.Item, len(listIssues)) - for i, issue := range listIssues { - items[i] = issue - } + header := m.header.View(m.width) + headerHeight := m.header.Height() - issueList := list.New(items, IssueListDelegate{}, 0, 0) + bottomView := m.helpBar.View() + bottomHeight := m.helpBar.Height() - issueList.Title = "Issues" - issueList.Styles.Title = styles.TitleStyle - issueList.SetShowHelp(false) + contentHeight := m.height - headerHeight - bottomHeight - issueView := issue.NewIssueView(issue.Model{}) + totalContentWidth := m.width - 1 + listWidth := totalContentWidth * styles.ListViewRatio / 100 + detailWidth := totalContentWidth - listWidth - m := Model{ - help: help.New(), - issueView: issueView, - issueList: issueList, - keyMap: defaultDashboardKeyMap, - svc: svc, - } + m.issueList.SetSize(listWidth, contentHeight) + m.issueDetail.SetSize(detailWidth, contentHeight) - m.updateIssueView() + listView := m.issueList.View() + detailView := m.issueDetail.View() - return m -} - -func (d Model) Init() tea.Cmd { - return nil -} - -func (d Model) View() string { - issueView := d.issueView.View() - - if d.focusedOnIssue { - issueView = styles.FocusedIssueStyle.Render(issueView) - } else { - issueView = styles.IssueStyle.Render(issueView) - } - - help := d.help.View(d.keyMap) - - str := lipgloss.JoinHorizontal(lipgloss.Left, styles.AppStyle.Render(d.issueList.View()), issueView) + "\n" + help - - return str + 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 index a246942..c7bc138 100644 --- a/pkg/tui/views/views.go +++ b/pkg/tui/views/views.go @@ -5,6 +5,6 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard" ) -func NewDashboardView(svc *service.Services) dashboard.Model { +func NewDashboardView(svc *service.Services) *dashboard.Model { return dashboard.NewDashboard(svc) } From d0ab541ec449ba837cb4a66da7e683255b8ed780 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:18:55 +0100 Subject: [PATCH 56/60] Update pkg/tui/views/dashboard/update.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/tui/views/dashboard/update.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/tui/views/dashboard/update.go b/pkg/tui/views/dashboard/update.go index b2f7d9a..7fe15cc 100644 --- a/pkg/tui/views/dashboard/update.go +++ b/pkg/tui/views/dashboard/update.go @@ -2,12 +2,13 @@ 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() == 1 { + if m.issueList.FilterState() == list.Filtering { cmd, _ := m.issueList.Update(msg) return m, cmd } From 14c4509942671f93da6601fec5f803faf648cab6 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:22:23 +0100 Subject: [PATCH 57/60] Update pkg/tui/views/dashboard/help_bar.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/tui/views/dashboard/help_bar.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go index 6593d8c..c030ab1 100644 --- a/pkg/tui/views/dashboard/help_bar.go +++ b/pkg/tui/views/dashboard/help_bar.go @@ -79,10 +79,10 @@ func (h HelpBar) fullHelp() string { } func (h HelpBar) Height() int { - if h.showAll { - return 3 + if h.width == 0 { + return 0 } - return 1 + return lipgloss.Height(h.View()) } func (h HelpBar) IsExpanded() bool { From ca13b4bc730a4aa2ced49c6a020730e99fb9d929 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:23:42 +0100 Subject: [PATCH 58/60] Update pkg/tui/views/dashboard/header.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/tui/views/dashboard/header.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tui/views/dashboard/header.go b/pkg/tui/views/dashboard/header.go index 9810188..fcd2b7e 100644 --- a/pkg/tui/views/dashboard/header.go +++ b/pkg/tui/views/dashboard/header.go @@ -28,5 +28,5 @@ func (h Header) View(width int) string { } func (h Header) Height() int { - return 2 + return 1 } From f3f42ddc8a80ea544ecd61c3d6a08b56a767818b Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 20:31:13 +0100 Subject: [PATCH 59/60] return early empty issue list if no issues found --- pkg/tui/views/dashboard/issue_list.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/tui/views/dashboard/issue_list.go b/pkg/tui/views/dashboard/issue_list.go index fbabee7..18504c1 100644 --- a/pkg/tui/views/dashboard/issue_list.go +++ b/pkg/tui/views/dashboard/issue_list.go @@ -76,16 +76,15 @@ func renderHeaders(cols []TableColumn) string { 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}) } - if err != nil { - listIssues = []ListIssue{} - } - items := make([]list.Item, len(listIssues)) for i, issue := range listIssues { items[i] = issue From 478e81b42dde937a9fdbfe66549c6709f1ee7bd8 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 10 Feb 2026 20:40:33 +0100 Subject: [PATCH 60/60] update readme --- README.md | 82 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 26 deletions(-) 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