From ef46540bb01703ddf05f4397c89771bb4080bc0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20B=C3=BCtow?= <1224732+snonux@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:13:33 +0300 Subject: ui: add undo for done tasks --- internal/ui/table.go | 11 ++++++++++ internal/ui/table_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/internal/ui/table.go b/internal/ui/table.go index bd1d4ca..3f0a16f 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -38,6 +38,8 @@ type Model struct { filter string tasks []task.Task + undoStack []int + total int inProgress int due int @@ -227,9 +229,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { idStr := ansi.Strip(row[0]) if id, err := strconv.Atoi(idStr); err == nil { task.Done(id) + m.undoStack = append(m.undoStack, id) m.reload() } } + case "U": + if n := len(m.undoStack); n > 0 { + id := m.undoStack[n-1] + m.undoStack = m.undoStack[:n-1] + task.SetStatus(id, "pending") + m.reload() + } case "d": if row := m.tbl.SelectedRow(); row != nil { idStr := ansi.Strip(row[0]) @@ -295,6 +305,7 @@ func (m Model) View() string { "E: edit task", "s: toggle start/stop", "D: mark task done", + "U: undo done", "d: set due date", "r: random due date", "a: annotate task", diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index f02d587..0891cf7 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -1,6 +1,7 @@ package ui import ( + "fmt" "os" "path/filepath" "strings" @@ -174,6 +175,60 @@ func TestDoneHotkey(t *testing.T) { } } +func TestUndoHotkey(t *testing.T) { + tmp := t.TempDir() + taskPath := filepath.Join(tmp, "task") + logFile := filepath.Join(tmp, "log.txt") + + script := fmt.Sprintf("#!/bin/sh\n"+ + "if echo \"$@\" | grep -q export; then\n"+ + " echo '{\"id\":1,\"uuid\":\"x\",\"description\":\"d\",\"status\":\"pending\",\"entry\":\"\",\"priority\":\"\",\"urgency\":0}'\n"+ + " exit 0\n"+ + "fi\n"+ + "echo \"$@\" >> %s\n", logFile) + + if err := os.WriteFile(taskPath, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + + origPath := os.Getenv("PATH") + os.Setenv("PATH", tmp+":"+origPath) + t.Cleanup(func() { os.Setenv("PATH", origPath) }) + + os.Setenv("TASKDATA", tmp) + os.Setenv("TASKRC", "/dev/null") + t.Cleanup(func() { + os.Unsetenv("TASKDATA") + os.Unsetenv("TASKRC") + }) + + m, err := New("") + if err != nil { + t.Fatalf("New: %v", err) + } + + mv, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'D'}}) + m = mv.(Model) + mv, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'U'}}) + m = mv.(Model) + + data, err := os.ReadFile(logFile) + if err != nil { + t.Fatalf("read log: %v", err) + } + + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + if len(lines) < 2 { + t.Fatalf("expected at least two commands, got %d", len(lines)) + } + if lines[0] != "1 done" { + t.Fatalf("done not called: %q", lines[0]) + } + if lines[1] != "1 modify status:pending" { + t.Fatalf("undo not called: %q", lines[1]) + } +} + func TestDueDateHotkey(t *testing.T) { tmp := t.TempDir() taskPath := filepath.Join(tmp, "task") -- cgit v1.2.3