Public Access
Tab used to complete a command's first word only. It now follows the help text word by word: `lora st` gives `lora status`, `gnss track ` lists `start stop`. The words are read from the help text as written, so a new command completes with no table to keep; the Shell's own words are added in the same notation. `*` and `?` in the last part of a path, for ls, du, rm, cp and mv, from the Shell and both consoles. The command runs once for each name matched, lined up and run by the main loop as each finishes; `cancel` empties the line-up. 64 matches at most, refused whole past that. In the Shell, rm with a pattern asks once, with the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
172 lines
5.9 KiB
C++
172 lines
5.9 KiB
C++
#include "shell_log.h"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace roro {
|
|
|
|
void ShellLog::push(const std::string& line) {
|
|
lines_.push_back(line);
|
|
bytes_ += line.size() + 1;
|
|
while (bytes_ > kMaxBytes && lines_.size() > 1) {
|
|
bytes_ -= lines_.front().size() + 1;
|
|
lines_.pop_front();
|
|
}
|
|
revision_++;
|
|
}
|
|
|
|
void ShellLog::add(const std::string& line) { push(line); }
|
|
|
|
void ShellLog::feed(const char* data, size_t len) {
|
|
for (size_t i = 0; i < len; i++) {
|
|
char c = data[i];
|
|
if (c == '\r') continue;
|
|
if (c != '\n') {
|
|
if (partial_.size() < 512) partial_ += c; // a line that never ends doesn't take the heap
|
|
continue;
|
|
}
|
|
if (partial_.rfind("status: heap ", 0) != 0) push(partial_);
|
|
partial_.clear();
|
|
}
|
|
}
|
|
|
|
void ShellLog::clear() {
|
|
lines_.clear();
|
|
partial_.clear();
|
|
bytes_ = 0;
|
|
revision_++;
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool isWord(const std::string& w) {
|
|
if (w.empty()) return false;
|
|
for (size_t i = 0; i < w.size(); i++) {
|
|
bool small = w[i] >= 'a' && w[i] <= 'z', capital = w[i] >= 'A' && w[i] <= 'Z';
|
|
if (!(small || (i == 0 && capital))) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::vector<std::string> split(const std::string& text, const std::string& by) {
|
|
std::vector<std::string> out;
|
|
size_t at = 0;
|
|
for (;;) {
|
|
size_t next = text.find(by, at);
|
|
out.push_back(text.substr(at, next == std::string::npos ? std::string::npos : next - at));
|
|
if (next == std::string::npos) return out;
|
|
at = next + by.size();
|
|
}
|
|
}
|
|
|
|
// The words a command can have at `index`, given the `index` words before it: added to `out`.
|
|
void nextWords(const std::string& command, const std::vector<std::string>& before, size_t index, std::vector<std::string>& out) {
|
|
std::vector<std::string> tokens;
|
|
for (auto& t : split(command, " "))
|
|
if (!t.empty()) tokens.push_back(t);
|
|
for (size_t i = 0; i < tokens.size(); i++) {
|
|
std::vector<std::string> either = split(tokens[i], "|"); // on|off: either of them
|
|
bool words = true;
|
|
for (auto& w : either) words = words && isWord(w);
|
|
if (!words) return; // an <argument>, an [option], "...": the command's words end here
|
|
if (i == index) {
|
|
for (auto& w : either)
|
|
if (std::find(out.begin(), out.end(), w) == out.end()) out.push_back(w);
|
|
return;
|
|
}
|
|
if (std::find(either.begin(), either.end(), before[i]) == either.end()) return; // another command
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string completeWords(const std::string& typed, const char* helpText, std::vector<std::string>& matches) {
|
|
matches.clear();
|
|
std::vector<std::string> words = split(typed, " ");
|
|
for (size_t i = 0; i + 1 < words.size(); i++)
|
|
if (words[i].empty()) return typed; // two spaces: not ours to guess
|
|
const std::string last = words.back();
|
|
words.pop_back();
|
|
if (words.empty() && last.empty()) return typed;
|
|
|
|
std::vector<std::string> next;
|
|
for (auto& line : split(helpText ? helpText : "", "\n")) {
|
|
std::string commands = line.substr(0, line.find(" ")); // the description starts at three spaces
|
|
for (auto& command : split(commands, " | ")) nextWords(command, words, words.size(), next);
|
|
}
|
|
for (auto& w : next)
|
|
if (w.rfind(last, 0) == 0) matches.push_back(w);
|
|
if (matches.empty()) return typed;
|
|
|
|
std::string common = matches[0];
|
|
for (auto& m : matches) {
|
|
size_t n = 0;
|
|
while (n < common.size() && n < m.size() && common[n] == m[n]) n++;
|
|
common.resize(n);
|
|
}
|
|
std::string head = typed.substr(0, typed.size() - last.size());
|
|
if (matches.size() == 1) {
|
|
matches.clear();
|
|
return head + common + " ";
|
|
}
|
|
return head + common;
|
|
}
|
|
|
|
namespace {
|
|
|
|
char lower(char c) { return c >= 'A' && c <= 'Z' ? static_cast<char>(c - 'A' + 'a') : c; }
|
|
|
|
bool startsWithNoCase(const std::string& name, const std::string& prefix) {
|
|
if (name.size() < prefix.size()) return false;
|
|
for (size_t i = 0; i < prefix.size(); i++)
|
|
if (lower(name[i]) != lower(prefix[i])) return false;
|
|
return true;
|
|
}
|
|
|
|
bool takesAPath(const std::string& command) {
|
|
static const char* const kCommands[] = {"ls", "du", "mkdir", "rm", "cp", "mv", "cat", "install"};
|
|
for (auto c : kCommands)
|
|
if (command == c) return true;
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool splitForPath(const std::string& typed, PathToComplete& out) {
|
|
size_t space = typed.rfind(' ');
|
|
if (space == std::string::npos) return false; // still the command's own word
|
|
std::string word = typed.substr(space + 1);
|
|
if (!word.empty() && word[0] == '-') return false; // a switch
|
|
if (word.empty() || word[0] != '/') {
|
|
if (!takesAPath(typed.substr(0, typed.find(' ')))) return false;
|
|
word = "/" + word;
|
|
}
|
|
size_t slash = word.rfind('/');
|
|
out.head = typed.substr(0, space + 1);
|
|
out.folder = word.substr(0, slash + 1);
|
|
out.prefix = word.substr(slash + 1);
|
|
return true;
|
|
}
|
|
|
|
std::string completePath(const PathToComplete& what, const std::vector<std::string>& names, std::vector<std::string>& matches) {
|
|
matches.clear();
|
|
for (auto& n : names)
|
|
if (startsWithNoCase(n, what.prefix)) matches.push_back(n);
|
|
if (matches.empty()) return what.head + what.folder + what.prefix;
|
|
std::string common = matches[0];
|
|
for (auto& m : matches) {
|
|
size_t n = 0;
|
|
while (n < common.size() && n < m.size() && lower(common[n]) == lower(m[n])) n++;
|
|
common.resize(n);
|
|
}
|
|
if (matches.size() == 1) {
|
|
bool folder = !common.empty() && common.back() == '/';
|
|
matches.clear();
|
|
return what.head + what.folder + common + (folder ? "" : " ");
|
|
}
|
|
// Several: never shorter than what was typed (cases may differ past the prefix).
|
|
if (common.size() < what.prefix.size()) common = what.prefix;
|
|
return what.head + what.folder + common;
|
|
}
|
|
|
|
} // namespace roro
|