Shell: Tab completes a path on the SD card (#67)
CI / build (pull_request) Successful in 1m48s
Site / build (pull_request) Successful in 10s

Past the command's name, Tab completes the word being typed as a path: a
folder keeps its slash to go on from, a file completed whole gets a space,
several candidates are listed. Any case typed, the name's own is taken.
After a file command the first slash is understood (`cat no` is /no).
The folder is read on the storage task, bounded to 400 entries looked at
and 24 candidates.

489 host tests (2 new). Checked on the device: a folder, a file inside it,
several candidates, no slash, another case, nothing matching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-07 12:03:44 +02:00
co-authored by Claude Opus 5.5
parent 7b5df713ad
commit d17d10948d
12 changed files with 184 additions and 14 deletions
+57
View File
@@ -88,4 +88,61 @@ std::string completeCommand(const std::string& typed, const std::vector<std::str
return 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
+16
View File
@@ -43,4 +43,20 @@ std::vector<std::string> commandWords(const char* helpText);
// there is more than one.
std::string completeCommand(const std::string& typed, const std::vector<std::string>& words, std::vector<std::string>& matches);
// Tab on a later word: a path on the SD card (issue #67). What is being completed, taken apart:
// `rm -r /notes/sh` is head "rm -r ", folder "/notes/", prefix "sh". False when the cursor is still
// on the first word, when the word is a switch (-r), or when it isn't a path and the command doesn't
// take one. After a file command a path may be started without its slash: `cat no` is /no.
struct PathToComplete {
std::string head, folder, prefix;
};
bool splitForPath(const std::string& typed, PathToComplete& out);
// `names` are the folder's entries, a folder's with a slash at its end. The line with the path
// completed as far as the entries that start with the prefix agree, whatever their case (the card
// doesn't tell cases apart, so the name's own case is taken). A file completed whole gets a space
// after it; a folder keeps its slash, to go on from. `matches` gets the candidates when there are
// several. Unchanged when nothing matches.
std::string completePath(const PathToComplete& what, const std::vector<std::string>& names, std::vector<std::string>& matches);
} // namespace roro