Public Access
Help: Fn+h lists the keys of the screen you're on, and no screen names its keys any more (#69)
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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(), '*'));
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 " +
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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)"));
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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([&]() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user