2 unstable releases
Uses new Rust 2024
| new 0.2.0 | Feb 4, 2026 |
|---|---|
| 0.1.0 | Jan 27, 2026 |
#387 in Data structures
125KB
2K
SLoC
Sundo - Snapshot Undo/Redo
A flexible and efficient undo/redo library for Rust, with first-class support for persistent data structures.
Features
- Generic undo/redo for any cloneable type
- Optimized for persistent data structures (
im::Vector,im::HashMap, etc.) - Transaction support with automatic rollback (RAII)
- History limits (count-based and memory-based) to prevent unbounded memory growth
- In-place updates with
replace_current()for ephemeral state (navigation, UI state) - Serialization support via snapshot export/import
- Builder pattern for flexible configuration
- Type-safe and ergonomic API
- Zero-cost abstractions when features aren't used
Installation
[dependencies]
sundo = "0.1.0"
Quick Start
Basic Usage
use sundo::{Actions, UndoRedo};
let mut undo_redo = UndoRedo::new();
// Push initial state
undo_redo.push(42, "Initial".to_string());
// Make changes
undo_redo.update(|x| x + 10, "Add 10".to_string());
undo_redo.update(|x| x * 2, "Multiply by 2".to_string());
// Undo and redo
assert_eq!(*undo_redo.present().unwrap(), 104);
undo_redo.undo();
assert_eq!(*undo_redo.present().unwrap(), 52);
undo_redo.redo();
assert_eq!(*undo_redo.present().unwrap(), 104);
With Persistent Data Structures
use sundo::{PersistentActions, PersistentUndoRedo};
use im::Vector;
let mut undo_redo = PersistentUndoRedo::new();
undo_redo.push(Vector::new(), "Empty".to_string());
undo_redo.update(|v| v.push_back(1), "Add 1".to_string());
undo_redo.update(|v| v.push_back(2), "Add 2".to_string());
// No Rc overhead - direct storage with structural sharing
let current = undo_redo.present().unwrap();
assert_eq!(current.len(), 2);
With History Limits
use sundo::UndoRedoBuilder;
// Count-based limit
let mut undo_redo = UndoRedoBuilder::new()
.with_max_entries(100) // Keep only last 100 states
.build();
// Memory-based limit
let mut undo_redo = UndoRedoBuilder::new()
.with_max_memory_mb(50) // Keep up to 50 MB of history
.build();
// Combined limits (whichever is hit first)
let mut undo_redo = UndoRedoBuilder::new()
.with_max_entries(1000)
.with_max_memory_mb(100)
.build();
// Automatically prunes oldest entries when limit exceeded
With Transactions
use sundo::{Actions, UndoRedo, ScopedTransaction};
let mut undo_redo = UndoRedo::new();
undo_redo.push(0, "Initial".to_string());
{
let mut tx = ScopedTransaction::begin(&mut undo_redo, "Batch update");
tx.get().update(|x| x + 1, "temp".to_string());
tx.get().update(|x| x + 2, "temp".to_string());
tx.commit(); // Commits as single entry "Batch update"
}
// Or auto-rollback on error:
{
let mut tx = ScopedTransaction::begin(&mut undo_redo, "Risky operation");
tx.get().update(|x| x + 100, "temp".to_string());
// If we panic or return early, transaction auto-rolls back
}
Ephemeral State Updates
For state changes that shouldn't be undoable (like navigation or UI state):
use sundo::{PersistentActions, PersistentUndoRedo};
use im::{HashMap, Vector};
#[derive(Clone)]
struct AppState {
todos: Vector<String>,
current_page: String, // Navigation - shouldn't be undoable
}
let mut undo_redo = PersistentUndoRedo::new();
// ... add some todos (undoable) ...
// Change page without creating history entry
undo_redo.replace_current(|state| AppState {
todos: state.todos.clone(),
current_page: "Settings".to_string(),
});
// Undo still works on todos, navigation stays at Settings
undo_redo.undo(); // Reverts todo changes, keeps current_page
API Overview
Core Traits
Actions<T>- Standard undo/redo operations withRcwrappingPersistentActions<T>- Optimized for persistent data structures (noRc)
Core Types
UndoRedo<T>- Standard implementation for any cloneable typePersistentUndoRedo<T>- Optimized forim::types
Builder Pattern
UndoRedoBuilder<T>- Configure with capacity and limitsPersistentUndoRedoBuilder<T>- Builder for persistent variant
Transactions
ScopedTransaction- RAII transaction with auto-rollbackPersistentScopedTransaction- Transaction for persistent types
Memory Estimation
MemoryFootprint- Trait for memory-based history limits
Examples
See the demos/ directory for complete examples:
- basic-example - Comparison of standard vs persistent implementations
- persistent-example - Deep dive into persistent data structures
- scoped-transaction - Transaction patterns and error handling with RAII
- builder-pattern - History limits configuration
- memory-limits - Memory-based limits with MemoryFootprint trait
- mem-performance - Comprehensive memory performance testing and analysis
- serialization-example - Save/load history without serde
- delta-example - Compute diffs between history states
- todo_app - Full TUI todo application with undo/redo, transactions, save/load
Run demos with:
cd demos/basic-example && cargo run
cd demos/memory-limits && cargo run
cd demos/delta-example && cargo run
cd demos/todo_app && cargo run
# etc.
Note: All demos use the library via a local path dependency:
[dependencies]
sundo = { path = "../.." }
This is a good pattern for your own projects during development.
Design Philosophy
Memory Efficiency
For standard types, sundo uses Rc<T> to minimize memory overhead when cloning is expensive. For persistent data structures (which already use structural sharing), sundo stores values directly without the Rc layer.
Persistent Data Structures
When using im::Vector, im::HashMap, etc., the library leverages their built-in structural sharing. This means history entries share most of their data, making undo/redo memory-efficient even for large collections.
Zero-Cost Abstractions
- Builder pattern only allocates when configured
- History limits only check when set
- Transaction overhead is minimal (single Option field)
Documentation
- Usage Guide - for additional info
Generate API documentation locally:
cargo doc --open
Use Cases
- we will find out...
License
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Dependencies
~250KB