diff --git a/cmd/pm/main.go b/cmd/pm/main.go index d067ef4..3ac39fd 100644 --- a/cmd/pm/main.go +++ b/cmd/pm/main.go @@ -3,20 +3,12 @@ package main import ( "context" + "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/cli" ) func main() { - config := cli.CLIConfig{ - RootCmd: "pm", - IssuePrefix: "pm", - BeadsDBPath: "./.pm/db.db", - StatisticsStoragePath: "./.pm/stats.json", - } - - cli := cli.NewCli() - - if err := cli.Run(context.Background(), config); err != nil { + if err := cli.NewCli().Run(context.Background(), service.BaseConfig); err != nil { return } } diff --git a/cmd/survey/init.go b/cmd/survey/init.go index 6016132..6587a51 100644 --- a/cmd/survey/init.go +++ b/cmd/survey/init.go @@ -14,19 +14,19 @@ import ( ) func init() { - task.Register("create_issue", func(svc *service.Services) task.Tasker { - return tasks.NewCreateIssueTask(svc) + task.Register("create_issue", func(app *service.App) task.Tasker { + return tasks.NewCreateIssueTask(app) }) - task.Register("coding_task", func(svc *service.Services) task.Tasker { - return tasks.NewCodingTask(svc) + task.Register("coding_task", func(app *service.App) task.Tasker { + return tasks.NewCodingTask(app) }) } -func initTasks(svc *service.Services) []task.Tasker { +func initTasks(app *service.App) []task.Tasker { var taskers []task.Tasker for _, name := range task.List() { - t, err := task.Get(name, svc) + t, err := task.Get(name, app) if err != nil { continue } @@ -36,7 +36,7 @@ func initTasks(svc *service.Services) []task.Tasker { return taskers } -func initializeServices(ctx context.Context) (*service.Services, func(), error) { +func initializeServices(ctx context.Context) (*service.App, func(), error) { return service.NewServices(ctx, tasks.BaseConfig()) } diff --git a/cmd/survey/tasks/base.go b/cmd/survey/tasks/base.go index 1550bd9..ccc8bbe 100644 --- a/cmd/survey/tasks/base.go +++ b/cmd/survey/tasks/base.go @@ -38,17 +38,12 @@ func BaseDetails() taskui.TaskDetails { } } -func BaseConfig() task.TaskConfig { - return task.TaskConfig{ - IssuePrefix: "pm", - WebAddress: ":8080", - BeadsDBPath: "./.pm/db.db", - StatisticsStoragePath: "./.pm/stats.json", - } +func BaseConfig() task.Config { + return service.BaseConfig } -func ClearIssues(svc *service.Services) error { - return svc.Beads.DeleteIssues() +func ClearIssues(app *service.App) error { + return app.Issues.DeleteIssues() } func BaseQuestions(interfaceType task.InterfaceType) taskui.Questions { diff --git a/cmd/survey/tasks/codingTask.go b/cmd/survey/tasks/codingTask.go index 9d3110d..520e7ef 100644 --- a/cmd/survey/tasks/codingTask.go +++ b/cmd/survey/tasks/codingTask.go @@ -23,14 +23,14 @@ Please write your code below this line! ` type CodingTask struct { - svc *service.Services + app *service.App } -func NewCodingTask(svc *service.Services) *CodingTask { - return &CodingTask{svc: svc} +func NewCodingTask(app *service.App) *CodingTask { + return &CodingTask{app: app} } -func (t *CodingTask) Config() task.TaskConfig { +func (t *CodingTask) Config() task.Config { return BaseConfig().WithStatisticsStoragePath("./.pm/coding-task-stats.json") } @@ -59,7 +59,7 @@ func (t *CodingTask) Questions(interfaceType task.InterfaceType) (questions task } func (t *CodingTask) Setup(ctx context.Context) error { - if err := ClearIssues(t.svc); err != nil { + if err := ClearIssues(t.app); err != nil { return err } @@ -71,7 +71,6 @@ func (t *CodingTask) Setup(ctx context.Context) error { } func (t *CodingTask) Validate(ctx context.Context) (bool, error) { - file, err := os.ReadFile("./code.txt") if err != nil { return false, err diff --git a/cmd/survey/tasks/createIssue.go b/cmd/survey/tasks/createIssue.go index f6552d1..ee96e55 100644 --- a/cmd/survey/tasks/createIssue.go +++ b/cmd/survey/tasks/createIssue.go @@ -17,14 +17,14 @@ Assign this task to yourself and start creating the issue. Make sure to fill out all the necessary details, including the title, description, and assignee.` type CreateIssueTask struct { - svc *service.Services + app *service.App } -func NewCreateIssueTask(svc *service.Services) *CreateIssueTask { - return &CreateIssueTask{svc: svc} +func NewCreateIssueTask(app *service.App) *CreateIssueTask { + return &CreateIssueTask{app: app} } -func (t *CreateIssueTask) Config() task.TaskConfig { +func (t *CreateIssueTask) Config() task.Config { return BaseConfig().WithStatisticsStoragePath("./.pm/create-issue-stats.json") } @@ -38,18 +38,18 @@ func (t *CreateIssueTask) Questions(interfaceType task.InterfaceType) taskui.Que func (t *CreateIssueTask) Setup(ctx context.Context) error { // Clear existing issues to ensure a clean state for the task - if err := ClearIssues(t.svc); err != nil { + if err := ClearIssues(t.app); err != nil { return err } issue := models.NewBaseIssue(). WithTitle("Create a New Issue").WithDescription(description).Build() - return t.svc.Beads.CreateIssue(ctx, &issue, "") + return t.app.Issues.CreateIssue(ctx, &issue, "") } func (t *CreateIssueTask) Validate(ctx context.Context) (bool, error) { - issues, err := t.svc.Beads.SearchIssues(ctx, "", models.IssueFilter{}) + issues, err := t.app.Issues.SearchIssues(ctx, "", models.IssueFilter{}) if err != nil { return false, err } diff --git a/cmd/tui/main.go b/cmd/tui/main.go index 824e6aa..b381675 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -3,19 +3,12 @@ package main import ( "context" + "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/tui" ) func main() { - config := tui.TUIConfig{ - StatisticsStoragePath: "./.pm/stats.json", - BeadsDBPath: "./.pm/db.db", - IssuePrefix: "pm", - } - - tui := tui.NewTui() - - if err := tui.Run(context.Background(), config); err != nil { - panic(err) + if err := tui.NewTui().Run(context.Background(), service.BaseConfig); err != nil { + return } } diff --git a/cmd/web/main.go b/cmd/web/main.go index 1cb7518..5bdc103 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -2,25 +2,13 @@ package main import ( "context" - "fmt" - "os" "github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/pkg/web" ) func main() { - web := web.NewWeb() - - config := service.Config{ - WebAddress: "localhost:8080", - BeadsDBPath: "./.pm/db.db", - IssuePrefix: "pm", - StatisticsStoragePath: "./.pm/stats.json", - } - - if err := web.Run(context.Background(), config); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) + if err := web.NewWeb().Run(context.Background(), service.BaseConfig); err != nil { + return } } diff --git a/internal/models/issue.go b/internal/models/issue.go index e751bfa..813f6ac 100644 --- a/internal/models/issue.go +++ b/internal/models/issue.go @@ -18,8 +18,9 @@ func NewBaseIssue() *IssueBuilder { WithID("pm-abc"). WithTitle("Basic Issue"). WithDescription("Basic Description"). - WithIssueType(TypeTask). - WithStatus(StatusOpen) + WithStatus(StatusOpen). + WithIssueType(TypeTask) + } func (b *IssueBuilder) WithID(id string) *IssueBuilder { diff --git a/internal/service/config.go b/internal/service/config.go index 7532ab5..c5cc627 100644 --- a/internal/service/config.go +++ b/internal/service/config.go @@ -8,6 +8,14 @@ type Config struct { StatisticsStoragePath string } +var BaseConfig = Config{ + RootCmd: "pm", + IssuePrefix: "pm", + WebAddress: ":8080", + BeadsDBPath: "./.pm/db.db", + StatisticsStoragePath: "./.pm/stats.json", +} + func (c Config) WithRootCmd(rootCmd string) Config { c.RootCmd = rootCmd return c diff --git a/internal/service/interfaces.go b/internal/service/interfaces.go new file mode 100644 index 0000000..33095ba --- /dev/null +++ b/internal/service/interfaces.go @@ -0,0 +1,28 @@ +package service + +import ( + "context" + "log/slog" + + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/steveyegge/beads" +) + +type App struct { + Config Config + Issues IssueService + Stats StatsService + Logger *slog.Logger +} + +type IssueService interface { + beads.Storage + AllIssues(ctx context.Context) ([]models.Issue, error) + DeleteIssues() error +} + +type StatsService interface { + Load(ctx context.Context) error + Save(ctx context.Context) error + GetStatistics() (models.Statistics, error) +} diff --git a/internal/service/service.go b/internal/service/service.go index 009f3a5..72d72d1 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -2,8 +2,8 @@ package service import ( "context" - "database/sql" "fmt" + "log/slog" "os" "time" @@ -15,14 +15,7 @@ import ( "github.com/steveyegge/beads" ) -type Services struct { - Config Config - DB *sql.DB - Beads *BeadsService - Statistics *StatisticsService -} - -func NewServices(ctx context.Context, config Config) (*Services, func(), error) { +func NewServices(ctx context.Context, config Config) (*App, func(), error) { var cleanupFuncs []func() if !initialized(config.BeadsDBPath) { @@ -47,16 +40,18 @@ func NewServices(ctx context.Context, config Config) (*Services, func(), error) StartTime: time.Now(), }) + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + statSvc, err := NewStatisticsService(statStore) if err != nil { return nil, nil, err } - return &Services{ - DB: beadsSvc.UnderlyingDB(), - Beads: beadsSvc, - Statistics: statSvc, - Config: config, + return &App{ + Issues: beadsSvc, + Stats: statSvc, + Config: config, + Logger: logger, }, func() { runCleanup(cleanupFuncs) }, nil } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 27dd22a..420e37d 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -8,8 +8,8 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/cli/commands" ) -// CLIConfig is an alias for service.Config, used to configure the CLI. -type CLIConfig = service.Config +// Config is an alias for service.Config, used to configure the CLI. +type Config = service.Config type CLI struct{} @@ -18,15 +18,15 @@ func NewCli() *CLI { } // Run initializes the services and executes the CLI commands. -func (c *CLI) Run(ctx context.Context, config CLIConfig) error { - svc, cleanup, err := service.NewServices(ctx, config) +func (c *CLI) Run(ctx context.Context, config Config) error { + app, cleanup, err := service.NewServices(ctx, config) if err != nil { return err } defer cleanup() - commands.SetServices(svc) + commands.SetApp(app) if err := commands.Execute(); err != nil { return err @@ -36,15 +36,15 @@ func (c *CLI) Run(ctx context.Context, config CLIConfig) error { } // RunWithArgs initializes the services and executes the CLI commands with the provided arguments. -func (c *CLI) RunWithArgs(ctx context.Context, config CLIConfig, args []string) error { - svc, cleanup, err := service.NewServices(ctx, config) +func (c *CLI) RunWithArgs(ctx context.Context, config Config, args []string) error { + app, cleanup, err := service.NewServices(ctx, config) if err != nil { return err } defer cleanup() - commands.SetServices(svc) + commands.SetApp(app) if err := commands.ExecuteArgs(args); err != nil { return err diff --git a/pkg/cli/commands/close.go b/pkg/cli/commands/close.go index b4ff882..2bafcbb 100644 --- a/pkg/cli/commands/close.go +++ b/pkg/cli/commands/close.go @@ -30,8 +30,10 @@ func runCloseCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("issue ID cannot be empty") } + app := AppFromContext(cmd.Context()) + // Fetch the issue to ensure it exists before closing. - issue, err := svc.Beads.GetIssue(cmd.Context(), closeID) + issue, err := app.Issues.GetIssue(cmd.Context(), closeID) if err != nil { return fmt.Errorf("error fetching issue: %w", err) } @@ -47,7 +49,7 @@ func runCloseCmd(cmd *cobra.Command, args []string) error { } // Close the issue. - err = svc.Beads.CloseIssue(cmd.Context(), closeID, issue.CloseReason, "", "") + err = app.Issues.CloseIssue(cmd.Context(), closeID, issue.CloseReason, "", "") if err != nil { return fmt.Errorf("error closing issue: %w", err) } diff --git a/pkg/cli/commands/completion.go b/pkg/cli/commands/completion.go index 77d3fcf..e164585 100644 --- a/pkg/cli/commands/completion.go +++ b/pkg/cli/commands/completion.go @@ -34,11 +34,12 @@ func completeIssues(cmd *cobra.Command, args []string, toComplete string) ([]str // GetIssueCompletions fetches issues matching the toComplete string for shell completion. func GetIssueCompletions(ctx context.Context, toComplete string) ([]models.Issue, cobra.ShellCompDirective) { - if svc == nil { + app := AppFromContext(ctx) + if app == nil { return nil, cobra.ShellCompDirectiveNoFileComp } - issues, err := svc.Beads.AllIssues(ctx) + issues, err := app.Issues.AllIssues(ctx) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/pkg/cli/commands/create.go b/pkg/cli/commands/create.go index 538ed27..a270f03 100644 --- a/pkg/cli/commands/create.go +++ b/pkg/cli/commands/create.go @@ -54,7 +54,8 @@ func runCreateCmd(cmd *cobra.Command, args []string) error { } // Create the issue using the service layer. - err := svc.Beads.CreateIssue(cmd.Context(), issue, "test_actor") + app := AppFromContext(cmd.Context()) + err := app.Issues.CreateIssue(cmd.Context(), issue, "test_actor") if err != nil { return fmt.Errorf("error creating issue: %w", err) } diff --git a/pkg/cli/commands/delete.go b/pkg/cli/commands/delete.go index b96e659..0b66590 100644 --- a/pkg/cli/commands/delete.go +++ b/pkg/cli/commands/delete.go @@ -46,8 +46,10 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("issue ID cannot be empty") } + app := AppFromContext(cmd.Context()) + // Fetch the issue to ensure it exists before deletion. - issue, err := svc.Beads.GetIssue(cmd.Context(), deleteID) + issue, err := app.Issues.GetIssue(cmd.Context(), deleteID) if err != nil { return fmt.Errorf("error fetching issue: %w", err) } @@ -70,7 +72,7 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { } // Delete the issue. - err = svc.Beads.DeleteIssue(cmd.Context(), deleteID) + err = app.Issues.DeleteIssue(cmd.Context(), deleteID) if err != nil { return fmt.Errorf("error deleting issue: %w", err) } @@ -83,9 +85,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(ctx context.Context) error { + app := AppFromContext(ctx) options := []huh.Option[string]{} - issues, err := svc.Beads.SearchIssues(ctx, "", models.IssueFilter{}) + issues, err := app.Issues.SearchIssues(ctx, "", models.IssueFilter{}) if err != nil { return fmt.Errorf("error fetching issues: %w", err) } @@ -110,7 +113,7 @@ func runDeleteInteractive(ctx context.Context) error { } for _, id := range deleteIDs { - err := svc.Beads.DeleteIssue(ctx, id) + err := app.Issues.DeleteIssue(ctx, id) if err != nil { return fmt.Errorf("error deleting issue with ID %s: %w", id, err) } diff --git a/pkg/cli/commands/ls.go b/pkg/cli/commands/ls.go index bd65899..1eb13cc 100644 --- a/pkg/cli/commands/ls.go +++ b/pkg/cli/commands/ls.go @@ -55,7 +55,8 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error { } // Fetch issues based on the search query and filters. - issuesPtr, err := svc.Beads.SearchIssues(cmd.Context(), queryArg, filter) + app := AppFromContext(cmd.Context()) + issuesPtr, err := app.Issues.SearchIssues(cmd.Context(), queryArg, filter) if err != nil { return err } diff --git a/pkg/cli/commands/read.go b/pkg/cli/commands/read.go index 4ea6a54..40c8c8c 100644 --- a/pkg/cli/commands/read.go +++ b/pkg/cli/commands/read.go @@ -24,7 +24,8 @@ func runGetCmd(cmd *cobra.Command, args []string) error { issueID := args[0] // Fetch the issue details using the service layer. - issuePtr, err := svc.Beads.GetIssue(cmd.Context(), issueID) + app := AppFromContext(cmd.Context()) + issuePtr, err := app.Issues.GetIssue(cmd.Context(), issueID) if err != nil { return err } diff --git a/pkg/cli/commands/root.go b/pkg/cli/commands/root.go index 9e18856..45b1c45 100644 --- a/pkg/cli/commands/root.go +++ b/pkg/cli/commands/root.go @@ -10,9 +10,12 @@ import ( "github.com/spf13/cobra" ) -// svc is a global variable that holds beads, config and stats services. -// Must be called before executing any commands to ensure services are available. -var svc *service.Services +type contextKey string + +const appKey contextKey = "app" + +// app is a package-level variable used during command setup +var app *service.App // Flags struct to hold command-line flag values for issues. type Flags struct { @@ -30,13 +33,28 @@ type Flags struct { var rootCmd = &cobra.Command{ Short: "Project Management CLI", Long: `Project Management CLI for managing issues and tasks.`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // Inject app into context for all commands + if app != nil { + cmd.SetContext(context.WithValue(cmd.Context(), appKey, app)) + } + }, } -// SetServices sets the global services variable for use in command execution. +// SetApp sets the app variable for use in command execution. // Must be called before executing any commands to ensure services are available. -func SetServices(services *service.Services) { - svc = services - rootCmd.Use = svc.Config.RootCmd +func SetApp(application *service.App) { + app = application + rootCmd.Use = app.Config.RootCmd +} + +// AppFromContext retrieves the App from the command context +func AppFromContext(ctx context.Context) *service.App { + if a, ok := ctx.Value(appKey).(*service.App); ok { + return a + } + // Fallback to package-level app (for testing or edge cases) + return app } // Execute executes the root command using the fang library. diff --git a/pkg/cli/commands/update.go b/pkg/cli/commands/update.go index 099a08c..f26b23e 100644 --- a/pkg/cli/commands/update.go +++ b/pkg/cli/commands/update.go @@ -23,7 +23,9 @@ var updateCmd = &cobra.Command{ func runUpdateCmd(cmd *cobra.Command, args []string) error { issueID := args[0] - issue, err := svc.Beads.GetIssue(cmd.Context(), issueID) + app := AppFromContext(cmd.Context()) + + issue, err := app.Issues.GetIssue(cmd.Context(), issueID) if err != nil { return fmt.Errorf("error getting issue: %w", err) } @@ -37,12 +39,12 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("error getting update values: %w", err) } - err = svc.Beads.UpdateIssue(cmd.Context(), issueID, updates, "test_actor") + err = app.Issues.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) + updatedIssue, err := app.Issues.GetIssue(cmd.Context(), issueID) if err != nil { return fmt.Errorf("error getting updated issue: %w", err) } diff --git a/pkg/cli/repl/repl.go b/pkg/cli/repl/repl.go index 1133586..ff49153 100644 --- a/pkg/cli/repl/repl.go +++ b/pkg/cli/repl/repl.go @@ -37,7 +37,7 @@ func NewRepl() *REPL { } // Run starts the interactive Read-Eval-Print Loop for the PM CLI. -func (r *REPL) Run(ctx context.Context, config cli.CLIConfig) error { +func (r *REPL) Run(ctx context.Context, config cli.Config) 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. @@ -48,14 +48,14 @@ func (r *REPL) Run(ctx context.Context, config cli.CLIConfig) error { defer term.Restore(int(os.Stdin.Fd()), oldState) // Initialize services for beads, config and stats. - svc, cleanup, err := service.NewServices(ctx, config) + app, 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) + // Make sure to set app, to ensure they are available. + commands.SetApp(app) // Set the REPL instance so status command can access it commands.SetRepl(r) diff --git a/pkg/task/register.go b/pkg/task/register.go index 7fda855..4f57ae4 100644 --- a/pkg/task/register.go +++ b/pkg/task/register.go @@ -6,21 +6,21 @@ import ( "github.com/LazyBachelor/LazyPM/internal/service" ) -var registry = make(map[string]func(*service.Services) Tasker) +var registry = make(map[string]func(*service.App) Tasker) -func Register(name string, constructor func(*service.Services) Tasker) { +func Register(name string, constructor func(*service.App) Tasker) { if _, exists := registry[name]; exists { panic(fmt.Sprintf("task %q already registered", name)) } registry[name] = constructor } -func Get(name string, svc *service.Services) (Tasker, error) { +func Get(name string, app *service.App) (Tasker, error) { constructor, ok := registry[name] if !ok { return nil, fmt.Errorf("task %q not found", name) } - return constructor(svc), nil + return constructor(app), nil } func List() []string { diff --git a/pkg/task/runner.go b/pkg/task/runner.go index 37246f9..d060cdd 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -94,14 +94,13 @@ func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan Valida feedbackChan <- feedback doneChan <- true return - } else { - if err != nil { - feedback.Message = err.Error() - } else { - feedback.Message = "Task not yet complete" - } - feedbackChan <- feedback } + if err != nil { + feedback.Message = err.Error() + } else { + feedback.Message = "Task not yet complete" + } + feedbackChan <- feedback case <-quitChan: return case <-ctx.Done(): diff --git a/pkg/task/types.go b/pkg/task/types.go index abffe0e..085d3bb 100644 --- a/pkg/task/types.go +++ b/pkg/task/types.go @@ -8,15 +8,15 @@ import ( taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui" ) -type TaskConfig = service.Config +type Config = service.Config type InterfaceType string type Interface interface { - Run(context.Context, TaskConfig) error + Run(context.Context, Config) error } type Tasker interface { - Config() TaskConfig + Config() Config Details() taskui.TaskDetails Questions(InterfaceType) taskui.Questions Setup(context.Context) error diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 2c8f501..25c08c8 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -9,7 +9,7 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type TUIConfig = service.Config +type Config = service.Config type Tui struct { feedbackChan chan task.ValidationFeedback @@ -20,15 +20,15 @@ func NewTui() *Tui { return &Tui{} } -func (t *Tui) Run(ctx context.Context, config TUIConfig) error { - svc, cleanup, err := service.NewServices(ctx, config) +func (t *Tui) Run(ctx context.Context, config Config) error { + app, cleanup, err := service.NewServices(ctx, config) if err != nil { return err } defer cleanup() - p := tea.NewProgram(views.NewDashboardView(svc, t.feedbackChan, t.quitChan), + p := tea.NewProgram(views.NewDashboardView(app, t.feedbackChan, t.quitChan), tea.WithAltScreen(), tea.WithMouseAllMotion()) if t.quitChan != nil { diff --git a/pkg/tui/views/dashboard/issue_list.go b/pkg/tui/views/dashboard/issue_list.go index 2ea4d80..f3f5dbc 100644 --- a/pkg/tui/views/dashboard/issue_list.go +++ b/pkg/tui/views/dashboard/issue_list.go @@ -15,7 +15,7 @@ import ( type IssueList struct { list list.Model - svc *service.Services + app *service.App width int height int } @@ -74,8 +74,8 @@ func renderHeaders(cols []TableColumn) string { return lipgloss.JoinHorizontal(lipgloss.Left, parts...) } -func NewIssueList(svc *service.Services, width, height int) IssueList { - issues, err := svc.Beads.AllIssues(context.Background()) +func NewIssueList(app *service.App, width, height int) IssueList { + issues, err := app.Issues.AllIssues(context.Background()) if err != nil { return IssueList{} } @@ -102,13 +102,13 @@ func NewIssueList(svc *service.Services, width, height int) IssueList { return IssueList{ list: l, - svc: svc, + app: app, width: width, height: height, } } -func NewIssueListFromIssues(svc *service.Services, issues []models.Issue, width, height int) IssueList { +func NewIssueListFromIssues(app *service.App, issues []models.Issue, width, height int) IssueList { // for making an IssueList from a pre-existing list of issues. listIssues := make([]list.Item, len(issues)) for i, issue := range issues { @@ -125,7 +125,7 @@ func NewIssueListFromIssues(svc *service.Services, issues []models.Issue, width, l.FilterInput.Prompt = "🔍 " return IssueList{ list: l, - svc: svc, + app: app, width: width, height: height, } diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index 6a925b6..65fc34a 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -15,21 +15,21 @@ type ValidationFeedbackMsg struct { } type Model struct { - header Header - issueList IssueList - issueDetail IssueDetail - closedIssueList IssueList - helpBar HelpBar - keyMap DashboardKeyMap - svc *service.Services - width int - height int - focusedWindow int // 0 = main (display issues), 1 = closed issues - focusedPaneMain int // 0 = list, 1 = detail + header Header + issueList IssueList + issueDetail IssueDetail + closedIssueList IssueList + helpBar HelpBar + keyMap DashboardKeyMap + app *service.App + width int + height int + focusedWindow int // 0 = main (display issues), 1 = closed issues + focusedPaneMain int // 0 = list, 1 = detail focusedPaneClosed int - editingTitle bool // true while we are editing a title - titleInput textinput.Model - editingIssueID string + editingTitle bool // true while we are editing a title + titleInput textinput.Model + editingIssueID string editingDescription bool // true while editing a description descriptionInput textarea.Model @@ -41,32 +41,32 @@ type Model struct { deleteConfirmID string deleteConfirmIndex int - choosingStatus bool - statusIssueID string + choosingStatus bool + statusIssueID string feedbackChan chan task.ValidationFeedback quitChan chan bool currentFeedback task.ValidationFeedback showComplete bool } -func NewDashboard(svc *service.Services, feedbackChan chan task.ValidationFeedback, quitChan chan bool) *Model { +func NewDashboard(app *service.App, feedbackChan chan task.ValidationFeedback, quitChan chan bool) *Model { m := &Model{ header: NewHeader("Project Manager Dashboard"), keyMap: defaultDashboardKeyMap, - svc: svc, + app: app, width: 80, height: 24, - focusedWindow: 0, - focusedPaneMain: 0, - focusedPaneClosed: 0, - feedbackChan: feedbackChan, - quitChan: quitChan, + focusedWindow: 0, + focusedPaneMain: 0, + focusedPaneClosed: 0, + feedbackChan: feedbackChan, + quitChan: quitChan, } - allIssues, _ := svc.Beads.AllIssues(context.Background()) - m.issueList = NewIssueListFromIssues(svc, OpenAndInProgressOnly(allIssues), 0, 0) + allIssues, _ := app.Issues.AllIssues(context.Background()) + m.issueList = NewIssueListFromIssues(app, OpenAndInProgressOnly(allIssues), 0, 0) m.issueDetail = NewIssueDetail() - m.closedIssueList = NewIssueListFromIssues(svc, ClosedOnly(allIssues), 0, 0) + m.closedIssueList = NewIssueListFromIssues(app, ClosedOnly(allIssues), 0, 0) m.helpBar = NewHelpBar(m.keyMap) ti := textinput.New() diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 47e9387..a0a3d5d 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -39,45 +39,45 @@ type issueDeletedMsg struct { PreviousIndex int } -func updateIssueTitleCmd(svc *service.Services, issueID, newTitle string) tea.Cmd { +func updateIssueTitleCmd(app *service.App, issueID, newTitle string) tea.Cmd { return func() tea.Msg { updates := map[string]interface{}{"title": newTitle} - err := svc.Beads.UpdateIssue(context.Background(), issueID, updates, "tui") + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") return issueTitleUpdatedMsg{IssueID: issueID, Err: err} } } -func updateIssueDescriptionCmd(svc *service.Services, issueID, newDescription string) tea.Cmd { +func updateIssueDescriptionCmd(app *service.App, issueID, newDescription string) tea.Cmd { return func() tea.Msg { updates := map[string]interface{}{"description": newDescription} - err := svc.Beads.UpdateIssue(context.Background(), issueID, updates, "tui") + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") return issueDescriptionUpdatedMsg{IssueID: issueID, Err: err} } } -func updateIssueStatusCmd(svc *service.Services, issueID, status string) tea.Cmd { +func updateIssueStatusCmd(app *service.App, issueID, status string) tea.Cmd { return func() tea.Msg { updates := map[string]interface{}{"status": status} - err := svc.Beads.UpdateIssue(context.Background(), issueID, updates, "tui") + err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui") return issueStatusUpdatedMsg{IssueID: issueID, Err: err} } } -func createIssueCmd(svc *service.Services, title string) tea.Cmd { +func createIssueCmd(app *service.App, title string) tea.Cmd { return func() tea.Msg { issue := &models.Issue{ Title: title, Status: models.StatusOpen, IssueType: models.TypeTask, } - err := svc.Beads.CreateIssue(context.Background(), issue, "tui") + err := app.Issues.CreateIssue(context.Background(), issue, "tui") return issueCreatedMsg{Issue: issue, Err: err} } } -func deleteIssueCmd(svc *service.Services, issueID string, currentIndex int) tea.Cmd { +func deleteIssueCmd(app *service.App, issueID string, currentIndex int) tea.Cmd { return func() tea.Msg { - err := svc.Beads.DeleteIssue(context.Background(), issueID) + err := app.Issues.DeleteIssue(context.Background(), issueID) return issueDeletedMsg{IssueID: issueID, Err: err, PreviousIndex: currentIndex} } } @@ -91,7 +91,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - issues, err := m.svc.Beads.AllIssues(context.Background()) + issues, err := m.app.Issues.AllIssues(context.Background()) if err != nil { return m, nil } @@ -112,7 +112,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - issues, err := m.svc.Beads.AllIssues(context.Background()) + issues, err := m.app.Issues.AllIssues(context.Background()) if err != nil { return m, nil } @@ -132,7 +132,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - issues, err := m.svc.Beads.AllIssues(context.Background()) + issues, err := m.app.Issues.AllIssues(context.Background()) if err != nil { return m, nil } @@ -158,7 +158,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil || msg.Issue == nil { return m, nil } - issues, err := m.svc.Beads.AllIssues(context.Background()) + issues, err := m.app.Issues.AllIssues(context.Background()) if err != nil { return m, nil } @@ -185,7 +185,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - issues, err := m.svc.Beads.AllIssues(context.Background()) + issues, err := m.app.Issues.AllIssues(context.Background()) if err != nil { return m, nil } @@ -241,7 +241,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { idx := m.deleteConfirmIndex m.confirmingDelete = false m.deleteConfirmID = "" - return m, deleteIssueCmd(m.svc, issueID, idx) + return m, deleteIssueCmd(m.app, issueID, idx) case "n", "N", "esc": m.confirmingDelete = false m.deleteConfirmID = "" @@ -255,17 +255,17 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, updateIssueStatusCmd(m.svc, issueID, string(models.StatusOpen)) + return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) case "i": issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, updateIssueStatusCmd(m.svc, issueID, string(models.StatusInProgress)) + return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) case "c": issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" - return m, updateIssueStatusCmd(m.svc, issueID, string(models.StatusClosed)) + return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusClosed)) case "esc": m.choosingStatus = false m.statusIssueID = "" @@ -277,7 +277,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "enter" { title := m.createTitleInput.Value() if title != "" { - return m, createIssueCmd(m.svc, title) + return m, createIssueCmd(m.app, title) } } if msg.String() == "esc" { @@ -295,7 +295,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "enter" { newTitle := m.titleInput.Value() if newTitle != "" { - return m, updateIssueTitleCmd(m.svc, m.editingIssueID, newTitle) + return m, updateIssueTitleCmd(m.app, m.editingIssueID, newTitle) } } if msg.String() == "esc" { @@ -316,7 +316,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() - return m, updateIssueDescriptionCmd(m.svc, issueID, newDesc) + return m, updateIssueDescriptionCmd(m.app, issueID, newDesc) } if msg.String() == "esc" { m.editingDescription = false diff --git a/pkg/tui/views/views.go b/pkg/tui/views/views.go index 04bbea6..f60f508 100644 --- a/pkg/tui/views/views.go +++ b/pkg/tui/views/views.go @@ -6,6 +6,6 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard" ) -func NewDashboardView(svc *service.Services, feedbackChan chan task.ValidationFeedback, quitChan chan bool) *dashboard.Model { - return dashboard.NewDashboard(svc, feedbackChan, quitChan) +func NewDashboardView(app *service.App, feedbackChan chan task.ValidationFeedback, quitChan chan bool) *dashboard.Model { + return dashboard.NewDashboard(app, feedbackChan, quitChan) } diff --git a/pkg/web/handler/comments.go b/pkg/web/handler/comments.go index 8c5db6d..043ea2c 100644 --- a/pkg/web/handler/comments.go +++ b/pkg/web/handler/comments.go @@ -29,7 +29,7 @@ func ListComments(w http.ResponseWriter, r *http.Request) { func CreateComment(w http.ResponseWriter, r *http.Request) { issue := r.Context().Value(issueKey).(*models.Issue) - svc := Services(r) + app := App(r) hx := HTMX(r) form, err := ParseForm[CommentForm](r) @@ -47,7 +47,7 @@ func CreateComment(w http.ResponseWriter, r *http.Request) { return } - comment, err := svc.Beads.AddIssueComment(r.Context(), issue.ID, form.Author, form.Text) + comment, err := app.Issues.AddIssueComment(r.Context(), issue.ID, form.Author, form.Text) if err != nil { http.Error(w, "Failed to create comment: "+err.Error(), http.StatusInternalServerError) return diff --git a/pkg/web/handler/context.go b/pkg/web/handler/context.go index bdb1b51..a7631ed 100644 --- a/pkg/web/handler/context.go +++ b/pkg/web/handler/context.go @@ -11,14 +11,14 @@ import ( type contextKey string const ( - servicesKey contextKey = "services" - htmxKey contextKey = "htmx" + appKey contextKey = "app" + htmxKey contextKey = "htmx" ) -func ServicesMiddleware(svc *service.Services) func(http.Handler) http.Handler { +func AppMiddleware(app *service.App) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), servicesKey, svc) + ctx := context.WithValue(r.Context(), appKey, app) next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -33,8 +33,8 @@ func HTMXMiddleware(next http.Handler) http.Handler { }) } -func Services(r *http.Request) *service.Services { - return r.Context().Value(servicesKey).(*service.Services) +func App(r *http.Request) *service.App { + return r.Context().Value(appKey).(*service.App) } func HTMX(r *http.Request) *htmx.Handler { diff --git a/pkg/web/handler/index.go b/pkg/web/handler/index.go index 677a033..98e559b 100644 --- a/pkg/web/handler/index.go +++ b/pkg/web/handler/index.go @@ -8,10 +8,10 @@ import ( ) func IndexHandler(w http.ResponseWriter, r *http.Request) { - svc := Services(r) + app := App(r) hx := HTMX(r) - issues, err := svc.Beads.AllIssues(r.Context()) + issues, err := app.Issues.AllIssues(r.Context()) if err != nil { http.Error(w, "failed to retrieve issues", http.StatusInternalServerError) return diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index d0bb77a..b9ceace 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -30,7 +30,7 @@ type UpdateIssueForm struct { } func CreateIssue(w http.ResponseWriter, r *http.Request) { - svc := Services(r) + app := App(r) hx := HTMX(r) form, err := ParseForm[IssueForm](r) @@ -49,7 +49,7 @@ func CreateIssue(w http.ResponseWriter, r *http.Request) { } issue := form.toIssue() - if err := svc.Beads.CreateIssue(r.Context(), &issue, ""); err != nil { + if err := app.Issues.CreateIssue(r.Context(), &issue, ""); err != nil { http.Error(w, "Failed to create issue: "+err.Error(), http.StatusInternalServerError) return } @@ -64,10 +64,10 @@ func CreateIssue(w http.ResponseWriter, r *http.Request) { } func ListIssues(w http.ResponseWriter, r *http.Request) { - svc := Services(r) + app := App(r) hx := HTMX(r) - issues, err := svc.Beads.AllIssues(r.Context()) + issues, err := app.Issues.AllIssues(r.Context()) if err != nil { http.Error(w, "Failed to retrieve issues", http.StatusInternalServerError) return @@ -79,10 +79,10 @@ func ListIssues(w http.ResponseWriter, r *http.Request) { func IssueCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - svc := Services(r) + app := App(r) id := chi.URLParam(r, "id") - issue, err := svc.Beads.GetIssue(r.Context(), id) + issue, err := app.Issues.GetIssue(r.Context(), id) if err != nil { http.Error(w, "Error getting issue: "+err.Error(), http.StatusNotFound) return @@ -92,7 +92,7 @@ func IssueCtx(next http.Handler) http.Handler { return } - comments, err := svc.Beads.GetIssueComments(r.Context(), issue.ID) + comments, err := app.Issues.GetIssueComments(r.Context(), issue.ID) if err != nil { http.Error(w, "Error getting comments: "+err.Error(), http.StatusInternalServerError) return @@ -132,7 +132,7 @@ func GetIssue(w http.ResponseWriter, r *http.Request) { func UpdateIssue(w http.ResponseWriter, r *http.Request) { issue := r.Context().Value(issueKey).(*models.Issue) - svc := Services(r) + app := App(r) hx := HTMX(r) form, err := ParseForm[UpdateIssueForm](r) @@ -152,12 +152,12 @@ func UpdateIssue(w http.ResponseWriter, r *http.Request) { changes := form.toChanges() - if err := svc.Beads.UpdateIssue(r.Context(), issue.ID, changes, ""); err != nil { + if err := app.Issues.UpdateIssue(r.Context(), issue.ID, changes, ""); err != nil { http.Error(w, "Failed to update issue", http.StatusInternalServerError) return } - issue, err = svc.Beads.GetIssue(r.Context(), issue.ID) + issue, err = app.Issues.GetIssue(r.Context(), issue.ID) if err != nil { http.Error(w, "Failed to retrieve updated issue", http.StatusInternalServerError) return @@ -170,8 +170,8 @@ func UpdateIssue(w http.ResponseWriter, r *http.Request) { func DeleteIssue(w http.ResponseWriter, r *http.Request) { issue := r.Context().Value(issueKey).(*models.Issue) - svc := Services(r) - if err := svc.Beads.DeleteIssue(r.Context(), issue.ID); err != nil { + app := App(r) + if err := app.Issues.DeleteIssue(r.Context(), issue.ID); err != nil { http.Error(w, "Failed to delete issue", http.StatusInternalServerError) return } diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 1f69bc7..9a2169c 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -22,7 +22,7 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { r.Use(middleware.CleanPath) r.Use(handler.HTMXMiddleware) - r.Use(handler.ServicesMiddleware(s.Services)) + r.Use(handler.AppMiddleware(s.App)) s.handleAssets(r, assets) diff --git a/pkg/web/server/server.go b/pkg/web/server/server.go index da06a1b..7931c5a 100644 --- a/pkg/web/server/server.go +++ b/pkg/web/server/server.go @@ -9,9 +9,9 @@ import ( ) type Server struct { - Address string - Assets embed.FS - Services *service.Services + Address string + Assets embed.FS + App *service.App } // NewServer creates and configures a new HTTP server instance. diff --git a/pkg/web/web.go b/pkg/web/web.go index f9ad4b3..896d3d6 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -3,6 +3,7 @@ package web import ( "context" "embed" + "errors" "fmt" "net/http" "time" @@ -13,7 +14,7 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/web/server" ) -type WebConfig = service.Config +type Config = service.Config type Web struct { feedbackChan chan task.ValidationFeedback @@ -27,8 +28,8 @@ func NewWeb() *Web { //go:embed assets/* var assets embed.FS -func (w *Web) Run(ctx context.Context, config WebConfig) error { - svc, cleanup, err := service.NewServices(ctx, config) +func (w *Web) Run(ctx context.Context, config Config) error { + app, cleanup, err := service.NewServices(ctx, config) if err != nil { return err } @@ -36,16 +37,16 @@ func (w *Web) Run(ctx context.Context, config WebConfig) error { defer cleanup() httpServer := server.NewServer(server.Server{ - Address: config.WebAddress, - Assets: assets, - Services: svc, + Address: config.WebAddress, + Assets: assets, + App: app, }) fmt.Printf("Starting web server on %s...\n", config.WebAddress) serverErr := make(chan error, 1) go func() { - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { serverErr <- err } }()