# G1 — Gemini client **Status:** done, tagged v0.5.0. Every step was checked on the device; reading Saved Pages with Wi-Fi off was checked by hand (2026-10-05). **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 ` ` 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 ` 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//.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, revised after Q88.* **Two floors:** free heap stays above 40 KB in steady state; a fetch refuses to start below **55 KB** free ("not enough memory: stop IRC or retry"). The firmware's own allocations during a fetch (a page in RAM, a window) keep 20 KB free. With IRC connected, the TLS connection itself can briefly take the heap lower, depending on the server's record sizes: measured 24, 19.5, 15.5 and **13 KB**. *Accepted:* about 12 KB for a moment during a fetch with IRC up, rather than refusing most fetches (a 70 KB start floor) or dropping IRC's connection for each page. | | 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). | | Q88 | *Added after step 6.* **A page bigger than memory allows is read from the card as you scroll.** Opening it, one pass over its file counts the lines, records where every 64th starts (and whether it's inside a preformatted block), and loads the first window. Scrolling near either end of the window reads the next or previous one in the background, keeping the line on top of the screen where it is; the scrollbar follows the whole page. Display pages alternate between two cache files, so the one on screen is never overwritten by the next fetch; background jobs use a third. | | 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. - **Steps 4–6 on the device:** Project Gemini and its relative links, Back with the scroll restored, a refused YouTube link; `b` bookmarks, `s` saves (and says when it replaced an older copy), `S` saved 6 of 6 pages, the start page lists both; a Saved Page opens from the card with its origin, its saved links open saved copies, `r` refreshes, `d` asks first; Kennedy's input prompt sent "cardputer" and got 87 results; emoji drawn as `?`. - **A bug found there:** refreshing first loaded the whole Saved Page into RAM just to read its origin, next to the App's copy and a TLS connection: the heap fell to 436 bytes. Now only the first line is read, and every fetch (pages, saves, refreshes) checks the 55 KB start floor. Lowest since boot afterwards: 53.8 KB. - **Windowed pages (Q88), Cosmos with IRC connected:** 226 of 419 lines in memory at first; paging down loaded lines 192–419 in one window, scrolling back up loaded 64 onwards, then 0 onwards. Window budgets count the memory the old window gives back. - **Heap during a fetch with IRC connected:** lowest 13–15.5 KB in later runs (24 and 19.5 KB earlier), the TLS receive buffers varying with the server's records. Accepted (Q86, revised). - 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.