Shell: only its own replies, Apps by their names, and rm as Unix has it (#67)
CI / build (pull_request) Successful in 1m38s
Site / build (pull_request) Successful in 10s

The Shell shows the replies to its own commands and nothing else. The
console knows who each line is printed for (Console::As, Console::origin):
a command run from the Shell prints as the Shell's, and what answers it
later from another task carries that along (ls, tasks, du, cp, update
check, sd list, screenshot, gemini get). Ctrl+b shows everything instead.
This replaces the ten-second window, which was a guess.

An App's name with a capital opens it (Notes, Irc, Wifi, Gnss, Gemini, Lora,
Storage, Shell, System, Settings), from the Shell and from the consoles.

rm needs -r for a folder, here and over the consoles. In the Shell a file,
or a folder with something in it, is asked about unless -f; an empty folder
with -r goes without a word.

The Shell now hands its line to the main loop to run: run from inside the
key handler, rm on a folder overflowed the loop's stack and crashed the
device. `info` says which App is in front.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-07 11:35:29 +02:00
co-authored by Claude Opus 5.5
parent 3863d28593
commit 7b5df713ad
24 changed files with 414 additions and 124 deletions
+31 -15
View File
@@ -2,7 +2,10 @@
#include <Arduino.h>
#include <algorithm>
#include "app_keys.h"
#include "file_names.h"
#include "platform/console.h"
#include "ui/fonts.h"
#include "ui/theme.h"
@@ -18,10 +21,15 @@ bool startsWith(const std::string& s, const char* prefix) { return s.rfind(prefi
void ShellApp::onEnter() {
open_ = console.openShellRing();
console.shellShowsAll(false); // its own replies only, each time it's opened
ringPos_ = 0;
scroll_ = 0;
confirm_.reset();
words_ = commandWords(helpText_);
for (auto* app : apps_.visibleApps()) { // and the Apps, by their names with a capital
std::string name = AppManager::commandFor(app->id);
if (std::find(words_.begin(), words_.end(), name) == words_.end()) words_.push_back(name);
}
words_.push_back("help");
words_.push_back("clear"); // the Shell's own
words_.push_back("quit");
@@ -32,19 +40,20 @@ void ShellApp::onEnter() {
// Nothing is kept once it's left: the ring, the lines and the list of commands all go (Q207).
void ShellApp::onExit() {
console.closeShellRing();
console.shellShowsAll(false);
open_ = false;
log_.clear();
std::vector<std::string>().swap(words_);
confirm_.reset();
}
void ShellApp::update(uint32_t nowMs) {
void ShellApp::update(uint32_t) {
uint8_t buf[256];
uint32_t skipped = 0;
size_t n;
while ((n = console.readShellSince(ringPos_, buf, sizeof buf, skipped)) > 0) {
if (skipped) log_.add("[... " + std::to_string(skipped) + " bytes lost: more was printed than fits]");
log_.feed(reinterpret_cast<const char*>(buf), n, nowMs);
log_.feed(reinterpret_cast<const char*>(buf), n);
}
if (log_.revision() != seenRevision_) {
seenRevision_ = log_.revision();
@@ -53,9 +62,8 @@ void ShellApp::update(uint32_t nowMs) {
}
void ShellApp::runNow(const std::string& line) {
log_.commandRun(millis());
// The runner echoes the line into the console, where the Shell reads it back like everything
// else, except a token being set, which goes nowhere (Q210): that one is shown here, masked.
// The runner echoes the line into the console, where the Shell reads it back with the reply,
// except a token being set, which goes nowhere (Q210): that one is shown here, masked.
if (startsWith(line, "debug token ") && line != "debug token new") log_.add("> debug token ...");
run_(line);
}
@@ -65,11 +73,19 @@ void ShellApp::enter(const std::string& line) {
scroll_ = 0;
if (line == "quit" || line == "exit") return apps_.home();
if (line == "clear") return log_.clear();
// Q209: deleting asks first, here where a slip of the finger is a key away. `rm -f` doesn't.
if (startsWith(line, "rm ") && !startsWith(line, "rm -f ")) {
pending_ = line;
confirm_.reset(new DialogModel({"Cancel", "Delete"}));
return;
// Q209: `rm` as Unix has it, with a question where Unix has none, since a slip of the finger is
// a key away here. A file, or a folder with something in it, is asked about unless -f says not
// to. An empty folder goes without a word; anything `rm` would refuse anyway, it refuses itself.
if (startsWith(line, "rm ")) {
files::RmArgs args = files::parseRm(line.substr(3));
Target target = args.force || args.path.empty() ? Target::Missing : probe_(args.path);
bool ask = target == Target::File || (target == Target::FullFolder && args.recursive);
if (ask) {
pending_ = line;
question_ = target == Target::File ? args.path + ". It can't be undone." : args.path + " and everything in it. It can't be undone.";
confirm_.reset(new DialogModel({"Cancel", "Delete"}));
return;
}
}
runNow(line);
}
@@ -91,8 +107,8 @@ bool ShellApp::onKey(const KeyEvent& e) {
return true;
}
if (e.key == Key::Char && e.ctrl && (e.ch == 'b' || e.ch == 'B')) { // Q206
log_.showBackground(!log_.showsBackground());
log_.add(log_.showsBackground() ? "Showing everything the console prints." : "Showing only what follows your commands.");
console.shellShowsAll(!console.shellShowsAll());
log_.add(console.shellShowsAll() ? "Showing everything the console prints." : "Showing only the replies to your commands.");
return true;
}
std::string recalled;
@@ -167,14 +183,14 @@ void ShellApp::draw(Canvas& c) {
if (scroll_ > 0) {
c.setTextColor(theme::kWarning);
c.drawString(("^ " + std::to_string(scroll_)).c_str(), area.w - 3, output.y + 1);
} else if (!log_.showsBackground()) {
} else if (console.shellShowsAll()) {
c.setTextColor(theme::kMuted);
c.drawString("mine", area.w - 3, output.y + 1);
c.drawString("all", area.w - 3, output.y + 1);
}
c.setTextDatum(top_left);
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
if (confirm_) widgets::dialog(c, "Delete?", pending_.substr(3) + ", with what's inside it. It can't be undone.", *confirm_);
if (confirm_) widgets::dialog(c, "Delete?", question_, *confirm_);
}
} // namespace roro
+10 -4
View File
@@ -18,13 +18,18 @@ namespace roro {
// place to type them, after USB serial and the Debug Console, and trusted like the first: whoever
// holds the device can do all of it in Settings anyway (Q205).
//
// It shows what the console prints while it is open, read from a ring of the console's that exists
// only meanwhile; nothing is kept once the App is left.
// It shows the replies to its own commands, and only those unless asked otherwise (Q206): the
// console knows who each line was printed for (Console::Origin) and fills a ring that exists only
// while the App is open. Nothing is kept once it is left.
class ShellApp : public App {
public:
using Run = std::function<void(const std::string& line)>;
// What `rm` is pointed at: it decides whether the Shell asks first.
enum class Target { Missing, File, EmptyFolder, FullFolder };
using Probe = std::function<Target(const std::string& path)>;
ShellApp(Run run, const char* helpText, AppManager& apps) : run_(std::move(run)), helpText_(helpText), apps_(apps) {}
ShellApp(Run run, Probe probe, const char* helpText, AppManager& apps)
: run_(std::move(run)), probe_(std::move(probe)), helpText_(helpText), apps_(apps) {}
void onEnter() override;
void onExit() override;
@@ -39,6 +44,7 @@ class ShellApp : public App {
void runNow(const std::string& line);
Run run_;
Probe probe_;
const char* helpText_;
AppManager& apps_;
ShellLog log_;
@@ -46,7 +52,7 @@ class ShellApp : public App {
LineEditor input_{240};
InputHistory history_{16};
std::unique_ptr<DialogModel> confirm_;
std::string pending_; // the `rm` being asked about
std::string pending_, question_; // the `rm` being asked about, and what's asked
uint32_t ringPos_ = 0, seenRevision_ = 0;
int scroll_ = 0; // wrapped lines scrolled back from the bottom
bool open_ = false;
+87 -12
View File
@@ -3,6 +3,7 @@
#include <SD.h>
#include <deque>
#include <atomic>
#include <memory>
@@ -155,6 +156,7 @@ size_t getArduinoLoopTaskStackSize() { return 6144; }
static void setupSafeMode(int crashes);
static const char* helpText();
static void shellRun(const std::string& line);
static ShellApp::Target shellProbe(const std::string& path);
void setup() {
nvs.begin();
@@ -213,7 +215,7 @@ void setup() {
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService)});
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus)});
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, helpText(), *apps)});
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, shellProbe, helpText(), *apps)});
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
power->beforePowerOff = []() { apps->home(); };
apps->registerApp({"system", "System", false,
@@ -275,10 +277,13 @@ static void setupSafeMode(int crashes) {
// Dev aid: commands to drive the UI without the keyboard, from the serial port or the Debug Console.
static bool listingWanted = false;
// Who asked for each reply that is printed later (Console::Origin): the Shell sees its own, only.
static Console::Origin listingFrom, fileOpFrom, updateFrom, tasksFrom;
static void printListingWhenReady() {
if (!listingWanted || !storageService->listingReady()) return;
listingWanted = false;
Console::As as(listingFrom);
auto listing = storageService->listing();
for (size_t i = 0; i < listing.size(); i++) {
uint64_t bytes = 0;
@@ -431,6 +436,7 @@ static void fileOpsStep() {
FileOps::Status s;
if (!consoleFileOp || !fileOps->finished(s)) return;
consoleFileOp = false;
Console::As as(fileOpFrom);
static const char* const kNames[] = {"", "ls", "du", "cp", "mv", "rm", "mkdir"};
const char* name = kNames[static_cast<int>(s.op)];
if (!s.error.empty()) console.printf("%s: error %s\n", name, s.error.c_str());
@@ -464,6 +470,7 @@ static void printReleases(bool list) {
static void updateStep() {
if (!updateWatch || update->giteaBusy()) return;
Console::As as(updateFrom);
if (updateWatch == 3) {
console.printf("update: probe: %s\n", update->probeResult().c_str());
updateWatch = 0;
@@ -478,6 +485,7 @@ static void updateCommand(const String& args) {
if (update->giteaBusy()) return (void)console.println("update: busy");
args == "check" ? update->requestCheck() : update->requestList();
updateWatch = args == "check" ? 1 : 2;
updateFrom = console.origin();
} else if (args == "status") {
GiteaReleases& g = update->gitea();
console.printf("update: running %s, failed here before: %s, daily check %s, heap %u\n", update->runningVersion().c_str(),
@@ -494,6 +502,7 @@ static void updateCommand(const String& args) {
int space = rest.indexOf(' ');
update->requestProbe((space < 0 ? rest : rest.substring(0, space)).c_str(), space < 0 ? "/" : rest.substring(space + 1).c_str());
updateWatch = 3;
updateFrom = console.origin();
} else if (args.startsWith("damage ")) { // update damage cut <bytes> | flip <offset>: for the next download
long n = args.substring(args.lastIndexOf(' ') + 1).toInt();
args.startsWith("damage cut") ? update->damageNextDownload(n, -1) : update->damageNextDownload(-1, n);
@@ -513,6 +522,7 @@ static void updateCommand(const String& args) {
static void fileCommand(const String& line) {
int space = line.indexOf(' ');
std::string command = line.substring(0, space).c_str(), rest = line.substring(space + 1).c_str();
const std::string everything = rest; // rm reads its own switches
bool force = rest.rfind("-f ", 0) == 0; // replace what's there
if (force) rest = rest.substr(3);
size_t split = rest.find('\t'); // two paths: a tab between them if either has a space
@@ -524,9 +534,13 @@ static void fileCommand(const String& line) {
if (!storageService->state().present) why = "no SD card";
else if (command == "du") why = fileOps->count(rest);
else if (command == "mkdir") why = fileOps->makeFolder(rest);
else if (command == "rm") {
folder = fileOps->isFolder(rest, exists);
why = exists ? fileOps->remove(rest, folder) : "It isn't there";
else if (command == "rm") { // as Unix has it: a folder needs -r. The questions are the Shell's (-f is its)
files::RmArgs args = files::parseRm(everything);
folder = fileOps->isFolder(args.path, exists);
if (args.path.empty()) why = "rm [-r] [-f] <path>";
else if (!exists) why = "It isn't there";
else if (folder && !args.recursive) why = "It's a folder: rm -r removes it and what's in it";
else why = fileOps->remove(args.path, folder);
} else if (b.empty()) why = "two paths, please";
else {
folder = fileOps->isFolder(a, exists);
@@ -538,6 +552,7 @@ static void fileCommand(const String& line) {
}
if (!why.empty()) return (void)console.printf("%s: error %s\n", command.c_str(), why.c_str());
consoleFileOp = true;
fileOpFrom = console.origin();
}
static uint32_t loopPasses = 0; // counted in loop(), for `tasks` (issue #40)
@@ -547,6 +562,7 @@ static uint32_t tasksPasses = 0;
static void tasksStep() {
if (!tasksPending || static_cast<int32_t>(millis() - tasksDueMs) < 0) return;
tasksPending = false;
Console::As as(tasksFrom);
system_info::printTasks(console, tasksBefore, tasksTotal);
console.printf("loop: %lu passes in the last second, chip %.1f C\n", (unsigned long)(loopPasses - tasksPasses), temperatureRead());
std::vector<TaskSample>().swap(tasksBefore);
@@ -558,7 +574,7 @@ static const char* const kHelp =
"reboot restart\n"
"boot other restart into the other app slot (manual Rollback)\n"
"log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n"
"ls [folder] | du <path> | mkdir <path> | rm [-f] <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules\n"
"ls [folder] | du <path> | mkdir <path> | rm [-r] [-f] <path> | cp [-f] <from> <to> | mv [-f] <from> <to> | cancel the SD card, with the Storage App's rules (rm -r for a folder; -f: the Shell doesn't ask)\n"
"screenshot [seconds] the screen as a PNG in /screenshots on the card, now or after a pause\n"
"lora probe | lora status | lora rx on|off | lora preset <name> the LoRa radio, receive only\n"
"lora capture start|stop a LoRa Capture to /captures/lora (pcap, LoRaTap)\n"
@@ -577,6 +593,7 @@ static const char* const kHelp =
"install <path.ota> Update from SD\n"
"update check | update list | update status | update install <tag> the project's releases on Gitea\n"
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
"Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings open that App: a capital letter is an App, not a command\n"
"debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\n"
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token\n"
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
@@ -596,6 +613,7 @@ static const char* helpText() { return kHelp; }
// `screenshot [seconds]` (issue #67, Q209): the frame as it is composed, as a PNG on the card. The
// pause is for getting to the screen you want: from the Shell, "now" is always the Shell.
static bool shotPending = false;
static Console::Origin shotFrom;
static uint32_t shotDueMs = 0;
static std::atomic<bool> shotSaved{false};
@@ -611,7 +629,9 @@ static void saveScreenshot() {
local.tm_hour, local.tm_min, local.tm_sec);
} else snprintf(name, sizeof name, "/screenshots/shot-%lu.png", (unsigned long)(millis() / 1000)); // no clock yet
std::string path = name;
storageService->runJob([path]() {
Console::Origin from = shotFrom;
storageService->runJob([path, from]() {
Console::As as(from);
Canvas& frame = screen.canvas();
const uint8_t* pixels = static_cast<const uint8_t*>(frame.getBuffer());
int w = frame.width(), h = frame.height();
@@ -696,10 +716,22 @@ static void runCommand(String line, bool fromSerial = false) {
if (line.isEmpty()) return;
if (safeMode && !safeModeCommand(line)) return (void)console.println("not available in Safe Mode");
if (line == "help") console.print(kHelp);
if (line[0] >= 'A' && line[0] <= 'Z') { // an App, by its name with a capital
const AppInfo* app = apps ? apps->byCommand(line.c_str()) : nullptr;
if (!app) {
console.printf("No App called %s:", line.c_str());
if (apps)
for (auto* a : apps->visibleApps()) console.printf(" %s", AppManager::commandFor(a->id).c_str());
console.println();
} else if (apps->open(app->id)) console.printf("%s\n", app->title);
else console.println("Not now: Setup is running");
return;
}
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
if (line == "screenshot" || line.startsWith("screenshot ")) {
uint32_t seconds = constrain(line.substring(10).toInt(), 0, 60);
shotPending = true;
shotFrom = console.origin();
shotDueMs = millis() + seconds * 1000;
if (seconds) console.printf("screenshot: in %lu s\n", (unsigned long)seconds);
return;
@@ -711,6 +743,7 @@ static void runCommand(String line, bool fromSerial = false) {
system_info::printSystem(console);
console.printf("wifi: %s, ip %s, rssi %d | sd: %s, %u write faults\n", wifi->ssid().c_str(), wifi->ip().c_str(),
wifi->rssi(), storageService->state().present ? "present" : "none", (unsigned)sdLastFault().count);
if (apps) console.printf("app: %s%s\n", apps->foregroundTitle() ? apps->foregroundTitle() : "Launcher", apps->help().isOpen() ? " (the help panel is open)" : "");
console.printf("update: %s | debug console: %s\n", update->onProbation() ? "on probation" : "confirmed",
!debugConsole->on() ? "off" : debugConsole->clientConnected() ? "on, a client connected" : "on");
system_info::printSlots(console, nvs);
@@ -724,6 +757,7 @@ static void runCommand(String line, bool fromSerial = false) {
tasksDueMs = millis() + 1000;
tasksPasses = loopPasses;
tasksPending = true;
tasksFrom = console.origin();
}
if (line == "net") { // bytes each service has read and written since boot (S1, Q121)
for (int i = 0; i < static_cast<int>(net::User::Count); i++) {
@@ -750,7 +784,9 @@ static void runCommand(String line, bool fromSerial = false) {
if (line == "ls" || line.startsWith("ls ")) {
if (!storageService->state().present) return (void)console.println("sd: no card");
std::string path = line.length() > 3 ? line.substring(3).c_str() : "/";
storageService->runJob([path]() { // card access stays on the storage task
Console::Origin from = console.origin();
storageService->runJob([path, from]() { // card access stays on the storage task
Console::As as(from);
File dir = SD.open(path.c_str());
if (!dir || !dir.isDirectory()) return (void)console.printf("ls: %s is not a folder\n", path.c_str());
for (File f = dir.openNextFile(); f; f = dir.openNextFile())
@@ -764,7 +800,9 @@ static void runCommand(String line, bool fromSerial = false) {
int space = line.lastIndexOf(' ');
std::string folder = line.substring(8, space).c_str();
int count = constrain(line.substring(space + 1).toInt(), 0, 1000);
storageService->runJob([folder, count]() {
Console::Origin from = console.origin();
storageService->runJob([folder, count, from]() {
Console::As as(from);
int made = 0;
for (int i = 1; i <= count; i++) {
char name[24];
@@ -950,7 +988,9 @@ static void runCommand(String line, bool fromSerial = false) {
if (line.startsWith("sd put ")) startUpload(line.substring(7)); // then raw bytes: see uploadStep()
if (line == "sd card") { // what the card says it is, from its CID register
if (!storageService->state().present) return (void)console.println("sd card: no card");
storageService->runJob([]() {
Console::Origin from = console.origin();
storageService->runJob([from]() {
Console::As as(from);
uint8_t cid[16];
const char* type = SD.cardType() == CARD_SDHC ? "SDHC/SDXC" : SD.cardType() == CARD_SD ? "SDSC" : "MMC or unknown";
if (!sdReadCid(cid)) return (void)console.println("sd card: the card didn't answer");
@@ -963,6 +1003,7 @@ static void runCommand(String line, bool fromSerial = false) {
if (line == "sd list") {
storageService->requestListing();
listingWanted = true;
listingFrom = console.origin();
}
if (line.startsWith("gemini trust ")) { // gemini trust <host> <port> <sha256>: accept a changed certificate
char host[96] = "", fp[80] = "";
@@ -1078,10 +1119,43 @@ static void runCommand(String line, bool fromSerial = false) {
// What the Shell App runs (issue #67): trusted like USB serial (Q205), and echoed into the console so
// that a session reads the same from afar, except a token being set (Q210).
//
// The App only hands the line over: it is run from the main loop, like the consoles' commands, not
// from inside the key handler. Run from there, `rm` on a folder overflowed the main loop's stack
// (a key from the Debug Console, the App manager, the Shell, then runCommand a second time and
// printf under all of it): the loop has under 2 KB of stack to spare.
static std::deque<std::string> shellLines;
static void shellRun(const std::string& line) {
String l = line.c_str();
if (!l.startsWith("debug token ") || l == "debug token new") console.printf("> %s\n", line.c_str());
runCommand(l, true);
if (shellLines.size() < 8) shellLines.push_back(line);
}
static void shellCommands() {
while (!shellLines.empty()) {
std::string line = std::move(shellLines.front());
shellLines.pop_front();
Console::As as(Console::Origin::Shell); // what it prints, and what answers it later, is the Shell's
String l = line.c_str();
if (!l.startsWith("debug token ") || l == "debug token new") console.printf("> %s\n", line.c_str());
runCommand(l, true);
}
}
// What a path is, for the Shell to know whether `rm` deserves a question (Q209).
static ShellApp::Target shellProbe(const std::string& path) {
bool exists = false;
bool folder = fileOps->isFolder(path, exists);
if (!exists) return ShellApp::Target::Missing;
if (!folder) return ShellApp::Target::File;
bool empty = true;
storageService->runAndWait([&]() {
File dir = SD.open(path.c_str());
if (dir) {
File first = dir.openNextFile();
empty = !first;
}
});
return empty ? ShellApp::Target::EmptyFolder : ShellApp::Target::FullFolder;
}
static void serialCommands() {
@@ -1172,6 +1246,7 @@ static void loopPass() {
serialCommands();
remoteCommands();
shellCommands();
tasksStep();
screenshotStep();
ipTrialStep();
+52 -1
View File
@@ -4,6 +4,7 @@
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <algorithm>
#include <cstdio>
@@ -37,6 +38,20 @@ void Console::captureEspLogs() {
namespace {
// The tasks printing As the Shell right now, and how deep each is in it. Under ringLock. Two at
// once is the most there is (the main loop, and the storage task finishing a listing).
struct ShellTask {
TaskHandle_t task;
uint8_t depth;
};
ShellTask shellTasks[4] = {};
bool fromShellLocked(TaskHandle_t task) {
for (auto& t : shellTasks)
if (t.depth && t.task == task) return true;
return false;
}
// One ring: the last kRingBytes written, and how many were written in all. Under ringLock.
bool openOne(uint8_t*& ring, uint32_t& head) {
if (ring) return true;
@@ -97,9 +112,45 @@ void Console::toRing(const uint8_t* data, size_t len) {
data += len - kRingBytes; // only the tail can fit
len = kRingBytes;
}
TaskHandle_t task = xTaskGetCurrentTaskHandle();
portENTER_CRITICAL(&ringLock);
writeOne(ring_, head_, data, len);
writeOne(shellRing_, shellHead_, data, len);
if (shellRing_ && (shellAll_ || fromShellLocked(task))) writeOne(shellRing_, shellHead_, data, len);
portEXIT_CRITICAL(&ringLock);
}
Console::Origin Console::origin() const {
TaskHandle_t task = xTaskGetCurrentTaskHandle();
portENTER_CRITICAL(&ringLock);
bool shell = fromShellLocked(task);
portEXIT_CRITICAL(&ringLock);
return shell ? Origin::Shell : Origin::System;
}
Console::As::As(Origin origin) : entered_(false) {
if (origin != Origin::Shell) return;
TaskHandle_t task = xTaskGetCurrentTaskHandle();
portENTER_CRITICAL(&ringLock);
ShellTask* slot = nullptr;
for (auto& t : shellTasks)
if (t.depth && t.task == task) slot = &t;
if (!slot)
for (auto& t : shellTasks)
if (!t.depth && !slot) slot = &t;
if (slot) {
slot->task = task;
slot->depth++;
entered_ = true;
}
portEXIT_CRITICAL(&ringLock);
}
Console::As::~As() {
if (!entered_) return;
TaskHandle_t task = xTaskGetCurrentTaskHandle();
portENTER_CRITICAL(&ringLock);
for (auto& t : shellTasks)
if (t.depth && t.task == task) t.depth--;
portEXIT_CRITICAL(&ringLock);
}
+23 -2
View File
@@ -34,10 +34,30 @@ class Console : public Print {
// The ring only, for output that already reaches the serial port another way.
void toRing(const uint8_t* data, size_t len);
// A second ring of the same kind, for the Shell App (issue #67): open while the App is, so that
// it can show what the console prints. The two don't know of each other.
// Who asked for what is being printed. A command typed in the Shell App prints its reply `As`
// the Shell, and so does whatever answers it later from another task (a listing read by the
// storage task, `tasks` a second on): the one that starts the work notes origin() and the one that
// prints takes it back. Everything else is the System's: logs, and the consoles' own commands.
enum class Origin : uint8_t { System, Shell };
Origin origin() const; // of the task that asks, now
class As {
public:
explicit As(Origin origin);
~As();
As(const As&) = delete;
As& operator=(const As&) = delete;
private:
bool entered_;
};
// A second ring of the same kind, for the Shell App (issue #67): open while the App is. It gets
// what is printed As the Shell, and nothing else, so that the Shell shows the replies to its own
// commands and not the rest (Q206); or everything, when asked.
bool openShellRing();
void closeShellRing();
void shellShowsAll(bool all) { shellAll_ = all; }
bool shellShowsAll() const { return shellAll_; }
size_t readShellSince(uint32_t& pos, uint8_t* out, size_t max, uint32_t& skipped);
private:
@@ -45,6 +65,7 @@ class Console : public Print {
uint32_t head_ = 0; // total bytes written since the ring opened; it holds the last kRingBytes of them
uint8_t* shellRing_ = nullptr;
uint32_t shellHead_ = 0;
volatile bool shellAll_ = false;
};
extern Console console;
+5 -1
View File
@@ -140,6 +140,7 @@ bool GeminiService::fetch(const std::string& url, bool preferSaved) {
bool GeminiService::fetchToConsole(const std::string& url) {
if (busy_) return false;
toConsole_ = true;
consoleFrom_ = console.origin();
if (!fetch(url)) {
toConsole_ = false;
return false;
@@ -380,7 +381,10 @@ void GeminiService::runFetch(GeminiPage& page) {
}
}
page.ms = millis() - started;
if (toConsole_) report(page, before, lowest_);
if (toConsole_) {
Console::As as(consoleFrom_);
report(page, before, lowest_);
}
}
// One request, following up to kMaxRedirects redirects.
+2
View File
@@ -6,6 +6,7 @@
#include <string>
#include <vector>
#include "platform/console.h"
#include "event_bus.h"
#include "gemini_response.h"
#include "key_value_store.h"
@@ -95,6 +96,7 @@ class GeminiService {
// `gemini get <url>`: the same fetch, reported on the console instead of to the App.
bool fetchToConsole(const std::string& url);
Console::Origin consoleFrom_ = Console::Origin::System; // who asked for that fetch: its report is theirs
private:
enum class Job { Fetch, Save, SaveWithLinks, Refresh, Delete, Bookmark, Download, Window };