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
+9
View File
@@ -2,7 +2,10 @@
#include <cstdint>
#include <vector>
#include "key_event.h"
#include "key_help.h"
namespace roro {
@@ -25,6 +28,12 @@ class App {
// True while the App is editing text: the arrow keys then type ; . , / and need Fn to move.
virtual bool textEntryActive() const { return false; }
// The keys that work right now, for the help panel (Fn+h): the App's own, in the state it's
// in. Back, Home and the arrows are added for it. No screen names keys any other way.
virtual void help(std::vector<KeyHelp>& out) const { (void)out; }
// What the panel is titled with, when the App's name isn't enough ("Notes: editor").
virtual const char* helpTitle() const { return nullptr; }
// Called every main-loop pass while in the foreground (e.g. to refresh live values).
virtual void update(uint32_t nowMs) { (void)nowMs; }
+16
View File
@@ -52,6 +52,21 @@ void AppManager::endModal() {
}
void AppManager::handleKey(const KeyEvent& event) {
if (help_.isOpen()) { // scrolls or closes; nothing reaches the App, Home included
help_.onKey(event);
redraw_ = true;
return;
}
if (event.key == Key::Help) {
std::vector<KeyHelp> rows;
foreground_->help(rows);
help::everywhere(rows);
const char* scope = foreground_->helpTitle();
const char* app = foregroundTitle();
help_.open(scope ? scope : app ? app : "Launcher", std::move(rows));
redraw_ = true;
return;
}
if (modal_) {
foreground_->onKey(event);
return;
@@ -77,6 +92,7 @@ bool AppManager::takeRedraw() {
}
void AppManager::switchTo(App& app) {
help_.close(); // an App opened from elsewhere (a Notification, a command): its keys, not the last one's
if (&app == foreground_) return;
foreground_->onExit();
foreground_ = &app;
+4
View File
@@ -37,6 +37,9 @@ class AppManager {
App& foreground() const { return *foreground_; }
const char* foregroundTitle() const; // nullptr for the Launcher
// The help panel (Fn+h): open, it takes every key, and the App sees none of them.
const HelpModel& help() const { return help_; }
// True once after the screen needs redrawing (App switch, or the App asked for it).
bool takeRedraw();
@@ -47,6 +50,7 @@ class AppManager {
App& launcher_;
App* foreground_;
std::vector<AppInfo> apps_;
HelpModel help_;
bool redraw_ = true;
bool modal_ = false;
};
+1
View File
@@ -16,6 +16,7 @@ enum class Key : uint8_t {
Home,
Tab,
Delete,
Help, // Fn+h anywhere, or ? outside Text Entry: the keys of this screen (issue #69)
};
struct KeyEvent {
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <algorithm>
#include <string>
#include <vector>
#include "key_event.h"
// The help panel (issue #69, docs/milestones/U1.md): Fn+h on any screen lists the keys that work
// there. Every App says what its keys are in the state it's in; nothing on a screen names keys.
namespace roro {
// One line of the panel: a key (or keys) and what it does. A line with no action is a heading.
struct KeyHelp {
const char* keys;
const char* action;
};
namespace help {
// Lists that several screens share.
inline void list(std::vector<KeyHelp>& out, const char* enter = "open") {
out.push_back({"; .", "up, down"});
out.push_back({"Enter", enter});
}
inline void dialog(std::vector<KeyHelp>& out) {
out.push_back({", /", "the other answer"});
out.push_back({"Enter", "choose it"});
out.push_back({"`", "cancel"});
}
inline void textEntry(std::vector<KeyHelp>& out, const char* enter = "save") {
out.push_back({"Enter", enter});
out.push_back({"`", "cancel"});
out.push_back({"Del", "delete backwards"});
out.push_back({"Fn , /", "move the cursor"});
out.push_back({"opt ' e", "an accent: \xC3\xA9"});
}
// What works on every screen: the end of every panel.
inline void everywhere(std::vector<KeyHelp>& out) {
out.push_back({"Everywhere", nullptr});
out.push_back({"`", "back"});
out.push_back({"Fn `", "home, the Launcher"});
out.push_back({"; . , /", "arrows (Fn+ while typing)"});
out.push_back({"Fn h ?", "these keys (? not typing)"});
}
} // namespace help
// The panel itself: what it lists, and how far it's scrolled. The keys it takes while open are the
// arrows; any other key closes it, and none reaches the App.
class HelpModel {
public:
explicit HelpModel(int visibleRows = 8) : visible_(visibleRows) {}
void open(const std::string& title, std::vector<KeyHelp> rows) {
title_ = title;
rows_ = std::move(rows);
top_ = 0;
open_ = true;
}
void close() {
open_ = false;
rows_.clear();
rows_.shrink_to_fit(); // nothing is kept while it's closed
}
bool isOpen() const { return open_; }
void onKey(const KeyEvent& e) {
int last = std::max(0, static_cast<int>(rows_.size()) - visible_);
switch (e.key) {
case Key::Up: top_ = std::max(0, top_ - 1); break;
case Key::Down: top_ = std::min(last, top_ + 1); break;
case Key::Left: top_ = std::max(0, top_ - visible_); break;
case Key::Right: top_ = std::min(last, top_ + visible_); break;
default: close(); break;
}
}
const std::string& title() const { return title_; }
const std::vector<KeyHelp>& rows() const { return rows_; }
int top() const { return top_; }
int visibleRows() const { return visible_; }
private:
std::string title_;
std::vector<KeyHelp> rows_;
int top_ = 0;
int visible_;
bool open_ = false;
};
} // namespace roro
+14
View File
@@ -99,6 +99,20 @@ void KeyMapper::onChar(char c, const RawKeys& keys, std::vector<KeyEvent>& out)
return;
}
break;
case 'h':
case 'H':
if (keys.fn) { // Fn+h: help, while typing too
out.push_back(KeyEvent::of(Key::Help));
return;
}
break;
case '?':
if (!textEntry_ && !keys.fn) { // ? alone, when it wouldn't be typed
out.push_back(KeyEvent::of(Key::Help));
return;
}
if (keys.fn) return;
break;
default:
if (keys.fn) return; // other Fn combos are unassigned
break;
+1 -1
View File
@@ -23,7 +23,7 @@ struct RawKeys {
// Turns keyboard state changes into logical KeyEvents: only newly pressed keys produce events;
// Fn + ; . , / are arrows, and so are ; . , / alone when no text is being entered; ` is Back and
// Fn + ` is Home; the Compose Key (opt) followed by an accent and a letter types the accented
// Fn + ` is Home; Fn + h is Help anywhere, and so is ? when no text is being entered; the Compose Key (opt) followed by an accent and a letter types the accented
// letter (opt ' e -> é).
class KeyMapper {
public:
+1
View File
@@ -44,6 +44,7 @@ const Definition kDefinitions[] = {
{"check_updates", Kind::Bool, 1, nullptr, 0, 1}, // on: it only looks, and says so (R1, Q165)
{"debug_on", Kind::Bool, 0, nullptr, 0, 1}, // off: nothing listens until the owner says so (Q189)
{"debug_token", Kind::String, 0, "", 0, 64}, // empty, or a valid token
{"help_told", Kind::Bool, 0, nullptr, 0, 1},
};
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
"every Setting needs a definition");
+1
View File
@@ -33,6 +33,7 @@ enum class Setting : uint8_t {
CheckUpdates, // bool: look for a newer release on Gitea once a day (R1, Q165)
DebugConsole, // bool: the Debug Console listens on Wi-Fi (ADR 0010, Q189: off unless switched on)
DebugToken, // string: its token, tidied (debug_auth.h); empty until the console is first switched on
HelpTold, // bool: this device has been told about the help key once (issue #69, Q201)
Count
};