GLOWTest Tagline
Architecture

Architecture

Technical architecture for contributors


Overview

This page describes the internal architecture of Glow for contributors who want to understand how the codebase is structured, extend functionality, or fix bugs. It traces the path from user input — a file path, URL, or standard input — through rendering and display, explaining why each layer exists and how the pieces fit together. Understanding this architecture will help you locate the right component when making changes and anticipate how a modification in one layer affects the rest of the system.


Architecture diagram
Loading diagram...

Components

Input Resolver Type: Routing / Dispatch layer The entry point after flag parsing. It inspects the arguments you provide — or their absence — and decides which reader to invoke. This keeps the rest of the pipeline agnostic about where bytes come from.


File System Reader Type: I/O adapter Reads one or more local Markdown files from disk, including glob expansion (e.g., *.md). It normalises line endings and encodes the content as a UTF-8 string for downstream consumers.


HTTP Fetcher Type: I/O adapter Fetches Markdown from a remote URL when you pass a https:// argument. It validates the response content-type where possible and hands raw Markdown bytes to the aggregator, making remote README rendering feel identical to local rendering.


Stdin Reader Type: I/O adapter Detects when input is piped (non-interactive TTY) and reads from standard input. This enables Glow to participate in shell pipelines, e.g., curl … | glow -.


File Browser (Bubbles / Bubble Tea) Type: Interactive TUI component A full-screen terminal UI activated when you run glow with no file argument in an interactive session. Built on the Bubble Tea Elm-architecture framework, it lets you navigate the local filesystem, search Markdown files, and select one for rendering — all from the keyboard.


Source Aggregator Type: Internal bus Collects one or more Markdown strings from whichever reader(s) were activated and presents a uniform stream to the renderer. For multi-file invocations it concatenates sources with clear separators.


Glamour Renderer Type: Core transformation engine The heart of Glow. Glamour parses Markdown (via Goldmark) and converts it to ANSI-escaped terminal output. It consumes the merged configuration to apply your chosen style (dark, light, notty, or a custom JSON theme), word-wrap width, and other rendering options.


Output Router Type: Display dispatch Decides whether rendered output goes straight to stdout or is piped into a pager. It checks your --pager flag and the PAGER environment variable, then either streams directly or forks the pager process.


Pager Type: Output consumer By default uses the system pager (typically less) or Glow's built-in pager when you need to scroll long documents without leaving the terminal. Keyboard shortcuts (q to quit, arrow keys or j/k to scroll) are provided by the pager layer.


Config Layer (Config File + CLI Flags + Env Vars) Type: Configuration aggregator Merges settings from three sources in priority order: CLI flags override environment variables, which override the persistent config file (~/.config/glow/glow.yml on most systems). This layered approach lets you set sane defaults globally while overriding them per-invocation without editing files.


Lip Gloss Type: Styling library Provides the ANSI styling primitives used by both the TUI file browser and Glamour's theme system. Keeping styles in Lip Gloss lets the whole Charm ecosystem share a consistent visual language.


Data flow

The following trace follows the workflow of a developer running glow --style dark --width 100 README.md.

  1. Flag parsing — Cobra (the CLI framework) parses the command line. --style dark and --width 100 are captured as flag values; README.md is stored as a positional argument.

  2. Config merge — The Config Layer reads ~/.config/glow/glow.yml, applies any matching environment variables (e.g., GLAMOUR_STYLE), then overlays the explicit CLI flags. The result is a single Config struct passed throughout the rest of the run.

  3. Input resolution — The Input Resolver sees one positional argument (README.md) that is not a URL and not stdin, so it hands control to the File System Reader.

  4. File reading — The File System Reader opens README.md relative to the working directory, reads its bytes, and passes the UTF-8 string to the Source Aggregator.

  5. Aggregation — Because only one file was provided, the aggregator emits a single Markdown string with no concatenation overhead.

  6. Rendering — Glamour receives the Markdown string and the merged Config. It:

    • Parses Markdown into an AST via Goldmark.
    • Loads the dark style definition (a bundled JSON theme).
    • Walks the AST, applying ANSI escape sequences for headings, bold, code fences, links, and so on.
    • Applies hard word-wrap at column 100.
    • Returns a fully styled, ANSI-escaped string.
  7. Output routing — The Output Router checks whether --pager is set (or PAGER is in the environment). Assuming the default pager behaviour and a long document, it forks less -r and pipes the rendered string into it.

  8. Display — The user sees beautifully rendered Markdown in the terminal, scrollable with arrow keys or j/k, and exits with q.

TUI path variation: If the user runs plain glow in an interactive terminal, step 3 instead activates the File Browser. The Bubble Tea runtime takes over the TTY, rendering the file list via Lip Gloss. When the user presses Enter on a file, the selected path is injected back into the pipeline at step 4, and the rest of the flow is identical.


Design decisions

1. Elm-architecture TUI via Bubble Tea Glow's interactive file browser is built on the Bubble Tea framework, which enforces a strict Model → Update → View loop. This was chosen because it makes TUI state predictable and testable — the entire UI state is a plain struct, and every keypress is a pure function from old state to new state. The trade-off is that contributors must learn the Bubble Tea mental model; imperative TUI code is not accepted in this layer.


2. Glamour as the rendering engine (not a custom renderer) Rather than writing an AST-to-ANSI converter from scratch, Glow delegates all Markdown rendering to Glamour, a separate library in the Charm ecosystem. This decision keeps Glow's own codebase small and focused on UX concerns (input routing, paging, configuration) while Glamour independently evolves to support new Markdown features. The cost is that Glow's rendering fidelity is bounded by Glamour's capabilities.


3. JSON-based theme system Styles are expressed as JSON documents rather than Go code. This allows power users to supply a --style /path/to/theme.json without recompiling, and it makes theme authoring accessible without knowing Go. The design deliberately avoids a DSL — plain JSON keys map directly to Lip Gloss style fields, keeping the schema self-documenting.


4. Config layering (file < env < flag) The three-tier configuration merge (persistent file, environment variables, CLI flags) mirrors the convention established by tools like git and kubectl. It lets CI pipelines control behaviour via env vars without touching config files, while developers can override anything ad-hoc with flags. A single flat config mechanism would have been simpler to implement but would force users to choose between scripting convenience and persistent preferences.


5. Goldmark as the Markdown parser Glamour uses Goldmark under the hood, which is a CommonMark-compliant, extensible parser. CommonMark compliance was non-negotiable because Glow must faithfully render GitHub-flavoured README files, which are the primary use case. Goldmark's extension API also allows Glamour to add GFM extras (tables, strikethrough, task lists) without forking the parser.


Trade-offs

Known limitations

  • HTML passthrough is limited. Raw HTML blocks inside Markdown are stripped or escaped rather than rendered. Full HTML rendering would require a headless browser or an HTML-to-ANSI converter, which is out of scope for a terminal tool. If your Markdown relies heavily on inline HTML, the output will differ from a browser preview.

  • Images are not rendered. Image references (![alt](url)) display only the alt text or a placeholder. Terminal image protocols (Kitty, iTerm2 inline images) are not currently implemented; Glow does not attempt to detect the terminal's image capabilities.

  • Very wide tables. Tables wider than the configured word-wrap width are truncated or wrapped in ways that may reduce readability. There is no horizontal scrolling within a rendered block.

  • Single-threaded rendering. When you pass a glob that matches many files, they are rendered sequentially. For large batches this can feel slow compared to parallelised alternatives.


Alternatives not chosen

  • Pandoc as a backend. Pandoc can render Markdown to terminal output (pandoc -t plain), but it is a large external dependency with a process-spawn overhead per invocation. Bundling a pure-Go parser keeps Glow self-contained and fast.

  • A custom TUI library. Before Bubble Tea matured, writing raw ANSI escape manipulation was common in Go CLIs. That approach produces brittle, hard-to-test code; Bubble Tea's model-based design was chosen explicitly to avoid it.

  • termenv for theming. While termenv handles terminal colour capability detection, Lip Gloss (which wraps termenv) was chosen because it provides a higher-level style composition API that matches how designers think about visual components.


When to use a different approach

  • If you need to render Markdown in a web UI or PDF, use a dedicated tool (Pandoc, marked, etc.). Glow's output is ANSI text; it is not suitable for non-terminal targets.
  • If you need full HTML/CSS rendering fidelity (e.g., complex badge tables, embedded SVG), a browser-based previewer such as grip or a VS Code extension will serve you better.
  • If you are processing thousands of files in a pipeline and performance is critical, consider calling Glamour directly as a library rather than shelling out to Glow, which incurs process-startup and config-load overhead on every invocation.