Public Access
Shell: Tab completes a path on the SD card (#67)
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:
@@ -123,13 +123,16 @@ bool ShellApp::onKey(const KeyEvent& e) {
|
||||
case Key::Down:
|
||||
if (history_.down(recalled)) input_.setText(recalled);
|
||||
break;
|
||||
case Key::Tab: {
|
||||
case Key::Tab: { // the command's name, or past it a path on the card
|
||||
std::vector<std::string> matches;
|
||||
input_.setText(completeCommand(input_.text(), words_, matches));
|
||||
PathToComplete path;
|
||||
bool more = false;
|
||||
if (splitForPath(input_.text(), path)) input_.setText(completePath(path, list_(path.folder, path.prefix, more), matches));
|
||||
else 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);
|
||||
log_.add(all + (more ? " ..." : ""));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,12 @@ class ShellApp : public App {
|
||||
// What `rm` is pointed at: it decides whether the Shell asks first.
|
||||
enum class Target { Missing, File, EmptyFolder, FullFolder };
|
||||
using Probe = std::function<Target(const std::string& path)>;
|
||||
// A folder's entries that start with `prefix`, whatever their case, a folder's with a slash at its
|
||||
// end: what Tab completes a path from. `more` when there were too many to give them all.
|
||||
using List = std::function<std::vector<std::string>(const std::string& folder, const std::string& prefix, bool& more)>;
|
||||
|
||||
ShellApp(Run run, Probe probe, const char* helpText, AppManager& apps)
|
||||
: run_(std::move(run)), probe_(std::move(probe)), helpText_(helpText), apps_(apps) {}
|
||||
ShellApp(Run run, Probe probe, List list, const char* helpText, AppManager& apps)
|
||||
: run_(std::move(run)), probe_(std::move(probe)), list_(std::move(list)), helpText_(helpText), apps_(apps) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
@@ -45,6 +48,7 @@ class ShellApp : public App {
|
||||
|
||||
Run run_;
|
||||
Probe probe_;
|
||||
List list_;
|
||||
const char* helpText_;
|
||||
AppManager& apps_;
|
||||
ShellLog log_;
|
||||
|
||||
+34
-1
@@ -157,6 +157,7 @@ static void setupSafeMode(int crashes);
|
||||
static const char* helpText();
|
||||
static void shellRun(const std::string& line);
|
||||
static ShellApp::Target shellProbe(const std::string& path);
|
||||
static std::vector<std::string> shellList(const std::string& folder, const std::string& prefix, bool& more);
|
||||
|
||||
void setup() {
|
||||
nvs.begin();
|
||||
@@ -215,7 +216,7 @@ void setup() {
|
||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService)});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus)});
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
||||
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, shellProbe, helpText(), *apps)});
|
||||
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, shellProbe, shellList, helpText(), *apps)});
|
||||
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
|
||||
power->beforePowerOff = []() { apps->home(); };
|
||||
apps->registerApp({"system", "System", false,
|
||||
@@ -1158,6 +1159,38 @@ static ShellApp::Target shellProbe(const std::string& path) {
|
||||
return empty ? ShellApp::Target::EmptyFolder : ShellApp::Target::FullFolder;
|
||||
}
|
||||
|
||||
// The entries of a folder that Tab could complete to, for the Shell (issue #67). Read on the storage
|
||||
// task while the main loop waits, so it is held to what a crowded folder allows: 400 entries looked
|
||||
// at, 24 given back.
|
||||
static std::vector<std::string> shellList(const std::string& folder, const std::string& prefix, bool& more) {
|
||||
std::vector<std::string> names;
|
||||
more = false;
|
||||
if (!storageService->state().present) return names;
|
||||
storageService->runAndWait([&]() {
|
||||
std::string path = folder.size() > 1 ? folder.substr(0, folder.size() - 1) : folder; // without the slash at its end
|
||||
File dir = SD.open(path.c_str());
|
||||
if (!dir || !dir.isDirectory()) return;
|
||||
int seen = 0;
|
||||
for (File f = dir.openNextFile(); f; f = dir.openNextFile()) {
|
||||
if (++seen > 400) {
|
||||
more = true;
|
||||
break;
|
||||
}
|
||||
std::string name = f.name();
|
||||
bool fits = name.size() >= prefix.size();
|
||||
for (size_t i = 0; fits && i < prefix.size(); i++) fits = tolower(static_cast<unsigned char>(name[i])) == tolower(static_cast<unsigned char>(prefix[i]));
|
||||
if (!fits) continue;
|
||||
if (names.size() >= 24) {
|
||||
more = true;
|
||||
break;
|
||||
}
|
||||
names.push_back(name + (f.isDirectory() ? "/" : ""));
|
||||
}
|
||||
});
|
||||
std::sort(names.begin(), names.end());
|
||||
return names;
|
||||
}
|
||||
|
||||
static void serialCommands() {
|
||||
if (upload.active()) return readUploadBytes(); // raw file bytes, not commands
|
||||
static String line;
|
||||
|
||||
Reference in New Issue
Block a user