Redox devlog 0: planning
Redox lives in this GitHub repository.
First things first: what is Redox?
I'm entering the final semester of my computer science degree, which means it's time to start my capstone project. I've been building a Rust TUI framework called MinUI, and the capstone gives me four months to find out whether it works outside its own examples.
Redox will be a terminal text editor built on MinUI. More bluntly, it will be a Vim clone.
Four months is not enough time to reproduce all of Vim, but it is enough time to build the editing basics and expose the weak parts of my framework.
MinUI is an immediate-mode framework. It rebuilds the UI every frame instead of mutating a retained widget tree. That model is similar to a game loop and should suit an editor whose state changes after nearly every input.
Redox will use MinUI's UiScene routing for focus and events. Its deferred cursor API applies cursor changes once per frame to prevent flicker. The framework already has the containers, text rendering, and scrollbars I need for the first version.
The name is a chemistry joke. Redox reactions include oxidation, which gives us rust. "Dox" also sounds like "docs." I thought it was clever, so now we're all stuck with it.
Project architecture
MinUI already has immediate-mode rendering, UiScene routing, and a small widget set. Redox will probably use all of them.
The main architectural decision is to keep the editor core separate from the TUI layer.
editor_core will contain the Rust logic for buffers, cursor math, the undo tree, and editing actions. It will know nothing about terminals or MinUI, so standard unit tests can exercise the editor logic.
editor_tui will be the adapter. It will render state with MinUI, capture keyboard and mouse events, and translate them into actions for the core.
The workspace layout
A Cargo workspace will keep the two parts separate:
redox/
├── crates/
│ ├── editor_core/ # Buffer logic, action/effect system, undo tree
│ └── editor_tui/ # MinUI implementation, widgets, input translation
The data model
The minimum viable product will support one buffer. Tabs and split views can wait until basic editing works properly.
The text buffer
I plan to store text in a rope. A single string is easier to start with, but insertion and deletion cost O(n). That becomes painful with large logs or minified JavaScript.
A rope stores text as a tree of smaller strings. Insertions and deletions cost O(log n), and the editor can retrieve only the lines visible in the viewport. It also avoids requiring one contiguous block of memory for the entire file.
Ropes make indexing more complicated, but that cost is worthwhile for a Vim-like editor that should remain responsive on large files.
View state and cursors
Cursor movement is one of those details nobody notices until an editor gets it wrong. Vertical movement cannot simply keep the same column number.
If the cursor starts at column 10, moves to a five-character line, and then moves back, it should return to column 10. The short line temporarily clamps the visible cursor to column 5 without changing the preferred column.
I will track the cursor in terminal cells instead of raw character offsets.
Most rope implementations can convert between byte offsets, character indices, and line numbers. Redox needs to map:
(line, col_cells) to and from a rope index
Redox will store preferred_col_cells so vertical movement returns to the intended column after crossing a shorter line.
The action and effect system
An action and effect pattern will keep the UI separate from the core.
editor_tuireceives keyboard and mouse events.- The TUI translates each event into an action such as
MoveDownorInsertChar('a'). - The core processes the action and returns effects such as
WriteFile,SetStatus, orExit.
The core never performs terminal or file operations itself. It describes what the TUI should do next.
For example:
// Pseudocode
let effects = editor.apply(Action::InsertChar('a'));
for effect in effects {
match effect {
Effect::WriteFile { path, contents } => fs::write(path, contents)?,
Effect::SetStatus(msg) => statusline.set(msg),
Effect::Quit => return Ok(()),
}
}
This leaves input translation and side effects in the TUI while the core owns editor state.
Testing strategy
Because editor_core does not depend on a terminal, unit tests can cover:
- Cursor movement, including empty lines and Unicode
- Insert/delete operations
- Undo/redo correctness in the undo tree
- Search result positions
The editor_tui layer will need only a few input-mapping tests. Most correctness checks belong in the core.
Roadmap
I have about four months. Plans like this never survive intact, but here is the version I am starting with:
| Phase | Approximate timeline | Focus | Key features |
|---|---|---|---|
| 1: The MVP | Month 1 | Core functionality | File I/O, basic editing (insert/normal modes), scrolling |
| 2: The road to Vim | Month 2 | Editing flow | Visual mode, yank/paste, undo tree, search |
| 3: Polish | Month 3 | Quality of life | Statusline, commands such as :w and :q, multiple buffers |
| 4: If I have time... | Month 4? | Some extra "nice to have" features | Things like syntax highlighting with treesitter, LSP support, and custom config files |