diff options
| author | Paul Bütow <1224732+snonux@users.noreply.github.com> | 2025-06-20 21:14:52 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-20 21:14:52 +0300 |
| commit | fbe2cb7b091d8b10f70bf90081f9334e5720b100 (patch) | |
| tree | c7620fffda01abf4baaa38226ad2feef268c6720 | |
| parent | da2d738eaa5e297c44aba89ad9a3fc9df9a45ca0 (diff) | |
| parent | ef46540bb01703ddf05f4397c89771bb4080bc0d (diff) | |
Merge pull request #41 from snonux/codex/add-undo-feature-for-task-completion
Add undo hotkey
| -rw-r--r-- | internal/ui/table.go | 11 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 55 |
2 files changed, 66 insertions, 0 deletions
diff --git a/internal/ui/table.go b/internal/ui/table.go index 50fb7d1..d7881d7 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -39,6 +39,8 @@ type Model struct { filters []string tasks []task.Task + undoStack []int + total int inProgress int due int @@ -219,9 +221,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]) @@ -287,6 +297,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 0ce8c27..1161ec2 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") |
