Files
roro9stack/docs/milestones/G1.md
T
twislaandClaude Opus 5.5 7b8d9391a4 G1 step 3: the Gemini fetcher (TOFU, redirects, floors, pages via the card)
GeminiService fetches on a short-lived task and hands the App a
GeminiPage: header, the body as lines in 4 KB chunks (TextBuffer: no
large block, no doubling copies), the final URL after up to 5
redirects. Certificates are pinned on first use per host and port; a
change comes back as its own outcome with both fingerprints. No fetch
starts below 55 KB free (Q86).

With a card, the body streams to /gemini/cache/page.gmi in 1 KB pieces
while the connection is open, then loads into RAM once its memory is
back (Q87); StorageService::runAndWait (moved from the Debug Console)
keeps every card access on the storage task. Without a card: RAM, with
the steady and transient floors.

Measured with IRC connected: Cosmos (31.6 KB) went from 4.6 KB to the
whole page on the card and 20 KB on screen; lowest free heap 19.5 KB
in transfer, 43 KB once loaded. `gemini get` and `gemini trust` on the
console. 14 Gemini tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-05 00:58:01 +02:00

6.5 KiB
Raw Blame History

G1 — Gemini client

Goal: browse Geminispace from the Cardputer: fetch and read gemtext over TLS, follow links, answer input prompts, keep bookmarks, and save pages to the SD card to read later, offline. A side milestone between M2 and M3, tagged v0.5.0 when done.

Gemini (geminiprotocol.net): one request per TLS connection on port 1965, the request is the URL and CRLF, the response a <status> <meta> header line, then the body. Most capsules use self-signed certificates: trust on first use is the norm.

Decisions (design round 2026-10-05)

# Decision
Q70 A milestone of its own, G1, before M3: plan, tests first, measured on the device, tagged v0.5.0.
Q71 TOFU: the first certificate seen for a host is pinned (SHA-256, in NVS). If it changes, the page isn't shown; a dialog shows both fingerprints and asks whether to trust the new one. Self-signed or expired certificates are fine; only a change counts.
Q72 Responses: 1x input (11 hidden, for passwords), 2x content, up to 5 redirects (3x), 4x/5x errors with the server's message. 6x (client certificates): "not supported".
Q73 text/gemini is rendered, other text/* shown as plain text. Anything else can be saved to /gemini/downloads/, not shown.
Q74 Up to 64 KB on screen, larger pages truncated with a notice. Saving streams to the card, so a larger page is saved whole.
Q75 Gemtext rendering: text wrapped to the 40-column screen; #/##/### headings in bold and accent; * lists with bullets; > quotes indented and muted; preformatted blocks unwrapped, Left/Right to scroll; => links with their label, numbered.
Q76 Up/Down scroll; Tab and Shift+Tab move between links; Enter follows; Backspace goes back; g opens the address line. Links to other protocols show their URL and aren't followed.
Q77 Back history of 20 URLs in RAM, with scroll positions; going back refetches (or reopens a Saved Page). Bookmarks in /gemini/bookmarks.gmi (a gemtext page, shown on the start page); b adds the current page. Without a card, a built-in start page.
Q78 Start page: bookmarks, then Saved Pages, then defaults: geminiprotocol.net, a search engine (kennedy.gemi.dev), an aggregator (Cosmos; Antenna was down when measured).
Q79 UTF-8 decoded; characters outside the Latin-1 fonts shown as ?.
Q80 IRC and Gemini can run together: each fetch opens one connection, reads and closes it. If there isn't memory for a second TLS connection, the fetch fails with a clear message and IRC is untouched. Measured in step 1.
Q81 Debug aid: gemini get <url> prints the status, MIME type, size, certificate fingerprint and the first lines. URL resolution (RFC 3986), the response header and gemtext parsing are host-tested.
Q82 s saves the page on screen as a Saved Page: /gemini/saved/<host>/<path>.gmi, the gemtext as received plus a first line with its URL and save date. Saving again replaces it, and says so.
Q83 S saves the page and the pages it links to, one level deep: gemtext only, same host only, at most 30 pages, in the background with a progress Toast.
Q84 The start page lists Saved Pages, newest first, grouped by capsule; they open with no network. In a Saved Page, a link to another Saved Page opens the saved copy; other links fetch online if Wi-Fi is up, or say "not saved, offline". A Saved Page shows when it was saved; r refreshes it.
Q86 Decided after step 1. Two floors: free heap stays above 40 KB in steady state, and above 20 KB for the second or two of a TLS handshake (measured: 24 KB with IRC connected). A fetch refuses to start below 55 KB free ("not enough memory: stop IRC or retry"), so nothing pushes lower.
Q87 Decided in step 3. With a card, every page streams to /gemini/cache/page.gmi in 1 KB pieces while its TLS connection is open; once the connection closes and its ~45 KB is back, the page is loaded into RAM as far as the 40 KB floor allows. The whole page stays on the card (Saved Pages copy it). Without a card, the page goes straight to RAM under the same two floors. Pages are held as lines in 4 KB chunks, never one large block (the largest free block with IRC connected is about 31 KB).
Q85 Saved Pages are deleted from the App only (d, with confirmation), never by Storage Clean-up's age rules, like Notes.

Measured (step 1)

  • gemini://geminiprotocol.net/: 20 text/gemini, 1,184 bytes, TLS handshake 0.7–1.1 s, whole fetch 0.7–1.1 s; kennedy.gemi.dev 1.9 s. The fetch task's stack peaks at about 3.6 KB of 6.
  • Heap, Debug Build, IRC connected over TLS: about 68 KB free before a fetch. After the handshake the fetch holds about 32 KB (36–40 KB left); the handshake itself (certificate chain parsed with the 16 KB receive buffer allocated) dips to about 24 KB for a second or two. Nothing leaks: the heap after matches the heap before.
  • Step 3, Cosmos (31.6 KB) with IRC connected: first stopped at 4.6 KB (RAM only, the transfer's 20 KB floor). Streamed to the card: the whole page on the card, 20 KB of it loaded, lowest free heap 19.5 KB during the transfer and 43 KB once loaded. Without IRC: the whole page in RAM. Redirects (Cosmos 31), input (10), not found (51) and a changed certificate (refused, both fingerprints shown) all checked on the device.
  • Antenna (warmedal.se) doesn't answer, from the PC either; the default aggregator becomes Cosmos (gemini://skyjake.fi/~Cosmos/, which redirects to cosmos.skyjake.fi).

Done when

  • gemini get gemini://geminiprotocol.net/ prints the header, size and fingerprint on the console.
  • The Gemini App opens the start page, follows links (relative ones included), goes back, and follows redirects.
  • An input prompt (e.g. a search) takes a query and shows the results.
  • A changed certificate stops the page and asks.
  • s saves a page, S a page and its links; with Wi-Fi off, Saved Pages open and their saved links work.
  • Bookmarks are added with b and listed on the start page.
  • With IRC connected over TLS, a fetch still works, and the free heap stays above 40 KB.

Work breakdown

  1. Two TLS connections: measure the heap with IRC connected while a Gemini fetch runs.
  2. Parsers (host-tested): URL parsing and relative resolution, the response header, gemtext lines.
  3. Fetch on its own task, with TOFU and gemini get.
  4. Gemini App: rendering, scrolling, links, history, the address line.
  5. Input prompts, redirects, bookmarks, downloads.
  6. Saved Pages, then saving with linked pages.