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:

  1. Parse SGR sequences and map them to color attributes.
  2. Maintain per-cell attributes in a screen model.
  3. Render ANSI colors correctly for 8/16/256/truecolor modes.
  4. Handle attribute stacking and resets deterministically.
  5. 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)

  1. Parse CSI params ending with m.
  2. For each param, update current attribute state.
  3. When printing a character, copy current attrs into the cell.
  4. 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/49 resets.

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

  1. What does ESC [ 38;2;255;0;0 m mean?
  2. How do you reset only the background color?
  3. Why must attributes be stored per cell?

Check-Your-Understanding Answers

  1. Set foreground to truecolor RGB (255,0,0).
  2. Use ESC [ 49 m.
  3. Because attributes persist across text and must be preserved per character.

Real-World Applications

  • ls --color output
  • git diff colored output
  • TUIs like htop and vim

Where You’ll Apply It

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

  1. Render 16 colors and verify against a known palette.
  2. Implement 256-color mapping and test index 196 (bright red).
  3. Implement truecolor and compare with RGB values.

Solutions to the Homework/Exercises

  1. Use a table of ANSI base colors and print a grid.
  2. Map index 196 to (255,0,0) in the 6x6x6 cube.
  3. 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)

  1. Initialize default attributes.
  2. Parse SGR params and update current attrs.
  3. On text output, copy attrs into each cell.
  4. 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

  1. What is the difference between 0 and 39?
  2. How would you implement inverse mode?
  3. Why store attributes in each cell?

Check-Your-Understanding Answers

  1. 0 resets everything; 39 resets only foreground color.
  2. Swap fg and bg at render time or update attrs on SGR 7.
  3. 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

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

  1. Implement SGR 7 and verify inverse output.
  2. Create a log that alternates colors and ensure no leakage.
  3. Add tests for 0, 39, and 49 resets.

Solutions to the Homework/Exercises

  1. Swap foreground and background at render time.
  2. Store attrs per cell and reset between segments.
  3. 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

  1. SGR parsing: interpret parameter lists in order.
  2. Attribute state: update current attrs and apply to new text.
  3. Per-cell storage: store fg/bg/bold/underline in each cell.
  4. Palette mapping: 256-color index to RGB.
  5. Truecolor: support RGB values directly.
  6. 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 m should 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)

  1. Render samples/ansi-colors.log.
  2. 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

  1. Iterate params in order.
  2. Update current attribute state.
  3. 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

  1. SGR parameter semantics and defaults.
  2. Attribute state and resets.
  3. 256-color and truecolor mapping.

5.5 Questions to Guide Your Design

  1. Will you store RGB or palette indices in cells?
  2. How will you handle unknown SGR params?
  3. 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

  1. How does truecolor differ from 256-color mode?
  2. Why are resets important for terminal correctness?
  3. 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:

  1. Implement params 0, 1, 22, 30-37, 40-47.
  2. Add per-cell attr storage. Checkpoint: ls --color looks correct.

Phase 2: Extended colors (2-3 days)

Goals: 256-color mapping. Tasks:

  1. Implement 38;5;n and 48;5;n.
  2. Add palette mapping table. Checkpoint: Color grid matches expected values.

Phase 3: Truecolor (2-3 days)

Goals: 24-bit color support. Tasks:

  1. Implement 38;2;r;g;b and 48;2;r;g;b.
  2. 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

  1. Reset semantics: ESC [ 0 m clears all attributes.
  2. 256-color mapping: index 196 equals RGB(255,0,0).
  3. Truecolor: 38;2;12;34;56 stored 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)
  • 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

  • vttest SGR tests

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.