Project 5: ANSI Color Renderer
Implement SGR (Select Graphic Rendition) parsing and color attributes, then render colored text to a minimal screen model.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Intermediate |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C (Alternatives: Rust, Go) |
| Alternative Programming Languages | Rust, Go |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | Level 2: Open Source Builder |
| Prerequisites | Minimal terminal + parser, arrays |
| Key Topics | SGR, color palettes, attributes |
1. Learning Objectives
By completing this project, you will:
- Parse SGR sequences and map them to color attributes.
- Maintain per-cell attributes in a screen model.
- Render ANSI colors correctly for 8/16/256/truecolor modes.
- Handle attribute stacking and resets deterministically.
- Build tests that compare rendered output to expected palettes.
2. All Theory Needed (Per-Concept Breakdown)
Concept 1: SGR (Select Graphic Rendition) Semantics
Fundamentals
SGR sequences are CSI commands ending with m that set text attributes such as foreground color, background color, bold, underline, and reset. The simplest forms are ESC [ 31 m for red foreground and ESC [ 0 m to reset all attributes. More complex forms include 256-color and truecolor modes, e.g., ESC [ 38;5;196 m and ESC [ 38;2;255;0;0 m. Correct SGR handling is essential for program output, as many TUIs rely on color for readability.
Deep Dive into the Concept
SGR parameters are a list of integers. Each parameter can either set a specific attribute or modify how subsequent parameters should be interpreted. For example, 0 resets all attributes; 1 turns on bold; 22 resets bold to normal. Foreground colors are set with 30-37 (standard), 90-97 (bright), 39 (default). Background colors use 40-47, 100-107, and 49 (default). These are the basic 8 and 16 color palettes. A minimal terminal can implement these first.
For 256-color mode, the sequence 38;5;n sets the foreground color index n and 48;5;n sets the background. The palette includes 0-15 for the basic colors, 16-231 for a 6x6x6 color cube, and 232-255 for grayscale. You must map these indices to RGB values when rendering in a modern context. Truecolor mode uses 38;2;r;g;b or 48;2;r;g;b, which requires storing full 24-bit RGB values per cell.
SGR sequences are stateful: they set attributes that apply to subsequent text until changed or reset. This means your screen model must carry current attributes, and each character written to a cell copies the current attributes into that cell. If you fail to copy attributes, colors will not persist correctly. Reset semantics are also subtle: 0 resets all attributes, but individual resets like 39 or 49 only reset foreground or background. A correct implementation must interpret each parameter in order.
Compatibility quirks matter. Many programs emit redundant resets or combine multiple attributes in one SGR sequence. Some emit ESC [ m with no params, which is equivalent to reset. Your parser must treat missing params as 0 and handle sequences like ESC [ 0;1;31 m properly. This is a good place to test deterministic behavior with known logs.
How this fits on projects
This concept is central to this project and needed for P13 and P15.
Definitions & Key Terms
- SGR -> Select Graphic Rendition (CSI … m).
- Foreground/Background -> text and cell background colors.
- Palette -> mapping of indices to RGB values.
- Truecolor -> 24-bit RGB color support.
Mental Model Diagram (ASCII)
Current attrs: fg=red, bg=default, bold=on
Text "hi" -> cells store {ch='h', fg=red, bg=default, bold=on}
How It Works (Step-by-Step)
- Parse CSI params ending with
m. - For each param, update current attribute state.
- When printing a character, copy current attrs into the cell.
- On reset params, return to defaults.
Invariants:
- Attribute state is updated in order.
- Each cell stores a snapshot of attributes.
Failure modes:
- Missing default handling for
ESC [ m. - Ignoring
39/49resets.
Minimal Concrete Example
if (param == 0) attrs = defaults;
else if (param == 31) attrs.fg = COLOR_RED;
else if (param == 39) attrs.fg = COLOR_DEFAULT;
Common Misconceptions
- “SGR colors are only 8 colors.” -> 256-color and truecolor exist.
- “Attributes apply per-line.” -> They apply per-character until reset.
- “Missing params mean no-op.” -> Missing params mean reset.
Check-Your-Understanding Questions
- What does
ESC [ 38;2;255;0;0 mmean? - How do you reset only the background color?
- Why must attributes be stored per cell?
Check-Your-Understanding Answers
- Set foreground to truecolor RGB (255,0,0).
- Use
ESC [ 49 m. - Because attributes persist across text and must be preserved per character.
Real-World Applications
ls --coloroutputgit diffcolored output- TUIs like
htopandvim
Where You’ll Apply It
- This project: Section 3.2 (SGR handling), Section 4.3 (cell struct)
- Also used in: P13-full-terminal-emulator, P15-feature-complete-terminal-capstone
References
- xterm SGR documentation
- ECMA-48 color definitions
Key Insight
SGR is a stateful attribute machine; correctness depends on applying params in order.
Summary
Color support requires accurate SGR parsing and per-cell attribute storage.
Homework/Exercises to Practice the Concept
- Render 16 colors and verify against a known palette.
- Implement 256-color mapping and test index 196 (bright red).
- Implement truecolor and compare with RGB values.
Solutions to the Homework/Exercises
- Use a table of ANSI base colors and print a grid.
- Map index 196 to (255,0,0) in the 6x6x6 cube.
- Store RGB values directly in the cell attributes.
Concept 2: Attribute State, Resets, and Rendering
Fundamentals
Attributes like bold, underline, and colors are stateful and cumulative. The terminal maintains a “current attribute state” that changes when SGR sequences arrive. Each printed character captures the current state, and resets restore defaults. Rendering must convert those stored attributes into actual colors or style settings.
Deep Dive into the Concept
Attribute handling is a state machine layered on top of parsing. The terminal maintains a struct representing current attributes: foreground, background, bold, underline, inverse, etc. When a new SGR sequence arrives, you update this struct in parameter order. This is critical because some parameters depend on the current state: for example, 22 resets bold but does not touch colors; 39 resets only the foreground color. A robust implementation should handle each param individually and update only the relevant fields.
In a renderer, you may need to map terminal attributes to actual output capabilities. If you are rendering to a real terminal (stdout), you might output ANSI sequences again, but for a screen model you should store the attributes and then render them with color codes or a color library. This project focuses on correctness rather than performance, so you can render by clearing the screen and printing each cell with the appropriate color codes. This is slower but keeps the model simple.
Inverse (SGR 7) swaps foreground and background. This can be implemented at render time (swap colors) or by modifying the attribute state. Both approaches are valid, but render-time swapping is simpler if you already store both colors. Bold and dim can be approximated by using bright colors or adjusting intensity if your renderer supports it.
Reset semantics are easy to get wrong. 0 resets everything; 1 sets bold; 22 resets bold; 24 resets underline; 27 resets inverse. If you ignore the reset parameters, attributes can “leak” across lines and cause confusing output. This is a good target for tests: replay a log with known resets and verify the output matches expected colors.
How this fits on projects
This concept is reused in P06 (scrollback stores attrs) and P13.
Definitions & Key Terms
- Attribute state -> current style settings applied to new text.
- Inverse -> swaps foreground and background colors.
- Reset -> SGR parameters that revert attributes to defaults.
Mental Model Diagram (ASCII)
SGR sequence -> update attrs -> next chars copy attrs into cells
How It Works (Step-by-Step)
- Initialize default attributes.
- Parse SGR params and update current attrs.
- On text output, copy attrs into each cell.
- Render cells by mapping attrs to colors.
Invariants:
- Cells store immutable snapshots of attributes.
- Resets only affect the fields they target.
Failure modes:
- Applying resets globally when only one field should reset.
- Not storing attrs per cell, causing color “smear”.
Minimal Concrete Example
struct Attr { int fg, bg; bool bold; } cur;
if (param == 0) cur = defaults;
else if (param == 1) cur.bold = true;
else if (param == 22) cur.bold = false;
Common Misconceptions
- “Bold means a heavier font.” -> Many terminals use bright colors instead.
- “Reset 0 is optional.” -> Many programs rely on it to clear styles.
- “Attributes can be inferred from text.” -> They must be stored explicitly.
Check-Your-Understanding Questions
- What is the difference between
0and39? - How would you implement inverse mode?
- Why store attributes in each cell?
Check-Your-Understanding Answers
0resets everything;39resets only foreground color.- Swap fg and bg at render time or update attrs on SGR 7.
- Because attributes persist across text and must be preserved.
Real-World Applications
- Syntax highlighting in editors
- Colorized logs and dashboards
Where You’ll Apply It
- This project: Section 4.3 (cell struct), Section 7.1 (pitfalls)
- Also used in: P06-scrollback-buffer-implementation, P13-full-terminal-emulator
References
- xterm SGR reference
- vt100 attribute documentation
Key Insight
Attributes are stateful; correct rendering requires storing them per cell and applying resets precisely.
Summary
A correct color renderer is an attribute state machine plus a faithful mapping to colors.
Homework/Exercises to Practice the Concept
- Implement
SGR 7and verify inverse output. - Create a log that alternates colors and ensure no leakage.
- Add tests for
0,39, and49resets.
Solutions to the Homework/Exercises
- Swap foreground and background at render time.
- Store attrs per cell and reset between segments.
- Verify that only intended fields reset in tests.
3. Project Specification
3.1 What You Will Build
A color-aware terminal renderer that:
- Parses SGR sequences (0, 1, 4, 7, 22, 24, 27, 30-37, 40-47, 90-97, 100-107, 38;5;n, 48;5;n, 38;2;r;g;b, 48;2;r;g;b).
- Stores per-cell attributes in a screen grid.
- Renders the grid with correct colors to stdout or a simple UI.
Intentionally excluded:
- Full VT state machine, scrollback, or GPU rendering.
3.2 Functional Requirements
- SGR parsing: interpret parameter lists in order.
- Attribute state: update current attrs and apply to new text.
- Per-cell storage: store fg/bg/bold/underline in each cell.
- Palette mapping: 256-color index to RGB.
- Truecolor: support RGB values directly.
- Renderer: print colored output deterministically.
3.3 Non-Functional Requirements
- Determinism: fixed palette mapping and demo logs.
- Correctness: resets are handled precisely.
- Clarity: code makes attribute state transitions obvious.
3.4 Example Usage / Output
$ ./color_term --demo
[demo] SGR 31 red
[demo] SGR 38;2;255;128;0 truecolor
3.5 Data Formats / Schemas / Protocols
- Cell structure:
{ch, fg, bg, bold, underline}.
3.6 Edge Cases
ESC [ m(no params) should reset.ESC [ 38;5;999 mshould clamp or reject.- Nested attributes without reset.
3.7 Real World Outcome
A colored terminal renderer that matches xterm color output for common sequences.
3.7.1 How to Run (Copy/Paste)
cc -O2 -o color_term color_term.c
TZ=UTC LC_ALL=C ./color_term --demo --palette xterm
3.7.2 Golden Path Demo (Deterministic)
- Render
samples/ansi-colors.log. - Compare against
samples/ansi-colors.expected.
3.7.3 Failure Demo (Deterministic)
$ ./color_term --sgr "38;5;999"
error: color index out of range (0-255)
exit status: 65
3.7.4 If TUI: ASCII layout
+--------------------------------------+
| ANSI Color Demo |
| red green yellow blue magenta |
| 256: index 196 -> bright red |
+--------------------------------------+
4. Solution Architecture
4.1 High-Level Design
Parser (SGR) -> Attr State -> Screen Grid -> Renderer -> stdout
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| SGR Parser | Interpret SGR params | Stateful param processing |
| Attr State | Current attributes | Defaults + partial resets |
| Screen Grid | Store cells with attrs | Per-cell struct |
| Renderer | Map attrs to output | Fixed palette mapping |
4.3 Data Structures (No Full Code)
struct Attr { int fg; int bg; bool bold; bool underline; bool inverse; };
struct Cell { char ch; struct Attr attr; };
4.4 Algorithm Overview
Key Algorithm: Apply SGR Params
- Iterate params in order.
- Update current attribute state.
- On text output, copy attrs to cell.
Complexity Analysis:
- Time: O(n) per param list
- Space: O(rows*cols)
5. Implementation Guide
5.1 Development Environment Setup
cc --version
5.2 Project Structure
color-term/
|-- src/
| |-- main.c
| |-- sgr.c
| `-- render.c
|-- samples/
| |-- ansi-colors.log
| `-- ansi-colors.expected
|-- Makefile
`-- README.md
5.3 The Core Question You’re Answering
“How do SGR parameters map to actual colors and attributes on a terminal?”
5.4 Concepts You Must Understand First
- SGR parameter semantics and defaults.
- Attribute state and resets.
- 256-color and truecolor mapping.
5.5 Questions to Guide Your Design
- Will you store RGB or palette indices in cells?
- How will you handle unknown SGR params?
- How will you keep palette mapping deterministic?
5.6 Thinking Exercise
Decode ESC [ 1;31;44 m and list all attributes that should be active.
5.7 The Interview Questions They’ll Ask
- How does truecolor differ from 256-color mode?
- Why are resets important for terminal correctness?
- How does inverse mode work?
5.8 Hints in Layers
Hint 1: Start with 8 colors Implement 30-37 and 40-47 first.
Hint 2: Add reset paths early Resets prevent attribute leakage.
Hint 3: Implement 256-color mapping Use the 6x6x6 cube and grayscale range.
Hint 4: Add truecolor Store RGB values per cell.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Terminal control | “The Linux Programming Interface” | Ch. 62 |
| Graphics basics | “Computer Graphics from Scratch” | Ch. 2 |
5.10 Implementation Phases
Phase 1: Core SGR (2-3 days)
Goals: 8/16 colors and reset. Tasks:
- Implement params 0, 1, 22, 30-37, 40-47.
- Add per-cell attr storage.
Checkpoint:
ls --colorlooks correct.
Phase 2: Extended colors (2-3 days)
Goals: 256-color mapping. Tasks:
- Implement
38;5;nand48;5;n. - Add palette mapping table. Checkpoint: Color grid matches expected values.
Phase 3: Truecolor (2-3 days)
Goals: 24-bit color support. Tasks:
- Implement
38;2;r;g;band48;2;r;g;b. - Extend renderer. Checkpoint: Truecolor sample renders correctly.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Color storage | Index vs RGB | RGB for truecolor | Avoid conversion at render |
| Reset handling | Global vs per-field | Per-field | Matches SGR spec |
| Rendering | Inline ANSI vs UI lib | ANSI output | Simplicity |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | SGR parsing | 31, 38;5;196 |
| Integration Tests | Render logs | ansi-colors.log |
| Edge Case Tests | Invalid index | 38;5;999 |
6.2 Critical Test Cases
- Reset semantics:
ESC [ 0 mclears all attributes. - 256-color mapping: index 196 equals RGB(255,0,0).
- Truecolor:
38;2;12;34;56stored as RGB.
6.3 Test Data
Input: ESC [ 31 m red ESC [ 0 m
Expected: cells colored red, then reset to default
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Missing resets | Color leaks across output | Implement 0/39/49/22/24/27 |
| Wrong palette | Colors look off | Use standard xterm palette |
| Ignoring params order | Wrong styles | Apply params sequentially |
7.2 Debugging Strategies
- Add a debug view that prints current attrs per cell.
- Compare output to xterm screenshots for known sequences.
7.3 Performance Traps
Rendering each cell with separate ANSI codes can be slow; batch runs where possible.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add underline and italic rendering.
- Add a color palette preview mode.
8.2 Intermediate Extensions
- Implement faint and bright intensity mapping.
- Add theme switching.
8.3 Advanced Extensions
- Implement palette remapping like xterm.
- Add color profile support.
9. Real-World Connections
9.1 Industry Applications
- Colored logs and dashboards
- Terminal UI libraries (ncurses, tui-rs)
9.2 Related Open Source Projects
- xterm: reference SGR behavior
- vte: GNOME terminal engine
9.3 Interview Relevance
- State machines and attribute handling
- Data modeling and rendering trade-offs
10. Resources
10.1 Essential Reading
- xterm SGR documentation
- ECMA-48 control sequence reference
10.2 Video Resources
- Talks on terminal rendering internals
10.3 Tools & Documentation
vttestSGR tests
10.4 Related Projects in This Series
11. Self-Assessment Checklist
11.1 Understanding
- I can decode common SGR sequences.
- I know how to map 256-color indices.
- I understand attribute resets.
11.2 Implementation
- Colors render correctly for 8/16/256/truecolor.
- Attributes do not leak across resets.
- Tests pass on known logs.
11.3 Growth
- I can explain SGR to someone else.
- I can integrate this into a full terminal.
12. Submission / Completion Criteria
Minimum Viable Completion:
- 8/16 color SGR working with resets.
- Per-cell attribute storage in screen model.
Full Completion:
- 256-color and truecolor support.
- Deterministic tests against sample logs.
Excellence (Going Above & Beyond):
- Palette remapping and theme support.
- Optimized rendering of color runs.