Undo & Redo
The store keeps two stacks of history entries: the past and the future. Every mutation call that changed something pushes an entry onto the past — and clears the future, because a new change invalidates any previously undone ones (the classic linear-history model).
store.bump_count() # past: [bump], future: []
store.undo() # past: [], future: [bump]
store.redo() # past: [bump], future: []
undo() pops the most recent entry, applies its reverse patches to the state and pushes the entry onto the future. redo() does the mirror image. Both are no-ops when their stack is empty, so it is always safe to call them.
Enabling UI affordances
can_undo and can_redo report whether the respective stack is non-empty. The history stacks are reactive, so these properties can be watched — a menu item or toolbar button stays in sync by itself:
from observ import watch
watcher = watch(
lambda: store.can_undo,
lambda enabled: undo_action.setEnabled(enabled),
sync=True,
immediate=True,
)
For user-facing labels such as "Undo Add item", see mutation context.
Granularity
One mutation call is one undo step. If a user-level action spans several state changes, put them in one mutation — or let one mutation call others: nested mutations collapse into a single entry on the outermost call, which is the transactional boundary.