Public Access
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
197 lines
7.4 KiB
C++
197 lines
7.4 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();
|
|
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");
|
|
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::vector<std::string>().swap(words_);
|
|
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));
|
|
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);
|
|
}
|
|
|
|
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: {
|
|
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 (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
|