Public Access
Screenshots: Fn+p on every screen (#83)
Fn+p saves the screen as it is to /screenshots, as the Shell's `screenshot` does, from anywhere: text fields, dialogs and the help panel included. The key never reaches an App. It refuses on Settings > Debug Console, which shows the token: a picture of that page is a copy of the token in a file (App::showsSecret). `key shot` presses it over the consoles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -168,7 +168,7 @@ The Shell App (docs/milestones/S1.md) runs the commands below on the device's ow
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `burst` | Publishes 5 Notifications at once |
|
||||
| `key up\|down\|left\|right\|select\|back\|home\|del\|tab\|space\|help`, or `key <char>` | Injects a key press (`help` is Fn+h: the keys of the screen that is showing). `ctrl-`, `alt-` and `shift-` before it hold that key: `key ctrl-down`, `key alt-up`, `key ctrl-b` |
|
||||
| `key up\|down\|left\|right\|select\|back\|home\|del\|tab\|space\|help\|shot`, or `key <char>` | Injects a key press (`help` is Fn+h: the keys of the screen that is showing; `shot` is Fn+p: a screenshot). `ctrl-`, `alt-` and `shift-` before it hold that key: `key ctrl-down`, `key alt-up`, `key ctrl-b` |
|
||||
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
||||
| `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s |
|
||||
| `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) |
|
||||
|
||||
@@ -44,3 +44,18 @@ The lists first lived in each App's `help()`, as code. They are now **data, in o
|
||||
Three rows lost their second wording on the way, since a table is constant: GNSS's `Tab` and `r`, and the Scanner's `c`, now say both things they do ("record a Track, or stop it") instead of the one that applies. The guide pages keep their written tables too, where they say more than a key list can; those can still drift, and the generated ones under them are the reference.
|
||||
|
||||
**Not checked:** the real Fn+h and `?` on the keyboard (the mapper is host-tested; the device was driven with `key help`); the Setup screens, which only a device that was never set up shows, so their new text has not been seen on a screen; and the states that need something to happen first (a dialog, a copy in progress, a Gemini prompt, a packet's details): their lists were read against the key handling, not looked at.
|
||||
|
||||
## The screenshot key (issue #83)
|
||||
|
||||
A screenshot could only be taken by typing `screenshot` in the Shell, where "now" is a picture of the Shell.
|
||||
|
||||
- **Fn+p, on every screen**, text fields included, saves the screen as it is (a dialog, the help panel or a Toast if one is showing) as a PNG in `/screenshots`, the way the Shell's command does. A Toast says so once the file is written, so it is never in the picture.
|
||||
- **It never reaches an App.** `Key::Screenshot` comes out of the key mapper and is handled before the App manager, so the help panel stays open and a dialog keeps its selection.
|
||||
- **Not on Settings > Debug Console:** that page shows the token, and a picture of it is a copy of the token in a file. `App::showsSecret()` says so, and the key answers with a Toast instead.
|
||||
- **No card:** a Toast says so.
|
||||
- No setting to switch it off: Fn with a letter isn't pressed by accident.
|
||||
- It is in the "Everywhere" group of the help panel, and so in the website's key tables. `key shot` presses it over the consoles.
|
||||
|
||||
**Checked on the device** (2026-10-07, with `key shot`): in the Launcher, a 33,383-byte PNG appears in `/screenshots` and is the Launcher; with the help panel open, the picture is of the panel (which now lists Fn p) and the panel stays open; on Settings > Debug Console, no file is written; back on the Settings list, one is. The test pictures were removed.
|
||||
|
||||
**Not checked:** the real Fn+p on the keyboard (the mapper is host-tested); the two Toasts that refuse, which weren't looked at (one of them is on the page that mustn't be photographed); a device with no card.
|
||||
|
||||
@@ -39,6 +39,10 @@ class App {
|
||||
|
||||
virtual void draw(Canvas& canvas) = 0;
|
||||
|
||||
// True while the screen shows something a picture of it shouldn't hold: the screenshot key
|
||||
// (Fn+p, issue #83) then refuses, and says so.
|
||||
virtual bool showsSecret() const { return false; }
|
||||
|
||||
// An App whose screen is costly to draw again (a picture decoded from the card, issue #45) can
|
||||
// keep what it drew: while this is true its part of the screen isn't cleared before draw(),
|
||||
// which then draws only what changed. contentLost() says that it was cleared after all, or
|
||||
|
||||
@@ -27,6 +27,7 @@ inline constexpr KeyHelp kEverywhere[] = {
|
||||
{"Fn `", "home, the Launcher"},
|
||||
{"; . , /", "arrows (Fn+ while typing)"},
|
||||
{"Fn h ?", "these keys (? not typing)"},
|
||||
{"Fn p", "a screenshot, on the card"},
|
||||
};
|
||||
|
||||
// dialog: A question
|
||||
|
||||
@@ -17,6 +17,7 @@ enum class Key : uint8_t {
|
||||
Tab,
|
||||
Delete,
|
||||
Help, // Fn+h anywhere, or ? outside Text Entry: the keys of this screen (issue #69)
|
||||
Screenshot, // Fn+p anywhere: the screen as a PNG on the card (issue #83). Never reaches an App
|
||||
};
|
||||
|
||||
struct KeyEvent {
|
||||
|
||||
@@ -106,6 +106,13 @@ void KeyMapper::onChar(char c, const RawKeys& keys, std::vector<KeyEvent>& out)
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
case 'P':
|
||||
if (keys.fn) { // Fn+p: a screenshot, while typing too
|
||||
out.push_back(KeyEvent::of(Key::Screenshot));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case '?':
|
||||
if (!textEntry_ && !keys.fn) { // ? alone, when it wouldn't be typed
|
||||
out.push_back(KeyEvent::of(Key::Help));
|
||||
|
||||
@@ -29,7 +29,7 @@ gnss quiet on|off pause the GNSS receiver while the LoRa radio listens (it cos
|
||||
gnss status | gnss restart | gnss track start|stop | gnss nmea on|off | gnss send <sentence without $ and checksum>
|
||||
crash the last crash: firmware, reason, task, backtrace
|
||||
coredump erase forget the core dump in flash
|
||||
key <name|char> press a key: up down left right select back home del tab space help, or one character; ctrl- alt- shift- before it (key ctrl-down)
|
||||
key <name|char> press a key: up down left right select back home del tab space help shot, or one character; ctrl- alt- shift- before it (key ctrl-down)
|
||||
wifi status | wifi add <ssid><TAB><password>
|
||||
wifi ip <ssid> dhcp | wifi ip <ssid> <address>/<prefix> [gateway] a Saved Network's IP setting
|
||||
wifi dns <a> [b] | wifi dns always on|off | wifi ntp <a> [b] DNS and NTP servers
|
||||
@@ -62,7 +62,7 @@ In **Safe Mode** (see [Crashes and Safe Mode](/dev/debug/crashes/)) only a few r
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `burst` | Publishes 5 Notifications at once |
|
||||
| `key up\|down\|left\|right\|select\|back\|home\|del\|tab\|space\|help`, or `key <char>` | Injects a key press (`help` is Fn+h: the keys of the screen that is showing). `ctrl-`, `alt-` and `shift-` before it hold that key: `key ctrl-down`, `key alt-up`, `key ctrl-b` |
|
||||
| `key up\|down\|left\|right\|select\|back\|home\|del\|tab\|space\|help\|shot`, or `key <char>` | Injects a key press (`help` is Fn+h: the keys of the screen that is showing; `shot` is Fn+p: a screenshot). `ctrl-`, `alt-` and `shift-` before it hold that key: `key ctrl-down`, `key alt-up`, `key ctrl-b` |
|
||||
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
||||
| `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s |
|
||||
| `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) |
|
||||
|
||||
@@ -11,7 +11,7 @@ Everything the keyboard can do, a command can do, and everything on the screen c
|
||||
## Keys
|
||||
|
||||
```
|
||||
key up|down|left|right|select|back|home|del|tab|space|help
|
||||
key up|down|left|right|select|back|home|del|tab|space|help|shot
|
||||
key a # any single character: it is typed
|
||||
```
|
||||
|
||||
|
||||
@@ -52,3 +52,18 @@ The lists first lived in each App's `help()`, as code. They are now **data, in o
|
||||
Three rows lost their second wording on the way, since a table is constant: GNSS's `Tab` and `r`, and the Scanner's `c`, now say both things they do ("record a Track, or stop it") instead of the one that applies. The guide pages keep their written tables too, where they say more than a key list can; those can still drift, and the generated ones under them are the reference.
|
||||
|
||||
**Not checked:** the real Fn+h and `?` on the keyboard (the mapper is host-tested; the device was driven with `key help`); the Setup screens, which only a device that was never set up shows, so their new text has not been seen on a screen; and the states that need something to happen first (a dialog, a copy in progress, a Gemini prompt, a packet's details): their lists were read against the key handling, not looked at.
|
||||
|
||||
## The screenshot key (issue #83)
|
||||
|
||||
A screenshot could only be taken by typing `screenshot` in the Shell, where "now" is a picture of the Shell.
|
||||
|
||||
- **Fn+p, on every screen**, text fields included, saves the screen as it is (a dialog, the help panel or a Toast if one is showing) as a PNG in `/screenshots`, the way the Shell's command does. A Toast says so once the file is written, so it is never in the picture.
|
||||
- **It never reaches an App.** `Key::Screenshot` comes out of the key mapper and is handled before the App manager, so the help panel stays open and a dialog keeps its selection.
|
||||
- **Not on Settings > Debug Console:** that page shows the token, and a picture of it is a copy of the token in a file. `App::showsSecret()` says so, and the key answers with a Toast instead.
|
||||
- **No card:** a Toast says so.
|
||||
- No setting to switch it off: Fn with a letter isn't pressed by accident.
|
||||
- It is in the "Everywhere" group of the help panel, and so in the website's key tables. `key shot` presses it over the consoles.
|
||||
|
||||
**Checked on the device** (2026-10-07, with `key shot`): in the Launcher, a 33,383-byte PNG appears in `/screenshots` and is the Launcher; with the help panel open, the picture is of the panel (which now lists Fn p) and the panel stays open; on Settings > Debug Console, no file is written; back on the Settings list, one is. The test pictures were removed.
|
||||
|
||||
**Not checked:** the real Fn+p on the keyboard (the mapper is host-tested); the two Toasts that refuse, which weren't looked at (one of them is on the page that mustn't be photographed); a device with no card.
|
||||
|
||||
@@ -26,6 +26,7 @@ The Cardputer's keyboard has no arrow keys and no Escape, so the firmware gives
|
||||
| <kbd>Fn</kbd> + <kbd>;</kbd> <kbd>.</kbd> <kbd>,</kbd> <kbd>/</kbd> | The arrows: up, down, left, right |
|
||||
| <kbd>;</kbd> <kbd>.</kbd> <kbd>,</kbd> <kbd>/</kbd> alone | The same arrows, as long as you are **not** typing text |
|
||||
| <kbd>Fn</kbd> + <kbd>h</kbd>, or <kbd>?</kbd> when not typing | **Help:** the keys of the screen you are on |
|
||||
| <kbd>Fn</kbd> + <kbd>p</kbd> | **A screenshot:** the screen as it is, saved as a picture in `/screenshots` on the SD card |
|
||||
| <kbd>Tab</kbd> | Switches view in an App that has more than one |
|
||||
| <kbd>Del</kbd> | Deletes backwards when you type |
|
||||
| <kbd>opt</kbd> then an accent, then a letter | Types an accented letter: <kbd>opt</kbd> <kbd>'</kbd> <kbd>e</kbd> gives é |
|
||||
|
||||
@@ -77,7 +77,7 @@ screenshot the screen, now
|
||||
screenshot 5 the screen in 5 seconds: time to go to another App
|
||||
```
|
||||
|
||||
The picture is saved as a PNG in `/screenshots` on the SD card, named by date and time, and a Toast says so once it is written (so the Toast is never in the picture). From the Shell, "now" is always a picture of the Shell: use the pause to get to the screen you want. The [Storage App](/guide/storage/) shows the files; to look at them, take the card to a computer.
|
||||
The picture is saved as a PNG in `/screenshots` on the SD card, named by date and time, and a Toast says so once it is written (so the Toast is never in the picture). From the Shell, "now" is always a picture of the Shell: use the pause to get to the screen you want, or, simpler, press <kbd>Fn</kbd> + <kbd>p</kbd> on that screen: it takes the same picture from anywhere. The [Storage App](/guide/storage/) lists the files and [shows them](/guide/storage/#pictures).
|
||||
|
||||
## What it costs
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ rows = [
|
||||
["Fn `", "home, the Launcher"],
|
||||
["; . , /", "arrows (Fn+ while typing)"],
|
||||
["Fn h ?", "these keys (? not typing)"],
|
||||
["Fn p", "a screenshot, on the card"],
|
||||
]
|
||||
|
||||
[[scope]]
|
||||
|
||||
@@ -52,6 +52,7 @@ class SettingsApp : public App {
|
||||
void draw(Canvas& c) override;
|
||||
void help(std::vector<KeyHelp>& out) const override;
|
||||
const char* helpTitle() const override;
|
||||
bool showsSecret() const override { return page_ == Page::Debug; } // the Debug Console's token
|
||||
|
||||
private:
|
||||
enum class Page { Menu, Text, Choice, About, Wifi, Firmware, Debug };
|
||||
|
||||
@@ -26,7 +26,7 @@ void ShellApp::onEnter() {
|
||||
scroll_ = 0;
|
||||
confirm_.reset();
|
||||
// What Tab completes besides the firmware's own commands, written as `help` writes them.
|
||||
ownHelp_ = "help | clear | quit | exit\nkey up|down|left|right|select|back|home|del|tab|space|help\n";
|
||||
ownHelp_ = "help | clear | quit | exit\nkey up|down|left|right|select|back|home|del|tab|space|help|shot\n";
|
||||
log_.clear();
|
||||
log_.add(open_ ? "The console's commands. `help` lists them." : "No memory for the Shell: leave an App, or stop IRC.");
|
||||
}
|
||||
|
||||
+24
-4
@@ -656,7 +656,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 help, or one character; ctrl- alt- shift- before it (key ctrl-down)\n"
|
||||
"key <name|char> press a key: up down left right select back home del tab space help shot, or one character; ctrl- alt- shift- before it (key ctrl-down)\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"
|
||||
@@ -725,6 +725,26 @@ static void saveScreenshot() {
|
||||
});
|
||||
}
|
||||
|
||||
// Fn+p (issue #83): the screen as it is, dialog, help panel or Toast included. Not the page that
|
||||
// shows the Debug Console's token: a picture of it is a copy of the token in a file.
|
||||
static void screenshotKey() {
|
||||
const char* why = nullptr;
|
||||
if (apps->foreground().showsSecret()) why = "No screenshot here: it shows the token";
|
||||
else if (!storageService || !storageService->state().present) why = "No SD card for the screenshot";
|
||||
if (why) {
|
||||
bus.publish(Event::withText(EventType::Notification, why, static_cast<int32_t>(NotificationLevel::Warning)));
|
||||
return;
|
||||
}
|
||||
shotFrom = Console::Origin::System;
|
||||
saveScreenshot();
|
||||
}
|
||||
|
||||
// Every key goes through here, from the keyboard or from a console's `key`.
|
||||
static void handleKey(const KeyEvent& e) {
|
||||
if (e.key == Key::Screenshot) return screenshotKey();
|
||||
apps->handleKey(e);
|
||||
}
|
||||
|
||||
static void screenshotStep() {
|
||||
if (shotPending && static_cast<int32_t>(millis() - shotDueMs) >= 0) {
|
||||
shotPending = false;
|
||||
@@ -1039,14 +1059,14 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
}
|
||||
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 : k == "help" ? Key::Help : Key::Select;
|
||||
: k == "del" ? Key::Delete : k == "tab" ? Key::Tab : k == "help" ? Key::Help : k == "shot" ? Key::Screenshot : Key::Select;
|
||||
KeyEvent ev = k == "space" ? KeyEvent::character(' ')
|
||||
: k.length() == 1 && k[0] > ' ' ? KeyEvent::character((unsigned char)k[0])
|
||||
: KeyEvent::of(key);
|
||||
ev.ctrl = ctrl;
|
||||
ev.alt = alt;
|
||||
ev.shift = shift;
|
||||
if (!power->onKey(millis())) apps->handleKey(ev);
|
||||
if (!power->onKey(millis())) handleKey(ev);
|
||||
}
|
||||
if (line.startsWith("wifi add ")) { // wifi add <ssid>\t<password>: credentials never touch the repo
|
||||
int tab = line.indexOf('\t');
|
||||
@@ -1385,7 +1405,7 @@ static void loopPass() {
|
||||
keyMapper.setTextEntry(apps->foreground().textEntryActive());
|
||||
auto events = keyMapper.update(readKeys()); // always, so held keys are tracked
|
||||
if (!swallow)
|
||||
for (auto& e : events) apps->handleKey(e);
|
||||
for (auto& e : events) handleKey(e);
|
||||
}
|
||||
|
||||
// Issue #20: the GNSS receiver costs the LoRa radio 8 dB while it runs. If the user chose so,
|
||||
|
||||
@@ -231,6 +231,22 @@ void test_fn_h_is_help_in_both_modes() {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #83: Fn+p everywhere.
|
||||
void test_fn_p_is_a_screenshot_in_both_modes() {
|
||||
for (bool typing : {true, false}) {
|
||||
KeyMapper m;
|
||||
m.setTextEntry(typing);
|
||||
auto ev = press(m, withFn({'p'}));
|
||||
TEST_ASSERT_EQUAL(1, ev.size());
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Screenshot), static_cast<int>(ev[0].key));
|
||||
release(m);
|
||||
ev = press(m, chars({'p'}));
|
||||
TEST_ASSERT_EQUAL(1, ev.size());
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Char), static_cast<int>(ev[0].key));
|
||||
TEST_ASSERT_EQUAL('p', ev[0].ch);
|
||||
}
|
||||
}
|
||||
|
||||
void test_plain_h_still_types() {
|
||||
for (bool typing : {true, false}) {
|
||||
KeyMapper m;
|
||||
@@ -278,6 +294,7 @@ int main() {
|
||||
RUN_TEST(test_other_characters_still_type_when_not_typing);
|
||||
RUN_TEST(test_shifted_arrow_keys_type_their_symbols_when_not_typing);
|
||||
RUN_TEST(test_fn_h_is_help_in_both_modes);
|
||||
RUN_TEST(test_fn_p_is_a_screenshot_in_both_modes);
|
||||
RUN_TEST(test_plain_h_still_types);
|
||||
RUN_TEST(test_question_mark_is_help_only_when_not_typing);
|
||||
return UNITY_END();
|
||||
|
||||
Reference in New Issue
Block a user