Public Access
Shell: the console's commands on the device's own screen and keyboard (#67)
An App in the Launcher that runs the same commands as USB serial and the Debug Console, trusted like the first. It shows what the console prints while it is open, through a second ring of the console's that exists only meanwhile; Ctrl+b keeps only what follows your own commands. Tab completes a command's name from the firmware's help text, Fn with up and down recalls earlier lines, Alt with up and down scrolls back. `rm` asks first in the Shell, `rm -f` doesn't. Nothing is kept once the App is left. `screenshot [seconds]` saves the screen as a PNG in /screenshots on the card, now or after a pause: written a row at a time, indexed colour with RGB332 as the palette, in one stored deflate block. 487 host tests (11 new: the PNG writer, the Shell's log filter, Tab). Checked on the device over the Debug Console: commands, Tab, history, a screenshot fetched and decoded on the PC, rm with and without the question, a delayed screenshot of another screen. 12 KB of flash; 7 KB of heap while open. Decisions Q204 to Q212 in docs/milestones/S1.md. Safe Mode: #77. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
#include "apps/shell_app.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "app_keys.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();
|
||||
ringPos_ = 0;
|
||||
scroll_ = 0;
|
||||
confirm_.reset();
|
||||
words_ = commandWords(helpText_);
|
||||
words_.push_back("help");
|
||||
words_.push_back("clear"); // the Shell's own
|
||||
words_.push_back("quit");
|
||||
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();
|
||||
open_ = false;
|
||||
log_.clear();
|
||||
std::vector<std::string>().swap(words_);
|
||||
confirm_.reset();
|
||||
}
|
||||
|
||||
void ShellApp::update(uint32_t nowMs) {
|
||||
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);
|
||||
}
|
||||
if (log_.revision() != seenRevision_) {
|
||||
seenRevision_ = log_.revision();
|
||||
requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
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: 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;
|
||||
}
|
||||
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
|
||||
log_.showBackground(!log_.showsBackground());
|
||||
log_.add(log_.showsBackground() ? "Showing everything the console prints." : "Showing only what follows 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: {
|
||||
std::vector<std::string> matches;
|
||||
input_.setText(completeCommand(input_.text(), words_, matches));
|
||||
if (matches.size() > 1) {
|
||||
std::string all;
|
||||
for (auto& m : matches) all += (all.empty() ? "" : " ") + m;
|
||||
log_.add(all);
|
||||
}
|
||||
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 (!log_.showsBackground()) {
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("mine", 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_);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user