Project 1: ANSI Paint and Cursor Lab
A tiny paint program that moves a cursor and draws colored blocks using raw control sequences.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 1: Beginner (REFERENCE.md) |
| Time Estimate | Weekend |
| Main Programming Language | Python |
| Alternative Programming Languages | C, Go, Rust |
| Coolness Level | Level 2: Practical but Forgettable (REFERENCE.md) |
| Business Potential | Level 1: Resume Gold (REFERENCE.md) |
| Prerequisites | Terminal Control Model, Input Modes and Key Decoding, Screen Rendering, Buffering, and Diffing |
| Key Topics | Terminal Control, Rendering |
1. Learning Objectives
By completing this project, you will:
- Build and validate the core behavior described in the real-world outcome.
- Apply Terminal Control Model, Input Modes and Key Decoding, Screen Rendering, Buffering, and Diffing 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)
Terminal Control Model (ECMA-48 / ANSI Sequences)
Fundamentals Terminal control is the art of sending special byte sequences that change the terminal’s state: move the cursor, clear regions, and apply styles. These sequences are standardized by ECMA-48 (also ISO/IEC 6429), which defines control functions and their coded representations for character-imaging devices. Most terminal emulators implement a subset of these functions, especially the Control Sequence Introducer (CSI) family used for cursor movement and styling. The key mental model is that your program is not painting pixels; it is describing a sequence of state transitions for a text grid. You output bytes, the terminal interprets them, and the screen updates accordingly. You must assume partial support and variability across emulators, which is why portable TUIs rely on capability lookup rather than hardcoded sequences. citeturn1search0
Deep Dive into the concept ECMA-48 defines control functions as embedded control codes inside a character stream, and most of the time you interact with them through escape sequences that begin with ESC (0x1B). The CSI sequences are the most common: they use ESC + ‘[’ and a set of parameters to modify cursor position, clear lines, set attributes (SGR), or query terminal state. A typical rendering cycle is a stream of printable characters interleaved with CSI sequences. For example, you might clear the screen, move the cursor to row 1 column 1, draw a header, then move to a different region and draw a table.
However, the standard is intentionally broad: it covers 7-bit and 8-bit forms, allows for extensions, and explicitly acknowledges that devices will implement only subsets appropriate to their role. This is why terminal-specific features exist (like xterm private modes) and why portability is hard if you hardcode sequences. The terminal emulator is a state machine: it maintains cursor position, current attributes, scrolling regions, tab stops, and more. When you send sequences, you modify that state. If you do not reset it properly (e.g., leaving bold mode on), you can affect everything after your program exits.
From a system-design perspective, this means you need a strategy for deterministic state transitions: always enter a known state on startup (clear screen, reset attributes, set cursor visibility), and always restore on exit. It also means you need to avoid assuming a fixed geometry: the terminal can be resized at any time, and the standard defines ways to query or react to size changes.
The rendering problem becomes “how do I minimize the number of control functions I emit while still matching my intended screen?” Naively re-rendering the full screen with full clears causes flicker and wastes bandwidth. Instead, you compute diffs between the old and new screen states and only emit the sequences necessary to update the changed cells. This is the basis of higher-level libraries, but understanding it at the sequence level is crucial when you debug glitches, ghosting, or cursor drift.
Finally, the control layer is not only about rendering: it also includes modes like alternate screen buffers and bracketed paste, which dramatically impact user experience. These are often private modes not guaranteed by ECMA-48, so you must treat them as optional and negotiate via terminfo or feature detection.
Control sequences are also a tiny, compact language. A CSI sequence has parameters, intermediate bytes, and a final byte; if you treat this as a grammar rather than ad hoc string concatenation, you will avoid malformed sequences and undefined behavior. Some sequences are absolute (move cursor to row/col), while others are relative (cursor up/down). Absolute positioning is usually more robust in diff renderers because it does not depend on the current cursor state. Relative moves can be cheaper in bytes but are more error-prone if your state tracking is incorrect.
Another critical practical detail is line wrapping. Many terminals auto-wrap when you print beyond the last column, which can accidentally scroll the screen and break your layout. A robust renderer ensures that it does not write past the last column, or it explicitly disables auto-wrap if the terminal supports it. This is one reason why TUIs often reserve the final column in a row and why they normalize strings to the available width before writing.
At scale, the control layer becomes a bandwidth problem. Over slow connections (SSH, serial), the number of bytes you emit directly impacts latency. Good TUIs reduce redundant attribute changes, minimize cursor moves, and batch output, which is why diffing and run-length optimizations matter. When you understand the control language, you can reason about the byte cost of your rendering decisions.
How this fits on projects
- Project 1 (ANSI Paint), Project 4 (Screen Diff Renderer), Project 5 (ncurses Dashboard)
Definitions & key terms
- ECMA-48 / ISO 6429: Standard that defines control functions and their coded representations for character-imaging devices. citeturn1search0
- ESC (0x1B): The escape byte that introduces a control sequence.
- CSI: Control Sequence Introducer, a common ESC-prefixed control family.
- SGR: Select Graphic Rendition; controls color and attributes.
Mental model diagram
[App Output Stream]
text + ESC [ params cmd
| |
v v
Terminal Parser -> State Machine -> Screen Buffer -> Display
How it works
- Your program writes bytes to stdout.
- The terminal emulator parses bytes into either printable glyphs or control sequences.
- Control sequences mutate terminal state (cursor, attributes, scroll region).
- Printable characters render into the current cursor location.
- The screen buffer is updated; the display refreshes.
Minimal concrete example
PSEUDOCODE:
WRITE "ESC[2J" # clear screen
WRITE "ESC[H" # move cursor to home
WRITE "Title" # draw text at top-left
WRITE "ESC[5;10H" # move cursor to row 5 col 10
WRITE "*" # draw a marker
Common misconceptions
- “ANSI codes are universal” -> They are only partially implemented; use terminfo for portability.
- “Clearing the screen is harmless” -> It can cause flicker and reset scrollback unexpectedly.
- “The terminal is stateless” -> It is stateful and preserves modes until you reset them.
Check-your-understanding questions
- Why can two terminal emulators display the same escape sequence differently?
- What is the difference between printable characters and control functions in ECMA-48?
- Why is diff-based rendering more efficient than full-screen redraws?
Check-your-understanding answers
- Terminals implement different subsets and extensions of the standard.
- Printable characters render glyphs; control functions mutate terminal state.
- It reduces bandwidth and avoids flicker by only updating changed cells.
Real-world applications
top,htop,less,vim, terminal dashboards
Where you’ll apply it
- Project 1, Project 4, Project 5, Project 8
References
- ECMA-48 standard (control functions and coded representations) citeturn1search0
- “The Linux Programming Interface” by Michael Kerrisk - Ch. 62
Key insights A terminal UI is a state machine driven by a byte stream, not a pixel canvas.
Summary You control terminals by emitting control sequences that mutate display state. Understanding this layer lets you debug and optimize any higher-level library.
Homework/Exercises to practice the concept
- Write a text plan for clearing, drawing a header, and drawing a status bar without flicker.
- List the terminal states you must restore on exit.
Solutions to the homework/exercises
- Use a fixed update order: hide cursor -> clear or diff -> draw header -> draw body -> draw status -> show cursor.
- Restore cursor visibility, attributes (color/bold), alternate screen, and input mode.
Input Modes and Key Decoding (termios Line Discipline)
Fundamentals
Terminal input is not delivered to your program exactly as the user types it. The terminal driver applies a line discipline that can buffer input, handle editing keys, and interpret control characters. POSIX termios defines canonical (line-based) and non-canonical (raw-ish) input processing. In canonical mode, input is delivered line-by-line and read() waits until newline or EOF; editing keys like erase and kill are handled by the driver. In non-canonical mode, input is delivered immediately and the driver does not perform line editing. This distinction is essential for interactive TUIs because you need keypress-level input, not line-buffered input. citeturn1search3
Deep Dive into the concept
The termios interface defines how a terminal device processes input and output. Canonical mode (often called “cooked”) is designed for typical shell usage: the driver buffers input until it sees a line delimiter, then passes it to the program. This means your program cannot react to individual keypresses in real time, and special characters like Backspace are processed before the program ever sees them. In contrast, non-canonical mode disables line buffering and line editing, and uses the MIN and TIME settings to control when read() returns. This lets a TUI receive bytes immediately and implement its own key handling.
Raw mode in modern libraries is usually a bundle of termios changes: disable canonical input, disable echo, and disable signal generation for control characters. This is crucial for handling keys like Ctrl+C yourself. Libraries like crossterm summarize the effects: input is no longer line buffered, special keys are not processed by the terminal driver, and input is delivered byte-by-byte. citeturn3search2
Once in non-canonical/raw mode, you must interpret input bytes. Many keys are sent as multi-byte escape sequences (for example, arrow keys often begin with ESC). These sequences are not standardized across all terminals, which is why terminfo also contains key capabilities. Your input decoder must handle partial sequences, timeouts, and ambiguous prefixes (ESC alone vs ESC as the start of a sequence). A robust approach is to implement a small state machine: when you see ESC, start collecting bytes; if a complete known sequence is matched, emit a high-level key event; if timeout occurs, treat ESC as a standalone key.
Another key aspect is signals and terminal restoration. If your program is interrupted (SIGINT, SIGTERM) while in raw mode, the terminal may remain in a broken state. A reliable TUI sets up cleanup handlers to restore termios state on exit, and uses an alternate screen buffer to keep the user’s shell clean.
Finally, input in TUIs is not just keys. Resize events are delivered via signals (like SIGWINCH) or via platform-specific APIs, and mouse input can be enabled in some terminals. In your architecture, all of these should be normalized into a single event stream to keep update logic deterministic.
Non-canonical mode uses two parameters, often called VMIN and VTIME, to control when reads return. This lets you trade off latency and CPU usage. A common approach is to request at least one byte and use a short timeout, which yields responsive input without a busy loop. For portability, you should treat these parameters as a contract: they define the boundary between polling and blocking behavior in your main loop.
The line discipline can also transform bytes (for example, carriage return to newline) and handle special control characters. When you disable canonical processing, these transformations often stop, which means you must decide how to handle them yourself. This is why a raw-mode TUI frequently treats Enter, Backspace, and Tab as high-level events that it interprets explicitly.
Key decoding is best done in layers: a byte reader, a sequence parser, and an event normalizer. The parser recognizes known escape sequences and converts them into symbolic events, while the normalizer maps them into consistent key names across platforms. This layered design lets you test the parser independently from the rest of the UI, and it makes it easier to support both simple keys (single byte) and complex keys (multi-byte sequences).
How this fits on projects
- Project 2 (Raw Key Decoder), Project 5 (ncurses Dashboard), Project 8 (Bubble Tea Git Client)
Definitions & key terms
- Canonical mode: Line-buffered input processing;
read()returns after newline/EOF. citeturn1search3 - Non-canonical mode: Byte-level input without line editing. citeturn1search3
- Raw mode: A common configuration that disables echo, line buffering, and special-key processing. citeturn3search2
- Escape sequence: Multi-byte sequences starting with ESC representing special keys.
Mental model diagram
Keyboard -> Driver Buffer -> termios line discipline -> read() -> Key Decoder -> Events
(canonical or raw) (state machine)
How it works
- Configure termios to canonical or non-canonical mode.
- Read bytes from stdin as they arrive.
- Feed bytes into a decoder that recognizes escape sequences.
- Emit high-level events (Up, Down, Ctrl+C, etc.).
- Feed events into your app’s update loop.
Minimal concrete example
PSEUDOCODE:
SET_INPUT_MODE(raw)
WHILE running:
bytes = READ_NONBLOCKING()
FOR b IN bytes:
decoder.feed(b)
IF decoder.has_event():
event = decoder.pop_event()
dispatch(event)
Common misconceptions
- “Raw mode means no rules” -> It still follows MIN/TIME semantics.
- “Arrow keys are single bytes” -> They are usually escape sequences.
- “SIGINT always means exit” -> You can capture Ctrl+C in raw mode.
Check-your-understanding questions
- Why does canonical mode prevent real-time key handling?
- What is the role of MIN and TIME in non-canonical mode?
- Why must you restore terminal state on exit?
Check-your-understanding answers
- Input is buffered until newline/EOF, so keypresses are not delivered immediately.
- They control when read() returns in byte-oriented input.
- Otherwise the user’s terminal can remain in raw/no-echo mode.
Real-world applications
- TUIs, terminal games, interactive debuggers, SSH-based tools
Where you’ll apply it
- Project 2, Project 5, Project 8, Project 12
References
- POSIX termios input processing (canonical vs non-canonical) citeturn1search3
- crossterm raw mode behavior (summary of effects) citeturn3search2
- “Advanced Programming in the UNIX Environment” - Ch. 18
Key insights Raw input is a controlled state machine, not a free-for-all stream.
Summary Input handling is a layered pipeline; understanding termios is required to build reliable keyboard-driven interfaces.
Homework/Exercises to practice the concept
- Draw a state machine for decoding ESC-based arrow keys.
- Describe how you would detect a resize event in your app.
Solutions to the homework/exercises
- Start in NORMAL; on ESC move to ESC_SEEN; accept ‘[’ then digits; map known final byte to key.
- Use a signal handler for window resize and push a Resize event into your loop.
Screen Rendering, Buffering, and Diffing
Fundamentals Rendering in TUIs is about mapping a logical state to a 2D grid of cells and emitting the minimal set of updates. Unlike GUIs, you do not have a compositor; you are the compositor. The screen is a matrix of characters with attributes (color, bold, underline). To avoid flicker and wasted output, you typically keep a back buffer (last frame) and compute a diff to the new frame. The diff tells you which cells changed and where to move the cursor. Many libraries implement this for you, but you need to understand it to debug performance or visual glitches. A frame is a full snapshot of the terminal grid, and your renderer is responsible for ensuring that each frame is consistent and complete.
Deep Dive into the concept A screen buffer is a 2D array of cells, where each cell has a glyph and style attributes. A frame is a complete snapshot of this buffer. When your app state changes, you generate a new frame. If you were to naively clear the screen and print the entire frame every time, performance would degrade on slow terminals and over SSH. This is why most TUI systems use double buffering and diffing.
Diffing can be done at multiple granularities. The simplest method is to compare cell-by-cell and emit updates for every change, moving the cursor as needed. A more efficient method groups contiguous runs of cells in the same row to reduce cursor movements. Another technique is damage tracking: instead of comparing full frames, you track which regions changed as a result of state updates. The tradeoff is complexity vs performance.
A robust renderer also handles terminal-specific features like alternate screen buffers. Alternate screen buffer usage prevents your UI from polluting the shell scrollback; when you exit, the original screen is restored. Libraries like crossterm expose entry and exit commands for alternate screens. citeturn2search2
The critical invariants are: (1) you must know where the cursor is, (2) you must know the current style attributes, and (3) you must restore these on exit. A diffing renderer is essentially a small compiler that transforms an intended frame into a minimal control-sequence program. This is the core of performance optimization in TUIs.
In projects, you will build a simplified diff engine: keep a 2D array of cells for the previous frame, compute a new frame each tick, and emit only the changed cells. You will learn to coalesce updates, manage cursor moves, and reduce redundant style changes.
There are also subtle correctness issues. For example, if you update a cell with a different style, you must ensure the style is set before printing the character, and you must avoid leaking that style into subsequent cells. This means your renderer needs a model of the current style state in the terminal and must emit style reset sequences at the right time. Similarly, when rendering wide characters or combining characters, you must consider how many columns a glyph occupies; otherwise your cursor calculations will drift. Even if you avoid complex Unicode in your own output, the terminal width can vary by locale, so your renderer should treat width as a property of a glyph rather than as a constant.
Resizing adds another layer. When the terminal size changes, your frame dimensions change, and a previously valid cursor position may be out of bounds. A robust renderer clamps or reflows content, re-creates buffers to the new size, and forces a full redraw to prevent artifacts from stale rows. For dashboards, you often choose a strategy: either truncate content to fit the new size or change layout to a stacked mode.
Finally, you must consider how frequently to render. Some TUIs render only on state changes, others render on a fixed tick. Rendering on every tick simplifies animations but can waste bandwidth. Rendering on state change reduces output but requires careful invalidation logic. The projects will give you a chance to compare both approaches and measure their impact.
How this fits on projects
- Project 4 (Screen Diff Renderer), Project 5 (ncurses Dashboard), Project 9 (Ratatui Dashboard)
Definitions & key terms
- Back buffer: The previous frame stored for diffing.
- Damage tracking: Tracking which regions changed to reduce diff work.
- Alternate screen: A separate buffer that restores the original screen on exit. citeturn2search2
Mental model diagram
State -> Frame A (buffer) -> diff(Frame A, Frame B) -> Emit sequences -> Screen
^ |
| v
Frame B <--------------------------- Next tick
How it works
- Generate a full frame from app state.
- Compare with the previous frame.
- For each changed region, emit cursor moves and text.
- Update the stored previous frame.
Minimal concrete example
PSEUDOCODE:
new_frame = render(state)
for each cell in grid:
if new_frame[cell] != old_frame[cell]:
move_cursor(cell.x, cell.y)
set_style(cell.style)
write(cell.glyph)
old_frame = new_frame
Common misconceptions
- “Diffing is only for speed” -> It also prevents flicker and keeps cursor stable.
- “You can ignore cursor position” -> Incorrect; cursor drift causes corrupted layouts.
Check-your-understanding questions
- Why is a diff-based renderer faster over SSH?
- What happens if you fail to restore the cursor or styles on exit?
- Why might you prefer damage tracking to full diffing?
Check-your-understanding answers
- It emits fewer bytes and avoids full-screen clears.
- The user’s terminal remains in a modified state.
- It reduces diff cost by focusing on known dirty regions.
Real-world applications
- Terminal dashboards, log viewers, system monitors
Where you’ll apply it
- Project 4, Project 5, Project 9, Project 11
References
- crossterm terminal features (alternate screen) citeturn2search2
- “The Pragmatic Programmer” - Ch. “Orthogonality” (designing clear rendering stages)
Key insights Rendering is a compiler: state is compiled into the smallest correct terminal program.
Summary Efficient TUIs rely on diff-based rendering and strict state management to avoid flicker and maintain control over the screen.
Homework/Exercises to practice the concept
- Design a diff strategy that minimizes cursor moves.
- Create a list of rendering invariants you will enforce in every frame.
Solutions to the homework/exercises
- Group contiguous changes by row and emit a single cursor move per group.
- Always reset styles, hide/show cursor explicitly, and restore terminal state on exit.
3. Project Specification
3.1 What You Will Build
A tiny paint program that moves a cursor and draws colored blocks using raw control sequences.
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
$ ./ansi-paint
You launch a program that displays a blank grid. Arrow keys move a visible cursor. Pressing Space toggles a cell to a colored block. The screen updates without scrolling or flicker.
$ ./ansi-paint [ANSI Paint] Use arrows to move, Space to draw, Q to quit
+——————————+ | . . . . . . . . . . . . . . | | . . . . . . . . . . . . . . | | . . . . . . . . . . . . . . | | . . . . . . . . . . . . . . | +——————————+
ASCII layout:
[Header] [Grid 20x40] [Footer: controls]
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)
$ ./ansi-paint
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
$ ./ansi-paint
You launch a program that displays a blank grid. Arrow keys move a visible cursor. Pressing Space toggles a cell to a colored block. The screen updates without scrolling or flicker.
$ ./ansi-paint [ANSI Paint] Use arrows to move, Space to draw, Q to quit
+——————————+ | . . . . . . . . . . . . . . | | . . . . . . . . . . . . . . | | . . . . . . . . . . . . . . | | . . . . . . . . . . . . . . | +——————————+
ASCII layout:
[Header] [Grid 20x40] [Footer: controls]
3.7.4 Failure Demo (Deterministic)
$ ./ansi-paint --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 bytes become a live, interactive terminal screen?”
This question forces you to connect control sequences, cursor state, and rendering.
5.4 Concepts You Must Understand First
- ECMA-48 Control Sequences
- What is CSI and why is ESC the prefix?
- Book Reference: “The Linux Programming Interface” - Ch. 62
- Raw Input vs Canonical Input
- Why does canonical mode block keypresses?
- Book Reference: “Advanced Programming in the UNIX Environment” - Ch. 18
- Frame vs Cell Rendering
- Why is full-screen redraw inefficient?
- Book Reference: “Clean Architecture” - Ch. 4-5
5.5 Questions to Guide Your Design
- Rendering
- How will you store the grid state (2D array, map)?
- How will you redraw only changed cells?
- Input
- How will you decode arrow keys from escape sequences?
- How will you handle a standalone ESC vs an escape sequence?
5.6 Thinking Exercise
Draw the Cursor State Machine
Sketch how the cursor moves and how a draw command toggles a cell.
Questions to answer:
- What happens at the grid boundaries?
- How do you prevent the cursor from leaving the grid?
5.7 The Interview Questions They’ll Ask
- “Explain how ANSI escape sequences move the cursor.”
- “Why does raw mode matter for TUIs?”
- “What causes flicker in a terminal UI?”
- “How would you test a terminal renderer?”
- “How do you restore terminal state after exit?”
5.8 Hints in Layers
Hint 1: Start with a static grid Draw a fixed grid and verify you can position the cursor with absolute coordinates.
Hint 2: Track cursor state Store cursor position in variables and update them on arrow keys.
Hint 3: Pseudocode for redraw
if cell_changed:
move_cursor(x, y)
write(cell_glyph)
Hint 4: Debugging Add a key that prints the current cursor coordinates in the footer.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Terminal I/O | “The Linux Programming Interface” | Ch. 62 |
| Input handling | “Advanced Programming in the UNIX Environment” | Ch. 18 |
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: “Screen scrolls instead of updating”
- Why: You are printing newlines instead of moving the cursor.
- Fix: Use cursor positioning sequences or library equivalents.
- Quick test: Draw a header and rewrite it in place.
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
- “The Linux Programming Interface” by Michael Kerrisk
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