Article

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:

ToolRoleWhat It DOESN'T Have
lite-xlText editorNo LSP, no autocompletion, no intellisense
baconBackground code checkerRuns clippy continuously — your only feedback loop
TerminalCompile and runNo IDE, no "play" button
Why? Because every shortcut that saves you from the error also saves you from the lesson.

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"
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?"
Not a single line of code. Just the right mental model for me to find the solution.

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, and oxidize-pdf explaining encoding trade-offs and maintenance
  • Conceptual guidance — ownership, borrowing, lifetimes, the Elm Architecture pattern, and Rust's philosophy
Documenting isn't the same as implementing. The AI added real value without replacing the learning process.

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:

  1. State contains all application state in a single struct
  2. View renders the interface based on the current state
  3. The user interacts (presses a key, opens a file)
  4. Message represents that event as a concrete enum value
  5. Update processes the message and produces a new state
  6. 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:

LayerFunctionProblem If It Fails
CMap (Character Map)Maps character codes to glyph IDsCharacters substituted by others
ToUnicodeMaps glyph IDs to Unicode code pointsAccented and special characters corrupted
Identity-HEncoding that uses glyph IDs as character codesCompletely illegible text
If the PDF parser doesn't correctly implement the CMap → ToUnicode → Unicode chain, every character outside basic ASCII gets corrupted.

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 Result and Option — handling pages that fail without crashing the entire application
  • The .ok()? chain — converting Result to Option to propagate errors in functions that return Option. An idiomatic pattern that only makes sense when you understand both types
  • into_iter() vs iter() — the difference between transferring ownership and borrowing. An error the compiler explains precisely, but that you only internalize when it burns you
  • std::io::Cursor — wrapping an in-memory byte buffer to implement the Read + Seek traits that the parser needs
Each of these concepts was a fight with the compiler. And every fight resulted in deep understanding — not superficial memorization.


Features

What started as a learning exercise ended up becoming a complete application:

FeatureDetail
Multi-formatLoads TXT, CSV, MD, HTML, and PDF via native file dialogs
PDF extractionFull CMap/ToUnicode for special characters
Custom fontsTTF, OTF, WOFF, WOFF2 — any font from your system
Color themingBackground, text, and primary color with color picker
Adjustable speedArrow keys control WPM in real time
Playback controlsSpace to pause/resume, automatic progression
Text cacheProcessed files are cached to avoid re-parsing
FullscreenF11 to toggle, Escape to exit
Loading indicatorSpinner during file processing
PersistenceSettings saved in TOML, restored on startup

The Stack

CratePurpose
iced 0.14.0GUI framework with async tokio runtime
iced_aw 0.13.0Additional widgets (loading spinner)
oxidize-pdf 1.7.0PDF text extraction with full encoding support
html2text 0.16.7HTML to plain text conversion
rfd 0.17.2Native OS file dialogs
ttf-parser 0.25.1Font metadata extraction
serde / tomlConfiguration serialization and persistence
derive_more 2.1.1Ergonomic 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 'a is 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 lopdf and oxidize-pdf was 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.