Project 6: system-monitor-tui (Full-Screen Dashboard)

Build a full-screen TUI system monitor with fast rendering, keyboard navigation, and safe terminal handling.

Quick Reference

Attribute Value
Difficulty Level 4 (Expert)
Time Estimate 2 weeks
Main Programming Language Go (Alternatives: Rust)
Alternative Programming Languages Rust
Coolness Level Level 4: Hardcore tech flex
Business Potential 4: Monitoring tooling
Prerequisites Terminal control, OS metrics, concurrency
Key Topics Raw mode, rendering loop, signals, metrics

1. Learning Objectives

By completing this project, you will:

  1. Enter and restore terminal raw mode safely.
  2. Build a fast render loop with diff-based updates.
  3. Collect CPU, memory, and process metrics efficiently.
  4. Handle SIGWINCH and resize events gracefully.
  5. Provide a reliable TUI that leaves the terminal clean on exit.

2. All Theory Needed (Per-Concept Breakdown)

2.1 Terminal Raw Mode and Input Handling

Fundamentals

A full-screen TUI requires control over how the terminal handles input and output. In canonical mode, the terminal buffers input until Enter is pressed and echoes characters automatically. For a TUI, you need raw mode, which disables line buffering and echo. This allows you to react to individual key presses and to render the screen manually. Raw mode also makes it easy to capture special keys like arrows, but it can leave the terminal in a broken state if not restored properly.

Deep Dive into the concept

Raw mode is achieved by modifying terminal attributes using APIs like termios (Unix) or by using TUI frameworks that abstract it. When you switch to raw mode, you typically disable echo, canonical input, and signal generation from keys like Ctrl+C (depending on configuration). This means your program becomes responsible for interpreting input bytes. For example, arrow keys are sent as escape sequences like \x1b[A, and you must parse them to map to user actions.

Entering raw mode is not enough; you must ensure it is always restored on exit. This includes normal exit paths and signal-triggered exits. The usual approach is to save the original terminal settings on startup and use defer or a shutdown handler to restore them. If you fail to restore the terminal, the user’s shell will remain in raw mode, which is a terrible user experience. Proper cleanup is one of the hallmarks of professional CLI tools.

Input handling is not just about parsing keys. It also involves dealing with repeated key presses, key repeat rates, and non-ASCII sequences. Many TUIs implement an input event loop that reads bytes and decodes them into high-level events (Up, Down, Enter, q). This separation makes your code more maintainable and allows for customization or key binding features later.

Finally, raw mode changes how signals work. In raw mode, Ctrl+C might not generate SIGINT automatically, depending on settings. If you disable signal generation, you must manually map Ctrl+C to a quit action. Some frameworks handle this for you. Either way, you need to be explicit about how users exit and ensure you handle both keyboard exits and external signals.

How this fit on projects

This concept defines how your TUI captures input and how it cleans up on exit. It influences the event loop design and the terminal safety requirements.

Definitions & key terms

  • Raw mode: Terminal mode without line buffering or echo.
  • Canonical mode: Default terminal mode with line buffering.
  • Escape sequence: Multi-byte sequence representing special keys.
  • Terminal state: The set of flags that define terminal behavior.

Mental model diagram (ASCII)

Keypress -> bytes -> decoder -> input event -> update model

How it works (step-by-step)

  1. Save current terminal settings.
  2. Switch to raw mode.
  3. Read input bytes and decode events.
  4. Update UI state based on events.
  5. On exit, restore original terminal settings.

Minimal concrete example

Input bytes: 0x1b 0x5b 0x41 -> "Arrow Up"

Common misconceptions

  • “Raw mode is optional.” -> Full-screen TUIs require it.
  • “Terminal state restores itself.” -> It does not; you must restore it.
  • “Ctrl+C always works.” -> It can be disabled in raw mode.

Check-your-understanding questions

  1. Why does a TUI need raw mode?
  2. What happens if raw mode is not restored?
  3. How are arrow keys represented in input bytes?

Check-your-understanding answers

  1. It enables per-key input and disables automatic echo.
  2. The terminal stays broken, with no echo and strange input behavior.
  3. As multi-byte escape sequences like \x1b[A.

Real-world applications

  • top and htop rely on raw mode to read keys.
  • Text editors like vim use raw mode extensively.

Where you will apply it

References

  • Advanced Programming in the UNIX Environment, terminal I/O chapters
  • termios documentation

Key insights

Raw mode is powerful but dangerous; always restore state on exit.

Summary

Raw mode enables responsive TUIs but requires careful setup and cleanup to avoid corrupting the terminal.

Homework/Exercises to practice the concept

  1. Write a program that toggles raw mode and prints key codes.
  2. Handle arrow keys by decoding escape sequences.
  3. Ensure terminal state is restored on SIGINT.

Solutions to the homework/exercises

  1. Use termios and read from stdin.
  2. Map \x1b[A to Up, \x1b[B to Down.
  3. Use a signal handler to restore state.

2.2 Render Loop, Diffing, and Performance

Fundamentals

A TUI must redraw the screen quickly without flickering. The simplest approach is to clear the screen and redraw everything each frame, but that becomes inefficient and can cause visible flicker. A better approach is diff-based rendering: compute the differences between the previous frame and the next frame, then only update the changed regions. This reduces output volume and improves performance, especially for large screens or high refresh rates.

Deep Dive into the concept

Rendering in a terminal is essentially writing characters to stdout. Each write is a sequence of bytes, and large redraws can be expensive. Diff-based rendering treats the screen as a 2D buffer of characters. Each frame, you compute a new buffer, compare it to the previous buffer, and emit only the changed cells. This is similar to how modern GUI frameworks render, but at a text grid level.

To make this work, you need a render loop with a fixed or adaptive frame rate. Many TUIs choose 10-30 FPS, which is enough for smooth updates without wasting CPU. You should also decouple data collection (metrics) from rendering. Metrics collection might be expensive or have different timing. For example, CPU usage might be updated every second, while the UI could refresh at 10 FPS using the latest data.

Terminal resizing adds complexity. When the terminal size changes, your buffer dimensions change. You must handle SIGWINCH, recalculate layout, and rebuild your buffers. If you ignore resize events, your UI will either overflow or leave blank regions. A proper TUI should detect the new size and adapt. It should also display a warning if the terminal is too small to render the UI properly.

Performance also depends on avoiding excessive allocations. Reuse buffers where possible. Use a single render buffer and update in place. Avoid heavy string concatenation in hot loops. If you use a TUI framework like Bubble Tea or Ratatui, it handles many of these concerns, but you should still understand the underlying mechanics so you can tune performance.

How this fit on projects

This concept drives the architecture of your TUI render loop and ensures that the monitor feels smooth and responsive.

Definitions & key terms

  • Diff-based rendering: Only update changed screen regions.
  • Frame rate: Number of UI updates per second.
  • SIGWINCH: Signal for terminal resize.
  • Render buffer: 2D array representing screen state.

Mental model diagram (ASCII)

Model -> Render buffer -> Diff -> Terminal output

How it works (step-by-step)

  1. Build a render buffer from current metrics and layout.
  2. Compare it to previous buffer.
  3. Emit only changed cells or lines.
  4. Sleep until next frame.
  5. On resize, rebuild buffers and layout.

Minimal concrete example

Prev: CPU 40%
Next: CPU 41%
Diff: update only the percentage area

Common misconceptions

  • “Full redraw is always fine.” -> It wastes CPU and causes flicker.
  • “Higher FPS is always better.” -> It can saturate the terminal.
  • “Resize events can be ignored.” -> UI breaks on small terminals.

Check-your-understanding questions

  1. Why is diff-based rendering more efficient?
  2. How does SIGWINCH affect rendering?
  3. What is a reasonable FPS for a TUI?

Check-your-understanding answers

  1. It reduces output size by updating only changed cells.
  2. It changes the terminal dimensions, requiring layout rebuild.
  3. 10-30 FPS is usually sufficient.

Real-world applications

  • htop and btop use diff-based rendering.
  • TUI frameworks implement buffer diffing internally.

Where you will apply it

References

  • Bubble Tea or Ratatui documentation
  • Terminal rendering articles

Key insights

Efficient rendering is about minimizing output, not maximizing frame rate.

Summary

A smooth TUI depends on diff-based rendering, fixed refresh rates, and resize handling.

Homework/Exercises to practice the concept

  1. Implement a screen buffer and diff algorithm.
  2. Measure output size with full redraw vs diff.
  3. Handle SIGWINCH and recalc layout.

Solutions to the homework/exercises

  1. Use a 2D array and compare cells.
  2. Count bytes written in each strategy.
  3. Update buffer sizes on resize.

2.3 Metrics Collection and OS Interfaces

Fundamentals

A system monitor is only as accurate as its metrics. CPU usage, memory usage, and process lists come from OS-specific interfaces. On Linux, these come from /proc; on macOS, from sysctl or system APIs. The challenge is to gather metrics efficiently without slowing down the system. You must also normalize metrics across platforms if you want cross-platform behavior.

Deep Dive into the concept

CPU usage is typically computed as a delta between two snapshots of CPU time. On Linux, /proc/stat provides total and idle ticks. The usage percentage is 1 - (idle_delta / total_delta). Memory usage can be derived from /proc/meminfo using fields like MemTotal and MemAvailable. Process lists come from /proc/[pid] entries or from system APIs. On macOS, you might use sysctl or libraries like gopsutil to abstract differences.

Sampling frequency matters. If you sample too frequently, you may increase overhead and get noisy data. A 1-second interval is common for system monitors. You should store the previous snapshot and compute deltas. For process CPU usage, you need per-process deltas, which can be expensive. You can start with a limited view (top N processes) and refresh every few seconds.

Cross-platform support is a trade-off. If you want to support multiple OSes, consider using a library that abstracts metrics collection. If you implement it yourself, you need OS-specific code paths. For this project, you can focus on one OS and document the limitation, but design your interfaces so a second implementation can be added later.

Finally, metrics collection should be decoupled from rendering. Use a background goroutine or thread to update metrics and store them in a shared model. The render loop reads the latest snapshot. This avoids blocking the UI while waiting for metrics to update.

How this fit on projects

This concept determines the data collection layer and how metrics are displayed. It is critical to performance and accuracy.

Definitions & key terms

  • Snapshot: A point-in-time metrics reading.
  • Delta: Difference between two snapshots.
  • /proc: Linux process and system info filesystem.
  • Sampling interval: Time between metric updates.

Mental model diagram (ASCII)

OS metrics -> snapshot A -> snapshot B -> delta -> UI model

How it works (step-by-step)

  1. Read metrics from OS interface.
  2. Store snapshot in memory.
  3. On next interval, read again and compute deltas.
  4. Update UI model with percentages.
  5. Render the model.

Minimal concrete example

CPU usage = 1 - (idle_delta / total_delta)

Common misconceptions

  • “Reading /proc is free.” -> It has overhead if done too often.
  • “Single snapshot is enough.” -> CPU usage requires deltas.
  • “Process lists are cheap.” -> Enumerating all processes is expensive.

Check-your-understanding questions

  1. Why do you need two snapshots to compute CPU usage?
  2. What is a good sampling interval for a monitor?
  3. Why should metrics collection be decoupled from rendering?

Check-your-understanding answers

  1. CPU usage is based on change over time.
  2. Around 1 second for stable data.
  3. To keep UI responsive and avoid blocking.

Real-world applications

  • top and htop use snapshot deltas for CPU usage.
  • Monitoring agents collect metrics at fixed intervals.

Where you will apply it

References

  • /proc documentation
  • gopsutil library docs

Key insights

Metrics are snapshots over time; deltas turn raw counters into meaningful rates.

Summary

Efficient metrics collection depends on sampling intervals, deltas, and decoupled pipelines.

Homework/Exercises to practice the concept

  1. Read /proc/stat twice and compute CPU usage.
  2. Implement a memory usage calculator from /proc/meminfo.
  3. List the top 5 processes by CPU.

Solutions to the homework/exercises

  1. Compute total and idle deltas.
  2. Use MemTotal and MemAvailable.
  3. Sort by CPU delta and slice top 5.

3. Project Specification

3.1 What You Will Build

A full-screen TUI system monitor named system-monitor-tui that displays CPU usage, memory usage, and top processes. It uses raw mode, handles resize events, and exits cleanly without breaking the terminal. It supports keyboard navigation and a help overlay.

3.2 Functional Requirements

  1. Display CPU and memory usage with bars and percentages.
  2. List top processes by CPU or memory.
  3. Keyboard controls: up/down, sort toggle, quit.
  4. Resize handling via SIGWINCH.
  5. Clean exit that restores terminal state.
  6. Deterministic fixture mode: --fixture <json> loads sample metrics.

3.3 Non-Functional Requirements

  • Performance: refresh at 10 FPS with low CPU overhead.
  • Reliability: no terminal corruption on exit.
  • Usability: clear labels and key hints.

3.4 Example Usage / Output

+----------------- System Monitor -----------------+
| CPU [################----] 68%                   |
| RAM [##############------] 42%                   |
+----------------- Top Processes ------------------+
| PID   CMD            CPU%   MEM%                 |
| 1223  node           15.2   4.1                  |
| 887   postgres       10.4   3.8                  |
+--------------------------------------------------+
[q] Quit  [s] Sort  [r] Refresh

3.5 Data Formats / Schemas / Protocols

Internal metrics model:

{"cpu":68.0,"mem":42.0,"procs":[{"pid":1223,"cmd":"node","cpu":15.2,"mem":4.1}]}

3.6 Edge Cases

  • Terminal too small -> show warning message.
  • Metrics unavailable -> show “N/A” with error state.
  • SIGINT -> exit cleanly and restore terminal.

3.7 Real World Outcome

3.7.1 How to Run (Copy/Paste)

# Build
go build -o system-monitor-tui ./cmd/system-monitor-tui

# Run
./system-monitor-tui

3.7.2 Golden Path Demo (Deterministic)

$ ./system-monitor-tui --fixture ./fixtures/metrics_sample.json
# UI shows CPU 20%, RAM 35%, process list with stable ordering from fixture
# Press q to exit
$ echo $?
0

3.7.3 Failure Demo (Deterministic)

$ ./system-monitor-tui
system-monitor-tui: terminal too small (min 80x20)
$ echo $?
2

3.7.4 Exit Codes

  • 0: Normal exit.
  • 2: Terminal too small or metrics unavailable.
  • 130: Interrupted by SIGINT.

4. Solution Architecture

4.1 High-Level Design

+------------------+
| Metrics Sampler  |
+------------------+
          |
          v
+------------------+     +------------------+
| UI Model         | --> | Renderer         |
+------------------+     +------------------+
          |                        |
          v                        v
+------------------+     +------------------+
| Input Handler    | --> | Terminal Control |
+------------------+     +------------------+

4.2 Key Components

Component Responsibility Key Decisions
Sampler collect metrics 1s sampling interval
UI Model store current state shared mutex or channel
Renderer draw UI diff-based rendering
Input handler decode keys raw mode and escape parsing

4.3 Data Structures (No Full Code)

type Metrics struct {
    CPU float64
    Mem float64
    Procs []Proc
}

4.4 Algorithm Overview

Key Algorithm: Render Loop

  1. Read current metrics snapshot.
  2. Build render buffer from layout.
  3. Diff against previous buffer.
  4. Write updates to terminal.

Complexity Analysis:

  • Time: O(W*H) per frame for full diff.
  • Space: O(W*H) for buffers.

5. Implementation Guide

5.1 Development Environment Setup

mkdir system-monitor-tui && cd system-monitor-tui

5.2 Project Structure

cmd/system-monitor-tui/
  main.go
internal/
  metrics/
  tui/
  input/

5.3 The Core Question You’re Answering

“How do I maintain a live terminal UI without breaking terminal state?”

5.4 Concepts You Must Understand First

  1. Raw mode and input handling.
  2. Diff-based rendering and resize handling.
  3. Metrics sampling and deltas.

5.5 Questions to Guide Your Design

  1. How will you handle terminal resize events?
  2. What is the minimum terminal size?
  3. How do you avoid flicker?

5.6 Thinking Exercise

Draw a layout with header, metrics bars, and process table.

5.7 The Interview Questions They’ll Ask

  1. “Why do TUIs need raw mode?”
  2. “How do you avoid flicker?”
  3. “How do you handle SIGWINCH?”

5.8 Hints in Layers

Hint 1: Use a TUI framework Bubble Tea or Ratatui handle raw mode.

Hint 2: Separate update and render Update model on a timer, render on frame loop.

Hint 3: Handle SIGWINCH Recalculate layout on resize.

Hint 4: Restore terminal on exit Always defer cleanup.

5.9 Books That Will Help

Topic Book Chapter
Terminal I/O Advanced Programming in the UNIX Environment Ch. 18
Signals Advanced Programming in the UNIX Environment Ch. 10

5.10 Implementation Phases

Phase 1: Terminal and Input (3-4 days)

Goals: raw mode, key handling, quit.

Phase 2: Metrics and Layout (4-5 days)

Goals: sampler, layout, render buffer.

Phase 3: Polish (3-4 days)

Goals: resize handling, performance, tests.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Framework none vs TUI lib TUI lib reduces risk.
FPS 10 vs 60 10 less CPU, enough smoothness.
Sampling 0.2s vs 1s 1s stable metrics.

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests metrics parsing cpu delta computation
Integration Tests render buffer stable diff output
Edge Case Tests small terminal warning state

6.2 Critical Test Cases

  1. Terminal restored after exit.
  2. SIGWINCH triggers layout recalculation.
  3. Metrics sampling stable across time.

6.3 Test Data

fixtures/proc_stat.txt

7. Common Pitfalls and Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Raw mode not restored broken terminal always restore in defer
Full redraw flicker shaky UI diff-based rendering
Heavy sampling high CPU usage lower sampling rate

7.2 Debugging Strategies

  • Add a log mode that writes to a file, not stdout.
  • Use synthetic metrics for deterministic tests.
  • Simulate resize events.

7.3 Performance Traps

  • Redrawing entire screen every frame wastes output bandwidth.

8. Extensions and Challenges

8.1 Beginner Extensions

  • Add disk usage widget.
  • Add a help overlay screen.

8.2 Intermediate Extensions

  • Add process filtering/search.
  • Add sorting by memory.

8.3 Advanced Extensions

  • Add remote metrics via SSH.
  • Add plugin widgets.

9. Real-World Connections

9.1 Industry Applications

  • System monitors like htop, btop.
  • htop, btop, gotop.

9.3 Interview Relevance

  • Terminal control and OS metrics are common systems interview topics.

10. Resources

10.1 Essential Reading

  • Advanced Programming in the UNIX Environment, Ch. 18 and 10
  • gopsutil documentation

10.2 Video Resources

  • “Building TUIs” talks

10.3 Tools and Documentation

  • Bubble Tea or Ratatui docs

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain raw mode and escape sequences.
  • I can explain diff-based rendering.
  • I can explain CPU usage calculation.

11.2 Implementation

  • Terminal is restored on exit.
  • UI updates without flicker.
  • Metrics are accurate.

11.3 Growth

  • I can add a new widget safely.
  • I can handle resize events gracefully.
  • I can demo stable TUI behavior.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Full-screen TUI renders with CPU/memory.
  • Input handling and quit works.
  • Terminal state restored on exit.

Full Completion:

  • Process list with sorting and resize handling.

Excellence (Going Above and Beyond):

  • Plugin widgets and remote metrics support.