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:
@@ -158,7 +158,7 @@ A note holds up to 16 KB while it's edited. A bigger text file opens read-only i
|
||||
|
||||
## Shell
|
||||
|
||||
The Shell App (docs/milestones/S1.md) runs the commands below on the device's own screen and keyboard: no PC, no cable, no Wi-Fi. **It shows the replies to its own commands and nothing else**: the console knows who each line is printed for, so a listing read by another task a moment later is still the Shell's, and what USB or the Debug Console asked for is not. Ctrl+b shows everything instead. Tab completes a command's name, Fn with up and down recalls earlier lines, Alt with up and down scrolls back. **An App's name with a capital opens it** (`Notes`, `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `System`, `Settings`), from the consoles too. `rm` is Unix's, with a question: a folder needs `-r`; a file, or a folder with something in it, is asked about unless `-f` (`rm -rf`); an empty folder goes without a word. `clear` empties the screen and `quit` leaves. It is trusted like the USB port: `debug on` and `debug token` work from it. It uses about 7 KB of memory while it is open, and none otherwise.
|
||||
The Shell App (docs/milestones/S1.md) runs the commands below on the device's own screen and keyboard: no PC, no cable, no Wi-Fi. **It shows the replies to its own commands and nothing else**: the console knows who each line is printed for, so a listing read by another task a moment later is still the Shell's, and what USB or the Debug Console asked for is not. Ctrl+b shows everything instead. Tab completes a command's name and, past it, a path on the SD card (`ls /no` gives `ls /notes/`), Fn with up and down recalls earlier lines, Alt with up and down scrolls back. **An App's name with a capital opens it** (`Notes`, `Irc`, `Wifi`, `Gnss`, `Gemini`, `Lora`, `Storage`, `System`, `Settings`), from the consoles too. `rm` is Unix's, with a question: a folder needs `-r`; a file, or a folder with something in it, is asked about unless `-f` (`rm -rf`); an empty folder goes without a word. `clear` empties the screen and `quit` leaves. It is trusted like the USB port: `debug on` and `debug token` work from it. It uses about 7 KB of memory while it is open, and none otherwise.
|
||||
|
||||
## Development aids
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ The console's commands could only be typed on a PC: over USB, or over Wi-Fi with
|
||||
| Q205 | **Trusted like USB serial**, not like the network: `debug on` and `debug token` work from it, as they do in Settings. The token is never shown. |
|
||||
| Q206 | *Revised the same day, after trying it.* **It shows the replies to its own commands, and only those.** The console knows who each line is printed for (`Console::Origin`): a command run from the Shell prints as the Shell's, and so does what answers it later from another task, which notes who asked and takes it back when it prints (`ls`, `tasks`, `du`, `cp`, `update check`, `sd list`, `screenshot`, `gemini get`). What USB or the Debug Console asked for, and the system's own lines, are not the Shell's. **Ctrl+b shows everything** instead. The first version kept whatever was printed in the ten seconds after a command, which was a guess, and a noisy one. |
|
||||
| Q207 | **Nothing while it's closed.** Open, a 4 KB ring of the console's and up to 4 KB of lines; both go when the App is left, with the list of commands for Tab. |
|
||||
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back; **Tab completes the command's first word** from the firmware's `help` text. Paths aren't completed. |
|
||||
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back. **Tab completes** the command's first word, from the firmware's `help` text, and *(added the same day)* **past it a path on the SD card**: a folder keeps its slash to go on from, a file completed whole gets a space, several candidates are listed. Whatever the case typed, the name's own is taken, since the card doesn't tell them apart. After a file command the first slash is understood (`cat no` is `/no`). A name with a space in it isn't completed. |
|
||||
| Q209 | *Revised the same day.* **`rm` is Unix's, with a question.** A folder needs `-r`, here and over the consoles (where `rm <folder>` used to remove it with what was in it). In the Shell, a file, or a folder with something in it, is asked about unless `-f` (`-rf`, `-r -f`); an empty folder with `-r` goes without a word; what `rm` would refuse anyway, it refuses itself. Over the consoles nothing is asked: scripts delete as before. **`screenshot [seconds]`** saves the screen as a PNG in `/screenshots`, now or after a pause, since from the Shell "now" is the Shell. `get`, `put`, `coredump get` and `reset` answer `Debug Console only`. |
|
||||
| Q210 | Commands are echoed as `> command` into the console, so a session reads the same from afar. **A token being set is not echoed.** |
|
||||
| Q211 | **Not in Safe Mode**, which starts no Apps: issue #77. |
|
||||
@@ -164,7 +164,8 @@ The console's commands could only be typed on a PC: over USB, or over Wi-Fi with
|
||||
- **`info` says which App is in front** (`app: Shell`): so that a hand driving the device from afar can look before it types.
|
||||
- **`screenshot`** writes the PNG a row at a time with no buffer: 8-bit indexed colour, the 256 colours of RGB332 as the palette, the pixels in one stored deflate block (`lib/files/src/png_rgb332.h`, 4 tests). 33,383 bytes for the 240 x 135 screen. A Toast says so once it is on the card, so the Toast is never in the picture.
|
||||
- **The `help` text lost its shorthand** (`update check | list | status` is written out), since Tab reads its first words from there.
|
||||
- **Cost:** 16 KB of flash, 56 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
||||
- **Tab on a path** reads the folder on the storage task while the main loop waits, so it is bounded: 400 entries looked at, 24 candidates given back, and `...` after the list when there were more.
|
||||
- **Cost:** 21 KB of flash, 96 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
||||
|
||||
### What went wrong while building it
|
||||
|
||||
@@ -188,6 +189,7 @@ Two changes came of it. The Shell now **queues** its line and the main loop runs
|
||||
| `rm -r` on an empty folder | Removed, no question |
|
||||
| `rm -r` on a folder with files | Asks; Cancel leaves it. `rm -rf` removes it without asking |
|
||||
| `rm` on a file | Asks; Delete removes it |
|
||||
| Tab on a path | `ls /no` becomes `ls /notes/`, and again, with one note in it, the note's whole name and a space. `ls /g` lists `gemini/ gnss/`. `cat no` becomes `cat /notes/`. `rm -r /CAP` becomes `rm -r /captures/`. `ls /zz` stays as it is |
|
||||
| `No`, Tab, Enter | Completes to `Notes ` and opens Notes. `Shell` from the Debug Console opens the Shell |
|
||||
| `screenshot 4`, then Home | The picture, taken four seconds later, is of the Launcher: the pause works, and the Toast isn't in it |
|
||||
| `quit` | Back to the Launcher, and the memory comes back |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -326,7 +326,7 @@ inline constexpr KeyHelp kNotesName[] = {
|
||||
// shell: Shell
|
||||
inline constexpr KeyHelp kShell[] = {
|
||||
{"Enter", "run the line"},
|
||||
{"Tab", "complete the command"},
|
||||
{"Tab", "complete: a command, a path"},
|
||||
{"Fn ; .", "lines you typed before"},
|
||||
{"Alt ; .", "scroll back, forward"},
|
||||
{"Ctrl b", "your replies only, or all"},
|
||||
|
||||
@@ -157,7 +157,7 @@ The console's commands could only be typed on a PC: over USB, or over Wi-Fi with
|
||||
| Q205 | **Trusted like USB serial**, not like the network: `debug on` and `debug token` work from it, as they do in Settings. The token is never shown. |
|
||||
| Q206 | *Revised the same day, after trying it.* **It shows the replies to its own commands, and only those.** The console knows who each line is printed for (`Console::Origin`): a command run from the Shell prints as the Shell's, and so does what answers it later from another task, which notes who asked and takes it back when it prints (`ls`, `tasks`, `du`, `cp`, `update check`, `sd list`, `screenshot`, `gemini get`). What USB or the Debug Console asked for, and the system's own lines, are not the Shell's. **Ctrl+b shows everything** instead. The first version kept whatever was printed in the ten seconds after a command, which was a guess, and a noisy one. |
|
||||
| Q207 | **Nothing while it's closed.** Open, a 4 KB ring of the console's and up to 4 KB of lines; both go when the App is left, with the list of commands for Tab. |
|
||||
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back; **Tab completes the command's first word** from the firmware's `help` text. Paths aren't completed. |
|
||||
| Q208 | Enter runs the line; Fn with up and down recalls the last 16; Alt with up and down scrolls back. **Tab completes** the command's first word, from the firmware's `help` text, and *(added the same day)* **past it a path on the SD card**: a folder keeps its slash to go on from, a file completed whole gets a space, several candidates are listed. Whatever the case typed, the name's own is taken, since the card doesn't tell them apart. After a file command the first slash is understood (`cat no` is `/no`). A name with a space in it isn't completed. |
|
||||
| Q209 | *Revised the same day.* **`rm` is Unix's, with a question.** A folder needs `-r`, here and over the consoles (where `rm <folder>` used to remove it with what was in it). In the Shell, a file, or a folder with something in it, is asked about unless `-f` (`-rf`, `-r -f`); an empty folder with `-r` goes without a word; what `rm` would refuse anyway, it refuses itself. Over the consoles nothing is asked: scripts delete as before. **`screenshot [seconds]`** saves the screen as a PNG in `/screenshots`, now or after a pause, since from the Shell "now" is the Shell. `get`, `put`, `coredump get` and `reset` answer `Debug Console only`. |
|
||||
| Q210 | Commands are echoed as `> command` into the console, so a session reads the same from afar. **A token being set is not echoed.** |
|
||||
| Q211 | **Not in Safe Mode**, which starts no Apps: issue #77. |
|
||||
@@ -172,7 +172,8 @@ The console's commands could only be typed on a PC: over USB, or over Wi-Fi with
|
||||
- **`info` says which App is in front** (`app: Shell`): so that a hand driving the device from afar can look before it types.
|
||||
- **`screenshot`** writes the PNG a row at a time with no buffer: 8-bit indexed colour, the 256 colours of RGB332 as the palette, the pixels in one stored deflate block (`lib/files/src/png_rgb332.h`, 4 tests). 33,383 bytes for the 240 x 135 screen. A Toast says so once it is on the card, so the Toast is never in the picture.
|
||||
- **The `help` text lost its shorthand** (`update check | list | status` is written out), since Tab reads its first words from there.
|
||||
- **Cost:** 16 KB of flash, 56 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
||||
- **Tab on a path** reads the folder on the storage task while the main loop waits, so it is bounded: 400 entries looked at, 24 candidates given back, and `...` after the list when there were more.
|
||||
- **Cost:** 21 KB of flash, 96 bytes of static RAM. Open: 7 KB of heap (107.6 KB free before, 100.7 with it open, 106.1 after leaving).
|
||||
|
||||
### What went wrong while building it
|
||||
|
||||
@@ -196,6 +197,7 @@ Two changes came of it. The Shell now **queues** its line and the main loop runs
|
||||
| `rm -r` on an empty folder | Removed, no question |
|
||||
| `rm -r` on a folder with files | Asks; Cancel leaves it. `rm -rf` removes it without asking |
|
||||
| `rm` on a file | Asks; Delete removes it |
|
||||
| Tab on a path | `ls /no` becomes `ls /notes/`, and again, with one note in it, the note's whole name and a space. `ls /g` lists `gemini/ gnss/`. `cat no` becomes `cat /notes/`. `rm -r /CAP` becomes `rm -r /captures/`. `ls /zz` stays as it is |
|
||||
| `No`, Tab, Enter | Completes to `Notes ` and opens Notes. `Shell` from the Debug Console opens the Shell |
|
||||
| `screenshot 4`, then Home | The picture, taken four seconds later, is of the Launcher: the pause works, and the Toast isn't in it |
|
||||
| `quit` | Back to the Launcher, and the memory comes back |
|
||||
|
||||
@@ -23,7 +23,7 @@ Type a command and press <kbd>Enter</kbd>. `help` lists them all; the [command r
|
||||
| `update check` | Whether a newer release exists |
|
||||
| `lora status` | What the radio is set to and what it has heard |
|
||||
|
||||
- <kbd>Tab</kbd> **completes** the command's name. If several fit, it lists them.
|
||||
- <kbd>Tab</kbd> **completes** what you are typing: the command's name first, then **a path on the SD card**. `ls /no` and Tab gives `ls /notes/`; Tab again goes on inside the folder. If several names fit, it completes as far as they agree and lists them. You can type the name in any case, and after a file command you can leave out the first slash: `cat no` and Tab gives `cat /notes/`. A name with a space in it isn't completed.
|
||||
- <kbd>Fn</kbd> with up and down brings back **lines you typed before**.
|
||||
- <kbd>Alt</kbd> with up and down **scrolls back** through what was printed.
|
||||
- `clear` empties the screen, and `quit` (or Back) leaves.
|
||||
|
||||
+1
-1
@@ -380,7 +380,7 @@ id = "shell"
|
||||
title = "Shell"
|
||||
rows = [
|
||||
["Enter", "run the line"],
|
||||
["Tab", "complete the command"],
|
||||
["Tab", "complete: a command, a path"],
|
||||
["Fn ; .", "lines you typed before"],
|
||||
["Alt ; .", "scroll back, forward"],
|
||||
["Ctrl b", "your replies only, or all"],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -77,6 +77,57 @@ void test_tab_completes_the_first_word() {
|
||||
TEST_ASSERT_EQUAL_STRING("n", completeCommand("n", words, matches).c_str()); // and no command starts with n here
|
||||
}
|
||||
|
||||
void test_a_later_word_is_taken_apart_as_a_path() {
|
||||
PathToComplete p;
|
||||
TEST_ASSERT_FALSE(splitForPath("ls", p)); // still the command
|
||||
TEST_ASSERT_FALSE(splitForPath("rm -r", p)); // a switch
|
||||
TEST_ASSERT_FALSE(splitForPath("irc say he", p)); // not a path, and irc doesn't take one
|
||||
TEST_ASSERT_TRUE(splitForPath("rm -r /notes/sh", p));
|
||||
TEST_ASSERT_EQUAL_STRING("rm -r ", p.head.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("/notes/", p.folder.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("sh", p.prefix.c_str());
|
||||
TEST_ASSERT_TRUE(splitForPath("ls /", p));
|
||||
TEST_ASSERT_EQUAL_STRING("/", p.folder.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("", p.prefix.c_str());
|
||||
TEST_ASSERT_TRUE(splitForPath("cat no", p)); // a file command: the slash is understood
|
||||
TEST_ASSERT_EQUAL_STRING("cat ", p.head.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("/", p.folder.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("no", p.prefix.c_str());
|
||||
TEST_ASSERT_TRUE(splitForPath("ls ", p));
|
||||
TEST_ASSERT_EQUAL_STRING("/", p.folder.c_str());
|
||||
TEST_ASSERT_TRUE(splitForPath("gemini get /x", p)); // any word that starts with a slash
|
||||
TEST_ASSERT_TRUE(splitForPath("cp /notes/a.txt /gem", p)); // the second path of two
|
||||
TEST_ASSERT_EQUAL_STRING("cp /notes/a.txt ", p.head.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("gem", p.prefix.c_str());
|
||||
}
|
||||
|
||||
void test_tab_completes_a_path() {
|
||||
std::vector<std::string> root = {"captures/", "gemini/", "gnss/", "irc/", "notes/", "updates/", "wifi/", "readme.txt"};
|
||||
std::vector<std::string> matches;
|
||||
PathToComplete p;
|
||||
splitForPath("ls /no", p);
|
||||
TEST_ASSERT_EQUAL_STRING("ls /notes/", completePath(p, root, matches).c_str()); // a folder: its slash, to go on
|
||||
TEST_ASSERT_EQUAL(0, matches.size());
|
||||
splitForPath("cat /re", p);
|
||||
TEST_ASSERT_EQUAL_STRING("cat /readme.txt ", completePath(p, root, matches).c_str()); // a file: done, a space
|
||||
splitForPath("ls /g", p);
|
||||
TEST_ASSERT_EQUAL_STRING("ls /g", completePath(p, root, matches).c_str()); // gemini, gnss: they agree on no more
|
||||
TEST_ASSERT_EQUAL(2, matches.size());
|
||||
TEST_ASSERT_EQUAL_STRING("gemini/", matches[0].c_str());
|
||||
splitForPath("ls /zz", p);
|
||||
TEST_ASSERT_EQUAL_STRING("ls /zz", completePath(p, root, matches).c_str()); // nothing: as it was
|
||||
TEST_ASSERT_EQUAL(0, matches.size());
|
||||
splitForPath("rm -r /NO", p); // the card doesn't tell cases apart: the name's own case is taken
|
||||
TEST_ASSERT_EQUAL_STRING("rm -r /notes/", completePath(p, root, matches).c_str());
|
||||
std::vector<std::string> notes = {"shopping-list.txt", "Shed.txt", "todo.txt"};
|
||||
splitForPath("cat /notes/sh", p);
|
||||
TEST_ASSERT_EQUAL_STRING("cat /notes/sh", completePath(p, notes, matches).c_str()); // "sh" and "Sh": as far as they agree
|
||||
TEST_ASSERT_EQUAL(2, matches.size());
|
||||
splitForPath("cat /notes/", p);
|
||||
TEST_ASSERT_EQUAL_STRING("cat /notes/", completePath(p, notes, matches).c_str()); // everything fits: the list
|
||||
TEST_ASSERT_EQUAL(3, matches.size());
|
||||
}
|
||||
|
||||
int main(int, char**) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_lines_arrive_in_pieces);
|
||||
@@ -84,5 +135,7 @@ int main(int, char**) {
|
||||
RUN_TEST(test_the_oldest_lines_go_past_4_kb);
|
||||
RUN_TEST(test_command_words_come_from_the_help_text);
|
||||
RUN_TEST(test_tab_completes_the_first_word);
|
||||
RUN_TEST(test_a_later_word_is_taken_apart_as_a_path);
|
||||
RUN_TEST(test_tab_completes_a_path);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user