Help: Fn+h lists the keys of the screen you're on, and no screen names its keys any more (#69)
CI / build (pull_request) Successful in 7m22s
Site / build (pull_request) Successful in 14s

Fn+h on any screen, text fields included, and ? outside Text Entry, open a
panel over the content area: the screen's own keys, then the ones that work
everywhere. Every App declares its keys for the state it is in (pages,
viewers, dialogs and text fields answer for themselves); the App manager
opens the panel and takes every key while it is open.

About 30 hint lines are gone, from every App. What stays on a screen is
state. The first-start Setup keeps its hints and teaches the key; a device
set up before gets one Toast, once. The guide and the FAQ open with it.
`key help` over the consoles.

476 host tests (8 new). Checked on the device with key help and screenshots:
the Launcher, all nine Apps and several of their states. 8.5 KB of flash and
40 bytes of static RAM. Decisions Q196 to Q203 in docs/milestones/U1.md.

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-07 01:29:51 +02:00
co-authored by Claude Opus 5.5
parent 1c6ae0e04f
commit 7fe8b3d22a
62 changed files with 855 additions and 81 deletions
+7 -2
View File
@@ -74,6 +74,13 @@ bool DebugConsolePage::onKey(const KeyEvent& e) {
return true;
}
void DebugConsolePage::help(std::vector<KeyHelp>& out) const {
if (confirm_) return help::dialog(out);
if (typing_) return help::textEntry(out);
help::list(out, "switch, or open");
out.push_back({", /", "switch the console on or off"});
}
void DebugConsolePage::draw(Canvas& c) {
const auto& area = theme::kContent;
c.setTextDatum(top_left);
@@ -84,8 +91,6 @@ void DebugConsolePage::draw(Canvas& c) {
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
c.setTextColor(refusal_.empty() ? theme::kMuted : theme::kWarning);
c.drawString(refusal_.empty() ? "16 to 64 characters. Capitals or not, it's the same." : refusal_.c_str(), 4, area.y + 44);
c.setTextColor(theme::kMuted);
c.drawString("Enter: save `: cancel", 4, area.y + 44 + theme::kLineHeight);
return;
}
+3
View File
@@ -5,6 +5,7 @@
#include "dialog_model.h"
#include "key_event.h"
#include "key_help.h"
#include "line_editor.h"
#include "list_model.h"
#include "services/wifi_service.h"
@@ -25,6 +26,8 @@ class DebugConsolePage {
bool onKey(const KeyEvent& e); // false: leave the page
bool textEntryActive() const { return typing_; }
void draw(Canvas& c);
void help(std::vector<KeyHelp>& out) const;
const char* helpTitle() const { return typing_ ? "A token of your own" : "Debug Console"; }
private:
enum Row { kSwitch, kAddress, kNewToken, kTypeToken, kRows };
+10 -1
View File
@@ -25,6 +25,15 @@ void DemoApp::notify(const char* text, NotificationLevel level) {
bus_.publish(Event::withText(EventType::Notification, text, static_cast<int32_t>(level)));
}
void DemoApp::help(std::vector<KeyHelp>& out) const {
if (dialog_) return help::dialog(out);
switch (page_) {
case Page::Menu: help::list(out, "try the widget"); break;
case Page::Text: out.push_back({"; .", "scroll"}); break;
case Page::Editor: help::textEntry(out, "show the text as a Toast"); break;
}
}
bool DemoApp::onKey(const KeyEvent& e) {
requestRedraw();
if (dialog_) {
@@ -97,7 +106,7 @@ void DemoApp::draw(Canvas& c) {
}
case Page::Editor:
c.setTextColor(theme::kMuted);
c.drawString("Type (opt ' e = \xC3\xA9). Enter: toast", 4, area.y + 4);
c.drawString("A line editor", 4, area.y + 4);
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
c.setTextColor(theme::kMuted);
c.drawString((std::to_string(editor_.text().size()) + "/39 bytes").c_str(), 4, area.y + 44);
+1
View File
@@ -21,6 +21,7 @@ class DemoApp : public App {
bool onKey(const KeyEvent& e) override;
bool textEntryActive() const override { return page_ == Page::Editor && !dialog_; }
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
private:
enum class Page { Menu, Text, Editor };
+26 -7
View File
@@ -265,6 +265,30 @@ void FileViewer::openPacket(int index) {
mode_ = Mode::Packet;
}
void FileViewer::help(std::vector<KeyHelp>& out) const {
if (confirm_) return help::dialog(out);
switch (mode_) {
case Mode::Packet:
out.push_back({"; .", "scroll"});
out.push_back({"Enter", "back to the packets"});
return;
case Mode::Text:
case Mode::Hex:
out.push_back({"; .", "a line up, down"});
out.push_back({", /", "a page up, down"});
out.push_back({"t b", "the top, the end"});
if (mode_ == Mode::Text) out.push_back({"e", "edit it (up to 16 KB)"});
break;
case Mode::Pcap:
help::list(out, "the packet");
out.push_back({", /", "a page up, down"});
break;
case Mode::Ota: out.push_back({"Enter", "install it, if it's genuine"}); break;
case Mode::Gpx: break;
}
out.push_back({"Tab", mode_ != base_ ? "back to the file's own view" : base_ == Mode::Hex || base_ == Mode::Gpx ? "the file as text" : "the file as hex"});
}
bool FileViewer::onKey(const KeyEvent& e) {
if (confirm_) {
confirm_->onKey(e);
@@ -380,7 +404,7 @@ void FileViewer::draw(Canvas& c) {
refresh();
c.setTextDatum(top_left);
theme::Rect body{area.x, area.y + 10, area.w, kRows * theme::kLineHeight};
std::string right, keys = "Tab: hex";
std::string right;
bool scanning = scan_ && !scan_->done;
switch (mode_) {
@@ -396,8 +420,6 @@ void FileViewer::draw(Canvas& c) {
c.fillRect(area.w - 2, barY, 2, barH, theme::kMuted);
}
right = formatBytes(size_);
keys = std::string("Tab: ") + (mode_ != base_ ? "back" : mode_ == Mode::Text ? "hex" : "text") + " t: top b: end";
if (mode_ == Mode::Text) keys += " e: edit";
break;
}
case Mode::Gpx: {
@@ -407,7 +429,6 @@ void FileViewer::draw(Canvas& c) {
else if (scan_) lines = scan_->gpx.lines();
widgets::textLines(c, lines, 0, body);
right = formatBytes(size_);
keys = "Tab: the file as text";
break;
}
case Mode::Pcap: {
@@ -423,7 +444,6 @@ void FileViewer::draw(Canvas& c) {
return row < shown_.size() ? shown_[row] : std::string();
});
right = std::to_string(scan_->packets.size()) + (scan_->morePackets ? "+ packets" : " packets");
keys = "Enter: the packet Tab: hex";
}
break;
}
@@ -447,7 +467,6 @@ void FileViewer::draw(Canvas& c) {
}
widgets::textLines(c, lines, 0, body);
right = formatBytes(size_);
keys = ok ? "Enter: install Tab: hex" : "Tab: hex";
if (confirm_) {
widgets::dialog(c, "Install update?", "The device restarts into " + scan_->version + " once it's written.", *confirm_);
return;
@@ -464,7 +483,7 @@ void FileViewer::draw(Canvas& c) {
c.drawString(right.c_str(), area.w - 3, area.y + 1);
c.setTextDatum(top_left);
if (!message_.empty()) c.setTextColor(theme::kWarning);
c.drawString(message_.empty() ? keys.c_str() : message_.c_str(), 4, area.y + area.h - 9);
if (!message_.empty()) c.drawString(message_.c_str(), 4, area.y + area.h - 9);
}
} // namespace roro
+2
View File
@@ -7,6 +7,7 @@
#include <vector>
#include "dialog_model.h"
#include "key_help.h"
#include "key_event.h"
#include "list_model.h"
#include "services/storage_service.h"
@@ -27,6 +28,7 @@ class FileViewer {
void open(const std::string& path, uint32_t size);
void close();
bool onKey(const KeyEvent& e); // false: leave the viewer
void help(std::vector<KeyHelp>& out) const;
bool update(uint32_t nowMs); // true: draw again
void draw(Canvas& c);
+26 -6
View File
@@ -63,9 +63,9 @@ std::string FirmwarePage::latestText(bool& warn) const {
release::Release r;
if (g.status() == GiteaReleases::Status::Failed && !g.latest(r)) {
warn = true;
return "failed: Enter retries";
return "the check failed";
}
if (!g.latest(r)) return "Enter: check";
if (!g.latest(r)) return "not checked yet";
return r.tag + (release::isNewer(r, update_.runningVersion()) ? " (new)" : " (current)");
}
@@ -202,7 +202,7 @@ void FirmwarePage::drawRelease(Canvas& c) {
c.setFont(&fonts::small);
c.setTextDatum(top_left);
c.setTextColor(theme::kMuted);
c.drawString(ok ? "Enter: install c: check again" : (why + " c: check").c_str(), 4, area.y + area.h - 9);
if (!ok) c.drawString(why.c_str(), 4, area.y + area.h - 9); // why it can't be installed from here
}
void FirmwarePage::drawOlder(Canvas& c) {
@@ -232,9 +232,29 @@ void FirmwarePage::drawOlder(Canvas& c) {
return r.tag + (r.tag == running ? " (running)" : release::versionNewer(r.tag, running) ? " (new)" : "");
},
[&](int i) { return olderReleases_[i].date(); });
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
c.drawString("Enter: details c: look again", 4, area.y + area.h - 9);
}
void FirmwarePage::help(std::vector<KeyHelp>& out) const {
if (confirm_) return help::dialog(out);
switch (view_) {
case View::Main:
help::list(out, "check, open, or install");
out.push_back({"c", "look for a newer release"});
break;
case View::Release:
out.push_back({"; .", "scroll"});
out.push_back({"Enter", "install it"});
out.push_back({"c", "check again"});
break;
case View::Older:
help::list(out, "its details");
out.push_back({"c", "read the list again"});
break;
}
}
const char* FirmwarePage::helpTitle() const {
return view_ == View::Release ? "A release" : view_ == View::Older ? "Older releases" : "Firmware";
}
void FirmwarePage::drawMain(Canvas& c) {
+3
View File
@@ -7,6 +7,7 @@
#include "dialog_model.h"
#include "key_event.h"
#include "key_help.h"
#include "list_model.h"
#include "release_info.h"
#include "services/storage_service.h"
@@ -28,6 +29,8 @@ class FirmwarePage {
void enter();
bool onKey(const KeyEvent& e); // false: leave the page
void draw(Canvas& c);
void help(std::vector<KeyHelp>& out) const;
const char* helpTitle() const;
private:
enum Row { kVersion, kStatus, kAddress, kLatest, kOlder, kSdHeader, kFixed };
+30 -4
View File
@@ -171,7 +171,7 @@ void GeminiApp::update(uint32_t nowMs) {
else {
std::string mime = result.header.mimeType();
showMessage(url, "# Not a text page\nThis is " + mime +
", which the Cardputer can't show. s saves it to the card, in "
", which the Cardputer can't show. Saving it puts it on the card, in "
"/gemini/downloads.\n");
page_.header.meta = mime; // so s knows to download it
}
@@ -302,6 +302,32 @@ void GeminiApp::selectLink(int direction) {
requestRedraw();
}
void GeminiApp::help(std::vector<KeyHelp>& out) const {
if (certDialog_ || deleteDialog_) return help::dialog(out);
if (inputOpen_) return help::textEntry(out, "send it");
if (addressOpen_) return help::textEntry(out, "go there");
out.push_back({"Tab", "the next link"});
out.push_back({"Aa Tab", "the link before"});
out.push_back({"Enter", "follow the link"});
out.push_back({"` Del", "the page before"});
out.push_back({"; .", "scroll"});
out.push_back({"Space", "a page down"});
out.push_back({", /", "sideways, in wide blocks"});
out.push_back({"g", "type an address"});
out.push_back({"b", "bookmark this page"});
if (isSaved()) {
out.push_back({"r", "refresh this Saved Page"});
out.push_back({"d", "delete this Saved Page"});
} else {
out.push_back({"s", "save the page to the card"});
out.push_back({"S", "...with the pages it links to"});
}
}
const char* GeminiApp::helpTitle() const {
return inputOpen_ ? "Gemini: an answer" : addressOpen_ ? "Gemini: an address" : nullptr;
}
bool GeminiApp::onKey(const KeyEvent& e) {
requestRedraw();
if (certDialog_) {
@@ -388,7 +414,7 @@ bool GeminiApp::onKey(const KeyEvent& e) {
if (isGemini(page_.base())) gemini_.addBookmark(page_.base(), titleOf());
break;
case 's': // Q82, Q73
if (isSaved()) status_ = "Already saved; r refreshes it";
if (isSaved()) status_ = "Already saved: it can be refreshed";
else if (!isGemini(page_.url)) status_ = "Only Gemini pages can be saved";
else if (page_.header.mimeType().rfind("text/", 0) == 0) gemini_.save(page_.url);
else gemini_.download(page_.url);
@@ -485,7 +511,7 @@ void GeminiApp::draw(Canvas& c) {
c.fillRect(box.x - 2, box.y - 12, box.w + 4, box.h + 14, theme::kBackground);
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
c.drawString("Go to (Enter, or ` to cancel)", box.x, box.y - 10);
c.drawString("Go to", box.x, box.y - 10);
widgets::lineEditor(c, address_, box);
}
if (inputOpen_) {
@@ -496,7 +522,7 @@ void GeminiApp::draw(Canvas& c) {
c.drawString(displayText(inputPrompt_).substr(0, 39).c_str(), box.x, box.y - 22);
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
c.drawString(inputSensitive_ ? "(hidden) Enter sends, ` cancels" : "Enter sends, ` cancels", box.x, box.y - 9);
if (inputSensitive_) c.drawString("(hidden)", box.x, box.y - 9);
if (inputSensitive_) {
LineEditor masked(1024);
masked.setText(std::string(input_.text().size(), '*'));
+2
View File
@@ -24,6 +24,8 @@ class GeminiApp : public App {
bool textEntryActive() const override { return addressOpen_ || inputOpen_; }
void update(uint32_t nowMs) override;
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override;
private:
struct Visit {
+7 -10
View File
@@ -80,11 +80,11 @@ void GnssApp::draw(Canvas& c) {
return;
}
sky_ ? drawSky(c) : drawPosition(c);
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
c.setTextDatum(top_right);
c.drawString(sky_ ? "Tab: position" : "Tab: sky", area.w - 3, area.y + area.h - 9);
c.setTextDatum(top_left);
}
void GnssApp::help(std::vector<KeyHelp>& out) const {
out.push_back({"Tab", sky_ ? "the position" : "the sky"});
out.push_back({"r", gnss_.tracking() ? "stop the Track" : "record a Track"});
}
void GnssApp::drawPosition(Canvas& c) {
@@ -139,15 +139,12 @@ void GnssApp::drawPosition(Canvas& c) {
c.setFont(&fonts::small);
if (gnss_.tracking()) {
c.setTextColor(theme::kWarning);
std::snprintf(line, sizeof line, "REC %d point%s, %s r: stop", gnss_.trackPoints(),
std::snprintf(line, sizeof line, "REC %d point%s, %s", gnss_.trackPoints(),
gnss_.trackPoints() == 1 ? "" : "s", duration(now - gnss_.trackStartMs()).c_str());
} else if (!refusal_.empty() && now - refusalMs_ < 4000) {
c.setTextColor(theme::kWarning);
std::snprintf(line, sizeof line, "%s", refusal_.c_str());
} else {
c.setTextColor(theme::kMuted);
std::snprintf(line, sizeof line, "r: record a Track");
}
} else line[0] = 0;
c.drawString(line, 4, area.y + area.h - 9);
}
+1
View File
@@ -17,6 +17,7 @@ class GnssApp : public App {
bool onKey(const KeyEvent& e) override;
void update(uint32_t nowMs) override;
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
private:
void drawPosition(Canvas& c);
+31 -2
View File
@@ -299,6 +299,36 @@ void IrcApp::drawChat(Canvas& c) {
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
}
void IrcApp::help(std::vector<KeyHelp>& out) const {
if (page_ == Page::Settings) {
if (editing_) return help::textEntry(out, "keep it");
help::list(out, "edit, switch, or save");
out.push_back({"`", "leave without saving"});
return;
}
out.push_back({"Enter", "send the line"});
out.push_back({"Tab", "the next buffer"});
out.push_back({"Alt ; .", "scroll back, forward"});
out.push_back({"Fn ; .", "lines you sent before"});
out.push_back({"Fn , /", "move the cursor"});
out.push_back({"Del", "delete backwards"});
out.push_back({"/settings", "server, nick, passwords"});
out.push_back({"/join #x", "join a channel"});
out.push_back({"/part", "leave it"});
out.push_back({"/msg nick", "a private chat"});
out.push_back({"/me", "an action"});
out.push_back({"/nick", "change your nick"});
out.push_back({"/topic", "see or set the topic"});
out.push_back({"/names", "who is there"});
out.push_back({"/quit", "disconnect, and stay so"});
out.push_back({"/raw", "a line as it is"});
out.push_back({"`", "leave: IRC stays connected"});
}
const char* IrcApp::helpTitle() const {
return page_ == Page::Settings ? (editing_ ? "IRC: a setting" : "IRC settings") : nullptr;
}
void IrcApp::drawSettings(Canvas& c) {
const auto& area = theme::kContent;
if (editing_) {
@@ -306,8 +336,7 @@ void IrcApp::drawSettings(Canvas& c) {
c.setTextColor(theme::kMuted);
c.drawString(fieldLabel(fields_.selected()).c_str(), 4, area.y + 4);
widgets::lineEditor(c, fieldEditor_, {4, area.y + 22, area.w - 8, 0});
c.drawString(fields_.selected() == kAutojoin ? "e.g. #roro, #private key" : "Enter: OK `: cancel", 4,
area.y + 44);
if (fields_.selected() == kAutojoin) c.drawString("e.g. #roro, #private key", 4, area.y + 44);
return;
}
widgets::list(
+2
View File
@@ -24,6 +24,8 @@ class IrcApp : public App {
void update(uint32_t nowMs) override;
bool textEntryActive() const override { return page_ == Page::Chat || editing_; }
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override;
private:
enum class Page { Chat, Settings };
+1
View File
@@ -14,6 +14,7 @@ class LauncherApp : public App {
void onEnter() override;
bool onKey(const KeyEvent& e) override;
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override { help::list(out, "open the App"); }
private:
AppManager* manager_ = nullptr;
+29 -4
View File
@@ -165,6 +165,32 @@ void LoraScannerApp::update(uint32_t nowMs) {
requestRedraw();
}
void LoraScannerApp::help(std::vector<KeyHelp>& out) const {
switch (view_) {
case View::Packets:
help::list(out, "the packet's details");
out.push_back({"p", "pick a Meshtastic preset"});
out.push_back({"c", capture_.capturing() ? "stop the Capture" : "start a Capture (pcap)"});
out.push_back({"Tab", "the Sweep"});
break;
case View::Details:
out.push_back({"; .", "scroll"});
out.push_back({"Enter", "back to the list"});
break;
case View::Presets: help::list(out, "listen with this preset"); break;
case View::Sweep: out.push_back({"Tab", "the Sniffer"}); break;
}
}
const char* LoraScannerApp::helpTitle() const {
switch (view_) {
case View::Details: return "A LoRa packet";
case View::Presets: return "LoRa presets";
case View::Sweep: return "LoRa Sweep";
default: return nullptr;
}
}
void LoraScannerApp::draw(Canvas& c) {
lastDrawMs_ = millis();
c.setTextDatum(top_left);
@@ -234,8 +260,7 @@ void LoraScannerApp::drawPackets(Canvas& c) {
c.setFont(&fonts::small);
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
std::string keys = std::string("Enter: details p: preset Tab: sweep c: ") + (capture_.capturing() ? "stop" : "capture");
c.drawString(showMessage ? message_.c_str() : keys.c_str(), 4, area.y + area.h - 9);
if (showMessage) c.drawString(message_.c_str(), 4, area.y + area.h - 9);
}
void LoraScannerApp::drawDetails(Canvas& c) {
@@ -329,9 +354,9 @@ void LoraScannerApp::drawSweep(Canvas& c) {
c.drawString(line, x0 + f.steps * cell, wfTop + kWaterfallRows + 1);
c.setTextDatum(top_left);
if (st.peaks.empty())
std::snprintf(line, sizeof line, "floor %d dBm, nothing above it Tab: sniffer", st.floor);
std::snprintf(line, sizeof line, "floor %d dBm, nothing above it", st.floor);
else
std::snprintf(line, sizeof line, "floor %d, %.1f MHz at %d dBm Tab: sniffer", st.floor, st.peaks[0].hz / 1e6,
std::snprintf(line, sizeof line, "floor %d, %.1f MHz at %d dBm", st.floor, st.peaks[0].hz / 1e6,
st.peaks[0].dbm);
c.setTextColor(theme::kMuted);
c.drawString(line, 4, y);
+2
View File
@@ -26,6 +26,8 @@ class LoraScannerApp : public App {
bool onKey(const KeyEvent& e) override;
void update(uint32_t nowMs) override;
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override;
private:
enum class View { Packets, Details, Presets, Sweep };
+5
View File
@@ -35,6 +35,11 @@ CleanupPlan MaintenancePage::planFor(int age) const {
return CleanupPlan::make(listing_[categories_.selected()], cutoff, today);
}
void MaintenancePage::help(std::vector<KeyHelp>& out) const {
if (dialog_) return help::dialog(out);
help::list(out, view_ == View::Main ? "open" : view_ == View::Categories ? "choose what to clean" : "choose how old");
}
bool MaintenancePage::onKey(const KeyEvent& e) {
if (dialog_) {
dialog_->onKey(e);
+2
View File
@@ -5,6 +5,7 @@
#include "cleanup_plan.h"
#include "dialog_model.h"
#include "key_help.h"
#include "event_bus.h"
#include "key_event.h"
#include "list_model.h"
@@ -24,6 +25,7 @@ class MaintenancePage {
void enter();
bool onKey(const KeyEvent& e); // false: leave the page
void draw(Canvas& c);
void help(std::vector<KeyHelp>& out) const;
private:
enum class View { Main, Categories, Ages };
+13 -1
View File
@@ -182,6 +182,18 @@ bool NoteEditor::save() {
return true;
}
void NoteEditor::help(std::vector<KeyHelp>& out) const {
if (dialog_) return help::dialog(out);
out.push_back({"Enter", "a new line"});
out.push_back({"Del", "delete backwards"});
out.push_back({"Tab", "two spaces"});
out.push_back({"Fn ; . , /", "move the cursor"});
out.push_back({"Alt Fn ; .", "a page up, down"});
out.push_back({"Ctrl a e", "start, end of the line"});
out.push_back({"opt ' e", "an accent: \xC3\xA9"});
out.push_back({"`", "done: it saves by itself"});
}
bool NoteEditor::onKey(const KeyEvent& e) {
if (!text_) return false;
redraw_ = true;
@@ -290,7 +302,7 @@ void NoteEditor::draw(Canvas& c) {
c.setFont(&fonts::small);
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
c.drawString(showMessage ? message_.c_str() : "Fn+arrows: move Ctrl+A/E: line Back: done", 4, area.y + area.h - 9);
if (showMessage) c.drawString(message_.c_str(), 4, area.y + area.h - 9);
if (dialog_ && ask_ == Ask::Recover)
widgets::dialog(c, "Unsaved copy", "A save of this note was cut short. Its copy has " + formatBytes(recovered_.size()) + ", the note " +
+2
View File
@@ -5,6 +5,7 @@
#include <vector>
#include "dialog_model.h"
#include "key_help.h"
#include "key_event.h"
#include "note_text.h"
#include "services/clock_service.h"
@@ -33,6 +34,7 @@ class NoteEditor {
const std::string& path() const { return path_; } // "" for a new note nothing was typed in
bool onKey(const KeyEvent& e); // false: done, and saved
void help(std::vector<KeyHelp>& out) const;
bool update(uint32_t nowMs); // true: draw again
void draw(Canvas& c);
+18 -2
View File
@@ -173,6 +173,23 @@ std::string NotesApp::titleOf(int row) {
return files::fitName(list_.name(notes_[row]), kTitleChars); // an empty note, or not a text
}
void NotesApp::help(std::vector<KeyHelp>& out) const {
if (view_ == View::Edit) return editor_.help(out);
if (dialog_) return help::dialog(out);
if (view_ == View::Name) return help::textEntry(out, "rename the file");
if (view_ == View::NoMemory) return;
help::list(out, "open the note");
out.push_back({", /", "a page up, down"});
out.push_back({"n", "a new note"});
out.push_back({"r", "rename its file"});
out.push_back({"d Del", "delete it"});
out.push_back({"s", "sort: newest, or by name"});
}
const char* NotesApp::helpTitle() const {
return view_ == View::Edit ? "Notes: the editor" : view_ == View::Name ? "Notes: a file name" : nullptr;
}
bool NotesApp::onKey(const KeyEvent& e) {
requestRedraw();
if (view_ == View::NoMemory) return false;
@@ -315,7 +332,6 @@ void NotesApp::draw(Canvas& c) {
} else if (notes_.empty()) {
c.setTextColor(theme::kMuted);
c.drawString("No notes yet.", 4, rows.y + 4);
c.drawString("n starts one.", 4, rows.y + 4 + theme::kLineHeight);
} else {
readTitles();
widgets::list(
@@ -329,7 +345,7 @@ void NotesApp::draw(Canvas& c) {
c.setFont(&fonts::small);
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
c.drawString(showMessage ? message_.c_str() : "n: new Enter: open r: name d: del s: sort", 4, area.y + area.h - 9);
if (showMessage) c.drawString(message_.c_str(), 4, area.y + area.h - 9);
if (view_ == View::Name) {
theme::Rect box{16, 44, theme::kWidth - 32, 48};
+2
View File
@@ -28,6 +28,8 @@ class NotesApp : public App {
bool textEntryActive() const override { return view_ == View::Edit || view_ == View::Name; }
void update(uint32_t nowMs) override;
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override;
private:
enum class View { List, Edit, Name, NoMemory };
+27 -4
View File
@@ -129,6 +129,31 @@ bool SettingsApp::onAboutKey(const KeyEvent& e) {
return true;
}
void SettingsApp::help(std::vector<KeyHelp>& out) const {
switch (page_) {
case Page::Menu:
help::list(out, "edit, or open the page");
out.push_back({", /", "change a switch or a slider"});
break;
case Page::Text: help::textEntry(out); break;
case Page::Choice: help::list(out, "choose it"); break;
case Page::About: break;
case Page::Wifi: wifiPage_.help(out); break;
case Page::Firmware: firmwarePage_.help(out); break;
case Page::Debug: debugPage_.help(out); break;
}
}
const char* SettingsApp::helpTitle() const {
switch (page_) {
case Page::Wifi: return wifiPage_.helpTitle();
case Page::Firmware: return firmwarePage_.helpTitle();
case Page::Debug: return debugPage_.helpTitle();
case Page::About: return "About";
default: return nullptr;
}
}
void SettingsApp::update(uint32_t nowMs) {
// Live values on About and Firmware.
bool live = page_ == Page::About || page_ == Page::Firmware || page_ == Page::Debug ||
@@ -177,10 +202,8 @@ void SettingsApp::draw(Canvas& c) {
c.setTextColor(theme::kMuted);
c.drawString(menu_.label(editingRow_).c_str(), 4, area.y + 4);
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
c.drawString(("Enter: save `: cancel " + std::to_string(editor_.text().size()) + "/" +
std::to_string(editor_.maxBytes()) + " bytes")
.c_str(),
4, area.y + 44);
c.drawString((std::to_string(editor_.text().size()) + " of " + std::to_string(editor_.maxBytes()) + " bytes").c_str(), 4,
area.y + 44);
break;
case Page::Choice: {
auto options = menu_.choices(editingRow_);
+2
View File
@@ -50,6 +50,8 @@ class SettingsApp : public App {
(page_ == Page::Debug && debugPage_.textEntryActive());
}
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override;
private:
enum class Page { Menu, Text, Choice, About, Wifi, Firmware, Debug };
+23 -2
View File
@@ -44,6 +44,24 @@ bool SetupApp::onKey(const KeyEvent& e) {
return true;
}
void SetupApp::help(std::vector<KeyHelp>& out) const {
switch (wizard_.step()) {
case Step::LongName:
case Step::ShortName: help::textEntry(out, "next step"); break;
case Step::Region:
case Step::Timezone:
help::list(out, "choose it, next step");
out.push_back({"`", "the step before"});
break;
default:
out.push_back({"Enter", "continue"});
out.push_back({"`", "the step before"});
break;
}
}
// The one place that names keys on the screen (Q200): someone in their first minute doesn't know
// the help key yet, and this is where they learn it.
void SetupApp::draw(Canvas& c) {
const auto& area = theme::kContent;
auto heading = [&](const char* text) {
@@ -63,6 +81,7 @@ void SetupApp::draw(Canvas& c) {
heading("Welcome to roro9stack");
note("Let's set up this device: your names on the mesh, your radio region and your timezone.",
area.y + 22, theme::kText);
note("One key to know: Fn + h lists the keys of the screen you're on.", area.y + 22 + 4 * theme::kLineHeight, theme::kAccent);
note("Enter: continue", area.y + area.h - 14);
break;
case Step::LongName:
@@ -88,8 +107,10 @@ void SetupApp::draw(Canvas& c) {
[this](int i) { return wizard_.choiceLabel(i); });
break;
case Step::Done:
heading("All set");
note("You can change all of this later in Settings.", area.y + 22, theme::kText);
heading("All set. One key to remember:");
note("Fn + h, on any screen, lists the keys that work there. No screen names its keys: that key does. Try it now.",
area.y + 22, theme::kAccent);
note("The rest can be changed later in Settings.", area.y + 22 + 4 * theme::kLineHeight, theme::kText);
note("Enter: start `: go back", area.y + area.h - 14);
break;
}
+2
View File
@@ -17,6 +17,8 @@ class SetupApp : public App {
return wizard_.step() == SetupWizard::Step::LongName || wizard_.step() == SetupWizard::Step::ShortName;
}
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override { return "Setup"; }
private:
AppManager& apps_;
+45 -9
View File
@@ -199,7 +199,7 @@ void StorageApp::onFinished(const FileOps::Status& s) {
}
void StorageApp::paste(bool replace) {
if (!clip_) return say("Nothing to paste: c copies, x cuts");
if (!clip_) return say("Nothing to paste: copy or cut something first");
std::string name = baseName(clip_.path), why;
if (cut_) {
why = ops_.move(clip_.path, clip_.folder, joinPath(cwd_, name), replace);
@@ -273,6 +273,45 @@ void StorageApp::showDetails(const Item& item) {
view_ = View::Details;
}
void StorageApp::help(std::vector<KeyHelp>& out) const {
switch (view_) {
case View::NoMemory: return;
case View::Maintenance: return maintenance_.help(out);
case View::Editor: return noteEditor_.help(out);
case View::Viewer: return viewer_.help(out);
case View::Details:
out.push_back({"; .", "scroll"});
out.push_back({"Enter", "back to the folder"});
return;
default: break;
}
if (dialog_) return help::dialog(out);
if (wantList_ || wait_ != Wait::None) return (void)out.push_back({"`", "stop the copy or the delete"});
if (view_ == View::Name) return help::textEntry(out, renaming_ ? "rename it" : "make the folder");
help::list(out, "open the folder or the file");
out.push_back({", /", "a page up, down"});
out.push_back({"c x", "copy, cut"});
out.push_back({"v", "paste here"});
out.push_back({"r", "rename"});
out.push_back({"d Del", "delete, after asking"});
out.push_back({"n", "a new folder"});
out.push_back({"i", "details: size, date, type"});
out.push_back({"s", "sort: name, date, size"});
out.push_back({"m", "Maintenance: clean-up, erase"});
out.push_back({"`", "the folder above"});
}
const char* StorageApp::helpTitle() const {
switch (view_) {
case View::Maintenance: return "Storage: maintenance";
case View::Editor: return "Storage: the editor";
case View::Viewer: return "Storage: a file";
case View::Details: return "Storage: details";
case View::Name: return "Storage: a name";
default: return nullptr;
}
}
bool StorageApp::onKey(const KeyEvent& e) {
requestRedraw();
if (view_ == View::NoMemory) return false;
@@ -434,7 +473,7 @@ bool StorageApp::onBrowseKey(const KeyEvent& e) {
if (ch == 'c') {
clip_ = item;
cut_ = false;
say("v pastes a copy of " + name);
say("Copied " + name + ": paste it where you like");
return true;
}
// Cut, rename and delete change the original: the rules of Q130 first.
@@ -446,7 +485,7 @@ bool StorageApp::onBrowseKey(const KeyEvent& e) {
if (ch == 'x') {
clip_ = item;
cut_ = true;
say("v moves " + name + " here");
say("Cut " + name + ": paste it where it should go");
} else if (ch == 'r') {
target_ = item;
renaming_ = true;
@@ -542,7 +581,6 @@ void StorageApp::drawBrowse(Canvas& c) {
} else if (rows_.count() == 0) {
c.setTextColor(theme::kMuted);
c.drawString("Nothing in this folder.", 4, rows.y + 4);
c.drawString("n makes a folder, v pastes.", 4, rows.y + 4 + theme::kLineHeight);
} else {
int entries = static_cast<int>(list_.count());
auto value = [&](int i) {
@@ -558,13 +596,13 @@ void StorageApp::drawBrowse(Canvas& c) {
value);
}
// Footer: a message for a few seconds, what v would paste, or the keys.
// Footer: a message for a few seconds, or what a paste would do. Never the keys (Fn+h).
c.setFont(&fonts::small);
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
std::string keys = "c x v:paste r:name d:del n:new i:info s:sort";
std::string keys;
if ((wantList_ || wait_ == Wait::List) && millis() - waitSinceMs_ >= kQuietMs) keys = "Reading " + files::fitName(want_, 30) + "...";
else if (clip_) keys = std::string("v: ") + (cut_ ? "move " : "copy ") + files::fitName(baseName(clip_.path), 20) + " here";
else if (clip_) keys = std::string(cut_ ? "Cut: " : "Copied: ") + files::fitName(baseName(clip_.path), 28);
c.drawString(showMessage ? message_.c_str() : keys.c_str(), 4, area.y + area.h - 9);
}
@@ -589,8 +627,6 @@ void StorageApp::drawProgress(Canvas& c) {
line = formatBytes(s.doneBytes) + " of " + formatBytes(s.totalBytes);
}
c.drawString(line.c_str(), box.x + 6, box.y + 31);
c.setFont(&fonts::small);
c.drawString("Back: cancel", box.x + 6, box.y + box.h - 10);
}
} // namespace roro
+2
View File
@@ -34,6 +34,8 @@ class StorageApp : public App {
bool textEntryActive() const override { return view_ == View::Name || view_ == View::Editor; }
void update(uint32_t nowMs) override;
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override;
private:
enum class View { Browse, Details, Name, Viewer, Editor, Maintenance, NoMemory };
+23 -6
View File
@@ -73,6 +73,26 @@ void SystemApp::onExit() { // nothing is kept while it's closed (Q120)
load_[0] = load_[1] = -1;
}
void SystemApp::help(std::vector<KeyHelp>& out) const {
out.push_back({"Tab", "the next view"});
out.push_back({"Aa Tab", "the view before"});
if (view_ == View::Tasks) {
out.push_back({"; .", "scroll"});
out.push_back({"s", "sort: cpu, stack, name"});
}
if (view_ == View::System) out.push_back({"; .", "scroll"});
}
const char* SystemApp::helpTitle() const {
switch (view_) {
case View::Tasks: return "System: tasks";
case View::Memory: return "System: memory";
case View::Network: return "System: network";
case View::System: return "System: system";
default: return "System: overview";
}
}
bool SystemApp::onKey(const KeyEvent& e) {
if (e.key == Key::Tab) {
int n = static_cast<int>(View::Count), v = static_cast<int>(view_);
@@ -219,7 +239,6 @@ void SystemApp::drawOverview(Canvas& c) {
}
}
}
footer(c, "Tab: tasks, memory, network, system");
}
void SystemApp::drawTasks(Canvas& c) {
@@ -249,8 +268,7 @@ void SystemApp::drawTasks(Canvas& c) {
c.drawString(std::to_string(r.stackFree).c_str(), area.w - 4, y);
}
c.setTextDatum(top_left);
std::string keys = std::string("s: by ") + (sort_ == TaskSort::Share ? "stack" : sort_ == TaskSort::Stack ? "name" : "cpu") +
" " + std::to_string(rows_.size()) + " tasks Tab: memory";
std::string keys = std::to_string(rows_.size()) + " tasks, by " + (sort_ == TaskSort::Share ? "cpu" : sort_ == TaskSort::Stack ? "stack" : "name");
footer(c, keys.c_str());
}
@@ -288,7 +306,7 @@ void SystemApp::drawMemory(Canvas& c) {
int x0 = gx + 1 + static_cast<int>((i - 1) * (gw - 2) / kHistory), x1 = gx + 1 + static_cast<int>(i * (gw - 2) / kHistory);
c.drawLine(x0, yOf(heapHistory_.at(i - 1)), x1, yOf(heapHistory_.at(i)), theme::kAccent);
}
footer(c, "KB free, 2 min, with the floors Tab: network");
footer(c, "KB free, 2 min, with the floors");
}
void SystemApp::drawNetwork(Canvas& c) {
@@ -331,7 +349,7 @@ void SystemApp::drawNetwork(Canvas& c) {
y += theme::kLineHeight;
}
c.setTextDatum(top_left);
footer(c, "bytes since boot Tab: system");
footer(c, "bytes since boot");
}
void SystemApp::drawSystem(Canvas& c) {
@@ -355,7 +373,6 @@ void SystemApp::drawSystem(Canvas& c) {
wrappedLines_ = static_cast<int>(wrapped.size());
systemTop_ = std::clamp(systemTop_, 0, std::max(0, wrappedLines_ - 8));
widgets::textLines(c, wrapped, systemTop_, {area.x + 2, area.y + 2, area.w - 2, area.h - 12});
footer(c, "Tab: overview");
}
} // namespace roro
+2
View File
@@ -28,6 +28,8 @@ class SystemApp : public App {
bool onKey(const KeyEvent& e) override;
void update(uint32_t nowMs) override;
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override;
private:
enum class View : uint8_t { Overview, Tasks, Memory, Network, System, Count };
+39 -5
View File
@@ -413,7 +413,6 @@ void WifiSettingsPage::draw(Canvas& c) {
c.setTextColor(theme::kMuted);
c.drawString(editTitle_.c_str(), 4, area.y + 4);
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
c.drawString("Enter: save `: cancel", 4, area.y + 44);
break;
}
@@ -427,7 +426,7 @@ void WifiSettingsPage::draw(Canvas& c) {
if (!n.ssid.empty()) found.push_back(n);
if (scan_.count() != static_cast<int>(found.size())) scan_.setCount(static_cast<int>(found.size()));
if (found.empty()) {
widgets::textLines(c, {"No networks found.", "", "`: back"}, 0, area);
widgets::textLines(c, {"No networks found."}, 0, area);
break;
}
widgets::list(
@@ -443,20 +442,55 @@ void WifiSettingsPage::draw(Canvas& c) {
std::string title = view_ == View::Ssid ? "Hidden network name" : "Password for " + newSsid_;
c.drawString(title.c_str(), 4, area.y + 4);
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
c.drawString(view_ == View::Ssid ? "Enter: next `: cancel" : "Enter: save (empty = open) `: cancel", 4,
area.y + 44);
if (view_ == View::Password) c.drawString("Leave it empty for an open network.", 4, area.y + 44);
break;
}
}
}
void WifiSettingsPage::help(std::vector<KeyHelp>& out) const {
if (forgetDialog_) return help::dialog(out);
switch (view_) {
case View::Main:
help::list(out, "open, or change");
out.push_back({", /", "switch Wi-Fi on or off"});
break;
case View::Servers:
help::list(out, "edit");
out.push_back({", /", "Always use my DNS: on, off"});
break;
case View::Network:
help::list(out, "edit, or forget");
out.push_back({", /", "Automatic or Fixed"});
break;
case View::Details: out.push_back({"Enter", "back"}); break;
case View::Scan: help::list(out, "choose this network"); break;
case View::Ssid: help::textEntry(out, "next: the password"); break;
case View::Password:
case View::Edit: help::textEntry(out); break;
}
}
const char* WifiSettingsPage::helpTitle() const {
switch (view_) {
case View::Servers: return "DNS and NTP";
case View::Network: return "A saved network";
case View::Details: return "Wi-Fi status";
case View::Scan: return "Add a network";
case View::Ssid: return "Hidden network name";
case View::Password: return "Wi-Fi password";
case View::Edit: return "Wi-Fi: a field";
default: return "Wi-Fi";
}
}
// What the device is using and where each part came from (Q113).
void WifiSettingsPage::drawDetails(Canvas& c) {
const auto& area = theme::kContent;
WifiService::Connection n = wifi_.connection();
std::vector<std::string> lines;
if (!n.connected) {
lines = {statusText(), "", "`: back"};
lines = {statusText()};
} else {
lines.push_back(wifi_.ssid() + ", " + std::to_string(wifi_.rssi()) + " dBm");
lines.push_back("Address " + n.address + "/" + std::to_string(n.prefix) + (n.fixed ? " (fixed)" : " (DHCP)"));
+3
View File
@@ -6,6 +6,7 @@
#include "dialog_model.h"
#include "event_bus.h"
#include "key_event.h"
#include "key_help.h"
#include "line_editor.h"
#include "list_model.h"
#include "saved_networks.h"
@@ -30,6 +31,8 @@ class WifiSettingsPage {
// Redraw periodically: the status, a scan, the connection details.
bool live() const { return view_ == View::Main || view_ == View::Scan || view_ == View::Details; }
void draw(Canvas& c);
void help(std::vector<KeyHelp>& out) const;
const char* helpTitle() const;
private:
enum class View { Main, Scan, Ssid, Password, Details, Servers, Network, Edit };
+27 -2
View File
@@ -153,6 +153,31 @@ void WifiToolsApp::draw(Canvas& c) {
}
}
void WifiToolsApp::help(std::vector<KeyHelp>& out) const {
switch (view_) {
case View::Menu: help::list(out); break;
case View::Networks:
help::list(out, "track its signal");
out.push_back({"s", "sort: signal, channel, name"});
out.push_back({"o", "open networks only"});
out.push_back({"h", "hide the hidden ones"});
out.push_back({"w", "strong ones only"});
out.push_back({"l", "log the scans to the card"});
break;
case View::Channels: break;
case View::Tracker: out.push_back({"m", "clicks on or off"}); break;
}
}
const char* WifiToolsApp::helpTitle() const {
switch (view_) {
case View::Networks: return "Networks nearby";
case View::Channels: return "Channel occupancy";
case View::Tracker: return "Signal tracker";
default: return nullptr;
}
}
void WifiToolsApp::drawNetworks(Canvas& c) {
const auto& area = theme::kContent;
const auto& scan = wifi_.listScan();
@@ -178,7 +203,7 @@ void WifiToolsApp::drawNetworks(Canvas& c) {
widgets::textLines(c,
{wifi_.listScanDone() ? (any ? "Nothing matches the filter." : "No networks found yet.")
: "Scanning...",
"", "s sort o open h hide-hidden w strong l log"},
},
0, list);
return;
}
@@ -233,7 +258,7 @@ void WifiToolsApp::drawTracker(Canvas& c) {
c.drawString(targetName_.c_str(), 4, area.y + 2);
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
std::string sub = targetBssid_ + " ch" + std::to_string(targetChannel_) + " m: clicks " + (clicks_ ? "on" : "off");
std::string sub = targetBssid_ + " ch" + std::to_string(targetChannel_) + " clicks " + (clicks_ ? "on" : "off");
c.drawString(sub.c_str(), 4, area.y + 17);
c.setFont(&fonts::bold);
+2
View File
@@ -27,6 +27,8 @@ class WifiToolsApp : public App {
bool onKey(const KeyEvent& e) override;
void update(uint32_t nowMs) override;
void draw(Canvas& c) override;
void help(std::vector<KeyHelp>& out) const override;
const char* helpTitle() const override;
private:
enum class View { Menu, Networks, Channels, Tracker };
+9 -2
View File
@@ -225,6 +225,13 @@ void setup() {
services.startAll(millis());
apps->begin();
if (!settings.getBool(Setting::SetupDone)) apps->openModal("setup");
// No screen names its keys any more (issue #69): Setup teaches the help key to a new device, and
// one that was set up before this firmware is told once, here.
if (!settings.getBool(Setting::HelpTold)) {
if (settings.getBool(Setting::SetupDone))
bus.publish(Event::withText(EventType::Notification, "Fn+h: the keys of any screen", static_cast<int32_t>(NotificationLevel::Info)));
settings.setBool(Setting::HelpTold, true);
}
console.printf("%s %s ready, free heap %u, last start: %s\n", kProductName, versionString(), ESP.getFreeHeap(),
system_info::resetReason());
if (system_info::resetWasCrash()) {
@@ -555,7 +562,7 @@ static const char* const kHelp =
"gnss status | gnss restart | gnss track start|stop | gnss nmea on|off | gnss send <sentence without $ and checksum>\n"
"crash the last crash: firmware, reason, task, backtrace\n"
"coredump erase forget the core dump in flash\n"
"key <name|char> press a key: up down left right select back home del tab space, or one character\n"
"key <name|char> press a key: up down left right select back home del tab space help, or one character\n"
"wifi status | wifi add <ssid><TAB><password>\n"
"wifi ip <ssid> dhcp | wifi ip <ssid> <address>/<prefix> [gateway] a Saved Network's IP setting\n"
"wifi dns <a> [b] | wifi dns always on|off | wifi ntp <a> [b] DNS and NTP servers\n"
@@ -842,7 +849,7 @@ static void runCommand(String line, bool fromSerial = false) {
String k = line.substring(4);
Key key = k == "up" ? Key::Up : k == "down" ? Key::Down : k == "left" ? Key::Left
: k == "right" ? Key::Right : k == "back" ? Key::Back : k == "home" ? Key::Home
: k == "del" ? Key::Delete : k == "tab" ? Key::Tab : Key::Select;
: k == "del" ? Key::Delete : k == "tab" ? Key::Tab : k == "help" ? Key::Help : Key::Select;
KeyEvent ev = k == "space" ? KeyEvent::character(' ')
: k.length() == 1 && k[0] > ' ' ? KeyEvent::character((unsigned char)k[0])
: KeyEvent::of(key);
+1 -2
View File
@@ -459,8 +459,7 @@ void GeminiService::openSaved(const std::string& path, GeminiPage& page) {
// about:start (Q78, Q84): bookmarks, then Saved Pages by capsule, newest first, then defaults.
void GeminiService::composeStart(GeminiPage& page) {
std::string doc = "# Gemini\nTab picks a link, Enter follows it, Back returns, g types an address. "
"On a page: b bookmarks it, s saves it to read offline, S with the pages it links to.\n";
std::string doc = "# Gemini\nYour bookmarks and Saved Pages, then a few places to start.\n";
std::string bookmarks;
std::vector<SavedEntry> saved;
bool card = storage_.runAndWait([&]() {
+1
View File
@@ -17,6 +17,7 @@ void Screen::render(AppManager& apps, const StatusInfo& status, const Toast* toa
const auto& area = theme::kContent;
canvas_.setClipRect(area.x, area.y, area.w, area.h);
apps.foreground().draw(canvas_);
if (apps.help().isOpen()) widgets::help(canvas_, apps.help()); // over the App, under the Status Bar
canvas_.clearClipRect();
widgets::statusBar(canvas_, status);
if (toast) widgets::toast(canvas_, *toast);
+36
View File
@@ -175,4 +175,40 @@ void toast(Canvas& c, const Toast& t) {
c.clearClipRect();
}
void help(Canvas& c, const HelpModel& model) {
const auto& area = kContent;
c.fillRect(area.x, area.y, area.w, area.h, kBackground);
c.setTextDatum(top_left);
c.setFont(&fonts::bold);
c.setTextColor(kAccent);
c.drawString(("Keys: " + model.title()).c_str(), 4, area.y + 1);
c.drawFastHLine(4, area.y + 13, area.w - 8, kMuted);
const int top = area.y + 16, keysX = 4, actionX = 70;
const auto& rows = model.rows();
int count = static_cast<int>(rows.size());
for (int row = 0; row < model.visibleRows(); row++) {
int i = model.top() + row;
if (i >= count) break;
int y = top + row * kLineHeight;
if (!rows[i].action) { // a heading
c.setFont(&fonts::bold);
c.setTextColor(kMuted);
c.drawString(rows[i].keys, keysX, y);
continue;
}
c.setFont(&fonts::body);
c.setTextColor(kAccent);
c.drawString(rows[i].keys, keysX, y);
c.setTextColor(kText);
c.drawString(rows[i].action, actionX, y);
}
if (count > model.visibleRows()) {
int h = model.visibleRows() * kLineHeight;
int barH = h * model.visibleRows() / count;
int barY = top + (h - barH) * model.top() / (count - model.visibleRows());
c.fillRect(area.x + area.w - 2, barY, 2, barH, kMuted);
}
}
} // namespace roro::widgets
+4
View File
@@ -6,6 +6,7 @@
#include "canvas.h"
#include "dialog_model.h"
#include "key_help.h"
#include "line_editor.h"
#include "list_model.h"
#include "text_wrap.h"
@@ -59,6 +60,9 @@ void lineEditor(Canvas& c, const LineEditor& editor, theme::Rect area);
void dialog(Canvas& c, const std::string& title, const std::string& message, const DialogModel& model);
// The help panel (Fn+h): the keys of the screen under it, over the whole content area.
void help(Canvas& c, const HelpModel& model);
void toast(Canvas& c, const Toast& t);
} // namespace widgets