package command import ( "context" "fmt" "sync" "task/internal/ticket" ) // Executor runs commands and supports undo/redo of successful ones. type Executor interface { Do(cmd Command) error Undo() error Redo() error CanUndo() bool CanRedo() bool LastUndoName() string LastRedoName() string } // History is an undo/redo stack for Command values, optionally persisted to disk. type History struct { mu sync.Mutex undoS []Command redoS []Command path string store ticket.Store timeStore ticket.TimeStore max int } // NewHistory creates an empty in-memory command history (cap MaxHistory). func NewHistory() *History { return &History{max: MaxHistory} } // OpenHistory loads a persisted history from path (DataDir/history.json) or // starts empty if the file is missing. Subsequent Do/Undo/Redo write back. // timeStore may be nil if time-entry commands are unused. func OpenHistory(path string, store ticket.Store, timeStore ticket.TimeStore) (*History, error) { h := &History{ path: path, store: store, timeStore: timeStore, max: MaxHistory, } undo, redo, err := loadHistoryFile(path, store, timeStore) if err != nil { return nil, err } h.undoS = undo h.redoS = redo return h, nil } // Do executes cmd and pushes it onto the undo stack. The redo stack is cleared. func (h *History) Do(cmd Command) error { if err := cmd.Execute(context.Background()); err != nil { return err } h.mu.Lock() defer h.mu.Unlock() h.undoS = append(h.undoS, cmd) h.undoS = trimStack(h.undoS, h.max) h.redoS = nil return h.persistLocked() } // Undo reverses the last successful command. func (h *History) Undo() error { h.mu.Lock() if len(h.undoS) == 0 { h.mu.Unlock() return fmt.Errorf("nothing to undo") } cmd := h.undoS[len(h.undoS)-1] h.undoS = h.undoS[:len(h.undoS)-1] h.mu.Unlock() if err := cmd.Undo(context.Background()); err != nil { h.mu.Lock() h.undoS = append(h.undoS, cmd) h.mu.Unlock() return err } h.mu.Lock() defer h.mu.Unlock() h.redoS = append(h.redoS, cmd) h.redoS = trimStack(h.redoS, h.max) return h.persistLocked() } // Redo re-applies the last undone command. func (h *History) Redo() error { h.mu.Lock() if len(h.redoS) == 0 { h.mu.Unlock() return fmt.Errorf("nothing to redo") } cmd := h.redoS[len(h.redoS)-1] h.redoS = h.redoS[:len(h.redoS)-1] h.mu.Unlock() if err := cmd.Execute(context.Background()); err != nil { h.mu.Lock() h.redoS = append(h.redoS, cmd) h.mu.Unlock() return err } h.mu.Lock() defer h.mu.Unlock() h.undoS = append(h.undoS, cmd) h.undoS = trimStack(h.undoS, h.max) return h.persistLocked() } // CanUndo reports whether Undo would succeed. func (h *History) CanUndo() bool { h.mu.Lock() defer h.mu.Unlock() return len(h.undoS) > 0 } // CanRedo reports whether Redo would succeed. func (h *History) CanRedo() bool { h.mu.Lock() defer h.mu.Unlock() return len(h.redoS) > 0 } // LastUndoName returns the name of the next undo command, or "". func (h *History) LastUndoName() string { h.mu.Lock() defer h.mu.Unlock() if len(h.undoS) == 0 { return "" } return h.undoS[len(h.undoS)-1].Name() } // LastRedoName returns the name of the next redo command, or "". func (h *History) LastRedoName() string { h.mu.Lock() defer h.mu.Unlock() if len(h.redoS) == 0 { return "" } return h.redoS[len(h.redoS)-1].Name() }