This is a project to help engineers, researchers, project managers, and others create beautiful, retro ASCII flow diagrams. ASCII diagrams are pretty, and harken back to the mid-20th century technical documentation. They also have real advantages:
- ASCII diagrams optimize for thinking speed, not presentation quality. It encourages iteration and deletion instead of premature refinement
- ASCII diagrams can live inline with: PRs, Markdown files, Slack threads, etc
- Minimalist diagrams reduce visual noise (although they do still look retro and pretty)
- They're tool agnostic and can be rendered anywhere
- They work wonderfully in the age of agentic AI, which can easily read and parse these small diagram representations
You are a kind, immensely-intelligent engineer that has a love for ASCII-minimalism and the intersection of engineering and art and psychology. You enjoy adding a reasonable amount (not too much, but here and there it's okay) of ASCII art here and there in the project artifacts. You are also have a powerful mastery of the python language, and a vast knowledge of all of the currently popular tools and frameworks that are used.
When unsure about implementation details, ALWAYS ask the developer.
As much as possible, just pure python should be used in implementation. We want to the implementation light, fast, and not dependent on a bunch of packages.
At the same time, we optimize for maintainability over cleverness. When in doubt, choose the boring, well-tested solution that future developers can easily understand and modify.
Also use the python uv tool and its associated virtual environment for running tests and project code.
Every source code addition must be accompanied by:
* Either updating or adding new unit and integration tests in the tests/ folder. Ensure that test coverage is > 90% at any time. You must ensure that test coverage is above 90% after you make any changes.
* Linting and formatting with the ruff package. You must lint with each change.
* Updating both the README.md and CLAUDE.md file. If the source code update is a tiny fix, there is no need to update these documents. Generally, update the documents if a user-facing change has been made.
This project follows semantic versioning, with the current version being located in the pyproject.toml file and the git tag. Always ask the developer if your suggested version update is correct.
Never modify anything outside of this project folder without asking the developer for explicit permission.
This project uses GitHub Actions + PyPI Trusted Publishing for automated releases. No API tokens are needed.
-
CI/CD Workflows (in
.github/workflows/):test.yml: Runs linting and tests on every push/PR to mainpublish.yml: Builds and publishes to PyPI when a version tag is pushed
-
Coverage Requirements:
- 90% test coverage is enforced before publishing
- Codecov integration tracks coverage over time
- Configuration in
codecov.yml
Again, always confirm with the developer any updates or changes you make related to versioning.
# 1. Update version in pyproject.toml
# 2. Commit the change
git add pyproject.toml
git commit -m "Bump version to X.Y.Z"
# 3. Create and push a version tag
git tag vX.Y.Z
git push origin main
git push origin vX.Y.ZThe publish.yml workflow will automatically:
- Run tests with 90% coverage requirement
- Build the package with
uv build - Publish to PyPI via OIDC (Trusted Publishing)
- PyPI pending publisher configured for
ronikobrosly/retroflow - GitHub environment
pypicreated with OIDC permissions - Codecov integration activated at https://app.codecov.io/gh
- Simple syntax: Define flowcharts using intuitive
A -> Barrow notation - ASCII output: Generate text-based flowcharts for terminals and documentation
- PNG export: Save high-resolution PNG images with customizable fonts
- Intelligent layout: Automatic node positioning using NetworkX with barycenter heuristic
- Smart edge routing: Edges automatically route around intermediate boxes to avoid visual overlap
- Cycle detection: Handles cyclic graphs gracefully with back-edge routing
- Customizable: Adjust text width, box sizes, spacing, shadows, and fonts
- Unicode box-drawing: Beautiful boxes with optional shadow effects
- Title banners: Optional double-line bordered titles with automatic word wrapping (at ~15 chars)
- Horizontal flow: Left-to-right layout mode (
direction="LR") for compact diagrams - Group boxes: Visually cluster related nodes within dashed-border containers with titles
retroflow/
├── .github/workflows/
│ ├── publish.yml # PyPI release workflow (triggers on version tags)
│ └── test.yml # CI workflow (lint + test matrix)
├── src/retroflow/
│ ├── __init__.py # Public API exports
│ ├── generator.py # FlowchartGenerator class (main entry point)
│ ├── parser.py # Text input parser (A -> B syntax)
│ ├── layout.py # NetworkX-based layout with barycenter ordering
│ ├── renderer.py # ASCII canvas, box drawing, and line rendering
│ ├── router.py # Edge routing utilities (ports, waypoints)
│ ├── models.py # Data models (LayerBoundary, ColumnBoundary)
│ ├── positioning.py # Position calculation for nodes
│ ├── edge_drawing.py # Edge rendering for TB and LR modes
│ ├── export.py # PNG and text file export functionality
│ ├── tracer.py # Debug tracing infrastructure (RenderTrace, etc.)
│ ├── debug.py # Debug utilities (TracedCanvas, visual_diff, etc.)
│ └── py.typed # PEP 561 type marker
├── tests/
│ ├── conftest.py # Shared pytest fixtures
│ └── ... # Test modules
├── codecov.yml # Coverage threshold config (90%)
├── pyproject.toml # Package metadata and dependencies
├── README.md # User documentation
└── CLAUDE.md # Developer/agent guidance (this current file)
| File | Purpose |
|---|---|
generator.py |
Main FlowchartGenerator class - orchestrates parsing, layout, positioning, edge drawing, group rendering, and export |
parser.py |
Parses A -> B text syntax into connection tuples; also parses group definitions ([GROUP: nodes]) |
layout.py |
NetworkXLayout class using networkx for graph representation, cycle detection, topological sorting, and barycenter-based node ordering. SugiyamaLayout is an alias for backwards compatibility. |
renderer.py |
Canvas for 2D character grid, BoxRenderer for Unicode box drawing with shadows, GroupBoxRenderer for dashed group boxes, LineRenderer for edge drawing utilities |
router.py |
EdgeRouter for port allocation and orthogonal edge routing (utility module for future use) |
models.py |
Data models for layout boundaries (LayerBoundary, ColumnBoundary) and group definitions (GroupDefinition, GroupBoundary) |
positioning.py |
PositionCalculator class for calculating node positions, layer/column boundaries, port positions, and group-aware positioning |
edge_drawing.py |
EdgeDrawer class for rendering forward and back edges in TB and LR modes |
export.py |
FlowchartExporter class for PNG and text file export with font handling |
tracer.py |
Debug tracing infrastructure - RenderTrace, PipelineStage, CharacterPlacement for capturing rendering decisions |
debug.py |
Debug utilities - TracedCanvas wrapper, visual_diff, CanvasInspector for debugging and analysis |
The codebase includes a comprehensive debug tracing system designed to make it easier for Claude Code (and developers) to understand and debug the rendering pipeline. This system captures detailed information about every step of flowchart generation.
The flowchart rendering process involves:
- Multiple pipeline stages (parse → layout → positions → edges)
- Complex character merging logic (lines intersecting become tees, crosses, etc.)
- Coordinate transformations at each stage
- Multiple methods that can place/overwrite the same canvas position
Without visibility into these intermediate states, debugging rendering issues is extremely difficult. The debug tracing system solves this by capturing:
- Pipeline stages with snapshots of data at each step
- Every character placement with coordinates, previous character, and reason
from retroflow import FlowchartGenerator
# Enable debug mode
generator = FlowchartGenerator()
result = generator.generate("A -> B\nB -> C", debug=True)
# Get the trace
trace = generator.get_trace()
# Print summary
print(trace.summary())
# Dump full trace to file for analysis
trace.dump_to_file("debug_trace.txt")
# Show canvas evolution through stages
print(trace.dump_canvas_evolution())The RenderTrace object provides these key methods:
# Get summary statistics
trace.summary()
# Get full dump
trace.dump()
# Get canvas at specific stage
canvas_lines = trace.get_canvas_at_stage("boxes_drawn")
# Get all placements at a coordinate (useful for debugging overwrites)
placements = trace.get_placements_at(x=10, y=5)
# Find all character upgrades (where existing char was modified)
upgrades = trace.get_character_upgrades()
# Filter placements by source method
edge_placements = trace.get_placements_by_source("EdgeDrawer")
# Filter placements by reason
corners = trace.get_placements_by_reason("corner")The trace captures these stages:
| Stage | Description |
|---|---|
parse |
Connections extracted from input text |
layout |
Layer assignments, node positions, back edges identified |
dimensions |
Box dimensions (width, height) calculated for each node |
positions |
Canvas coordinates calculated for each box |
canvas_created |
Initial empty canvas created |
boxes_drawn |
All node boxes rendered |
forward_edges_drawn |
Forward edges rendered (normal flow) |
back_edges_drawn |
Back edges rendered (cycles) |
Each character placement includes a reason string. Common reasons:
| Reason | Meaning |
|---|---|
vertical_line |
Drawing vertical edge segment |
horizontal_line |
Drawing horizontal edge segment |
corner_top_left |
Placing ┌ corner |
corner_bottom_right |
Placing ┘ corner |
upgrade_left_corner_to_tee_right |
┌ or └ + vertical = ├ |
upgrade_horizontal_to_tee_down |
─ + corner = ┬ |
vertical_crosses_horizontal |
Creating ┼ intersection |
merge_corners_to_tee_* |
Two corners combining |
arrow_down |
Placing ▼ arrow |
source_exit_port |
Marking box exit point with ┬ |
fanout_junction |
Fan-out junction point |
The TracedCanvas wraps a regular Canvas and intercepts all character placements:
from retroflow.renderer import Canvas
from retroflow.tracer import RenderTrace
from retroflow.debug import TracedCanvas
canvas = Canvas(80, 40)
trace = RenderTrace()
traced = TracedCanvas(canvas, trace)
# Set the current source context
traced.set_source("MyModule.my_method")
# All set() calls are now recorded
traced.set(10, 5, "│", reason="my_vertical_line")
# Check what was recorded
print(trace.character_placements[-1])For comparing expected vs actual output:
from retroflow.debug import visual_diff
expected = "┌───┐\n│ A │\n└───┘"
actual = "┌───┐\n│ B │\n└───┘"
print(visual_diff(expected, actual))
# Shows exactly where characters differFor analyzing canvas contents:
from retroflow.debug import CanvasInspector
inspector = CanvasInspector(canvas)
# Find all positions of a character
corners = inspector.find_char("┌")
# Count line-drawing characters
counts = inspector.get_line_chars_count()
# Extract a region
region = inspector.get_region(x=5, y=3, width=10, height=5)- Start with trace.summary() - Get an overview of what happened
- Use get_placements_at() - When a specific position looks wrong, see all placements there
- Use get_character_upgrades() - To find all merge/upgrade operations
- Use dump_canvas_evolution() - To see how the canvas built up stage by stage
- Filter by source - To isolate placements from a specific method
- Write targeted tests - Use traces to verify specific rendering decisions
When adding new rendering code, include reason parameters:
# Good - with reason
canvas.set(x, y, LINE_CHARS["vertical"], "my_new_vertical_segment")
# Also acceptable - TracedCanvas will infer a basic reason
canvas.set(x, y, LINE_CHARS["vertical"])Group boxes allow users to visually cluster related nodes together within a labeled container. This makes diagrams easier to read and understand, especially when depicting systems with distinct subsystems or logical groupings.
Groups are defined at the top of input text, before edge definitions:
[GROUP TITLE: node1 node2 node3]
[ANOTHER GROUP: nodeA nodeB]
node1 -> node2
node2 -> node3
nodeA -> nodeB
node3 -> nodeA
- Text before the colon is the group title (centered above the group box)
- Text after the colon is a space-separated list of node names
- Multi-word node names are supported (matched against nodes found in edges)
- Group definitions must appear before any edge definitions
from retroflow import FlowchartGenerator
generator = FlowchartGenerator()
result = generator.generate("""
[API Layer: Gateway Auth]
[Data Layer: Database Cache]
Gateway -> Auth
Auth -> Database
Database -> Cache
Gateway -> Cache
""")
print(result)Group boxes have:
- Dashed borders: Using
┄(horizontal) and┆(vertical) characters - Solid corners: Standard box-drawing corners (┌ ┐ └ ┘) for clarity
- Shadows: On right and bottom edges (same as node boxes, can be disabled)
- Centered title: Displayed above the group box
Example output:
API LAYER
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
┆ ┆░
┆ ┌───────────┐ ┆░
┆ │ Gateway │░ ┆░
┆ └───────────┘░ ┆░
┆ ░░░░░░░░░░░░ ┆░
┆ │ ┆░
┆ ▼ ┆░
┆ ┌───────────┐ ┆░
┆ │ Auth │░ ┆░
┆ └───────────┘░ ┆░
┆ ░░░░░░░░░░░░ ┆░
└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘░
░░░░░░░░░░░░░░░░░░░░░
| Flow Direction | Node Arrangement Within Groups |
|---|---|
| TB (Top-to-Bottom) | Nodes arranged horizontally (side-by-side) |
| LR (Left-to-Right) | Nodes arranged vertically (stacked) |
Nodes are arranged perpendicular to the flow direction within groups, making groups visually compact.
- A node can belong to at most one group (enforced during parsing)
- All group members must exist in at least one edge definition
- Group definitions must appear before edge definitions in the input
| Class | Location | Purpose |
|---|---|---|
GroupDefinition |
models.py |
Parsed group from input (name, members, order) |
GroupBoundary |
models.py |
Calculated boundaries for rendering (x, y, width, height, title position) |
ParseResult |
parser.py |
Combined result of parsing (connections + groups) |
| File | Group-Related Changes |
|---|---|
parser.py |
parse_with_groups() method, group syntax parsing, multi-word node matching |
positioning.py |
calculate_group_aware_positions(), calculate_group_boundaries(), resolve_group_overlaps() |
renderer.py |
GroupBoxRenderer class, DASHED_BOX_CHARS constants |
generator.py |
Group orchestration in generate(), _draw_groups() method |
models.py |
GroupDefinition, GroupBoundary dataclasses |