Public Access
Fn+p saves the screen as it is to /screenshots, as the Shell's `screenshot` does, from anywhere: text fields, dialogs and the help panel included. The key never reaches an App. It refuses on Settings > Debug Console, which shows the token: a picture of that page is a copy of the token in a file (App::showsSecret). `key shot` presses it over the consoles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
205 lines
8.1 KiB
C++
205 lines
8.1 KiB
C++
#include "apps/shell_app.h"
|
|
|
|
#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"
|
|
#include "ui/widgets.h"
|
|
|
|
namespace roro {
|
|
|
|
namespace {
|
|
|
|
bool startsWith(const std::string& s, const char* prefix) { return s.rfind(prefix, 0) == 0; }
|
|
|
|
} // namespace
|
|
|
|
void ShellApp::onEnter() {
|
|
open_ = console.openShellRing();
|
|
console.shellShowsAll(false); // its own replies only, each time it's opened
|
|
ringPos_ = 0;
|
|
scroll_ = 0;
|
|
confirm_.reset();
|
|
// What Tab completes besides the firmware's own commands, written as `help` writes them.
|
|
ownHelp_ = "help | clear | quit | exit\nkey up|down|left|right|select|back|home|del|tab|space|help|shot\n";
|
|
log_.clear();
|
|
log_.add(open_ ? "The console's commands. `help` lists them." : "No memory for the Shell: leave an App, or stop IRC.");
|
|
}
|
|
|
|
// 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::string().swap(ownHelp_);
|
|
confirm_.reset();
|
|
}
|
|
|
|
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);
|
|
}
|
|
if (log_.revision() != seenRevision_) {
|
|
seenRevision_ = log_.revision();
|
|
requestRedraw();
|
|
}
|
|
}
|
|
|
|
void ShellApp::runNow(const std::string& line) {
|
|
// 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);
|
|
}
|
|
|
|
void ShellApp::enter(const std::string& line) {
|
|
history_.add(line);
|
|
scroll_ = 0;
|
|
if (line == "quit" || line == "exit") return apps_.home();
|
|
if (line == "clear") return log_.clear();
|
|
// 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));
|
|
bool ask = false;
|
|
if (args.force || args.path.empty()) {
|
|
} else if (files::hasGlob(args.path)) { // a pattern: one question for all it matches
|
|
bool more = false;
|
|
int n = count_(args.path, more);
|
|
ask = n > 0 && !more; // none, or too many: `rm` says so itself
|
|
question_ = "The " + std::to_string(n) + " that match " + args.path + (args.recursive ? ", folders and what's in them too" : "") + ". It can't be undone.";
|
|
} else {
|
|
Target target = probe_(args.path);
|
|
ask = target == Target::File || (target == Target::FullFolder && args.recursive);
|
|
question_ = target == Target::File ? args.path + ". It can't be undone." : args.path + " and everything in it. It can't be undone.";
|
|
}
|
|
if (ask) {
|
|
pending_ = line;
|
|
confirm_.reset(new DialogModel({"Cancel", "Delete"}));
|
|
return;
|
|
}
|
|
}
|
|
runNow(line);
|
|
}
|
|
|
|
bool ShellApp::onKey(const KeyEvent& e) {
|
|
requestRedraw();
|
|
if (confirm_) {
|
|
confirm_->onKey(e);
|
|
if (confirm_->result() == DialogModel::kPending) return true;
|
|
if (confirm_->result() == 1) runNow(pending_);
|
|
else log_.add("Not deleted.");
|
|
confirm_.reset();
|
|
return true;
|
|
}
|
|
// Alt + ; / Alt + . scroll back and forward; Up / Down (Fn + ; / Fn + .) recall earlier lines.
|
|
if (e.key == Key::Char && e.alt && (e.ch == ';' || e.ch == '.')) {
|
|
if (e.ch == ';') scroll_++;
|
|
else if (scroll_ > 0) scroll_--;
|
|
return true;
|
|
}
|
|
if (e.key == Key::Char && e.ctrl && (e.ch == 'b' || e.ch == 'B')) { // Q206
|
|
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;
|
|
switch (e.key) {
|
|
case Key::Char: input_.insert(e.ch); break;
|
|
case Key::Delete: input_.backspace(); break;
|
|
case Key::Left: input_.left(); break;
|
|
case Key::Right: input_.right(); break;
|
|
case Key::Up:
|
|
if (history_.up(input_.text(), recalled)) input_.setText(recalled);
|
|
break;
|
|
case Key::Down:
|
|
if (history_.down(recalled)) input_.setText(recalled);
|
|
break;
|
|
case Key::Tab: { // the command, every word of it; where its words end, a path on the card
|
|
std::vector<std::string> matches;
|
|
PathToComplete path;
|
|
bool more = false;
|
|
const std::string typed = input_.text();
|
|
std::string done = completeWords(typed, (std::string(helpText_) + ownHelp_).c_str(), matches);
|
|
if (done == typed && matches.empty() && splitForPath(typed, path)) done = completePath(path, list_(path.folder, path.prefix, more), matches);
|
|
input_.setText(done);
|
|
if (matches.size() > 1) {
|
|
std::string all;
|
|
for (auto& m : matches) all += (all.empty() ? "" : " ") + m;
|
|
log_.add(all + (more ? " ..." : ""));
|
|
}
|
|
break;
|
|
}
|
|
case Key::Select: {
|
|
std::string line = input_.text();
|
|
input_.setText("");
|
|
if (!line.empty()) enter(line);
|
|
break;
|
|
}
|
|
default: return false; // Back leaves the Shell
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ShellApp::help(std::vector<KeyHelp>& out) const {
|
|
if (confirm_) return keys::add(out, keys::kDialog);
|
|
keys::add(out, keys::kShell);
|
|
}
|
|
|
|
void ShellApp::draw(Canvas& c) {
|
|
const auto& area = theme::kContent;
|
|
const int inputH = theme::kLineHeight + 4;
|
|
const theme::Rect output{area.x, area.y, area.w, area.h - inputH - 1};
|
|
const int rows = output.h / theme::kLineHeight;
|
|
|
|
// Wrapped from the newest line backwards, only as far as the screen and the scroll need.
|
|
std::vector<std::pair<std::string, uint16_t>> shown; // newest first
|
|
auto measure = widgets::bodyMeasure(c);
|
|
c.setFont(&fonts::body);
|
|
const auto& lines = log_.lines();
|
|
int needed = rows + scroll_;
|
|
for (auto it = lines.rbegin(); it != lines.rend() && static_cast<int>(shown.size()) < needed; ++it) {
|
|
uint16_t color = it->rfind("> ", 0) == 0 ? theme::kAccent : theme::kText;
|
|
auto wrapped = wrapText(it->empty() ? std::string(" ") : *it, output.w - 8, measure);
|
|
for (auto w = wrapped.rbegin(); w != wrapped.rend(); ++w) shown.push_back({*w, color});
|
|
}
|
|
int total = static_cast<int>(shown.size());
|
|
if (scroll_ > total - rows) scroll_ = total > rows ? total - rows : 0;
|
|
c.setClipRect(output.x, output.y, output.w, output.h);
|
|
for (int r = 0; r < rows; r++) {
|
|
int i = scroll_ + (rows - 1 - r); // the row at the bottom is the newest
|
|
if (i >= total) continue;
|
|
c.setTextColor(shown[i].second);
|
|
c.drawString(shown[i].first.c_str(), 4, output.y + r * theme::kLineHeight + 1);
|
|
}
|
|
c.clearClipRect();
|
|
|
|
// State, not keys: how far back it's scrolled, and whether background lines are hidden.
|
|
c.setFont(&fonts::small);
|
|
c.setTextDatum(top_right);
|
|
if (scroll_ > 0) {
|
|
c.setTextColor(theme::kWarning);
|
|
c.drawString(("^ " + std::to_string(scroll_)).c_str(), area.w - 3, output.y + 1);
|
|
} else if (console.shellShowsAll()) {
|
|
c.setTextColor(theme::kMuted);
|
|
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?", question_, *confirm_);
|
|
}
|
|
|
|
} // namespace roro
|