summaryrefslogtreecommitdiff
path: root/internal/ui/keyactions.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/ui/keyactions.go')
-rw-r--r--internal/ui/keyactions.go202
1 files changed, 177 insertions, 25 deletions
diff --git a/internal/ui/keyactions.go b/internal/ui/keyactions.go
index 15e1b06..81ab468 100644
--- a/internal/ui/keyactions.go
+++ b/internal/ui/keyactions.go
@@ -63,6 +63,29 @@ func (m *Model) handleMarkDone() (tea.Model, tea.Cmd) {
return m, m.startBlink(id, true)
}
+func (m *Model) handleDeleteTask() (tea.Model, tea.Cmd) {
+ tsk := m.getTaskForDelete()
+ if tsk == nil {
+ return m, nil
+ }
+
+ count, recurring, err := m.deleteTaskWithUndo(*tsk)
+ if err != nil {
+ m.showError(err)
+ return m, nil
+ }
+ if !m.reloadAndReport() {
+ return m, nil
+ }
+
+ if recurring {
+ m.statusMsg = fmt.Sprintf("Deleted %d recurring tasks", count)
+ } else {
+ m.statusMsg = "Deleted task"
+ }
+ return m, nil
+}
+
func (m *Model) handleOpenURL() (tea.Model, tea.Cmd) {
task := m.getTaskForOpenURL()
if task == nil {
@@ -95,13 +118,14 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) {
return m, nil
}
- uuid := m.undoStack[len(m.undoStack)-1]
- m.undoStack = m.undoStack[:len(m.undoStack)-1]
-
- if err := task.SetStatusUUID(uuid, "pending"); err != nil {
- m.showError(err)
- return m, nil
+ action := m.undoStack[len(m.undoStack)-1]
+ for _, restore := range action.restores {
+ if err := task.SetStatusUUID(restore.uuid, restore.status); err != nil {
+ m.showError(err)
+ return m, nil
+ }
}
+ m.undoStack = m.undoStack[:len(m.undoStack)-1]
// Reload the task list to get the updated task with its new ID
if err := m.reload(); err != nil {
@@ -112,10 +136,15 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) {
// Find the task ID for blinking
var id int
var found bool
- for _, tsk := range m.tasks {
- if tsk.UUID == uuid {
- id = tsk.ID
- found = true
+ for _, restore := range action.restores {
+ for _, tsk := range m.tasks {
+ if tsk.UUID == restore.uuid {
+ id = tsk.ID
+ found = true
+ break
+ }
+ }
+ if found {
break
}
}
@@ -123,34 +152,157 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) {
// If task not found or has ID 0, try to get it directly from Taskwarrior
if !found || id == 0 {
// Use task export with UUID filter to get the specific task
- filters := []string{uuid}
- if m.filters != nil {
- filters = append(filters, m.filters...)
- }
- filters = append(filters, "status:pending")
-
- tasks, err := task.Export(filters...)
- if err == nil && len(tasks) > 0 {
- id = tasks[0].ID
- // Also update our local task list
- for i, tsk := range m.tasks {
- if tsk.UUID == uuid {
- m.tasks[i].ID = id
- break
+ for _, restore := range action.restores {
+ filters := []string{restore.uuid}
+ if m.filters != nil {
+ filters = append(filters, m.filters...)
+ }
+ filters = append(filters, "status:"+restore.status)
+
+ tasks, err := task.Export(filters...)
+ if err == nil && len(tasks) > 0 {
+ id = tasks[0].ID
+ // Also update our local task list
+ for i, tsk := range m.tasks {
+ if tsk.UUID == restore.uuid {
+ m.tasks[i].ID = id
+ break
+ }
}
+ break
}
}
}
// If we still don't have a valid ID, don't try to blink
if id == 0 {
- m.statusMsg = "Task restored"
+ m.statusMsg = undoStatus(action)
return m, nil
}
return m, m.startBlink(id, false)
}
+func (m *Model) getTaskForDelete() *task.Task {
+ if m.showTaskDetail && m.currentTaskDetail != nil {
+ return m.currentTaskDetail
+ }
+ return m.getTaskAtCursor()
+}
+
+func (m *Model) deleteTaskWithUndo(tsk task.Task) (int, bool, error) {
+ if strings.TrimSpace(tsk.UUID) == "" {
+ return 0, false, fmt.Errorf("task %d has no UUID", tsk.ID)
+ }
+
+ recurring := isRecurringTask(tsk)
+ tasks := []task.Task{tsk}
+ if recurring {
+ series, err := task.RecurringSeries(recurringRootUUID(tsk))
+ if err != nil {
+ return 0, true, fmt.Errorf("loading recurring series: %w", err)
+ }
+ tasks = mergeTasksByUUID(series, tsk)
+ }
+
+ tasks = deleteOrder(tasks, recurringRootUUID(tsk))
+ restores := make([]undoRestore, 0, len(tasks))
+ for _, candidate := range tasks {
+ if strings.TrimSpace(candidate.UUID) == "" {
+ continue
+ }
+ restores = append(restores, undoRestore{uuid: candidate.UUID, status: undoStatusForTask(candidate)})
+ }
+ if len(restores) == 0 {
+ return 0, recurring, fmt.Errorf("no task UUIDs to delete")
+ }
+
+ completed := make([]undoRestore, 0, len(restores))
+ for _, restore := range restores {
+ if err := task.SetStatusUUID(restore.uuid, "deleted"); err != nil {
+ rollbackUndoRestores(completed)
+ return 0, recurring, fmt.Errorf("deleting task %s: %w", restore.uuid, err)
+ }
+ completed = append(completed, restore)
+ }
+
+ m.pushUndoAction("delete", restores)
+ return len(restores), recurring, nil
+}
+
+func (m *Model) pushUndoAction(label string, restores []undoRestore) {
+ if len(restores) == 0 {
+ return
+ }
+ copied := append([]undoRestore(nil), restores...)
+ m.undoStack = append(m.undoStack, undoAction{label: label, restores: copied})
+}
+
+func isRecurringTask(tsk task.Task) bool {
+ return tsk.Parent != "" || tsk.Status == "recurring" || tsk.RType != "" || tsk.Recur != ""
+}
+
+func recurringRootUUID(tsk task.Task) string {
+ if tsk.Parent != "" {
+ return tsk.Parent
+ }
+ return tsk.UUID
+}
+
+func mergeTasksByUUID(tasks []task.Task, selected task.Task) []task.Task {
+ seen := make(map[string]struct{}, len(tasks)+1)
+ merged := make([]task.Task, 0, len(tasks)+1)
+ for _, tsk := range tasks {
+ if tsk.UUID == "" {
+ continue
+ }
+ if _, ok := seen[tsk.UUID]; ok {
+ continue
+ }
+ seen[tsk.UUID] = struct{}{}
+ merged = append(merged, tsk)
+ }
+ if selected.UUID != "" {
+ if _, ok := seen[selected.UUID]; !ok {
+ merged = append(merged, selected)
+ }
+ }
+ return merged
+}
+
+func deleteOrder(tasks []task.Task, rootUUID string) []task.Task {
+ ordered := make([]task.Task, 0, len(tasks))
+ var root []task.Task
+ for _, tsk := range tasks {
+ if tsk.UUID == rootUUID {
+ root = append(root, tsk)
+ continue
+ }
+ ordered = append(ordered, tsk)
+ }
+ return append(ordered, root...)
+}
+
+func undoStatusForTask(tsk task.Task) string {
+ if tsk.Status == "" || tsk.Status == "deleted" {
+ return "pending"
+ }
+ return tsk.Status
+}
+
+func rollbackUndoRestores(restores []undoRestore) {
+ for i := len(restores) - 1; i >= 0; i-- {
+ _ = task.SetStatusUUID(restores[i].uuid, restores[i].status)
+ }
+}
+
+func undoStatus(action undoAction) string {
+ if action.label == "delete" && len(action.restores) > 1 {
+ return "Tasks restored"
+ }
+ return "Task restored"
+}
+
func (m *Model) handleSetDueDate() (tea.Model, tea.Cmd) {
id, err := m.getSelectedTaskID()
if err != nil {