# G1 — Gemini client **Status:** steps 1–6 built and checked on the device (branch `g1`); one item left for a check by hand: reading Saved Pages with Wi-Fi off. **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.* **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. - **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. - 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.