Project 10: Textual File Explorer
A multi-pane file explorer with directory tree, file list, and preview.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced (REFERENCE.md) |
| Time Estimate | 2-3 weeks |
| Main Programming Language | Python |
| Alternative Programming Languages | Go (Bubble Tea), Rust (Ratatui) |
| Coolness Level | Level 4: Hardcore Tech Flex (REFERENCE.md) |
| Business Potential | Level 2: Micro-SaaS (REFERENCE.md) |
| Prerequisites | Modern TUI Architectures, Concurrency and External Integration |
| Key Topics | Widget-based UI |
1. Learning Objectives
By completing this project, you will:
- Build and validate the core behavior described in the real-world outcome.
- Apply Modern TUI Architectures, Concurrency and External Integration to a working TUI.
- Design a predictable input-to-rendering pipeline with explicit state changes.
- Produce a tool that behaves consistently across terminals and restores state on exit.
2. All Theory Needed (Per-Concept Breakdown)
Modern TUI Architectures (MVU, Immediate Mode, CSS Styling)
Fundamentals Modern TUI frameworks adopt patterns borrowed from web development: declarative rendering, component composition, and unidirectional data flow. Bubble Tea uses the Elm Architecture (Model-View-Update) where state, update logic, and view rendering are explicit and pure. citeturn5search3 Ratatui uses immediate-mode rendering: you redraw the UI each frame from current state, rather than keeping persistent widget objects. citeturn4search4 Textual brings CSS-like styling and a reactive model: widgets are styled via CSS and reactive attributes trigger automatic refresh. citeturn4search0turn2search4 These frameworks shift the developer’s focus from imperative cursor control to higher-level state modeling, which is essential as UIs grow in complexity.
Deep Dive into the concept The MVU pattern decomposes your app into three parts: a Model (state), an Update function (handles events and returns a new model), and a View function (renders a representation from the model). Bubble Tea follows this pattern closely; the view returns a string representing the UI, and update returns a new model plus optional commands to perform side effects. citeturn5search3 This unidirectional flow reduces hidden state and makes behavior easier to reason about, especially under asynchronous input.
Immediate-mode rendering, as described by Ratatui, means the UI is reconstructed every frame based on state. citeturn4search4 There is no persistent widget tree; instead, you issue draw commands in a deterministic order. The advantage is simplicity and explicitness: your UI is always exactly what your state says it is. The tradeoff is that you must be careful about performance and avoid heavy computations in each frame.
Textual introduces a retained-widget model with CSS styling. Widgets are objects organized in a DOM-like tree, and CSS rules apply to these widgets just as they do in the web. citeturn4search0 Reactive attributes allow you to declare that changes to certain values should trigger a refresh, which reduces manual rendering code and encourages a more data-driven design. citeturn2search4 This is a different mental model from curses or immediate-mode libraries, but it provides powerful styling and composition.
When choosing between these architectures, consider your project’s complexity and constraints. Curses is lowest-level and most portable but imperative and manual. MVU frameworks make state transitions explicit and testable but require modeling state carefully. Immediate-mode frameworks are simple and fast but require efficient diffing or backend support. CSS-based frameworks allow rich styling but impose their own lifecycle and event models.
In the projects, you will implement MVU with Bubble Tea, immediate-mode with Ratatui, and CSS/reactive widgets with Textual. You will see the tradeoffs directly: how view functions stay pure, how state is centralized, and how rendering pipelines differ.
There are also differences in how these frameworks handle side effects. In MVU, side effects are typically represented as commands that return messages later; this keeps the update function pure and makes behavior easier to test. In immediate-mode frameworks, you often separate data collection from rendering by sampling metrics in a background task, then rendering the latest snapshot each frame. In a retained-widget framework like Textual, side effects can be attached to widget lifecycle events, which is powerful but can make data flow harder to trace if you do not discipline it.
Layout is another area of divergence. MVU and immediate-mode frameworks often use explicit layout primitives (rows, columns, flex-like constraints) that you construct in code. Textual uses CSS rules that are applied after the widget tree is built, which means layout can be modified independently of logic. This makes experimentation easier, but it also means you need a solid mental model of CSS-like specificity and cascading rules to predict the final layout.
Finally, testing differs across architectures. MVU lends itself to pure-function tests: given a model and a message, verify the new model. Immediate-mode rendering is harder to unit test at the UI layer, but you can test layout calculations and data preparation separately. For Textual, you often test widget behavior and state transitions rather than raw rendering output. Understanding these differences will help you select the right architecture for the right kind of tool.
How this fits on projects
- Project 8 (Bubble Tea Git Client), Project 9 (Ratatui Dashboard), Project 10 (Textual File Explorer)
Definitions & key terms
- Model-View-Update (MVU): Unidirectional architecture separating state, update logic, and view rendering. citeturn5search3
- Immediate mode: UI is redrawn from scratch every frame based on current state. citeturn4search4
- Reactive attributes: Values that automatically trigger refresh when they change. citeturn2search4
- Textual CSS: CSS-based styling for terminal widgets. citeturn4search0
Mental model diagram
MVU Flow:
Event -> Update -> Model -> View -> Render
Immediate Mode:
State -> draw(frame) -> terminal
CSS/Reactive:
State -> widget tree -> CSS rules -> renderer
How it works
- Input events are normalized into messages.
- Update logic transforms state deterministically.
- View logic renders UI from state.
- Renderer diffs and outputs to terminal.
Minimal concrete example
PSEUDOCODE:
on_event(msg):
model = update(model, msg)
ui = view(model)
render(ui)
Common misconceptions
- “MVU hides state” -> It makes state explicit and centralized.
- “Immediate mode is inefficient” -> It can be efficient with diffing and batching.
- “CSS is only for web” -> Textual uses CSS for terminal widgets. citeturn4search0
Check-your-understanding questions
- How does MVU reduce complexity in state handling?
- What is the key difference between immediate and retained rendering?
- Why do reactive attributes reduce manual refresh code?
Check-your-understanding answers
- All state transitions pass through a single update function.
- Immediate mode redraws each frame; retained mode keeps widgets alive.
- They trigger refresh automatically when values change.
Real-world applications
lazygit-style clients, terminal dashboards, rich interactive apps
Where you’ll apply it
- Project 8, Project 9, Project 10, Project 12
References
- Bubble Tea README (MVU / Elm Architecture) citeturn5search3
- Ratatui rendering docs (immediate mode) citeturn4search4
- Textual CSS guide and reactivity guide citeturn4search0turn2search4
- “Clean Architecture” by Robert C. Martin - Ch. 4-5 (separation of concerns)
Key insights Modern TUIs are about deterministic state -> view pipelines, not imperative drawing.
Summary MVU and immediate-mode frameworks make state explicit and rendering predictable, while CSS-based frameworks improve styling and composition.
Homework/Exercises to practice the concept
- Sketch a state model for a two-pane file explorer and list its update messages.
- Explain how you would test an MVU update function without a terminal.
Solutions to the homework/exercises
- State includes selected path, list cursor, preview; messages include Up, Down, Enter, Refresh.
- Feed messages into update and assert the resulting model state.
Concurrency and External Integration (Async I/O and LSP)
Fundamentals Real TUI tools rarely operate in isolation: they run subprocesses, stream logs, and integrate with external protocols. You need asynchronous I/O so your UI stays responsive while waiting on disk or network. The Language Server Protocol (LSP) is a key example of external integration: it defines a JSON-RPC based protocol between an editor and a language server, with messages framed by Content-Length headers. citeturn8view0turn6search7 Understanding this framing and the request/response/notification model is critical for building a terminal IDE. Concurrency is not just a performance feature here; it is what keeps input and rendering from freezing while external work is happening.
Deep Dive into the concept A responsive TUI is an event-driven system: it must handle user input, background tasks, and rendering without blocking. This often requires non-blocking I/O or separate worker threads that communicate with the main loop via message queues. The architecture typically resembles a reactor: events enter a queue, state updates occur, and rendering happens on a fixed cadence or after each update. The challenge is coordinating asynchronous tasks without race conditions.
External integration often uses structured protocols. LSP is a canonical example for TUI IDEs. LSP defines a JSON-RPC based message format, and frames messages with a header that includes Content-Length (required) and Content-Type (optional). The header is separated from the JSON content by a blank line (double CRLF). citeturn8view0 The official LSP overview explains that the protocol standardizes communication between development tools and language servers. citeturn6search7 This means your TUI must implement a message framing layer: read bytes from the server, parse headers, then parse JSON payloads, then dispatch responses.
Another common integration pattern is log streaming. For example, a TUI log viewer needs to tail a file or subscribe to a stream without blocking input handling. This suggests a concurrency model where background workers push events (new lines) into the UI thread. You must also handle backpressure: if logs arrive faster than you can render, you need buffering and drop strategies.
Finally, clean shutdown matters. When you spawn subprocesses (Git, LSP servers), you must terminate them on exit and restore terminal state. A robust TUI uses a dedicated lifecycle: start external processes, manage their I/O, and ensure cleanup paths on error or signals.
Protocol integration also introduces ordering and correlation problems. JSON-RPC supports requests, responses, and notifications; requests carry an ID so the client can match responses. Your TUI must maintain a table of in-flight requests and timeouts so it can recover if a server is slow or unresponsive. This is not just for correctness; it also affects UI behavior. If you wait indefinitely for a response, you can block features like diagnostics or go-to-definition.
Cancellation is another practical concern. If the user types rapidly or navigates to a different file, outstanding requests may become irrelevant. A well-designed system can cancel or ignore outdated responses to avoid flicker or confusing UI updates. This requires you to tag requests with the state they were generated from and discard responses that no longer apply.
Error handling should be explicit. External processes can crash, JSON can be malformed, and connections can drop. Your event loop must treat these as normal events and surface them to the user in a non-blocking way. For example, a TUI IDE might show a diagnostics banner when the LSP server disconnects, while continuing to allow local editing.
You should also design for flow control. If your UI can only render 30 frames per second but your input stream can produce 1,000 messages per second, you need a strategy to coalesce or drop events. This is especially important for log viewers and diagnostics streams. A small, well-defined queue with clear drop rules keeps the UI stable under load.
How this fits on projects
- Project 11 (Async Log Monitor), Project 12 (TUI IDE with LSP)
Definitions & key terms
- JSON-RPC: Remote procedure call protocol encoded in JSON. citeturn8view0
- LSP: Protocol between editors and language servers using JSON-RPC. citeturn6search7
- Content-Length framing: LSP messages are preceded by a header specifying byte length. citeturn8view0
Mental model diagram
User Events -> UI Loop -> State -> Render
^
|
Background Tasks (I/O, subprocess, LSP)
|
Messages
How it works
- Start background workers or subprocesses.
- Read and parse their output into structured events.
- Send events into the main loop.
- Update state and render without blocking input.
Minimal concrete example
PROTOCOL TRANSCRIPT (LSP framing):
Content-Length: 85\r\n
\r\n
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { ... } }
Common misconceptions
- “Async means multi-threaded” -> It can be single-threaded with non-blocking I/O.
- “Parsing JSON-RPC is trivial” -> You must handle framing and partial reads.
Check-your-understanding questions
- Why is Content-Length framing required in LSP?
- What happens if a background task blocks the main loop?
- How would you handle a burst of log lines?
Check-your-understanding answers
- To delimit messages in a continuous byte stream.
- The UI freezes and input becomes unresponsive.
- Buffer lines and apply backpressure or drop strategy.
Real-world applications
- Terminal IDEs, log monitors, deployment dashboards
Where you’ll apply it
- Project 11, Project 12
References
- LSP specification base protocol (header + JSON-RPC content) citeturn8view0
- LSP official overview (protocol purpose) citeturn6search7
- “Operating Systems: Three Easy Pieces” - Ch. on concurrency
Key insights Responsive TUIs are event-driven systems that must parse and integrate external streams safely.
Summary Concurrency and protocol integration are essential for real tools; they require framing, parsing, and careful event-loop design.
Homework/Exercises to practice the concept
- Design an event queue schema that can carry input, timer ticks, and LSP messages.
- Describe a graceful shutdown sequence for a TUI that spawns subprocesses.
Solutions to the homework/exercises
- Use a tagged union: InputEvent, TimerEvent, LspEvent, LogEvent.
- Stop input loop -> terminate subprocess -> drain output -> restore terminal state.
3. Project Specification
3.1 What You Will Build
A multi-pane file explorer with directory tree, file list, and preview.
Included:
- The core UI flow described in the Real World Outcome
- Deterministic input handling and rendering
- Clean exit and terminal state restoration
Excluded:
- GUI features, mouse-first workflows, or non-terminal frontends
- Networked collaboration or cloud sync
3.2 Functional Requirements
- Core Interaction: Implements the main interaction loop and updates the screen correctly.
- Input Handling: Handles required keys without blocking and supports quit/exit.
- Rendering: Updates only what changes to avoid flicker.
- Resize Handling: Adapts to terminal resize or shows a clear warning state.
- Errors: Handles invalid input or missing data gracefully.
3.3 Non-Functional Requirements
- Performance: Stable refresh without visible flicker under normal usage.
- Reliability: Terminal state is restored on exit or error.
- Usability: Keyboard-first navigation with clear status/help hints.
3.4 Example Usage / Output
$ ./textual-explorer
$ ./textual-explorer [Tree] [File List] [Preview] /home/user main.py def main(): projects README.md …
ASCII layout:
[Left: tree] [Center: list] [Right: preview]
3.5 Data Formats / Schemas / Protocols
- Screen Model: 2D grid of cells with glyph + style
- Input Events: Normalized key events (Up, Down, Enter, Esc, Ctrl)
- State Snapshot: Immutable model used for rendering each frame
3.6 Edge Cases
- Terminal resized to smaller than minimum layout
- Rapid key repeat and partial escape sequences
- Missing or invalid input file (if applicable)
- Unexpected termination (SIGINT)
3.7 Real World Outcome
3.7.1 How to Run (Copy/Paste)
$ ./textual-explorer
3.7.2 Golden Path Demo (Deterministic)
- Launch the tool
- Perform the primary action once
- Observe the expected screen update
3.7.3 If CLI: provide an exact terminal transcript
$ ./textual-explorer
$ ./textual-explorer [Tree] [File List] [Preview] /home/user main.py def main(): projects README.md …
ASCII layout:
[Left: tree] [Center: list] [Right: preview]
3.7.4 Failure Demo (Deterministic)
$ ./textual-explorer --bad-flag
ERROR: unknown option: --bad-flag
exit code: 2
4. Solution Architecture
4.1 High-Level Design
Input -> Event Queue -> State Update -> Render -> Terminal
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Input Decoder | Normalize raw input into events | Handle partial sequences safely |
| State Model | Hold UI state and selections | Keep state immutable per frame |
| Renderer | Draw from state to terminal | Diff-based updates |
| Controller | Orchestrate loop and timers | Non-blocking IO |
4.3 Data Structures (No Full Code)
DATA STRUCTURE: Cell
- glyph
- fg_color
- bg_color
- attrs
DATA STRUCTURE: Frame
- width
- height
- cells[width][height]
4.4 Algorithm Overview
Key Algorithm: Render Diff
- Build new frame from current state
- Compare with old frame
- Emit minimal updates for changed cells
Complexity Analysis:
- Time: O(width * height)
- Space: O(width * height)
5. Implementation Guide
5.1 Development Environment Setup
# Build and run with your toolchain
5.2 Project Structure
project-root/
|-- src/
|-- tests/
|-- assets/
`-- README.md
5.3 The Core Question You’re Answering
“How do widget trees and reactive state simplify TUI development?”
5.4 Concepts You Must Understand First
- Textual CSS
- How do styles apply to widgets?
- Book Reference: “Fluent Python” - Ch. on descriptors
- Reactive attributes
- How do updates propagate automatically?
- Book Reference: “Fluent Python” - Ch. on descriptors
5.5 Questions to Guide Your Design
- UI structure
- Which widgets represent tree, list, preview?
- How do you layout them in CSS?
- Data flow
- How do you load file contents without blocking?
- How do you debounce rapid navigation?
5.6 Thinking Exercise
Design the Widget Tree
Sketch a DOM tree of widgets.
Questions to answer:
- Where do you mount new widgets?
- Which widget owns focus?
5.7 The Interview Questions They’ll Ask
- “What is a reactive attribute?”
- “How does CSS styling work in Textual?”
- “How do you handle async file reads?”
- “How do you manage focus between widgets?”
- “How does Textual differ from curses?”
5.8 Hints in Layers
Hint 1: Start with a static layout Build three panes with placeholder text.
Hint 2: Add reactive state Update preview when selection changes.
Hint 3: Pseudocode
on_select(path):
preview = read_file_async(path)
Hint 4: Debugging Use Textual dev console to inspect widget tree.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Python descriptors | “Fluent Python” | Ch. 23 |
| UI composition | “Clean Architecture” | Ch. 4 |
5.10 Implementation Phases
Phase 1: Foundation
Goals:
- Initialize the terminal and input handling
- Render the first static screen
Tasks:
- Implement setup and teardown
- Draw a static layout that matches the Real World Outcome
Checkpoint: The UI renders and exits cleanly.
Phase 2: Core Functionality
Goals:
- Implement the main interaction loop
- Update state based on input
Tasks:
- Add event processing
- Implement the main feature (draw, navigate, filter)
Checkpoint: The primary interaction works end-to-end.
Phase 3: Polish & Edge Cases
Goals:
- Handle resizing and invalid input
- Improve performance and usability
Tasks:
- Add resize handling
- Add error states and help hints
Checkpoint: No flicker and clean recovery from edge cases.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Input model | raw vs canonical | raw | Required for key-level input |
| Render strategy | full redraw vs diff | diff | Avoid flicker and reduce output |
| State model | mutable vs immutable | immutable | Predictable updates and testing |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Validate parsing and state transitions | Key decoder tests |
| Integration Tests | Verify rendering pipeline | Frame diff vs expected |
| Edge Case Tests | Terminal resize and invalid input | Small terminal size |
6.2 Critical Test Cases
- Resize: Shrink terminal below minimum and verify warning.
- Rapid Input: Hold down keys and ensure no crash.
- Exit: Force quit and verify terminal restoration.
6.3 Test Data
Input sequence: Up, Up, Down, Enter
Expected: selection moves and activates without crash
7. Common Pitfalls & Debugging
Problem 1: “UI freezes on large files”
- Why: File reads are synchronous.
- Fix: Use background workers for preview loading.
- Quick test: Open a large log file and verify responsiveness.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a help overlay with keybindings
- Add a status bar with timestamps
8.2 Intermediate Extensions
- Add configurable themes
- Add persistent settings file
8.3 Advanced Extensions
- Add plugin hooks for new views
- Add performance tracing for render time
9. Real-World Connections
9.1 Industry Applications
- Terminal dashboards for infrastructure monitoring
- Developer tools used over SSH and in containers
9.2 Related Open Source Projects
- htop, ranger, lazygit, nmtui (for UI design reference)
9.3 Interview Relevance
- Input handling, event loops, and state modeling questions
10. Resources
10.1 Essential Reading
- “Fluent Python”
10.2 Video Resources
- Conference talks on terminal UI architecture (choose one and take notes)
10.3 Tools & Documentation
- terminfo, curses, or framework docs used in this project
10.4 Related Projects in This Series
- See other projects in this folder for follow-on ideas
11. Self-Assessment Checklist
11.1 Understanding
- I can explain the rendering pipeline for this project
- I can explain how input is decoded and normalized
- I can explain how my UI state updates per event
11.2 Implementation
- All functional requirements are met
- All critical test cases pass
- Edge cases are handled and documented
11.3 Growth
- I documented lessons learned
- I can explain this project in a job interview
12. Submission / Completion Criteria
Minimum Viable Completion:
- Program runs and matches Real World Outcome
- Terminal state restored on exit
- Main interaction works
Full Completion:
- All minimum criteria plus:
- Resize handling and error states
- Tests for core parsing and rendering
Excellence (Going Above & Beyond):
- Performance profiling results included
- Additional features from Extensions completed