Redox devlog 1: the MVP
Once again, here's a link to the repo.

I spent way longer on this than you'd think!
Checking in
Redox is the Vim-like text editor I'm building for my university capstone. It is written in Rust on top of MinUI, my TUI framework.
So, what does it look like now?
This minimum viable product is the first version that feels like an editor instead of a prototype held together by spit and caffeine.
The editor logic and UI code now live in separate crates. I'm mostly happy with that boundary, though large files still expose performance problems. The demo below runs from an optimizations branch for a reason.
Demo
Redox can open and save files, stores text in a Ropey buffer, and supports most basic Vim motions. You can even quit it with :q, so at least I got the important part right.
Project architecture
The workspace remains split into two crates:
editor_coreowns the Ropey buffer, positions, editing operations, and motions such asw,b,e,gg, andG.editor_tuiowns the MinUI loop, input mapping, rendering, cursor projection, and statusline.
editor_core knows nothing about MinUI or terminal widgets. editor_tui does not define how editing works. That separation has already made changes and tests less painful.
The workspace layout
redox/
├── crates/
│ ├── editor_core/ # Buffer logic + motions + core editing primitives
│ └── editor_tui/ # MinUI loop + input mapping + rendering/widgets
Why Redox uses a rope and position types
Redox uses logical positions such as Pos { line, col } and character-based indexing for core operations. This keeps Unicode handling predictable and avoids confusing byte offsets with visual columns.
The text lives in ropey::Rope, not one giant String.
That keeps frequent insertions and deletions practical as files grow. It also provides line and character indexing, which an editor needs constantly.
Redox currently slows down on files larger than about 1 MB. Ropey should handle that size without trouble, so the bottleneck probably sits in MinUI's rendering loop, one of my abstractions, or the terminal itself. One megabyte is larger than most source files, but an editor should still cope with it.
Input pipeline
Input follows three steps:
- MinUI receives an event such as a keystroke.
editor_tuimaps them into high-levelInputActions.EditorState::apply_inputapplies the action to the buffer and cursor state.
The mapper handles Normal, Insert, and Command modes. Visual mode comes later. It also handles count prefixes such as 3w and multi-key sequences such as gg.
// Tiny prefix state machine that enables gg and count prefixes without
// bloating the key handling.
#[derive(Debug, Default, Clone)]
pub struct InputState {
pending_g: bool,
pending_count: Option<usize>,
}
impl InputState {
pub fn new() -> Self {
Self::default()
}
pub fn reset_prefixes(&mut self) {
self.pending_g = false;
self.pending_count = None;
}
fn push_count_digit(&mut self, d: u8) {
debug_assert!(d <= 9);
let current = self.pending_count.unwrap_or(0);
let next = current.saturating_mul(10).saturating_add(d as usize);
self.pending_count = Some(next);
}
fn take_count_or_1(&mut self) -> usize {
match self.pending_count.take() {
Some(0) | None => 1,
Some(n) => n,
}
}
}
Each keypress becomes an explicit action:
Motion { motion, count }EnterInsert(InsertKind::AppendLineEnd)CommandEnterPaste(String)
This gives editing behaviour one path through the code and makes it easier to test.
Cursor and viewport
Cursor behaviour can make or break a text editor.
Redox clamps cursor positions to the buffer and remembers the preferred column during vertical movement. The viewport follows the cursor with scrolloff-style margins, then adjusts those margins near the end of the file. Horizontal movement and scrolling use terminal cell widths instead of character counts.
Wide glyphs and tabs break simple column math. Redox segments graphemes and calculates their cell widths so the cursor and rendered text stay aligned.
Rendering and caching
The renderer builds a viewport snapshot from the core buffer and draws only visible rows.
// To avoid re-segmenting text every frame, rendered lines are cached
// by (line, hash).
// Cache hits are cheap, and misses pay segmentation cost only once.
pub fn graphemes_for_line<'a>(
&'a mut self,
line_idx: usize,
line_text: &str,
) -> &'a [Box<str>] {
self.tick = self.tick.wrapping_add(1);
let h = hash64(line_text);
if let Some(pos) = self
.entries
.iter()
.position(|e| e.line_idx == line_idx && e.hash == h)
{
// Bump usage
self.entries[pos].last_used_tick = self.tick;
return &self.entries[pos].graphemes;
}
// Miss: segment and insert.
let graphemes: Vec<Box<str>> = line_text
.graphemes(true)
.map(|g| g.to_owned().into_boxed_str())
.collect();
...
The MVP caches graphemes by line index and hash, so unchanged lines skip segmentation on later frames. A separate path avoids full-line allocations for unusually long lines, though it still needs work. The renderer also clips by terminal cell width so wide characters do not overflow the layout.
Redox does not soft-wrap yet. It uses horizontal scrolling, which matches the modal editors I am copying.
Command mode and statusline
Command mode supports:
:w:q:q!:wq
Redox tracks unsaved changes, so :q warns before discarding them.
The statusline shows:
- Current mode (
NORMAL,INSERT,COMMAND) - Active file path, status message, or the command as appropriate
- Cursor line/column
What works in the MVP
- Opening a file with
redox <file_name/location> - Modal editing flow (Normal/Insert/Command)
- Basic editing (insert, backspace, newline, bracketed paste)
- Vim-style motions and counts (
h/j/k/l,w/b/e,gg,G) - Viewport tracking and scrolling
- Write/quit command workflow
Deliberate tradeoffs
Several parts are still simple:
- UTF-8 file handling only
- Whole-file read/write path
- Single-buffer model
- No undo tree yet
I want a stable loop for input, editing, and rendering before I add larger features.
Roadmap
| Stage | Current status | Main focus | Highlights |
|---|---|---|---|
| Core MVP | Done | Editor foundation | Rope buffer, motions, core modal system, write/quit, statusline |
| Next step | In progress | Editing depth | Richer motion/command set, visual mode, improved performance, multi-buffer workflows |
| Later | Planned | Power features | Undo/redo model, polishing ergonomics, search, possibly syntax highlighting and LSP support |
The internal boundaries are the hardest part to change later. Redox has reached the point where I can add features without first tearing apart the editor core. That is a modest milestone, but it feels good.