Gemini: read big pages from the card as you scroll (Q88)

A page larger than memory allows is now windowed instead of cut: one
pass over its file (cache or Saved Page) counts lines, indexes every
64th (offset, and the preformatted state there in bit 31: about 1 KB
for a 1 MB page) and loads the first window. Scrolling near either end
reads the next or previous window on the Gemini task; the line on top
of the screen stays put, the scrollbar follows the whole page, and Tab
at a window's edge pages on instead of wrapping. Window budgets count
the memory the old window gives back.

Display pages alternate between two cache files so the one on screen
is never overwritten by the next fetch; jobs use a third.

On the device, Cosmos with IRC connected: 226 of 419 lines at first,
then lines 192-419, back to 64 and 0 while scrolling. `key space` added
to the console's key command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-05 01:44:01 +02:00
co-authored by Claude Opus 5.5
parent fd306d5013
commit 9ca9a16de8
6 changed files with 259 additions and 51 deletions
+27 -8
View File
@@ -36,6 +36,14 @@ struct GeminiPage {
std::string savedFrom; // a Saved Page: the URL it was saved from (links resolve against it)
std::string savedOn; // and when, "2026-10-05 01:20"
uint32_t ms = 0;
// A page bigger than memory allows is read from its file as you scroll: `text` then holds
// lines [windowStart, windowStart + text.lineCount()) of totalLines. lineIndex has where every
// 64th line starts in the file (bit 31: inside a preformatted block there).
std::string backing; // the file the page is in (a cache file or a Saved Page)
bool windowed = false;
size_t totalLines = 0, windowStart = 0;
std::vector<uint32_t> lineIndex;
std::string windowOf; // a window result: the backing file it belongs to
// What relative links resolve against.
const std::string& base() const { return savedFrom.empty() ? url : savedFrom; }
@@ -54,7 +62,8 @@ class GeminiService {
static constexpr int kMaxRedirects = 5; // Q72
static constexpr size_t kMaxOnCard = 1024 * 1024; // a text page larger than this is cut
static constexpr int kMaxLinkedPages = 30; // Q83
static constexpr const char* kCachePath = "/gemini/cache/page.gmi";
static constexpr size_t kIndexEvery = 64; // lines between lineIndex entries
static constexpr size_t kMaxLineBytes = 8192; // longer lines are cut
static constexpr const char* kBookmarks = "/gemini/bookmarks.gmi";
static constexpr const char* kStartUrl = "about:start";
@@ -70,6 +79,8 @@ class GeminiService {
bool deleteSaved(const std::string& savedPath); // d on a Saved Page (Q85)
bool addBookmark(const std::string& url, const std::string& title); // b (Q77)
bool download(const std::string& url); // s on a non-text page (Q73)
// The window of a big page starting at `firstLine` (a multiple of kIndexEvery), from its file.
bool loadWindow(const GeminiPage& page, size_t firstLine);
bool busy() const { return busy_; }
bool takeResult(GeminiPage& out);
@@ -82,22 +93,27 @@ class GeminiService {
bool fetchToConsole(const std::string& url);
private:
enum class Job { Fetch, Save, SaveWithLinks, Refresh, Delete, Bookmark, Download };
enum class Job { Fetch, Save, SaveWithLinks, Refresh, Delete, Bookmark, Download, Window };
// Cache files: two take turns for pages on screen (the one shown is never overwritten by the
// next), one for jobs (saving linked pages, refreshing).
static constexpr int kWorkSlot = 2;
static const char* slotPath(int slot);
bool start(Job job);
static void taskEntry(void* self);
void run();
void runFetch(GeminiPage& page);
void fetchFollowing(const std::string& url, GeminiPage& page, bool loadIntoRam);
void fetchOne(const std::string& url, GeminiPage& page, bool loadIntoRam);
bool receiveToCard(::NetworkClientSecure& tls, const char* first, size_t len, GeminiPage& page);
void loadFromCard(GeminiPage& page, size_t freeBefore, const char* path);
void fetchFollowing(const std::string& url, GeminiPage& page, bool loadIntoRam, int slot);
void fetchOne(const std::string& url, GeminiPage& page, bool loadIntoRam, int slot);
bool receiveToCard(::NetworkClientSecure& tls, const char* first, size_t len, GeminiPage& page, int slot);
void loadFromCard(GeminiPage& page, size_t freeBefore, const std::string& path);
void readWindow(GeminiPage& page, size_t firstLine, size_t budget);
void openSaved(const std::string& path, GeminiPage& page);
std::string savedFirstLine(const std::string& path);
std::string savedFrom(const std::string& path);
void composeStart(GeminiPage& page);
std::string saveCached(const std::string& url); // "" or what went wrong
bool cacheHolds(const std::string& url) const { return cachedUrl_ == url; }
const char* cacheOf(const std::string& url) const; // the cache file holding url's body, or nullptr
void finishWithMessage(const std::string& text);
void notify(const std::string& text);
void report(const GeminiPage& page, size_t heapBefore, size_t heapLowest);
@@ -115,7 +131,10 @@ class GeminiService {
bool preferSaved_ = false;
GeminiPage result_;
std::string message_;
std::string cachedUrl_; // the URL whose body is in kCachePath
std::string slotUrl_[3]; // the URL whose body each cache file holds
int nextDisplay_ = 0;
size_t windowLine_ = 0, windowReleasing_ = 0;
GeminiPage windowPage_;
bool replaced_ = false; // the last save replaced an older copy
volatile bool busy_ = false, ready_ = false, messageReady_ = false, cancel_ = false, toConsole_ = false;
size_t lowest_ = 0;