Architecture

How Cowser is organized and how rendering works.

Table of Contents

Module layout

cowser/
├── __init__.py     # re-exports the stable surface + __version__
├── __main__.py     # python -m cowser entry point
├── api.py          # cowser.api: backwards-compatible public names
├── cli.py          # argparse CLI, config defaults, REPL
├── config.py       # reads ~/.config/neostore/cowser/config.toml
├── core.py         # render(), render_pair(), bubble & wrapping logic
├── cows.py         # Cow dataclass + built-in COWS registry (35 animals)
├── modes.py        # mode -> (eyes, tongue) presets
├── ansi.py         # colors, rainbow, bold, legacy-console sanitizing
├── fortunes.py     # built-in fortunes for --fortune
├── plugins.py      # art packs (.cow/.txt/.art loaders)
└── py.typed        # PEP 561 marker

Supporting files at the repo root: main.py (thin wrapper for python main.py ...), cowser.cmd / cowser.ps1 (Windows wrappers), scripts/build_binary.py (PyInstaller), and config.example.toml.

Data flow

flowchart LR
    A[cli.py] -->|name / message / stdin / fortune| B[core.py]
    B --> C[cows.py registry]
    B --> D[modes.py presets]
    B --> E[core build_speech_bubble]
    E --> F[ansi.py styling]
    F --> G[cli.py prints]
    H[plugins.py] -->|register art pack cows| C
    I[config.py] -->|defaults| A

The Cow registry

cowser/cows.py defines a frozen Cow dataclass with name, art, category, and tags, and a module-level COWS list. resolve_cow() in core.py picks a cow by:

  • None — a random cow,
  • a Cow instance,
  • a zero-based index into COWS,
  • an exact name, or
  • a unique name prefix.

search_cows() filters the registry by name, category, or tag; register_cow() appends new cows at runtime (used by the art-pack loader).

Token substitution

Art can contain two placeholders:

  • {eyes} — replaced by --eyes (default o o).
  • {tongue|default} — replaced by --tongue or a mode's preset tongue; with no tongue given, the token's built-in default is used so normal rendering is unaffected.

--mode maps to an (eyes, tongue) pair in modes.py and only fills in eyes/tongue that were not otherwise provided:

"dead": ("xx", "U "),
"borg": ("==", None),   # None leaves the tongue alone

Rendering pipeline

core.render() (and render_pair() for two animals):

  1. Resolve the cow(s).
  2. Optionally word-wrap the text (wrap_text, preserving paragraphs).
  3. Apply eyes/tongue/mode substitutions (_substituted_art).
  4. Build the speech or thought bubble (build_speech_bubble).
  5. Apply styling (_style: rainbow, color, bold).
  6. Return the finished string — no I/O; render() is a pure function.

The CLI adds two steps around it: it loads art packs first, and it sanitizes output for legacy consoles (ansi.needs_fallback / ansi.sanitize_legacy) before printing. On codepages that cannot encode the output, smart quotes, dashes, accents, and other symbols are mapped to plain ASCII.

Design notes

  • Pure core, thin CLI. All rendering logic is side-effect free so it can be embedded in other tools; only cli.py touches stdout, stdin, and files.
  • Stable surface. cowser.api.__all__ is the backwards-compatible public API; internal helpers are private (leading underscore) or undocumented.
  • Zero-dependency default. On Python 3.11+ there are no runtime dependencies; tomli is only required on 3.8–3.10.

← Back to README