undo

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: BSD-3-Clause Imports: 1 Imported by: 0

Documentation

Overview

Package undo is a render-agnostic undo/redo command stack for the go-widgets MVVM layer. It has no dependency on any backend: it builds only on the dependency-free primitives of the parent github.com/go-widgets/mvvm package (Observable and Command), so the same stack drives pixel (toolkit) and terminal-cell (tui) views alike.

Model

A Command is a reversible action: Do applies it (and re-applies it on redo), Undo reverts it, and Label names it for the UI. A Stack records the commands the user performs and moves a cursor across them: Stack.Undo steps the cursor back and reverts, Stack.Redo steps it forward and re-applies. Pushing a fresh command after some undos discards the redo tail, exactly like every editor.

Coalescing

Contiguous commands of the same kind can collapse into a single undo step — so a run of keystrokes or a drag gesture undoes in one go rather than one character or pixel at a time. A command opts in by implementing Coalescer; NewCoalescing provides a ready-made key-matched implementation.

Binding to the UI

A Stack exposes its state as MVVM primitives so an "Undo"/"Redo" button or menu item binds with no glue: Stack.UndoCommand/Stack.RedoCommand are [mvvm.Command]s (their CanExecute tracks CanUndo/CanRedo), and Stack.CanUndoBinding, Stack.UndoTextBinding, and their redo twins are [mvvm.Observable]s carrying the enabled flag and the live "Undo <label>" caption. Wire them with the parent package's BindCommand / OneWay adapters.

Threading

Like the rest of the MVVM layer, a Stack is not safe for concurrent use — drive it from the UI goroutine.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Coalescer

type Coalescer interface {
	CoalesceWith(next Command) (merged Command, ok bool)
}

Coalescer is the optional interface a Command implements to merge with the command immediately preceding it on the stack. When Stack.Push is about to record next and the current top command implements Coalescer, it calls top.CoalesceWith(next): returning (merged, true) replaces the top in place so the two collapse into a single undo step, while (nil, false) records next as a new step. This is how a run of same-kind commands (keystrokes, drag deltas) undoes in one action.

type Command

type Command interface {
	Do()
	Undo()
	Label() string
}

Command is a single reversible action recorded on a Stack.

Do applies the action; it is called once when the command is pushed and again on every redo. Undo reverts it, restoring the exact state that existed before Do ran. Label names the action for the UI (e.g. "Typing", "Delete row"); it may be empty. Do and Undo must be exact inverses so that any Do/Undo or Undo/Redo pair round-trips.

func NewCoalescing

func NewCoalescing(key, label string, do, undo func()) Command

NewCoalescing returns a Command like NewCommand that also coalesces: when it is pushed immediately after another NewCoalescing command sharing the same non-empty key, the two collapse into one undo step whose Undo reverts both newest-first and whose Do re-applies both oldest-first. An empty key never coalesces (behaving like New). Coalescing re-applies on each further push, so an entire run folds into a single step.

Example

ExampleNewCoalescing shows a run of keystrokes collapsing into one undo step.

package main

import (
	"fmt"

	"github.com/go-widgets/mvvm/undo"
)

func main() {
	doc := ""
	s := undo.New()

	key := func(r string) undo.Command {
		return undo.NewCoalescing("type", "Typing",
			func() { doc += r },
			func() { doc = doc[:len(doc)-len(r)] })
	}

	for _, r := range []string{"h", "i", "!"} {
		s.Push(key(r))
	}
	fmt.Printf("%q steps=%d\n", doc, s.Len())

	s.Undo() // one undo reverts the whole run
	fmt.Printf("%q\n", doc)
}
Output:
"hi!" steps=1
""

func NewCommand

func NewCommand(label string, do, undo func()) Command

NewCommand returns a non-coalescing Command that runs do on Do (and redo), runs undo on Undo, and reports label. do and undo must be non-nil and exact inverses.

type Option

type Option func(*Stack)

Option configures a Stack at construction.

func WithCoalescing

func WithCoalescing(enabled bool) Option

WithCoalescing enables or disables coalescing of contiguous same-kind commands (see Coalescer). Coalescing is on by default.

func WithLimit

func WithLimit(n int) Option

WithLimit caps the number of retained commands at n (n <= 0 ⇒ unlimited, the default). When a push would exceed the cap, the oldest commands are dropped — they can no longer be undone, but the current state is untouched.

type Stack

type Stack struct {
	// contains filtered or unexported fields
}

Stack is a render-agnostic undo/redo command stack. It holds the sequence of commands the user has performed and a cursor marking how many are currently applied: everything before the cursor can be undone, everything from the cursor on can be redone. Push after an undo discards the redo tail.

A Stack also publishes its state as MVVM primitives (see Stack.UndoCommand, Stack.CanUndoBinding, Stack.UndoTextBinding and their redo twins) so an Undo/Redo button or menu binds directly with no extra wiring.

The zero Stack is not usable; construct one with New. A Stack is not safe for concurrent use.

Example

ExampleStack shows an app recording edits, then undoing and redoing them. The document below stands in for any model; each command captures the exact inverse so state round-trips.

package main

import (
	"fmt"

	"github.com/go-widgets/mvvm/undo"
)

func main() {
	doc := "" // the model
	s := undo.New()

	write := func(text string) undo.Command {
		return undo.NewCommand("Write "+text,
			func() { doc += text },
			func() { doc = doc[:len(doc)-len(text)] })
	}

	s.Push(write("hello"))
	s.Push(write(" world"))
	fmt.Printf("%q — %s\n", doc, s.UndoTextBinding().Get())

	s.Undo()
	fmt.Printf("%q — %s\n", doc, s.UndoTextBinding().Get())

	s.Redo()
	fmt.Printf("%q\n", doc)
}
Output:
"hello world" — Undo Write  world
"hello" — Undo Write hello
"hello world"

func New

func New(opts ...Option) *Stack

New builds an empty Stack. By default it is unlimited and coalescing is on; pass WithLimit / WithCoalescing to change either.

func (*Stack) CanRedo

func (s *Stack) CanRedo() bool

CanRedo reports whether there is a command to redo.

func (*Stack) CanRedoBinding

func (s *Stack) CanRedoBinding() *mvvm.Observable[bool]

CanRedoBinding returns an mvvm.Observable carrying whether a redo is available.

func (*Stack) CanUndo

func (s *Stack) CanUndo() bool

CanUndo reports whether there is a command to undo.

func (*Stack) CanUndoBinding

func (s *Stack) CanUndoBinding() *mvvm.Observable[bool]

CanUndoBinding returns an mvvm.Observable carrying whether an undo is available — bind it to a widget's enabled/visible state.

func (*Stack) Clear

func (s *Stack) Clear()

Clear empties the stack, discarding all undo and redo history. The current application state is left as-is.

func (*Stack) Cursor

func (s *Stack) Cursor() int

Cursor reports how many commands are currently applied — the number that can be undone.

func (*Stack) Len

func (s *Stack) Len() int

Len reports the number of commands retained (undoable plus redoable).

func (*Stack) Push

func (s *Stack) Push(cmd Command)

Push applies cmd (calling cmd.Do), discards any redo tail, and records it as the newest undoable step. When coalescing is on and the previous top command merges with cmd (see Coalescer), the two collapse into a single step instead of adding one. Honours the configured limit.

func (*Stack) Redo

func (s *Stack) Redo() bool

Redo re-applies the next command and steps the cursor forward, returning true. It is a no-op returning false when there is nothing to redo.

func (*Stack) RedoCommand

func (s *Stack) RedoCommand() *mvvm.Command

RedoCommand returns an mvvm.Command that redoes one step; its CanExecute tracks CanRedo.

func (*Stack) RedoLabel

func (s *Stack) RedoLabel() string

RedoLabel returns the Label of the command that Redo would re-apply, or "" when there is nothing to redo.

func (*Stack) RedoTextBinding

func (s *Stack) RedoTextBinding() *mvvm.Observable[string]

RedoTextBinding returns an mvvm.Observable carrying the live redo caption, "Redo" or "Redo <label>".

func (*Stack) Undo

func (s *Stack) Undo() bool

Undo reverts the most recently applied command and steps the cursor back, returning true. It is a no-op returning false when there is nothing to undo.

func (*Stack) UndoCommand

func (s *Stack) UndoCommand() *mvvm.Command

UndoCommand returns an mvvm.Command that undoes one step; its CanExecute tracks CanUndo, so a bound button greys out when there is nothing to undo.

func (*Stack) UndoLabel

func (s *Stack) UndoLabel() string

UndoLabel returns the Label of the command that Undo would revert, or "" when there is nothing to undo.

func (*Stack) UndoTextBinding

func (s *Stack) UndoTextBinding() *mvvm.Observable[string]

UndoTextBinding returns an mvvm.Observable carrying the live button caption: "Undo" when nothing is undoable or the command is unlabelled, else "Undo <label>".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL