#include "apps/shell_app.h" #include #include #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().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(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 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& 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> 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(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(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