Public Access
The Shell shows the replies to its own commands and nothing else. The console knows who each line is printed for (Console::As, Console::origin): a command run from the Shell prints as the Shell's, and what answers it later from another task carries that along (ls, tasks, du, cp, update check, sd list, screenshot, gemini get). Ctrl+b shows everything instead. This replaces the ten-second window, which was a guess. An App's name with a capital opens it (Notes, Irc, Wifi, Gnss, Gemini, Lora, Storage, Shell, System, Settings), from the Shell and from the consoles. rm needs -r for a folder, here and over the consoles. In the Shell a file, or a folder with something in it, is asked about unless -f; an empty folder with -r goes without a word. The Shell now hands its line to the main loop to run: run from inside the key handler, rm on a folder overflowed the loop's stack and crashed the device. `info` says which App is in front. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
150 lines
7.5 KiB
C++
150 lines
7.5 KiB
C++
#pragma once
|
|
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/semphr.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "platform/console.h"
|
|
#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"
|
|
|
|
class NetworkClientSecure;
|
|
|
|
namespace roro {
|
|
|
|
// What a fetch came back with.
|
|
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 (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
|
|
std::string truncatedWhy;
|
|
std::string error; // Failed: why, for a human
|
|
std::string host; // CertificateChanged: whose, and both fingerprints
|
|
int port = 1965;
|
|
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;
|
|
// 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; }
|
|
};
|
|
|
|
// 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
|
|
static constexpr size_t kStartFloor = 55 * 1024; // Q86: no fetch below this
|
|
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 size_t kIndexEvery = 64; // lines between lineIndex entries
|
|
static constexpr size_t kMaxLineBytes = 8192; // longer lines are cut
|
|
// What a page costs per line on top of its text: TextBuffer's index (8) and the App's tables
|
|
// (type, first row, links: about 8). Counting only the first left the steady floor 1.5 to 3 KB
|
|
// short on a windowed page (found in M3).
|
|
static constexpr size_t kBytesPerLine = 16;
|
|
static constexpr const char* kBookmarks = "/gemini/bookmarks.gmi";
|
|
static constexpr const char* kStartUrl = "about:start";
|
|
|
|
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)
|
|
// 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);
|
|
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);
|
|
|
|
// `gemini get <url>`: the same fetch, reported on the console instead of to the App.
|
|
bool fetchToConsole(const std::string& url);
|
|
Console::Origin consoleFrom_ = Console::Origin::System; // who asked for that fetch: its report is theirs
|
|
|
|
private:
|
|
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, 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
|
|
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);
|
|
std::string pinKey(const std::string& host, int port) const;
|
|
std::string now() const;
|
|
|
|
KeyValueStore& store_;
|
|
StorageService& storage_;
|
|
ClockService& clock_;
|
|
EventBus& bus_;
|
|
SemaphoreHandle_t lock_;
|
|
Job job_ = Job::Fetch;
|
|
std::string arg_, arg2_;
|
|
std::vector<std::string> links_;
|
|
bool preferSaved_ = false;
|
|
GeminiPage result_;
|
|
std::string message_;
|
|
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;
|
|
};
|
|
|
|
} // namespace roro
|