Public Access
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe8b3d22a | ||
|
|
1c6ae0e04f |
@@ -106,6 +106,10 @@ The regulatory band plan the device transmits under (here EU868). It sets the al
|
|||||||
**Duty Cycle Budget**:
|
**Duty Cycle Budget**:
|
||||||
The share of airtime the Region allows this device to transmit. When it's used up, outgoing traffic waits.
|
The share of airtime the Region allows this device to transmit. When it's used up, outgoing traffic waits.
|
||||||
|
|
||||||
|
**Help panel**:
|
||||||
|
The list of the keys that work on the screen you are on, opened with Fn+h anywhere (or `?` outside Text Entry). Each App answers for its current state; no screen names keys any other way, except the first-start Setup.
|
||||||
|
_Avoid_: hints, cheat sheet, shortcuts bar
|
||||||
|
|
||||||
**Text Entry**:
|
**Text Entry**:
|
||||||
When an App is editing text. During Text Entry, `;` `.` `,` `/` type their characters and Fn makes them arrows. Otherwise they are arrows on their own.
|
When an App is editing text. During Text Entry, `;` `.` `,` `/` type their characters and Fn makes them arrows. Otherwise they are arrows on their own.
|
||||||
_Avoid_: edit mode, insert mode
|
_Avoid_: edit mode, insert mode
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ A multi-app firmware for the **M5Stack Cardputer ADV** with the **Cap LoRa-1262*
|
|||||||
- Decisions: [docs/adr/](docs/adr/)
|
- Decisions: [docs/adr/](docs/adr/)
|
||||||
- Milestones: [docs/milestones/](docs/milestones/)
|
- Milestones: [docs/milestones/](docs/milestones/)
|
||||||
|
|
||||||
|
## On the device: one key
|
||||||
|
|
||||||
|
**Fn+h, on any screen, lists the keys that work there** (`?` does the same outside a text field). No screen names its keys itself (docs/milestones/U1.md): each App declares them for the state it is in, and the help panel shows them, followed by the ones that work everywhere.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Only **Docker** is needed. PlatformIO and the ESP32 toolchain run inside a container, and are cached in the `roro9stack-pio` Docker volume. The first build downloads about 1 GB and takes a few minutes.
|
Only **Docker** is needed. PlatformIO and the ESP32 toolchain run inside a container, and are cached in the `roro9stack-pio` Docker volume. The first build downloads about 1 GB and takes a few minutes.
|
||||||
@@ -159,7 +163,7 @@ A note holds up to 16 KB while it's edited. A bigger text file opens read-only i
|
|||||||
| Command | Effect |
|
| Command | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `burst` | Publishes 5 Notifications at once |
|
| `burst` | Publishes 5 Notifications at once |
|
||||||
| `key up\|down\|left\|right\|select\|back\|home`, or `key <char>` | Injects a key press |
|
| `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) |
|
||||||
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
||||||
| `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s |
|
| `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) |
|
| `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) |
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# U1 — Look and feel
|
||||||
|
|
||||||
|
**Status:** in progress. The help key (issue #69) is built and checked on the device, in a pull request. Screen recording (#17) and the rest of the milestone are not started.
|
||||||
|
|
||||||
|
**Goal:** the interface is consistent and uncrowded: the same thing is done the same way on every screen, and the 135 pixels of height go to content.
|
||||||
|
|
||||||
|
## The help key (issue #69)
|
||||||
|
|
||||||
|
Every screen used to say something about its keys, differently: a footer of abbreviations in one place (`c x v:paste r:name d:del n:new i:info s:sort`), a line under a text field in another (`Enter: save \`: cancel`), `Tab: sky` in a corner, and nothing at all in several. About 30 such strings, each costing a line of a small screen, and none of them complete.
|
||||||
|
|
||||||
|
### Decisions (design round 2026-10-07)
|
||||||
|
|
||||||
|
| # | Decision |
|
||||||
|
|---|---|
|
||||||
|
| Q196 | **Fn+h, on every screen,** text fields included (Fn is held, so nothing is typed). **`?` too, outside Text Entry.** |
|
||||||
|
| Q197 | It opens **a panel over the content area**, titled with where you are: the screen's own keys, then an "Everywhere" group (Back, Home, the arrows, the help key). The arrows scroll it; any other key closes it and is not passed on. |
|
||||||
|
| Q198 | **Each App answers "what are your keys right now?"** for the state it is in; pages, viewers, dialogs and text fields answer for themselves, with shared lists for dialogs, lists and text entry. The lists are constants; the panel's rows exist only while it is open. |
|
||||||
|
| Q199 | **Every hint that names a key goes,** text fields included. What stays is state: `REC 12 points`, `LOG 42`, `sort:signal`, `typing`/`saved`, what is waiting to be pasted, the Sweep's floor. Messages were reworded where they named a key ("v pastes a copy of…" is "Copied …: paste it where you like"). |
|
||||||
|
| Q200 | **The first-start Setup keeps its hints,** and is the one place that does: someone in their first minute doesn't know the help key yet. It tells them about it on its first and last screens. |
|
||||||
|
| Q201 | **Loud everywhere else:** the user guide opens with it, the FAQ has it first, and a device set up before this firmware gets one Toast, once: "Fn+h: the keys of any screen". |
|
||||||
|
| Q202 | `key help` over the consoles. Generating the website's key tables from the same lists is a follow-up, not this issue. |
|
||||||
|
| Q203 | The key and the panel first, host-tested; then one App at a time, declaring its keys and losing its hints in the same step; then every screen looked at on the device. |
|
||||||
|
|
||||||
|
### As built
|
||||||
|
|
||||||
|
- **`Key::Help`** from the key mapper: Fn+h in both modes, `?` only outside Text Entry (`lib/input`, 3 tests).
|
||||||
|
- **`App::help()` and `App::helpTitle()`** (`lib/core/src/app.h`), `KeyHelp` rows and `HelpModel` (`key_help.h`). The **App manager** opens the panel, appends the "Everywhere" group, and while it is open takes every key: nothing reaches the App, Home included. It closes when the App changes (5 tests).
|
||||||
|
- **Every App declares its keys by state:** the Launcher, IRC (chat, settings, a field), Wi-Fi Tools (4 views), GNSS, Gemini (page, saved page, address, answer, dialogs), the LoRa Scanner (4 views), Storage (browse, details, a name, the viewer's 6 modes, the editor, Maintenance, busy), Notes (list, editor, a file name), System (5 views), Settings (menu, text, choice, and the Wi-Fi, Firmware and Debug Console pages with their own states), Setup and the widget demo.
|
||||||
|
- **The hints are gone** from all of them. The footers that remain say state only.
|
||||||
|
- **It costs** 8.5 KB of flash and 40 bytes of static RAM.
|
||||||
|
|
||||||
|
### Checks
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| Host tests | 476 pass (468 before) |
|
||||||
|
| On the device, `key help` and a screenshot | The Launcher and all nine Apps, and these states: Storage scrolled, System's tasks, a Settings text field, Wi-Fi Tools' networks. The panel is titled with the scope, lists the right keys, scrolls, and closes on Tab |
|
||||||
|
| The one-time Toast | `notification: Fn+h: the keys of any screen` on the first start after the update |
|
||||||
|
|
||||||
|
**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.
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "key_event.h"
|
#include "key_event.h"
|
||||||
|
#include "key_help.h"
|
||||||
|
|
||||||
namespace roro {
|
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.
|
// True while the App is editing text: the arrow keys then type ; . , / and need Fn to move.
|
||||||
virtual bool textEntryActive() const { return false; }
|
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).
|
// Called every main-loop pass while in the foreground (e.g. to refresh live values).
|
||||||
virtual void update(uint32_t nowMs) { (void)nowMs; }
|
virtual void update(uint32_t nowMs) { (void)nowMs; }
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,21 @@ void AppManager::endModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AppManager::handleKey(const KeyEvent& event) {
|
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_) {
|
if (modal_) {
|
||||||
foreground_->onKey(event);
|
foreground_->onKey(event);
|
||||||
return;
|
return;
|
||||||
@@ -77,6 +92,7 @@ bool AppManager::takeRedraw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AppManager::switchTo(App& app) {
|
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;
|
if (&app == foreground_) return;
|
||||||
foreground_->onExit();
|
foreground_->onExit();
|
||||||
foreground_ = &app;
|
foreground_ = &app;
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class AppManager {
|
|||||||
App& foreground() const { return *foreground_; }
|
App& foreground() const { return *foreground_; }
|
||||||
const char* foregroundTitle() const; // nullptr for the Launcher
|
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).
|
// True once after the screen needs redrawing (App switch, or the App asked for it).
|
||||||
bool takeRedraw();
|
bool takeRedraw();
|
||||||
|
|
||||||
@@ -47,6 +50,7 @@ class AppManager {
|
|||||||
App& launcher_;
|
App& launcher_;
|
||||||
App* foreground_;
|
App* foreground_;
|
||||||
std::vector<AppInfo> apps_;
|
std::vector<AppInfo> apps_;
|
||||||
|
HelpModel help_;
|
||||||
bool redraw_ = true;
|
bool redraw_ = true;
|
||||||
bool modal_ = false;
|
bool modal_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ enum class Key : uint8_t {
|
|||||||
Home,
|
Home,
|
||||||
Tab,
|
Tab,
|
||||||
Delete,
|
Delete,
|
||||||
|
Help, // Fn+h anywhere, or ? outside Text Entry: the keys of this screen (issue #69)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct KeyEvent {
|
struct KeyEvent {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -99,6 +99,20 @@ void KeyMapper::onChar(char c, const RawKeys& keys, std::vector<KeyEvent>& out)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
if (keys.fn) return; // other Fn combos are unassigned
|
if (keys.fn) return; // other Fn combos are unassigned
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct RawKeys {
|
|||||||
|
|
||||||
// Turns keyboard state changes into logical KeyEvents: only newly pressed keys produce events;
|
// 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 + ; . , / 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 -> é).
|
// letter (opt ' e -> é).
|
||||||
class KeyMapper {
|
class KeyMapper {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const Definition kDefinitions[] = {
|
|||||||
{"check_updates", Kind::Bool, 1, nullptr, 0, 1}, // on: it only looks, and says so (R1, Q165)
|
{"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_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
|
{"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),
|
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
|
||||||
"every Setting needs a definition");
|
"every Setting needs a definition");
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ enum class Setting : uint8_t {
|
|||||||
CheckUpdates, // bool: look for a newer release on Gitea once a day (R1, Q165)
|
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)
|
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
|
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
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,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>
|
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
|
crash the last crash: firmware, reason, task, backtrace
|
||||||
coredump erase forget the core dump in flash
|
coredump erase forget the core dump in flash
|
||||||
key <name|char> press a key: up down left right select back home del tab space, or one character
|
key <name|char> press a key: up down left right select back home del tab space help, or one character
|
||||||
wifi status | wifi add <ssid><TAB><password>
|
wifi status | wifi add <ssid><TAB><password>
|
||||||
wifi ip <ssid> dhcp | wifi ip <ssid> <address>/<prefix> [gateway] a Saved Network's IP setting
|
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
|
wifi dns <a> [b] | wifi dns always on|off | wifi ntp <a> [b] DNS and NTP servers
|
||||||
@@ -60,7 +60,7 @@ In **Safe Mode** (see [Crashes and Safe Mode](/dev/debug/crashes/)) only a few r
|
|||||||
| Command | Effect |
|
| Command | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `burst` | Publishes 5 Notifications at once |
|
| `burst` | Publishes 5 Notifications at once |
|
||||||
| `key up\|down\|left\|right\|select\|back\|home`, or `key <char>` | Injects a key press |
|
| `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) |
|
||||||
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
||||||
| `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s |
|
| `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) |
|
| `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
|
## Keys
|
||||||
|
|
||||||
```
|
```
|
||||||
key up|down|left|right|select|back|home|del|tab|space
|
key up|down|left|right|select|back|home|del|tab|space|help
|
||||||
key a # any single character: it is typed
|
key a # any single character: it is typed
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@ Two things to know before you use them:
|
|||||||
|
|
||||||
`Fn` combinations, modifiers and the compose key have no command: the arrows are `key up|down|left|right`, and `key back` is the back key (`` ` `` on the device). Text is typed one character at a time.
|
`Fn` combinations, modifiers and the compose key have no command: the arrows are `key up|down|left|right`, and `key back` is the back key (`` ` `` on the device). Text is typed one character at a time.
|
||||||
|
|
||||||
|
**`key help` opens the help panel** (<kbd>Fn</kbd>+<kbd>h</kbd> on the device): the keys of the screen that is showing. A screenshot of it is the quickest way to learn what a screen accepts, and it is how every screen's list was checked. Any key but the arrows closes it.
|
||||||
|
|
||||||
## Look before you press
|
## Look before you press
|
||||||
|
|
||||||
**Take a screenshot before any key that deletes, renames or installs.** A blind sequence of `key` commands goes wrong the moment the screen is not where you think it is, and the screen is often not where you think it is: a Toast, a dialog that has not closed, a different App. A sequence that was meant to open a note once renamed real data instead.
|
**Take a screenshot before any key that deletes, renames or installs.** A blind sequence of `key` commands goes wrong the moment the screen is not where you think it is, and the screen is often not where you think it is: a Toast, a dialog that has not closed, a different App. A sequence that was meant to open a note once renamed real data instead.
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
+++
|
||||||
|
title = "Look and feel"
|
||||||
|
description = "The interface is consistent and uncrowded: the same thing is done the same way on every screen, and the 135 pixels of height go to content."
|
||||||
|
weight = 90
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
docs = true
|
||||||
|
source = "docs/milestones/U1.md"
|
||||||
|
tag = "U1"
|
||||||
|
+++
|
||||||
|
**Status:** in progress. The help key (issue #69) is built and checked on the device, in a pull request. Screen recording (#17) and the rest of the milestone are not started.
|
||||||
|
|
||||||
|
**Goal:** the interface is consistent and uncrowded: the same thing is done the same way on every screen, and the 135 pixels of height go to content.
|
||||||
|
|
||||||
|
## The help key (issue #69)
|
||||||
|
|
||||||
|
Every screen used to say something about its keys, differently: a footer of abbreviations in one place (`c x v:paste r:name d:del n:new i:info s:sort`), a line under a text field in another (`Enter: save \`: cancel`), `Tab: sky` in a corner, and nothing at all in several. About 30 such strings, each costing a line of a small screen, and none of them complete.
|
||||||
|
|
||||||
|
### Decisions (design round 2026-10-07)
|
||||||
|
|
||||||
|
| # | Decision |
|
||||||
|
|---|---|
|
||||||
|
| Q196 | **Fn+h, on every screen,** text fields included (Fn is held, so nothing is typed). **`?` too, outside Text Entry.** |
|
||||||
|
| Q197 | It opens **a panel over the content area**, titled with where you are: the screen's own keys, then an "Everywhere" group (Back, Home, the arrows, the help key). The arrows scroll it; any other key closes it and is not passed on. |
|
||||||
|
| Q198 | **Each App answers "what are your keys right now?"** for the state it is in; pages, viewers, dialogs and text fields answer for themselves, with shared lists for dialogs, lists and text entry. The lists are constants; the panel's rows exist only while it is open. |
|
||||||
|
| Q199 | **Every hint that names a key goes,** text fields included. What stays is state: `REC 12 points`, `LOG 42`, `sort:signal`, `typing`/`saved`, what is waiting to be pasted, the Sweep's floor. Messages were reworded where they named a key ("v pastes a copy of…" is "Copied …: paste it where you like"). |
|
||||||
|
| Q200 | **The first-start Setup keeps its hints,** and is the one place that does: someone in their first minute doesn't know the help key yet. It tells them about it on its first and last screens. |
|
||||||
|
| Q201 | **Loud everywhere else:** the user guide opens with it, the FAQ has it first, and a device set up before this firmware gets one Toast, once: "Fn+h: the keys of any screen". |
|
||||||
|
| Q202 | `key help` over the consoles. Generating the website's key tables from the same lists is a follow-up, not this issue. |
|
||||||
|
| Q203 | The key and the panel first, host-tested; then one App at a time, declaring its keys and losing its hints in the same step; then every screen looked at on the device. |
|
||||||
|
|
||||||
|
### As built
|
||||||
|
|
||||||
|
- **`Key::Help`** from the key mapper: Fn+h in both modes, `?` only outside Text Entry (`lib/input`, 3 tests).
|
||||||
|
- **`App::help()` and `App::helpTitle()`** (`lib/core/src/app.h`), `KeyHelp` rows and `HelpModel` (`key_help.h`). The **App manager** opens the panel, appends the "Everywhere" group, and while it is open takes every key: nothing reaches the App, Home included. It closes when the App changes (5 tests).
|
||||||
|
- **Every App declares its keys by state:** the Launcher, IRC (chat, settings, a field), Wi-Fi Tools (4 views), GNSS, Gemini (page, saved page, address, answer, dialogs), the LoRa Scanner (4 views), Storage (browse, details, a name, the viewer's 6 modes, the editor, Maintenance, busy), Notes (list, editor, a file name), System (5 views), Settings (menu, text, choice, and the Wi-Fi, Firmware and Debug Console pages with their own states), Setup and the widget demo.
|
||||||
|
- **The hints are gone** from all of them. The footers that remain say state only.
|
||||||
|
- **It costs** 8.5 KB of flash and 40 bytes of static RAM.
|
||||||
|
|
||||||
|
### Checks
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| Host tests | 476 pass (468 before) |
|
||||||
|
| On the device, `key help` and a screenshot | The Launcher and all nine Apps, and these states: Storage scrolled, System's tasks, a Settings text field, Wi-Fi Tools' networks. The panel is titled with the scope, lists the right keys, scrolls, and closes on Tab |
|
||||||
|
| The one-time Toast | `notification: Fn+h: the keys of any screen` on the first start after the update |
|
||||||
|
|
||||||
|
**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.
|
||||||
@@ -6,6 +6,10 @@ template = "guide-page.html"
|
|||||||
toc = true
|
toc = true
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
## Which keys work on this screen?
|
||||||
|
|
||||||
|
Press **<kbd>Fn</kbd> + <kbd>h</kbd>**, on any screen: it lists the keys that work there, then the ones that work everywhere. <kbd>?</kbd> does the same when you are not typing text. The screens themselves never name their keys, so this is the one key to remember. See [The basics](/guide/basics/).
|
||||||
|
|
||||||
## Can I send messages over the mesh?
|
## Can I send messages over the mesh?
|
||||||
|
|
||||||
**Not yet.** The LoRa Scanner **listens** to Meshtastic traffic and shows what it hears, but nothing is transmitted. A mesh messenger is the goal and is planned in two milestones, one for receiving and one for transmitting; it waits for a second node to test against.
|
**Not yet.** The LoRa Scanner **listens** to Meshtastic traffic and shows what it hears, but nothing is transmitted. A mesh messenger is the goal and is planned in two milestones, one for receiving and one for transmitting; it waits for a second node to test against.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ sort_by = "weight"
|
|||||||
page_template = "guide-page.html"
|
page_template = "guide-page.html"
|
||||||
+++
|
+++
|
||||||
|
|
||||||
This guide says what the firmware does **today** and nothing else. Start with the basics (the keys, the Launcher, the first start), then read the page of any App. It describes the latest release; the numbers and key names come from the firmware's own source.
|
This guide says what the firmware does **today** and nothing else. Start with the basics (the keys, the Launcher, the first start), then read the page of any App. **On the device itself, <kbd>Fn</kbd> + <kbd>h</kbd> lists the keys of whatever screen you are on:** nothing else on a screen names them. It describes the latest release; the numbers and key names come from the firmware's own source.
|
||||||
|
|
||||||
**The mesh messenger is planned, not built.** The LoRa Scanner listens to Meshtastic traffic and shows it, but the device sends nothing yet: that is the next milestone and waits for a second node to test with.
|
**The mesh messenger is planned, not built.** The LoRa Scanner listens to Meshtastic traffic and shows it, but the device sends nothing yet: that is the next milestone and waits for a second node to test with.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ weight = 1
|
|||||||
tag = "Start here"
|
tag = "Start here"
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
## One key to remember
|
||||||
|
|
||||||
|
**<kbd>Fn</kbd> + <kbd>h</kbd>, on any screen, lists the keys that work there.** No screen names its keys: that key does. It works everywhere, in a text field too, and <kbd>?</kbd> does the same whenever you are not typing. The arrows scroll the list; any other key closes it.
|
||||||
|
|
||||||
|
The list is for *the screen you are on*: in Storage it is the file keys, in a dialog it is the dialog's, in a text field it is the editing keys. Each list ends with the keys that work everywhere.
|
||||||
|
|
||||||
|
If you only read one paragraph of this guide, this was it.
|
||||||
|
|
||||||
## The keys
|
## The keys
|
||||||
|
|
||||||
The Cardputer's keyboard has no arrow keys and no Escape, so the firmware gives a few keys a second job:
|
The Cardputer's keyboard has no arrow keys and no Escape, so the firmware gives a few keys a second job:
|
||||||
@@ -17,11 +25,12 @@ The Cardputer's keyboard has no arrow keys and no Escape, so the firmware gives
|
|||||||
| <kbd>Fn</kbd> + <kbd>`</kbd> | **Home**: back to the Launcher |
|
| <kbd>Fn</kbd> + <kbd>`</kbd> | **Home**: back to the Launcher |
|
||||||
| <kbd>Fn</kbd> + <kbd>;</kbd> <kbd>.</kbd> <kbd>,</kbd> <kbd>/</kbd> | The arrows: up, down, left, right |
|
| <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>;</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>Tab</kbd> | Switches view in an App that has more than one |
|
| <kbd>Tab</kbd> | Switches view in an App that has more than one |
|
||||||
| <kbd>Del</kbd> | Deletes backwards when you type |
|
| <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 é |
|
| <kbd>opt</kbd> then an accent, then a letter | Types an accented letter: <kbd>opt</kbd> <kbd>'</kbd> <kbd>e</kbd> gives é |
|
||||||
|
|
||||||
While you type text (a note, an IRC line, a setting), `;` `.` `,` `/` type their own characters and you need <kbd>Fn</kbd> for the arrows. The bottom of the screen shows `opt` while a compose is waiting for its letter.
|
While you type text (a note, an IRC line, a setting), `;` `.` `,` `/` type their own characters and you need <kbd>Fn</kbd> for the arrows. The Status Bar shows `opt` while a compose is waiting for its letter.
|
||||||
|
|
||||||
## The Launcher
|
## The Launcher
|
||||||
|
|
||||||
@@ -50,7 +59,7 @@ News from a background service (an IRC mention, a Storage warning, an update tha
|
|||||||
|
|
||||||
## The first start
|
## The first start
|
||||||
|
|
||||||
On a new device a short Setup asks four things, then never appears again:
|
On a new device a short Setup asks four things, then never appears again. It also tells you about <kbd>Fn</kbd> + <kbd>h</kbd>, twice, and it is the only part of the firmware that names keys on the screen:
|
||||||
|
|
||||||
1. **Long name**, up to 39 bytes.
|
1. **Long name**, up to 39 bytes.
|
||||||
2. **Short name**, up to 4 characters.
|
2. **Short name**, up to 4 characters.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Storage shows what is on the SD card: each folder's entries with their size and
|
|||||||
|
|
||||||
| Key | Does |
|
| Key | Does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| <kbd>c</kbd> / <kbd>x</kbd> | Copies or cuts the selected file or folder; the footer shows what <kbd>v</kbd> would paste |
|
| <kbd>c</kbd> / <kbd>x</kbd> | Copies or cuts the selected file or folder; the footer shows what is waiting to be pasted |
|
||||||
| <kbd>v</kbd> | Pastes it into the folder shown. A copy next to its original is named `name (2).txt`; anything in the way is asked about first |
|
| <kbd>v</kbd> | Pastes it into the folder shown. A copy next to its original is named `name (2).txt`; anything in the way is asked about first |
|
||||||
| <kbd>r</kbd> | Renames |
|
| <kbd>r</kbd> | Renames |
|
||||||
| <kbd>d</kbd> | Deletes, after saying what is inside: "Delete saved and its 42 files (1.2 MB)?" |
|
| <kbd>d</kbd> | Deletes, after saying what is inside: "Delete saved and its 42 files (1.2 MB)?" |
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ REPO = SITE.parent
|
|||||||
OUT = SITE / "content" / "dev"
|
OUT = SITE / "content" / "dev"
|
||||||
REPO_URL = re.search(r'repo\s*=\s*"([^"]+)"', (SITE / "config.toml").read_text()).group(1)
|
REPO_URL = re.search(r'repo\s*=\s*"([^"]+)"', (SITE / "config.toml").read_text()).group(1)
|
||||||
|
|
||||||
MILESTONES = ["OTA", "M2", "G1", "M3", "S1", "F1", "R1", "W1"] # in the order they were done
|
MILESTONES = ["OTA", "M2", "G1", "M3", "S1", "F1", "R1", "W1", "U1"] # in the order they were done
|
||||||
# Left out on purpose: docs/milestones/M0.md, M1.md and CONTEXT.md (the glossary) describe Wi-Fi monitoring, which this site does not publish.
|
# Left out on purpose: docs/milestones/M0.md, M1.md and CONTEXT.md (the glossary) describe Wi-Fi monitoring, which this site does not publish.
|
||||||
# They stay in the repository.
|
# They stay in the repository.
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ bool DebugConsolePage::onKey(const KeyEvent& e) {
|
|||||||
return true;
|
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) {
|
void DebugConsolePage::draw(Canvas& c) {
|
||||||
const auto& area = theme::kContent;
|
const auto& area = theme::kContent;
|
||||||
c.setTextDatum(top_left);
|
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});
|
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
||||||
c.setTextColor(refusal_.empty() ? theme::kMuted : theme::kWarning);
|
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.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "dialog_model.h"
|
#include "dialog_model.h"
|
||||||
#include "key_event.h"
|
#include "key_event.h"
|
||||||
|
#include "key_help.h"
|
||||||
#include "line_editor.h"
|
#include "line_editor.h"
|
||||||
#include "list_model.h"
|
#include "list_model.h"
|
||||||
#include "services/wifi_service.h"
|
#include "services/wifi_service.h"
|
||||||
@@ -25,6 +26,8 @@ class DebugConsolePage {
|
|||||||
bool onKey(const KeyEvent& e); // false: leave the page
|
bool onKey(const KeyEvent& e); // false: leave the page
|
||||||
bool textEntryActive() const { return typing_; }
|
bool textEntryActive() const { return typing_; }
|
||||||
void draw(Canvas& c);
|
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:
|
private:
|
||||||
enum Row { kSwitch, kAddress, kNewToken, kTypeToken, kRows };
|
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)));
|
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) {
|
bool DemoApp::onKey(const KeyEvent& e) {
|
||||||
requestRedraw();
|
requestRedraw();
|
||||||
if (dialog_) {
|
if (dialog_) {
|
||||||
@@ -97,7 +106,7 @@ void DemoApp::draw(Canvas& c) {
|
|||||||
}
|
}
|
||||||
case Page::Editor:
|
case Page::Editor:
|
||||||
c.setTextColor(theme::kMuted);
|
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});
|
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
||||||
c.setTextColor(theme::kMuted);
|
c.setTextColor(theme::kMuted);
|
||||||
c.drawString((std::to_string(editor_.text().size()) + "/39 bytes").c_str(), 4, area.y + 44);
|
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 onKey(const KeyEvent& e) override;
|
||||||
bool textEntryActive() const override { return page_ == Page::Editor && !dialog_; }
|
bool textEntryActive() const override { return page_ == Page::Editor && !dialog_; }
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Page { Menu, Text, Editor };
|
enum class Page { Menu, Text, Editor };
|
||||||
|
|||||||
@@ -265,6 +265,30 @@ void FileViewer::openPacket(int index) {
|
|||||||
mode_ = Mode::Packet;
|
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) {
|
bool FileViewer::onKey(const KeyEvent& e) {
|
||||||
if (confirm_) {
|
if (confirm_) {
|
||||||
confirm_->onKey(e);
|
confirm_->onKey(e);
|
||||||
@@ -380,7 +404,7 @@ void FileViewer::draw(Canvas& c) {
|
|||||||
refresh();
|
refresh();
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
theme::Rect body{area.x, area.y + 10, area.w, kRows * theme::kLineHeight};
|
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;
|
bool scanning = scan_ && !scan_->done;
|
||||||
|
|
||||||
switch (mode_) {
|
switch (mode_) {
|
||||||
@@ -396,8 +420,6 @@ void FileViewer::draw(Canvas& c) {
|
|||||||
c.fillRect(area.w - 2, barY, 2, barH, theme::kMuted);
|
c.fillRect(area.w - 2, barY, 2, barH, theme::kMuted);
|
||||||
}
|
}
|
||||||
right = formatBytes(size_);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case Mode::Gpx: {
|
case Mode::Gpx: {
|
||||||
@@ -407,7 +429,6 @@ void FileViewer::draw(Canvas& c) {
|
|||||||
else if (scan_) lines = scan_->gpx.lines();
|
else if (scan_) lines = scan_->gpx.lines();
|
||||||
widgets::textLines(c, lines, 0, body);
|
widgets::textLines(c, lines, 0, body);
|
||||||
right = formatBytes(size_);
|
right = formatBytes(size_);
|
||||||
keys = "Tab: the file as text";
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Mode::Pcap: {
|
case Mode::Pcap: {
|
||||||
@@ -423,7 +444,6 @@ void FileViewer::draw(Canvas& c) {
|
|||||||
return row < shown_.size() ? shown_[row] : std::string();
|
return row < shown_.size() ? shown_[row] : std::string();
|
||||||
});
|
});
|
||||||
right = std::to_string(scan_->packets.size()) + (scan_->morePackets ? "+ packets" : " packets");
|
right = std::to_string(scan_->packets.size()) + (scan_->morePackets ? "+ packets" : " packets");
|
||||||
keys = "Enter: the packet Tab: hex";
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -447,7 +467,6 @@ void FileViewer::draw(Canvas& c) {
|
|||||||
}
|
}
|
||||||
widgets::textLines(c, lines, 0, body);
|
widgets::textLines(c, lines, 0, body);
|
||||||
right = formatBytes(size_);
|
right = formatBytes(size_);
|
||||||
keys = ok ? "Enter: install Tab: hex" : "Tab: hex";
|
|
||||||
if (confirm_) {
|
if (confirm_) {
|
||||||
widgets::dialog(c, "Install update?", "The device restarts into " + scan_->version + " once it's written.", *confirm_);
|
widgets::dialog(c, "Install update?", "The device restarts into " + scan_->version + " once it's written.", *confirm_);
|
||||||
return;
|
return;
|
||||||
@@ -464,7 +483,7 @@ void FileViewer::draw(Canvas& c) {
|
|||||||
c.drawString(right.c_str(), area.w - 3, area.y + 1);
|
c.drawString(right.c_str(), area.w - 3, area.y + 1);
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
if (!message_.empty()) c.setTextColor(theme::kWarning);
|
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
|
} // namespace roro
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "dialog_model.h"
|
#include "dialog_model.h"
|
||||||
|
#include "key_help.h"
|
||||||
#include "key_event.h"
|
#include "key_event.h"
|
||||||
#include "list_model.h"
|
#include "list_model.h"
|
||||||
#include "services/storage_service.h"
|
#include "services/storage_service.h"
|
||||||
@@ -27,6 +28,7 @@ class FileViewer {
|
|||||||
void open(const std::string& path, uint32_t size);
|
void open(const std::string& path, uint32_t size);
|
||||||
void close();
|
void close();
|
||||||
bool onKey(const KeyEvent& e); // false: leave the viewer
|
bool onKey(const KeyEvent& e); // false: leave the viewer
|
||||||
|
void help(std::vector<KeyHelp>& out) const;
|
||||||
bool update(uint32_t nowMs); // true: draw again
|
bool update(uint32_t nowMs); // true: draw again
|
||||||
void draw(Canvas& c);
|
void draw(Canvas& c);
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ std::string FirmwarePage::latestText(bool& warn) const {
|
|||||||
release::Release r;
|
release::Release r;
|
||||||
if (g.status() == GiteaReleases::Status::Failed && !g.latest(r)) {
|
if (g.status() == GiteaReleases::Status::Failed && !g.latest(r)) {
|
||||||
warn = true;
|
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)");
|
return r.tag + (release::isNewer(r, update_.runningVersion()) ? " (new)" : " (current)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ void FirmwarePage::drawRelease(Canvas& c) {
|
|||||||
c.setFont(&fonts::small);
|
c.setFont(&fonts::small);
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
c.setTextColor(theme::kMuted);
|
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) {
|
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)" : "");
|
return r.tag + (r.tag == running ? " (running)" : release::versionNewer(r.tag, running) ? " (new)" : "");
|
||||||
},
|
},
|
||||||
[&](int i) { return olderReleases_[i].date(); });
|
[&](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) {
|
void FirmwarePage::drawMain(Canvas& c) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "dialog_model.h"
|
#include "dialog_model.h"
|
||||||
#include "key_event.h"
|
#include "key_event.h"
|
||||||
|
#include "key_help.h"
|
||||||
#include "list_model.h"
|
#include "list_model.h"
|
||||||
#include "release_info.h"
|
#include "release_info.h"
|
||||||
#include "services/storage_service.h"
|
#include "services/storage_service.h"
|
||||||
@@ -28,6 +29,8 @@ class FirmwarePage {
|
|||||||
void enter();
|
void enter();
|
||||||
bool onKey(const KeyEvent& e); // false: leave the page
|
bool onKey(const KeyEvent& e); // false: leave the page
|
||||||
void draw(Canvas& c);
|
void draw(Canvas& c);
|
||||||
|
void help(std::vector<KeyHelp>& out) const;
|
||||||
|
const char* helpTitle() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum Row { kVersion, kStatus, kAddress, kLatest, kOlder, kSdHeader, kFixed };
|
enum Row { kVersion, kStatus, kAddress, kLatest, kOlder, kSdHeader, kFixed };
|
||||||
|
|||||||
+30
-4
@@ -171,7 +171,7 @@ void GeminiApp::update(uint32_t nowMs) {
|
|||||||
else {
|
else {
|
||||||
std::string mime = result.header.mimeType();
|
std::string mime = result.header.mimeType();
|
||||||
showMessage(url, "# Not a text page\nThis is " + mime +
|
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");
|
"/gemini/downloads.\n");
|
||||||
page_.header.meta = mime; // so s knows to download it
|
page_.header.meta = mime; // so s knows to download it
|
||||||
}
|
}
|
||||||
@@ -302,6 +302,32 @@ void GeminiApp::selectLink(int direction) {
|
|||||||
requestRedraw();
|
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) {
|
bool GeminiApp::onKey(const KeyEvent& e) {
|
||||||
requestRedraw();
|
requestRedraw();
|
||||||
if (certDialog_) {
|
if (certDialog_) {
|
||||||
@@ -388,7 +414,7 @@ bool GeminiApp::onKey(const KeyEvent& e) {
|
|||||||
if (isGemini(page_.base())) gemini_.addBookmark(page_.base(), titleOf());
|
if (isGemini(page_.base())) gemini_.addBookmark(page_.base(), titleOf());
|
||||||
break;
|
break;
|
||||||
case 's': // Q82, Q73
|
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 (!isGemini(page_.url)) status_ = "Only Gemini pages can be saved";
|
||||||
else if (page_.header.mimeType().rfind("text/", 0) == 0) gemini_.save(page_.url);
|
else if (page_.header.mimeType().rfind("text/", 0) == 0) gemini_.save(page_.url);
|
||||||
else gemini_.download(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.fillRect(box.x - 2, box.y - 12, box.w + 4, box.h + 14, theme::kBackground);
|
||||||
c.setFont(&fonts::small);
|
c.setFont(&fonts::small);
|
||||||
c.setTextColor(theme::kMuted);
|
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);
|
widgets::lineEditor(c, address_, box);
|
||||||
}
|
}
|
||||||
if (inputOpen_) {
|
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.drawString(displayText(inputPrompt_).substr(0, 39).c_str(), box.x, box.y - 22);
|
||||||
c.setFont(&fonts::small);
|
c.setFont(&fonts::small);
|
||||||
c.setTextColor(theme::kMuted);
|
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_) {
|
if (inputSensitive_) {
|
||||||
LineEditor masked(1024);
|
LineEditor masked(1024);
|
||||||
masked.setText(std::string(input_.text().size(), '*'));
|
masked.setText(std::string(input_.text().size(), '*'));
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class GeminiApp : public App {
|
|||||||
bool textEntryActive() const override { return addressOpen_ || inputOpen_; }
|
bool textEntryActive() const override { return addressOpen_ || inputOpen_; }
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Visit {
|
struct Visit {
|
||||||
|
|||||||
+7
-10
@@ -80,11 +80,11 @@ void GnssApp::draw(Canvas& c) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sky_ ? drawSky(c) : drawPosition(c);
|
sky_ ? drawSky(c) : drawPosition(c);
|
||||||
c.setFont(&fonts::small);
|
}
|
||||||
c.setTextColor(theme::kMuted);
|
|
||||||
c.setTextDatum(top_right);
|
void GnssApp::help(std::vector<KeyHelp>& out) const {
|
||||||
c.drawString(sky_ ? "Tab: position" : "Tab: sky", area.w - 3, area.y + area.h - 9);
|
out.push_back({"Tab", sky_ ? "the position" : "the sky"});
|
||||||
c.setTextDatum(top_left);
|
out.push_back({"r", gnss_.tracking() ? "stop the Track" : "record a Track"});
|
||||||
}
|
}
|
||||||
|
|
||||||
void GnssApp::drawPosition(Canvas& c) {
|
void GnssApp::drawPosition(Canvas& c) {
|
||||||
@@ -139,15 +139,12 @@ void GnssApp::drawPosition(Canvas& c) {
|
|||||||
c.setFont(&fonts::small);
|
c.setFont(&fonts::small);
|
||||||
if (gnss_.tracking()) {
|
if (gnss_.tracking()) {
|
||||||
c.setTextColor(theme::kWarning);
|
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());
|
gnss_.trackPoints() == 1 ? "" : "s", duration(now - gnss_.trackStartMs()).c_str());
|
||||||
} else if (!refusal_.empty() && now - refusalMs_ < 4000) {
|
} else if (!refusal_.empty() && now - refusalMs_ < 4000) {
|
||||||
c.setTextColor(theme::kWarning);
|
c.setTextColor(theme::kWarning);
|
||||||
std::snprintf(line, sizeof line, "%s", refusal_.c_str());
|
std::snprintf(line, sizeof line, "%s", refusal_.c_str());
|
||||||
} else {
|
} else line[0] = 0;
|
||||||
c.setTextColor(theme::kMuted);
|
|
||||||
std::snprintf(line, sizeof line, "r: record a Track");
|
|
||||||
}
|
|
||||||
c.drawString(line, 4, area.y + area.h - 9);
|
c.drawString(line, 4, area.y + area.h - 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class GnssApp : public App {
|
|||||||
bool onKey(const KeyEvent& e) override;
|
bool onKey(const KeyEvent& e) override;
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void drawPosition(Canvas& c);
|
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});
|
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) {
|
void IrcApp::drawSettings(Canvas& c) {
|
||||||
const auto& area = theme::kContent;
|
const auto& area = theme::kContent;
|
||||||
if (editing_) {
|
if (editing_) {
|
||||||
@@ -306,8 +336,7 @@ void IrcApp::drawSettings(Canvas& c) {
|
|||||||
c.setTextColor(theme::kMuted);
|
c.setTextColor(theme::kMuted);
|
||||||
c.drawString(fieldLabel(fields_.selected()).c_str(), 4, area.y + 4);
|
c.drawString(fieldLabel(fields_.selected()).c_str(), 4, area.y + 4);
|
||||||
widgets::lineEditor(c, fieldEditor_, {4, area.y + 22, area.w - 8, 0});
|
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,
|
if (fields_.selected() == kAutojoin) c.drawString("e.g. #roro, #private key", 4, area.y + 44);
|
||||||
area.y + 44);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
widgets::list(
|
widgets::list(
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class IrcApp : public App {
|
|||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
bool textEntryActive() const override { return page_ == Page::Chat || editing_; }
|
bool textEntryActive() const override { return page_ == Page::Chat || editing_; }
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Page { Chat, Settings };
|
enum class Page { Chat, Settings };
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class LauncherApp : public App {
|
|||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
bool onKey(const KeyEvent& e) override;
|
bool onKey(const KeyEvent& e) override;
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override { help::list(out, "open the App"); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AppManager* manager_ = nullptr;
|
AppManager* manager_ = nullptr;
|
||||||
|
|||||||
@@ -165,6 +165,32 @@ void LoraScannerApp::update(uint32_t nowMs) {
|
|||||||
requestRedraw();
|
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) {
|
void LoraScannerApp::draw(Canvas& c) {
|
||||||
lastDrawMs_ = millis();
|
lastDrawMs_ = millis();
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
@@ -234,8 +260,7 @@ void LoraScannerApp::drawPackets(Canvas& c) {
|
|||||||
c.setFont(&fonts::small);
|
c.setFont(&fonts::small);
|
||||||
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
||||||
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
|
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
|
||||||
std::string keys = std::string("Enter: details p: preset Tab: sweep c: ") + (capture_.capturing() ? "stop" : "capture");
|
if (showMessage) c.drawString(message_.c_str(), 4, area.y + area.h - 9);
|
||||||
c.drawString(showMessage ? message_.c_str() : keys.c_str(), 4, area.y + area.h - 9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoraScannerApp::drawDetails(Canvas& c) {
|
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.drawString(line, x0 + f.steps * cell, wfTop + kWaterfallRows + 1);
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
if (st.peaks.empty())
|
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
|
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);
|
st.peaks[0].dbm);
|
||||||
c.setTextColor(theme::kMuted);
|
c.setTextColor(theme::kMuted);
|
||||||
c.drawString(line, 4, y);
|
c.drawString(line, 4, y);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class LoraScannerApp : public App {
|
|||||||
bool onKey(const KeyEvent& e) override;
|
bool onKey(const KeyEvent& e) override;
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class View { Packets, Details, Presets, Sweep };
|
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);
|
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) {
|
bool MaintenancePage::onKey(const KeyEvent& e) {
|
||||||
if (dialog_) {
|
if (dialog_) {
|
||||||
dialog_->onKey(e);
|
dialog_->onKey(e);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "cleanup_plan.h"
|
#include "cleanup_plan.h"
|
||||||
#include "dialog_model.h"
|
#include "dialog_model.h"
|
||||||
|
#include "key_help.h"
|
||||||
#include "event_bus.h"
|
#include "event_bus.h"
|
||||||
#include "key_event.h"
|
#include "key_event.h"
|
||||||
#include "list_model.h"
|
#include "list_model.h"
|
||||||
@@ -24,6 +25,7 @@ class MaintenancePage {
|
|||||||
void enter();
|
void enter();
|
||||||
bool onKey(const KeyEvent& e); // false: leave the page
|
bool onKey(const KeyEvent& e); // false: leave the page
|
||||||
void draw(Canvas& c);
|
void draw(Canvas& c);
|
||||||
|
void help(std::vector<KeyHelp>& out) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class View { Main, Categories, Ages };
|
enum class View { Main, Categories, Ages };
|
||||||
|
|||||||
@@ -182,6 +182,18 @@ bool NoteEditor::save() {
|
|||||||
return true;
|
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) {
|
bool NoteEditor::onKey(const KeyEvent& e) {
|
||||||
if (!text_) return false;
|
if (!text_) return false;
|
||||||
redraw_ = true;
|
redraw_ = true;
|
||||||
@@ -290,7 +302,7 @@ void NoteEditor::draw(Canvas& c) {
|
|||||||
c.setFont(&fonts::small);
|
c.setFont(&fonts::small);
|
||||||
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
||||||
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
|
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)
|
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 " +
|
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 <vector>
|
||||||
|
|
||||||
#include "dialog_model.h"
|
#include "dialog_model.h"
|
||||||
|
#include "key_help.h"
|
||||||
#include "key_event.h"
|
#include "key_event.h"
|
||||||
#include "note_text.h"
|
#include "note_text.h"
|
||||||
#include "services/clock_service.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
|
const std::string& path() const { return path_; } // "" for a new note nothing was typed in
|
||||||
|
|
||||||
bool onKey(const KeyEvent& e); // false: done, and saved
|
bool onKey(const KeyEvent& e); // false: done, and saved
|
||||||
|
void help(std::vector<KeyHelp>& out) const;
|
||||||
bool update(uint32_t nowMs); // true: draw again
|
bool update(uint32_t nowMs); // true: draw again
|
||||||
void draw(Canvas& c);
|
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
|
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) {
|
bool NotesApp::onKey(const KeyEvent& e) {
|
||||||
requestRedraw();
|
requestRedraw();
|
||||||
if (view_ == View::NoMemory) return false;
|
if (view_ == View::NoMemory) return false;
|
||||||
@@ -315,7 +332,6 @@ void NotesApp::draw(Canvas& c) {
|
|||||||
} else if (notes_.empty()) {
|
} else if (notes_.empty()) {
|
||||||
c.setTextColor(theme::kMuted);
|
c.setTextColor(theme::kMuted);
|
||||||
c.drawString("No notes yet.", 4, rows.y + 4);
|
c.drawString("No notes yet.", 4, rows.y + 4);
|
||||||
c.drawString("n starts one.", 4, rows.y + 4 + theme::kLineHeight);
|
|
||||||
} else {
|
} else {
|
||||||
readTitles();
|
readTitles();
|
||||||
widgets::list(
|
widgets::list(
|
||||||
@@ -329,7 +345,7 @@ void NotesApp::draw(Canvas& c) {
|
|||||||
c.setFont(&fonts::small);
|
c.setFont(&fonts::small);
|
||||||
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
||||||
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
|
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) {
|
if (view_ == View::Name) {
|
||||||
theme::Rect box{16, 44, theme::kWidth - 32, 48};
|
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; }
|
bool textEntryActive() const override { return view_ == View::Edit || view_ == View::Name; }
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class View { List, Edit, Name, NoMemory };
|
enum class View { List, Edit, Name, NoMemory };
|
||||||
|
|||||||
@@ -129,6 +129,31 @@ bool SettingsApp::onAboutKey(const KeyEvent& e) {
|
|||||||
return true;
|
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) {
|
void SettingsApp::update(uint32_t nowMs) {
|
||||||
// Live values on About and Firmware.
|
// Live values on About and Firmware.
|
||||||
bool live = page_ == Page::About || page_ == Page::Firmware || page_ == Page::Debug ||
|
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.setTextColor(theme::kMuted);
|
||||||
c.drawString(menu_.label(editingRow_).c_str(), 4, area.y + 4);
|
c.drawString(menu_.label(editingRow_).c_str(), 4, area.y + 4);
|
||||||
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
||||||
c.drawString(("Enter: save `: cancel " + std::to_string(editor_.text().size()) + "/" +
|
c.drawString((std::to_string(editor_.text().size()) + " of " + std::to_string(editor_.maxBytes()) + " bytes").c_str(), 4,
|
||||||
std::to_string(editor_.maxBytes()) + " bytes")
|
area.y + 44);
|
||||||
.c_str(),
|
|
||||||
4, area.y + 44);
|
|
||||||
break;
|
break;
|
||||||
case Page::Choice: {
|
case Page::Choice: {
|
||||||
auto options = menu_.choices(editingRow_);
|
auto options = menu_.choices(editingRow_);
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class SettingsApp : public App {
|
|||||||
(page_ == Page::Debug && debugPage_.textEntryActive());
|
(page_ == Page::Debug && debugPage_.textEntryActive());
|
||||||
}
|
}
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Page { Menu, Text, Choice, About, Wifi, Firmware, Debug };
|
enum class Page { Menu, Text, Choice, About, Wifi, Firmware, Debug };
|
||||||
|
|||||||
+23
-2
@@ -44,6 +44,24 @@ bool SetupApp::onKey(const KeyEvent& e) {
|
|||||||
return true;
|
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) {
|
void SetupApp::draw(Canvas& c) {
|
||||||
const auto& area = theme::kContent;
|
const auto& area = theme::kContent;
|
||||||
auto heading = [&](const char* text) {
|
auto heading = [&](const char* text) {
|
||||||
@@ -63,6 +81,7 @@ void SetupApp::draw(Canvas& c) {
|
|||||||
heading("Welcome to roro9stack");
|
heading("Welcome to roro9stack");
|
||||||
note("Let's set up this device: your names on the mesh, your radio region and your timezone.",
|
note("Let's set up this device: your names on the mesh, your radio region and your timezone.",
|
||||||
area.y + 22, theme::kText);
|
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);
|
note("Enter: continue", area.y + area.h - 14);
|
||||||
break;
|
break;
|
||||||
case Step::LongName:
|
case Step::LongName:
|
||||||
@@ -88,8 +107,10 @@ void SetupApp::draw(Canvas& c) {
|
|||||||
[this](int i) { return wizard_.choiceLabel(i); });
|
[this](int i) { return wizard_.choiceLabel(i); });
|
||||||
break;
|
break;
|
||||||
case Step::Done:
|
case Step::Done:
|
||||||
heading("All set");
|
heading("All set. One key to remember:");
|
||||||
note("You can change all of this later in Settings.", area.y + 22, theme::kText);
|
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);
|
note("Enter: start `: go back", area.y + area.h - 14);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class SetupApp : public App {
|
|||||||
return wizard_.step() == SetupWizard::Step::LongName || wizard_.step() == SetupWizard::Step::ShortName;
|
return wizard_.step() == SetupWizard::Step::LongName || wizard_.step() == SetupWizard::Step::ShortName;
|
||||||
}
|
}
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override { return "Setup"; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AppManager& apps_;
|
AppManager& apps_;
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ void StorageApp::onFinished(const FileOps::Status& s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StorageApp::paste(bool replace) {
|
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;
|
std::string name = baseName(clip_.path), why;
|
||||||
if (cut_) {
|
if (cut_) {
|
||||||
why = ops_.move(clip_.path, clip_.folder, joinPath(cwd_, name), replace);
|
why = ops_.move(clip_.path, clip_.folder, joinPath(cwd_, name), replace);
|
||||||
@@ -273,6 +273,45 @@ void StorageApp::showDetails(const Item& item) {
|
|||||||
view_ = View::Details;
|
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) {
|
bool StorageApp::onKey(const KeyEvent& e) {
|
||||||
requestRedraw();
|
requestRedraw();
|
||||||
if (view_ == View::NoMemory) return false;
|
if (view_ == View::NoMemory) return false;
|
||||||
@@ -434,7 +473,7 @@ bool StorageApp::onBrowseKey(const KeyEvent& e) {
|
|||||||
if (ch == 'c') {
|
if (ch == 'c') {
|
||||||
clip_ = item;
|
clip_ = item;
|
||||||
cut_ = false;
|
cut_ = false;
|
||||||
say("v pastes a copy of " + name);
|
say("Copied " + name + ": paste it where you like");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Cut, rename and delete change the original: the rules of Q130 first.
|
// 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') {
|
if (ch == 'x') {
|
||||||
clip_ = item;
|
clip_ = item;
|
||||||
cut_ = true;
|
cut_ = true;
|
||||||
say("v moves " + name + " here");
|
say("Cut " + name + ": paste it where it should go");
|
||||||
} else if (ch == 'r') {
|
} else if (ch == 'r') {
|
||||||
target_ = item;
|
target_ = item;
|
||||||
renaming_ = true;
|
renaming_ = true;
|
||||||
@@ -542,7 +581,6 @@ void StorageApp::drawBrowse(Canvas& c) {
|
|||||||
} else if (rows_.count() == 0) {
|
} else if (rows_.count() == 0) {
|
||||||
c.setTextColor(theme::kMuted);
|
c.setTextColor(theme::kMuted);
|
||||||
c.drawString("Nothing in this folder.", 4, rows.y + 4);
|
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 {
|
} else {
|
||||||
int entries = static_cast<int>(list_.count());
|
int entries = static_cast<int>(list_.count());
|
||||||
auto value = [&](int i) {
|
auto value = [&](int i) {
|
||||||
@@ -558,13 +596,13 @@ void StorageApp::drawBrowse(Canvas& c) {
|
|||||||
value);
|
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);
|
c.setFont(&fonts::small);
|
||||||
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
bool showMessage = !message_.empty() && millis() - messageMs_ < kMessageMs;
|
||||||
c.setTextColor(showMessage ? theme::kWarning : theme::kMuted);
|
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) + "...";
|
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);
|
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);
|
line = formatBytes(s.doneBytes) + " of " + formatBytes(s.totalBytes);
|
||||||
}
|
}
|
||||||
c.drawString(line.c_str(), box.x + 6, box.y + 31);
|
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
|
} // namespace roro
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class StorageApp : public App {
|
|||||||
bool textEntryActive() const override { return view_ == View::Name || view_ == View::Editor; }
|
bool textEntryActive() const override { return view_ == View::Name || view_ == View::Editor; }
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class View { Browse, Details, Name, Viewer, Editor, Maintenance, NoMemory };
|
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;
|
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) {
|
bool SystemApp::onKey(const KeyEvent& e) {
|
||||||
if (e.key == Key::Tab) {
|
if (e.key == Key::Tab) {
|
||||||
int n = static_cast<int>(View::Count), v = static_cast<int>(view_);
|
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) {
|
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.drawString(std::to_string(r.stackFree).c_str(), area.w - 4, y);
|
||||||
}
|
}
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
std::string keys = std::string("s: by ") + (sort_ == TaskSort::Share ? "stack" : sort_ == TaskSort::Stack ? "name" : "cpu") +
|
std::string keys = std::to_string(rows_.size()) + " tasks, by " + (sort_ == TaskSort::Share ? "cpu" : sort_ == TaskSort::Stack ? "stack" : "name");
|
||||||
" " + std::to_string(rows_.size()) + " tasks Tab: memory";
|
|
||||||
footer(c, keys.c_str());
|
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);
|
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);
|
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) {
|
void SystemApp::drawNetwork(Canvas& c) {
|
||||||
@@ -331,7 +349,7 @@ void SystemApp::drawNetwork(Canvas& c) {
|
|||||||
y += theme::kLineHeight;
|
y += theme::kLineHeight;
|
||||||
}
|
}
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
footer(c, "bytes since boot Tab: system");
|
footer(c, "bytes since boot");
|
||||||
}
|
}
|
||||||
|
|
||||||
void SystemApp::drawSystem(Canvas& c) {
|
void SystemApp::drawSystem(Canvas& c) {
|
||||||
@@ -355,7 +373,6 @@ void SystemApp::drawSystem(Canvas& c) {
|
|||||||
wrappedLines_ = static_cast<int>(wrapped.size());
|
wrappedLines_ = static_cast<int>(wrapped.size());
|
||||||
systemTop_ = std::clamp(systemTop_, 0, std::max(0, wrappedLines_ - 8));
|
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});
|
widgets::textLines(c, wrapped, systemTop_, {area.x + 2, area.y + 2, area.w - 2, area.h - 12});
|
||||||
footer(c, "Tab: overview");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace roro
|
} // namespace roro
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class SystemApp : public App {
|
|||||||
bool onKey(const KeyEvent& e) override;
|
bool onKey(const KeyEvent& e) override;
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class View : uint8_t { Overview, Tasks, Memory, Network, System, Count };
|
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.setTextColor(theme::kMuted);
|
||||||
c.drawString(editTitle_.c_str(), 4, area.y + 4);
|
c.drawString(editTitle_.c_str(), 4, area.y + 4);
|
||||||
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
||||||
c.drawString("Enter: save `: cancel", 4, area.y + 44);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +426,7 @@ void WifiSettingsPage::draw(Canvas& c) {
|
|||||||
if (!n.ssid.empty()) found.push_back(n);
|
if (!n.ssid.empty()) found.push_back(n);
|
||||||
if (scan_.count() != static_cast<int>(found.size())) scan_.setCount(static_cast<int>(found.size()));
|
if (scan_.count() != static_cast<int>(found.size())) scan_.setCount(static_cast<int>(found.size()));
|
||||||
if (found.empty()) {
|
if (found.empty()) {
|
||||||
widgets::textLines(c, {"No networks found.", "", "`: back"}, 0, area);
|
widgets::textLines(c, {"No networks found."}, 0, area);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
widgets::list(
|
widgets::list(
|
||||||
@@ -443,20 +442,55 @@ void WifiSettingsPage::draw(Canvas& c) {
|
|||||||
std::string title = view_ == View::Ssid ? "Hidden network name" : "Password for " + newSsid_;
|
std::string title = view_ == View::Ssid ? "Hidden network name" : "Password for " + newSsid_;
|
||||||
c.drawString(title.c_str(), 4, area.y + 4);
|
c.drawString(title.c_str(), 4, area.y + 4);
|
||||||
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
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,
|
if (view_ == View::Password) c.drawString("Leave it empty for an open network.", 4, area.y + 44);
|
||||||
area.y + 44);
|
|
||||||
break;
|
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).
|
// What the device is using and where each part came from (Q113).
|
||||||
void WifiSettingsPage::drawDetails(Canvas& c) {
|
void WifiSettingsPage::drawDetails(Canvas& c) {
|
||||||
const auto& area = theme::kContent;
|
const auto& area = theme::kContent;
|
||||||
WifiService::Connection n = wifi_.connection();
|
WifiService::Connection n = wifi_.connection();
|
||||||
std::vector<std::string> lines;
|
std::vector<std::string> lines;
|
||||||
if (!n.connected) {
|
if (!n.connected) {
|
||||||
lines = {statusText(), "", "`: back"};
|
lines = {statusText()};
|
||||||
} else {
|
} else {
|
||||||
lines.push_back(wifi_.ssid() + ", " + std::to_string(wifi_.rssi()) + " dBm");
|
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)"));
|
lines.push_back("Address " + n.address + "/" + std::to_string(n.prefix) + (n.fixed ? " (fixed)" : " (DHCP)"));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "dialog_model.h"
|
#include "dialog_model.h"
|
||||||
#include "event_bus.h"
|
#include "event_bus.h"
|
||||||
#include "key_event.h"
|
#include "key_event.h"
|
||||||
|
#include "key_help.h"
|
||||||
#include "line_editor.h"
|
#include "line_editor.h"
|
||||||
#include "list_model.h"
|
#include "list_model.h"
|
||||||
#include "saved_networks.h"
|
#include "saved_networks.h"
|
||||||
@@ -30,6 +31,8 @@ class WifiSettingsPage {
|
|||||||
// Redraw periodically: the status, a scan, the connection details.
|
// Redraw periodically: the status, a scan, the connection details.
|
||||||
bool live() const { return view_ == View::Main || view_ == View::Scan || view_ == View::Details; }
|
bool live() const { return view_ == View::Main || view_ == View::Scan || view_ == View::Details; }
|
||||||
void draw(Canvas& c);
|
void draw(Canvas& c);
|
||||||
|
void help(std::vector<KeyHelp>& out) const;
|
||||||
|
const char* helpTitle() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class View { Main, Scan, Ssid, Password, Details, Servers, Network, Edit };
|
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) {
|
void WifiToolsApp::drawNetworks(Canvas& c) {
|
||||||
const auto& area = theme::kContent;
|
const auto& area = theme::kContent;
|
||||||
const auto& scan = wifi_.listScan();
|
const auto& scan = wifi_.listScan();
|
||||||
@@ -178,7 +203,7 @@ void WifiToolsApp::drawNetworks(Canvas& c) {
|
|||||||
widgets::textLines(c,
|
widgets::textLines(c,
|
||||||
{wifi_.listScanDone() ? (any ? "Nothing matches the filter." : "No networks found yet.")
|
{wifi_.listScanDone() ? (any ? "Nothing matches the filter." : "No networks found yet.")
|
||||||
: "Scanning...",
|
: "Scanning...",
|
||||||
"", "s sort o open h hide-hidden w strong l log"},
|
},
|
||||||
0, list);
|
0, list);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -233,7 +258,7 @@ void WifiToolsApp::drawTracker(Canvas& c) {
|
|||||||
c.drawString(targetName_.c_str(), 4, area.y + 2);
|
c.drawString(targetName_.c_str(), 4, area.y + 2);
|
||||||
c.setFont(&fonts::small);
|
c.setFont(&fonts::small);
|
||||||
c.setTextColor(theme::kMuted);
|
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.drawString(sub.c_str(), 4, area.y + 17);
|
||||||
|
|
||||||
c.setFont(&fonts::bold);
|
c.setFont(&fonts::bold);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class WifiToolsApp : public App {
|
|||||||
bool onKey(const KeyEvent& e) override;
|
bool onKey(const KeyEvent& e) override;
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
void help(std::vector<KeyHelp>& out) const override;
|
||||||
|
const char* helpTitle() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class View { Menu, Networks, Channels, Tracker };
|
enum class View { Menu, Networks, Channels, Tracker };
|
||||||
|
|||||||
+9
-2
@@ -225,6 +225,13 @@ void setup() {
|
|||||||
services.startAll(millis());
|
services.startAll(millis());
|
||||||
apps->begin();
|
apps->begin();
|
||||||
if (!settings.getBool(Setting::SetupDone)) apps->openModal("setup");
|
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(),
|
console.printf("%s %s ready, free heap %u, last start: %s\n", kProductName, versionString(), ESP.getFreeHeap(),
|
||||||
system_info::resetReason());
|
system_info::resetReason());
|
||||||
if (system_info::resetWasCrash()) {
|
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"
|
"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"
|
"crash the last crash: firmware, reason, task, backtrace\n"
|
||||||
"coredump erase forget the core dump in flash\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 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 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"
|
"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);
|
String k = line.substring(4);
|
||||||
Key key = k == "up" ? Key::Up : k == "down" ? Key::Down : k == "left" ? Key::Left
|
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 == "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(' ')
|
KeyEvent ev = k == "space" ? KeyEvent::character(' ')
|
||||||
: k.length() == 1 && k[0] > ' ' ? KeyEvent::character((unsigned char)k[0])
|
: k.length() == 1 && k[0] > ' ' ? KeyEvent::character((unsigned char)k[0])
|
||||||
: KeyEvent::of(key);
|
: 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.
|
// about:start (Q78, Q84): bookmarks, then Saved Pages by capsule, newest first, then defaults.
|
||||||
void GeminiService::composeStart(GeminiPage& page) {
|
void GeminiService::composeStart(GeminiPage& page) {
|
||||||
std::string doc = "# Gemini\nTab picks a link, Enter follows it, Back returns, g types an address. "
|
std::string doc = "# Gemini\nYour bookmarks and Saved Pages, then a few places to start.\n";
|
||||||
"On a page: b bookmarks it, s saves it to read offline, S with the pages it links to.\n";
|
|
||||||
std::string bookmarks;
|
std::string bookmarks;
|
||||||
std::vector<SavedEntry> saved;
|
std::vector<SavedEntry> saved;
|
||||||
bool card = storage_.runAndWait([&]() {
|
bool card = storage_.runAndWait([&]() {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ void Screen::render(AppManager& apps, const StatusInfo& status, const Toast* toa
|
|||||||
const auto& area = theme::kContent;
|
const auto& area = theme::kContent;
|
||||||
canvas_.setClipRect(area.x, area.y, area.w, area.h);
|
canvas_.setClipRect(area.x, area.y, area.w, area.h);
|
||||||
apps.foreground().draw(canvas_);
|
apps.foreground().draw(canvas_);
|
||||||
|
if (apps.help().isOpen()) widgets::help(canvas_, apps.help()); // over the App, under the Status Bar
|
||||||
canvas_.clearClipRect();
|
canvas_.clearClipRect();
|
||||||
widgets::statusBar(canvas_, status);
|
widgets::statusBar(canvas_, status);
|
||||||
if (toast) widgets::toast(canvas_, *toast);
|
if (toast) widgets::toast(canvas_, *toast);
|
||||||
|
|||||||
@@ -175,4 +175,40 @@ void toast(Canvas& c, const Toast& t) {
|
|||||||
c.clearClipRect();
|
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
|
} // namespace roro::widgets
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "canvas.h"
|
#include "canvas.h"
|
||||||
#include "dialog_model.h"
|
#include "dialog_model.h"
|
||||||
|
#include "key_help.h"
|
||||||
#include "line_editor.h"
|
#include "line_editor.h"
|
||||||
#include "list_model.h"
|
#include "list_model.h"
|
||||||
#include "text_wrap.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);
|
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);
|
void toast(Canvas& c, const Toast& t);
|
||||||
|
|
||||||
} // namespace widgets
|
} // namespace widgets
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ struct FakeApp : App {
|
|||||||
}
|
}
|
||||||
void update(uint32_t nowMs) override { lastUpdate = nowMs; }
|
void update(uint32_t nowMs) override { lastUpdate = nowMs; }
|
||||||
void draw(Canvas&) override {}
|
void draw(Canvas&) override {}
|
||||||
|
void help(std::vector<KeyHelp>& out) const override {
|
||||||
|
if (helpKey) out.push_back({helpKey, "does a thing"});
|
||||||
|
}
|
||||||
|
const char* helpTitle() const override { return scope; }
|
||||||
|
const char* helpKey = nullptr;
|
||||||
|
const char* scope = nullptr;
|
||||||
uint32_t lastUpdate = 0;
|
uint32_t lastUpdate = 0;
|
||||||
std::vector<KeyEvent> keys;
|
std::vector<KeyEvent> keys;
|
||||||
bool handles = false;
|
bool handles = false;
|
||||||
@@ -181,6 +187,79 @@ void test_modal_app_cannot_be_switched_away_by_open() {
|
|||||||
TEST_ASSERT_EQUAL_PTR(&f.demo, &f.manager.foreground());
|
TEST_ASSERT_EQUAL_PTR(&f.demo, &f.manager.foreground());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue #69: Fn+h lists the foreground App's keys, then the ones that work everywhere.
|
||||||
|
void test_help_lists_the_apps_keys_then_the_global_ones() {
|
||||||
|
Fixture f;
|
||||||
|
f.manager.begin();
|
||||||
|
f.manager.open("notes");
|
||||||
|
f.notes.helpKey = "n";
|
||||||
|
f.manager.handleKey(KeyEvent::of(Key::Help));
|
||||||
|
TEST_ASSERT_TRUE(f.manager.help().isOpen());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Notes", f.manager.help().title().c_str());
|
||||||
|
const auto& rows = f.manager.help().rows();
|
||||||
|
TEST_ASSERT_EQUAL_STRING("n", rows[0].keys);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Everywhere", rows[1].keys);
|
||||||
|
TEST_ASSERT_NULL(rows[1].action); // a heading
|
||||||
|
TEST_ASSERT_TRUE(rows.size() > 3);
|
||||||
|
TEST_ASSERT_EQUAL(0, f.notes.keys.size()); // the App never saw the key
|
||||||
|
TEST_ASSERT_TRUE(f.manager.takeRedraw());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_help_is_titled_with_the_scope_when_the_app_names_one() {
|
||||||
|
Fixture f;
|
||||||
|
f.manager.begin();
|
||||||
|
f.manager.handleKey(KeyEvent::of(Key::Help));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Launcher", f.manager.help().title().c_str());
|
||||||
|
f.manager.handleKey(KeyEvent::of(Key::Select)); // closes it
|
||||||
|
f.manager.open("notes");
|
||||||
|
f.notes.scope = "Notes: editor";
|
||||||
|
f.manager.handleKey(KeyEvent::of(Key::Help));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Notes: editor", f.manager.help().title().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_open_help_takes_every_key_and_any_but_the_arrows_closes_it() {
|
||||||
|
Fixture f;
|
||||||
|
f.manager.begin();
|
||||||
|
f.manager.open("notes");
|
||||||
|
f.manager.handleKey(KeyEvent::of(Key::Help));
|
||||||
|
f.manager.handleKey(KeyEvent::of(Key::Down)); // scrolls
|
||||||
|
TEST_ASSERT_TRUE(f.manager.help().isOpen());
|
||||||
|
f.manager.handleKey(KeyEvent::of(Key::Home)); // closes; doesn't go home
|
||||||
|
TEST_ASSERT_FALSE(f.manager.help().isOpen());
|
||||||
|
TEST_ASSERT_TRUE(&f.manager.foreground() == &f.notes);
|
||||||
|
TEST_ASSERT_EQUAL(0, f.notes.keys.size());
|
||||||
|
f.manager.handleKey(KeyEvent::character('x')); // closed: keys reach the App again
|
||||||
|
TEST_ASSERT_EQUAL(1, f.notes.keys.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_help_works_in_a_modal_app_and_closes_when_the_app_changes() {
|
||||||
|
Fixture f;
|
||||||
|
f.manager.begin();
|
||||||
|
f.manager.openModal("demo");
|
||||||
|
f.manager.handleKey(KeyEvent::of(Key::Help));
|
||||||
|
TEST_ASSERT_TRUE(f.manager.help().isOpen());
|
||||||
|
f.manager.endModal();
|
||||||
|
TEST_ASSERT_FALSE(f.manager.help().isOpen());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_the_help_panel_scrolls_within_its_rows() {
|
||||||
|
HelpModel m(3);
|
||||||
|
std::vector<KeyHelp> rows;
|
||||||
|
for (int i = 0; i < 8; i++) rows.push_back({"k", "a"});
|
||||||
|
m.open("T", rows);
|
||||||
|
m.onKey(KeyEvent::of(Key::Up));
|
||||||
|
TEST_ASSERT_EQUAL(0, m.top());
|
||||||
|
m.onKey(KeyEvent::of(Key::Right)); // a page
|
||||||
|
TEST_ASSERT_EQUAL(3, m.top());
|
||||||
|
for (int i = 0; i < 9; i++) m.onKey(KeyEvent::of(Key::Down));
|
||||||
|
TEST_ASSERT_EQUAL(5, m.top()); // the last row at the bottom, no further
|
||||||
|
m.onKey(KeyEvent::of(Key::Left));
|
||||||
|
TEST_ASSERT_EQUAL(2, m.top());
|
||||||
|
m.onKey(KeyEvent::of(Key::Tab));
|
||||||
|
TEST_ASSERT_FALSE(m.isOpen());
|
||||||
|
TEST_ASSERT_EQUAL(0, m.rows().size());
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_launcher_is_in_foreground_after_begin);
|
RUN_TEST(test_launcher_is_in_foreground_after_begin);
|
||||||
@@ -199,5 +278,10 @@ int main() {
|
|||||||
RUN_TEST(test_modal_app_keeps_home_and_back);
|
RUN_TEST(test_modal_app_keeps_home_and_back);
|
||||||
RUN_TEST(test_ending_the_modal_returns_to_launcher_and_unlocks_home);
|
RUN_TEST(test_ending_the_modal_returns_to_launcher_and_unlocks_home);
|
||||||
RUN_TEST(test_modal_app_cannot_be_switched_away_by_open);
|
RUN_TEST(test_modal_app_cannot_be_switched_away_by_open);
|
||||||
|
RUN_TEST(test_help_lists_the_apps_keys_then_the_global_ones);
|
||||||
|
RUN_TEST(test_help_is_titled_with_the_scope_when_the_app_names_one);
|
||||||
|
RUN_TEST(test_open_help_takes_every_key_and_any_but_the_arrows_closes_it);
|
||||||
|
RUN_TEST(test_help_works_in_a_modal_app_and_closes_when_the_app_changes);
|
||||||
|
RUN_TEST(test_the_help_panel_scrolls_within_its_rows);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,42 @@ void test_shifted_arrow_keys_type_their_symbols_when_not_typing() {
|
|||||||
TEST_ASSERT_EQUAL(':', press(m, r)[0].ch);
|
TEST_ASSERT_EQUAL(':', press(m, r)[0].ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue #69, Q196: Fn+h everywhere, ? when it wouldn't be typed.
|
||||||
|
void test_fn_h_is_help_in_both_modes() {
|
||||||
|
for (bool typing : {true, false}) {
|
||||||
|
KeyMapper m;
|
||||||
|
m.setTextEntry(typing);
|
||||||
|
auto ev = press(m, withFn({'h'}));
|
||||||
|
TEST_ASSERT_EQUAL(1, ev.size());
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(Key::Help), static_cast<int>(ev[0].key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_plain_h_still_types() {
|
||||||
|
for (bool typing : {true, false}) {
|
||||||
|
KeyMapper m;
|
||||||
|
m.setTextEntry(typing);
|
||||||
|
auto ev = press(m, chars({'h'}));
|
||||||
|
TEST_ASSERT_EQUAL(1, ev.size());
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(Key::Char), static_cast<int>(ev[0].key));
|
||||||
|
TEST_ASSERT_EQUAL('h', ev[0].ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_question_mark_is_help_only_when_not_typing() {
|
||||||
|
KeyMapper m;
|
||||||
|
m.setTextEntry(false);
|
||||||
|
auto ev = press(m, chars({'?'}));
|
||||||
|
TEST_ASSERT_EQUAL(1, ev.size());
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(Key::Help), static_cast<int>(ev[0].key));
|
||||||
|
release(m);
|
||||||
|
m.setTextEntry(true);
|
||||||
|
ev = press(m, chars({'?'}));
|
||||||
|
TEST_ASSERT_EQUAL(1, ev.size());
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(Key::Char), static_cast<int>(ev[0].key));
|
||||||
|
TEST_ASSERT_EQUAL('?', ev[0].ch);
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_plain_character);
|
RUN_TEST(test_plain_character);
|
||||||
@@ -241,5 +277,8 @@ int main() {
|
|||||||
RUN_TEST(test_fn_arrows_work_in_both_modes);
|
RUN_TEST(test_fn_arrows_work_in_both_modes);
|
||||||
RUN_TEST(test_other_characters_still_type_when_not_typing);
|
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_shifted_arrow_keys_type_their_symbols_when_not_typing);
|
||||||
|
RUN_TEST(test_fn_h_is_help_in_both_modes);
|
||||||
|
RUN_TEST(test_plain_h_still_types);
|
||||||
|
RUN_TEST(test_question_mark_is_help_only_when_not_typing);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user