Project 7: Curses Form Wizard with Validation

A multi-step form wizard with validation and error messages.

Quick Reference

Attribute Value
Difficulty Level 3: Advanced (REFERENCE.md)
Time Estimate 1-2 weeks
Main Programming Language C
Alternative Programming Languages Python, Rust
Coolness Level Level 3: Genuinely Clever (REFERENCE.md)
Business Potential Level 2: Micro-SaaS (REFERENCE.md)
Prerequisites Curses and ncurses Abstractions, Input Modes and Key Decoding
Key Topics Input Forms

1. Learning Objectives

By completing this project, you will:

  1. Build and validate the core behavior described in the real-world outcome.
  2. Apply Curses and ncurses Abstractions, Input Modes and Key Decoding to a working TUI.
  3. Design a predictable input-to-rendering pipeline with explicit state changes.
  4. Produce a tool that behaves consistently across terminals and restores state on exit.

2. All Theory Needed (Per-Concept Breakdown)

Curses and ncurses Abstractions

Fundamentals Curses (and its modern implementation ncurses) provides a higher-level, terminal-independent method for updating character screens. It abstracts the terminal into windows, handles input decoding, and optimizes screen updates. The goal is to let you work at the level of windows and characters rather than raw escape sequences. The ncurses family supports window and pad manipulation, input handling, color attributes, and terminfo-based portability. citeturn1search2 In practice, curses becomes your rendering engine and event loop helper: you draw into windows, then let the library decide the minimal updates to send to the terminal. It also standardizes common input options like keypad mode.

Deep Dive into the concept Curses is effectively a rendering engine plus a terminal capability adapter. It maintains an internal representation of the screen and computes the minimal set of updates to apply, based on terminfo. This lets you write portable TUIs without hardcoding escape sequences. A core concept is the window: a rectangular region with its own coordinate system. Windows can be subdivided to create multiple panes, and can be overlaid with panels. The standard screen (stdscr) is the default full-screen window. citeturn1search2

Pads extend windows beyond the visible screen: they are not constrained to terminal size and can be partially displayed with prefresh or pnoutrefresh. This is useful for scrollable content and large data views. citeturn9search0

Curses also integrates with terminfo to map key sequences to key codes, so you can read KEY_UP instead of raw escape sequences. It handles echo, cbreak, and other input modes internally. This helps you build interactive apps without re-implementing a full input decoder. However, you still need to understand its state model: curses keeps both a virtual screen and a physical screen, and only updates differences. If you bypass curses and write directly to stdout, you can desynchronize its internal state.

In projects like a top-like dashboard, curses helps you manage refresh loops, handle resize events, and maintain multiple windows. The tradeoff is that curses is more imperative: you issue commands to mutate windows, and the library decides how to apply them. This is different from modern MVU frameworks which are more declarative. Understanding this difference is crucial when deciding which tool fits a project.

Finally, curses is widely deployed and available on most Unix-like systems. It remains the best choice for low-level portability and performance, especially in constrained environments. But it comes with constraints: limited widget set, manual layout, and a steep learning curve if you do not already know terminal control.

From an architectural perspective, curses encourages a “draw then refresh” discipline. You update window contents, then call refresh on each window or call a batch update that applies all pending changes. This explicit staging is useful because it lets you separate computation from rendering: compute process statistics first, then draw, then refresh. It also means you can insert instrumentation around refresh to measure update costs.

Curses input handling is both powerful and subtle. The library can translate raw key sequences into symbolic key codes, but you must configure it correctly (cbreak vs raw, echo on/off, keypad mode). Because curses owns the terminal state, mixing it with direct writes or external libraries can lead to corrupted output. Therefore, treat curses as the sole authority for screen output once initialized. If you need to integrate with other output (like logging), direct it to a file instead of stdout.

For larger layouts, curses requires manual geometry management. You must compute window sizes from terminal dimensions, handle resizes, and decide what to do when the terminal is too small. A useful pattern is to define a layout function that returns window rectangles based on rows and columns, then re-create or resize windows on resize events. This keeps the geometry logic isolated and testable.

How this fits on projects

  • Project 5 (ncurses Dashboard), Project 6 (menuconfig Editor), Project 7 (Form Wizard)

Definitions & key terms

  • Window: A rectangular region with its own coordinate system. citeturn1search2
  • Pad: A window larger than the screen, displayed partially. citeturn9search0
  • Virtual screen: The desired screen state in curses; diffed to the physical screen.

Mental model diagram

App logic -> Curses virtual screen -> diff -> terminfo sequences -> terminal
                       |
                       +-> windows/pads -> layout -> refresh

How it works

  1. Initialize curses and create windows.
  2. Update window content based on app state.
  3. Call refresh to compute and apply diffs.
  4. Read input via curses key handling.

Minimal concrete example

PSEUDOCODE:
init_curses()
win = create_window(rows=10, cols=30, y=0, x=0)
write(win, "CPU: 12%")
refresh(win)
key = get_key()

Common misconceptions

  • “curses is obsolete” -> It is still the most portable low-level TUI option.
  • “curses updates immediately” -> It uses a virtual screen and refresh step.

Check-your-understanding questions

  1. Why does curses use a virtual screen?
  2. What is the difference between a window and a pad?
  3. Why should you avoid direct stdout writes in a curses app?

Check-your-understanding answers

  1. To compute minimal updates and reduce output.
  2. Pads can be larger than the visible screen and are partially displayed.
  3. It desynchronizes curses’ internal state and breaks diffing.

Real-world applications

  • menuconfig, htop, nmtui, text-mode installers

Where you’ll apply it

  • Project 5, Project 6, Project 7

References

  • GNU ncurses overview (terminal-independent updates, optimization) citeturn1search2
  • “Advanced Programming in the UNIX Environment” - Ch. 18

Key insights Curses is a stateful renderer and input decoder built on terminfo portability.

Summary Curses abstracts terminal differences while providing efficient screen updates, but it requires disciplined state management.

Homework/Exercises to practice the concept

  1. Design a two-pane layout for a TUI dashboard using window coordinates.
  2. Explain how a pad would be used for scrolling logs.

Solutions to the homework/exercises

  1. Split rows into header, body; split body into left/right windows.
  2. Store logs in a pad larger than screen and use prefresh to display a slice.

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. citeturn1search3

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. citeturn3search2

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. citeturn1search3
  • Non-canonical mode: Byte-level input without line editing. citeturn1search3
  • Raw mode: A common configuration that disables echo, line buffering, and special-key processing. citeturn3search2
  • 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

  1. Configure termios to canonical or non-canonical mode.
  2. Read bytes from stdin as they arrive.
  3. Feed bytes into a decoder that recognizes escape sequences.
  4. Emit high-level events (Up, Down, Ctrl+C, etc.).
  5. 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

  1. Why does canonical mode prevent real-time key handling?
  2. What is the role of MIN and TIME in non-canonical mode?
  3. Why must you restore terminal state on exit?

Check-your-understanding answers

  1. Input is buffered until newline/EOF, so keypresses are not delivered immediately.
  2. They control when read() returns in byte-oriented input.
  3. 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) citeturn1search3
  • crossterm raw mode behavior (summary of effects) citeturn3search2
  • “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

  1. Draw a state machine for decoding ESC-based arrow keys.
  2. Describe how you would detect a resize event in your app.

Solutions to the homework/exercises

  1. Start in NORMAL; on ESC move to ESC_SEEN; accept ‘[’ then digits; map known final byte to key.
  2. Use a signal handler for window resize and push a Resize event into your loop.

3. Project Specification

3.1 What You Will Build

A multi-step form wizard with validation and error messages.

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

  1. Core Interaction: Implements the main interaction loop and updates the screen correctly.
  2. Input Handling: Handles required keys without blocking and supports quit/exit.
  3. Rendering: Updates only what changes to avoid flicker.
  4. Resize Handling: Adapts to terminal resize or shows a clear warning state.
  5. 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

$ ./form-wizard

$ ./form-wizard Step 1/3: User Info Name: [____] Email: [____]

Errors:

  • Email must contain ‘@’ ```

ASCII layout:

[Step header]
[Form fields]
[Error box]

### 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)

$ ./form-wizard


#### 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

$ ./form-wizard

$ ./form-wizard
Step 1/3: User Info
Name: [__________]
Email: [__________]

Errors:
- Email must contain '@'

ASCII layout:

[Step header]
[Form fields]
[Error box]

#### 3.7.4 Failure Demo (Deterministic)

$ ./form-wizard –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

  1. Build new frame from current state
  2. Compare with old frame
  3. 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 I build reliable, keyboard-first forms in a terminal?”

5.4 Concepts You Must Understand First

  1. Input focus management
    • How do you switch between fields?
    • Book Reference: “Advanced Programming in the UNIX Environment” - Ch. 18
  2. Validation logic
    • Where does validation belong: input layer or state layer?
    • Book Reference: “Clean Architecture” - Ch. 4

5.5 Questions to Guide Your Design

  1. Form model
    • How do you represent field values and errors?
    • How do you store which field is active?
  2. User feedback
    • How will you display errors without breaking layout?

5.6 Thinking Exercise

Design Focus Movement

Draw the focus order and describe Tab/Shift+Tab behavior.

Questions to answer:

  • How do you handle enter vs tab?
  • What happens on invalid input?

5.7 The Interview Questions They’ll Ask

  1. “How do you manage focus in a TUI?”
  2. “Where should validation logic live?”
  3. “How do you display errors without flicker?”
  4. “How would you test form input?”
  5. “How do you support keyboard shortcuts?”

5.8 Hints in Layers

Hint 1: Start with one field Implement a single text field and cursor movement.

Hint 2: Add focus switching Store an index of active field.

Hint 3: Pseudocode

if key == TAB: focus = (focus + 1) % fields

Hint 4: Debugging Show the active field index in a hidden debug bar.


5.9 Books That Will Help

Topic Book Chapter
Input handling “Advanced Programming in the UNIX Environment” Ch. 18
UI design “Clean Architecture” Ch. 4-5

5.10 Implementation Phases

Phase 1: Foundation

Goals:

  • Initialize the terminal and input handling
  • Render the first static screen

Tasks:

  1. Implement setup and teardown
  2. 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:

  1. Add event processing
  2. 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:

  1. Add resize handling
  2. 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

  1. Resize: Shrink terminal below minimum and verify warning.
  2. Rapid Input: Hold down keys and ensure no crash.
  3. 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: “Input overwrites adjacent fields”

  • Why: Field width not enforced.
  • Fix: Clip input to field width.
  • Quick test: Enter a long string and verify layout.

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
  • 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

  • “Advanced Programming in the UNIX Environment”

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
  • 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