Keys: one table file for the device's help panel and the website's key tables (#72)
CI / build (pull_request) Successful in 7m14s
Site / build (pull_request) Successful in 8s

Every screen's keys are constant tables in lib/core/src/app_keys.h (52 of
them, each under an `// id: Title` comment). An App's help() picks the table
of the state it is in. site/tools/gen_dev_docs.py reads the same file and
writes site/data/keys.toml; the `keys` shortcode shows a screen's tables on
its guide page, and /guide/keys/ shows all of them.

The Site job fails when the data file is out of date or a page asks for a
table that doesn't exist, and now also runs when app_keys.h changes. A key
added to an App shows up on the website without anyone editing a page.

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:42:48 +02:00
co-authored by Claude Opus 5.5
parent 82023d36b3
commit 34e6714785
41 changed files with 1213 additions and 228 deletions
+4 -4
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "apps/debug_console_page.h"
#include "debug_auth.h"
@@ -75,10 +76,9 @@ bool DebugConsolePage::onKey(const KeyEvent& e) {
}
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"});
if (confirm_) return keys::add(out, keys::kDialog);
if (typing_) return keys::add(out, keys::kText);
keys::add(out, keys::kDebugConsole);
}
void DebugConsolePage::draw(Canvas& c) {
+4 -6
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "demo_app.h"
#include "ui/widgets.h"
@@ -26,12 +27,9 @@ void DemoApp::notify(const char* text, NotificationLevel 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;
}
if (dialog_) return keys::add(out, keys::kDialog);
if (page_ == Page::Editor) return keys::add(out, keys::kText);
if (page_ == Page::Menu) keys::add(out, keys::kDemo);
}
bool DemoApp::onKey(const KeyEvent& e) {
+8 -19
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "file_viewer.h"
#include <Arduino.h>
@@ -266,27 +267,15 @@ void FileViewer::openPacket(int index) {
}
void FileViewer::help(std::vector<KeyHelp>& out) const {
if (confirm_) return help::dialog(out);
if (confirm_) return keys::add(out, keys::kDialog);
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;
case Mode::Text: keys::add(out, keys::kViewerText); break;
case Mode::Hex: keys::add(out, keys::kViewerHex); break;
case Mode::Pcap: keys::add(out, keys::kViewerPcap); break;
case Mode::Packet: keys::add(out, keys::kViewerPacket); break;
case Mode::Gpx: keys::add(out, keys::kViewerGpx); break;
case Mode::Ota: keys::add(out, keys::kViewerOta); 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) {
+5 -14
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "firmware_page.h"
#include <SD.h>
@@ -235,21 +236,11 @@ void FirmwarePage::drawOlder(Canvas& c) {
}
void FirmwarePage::help(std::vector<KeyHelp>& out) const {
if (confirm_) return help::dialog(out);
if (confirm_) return keys::add(out, keys::kDialog);
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;
case View::Main: keys::add(out, keys::kFirmware); break;
case View::Release: keys::add(out, keys::kFirmwareRelease); break;
case View::Older: keys::add(out, keys::kFirmwareOlder); break;
}
}
+6 -19
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "gemini_app.h"
#include <Arduino.h>
@@ -303,25 +304,11 @@ void GeminiApp::selectLink(int direction) {
}
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"});
}
if (certDialog_ || deleteDialog_) return keys::add(out, keys::kDialog);
if (inputOpen_) return keys::add(out, keys::kGeminiAnswer);
if (addressOpen_) return keys::add(out, keys::kGeminiAddress);
if (isSaved()) keys::add(out, keys::kGeminiSaved);
else keys::add(out, keys::kGemini);
}
const char* GeminiApp::helpTitle() const {
+2 -2
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "gnss_app.h"
#include <Arduino.h>
@@ -83,8 +84,7 @@ void GnssApp::draw(Canvas& c) {
}
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"});
keys::add(out, keys::kGnss);
}
void GnssApp::drawPosition(Canvas& c) {
+4 -23
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "irc_app.h"
#include "ui/fonts.h"
@@ -300,29 +301,9 @@ void IrcApp::drawChat(Canvas& c) {
}
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"});
if (page_ == Page::Chat) return keys::add(out, keys::kIrc);
if (editing_) return keys::add(out, keys::kIrcField);
keys::add(out, keys::kIrcSettings);
}
const char* IrcApp::helpTitle() const {
+2 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "app.h"
#include "app_keys.h"
#include "app_manager.h"
#include "list_model.h"
#include "ui/theme.h"
@@ -14,7 +15,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"); }
void help(std::vector<KeyHelp>& out) const override { keys::add(out, keys::kLauncher); }
private:
AppManager* manager_ = nullptr;
+5 -12
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "lora_scanner_app.h"
#include <Arduino.h>
@@ -167,18 +168,10 @@ void LoraScannerApp::update(uint32_t nowMs) {
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;
case View::Packets: keys::add(out, keys::kLora); break;
case View::Details: keys::add(out, keys::kLoraPacket); break;
case View::Presets: keys::add(out, keys::kLoraPresets); break;
case View::Sweep: keys::add(out, keys::kLoraSweep); break;
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "maintenance_page.h"
#include "ui/widgets.h"
@@ -36,8 +37,8 @@ CleanupPlan MaintenancePage::planFor(int age) const {
}
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");
if (dialog_) return keys::add(out, keys::kDialog);
keys::add(out, keys::kMaintenance);
}
bool MaintenancePage::onKey(const KeyEvent& e) {
+3 -9
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "note_editor.h"
#include <Arduino.h>
@@ -183,15 +184,8 @@ bool NoteEditor::save() {
}
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"});
if (dialog_) return keys::add(out, keys::kDialog);
keys::add(out, keys::kNotesEditor);
}
bool NoteEditor::onKey(const KeyEvent& e) {
+4 -8
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "notes_app.h"
#include <Arduino.h>
@@ -175,15 +176,10 @@ std::string NotesApp::titleOf(int row) {
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 (dialog_) return keys::add(out, keys::kDialog);
if (view_ == View::Name) return keys::add(out, keys::kNotesName);
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"});
keys::add(out, keys::kNotes);
}
const char* NotesApp::helpTitle() const {
+4 -6
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "settings_app.h"
#include <Arduino.h>
@@ -131,12 +132,9 @@ bool SettingsApp::onAboutKey(const KeyEvent& e) {
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::Menu: keys::add(out, keys::kSettings); break;
case Page::Text: keys::add(out, keys::kText); break;
case Page::Choice: keys::add(out, keys::kSettingsChoice); break;
case Page::About: break;
case Page::Wifi: wifiPage_.help(out); break;
case Page::Firmware: firmwarePage_.help(out); break;
+4 -9
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "setup_app.h"
#include "platform/identity.h"
@@ -47,16 +48,10 @@ bool SetupApp::onKey(const KeyEvent& e) {
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::ShortName: keys::add(out, keys::kSetupText); 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;
case Step::Timezone: keys::add(out, keys::kSetupChoice); break;
default: keys::add(out, keys::kSetup); break;
}
}
+6 -18
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "storage_app.h"
#include <Arduino.h>
@@ -279,26 +280,13 @@ void StorageApp::help(std::vector<KeyHelp>& out) const {
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;
case View::Details: return keys::add(out, keys::kStorageDetails);
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"});
if (dialog_) return keys::add(out, keys::kDialog);
if (wantList_ || wait_ != Wait::None) return keys::add(out, keys::kStorageBusy);
if (view_ == View::Name) return keys::add(out, keys::kStorageName);
keys::add(out, keys::kStorage);
}
const char* StorageApp::helpTitle() const {
+4 -7
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "system_app.h"
#include <Arduino.h>
@@ -74,13 +75,9 @@ void SystemApp::onExit() { // nothing is kept while it's closed (Q120)
}
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"});
if (view_ == View::Tasks) keys::add(out, keys::kSystemTasks);
else if (view_ == View::System) keys::add(out, keys::kSystemSystem);
else keys::add(out, keys::kSystem);
}
const char* SystemApp::helpTitle() const {
+9 -17
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "wifi_settings_page.h"
#include "ipv4.h"
@@ -449,25 +450,16 @@ void WifiSettingsPage::draw(Canvas& c) {
}
void WifiSettingsPage::help(std::vector<KeyHelp>& out) const {
if (forgetDialog_) return help::dialog(out);
if (forgetDialog_) return keys::add(out, keys::kDialog);
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::Main: keys::add(out, keys::kWifi); break;
case View::Servers: keys::add(out, keys::kWifiServers); break;
case View::Network: keys::add(out, keys::kWifiNetwork); break;
case View::Details: keys::add(out, keys::kWifiStatus); break;
case View::Scan: keys::add(out, keys::kWifiScan); break;
case View::Ssid: keys::add(out, keys::kWifiName); break;
case View::Password:
case View::Edit: help::textEntry(out); break;
case View::Edit: keys::add(out, keys::kText); break;
}
}
+5 -11
View File
@@ -1,3 +1,4 @@
#include "app_keys.h"
#include "wifi_tools_app.h"
#include <M5Cardputer.h>
@@ -155,17 +156,10 @@ 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;
case View::Menu: keys::add(out, keys::kWifiTools); break;
case View::Networks: keys::add(out, keys::kWifiNetworks); break;
case View::Channels: break; // nothing but the keys that work everywhere
case View::Tracker: keys::add(out, keys::kWifiTracker); break;
}
}