summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-23 06:25:28 +0300
committerPaul Buetow <paul@buetow.org>2026-06-23 06:25:28 +0300
commit03cff75ae50399c9f43abde178f43e281db6caad (patch)
tree6837143d6e61303b4603396e1073b545e80d4926
parent5c591b4b3a070057b31a8b0ff2664e651b8d5808 (diff)
Refactor UI Taskwarrior dependency for vq0
-rw-r--r--internal/task/taskwarrior.go159
-rw-r--r--internal/ui/editor_handlers.go4
-rw-r--r--internal/ui/handlers.go22
-rw-r--r--internal/ui/keyactions.go28
-rw-r--r--internal/ui/shell.go8
-rw-r--r--internal/ui/table.go36
-rw-r--r--internal/ui/table_test.go64
-rw-r--r--internal/ui/ultra.go2
8 files changed, 279 insertions, 44 deletions
diff --git a/internal/task/taskwarrior.go b/internal/task/taskwarrior.go
new file mode 100644
index 0000000..10b31b6
--- /dev/null
+++ b/internal/task/taskwarrior.go
@@ -0,0 +1,159 @@
+package task
+
+import (
+ "context"
+ "os/exec"
+ "time"
+)
+
+// Taskwarrior describes the Taskwarrior operations required by the UI.
+type Taskwarrior interface {
+ Export(ctx context.Context, filters ...string) ([]Task, error)
+ SortTasks(tasks []Task)
+ TotalTasks(tasks []Task) int
+ InProgressTasks(tasks []Task) int
+ DueTasks(tasks []Task, now time.Time) int
+ EditCmd(id int) *exec.Cmd
+ RunShellLine(ctx context.Context, line string) (RunResult, error)
+ LoadCompletionSources(ctx context.Context) CompletionSources
+ AddLineContext(ctx context.Context, line string) error
+ AnnotateContext(ctx context.Context, id int, text string) error
+ ReplaceAnnotations(ctx context.Context, id int, text string) error
+ SetDescriptionContext(ctx context.Context, id int, desc string) error
+ AddTagsContext(ctx context.Context, id int, tags []string) error
+ RemoveTagsContext(ctx context.Context, id int, tags []string) error
+ SetDueDateContext(ctx context.Context, id int, due string) error
+ SetRecurrenceContext(ctx context.Context, id int, rec string) error
+ SetProjectContext(ctx context.Context, id int, project string) error
+ SetPriorityContext(ctx context.Context, id int, priority string) error
+ StartContext(ctx context.Context, id int) error
+ StopContext(ctx context.Context, id int) error
+ DoneContext(ctx context.Context, id int) error
+ SetStatusUUIDContext(ctx context.Context, uuid, status string) error
+ RecurringSeries(ctx context.Context, rootUUID string) ([]Task, error)
+}
+
+// Client is the production Taskwarrior implementation backed by the task CLI.
+type Client struct{}
+
+var _ Taskwarrior = Client{}
+
+// NewTaskwarrior returns a production Taskwarrior Client.
+func NewTaskwarrior() Client {
+ return Client{}
+}
+
+// Export retrieves tasks using Taskwarrior export.
+func (Client) Export(ctx context.Context, filters ...string) ([]Task, error) {
+ return Export(ctx, filters...)
+}
+
+// SortTasks orders tasks using TaskSamurai's default task ordering.
+func (Client) SortTasks(tasks []Task) {
+ SortTasks(tasks)
+}
+
+// TotalTasks returns the number of tasks provided.
+func (Client) TotalTasks(tasks []Task) int {
+ return TotalTasks(tasks)
+}
+
+// InProgressTasks returns the number of started, incomplete tasks.
+func (Client) InProgressTasks(tasks []Task) int {
+ return InProgressTasks(tasks)
+}
+
+// DueTasks returns the number of due tasks.
+func (Client) DueTasks(tasks []Task, now time.Time) int {
+ return DueTasks(tasks, now)
+}
+
+// EditCmd returns an editor command for a task.
+func (Client) EditCmd(id int) *exec.Cmd {
+ return EditCmd(id)
+}
+
+// RunShellLine runs a user-entered Taskwarrior shell command.
+func (Client) RunShellLine(ctx context.Context, line string) (RunResult, error) {
+ return RunShellLine(ctx, line)
+}
+
+// LoadCompletionSources returns Taskwarrior-provided completion candidates.
+func (Client) LoadCompletionSources(ctx context.Context) CompletionSources {
+ return LoadCompletionSources(ctx)
+}
+
+// AddLineContext adds a task from a shell-style input line.
+func (Client) AddLineContext(ctx context.Context, line string) error {
+ return AddLineContext(ctx, line)
+}
+
+// AnnotateContext adds an annotation to a task.
+func (Client) AnnotateContext(ctx context.Context, id int, text string) error {
+ return AnnotateContext(ctx, id, text)
+}
+
+// ReplaceAnnotations replaces all annotations on a task.
+func (Client) ReplaceAnnotations(ctx context.Context, id int, text string) error {
+ return ReplaceAnnotations(ctx, id, text)
+}
+
+// SetDescriptionContext changes a task description.
+func (Client) SetDescriptionContext(ctx context.Context, id int, desc string) error {
+ return SetDescriptionContext(ctx, id, desc)
+}
+
+// AddTagsContext adds tags to a task.
+func (Client) AddTagsContext(ctx context.Context, id int, tags []string) error {
+ return AddTagsContext(ctx, id, tags)
+}
+
+// RemoveTagsContext removes tags from a task.
+func (Client) RemoveTagsContext(ctx context.Context, id int, tags []string) error {
+ return RemoveTagsContext(ctx, id, tags)
+}
+
+// SetDueDateContext changes a task due date.
+func (Client) SetDueDateContext(ctx context.Context, id int, due string) error {
+ return SetDueDateContext(ctx, id, due)
+}
+
+// SetRecurrenceContext changes a task recurrence value.
+func (Client) SetRecurrenceContext(ctx context.Context, id int, rec string) error {
+ return SetRecurrenceContext(ctx, id, rec)
+}
+
+// SetProjectContext changes a task project.
+func (Client) SetProjectContext(ctx context.Context, id int, project string) error {
+ return SetProjectContext(ctx, id, project)
+}
+
+// SetPriorityContext changes a task priority.
+func (Client) SetPriorityContext(ctx context.Context, id int, priority string) error {
+ return SetPriorityContext(ctx, id, priority)
+}
+
+// StartContext starts a task.
+func (Client) StartContext(ctx context.Context, id int) error {
+ return StartContext(ctx, id)
+}
+
+// StopContext stops a task.
+func (Client) StopContext(ctx context.Context, id int) error {
+ return StopContext(ctx, id)
+}
+
+// DoneContext completes a task.
+func (Client) DoneContext(ctx context.Context, id int) error {
+ return DoneContext(ctx, id)
+}
+
+// SetStatusUUIDContext changes a task status by UUID.
+func (Client) SetStatusUUIDContext(ctx context.Context, uuid, status string) error {
+ return SetStatusUUIDContext(ctx, uuid, status)
+}
+
+// RecurringSeries returns a recurring task series.
+func (Client) RecurringSeries(ctx context.Context, rootUUID string) ([]Task, error) {
+ return RecurringSeries(ctx, rootUUID)
+}
diff --git a/internal/ui/editor_handlers.go b/internal/ui/editor_handlers.go
index 2db20d0..857e3c7 100644
--- a/internal/ui/editor_handlers.go
+++ b/internal/ui/editor_handlers.go
@@ -7,8 +7,6 @@ import (
"time"
tea "charm.land/bubbletea/v2"
-
- "codeberg.org/snonux/tasksamurai/internal/task"
)
// handleEditDone handles completion of external editor
@@ -57,7 +55,7 @@ func (m *Model) handleDescEditDone(msg descEditDoneMsg) (tea.Model, tea.Cmd) {
newDesc := strings.TrimSpace(string(content))
if m.currentTaskDetail != nil {
ctx, cancel := m.taskOperationContext()
- err = task.SetDescriptionContext(ctx, m.currentTaskDetail.ID, newDesc)
+ err = m.taskwarriorClient().SetDescriptionContext(ctx, m.currentTaskDetail.ID, newDesc)
cancel()
if err != nil {
m.statusMsg = fmt.Sprintf("Error updating description: %v", err)
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go
index e556570..cc11f89 100644
--- a/internal/ui/handlers.go
+++ b/internal/ui/handlers.go
@@ -7,8 +7,6 @@ import (
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
-
- "codeberg.org/snonux/tasksamurai/internal/task"
)
// handleTextInput provides generic text input handling for all input modes
@@ -49,14 +47,14 @@ func (m *Model) handleAnnotationMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
if m.replaceAnnotations {
ctx, cancel := m.taskOperationContext()
defer cancel()
- if err := task.ReplaceAnnotations(ctx, m.annotateID, value); err != nil {
+ if err := m.taskwarriorClient().ReplaceAnnotations(ctx, m.annotateID, value); err != nil {
return err
}
m.replaceAnnotations = false
} else {
ctx, cancel := m.taskOperationContext()
defer cancel()
- if err := task.AnnotateContext(ctx, m.annotateID, value); err != nil {
+ if err := m.taskwarriorClient().AnnotateContext(ctx, m.annotateID, value); err != nil {
return err
}
}
@@ -87,7 +85,7 @@ func (m *Model) handleDescriptionMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd)
}
ctx, cancel := m.taskOperationContext()
defer cancel()
- if err := task.SetDescriptionContext(ctx, m.descID, value); err != nil {
+ if err := m.taskwarriorClient().SetDescriptionContext(ctx, m.descID, value); err != nil {
return err
}
if err := m.reload(); err != nil {
@@ -135,12 +133,12 @@ func (m *Model) handleTagsMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
ctx, cancel := m.taskOperationContext()
defer cancel()
if len(adds) > 0 {
- if err := task.AddTagsContext(ctx, m.tagsID, adds); err != nil {
+ if err := m.taskwarriorClient().AddTagsContext(ctx, m.tagsID, adds); err != nil {
return err
}
}
if len(removes) > 0 {
- if err := task.RemoveTagsContext(ctx, m.tagsID, removes); err != nil {
+ if err := m.taskwarriorClient().RemoveTagsContext(ctx, m.tagsID, removes); err != nil {
return err
}
}
@@ -171,7 +169,7 @@ func (m *Model) handleDueEditMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "enter":
ctx, cancel := m.taskOperationContext()
- err := task.SetDueDateContext(ctx, m.dueID, m.dueDate.Format("2006-01-02"))
+ err := m.taskwarriorClient().SetDueDateContext(ctx, m.dueID, m.dueDate.Format("2006-01-02"))
cancel()
if err != nil {
m.statusMsg = fmt.Sprintf("Error: %v", err)
@@ -220,7 +218,7 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
}
ctx, cancel := m.taskOperationContext()
defer cancel()
- if err := task.SetRecurrenceContext(ctx, m.recurID, value); err != nil {
+ if err := m.taskwarriorClient().SetRecurrenceContext(ctx, m.recurID, value); err != nil {
return err
}
if err := m.reload(); err != nil {
@@ -250,7 +248,7 @@ func (m *Model) handleProjectMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
onEnter := func(value string) error {
ctx, cancel := m.taskOperationContext()
defer cancel()
- return task.SetProjectContext(ctx, m.projID, value)
+ return m.taskwarriorClient().SetProjectContext(ctx, m.projID, value)
}
onExit := func() {
@@ -282,7 +280,7 @@ func (m *Model) handlePriorityMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
return m, cmd
}
ctx, cancel := m.taskOperationContext()
- err := task.SetPriorityContext(ctx, m.priorityID, priority)
+ err := m.taskwarriorClient().SetPriorityContext(ctx, m.priorityID, priority)
cancel()
if err != nil {
m.statusMsg = fmt.Sprintf("Error: %v", err)
@@ -361,7 +359,7 @@ func (m *Model) handleAddTaskMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
}
ctx, cancel := m.taskOperationContext()
- err := task.AddLineContext(ctx, m.addInput.Value())
+ err := m.taskwarriorClient().AddLineContext(ctx, m.addInput.Value())
cancel()
if err != nil {
m.statusMsg = fmt.Sprintf("Error: %v", err)
diff --git a/internal/ui/keyactions.go b/internal/ui/keyactions.go
index ef0189a..9fede46 100644
--- a/internal/ui/keyactions.go
+++ b/internal/ui/keyactions.go
@@ -21,7 +21,7 @@ func (m *Model) handleEditTask() (tea.Model, tea.Cmd) {
return m, nil
}
m.editID = id
- return m, editCmd(id)
+ return m, m.editCmd(id)
}
func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) {
@@ -41,7 +41,7 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) {
if started {
ctx, cancel := m.taskOperationContext()
- err := task.StopContext(ctx, id)
+ err := m.taskwarriorClient().StopContext(ctx, id)
cancel()
if err != nil {
m.showError(err)
@@ -49,7 +49,7 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) {
}
} else {
ctx, cancel := m.taskOperationContext()
- err := task.StartContext(ctx, id)
+ err := m.taskwarriorClient().StartContext(ctx, id)
cancel()
if err != nil {
m.showError(err)
@@ -145,7 +145,7 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) {
action := m.undoStack[len(m.undoStack)-1]
ctx, cancel := m.taskOperationContext()
for _, restore := range action.restores {
- if err := task.SetStatusUUIDContext(ctx, restore.uuid, restore.status); err != nil {
+ if err := m.taskwarriorClient().SetStatusUUIDContext(ctx, restore.uuid, restore.status); err != nil {
cancel()
m.showError(err)
return m, nil
@@ -187,7 +187,7 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) {
filters = append(filters, "status:"+restore.status)
ctx, cancel := m.taskOperationContext()
- tasks, err := task.Export(ctx, filters...)
+ tasks, err := m.taskwarriorClient().Export(ctx, filters...)
cancel()
if err == nil && len(tasks) > 0 {
id = tasks[0].ID
@@ -229,7 +229,7 @@ func (m *Model) deleteTaskWithUndo(tsk task.Task) (int, bool, error) {
ctx, cancel := m.taskOperationContext()
defer cancel()
if recurring {
- series, err := task.RecurringSeries(ctx, recurringRootUUID(tsk))
+ series, err := m.taskwarriorClient().RecurringSeries(ctx, recurringRootUUID(tsk))
if err != nil {
return 0, true, fmt.Errorf("loading recurring series: %w", err)
}
@@ -250,8 +250,8 @@ func (m *Model) deleteTaskWithUndo(tsk task.Task) (int, bool, error) {
completed := make([]undoRestore, 0, len(restores))
for _, restore := range restores {
- if err := task.SetStatusUUIDContext(ctx, restore.uuid, "deleted"); err != nil {
- if rollbackErr := rollbackUndoRestores(completed); rollbackErr != nil {
+ if err := m.taskwarriorClient().SetStatusUUIDContext(ctx, restore.uuid, "deleted"); err != nil {
+ if rollbackErr := m.rollbackUndoRestores(completed); rollbackErr != nil {
return 0, recurring, fmt.Errorf("deleting task %s: %w; rollback failed: %w", restore.uuid, err, rollbackErr)
}
return 0, recurring, fmt.Errorf("deleting task %s: %w", restore.uuid, err)
@@ -323,13 +323,13 @@ func undoStatusForTask(tsk task.Task) string {
return tsk.Status
}
-func rollbackUndoRestores(restores []undoRestore) error {
+func (m *Model) rollbackUndoRestores(restores []undoRestore) error {
ctx, cancel := context.WithTimeout(context.Background(), taskOperationTimeout)
defer cancel()
var errs []error
for i := len(restores) - 1; i >= 0; i-- {
- if err := task.SetStatusUUIDContext(ctx, restores[i].uuid, restores[i].status); err != nil {
+ if err := m.taskwarriorClient().SetStatusUUIDContext(ctx, restores[i].uuid, restores[i].status); err != nil {
errs = append(errs, fmt.Errorf("restoring task %s to %s: %w", restores[i].uuid, restores[i].status, err))
}
}
@@ -365,7 +365,7 @@ func (m *Model) handleRemoveDueDate() (tea.Model, tea.Cmd) {
// In Taskwarrior, passing an empty value to due: removes the due date
ctx, cancel := m.taskOperationContext()
- err = task.SetDueDateContext(ctx, id, "")
+ err = m.taskwarriorClient().SetDueDateContext(ctx, id, "")
cancel()
if err != nil {
m.showError(err)
@@ -388,7 +388,7 @@ func (m *Model) handleRandomDueDate() (tea.Model, tea.Cmd) {
due := time.Now().AddDate(0, 0, days).Format("2006-01-02")
ctx, cancel := m.taskOperationContext()
- err = task.SetDueDateContext(ctx, id, due)
+ err = m.taskwarriorClient().SetDueDateContext(ctx, id, due)
cancel()
if err != nil {
m.showError(err)
@@ -532,7 +532,7 @@ func (m *Model) handleTagToProject() (tea.Model, tea.Cmd) {
// Set the tag as project
ctx, cancel := m.taskOperationContext()
- err = task.SetProjectContext(ctx, id, firstTag)
+ err = m.taskwarriorClient().SetProjectContext(ctx, id, firstTag)
if err != nil {
cancel()
m.showError(err)
@@ -540,7 +540,7 @@ func (m *Model) handleTagToProject() (tea.Model, tea.Cmd) {
}
// Remove the tag from the task
- if err := task.RemoveTagsContext(ctx, id, []string{firstTag}); err != nil {
+ if err := m.taskwarriorClient().RemoveTagsContext(ctx, id, []string{firstTag}); err != nil {
cancel()
m.showError(err)
return m, nil
diff --git a/internal/ui/shell.go b/internal/ui/shell.go
index b3b6285..d35a27c 100644
--- a/internal/ui/shell.go
+++ b/internal/ui/shell.go
@@ -16,12 +16,12 @@ import (
const shellCommandTimeout = 2 * time.Minute
-func shellRunCmd(parent context.Context, line string, selectedID int) tea.Cmd {
+func shellRunCmd(parent context.Context, tw task.Taskwarrior, line string, selectedID int) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(parent, shellCommandTimeout)
defer cancel()
- result, err := task.RunShellLine(ctx, line)
+ result, err := tw.RunShellLine(ctx, line)
return shellDoneMsg{result: result, err: err, selectedID: selectedID}
}
}
@@ -70,7 +70,7 @@ func (m *Model) handleShellMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.shellActive = false
m.shellInput.Blur()
m.updateTableHeight()
- return m, shellRunCmd(m.shellCommandContext(), line, selectedID)
+ return m, shellRunCmd(m.shellCommandContext(), m.taskwarriorClient(), line, selectedID)
case "esc":
m.shellActive = false
m.shellInput.Blur()
@@ -227,7 +227,7 @@ func (m *Model) loadShellCompletionsCmd() tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
- return shellCompletionMsg{sources: task.LoadCompletionSources(ctx)}
+ return shellCompletionMsg{sources: m.taskwarriorClient().LoadCompletionSources(ctx)}
}
}
diff --git a/internal/ui/table.go b/internal/ui/table.go
index 949abbb..1c526c0 100644
--- a/internal/ui/table.go
+++ b/internal/ui/table.go
@@ -217,6 +217,7 @@ type Model struct {
undoStack []undoAction
browserCmd string
agentFilterHotkey string
+ taskwarrior task.Taskwarrior
theme Theme
defaultTheme Theme
@@ -321,8 +322,8 @@ func prepareDescriptionTempFile(description string, newTempFile func() (descript
// editCmd returns a command that edits the task and sends an
// editDoneMsg once the process is complete.
-func editCmd(id int) tea.Cmd {
- c := task.EditCmd(id)
+func (m *Model) editCmd(id int) tea.Cmd {
+ c := m.taskwarriorClient().EditCmd(id)
return tea.ExecProcess(c, func(err error) tea.Msg { return editDoneMsg{err: err} })
}
@@ -360,6 +361,13 @@ func blinkCmd() tea.Cmd {
return tea.Tick(blinkInterval, func(time.Time) tea.Msg { return blinkMsg{} })
}
+func (m *Model) taskwarriorClient() task.Taskwarrior {
+ if m.taskwarrior == nil {
+ m.taskwarrior = task.NewTaskwarrior()
+ }
+ return m.taskwarrior
+}
+
// clearEditingModes ensures only one editing mode is active at a time
func (m *Model) clearEditingModes() {
m.annotating = false
@@ -405,7 +413,7 @@ func (m *Model) startBlink(id int, markDone bool) tea.Cmd {
}
}
ctx, cancel := m.taskOperationContext()
- err := task.DoneContext(ctx, id)
+ err := m.taskwarriorClient().DoneContext(ctx, id)
cancel()
if err != nil {
m.showError(err)
@@ -439,7 +447,15 @@ func (m *Model) startBlink(id int, markDone bool) tea.Cmd {
// New creates a new UI model with the provided rows.
func New(filters []string, browserCmd string) (Model, error) {
- m := Model{filters: filters, browserCmd: browserCmd, agentFilterHotkey: "3", blinkState: blinkState{blinkEnabled: true}}
+ return NewWithTaskwarrior(filters, browserCmd, task.NewTaskwarrior())
+}
+
+// NewWithTaskwarrior creates a UI model using the provided Taskwarrior client.
+func NewWithTaskwarrior(filters []string, browserCmd string, tw task.Taskwarrior) (Model, error) {
+ if tw == nil {
+ tw = task.NewTaskwarrior()
+ }
+ m := Model{filters: filters, browserCmd: browserCmd, agentFilterHotkey: "3", taskwarrior: tw, blinkState: blinkState{blinkEnabled: true}}
m.initTaskContext()
m.annotateInput = textinput.New()
m.annotateInput.Prompt = "annotation: "
@@ -523,12 +539,12 @@ func (m *Model) fetchTasks() (reloadData, error) {
ctx, cancel := m.taskOperationContext()
defer cancel()
- tasks, err := task.Export(ctx, filters...)
+ tasks, err := m.taskwarriorClient().Export(ctx, filters...)
if err != nil {
return reloadData{}, err
}
- task.SortTasks(tasks)
+ m.taskwarriorClient().SortTasks(tasks)
return reloadData{
tasks: tasks,
ultraFilterIDs: m.ultraFilteredTaskIDs(),
@@ -537,9 +553,9 @@ func (m *Model) fetchTasks() (reloadData, error) {
func (m *Model) processTasks(data *reloadData) {
m.tasks = data.tasks
- m.total = task.TotalTasks(data.tasks)
- m.inProgress = task.InProgressTasks(data.tasks)
- m.due = task.DueTasks(data.tasks, time.Now())
+ m.total = m.taskwarriorClient().TotalTasks(data.tasks)
+ m.inProgress = m.taskwarriorClient().InProgressTasks(data.tasks)
+ m.due = m.taskwarriorClient().DueTasks(data.tasks, time.Now())
if m.showTaskDetail {
m.refreshCurrentTaskDetail()
@@ -767,7 +783,7 @@ func (m *Model) handleBlinkMsg() (tea.Model, tea.Cmd) {
}
}
ctx, cancel := m.taskOperationContext()
- err := task.DoneContext(ctx, id)
+ err := m.taskwarriorClient().DoneContext(ctx, id)
cancel()
if err != nil {
m.showError(err)
diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go
index 0920deb..fdd2e9b 100644
--- a/internal/ui/table_test.go
+++ b/internal/ui/table_test.go
@@ -19,6 +19,70 @@ import (
"codeberg.org/snonux/tasksamurai/internal/task"
)
+type fakeTaskwarrior struct {
+ task.Client
+
+ tasks []task.Task
+ exportFilters [][]string
+ addLines []string
+}
+
+var _ task.Taskwarrior = (*fakeTaskwarrior)(nil)
+
+func (f *fakeTaskwarrior) Export(_ context.Context, filters ...string) ([]task.Task, error) {
+ f.exportFilters = append(f.exportFilters, append([]string(nil), filters...))
+ return append([]task.Task(nil), f.tasks...), nil
+}
+
+func (f *fakeTaskwarrior) AddLineContext(_ context.Context, line string) error {
+ f.addLines = append(f.addLines, line)
+ f.tasks = append(f.tasks, task.Task{
+ ID: len(f.tasks) + 1,
+ UUID: fmt.Sprintf("fake-%d", len(f.tasks)+1),
+ Description: line,
+ Status: "pending",
+ })
+ return nil
+}
+
+func TestNewWithTaskwarriorUsesFakeForAddTask(t *testing.T) {
+ fake := &fakeTaskwarrior{
+ tasks: []task.Task{
+ {ID: 1, UUID: "fake-1", Description: "existing", Status: "pending"},
+ },
+ }
+
+ m, err := NewWithTaskwarrior([]string{"project:home"}, "firefox", fake)
+ if err != nil {
+ t.Fatalf("NewWithTaskwarrior: %v", err)
+ }
+
+ if len(fake.exportFilters) != 1 {
+ t.Fatalf("export calls = %d, want 1", len(fake.exportFilters))
+ }
+ if !reflect.DeepEqual(fake.exportFilters[0], []string{"project:home", "status:pending"}) {
+ t.Fatalf("export filters = %v", fake.exportFilters[0])
+ }
+
+ m.addingTask = true
+ m.addInput.SetValue("new task +agent")
+ mv, _ := (&m).Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ m = *mv.(*Model)
+
+ if !reflect.DeepEqual(fake.addLines, []string{"new task +agent"}) {
+ t.Fatalf("add lines = %v", fake.addLines)
+ }
+ if len(fake.exportFilters) != 2 {
+ t.Fatalf("export calls after add = %d, want 2", len(fake.exportFilters))
+ }
+ if len(m.tasks) != 2 {
+ t.Fatalf("model tasks = %d, want 2", len(m.tasks))
+ }
+ if m.tasks[1].Description != "new task +agent" {
+ t.Fatalf("added task description = %q", m.tasks[1].Description)
+ }
+}
+
func TestAnnotateHotkey(t *testing.T) {
tmp := t.TempDir()
taskPath := filepath.Join(tmp, "task")
diff --git a/internal/ui/ultra.go b/internal/ui/ultra.go
index 656ffb7..999ad73 100644
--- a/internal/ui/ultra.go
+++ b/internal/ui/ultra.go
@@ -1144,7 +1144,7 @@ func (m *Model) handleUltraEditTask() (tea.Model, tea.Cmd) {
}
m.editID = id
- return m, editCmd(id)
+ return m, m.editCmd(id)
}
func (m *Model) handleUltraToggleStart() (tea.Model, tea.Cmd) {