Skip to main content

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

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

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

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:

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

Use

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.

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.

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:

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

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 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.

Metadata

Release files for glanceable 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for glanceable 0.2.0
File Size Uploaded Provenance Actions
glanceable-0.2.0.tar.gz 137.2 kB Without provenance Details for glanceable-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for glanceable 0.2.0
File Interpreter ABI Platform Actions
glanceable-0.2.0-py3-none-any.whl Python 3 none any Details for glanceable-0.2.0-py3-none-any.whl

Total release size: 180.1 kB

Release files / glanceable-0.2.0.tar.gz

Download URL glanceable-0.2.0.tar.gz
Size 137.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f6e96c4abe5cf586e5f976e0b432b5289577abb7d35a3b3f288b0319423007d3
BLAKE2b-256 checksum
How to use checksums
7ab13a7728225891d41be0cf8871e19ea8048ffb6f2e225e3f3afb443e9cd325
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Release files / glanceable-0.2.0-py3-none-any.whl

Download URL glanceable-0.2.0-py3-none-any.whl
Size 42.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
263d5dadf658e4e2c961fad25cec0728dabc0e7955f4bd06c0613677a5e2a283
BLAKE2b-256 checksum
How to use checksums
aeafe69833055652884e422492532a82c86b543a58e8e4bbe00001fbca8dfd5a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.13

Security for glanceable 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page