Public Access
G1 steps 5-6: input prompts, bookmarks, downloads, Saved Pages
Everything touching the network or the card is a job on the Gemini task (a missing card can block 5 s, past the main loop's watchdog): about:start is composed from /gemini/bookmarks.gmi and the Saved Pages (by capsule, newest first); file:// opens a Saved Page; s, S, r, d, b and downloads report one line to the URL bar, S with progress Toasts. A Saved Page is the cached body with a first line "> Saved from <url> on <date>": it shows as a quote and gives relative links their base; inside one, links to other Saved Pages open the saved copy, others go online or say "Not saved, and offline". Input prompts (11 masked) request the same URL with the answer as its query. Fixed on the way: re-wrapping indented lines rebuilt the text from its rows, inserting a space where a long word had been cut; refreshing loaded the whole Saved Page next to a TLS connection (heap down to 436 bytes), now it reads one line and every fetch checks the 55 KB floor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -4,9 +4,12 @@
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "event_bus.h"
|
||||
#include "gemini_response.h"
|
||||
#include "key_value_store.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "text_buffer.h"
|
||||
|
||||
@@ -19,7 +22,7 @@ struct GeminiPage {
|
||||
enum class Outcome { Ok, Failed, CertificateChanged };
|
||||
Outcome outcome = Outcome::Failed;
|
||||
std::string requested; // what was asked for
|
||||
std::string url; // where it ended up, after redirects
|
||||
std::string url; // where it ended up, after redirects (file://… for a Saved Page)
|
||||
gemini::Header header; // the final response's header
|
||||
gemini::TextBuffer text; // 2x only: the body as lines, up to kMaxBody
|
||||
bool truncated = false; // the body was cut: see truncatedWhy
|
||||
@@ -30,12 +33,18 @@ struct GeminiPage {
|
||||
std::string pinned, presented;
|
||||
std::string cachePath; // with a card: the whole body as received (Saved Pages copy it)
|
||||
size_t receivedBytes = 0; // the whole body, which may be more than `text` holds
|
||||
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;
|
||||
|
||||
// What relative links resolve against.
|
||||
const std::string& base() const { return savedFrom.empty() ? url : savedFrom; }
|
||||
};
|
||||
|
||||
// Gemini (docs/milestones/G1.md). Each fetch runs on a short-lived task of its own: a TLS
|
||||
// handshake can block for seconds, which the main loop's watchdog wouldn't allow, and an idle
|
||||
// client should cost no stack. Certificates are trusted on first use, per host and port (Q71).
|
||||
// Gemini (docs/milestones/G1.md). Everything that touches the network or the SD card runs as one
|
||||
// job at a time on a short-lived task of its own: a TLS handshake or a missing card can block for
|
||||
// seconds, which the main loop's watchdog wouldn't allow, and an idle client should cost no stack.
|
||||
// Certificates are trusted on first use, per host and port (Q71).
|
||||
class GeminiService {
|
||||
public:
|
||||
static constexpr size_t kMaxBody = 64 * 1024; // Q74
|
||||
@@ -43,16 +52,28 @@ class GeminiService {
|
||||
static constexpr size_t kSteadyFloor = 40 * 1024; // Q86: what must be left once the page is in
|
||||
static constexpr size_t kTransientFloor = 20 * 1024; // Q86: while the TLS connection is open
|
||||
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 const char* kBookmarks = "/gemini/bookmarks.gmi";
|
||||
static constexpr const char* kStartUrl = "about:start";
|
||||
|
||||
GeminiService(KeyValueStore& store, StorageService& storage);
|
||||
GeminiService(KeyValueStore& store, StorageService& storage, ClockService& clock, EventBus& bus);
|
||||
|
||||
// Pages: gemini://…, about:start (bookmarks, Saved Pages, defaults) or file:///gemini/saved/….
|
||||
// `preferSaved`: open the Saved Page of that URL if there is one (inside a Saved Page, Q84).
|
||||
bool fetch(const std::string& url, bool preferSaved = false);
|
||||
// Actions; each reports one line through takeMessage().
|
||||
bool save(const std::string& url); // s (Q82)
|
||||
bool saveWithLinks(const std::string& url, std::vector<std::string> links); // S (Q83)
|
||||
bool refresh(const std::string& savedPath); // r on a Saved Page
|
||||
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)
|
||||
|
||||
// Starts a fetch; false if one is running. The result comes from takeResult().
|
||||
bool fetch(const std::string& url);
|
||||
bool busy() const { return busy_; }
|
||||
bool takeResult(GeminiPage& out);
|
||||
bool takeMessage(std::string& out);
|
||||
void cancel() { cancel_ = true; }
|
||||
// After the user accepts a changed certificate.
|
||||
void trust(const std::string& host, int port, const std::string& fingerprint);
|
||||
@@ -61,21 +82,42 @@ class GeminiService {
|
||||
bool fetchToConsole(const std::string& url);
|
||||
|
||||
private:
|
||||
enum class Job { Fetch, Save, SaveWithLinks, Refresh, Delete, Bookmark, Download };
|
||||
|
||||
bool start(Job job);
|
||||
static void taskEntry(void* self);
|
||||
void run();
|
||||
void fetchOne(const std::string& url, GeminiPage& page);
|
||||
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 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; }
|
||||
void finishWithMessage(const std::string& text);
|
||||
void notify(const std::string& text);
|
||||
void report(const GeminiPage& page, size_t heapBefore, size_t heapLowest);
|
||||
std::string pinKey(const std::string& host, int port) const;
|
||||
|
||||
bool receiveToCard(::NetworkClientSecure& tls, const char* first, size_t len, GeminiPage& page);
|
||||
void loadFromCard(GeminiPage& page, size_t freeBefore);
|
||||
std::string now() const;
|
||||
|
||||
KeyValueStore& store_;
|
||||
StorageService& storage_;
|
||||
ClockService& clock_;
|
||||
EventBus& bus_;
|
||||
SemaphoreHandle_t lock_;
|
||||
std::string pendingUrl_;
|
||||
Job job_ = Job::Fetch;
|
||||
std::string arg_, arg2_;
|
||||
std::vector<std::string> links_;
|
||||
bool preferSaved_ = false;
|
||||
GeminiPage result_;
|
||||
volatile bool busy_ = false, ready_ = false, cancel_ = false, toConsole_ = false;
|
||||
std::string message_;
|
||||
std::string cachedUrl_; // the URL whose body is in kCachePath
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user