What Is RSVP?
Rapid Serial Visual Presentation is a technique from the 1970s that presents text word by word at a controlled speed. The concept is based on an insight from neuroscience: subvocalization — that "inner narrator" reading every word inside your head — is the biggest bottleneck in reading speed.
By forcing the brain to process words at a pace faster than the inner voice, RSVP trains direct reading: from eye to brain, bypassing speech.
Why Does It Work?
According to research on reading comprehension, the human brain can process visual text significantly faster than speech. The problem isn't capacity — it's habit. RSVP breaks that habit through brute force.
This connects directly with Robert Bjork's concept of desirable difficulty: when learning feels hard and uncomfortable, retention improves. The discomfort is a signal that the brain is forming new connections — not that something is wrong.
Why Rust (And Why by Hand)
I chose Rust for the same reason RSVP works: productive discomfort.
Rust doesn't let you escape. Every variable has an owner. Every reference has a lifetime. The compiler won't compile until you prove your code is correct — not "probably correct," but demonstrably correct.
No aids, on purpose
The development setup was deliberately spartan:
| Tool | Role | What It DOESN'T Have |
|---|---|---|
| lite-xl | Text editor | No LSP, no autocompletion, no intellisense |
| bacon | Background code checker | Runs clippy continuously — your only feedback loop |
| Terminal | Compile and run | No IDE, no "play" button |
This follows K. Anders Ericsson's principle of deliberate practice: progress requires sustained effort in the optimal difficulty zone. If it's easy, you're not learning. If it's impossible, you get frustrated. The sweet spot is difficult but achievable — what Vygotsky called the zone of proximal development.
Autocompletion pulls you out of that zone. It gives you the answer before you've formulated the question. The LSP shows you the type before you understand why it matters. For someone learning ownership and lifetimes from scratch, that's counterproductive.
The Agreement with AI
Claude Code was fundamental to this project — but not in the way most people expect.
In the repository's CLAUDE.md file, I established explicit rules:
- Forbidden: Generate, suggest, or show any Rust code — not a single line, no snippets, no pseudocode
- Allowed: Explain theoretical concepts, evaluate crates, reason about compiler errors, discuss architecture, and analyze trade-offs — all in natural language
Why does this model work?
Because it replicates the relationship with an expert mentor. A good teacher doesn't dictate the answer — they ask you the right questions so you arrive at the solution yourself.
In practice, this meant conversations like:
Me: "The compiler is throwing E0507 — move out of borrowed reference"Not a single line of code. Just the right mental model for me to find the solution.
Claude: "That error means you're trying to take ownership of something you're only borrowing. Think about the difference between.iter()which lends you each element and.into_iter()which gives you ownership. In your case, do you need the original element or a copy?"
What Claude did do
The agreement doesn't prohibit everything — it prohibits code. Claude generated:
- Commit messages — structured and descriptive for each feature
- README — complete project documentation with architecture and dependencies
- Crate evaluation — detailed comparisons between
lopdf,pdf-extract,extractous, andoxidize-pdfexplaining encoding trade-offs and maintenance - Conceptual guidance — ownership, borrowing, lifetimes, the Elm Architecture pattern, and Rust's philosophy
This aligns with the concept of scaffolding in education: the support structure that allows the student to reach higher than they could alone, but without replacing their own effort. Scaffolding is gradually removed — as I understood more Rust, I needed fewer explanations.
Architecture: Elm in Rust
The RSVP reader is built on iced 0.14.0, a GUI framework for Rust that follows the Elm Architecture — the same pattern that inspired Redux in JavaScript and SwiftUI in iOS.
The Pattern
State → View → [Interaction] → Message → Update → State → ...
The flow is unidirectional and predictable:
- State contains all application state in a single struct
- View renders the interface based on the current state
- The user interacts (presses a key, opens a file)
- Message represents that event as a concrete enum value
- Update processes the message and produces a new state
- The cycle repeats
Project Structure
src/
├── main.rs # Entry point, iced builder with theme and subscriptions
├── app.rs # State struct, new(), update(), subscription()
├── message.rs # Message enum — all app events
├── model/mod.rs # Data model for reading state
├── view/
│ ├── mod.rs
│ └── views.rs # RSVP display with rich_text, controls, spinner
├── infrastructure/
│ ├── mod.rs
│ ├── config.rs # Persistence, file dialogs, file processing
│ └── paths.rs # Path configuration
└── style/
├── mod.rs
└── theme.rs # Color theming and theme management
Each module has a single responsibility. The message enum centralizes all possible application events. The state struct contains all state. There's no distributed state or unexpected side effects.
Why This Pattern in Rust?
Because Rust and Elm Architecture share a philosophy: making invalid states unrepresentable.
In Rust, the type system guarantees at compile time that you can't access invalid memory. In Elm Architecture, the unidirectional flow guarantees that you can't have an inconsistent state. It's the same idea expressed at two different levels — one at the memory level, the other at the application logic level.
The Challenge: Special Characters in PDF
The most formative moment of the project was a bug that seemed simple: words like "retransmisión" appeared as character garbage when extracting text from certain PDFs.
The Real Problem
PDFs don't store text as UTF-8. They use a layered encoding system that most developers never see:
| Layer | Function | Problem If It Fails |
|---|---|---|
| CMap (Character Map) | Maps character codes to glyph IDs | Characters substituted by others |
| ToUnicode | Maps glyph IDs to Unicode code points | Accented and special characters corrupted |
| Identity-H | Encoding that uses glyph IDs as character codes | Completely illegible text |
The Migration
The first implementation used lopdf — a crate that works for simple documents, but with an incomplete CMap parser. It didn't handle Identity-H or complex ToUnicode mappings.
After evaluating alternatives — including C wrappers like pdftotext (Poppler), Java solutions like extractous (Apache Tika), and other Rust crates — we migrated to oxidize-pdf. Pure Rust, with explicit CMap and ToUnicode support, tested against 759 PDFs with special characters.
The result: improved extraction speed, more robust error handling, and special characters extracted correctly.
The Deeper Lesson
This bug taught me more about Rust than any chapter of the Rust Book:
- Pattern matching with
ResultandOption— handling pages that fail without crashing the entire application - The
.ok()?chain — convertingResulttoOptionto propagate errors in functions that returnOption. An idiomatic pattern that only makes sense when you understand both types into_iter()vsiter()— the difference between transferring ownership and borrowing. An error the compiler explains precisely, but that you only internalize when it burns youstd::io::Cursor— wrapping an in-memory byte buffer to implement theRead + Seektraits that the parser needs
Features
What started as a learning exercise ended up becoming a complete application:
| Feature | Detail |
|---|---|
| Multi-format | Loads TXT, CSV, MD, HTML, and PDF via native file dialogs |
| PDF extraction | Full CMap/ToUnicode for special characters |
| Custom fonts | TTF, OTF, WOFF, WOFF2 — any font from your system |
| Color theming | Background, text, and primary color with color picker |
| Adjustable speed | Arrow keys control WPM in real time |
| Playback controls | Space to pause/resume, automatic progression |
| Text cache | Processed files are cached to avoid re-parsing |
| Fullscreen | F11 to toggle, Escape to exit |
| Loading indicator | Spinner during file processing |
| Persistence | Settings saved in TOML, restored on startup |
The Stack
| Crate | Purpose |
|---|---|
| iced 0.14.0 | GUI framework with async tokio runtime |
| iced_aw 0.13.0 | Additional widgets (loading spinner) |
| oxidize-pdf 1.7.0 | PDF text extraction with full encoding support |
| html2text 0.16.7 | HTML to plain text conversion |
| rfd 0.17.2 | Native OS file dialogs |
| ttf-parser 0.25.1 | Font metadata extraction |
| serde / toml | Configuration serialization and persistence |
| derive_more 2.1.1 | Ergonomic derive for error types |
What I Learned
About Rust
- Ownership is not a restriction — it's a contract that guarantees your program is correct. Once you internalize it, you start seeing potential bugs in other languages that you used to ignore
- The compiler is the best teacher — its error messages are real-time documentation. E0507 isn't an obstacle, it's a lesson about ownership and borrowing
- Lifetimes exist for a reason — every
'ais a formal proof that your reference is valid for the duration you use it - The crate ecosystem matters — choosing the right dependency can save weeks of debugging. The difference between
lopdfandoxidize-pdfwas the difference between broken characters and perfect text
About Learning
- Discomfort is a sign of progress — RSVP and Rust share this fundamental truth
- No shortcuts, no cognitive debt — the code you fought to write is code you truly understand
- AI as a mentor works — when the boundary is clear and non-negotiable, AI amplifies learning instead of replacing it
- The editor matters less than you think — and at the same time, less help = more independent thinking
About AI and Development
- There's a space between "write everything for me" and "do it alone" — that middle ground is where AI truly adds value as a learning tool
- Documentation is a legitimate use case for AI — commits, README, technical evaluations, and yes, this blog post
- Rules matter — without an explicit
CLAUDE.md, the temptation to ask for "just one line" would have won eventually. The boundary has to be structural, not voluntary
Conclusions
Building an RSVP reader in Rust wasn't the most efficient path to having a working app. But efficiency wasn't the goal.
The goal was to learn a language that makes you a better programmer — and the only way to achieve that is by writing every line yourself.
RSVP teaches you to read by eliminating the inner narrator. Rust teaches you to program by eliminating mental shortcuts. Both are uncomfortable at first. Both transform you if you give them the chance.
They say Rust makes you a better programmer. I just wanted to survive the compiler — and ended up understanding why every restriction exists.
The complete code is on GitHub. Every line was written by hand. The AI was a mentor, not an author.
Need a team that combines deep technical expertise with real strategy? Let's talk.

