Metadata-Version: 2.5
Name: glanceable
Version: 0.2.0
Summary: Layout and typography engine for round, glanceable near-eye displays
Project-URL: Homepage, https://github.com/DryadAI/glanceable
Project-URL: Documentation, https://github.com/DryadAI/glanceable#readme
Project-URL: Repository, https://github.com/DryadAI/glanceable
Project-URL: Issues, https://github.com/DryadAI/glanceable/issues
Project-URL: Changelog, https://github.com/DryadAI/glanceable/blob/main/CHANGELOG.md
License: MIT License
        
        Copyright (c) 2026 glanceable contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: ar-glasses,brilliant-labs,circular-display,epaper,frame,halo,heads-up-display,hud,layout-engine,markdown,microoled,near-eye-display,oled,round-display,smart-glasses,text-wrapping,typography,wearable
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Presentation
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Fonts
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pillow>=10.0
Provides-Extra: dev
Requires-Dist: brilliant-msg>=7.1.1; extra == 'dev'
Requires-Dist: halo-emulator; extra == 'dev'
Requires-Dist: lupa>=2.0; extra == 'dev'
Requires-Dist: markdown-it-py>=3.0; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Provides-Extra: markdown
Requires-Dist: markdown-it-py>=3.0; extra == 'markdown'
Description-Content-Type: text/markdown

# glanceable

A layout and typography engine for round, glanceable near-eye displays.

Developed against Brilliant Labs Halo's 256×256 circular microOLED. Nothing
above `surface.py` contains a Halo-specific call.

![stock vs glanceable](https://raw.githubusercontent.com/DryadAI/glanceable/main/docs/comparison.png)

## Why

The Brilliant SDK ships `TxTextSpriteBlock`, which rasterizes TTF text
host-side and sends it as sprites. The primitive is the right idea and is
under-specified.

**Fair comparison caveat:** stock's contract is that the *caller* pre-wraps and
passes `\n`. The middle panel above does exactly that, and it is perfectly
usable. The honest gap is narrower than "the SDK is broken" — measured on the
sample string at 13px, `glanceable` fits the same text in 4 lines instead of 5
and uses 16% more width (223px vs 193px), because the equator chord is 240px
while the largest inscribed square is only 169px.

Verified against `brilliant-msg` 7.0.0:

| | stock | glanceable |
|---|---|---|
| word wrap | none — caller must pre-wrap; `width` merely sizes the scratch buffer, and no metrics are exposed to wrap *with* | greedy wrap, hyphenates over-wide words |
| line origin | each line cropped to its own ink bbox → baselines drift | single face-derived ascent → baselines exactly `line_height` apart |
| coverage | hard threshold `>127`, 1-bit | N-level quantization (default 4 of Halo's 16 palette entries) |
| bad font path | silent `load_default()`, `font_size` ignored | raises `FontLoadError` |
| overflow | clipped | surfaced in `Layout.leftover` |
| display shape | unaware — `TxPlainText` documents x:1-640, y:1-400 (Frame's panel) | chord-aware |

Nothing in the published SDK knows the display is round. Zero occurrences of
circle, radius, or a 256 display bound.

## The circular claim

On a round panel usable line width is a function of vertical position. Every
text engine ever written wraps to a rectangle, so apps either waste the middle
third of the glass or clip against the curve.

A line box is a rectangle, so it is constrained by whichever of its two
horizontal edges sits farther from the equator — **not** by its midpoint. Using
the midpoint is the intuitive move and it clips descenders near the poles.
`test_line_width_uses_narrow_edge_not_midpoint` pins this.

![chord-aware wrap vs the largest inscribed rectangle](https://raw.githubusercontent.com/DryadAI/glanceable/main/docs/chord-vs-inscribed.png)

Left is `glanceable`. Right is the same text, same face, same size, wrapped to
the largest inscribed square — what a rectangular layout engine does when you
point it at a circle. Both are centred on their widest band. The right-hand
panel runs out of budget mid-sentence.

How much that is worth depends on the line budget, and the tighter the budget
the more it matters — a longer block puts more of its lines near the poles
where the chord is narrow:

| line budget | chord-aware | inscribed square | gain |
|---|---|---|---|
| 3 | 97 chars | 49 | +98% |
| 4 | 125 | 72 | +74% |
| 5 | 154 | 95 | +62% |
| 6 | 185 | 121 | +53% |

DejaVu Sans 13px on Halo's 256×256 with an 8px safe inset. Equator chord 240px;
largest inscribed square 169px. Regenerate with `python tools/comparison.py`,
re-measure with `python tools/measure_table.py`.

Identical to four decimal places on Python 3.10.20, 3.12.3 and 3.14.4 — the
interpreter is not a variable. What *would* move these is FreeType or the
DejaVu build, since the counts come out of font rasterization, and all three
runs shared Pillow 12.3.0 and FreeType 2.14.3. So `tests/test_circular_claim.py`
asserts the geometry exactly and the typography as floors, rather than letting
this table drift from the code the way earlier figures here did.

A glanceable display is read in a second or two, so a small fixed line budget
*is* the design constraint — the regime where this gap is widest.

## Install

```bash
pip install -e ".[dev]"
pytest -q                      # 273 passed  (268 + 1 skip without dev extras)
python examples/compare.py     # regenerates docs/comparison.png
```

Core depends only on Pillow. The markdown renderer needs a CommonMark parser,
so it lives behind an extra:

```bash
pip install "glanceable[markdown]"    # adds markdown-it-py
```

## Use

```python
from glanceable import (Font, PILSurface, find_system_font,
                        ramp_palette, render_text)

surface = PILSurface(256, 256, ramp_palette(4))
layout = render_text(surface, Font(find_system_font(), 13), "…", levels=4)

if layout.truncated:
    ...  # paginate; never silently cut off
surface.to_rgb().save("frame.png")
```

Swap `PILSurface` for `SpriteSurface` to emit `TxSprite`-shaped payloads.

## Markdown

Markdown assumes a wide rectangular viewport and rich typography. A 256px circle
at a legible size gives roughly five to seven usable lines, the first and last
chord-narrowed. So most markdown syntax has to *degrade*, not render.

```python
from glanceable import HALO, Font, find_system_font
from glanceable.markdown import layout_markdown

page = layout_markdown(source, HALO, Font(find_system_font(), 13), page=0)

for line in page.lines:
    ...                          # line.run, line.kind, line.font
page.metadata.wikilinks          # [[Target]] retained for a caller to act on
page.metadata.dropped            # images, embeds, opaque fences — named, not lost
page.leftover_source             # hand back in to continue; no hidden cursor
```

Pagination is a discrete index and a pure function of its inputs — no cursor, no
scroll, no auto-advance. Every piece of source ends up in exactly one of three
places: on the glass, in `leftover_source`, or named in `metadata`.

Degradation policy, in brief: headings get one emphasis tier; bold and italic
drop their markup and keep the text (`Font` has no style axis to collapse *to*);
tables render as `header: cell` records, which is lossless at any column count
and degenerates to plain `key: value` at two; fenced code is never reflowed but
is broken at the chord with a visible continuation marker, in a rectangular
sub-viewport so it keeps one left edge; frontmatter is stripped before the
parser sees it. Full table in [core concepts](docs/core-concepts.md#markdown-degradation).

`examples/obsidian_bridge.py` renders notes from a vault directory to PNGs and
prints everything that did not reach the glass. It exists to run the renderer
against markdown written as notes rather than as test fixtures — it is evidence,
not a product, and nothing in `glanceable/` imports it:

```bash
python examples/obsidian_bridge.py --query chord
```

Retrieval is deliberately not implemented: `--query` is a fifteen-line substring
match, there to be deleted and replaced by an Obsidian MCP server or the Local
REST API plugin. `examples/vault/` is a committed corpus of deliberately ugly
notes — nested callouts, Dataview blocks, ragged tables, a 200-character token,
CJK, emoji, CRLF — and `tests/test_vault_corpus.py` asserts the three-way
guarantee over every one of them.

## Docs

- [Getting started](docs/getting-started.md)
- [Core concepts](docs/core-concepts.md) — display model, legibility rules
- [API reference](docs/api-reference.md)
- [Troubleshooting](docs/troubleshooting.md)
- [CLAUDE.md](CLAUDE.md) — constraints for contributors and AI assistants

## Status — v0.2, **never run on hardware**

Stated plainly because the point of this project is being the implementation
you can trust. 273 tests pass on Python 3.10.20, 3.12.3 and 3.14.4 with the
dev extras installed; 268 pass and the emulator round-trip skips without them. All of them are
host-side; none of them is a device.

- **The device-agnostic boundary is untested.** `PILSurface` and `SpriteSurface`
  were written by one author against one mental model, which is not independent
  evidence that the `Surface` ABC is the right abstraction. Getting a second,
  non-Brilliant backend running is [roadmap item 1](ROADMAP.md#prove-the-bet)
  for exactly that reason — it is the claim this project most needs to test.
- `SpriteSurface` emits against published `brilliant_msg` 7.0.0 shapes but has
  **not** been round-tripped on a physical Halo. The wire format is unconfirmed.
- No device-side Lua counterpart yet: port the dirty-flag tree from
  CitizenOneX's `base_layout.lua` and validate against the emulator's PIL
  framebuffer.
- **CJK renders as tofu** with the default face. DejaVu Sans has no CJK glyphs;
  every missing character is reported in `metadata.unrenderable`, but you need a
  CJK-capable face to read it.
- `blit_coverage` writes palette indices rather than alpha-blending, so it is
  correct only over a background matching `palette_base`. On `SpriteSurface` a
  non-zero `palette_base` raises — the sprite wire format cannot carry it.
- Latin only. No BiDi, no shaping. CJK is *conserved* by the markdown renderer
  but broken on the wrong boundaries — there is no dictionary breaking.
- No glyph atlas caching; every run rasterizes fresh.
- `markdown.py` has no widow control: a heading or a single list item can strand
  alone at a page boundary.

## Deliberately omitted: animation

Peripheral motion detection is pre-attentional — anything that moves out there
hijacks attention involuntarily, whether or not it matters. Animation in a
glanceable-display toolkit encourages the exact failure mode the toolkit exists
to prevent. This is a design decision, not a missing feature.

## License

MIT. Layout-tree lineage credited to CitizenOneX on adoption.
