# `Plushie.Undo`
[🔗](https://github.com/plushie-ui/plushie-elixir/blob/v0.7.2/lib/plushie/undo.ex#L1)

Undo/redo stack for reversible commands. Pure data structure, no processes.

Each command provides an `apply` function and an `undo` function. The stack
tracks entries as `{apply_fn, undo_fn}` pairs so that undo moves an entry
to the redo stack (calling `undo_fn`) and redo moves it back (calling
`apply_fn`).

## Max size

The undo stack is bounded by `:max_size` (default 100).
When a push exceeds the limit, the oldest entries are dropped. The redo
stack is unbounded (it can only shrink or be cleared, never grow past
the undo stack size).

## Coalescing

Commands with the same `:coalesce` key that arrive within
`:coalesce_window_ms` of each other are merged into a single undo entry.
The merged entry keeps the *original* undo function (so one undo reverses
all coalesced changes) and composes the apply functions.

## Example

    iex> u = Plushie.Undo.new(0)
    iex> cmd = %{apply: &(&1 + 1), undo: &(&1 - 1)}
    iex> u = Plushie.Undo.push(u, cmd)
    iex> Plushie.Undo.current(u)
    1
    iex> u = Plushie.Undo.undo(u)
    iex> Plushie.Undo.current(u)
    0

# `command`

```elixir
@type command() :: %{
  :apply =&gt; (term() -&gt; term()),
  :undo =&gt; (term() -&gt; term()),
  optional(:label) =&gt; String.t(),
  optional(:coalesce) =&gt; term(),
  optional(:coalesce_window_ms) =&gt; non_neg_integer()
}
```

# `entry`

```elixir
@type entry() :: %{
  apply_fn: (term() -&gt; term()),
  undo_fn: (term() -&gt; term()),
  label: String.t() | nil,
  coalesce: term() | nil,
  timestamp: integer()
}
```

# `t`

```elixir
@type t() :: %Plushie.Undo{
  current: term(),
  max_size: pos_integer(),
  redo_stack: [entry()],
  undo_size: non_neg_integer(),
  undo_stack: [entry()]
}
```

# `can_redo?`

```elixir
@spec can_redo?(undo :: t()) :: boolean()
```

Return `true` if there are entries on the redo stack.

# `can_undo?`

```elixir
@spec can_undo?(undo :: t()) :: boolean()
```

Return `true` if there are entries on the undo stack.

# `current`

```elixir
@spec current(undo :: t()) :: term()
```

Return the current model.

# `history`

```elixir
@spec history(undo :: t()) :: [String.t() | nil]
```

Return the labels from the undo stack, most recent first.

# `new`

```elixir
@spec new(model :: term(), opts :: keyword()) :: t()
```

Create a new undo stack with `model` as the initial state.

## Options

  * `:max_size` - maximum number of undo entries (default 100).
    When exceeded, the oldest entries are dropped silently.

# `push`

```elixir
@spec push(undo :: t(), command :: command()) :: t()
```

Push a command onto the undo stack, updating the current model. Clears
the redo stack.

The command must be a map with `:apply` and `:undo` keys (both single-arity
functions). Optional keys: `:label`, `:coalesce`, `:coalesce_window_ms`.

If the command carries a `:coalesce` key that matches the top of the undo
stack and the time delta is within `:coalesce_window_ms`, the entry is
merged rather than pushed.

When the undo stack exceeds `:max_size`, the oldest entries are dropped.

# `redo`

```elixir
@spec redo(undo :: t()) :: t()
```

Redo the last undone command. Returns unchanged if the redo stack is empty.

# `undo`

```elixir
@spec undo(undo :: t()) :: t()
```

Undo the last command. Returns unchanged if the undo stack is empty.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
