Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 11 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**A desktop workbench for the Pi coding agent.**
**Workspace-first, not chat-first.**

[Quick Start](#quick-start) · [Architecture](docs/pidex-architecture.md) · [Design tokens](#design-tokens) · [Docs](docs/pidex-project-brief.md)
[Quick Start](#quick-start) · [Architecture](docs/pidex-architecture.md)

[![CI](https://github.com/echohello-dev/pidex/actions/workflows/ci.yml/badge.svg)](https://github.com/echohello-dev/pidex/actions/workflows/ci.yml)
[![Electron 41](https://img.shields.io/badge/electron-41-blue)](https://www.electronjs.org/)
Expand Down Expand Up @@ -67,53 +67,16 @@ Pi runs out-of-process over RPC so the UI survives a runtime crash. Pretext hand
| Package manager | Bun |
| Toolchain | mise, all commands via `mise run` |

## Design tokens

The workbench inherits its palette from the [Pi coding agent](https://pi.dev) so it feels like part of the same family as the runtime. Source of truth: [`scripts/_design.py`](scripts/_design.py), hexes pulled from `pi.dev/style.css`.

| Token | Hex | Role |
|---|---|---|
| bg-deep | `#0d1116` | Window background |
| bg-canvas | `#161d27` | Canvas surface |
| panel | `#212730` | Sidebar / panel |
| panel-soft | `#252f3d` | Card surface |
| parchment | `#dacbc2` | Named cream, primary mark and text |
| moonstone | `#ebe7e4` | Lighter cream |
| driftwood | `#5c5752` | Warm mid-gray |
| terracotta | `#844f3b` | Warm bronze accent |
| terracotta-light | `#b86b52` | Status dots |
| sunkissed | `#e1b06e` | Warm gold |
| sage | `#a3a473` | Sage accent |
| accent-blue | `#6a9fcc` | Links, focus |

Regenerate assets after a token edit:

```bash
uv run --with fonttools python scripts/build-icon.py # assets/icon, mark
uv run --with fonttools python scripts/build-logo.py # assets/logo (composable π + pidex)
uv run --with fonttools python scripts/build-banner.py # docs/assets/banner-bg
```

`docs/assets/screenshot.png` is captured from the running app. To refresh: temporarily gate a `webContents.capturePage()` call behind an env var, run `PIDEX_CAPTURE=1 mise run dev`, then revert. The banner composites the screenshot over the design-system background (`docs/assets/banner-bg.png`, built by `scripts/build-banner.py`) with the composable logo watermark, all via `magick`.

The banner background is procedural: `bg-canvas` field with the blueprint grid (96px minor + 480px major, lifted in opacity for hero scale) and a vertical gradient into `bg-deep`. Strictly rectilinear; no generated imagery.

Banner and logo use the design system font pairing: **Georgia** (serif, from `--serif` in `tokens.css`) for the wordmark and body, **Commit Mono** (from `--mono`) for the code surfaces visible in the screenshot.

## What's next

- [ ] Diff viewer with virtualised summaries
- [ ] Session fork, branch sessions visually
- [ ] Worktree switching from the UI

## Documentation
## License

| Doc | What it covers |
|---|---|
| [docs/pidex-project-brief.md](docs/pidex-project-brief.md) | Thesis, pain points, positioning, MVP slice |
| [docs/pidex-architecture.md](docs/pidex-architecture.md) | Process boundaries, IPC schema, SDK vs RPC |
| [docs/pidex-pretext-deep-dive.md](docs/pidex-pretext-deep-dive.md) | Why DOM-free text measurement |
[Apache-2.0](./LICENSE): see the file for full text. Personal project, shared in the open.

## License
## Star History

[MIT](./LICENSE) — see the file for full text. Personal project, shared in the open.
<a href="https://star-history.com/#echohello-dev/pidex&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=echohello-dev/pidex&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=echohello-dev/pidex&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=echohello-dev/pidex&type=Date" />
</picture>
</a>
Binary file modified assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/assets/banner-bg.png
Binary file not shown.
25 changes: 1 addition & 24 deletions docs/assets/banner-bg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 0 additions & 34 deletions docs/assets/framed-window.svg

This file was deleted.

Binary file removed docs/assets/screenshot.png
Binary file not shown.
191 changes: 126 additions & 65 deletions scripts/build-banner.py
Original file line number Diff line number Diff line change
@@ -1,85 +1,146 @@
"""Build the README banner background using the pidex design system.

The background mirrors the app's own chrome (tokens.css base.css body
background): bg-canvas field with the blueprint grid (96px minor +
480px major, blue-tinted) and a vertical gradient into bg-deep. Shapes
are strictly rectilinear; no organic imagery. The real app screenshot
and the composable logo are laid on top of this background.

Output:
docs/assets/banner-bg.png (1600x900)

Compose the final banner with:
magick docs/assets/banner-bg.png \\
docs/assets/screenshot.png -resize 1000x -geometry +300+80 -composite \\
assets/logo.png -resize 200x -geometry +70+70 -composite \\
docs/assets/banner.png
#!/usr/bin/env python
"""Build the README banner: the pidex logo (isometric cube with the
Pi coding agent logo on its top face + wordmark) centered on the
design-system background (bg-canvas with the blueprint grid + vertical
gradient into bg-deep).

Outputs:
docs/assets/banner.png (1600x600)
assets/logo.png (regenerated as part of the build)
docs/assets/banner-bg.svg (the background SVG, tracked)
"""

from __future__ import annotations

import shutil
import subprocess
import sys
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent
OUT = REPO / "docs" / "assets"

sys.path.insert(0, str(Path(__file__).resolve().parent))
from _design import BG_DEEP, BG_CANVAS # noqa: E402

W, H = 1600, 900


def svg() -> str:
# Blueprint grid matches tokens.css (96px minor + 480px major, blue
# tint). At hero scale the token opacities would be invisible, so
# they're lifted slightly while keeping the same hue.
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">
<defs>
<linearGradient id="vert" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="{BG_DEEP}" stop-opacity="0"/>
<stop offset="1" stop-color="{BG_DEEP}" stop-opacity="0.85"/>
</linearGradient>
<pattern id="grid_minor" width="96" height="96" patternUnits="userSpaceOnUse">
<path d="M 96 0 L 0 0 0 96" fill="none" stroke="#8aa9d4" stroke-opacity="0.18" stroke-width="1"/>
</pattern>
<pattern id="grid_major" width="480" height="480" patternUnits="userSpaceOnUse">
<path d="M 480 0 L 0 0 0 480" fill="none" stroke="#8aa9d4" stroke-opacity="0.32" stroke-width="1.75"/>
</pattern>
</defs>

<!-- base canvas -->
<rect x="0" y="0" width="{W}" height="{H}" fill="{BG_CANVAS}"/>

<!-- blueprint grid -->
<rect x="0" y="0" width="{W}" height="{H}" fill="url(#grid_minor)"/>
<rect x="0" y="0" width="{W}" height="{H}" fill="url(#grid_major)"/>

<!-- vertical gradient into bg-deep (anchors the bottom) -->
<rect x="0" y="0" width="{W}" height="{H}" fill="url(#vert)"/>
</svg>
"""
DOCS = REPO / "docs" / "assets"
ASSETS = REPO / "assets"

BANNER_W, BANNER_H = 1600, 600

INK_BRONZE = "#13110f"
PARCHMENT = "#dacbc2"


def pi_path(font_path):
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen

font = TTFont(font_path)
cmap = font.getBestCmap()
gname = cmap[ord("π")]
pen = SVGPathPen(font.getGlyphSet())
font.getGlyphSet()[gname].draw(pen)
glyf = font["glyf"][gname]
bbox = (glyf.xMin, glyf.yMin, glyf.xMax, glyf.yMax)
return pen.getCommands(), bbox, font["head"].unitsPerEm


def build_logo_svg(width, height):
d, bbox, _ = pi_path("/System/Library/Fonts/Supplemental/Georgia Italic.ttf")
gx = (bbox[0] + bbox[2]) / 2
gy = (bbox[1] + bbox[3]) / 2
e = 200
dx = e * 0.866
dy = e * 0.5
cx, cy = width / 2 - 60, height / 2
top = (cx, cy - dy)
right = (cx + dx, cy)
bot = (cx, cy + dy)
left = (cx - dx, cy)
a = dx / 470.0
b = dy / 470.0
# Scale the Pi logo to 65% of the top face so it fits with padding
pad_scale = 0.65
a *= pad_scale
b *= pad_scale
transform = f"matrix({a} {b} {-a} {b} {cx} {cy - dy})"
p_d = (
"M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z "
"M282.65 282.65 V400 H400 V282.65 Z"
)
i_dot_d = "M517.36 400 H634.72 V634.72 H517.36 Z"
word_x = right[0] + 60
word_y = cy + 20
word_size = int(height * 0.22)
parts = []
parts.append(
f'<polygon points="{top[0]},{top[1]} {right[0]},{right[1]} {bot[0]},{bot[1]} {left[0]},{left[1]}" fill="#161d27" stroke="{INK_BRONZE}" stroke-width="3" stroke-linejoin="round"/>'
)
parts.append(
f'<polygon points="{right[0]},{right[1]} {bot[0]},{bot[1]} {bot[0]},{bot[1] + e} {right[0]},{right[1] + e}" fill="#212730" stroke="{INK_BRONZE}" stroke-width="3" stroke-linejoin="round"/>'
)
parts.append(
f'<polygon points="{left[0]},{left[1]} {bot[0]},{bot[1]} {bot[0]},{bot[1] + e} {left[0]},{left[1] + e}" fill="#0d1116" stroke="{INK_BRONZE}" stroke-width="3" stroke-linejoin="round"/>'
)
parts.append(f'<g transform="{transform}">')
parts.append(f'<path fill="{PARCHMENT}" fill-rule="evenodd" d="{p_d}"/>')
parts.append(f'<path fill="{PARCHMENT}" d="{i_dot_d}"/>')
parts.append("</g>")
parts.append(
f'<text x="{word_x}" y="{word_y}" font-family="Georgia, serif" font-size="{word_size}" font-weight="500" fill="{PARCHMENT}" letter-spacing="-1.2">pidex</text>'
)
inner = "".join(parts)
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" width="{width}" height="{height}">{inner}</svg>'
)


def render_png(svg: Path, png: Path, width: int) -> None:
def build_bg_svg():
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {BANNER_W} {BANNER_H}" width="{BANNER_W}" height="{BANNER_H}">'
'<defs>'
'<linearGradient id="vert" x1="0" y1="0" x2="0" y2="1">'
'<stop offset="0" stop-color="#0d1116" stop-opacity="0"/>'
'<stop offset="1" stop-color="#0d1116" stop-opacity="0.85"/>'
'</linearGradient>'
'<pattern id="grid_minor" width="96" height="96" patternUnits="userSpaceOnUse">'
'<path d="M 96 0 L 0 0 0 96" fill="none" stroke="hsl(218 60% 80% / 0.10)" stroke-width="1"/>'
'</pattern>'
'<pattern id="grid_major" width="480" height="480" patternUnits="userSpaceOnUse">'
'<path d="M 480 0 L 0 0 0 480" fill="none" stroke="hsl(218 60% 80% / 0.22)" stroke-width="1.5"/>'
'</pattern>'
'</defs>'
f'<rect x="0" y="0" width="{BANNER_W}" height="{BANNER_H}" fill="#161d27"/>'
f'<rect x="0" y="0" width="{BANNER_W}" height="{BANNER_H}" fill="url(#grid_minor)"/>'
f'<rect x="0" y="0" width="{BANNER_W}" height="{BANNER_H}" fill="url(#grid_major)"/>'
f'<rect x="0" y="0" width="{BANNER_W}" height="{BANNER_H}" fill="url(#vert)"/>'
'</svg>'
)


def render(svg, out, width):
if not shutil.which("inkscape"):
sys.exit("inkscape not found on PATH; install via brew install inkscape")
raise SystemExit("inkscape not found on PATH")
tmp = out.with_suffix(".tmp.svg")
tmp.write_text(svg)
subprocess.run(
["inkscape", str(svg), "--export-type=png", "--export-filename", str(png), "--export-width", str(width)],
["inkscape", str(tmp), "--export-type=png", "--export-filename", str(out), "--export-width", str(width)],
check=True,
)
tmp.unlink()


def main():
bg_svg = build_bg_svg()
(DOCS / "banner-bg.svg").write_text(bg_svg)
bg_png = DOCS / "banner-bg.png"
render(bg_svg, bg_png, BANNER_W)

logo_svg = build_logo_svg(BANNER_W, BANNER_H)
logo_png = ASSETS / "logo.png"
render(logo_svg, logo_png, BANNER_W)

def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
svg_path = OUT / "banner-bg.svg"
png_path = OUT / "banner-bg.png"
svg_path.write_text(svg())
render_png(svg_path, png_path, W)
print(f"wrote {svg_path}")
print(f"wrote {png_path}")
subprocess.run(
["magick", str(bg_png), str(logo_png), "-composite", str(DOCS / "banner.png")],
check=True,
)
bg_png.unlink()
print(f"wrote {DOCS / 'banner.png'}")


if __name__ == "__main__":
Expand Down
Loading