refactor to use Tasker interface instead of Task

add tasker register for registering tasks for extiensibility
This commit is contained in:
Robin Olsen
2026-02-18 13:07:23 +01:00
parent 89f8596884
commit 23747fb799
9 changed files with 276 additions and 300 deletions

32
pkg/task/register.go Normal file
View File

@@ -0,0 +1,32 @@
package task
import (
"fmt"
"github.com/LazyBachelor/LazyPM/internal/service"
)
var registry = make(map[string]func(*service.Services) Tasker)
func Register(name string, constructor func(*service.Services) 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) {
constructor, ok := registry[name]
if !ok {
return nil, fmt.Errorf("task %q not found", name)
}
return constructor(svc), nil
}
func List() []string {
names := make([]string, 0, len(registry))
for name := range registry {
names = append(names, name)
}
return names
}

111
pkg/task/runner.go Normal file
View File

@@ -0,0 +1,111 @@
package task
import (
"context"
"fmt"
"time"
taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui"
tea "github.com/charmbracelet/bubbletea"
)
var ErrUserQuit = fmt.Errorf("user quit")
// RunTask orchestrates the complete task execution flow:
// 1. Setup the task
// 2. Show task intro screen
// 3. Run the interface
// 4. Start validation loop in background
// 5. Show questionnaire when done
func RunTask(ctx context.Context, t Tasker, i Interface, ifaceType InterfaceType) error {
doneChan := make(chan bool, 1)
quitChan := make(chan bool, 1)
feedbackChan := make(chan ValidationFeedback, 10)
if validated, ok := i.(ValidatedInterface); ok {
validated.SetChannels(feedbackChan, quitChan)
}
// Setup task
if err := t.Setup(ctx); err != nil {
return fmt.Errorf("failed to setup task: %w", err)
}
// Show task intro
detailsScreen := taskui.NewTaskModel(t.Details())
model, err := tea.NewProgram(detailsScreen, tea.WithAltScreen()).Run()
if err != nil {
return err
}
if m, ok := model.(interface{ GetUserQuit() bool }); ok && m.GetUserQuit() {
return ErrUserQuit
}
// Start validation loop
go startValidationLoop(ctx, t, feedbackChan, doneChan, quitChan)
// Run interface
interfaceDone := make(chan error, 1)
go func() {
interfaceDone <- i.Run(ctx, t.Config())
}()
select {
case <-doneChan:
close(quitChan)
<-interfaceDone
fmt.Println("Task completed successfully!")
case err := <-interfaceDone:
close(quitChan)
if err != nil {
return fmt.Errorf("failed to start task interface: %w", err)
}
fmt.Println("Task incomplete - you exited early")
}
// Show questionnaire
questions := t.Questions(ifaceType)
questionare := taskui.NewQuestionnaireModel(questions)
model, err = tea.NewProgram(questionare, tea.WithAltScreen()).Run()
if err != nil {
return err
}
if m, ok := model.(interface{ GetUserQuit() bool }); ok && m.GetUserQuit() {
return ErrUserQuit
}
return nil
}
func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan ValidationFeedback, doneChan chan bool, quitChan chan bool) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ok, err := t.Validate(ctx)
feedback := ValidationFeedback{
Success: ok,
}
if ok {
feedback.Message = "Task completed successfully!"
feedbackChan <- feedback
doneChan <- true
return
} else {
if err != nil {
feedback.Message = err.Error()
} else {
feedback.Message = "Task not yet complete"
}
feedbackChan <- feedback
}
case <-quitChan:
return
case <-ctx.Done():
return
}
}
}

View File

@@ -1,170 +0,0 @@
package task
import (
"context"
"fmt"
"time"
"github.com/LazyBachelor/LazyPM/internal/service"
taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui"
tea "github.com/charmbracelet/bubbletea"
)
type Tasker interface {
Init() *Task
Config() TaskConfig
Details() taskui.TaskDetails
QuestionsFunc() QuestionsFunc
InterfaceType() Interface
ValidateFunc(context.Context) (ok bool, errorMsg error)
DbStateFunc(context.Context) error
}
type Task struct {
svc *service.Services
Config TaskConfig
Interface Interface
InterfaceType InterfaceType
details taskui.TaskDetails
questionsFunc QuestionsFunc
validateFunc ValidateFunc
dbStateFunc DbStateFunc
feedbackChan chan ValidationFeedback
doneChan chan bool
quitChan chan bool
}
func NewTask(svc *service.Services, details taskui.TaskDetails, questionsFunc QuestionsFunc) *Task {
return &Task{
details: details,
questionsFunc: questionsFunc,
svc: svc,
}
}
func (t *Task) IntroduceTask() error {
if t.details == (taskui.TaskDetails{}) {
return fmt.Errorf("details is not set")
}
detailsScreen := taskui.NewTaskModel(t.details)
model, err := tea.NewProgram(detailsScreen, tea.WithAltScreen()).Run()
if err != nil {
return err
}
if m, ok := model.(interface{ GetUserQuit() bool }); ok && m.GetUserQuit() {
return ErrUserQuit
}
return nil
}
func (t *Task) StartInterface(ctx context.Context, cfg TaskConfig) error {
if t.Interface == nil {
return fmt.Errorf("interfaceType is not set")
}
return t.Interface.Run(ctx, cfg)
}
func (t *Task) Initialize(ctx context.Context) error {
if t.dbStateFunc == nil {
return fmt.Errorf("dbStateFunc is not set")
}
return t.dbStateFunc(ctx)
}
func (t *Task) Validate(ctx context.Context) (bool, error) {
if t.validateFunc == nil {
return false, fmt.Errorf("validateFunc is not set")
}
return t.validateFunc(ctx)
}
func (t *Task) StartQuestionnaire() error {
if t.questionsFunc == nil {
return fmt.Errorf("questionsFunc is not set")
}
questions := t.questionsFunc(t.InterfaceType)
if questions == nil {
return fmt.Errorf("questions is nil")
}
questionare := taskui.NewQuestionnaireModel(questions)
model, err := tea.NewProgram(questionare, tea.WithAltScreen()).Run()
if err != nil {
return err
}
if m, ok := model.(interface{ GetUserQuit() bool }); ok && m.GetUserQuit() {
return ErrUserQuit
}
return nil
}
func (t *Task) SetConfigFunc(fn ConfigFunc) {
t.Config = fn()
}
func (t *Task) SetInterface(interfaceType Interface) {
t.Interface = interfaceType
}
func (t *Task) SetInterfaceType(interfaceType InterfaceType) {
t.InterfaceType = interfaceType
}
func (t *Task) SetDbStateFunc(fn DbStateFunc) {
t.dbStateFunc = fn
}
func (t *Task) SetValidateFunc(fn ValidateFunc) {
t.validateFunc = fn
}
func (t *Task) SetChannels(feedbackChan chan ValidationFeedback, doneChan chan bool, quitChan chan bool) {
t.feedbackChan = feedbackChan
t.doneChan = doneChan
t.quitChan = quitChan
}
func (t *Task) StartValidationLoop(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ok, err := t.Validate(ctx)
feedback := ValidationFeedback{
Timestamp: time.Now(),
}
if ok {
feedback.Success = true
feedback.Message = "Task completed successfully!"
t.feedbackChan <- feedback
t.doneChan <- true
return
} else {
feedback.Success = false
if err != nil {
feedback.Message = err.Error()
} else {
feedback.Message = "Task not yet complete"
}
t.feedbackChan <- feedback
}
case <-t.quitChan:
return
case <-ctx.Done():
return
}
}
}

View File

@@ -2,15 +2,11 @@ package task
import (
"context"
"errors"
"time"
"github.com/LazyBachelor/LazyPM/internal/service"
taskui "github.com/LazyBachelor/LazyPM/pkg/task/ui"
)
var ErrUserQuit = errors.New("user quit")
type TaskConfig = service.Config
type Interface interface {
@@ -25,20 +21,17 @@ const (
InterfaceWeb InterfaceType = "web"
)
type ConfigFunc func() TaskConfig
type ValidateFunc func(context.Context) (ok bool, err error)
type DbStateFunc func(context.Context) error
type QuestionsFunc func(InterfaceType) taskui.Questions
type ValidationFeedback struct {
Success bool
Message string
Timestamp time.Time
type Tasker interface {
Config() TaskConfig
Details() taskui.TaskDetails
Questions(InterfaceType) taskui.Questions
Setup(context.Context) error
Validate(context.Context) (bool, error)
}
type ValidationObserver interface {
OnValidationUpdate(feedback ValidationFeedback)
OnTaskComplete()
type ValidationFeedback struct {
Success bool
Message string
}
type ValidatedInterface interface {