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
148 lines
5.9 KiB
C++
148 lines
5.9 KiB
C++
#include "file_names.h"
|
|
|
|
#include <cctype>
|
|
|
|
namespace roro::files {
|
|
|
|
const char* const kFirmwareFolders[] = {"/irc", "/wifi", "/updates", "/gnss", "/gemini", "/captures", "/notes", "/screenshots"};
|
|
const size_t kFirmwareFolderCount = sizeof kFirmwareFolders / sizeof kFirmwareFolders[0];
|
|
|
|
std::string parentOf(const std::string& path) {
|
|
size_t slash = path.rfind('/');
|
|
return slash == std::string::npos || slash == 0 ? "/" : path.substr(0, slash);
|
|
}
|
|
|
|
std::string baseName(const std::string& path) {
|
|
size_t slash = path.rfind('/');
|
|
return slash == std::string::npos ? path : path.substr(slash + 1);
|
|
}
|
|
|
|
std::string joinPath(const std::string& folder, const std::string& name) {
|
|
return folder == "/" ? "/" + name : folder + "/" + name;
|
|
}
|
|
|
|
std::string extensionOf(const std::string& name) {
|
|
size_t dot = name.rfind('.');
|
|
if (dot == std::string::npos || dot == 0) return "";
|
|
std::string ext = name.substr(dot + 1);
|
|
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
return ext;
|
|
}
|
|
|
|
bool isInside(const std::string& path, const std::string& folder) {
|
|
if (folder == "/") return true;
|
|
if (path.compare(0, folder.size(), folder) != 0) return false;
|
|
return path.size() == folder.size() || path[folder.size()] == '/';
|
|
}
|
|
|
|
std::string checkName(const std::string& name) {
|
|
if (name.empty()) return "A name can't be empty";
|
|
if (name == "." || name == "..") return "\".\" and \"..\" aren't names";
|
|
if (name.size() > 64) return "A name can be 64 characters at most";
|
|
for (char c : name) {
|
|
if (static_cast<unsigned char>(c) < 0x20) return "A name can't contain control characters";
|
|
for (char bad : std::string("/\\:*?\"<>|"))
|
|
if (c == bad) return std::string("A name can't contain ") + c;
|
|
}
|
|
if (name.back() == '.' || name.back() == ' ') return "A name can't end with a dot or a space";
|
|
return "";
|
|
}
|
|
|
|
std::string whyReadOnly(const std::string& path, const std::vector<std::string>& inUse) {
|
|
if (path == "/" || path.empty()) return "That's the card itself";
|
|
if (isInside(path, kGeminiCache)) return std::string(kGeminiCache) + " is the Gemini App's working space";
|
|
for (const std::string& open : inUse) {
|
|
if (open == path) return "It's being written right now";
|
|
if (isInside(open, path)) return "It holds a file that's being written right now";
|
|
}
|
|
for (size_t i = 0; i < kFirmwareFolderCount; i++)
|
|
if (path == kFirmwareFolders[i]) return std::string("The firmware keeps its files in ") + path;
|
|
return "";
|
|
}
|
|
|
|
std::string whyNotInto(const std::string& source, const std::string& into) {
|
|
if (isInside(into, kGeminiCache)) return std::string(kGeminiCache) + " is the Gemini App's working space";
|
|
if (isInside(into, source)) return "A folder can't go inside itself";
|
|
if (parentOf(source) == into) return "It's already there";
|
|
return "";
|
|
}
|
|
|
|
std::string copyName(const std::string& name, int n) {
|
|
size_t dot = name.rfind('.');
|
|
std::string suffix = " (" + std::to_string(n) + ")";
|
|
if (dot == std::string::npos || dot == 0) return name + suffix;
|
|
return name.substr(0, dot) + suffix + name.substr(dot);
|
|
}
|
|
|
|
FileKind kindOf(const std::string& name) {
|
|
std::string ext = extensionOf(name);
|
|
if (ext == "gpx") return FileKind::Gpx;
|
|
if (ext == "pcap") return FileKind::Pcap;
|
|
if (ext == "ota") return FileKind::Ota;
|
|
for (const char* text : {"txt", "log", "gmi", "csv", "md", "json", "ini", "conf", "ir"})
|
|
if (ext == text) return FileKind::Text;
|
|
return FileKind::Unknown;
|
|
}
|
|
|
|
bool opensAtEnd(const std::string& name) { return extensionOf(name) == "log"; }
|
|
|
|
bool looksLikeText(const uint8_t* data, size_t len) {
|
|
size_t odd = 0;
|
|
for (size_t i = 0; i < len; i++) {
|
|
uint8_t b = data[i];
|
|
if (b == 0) return false;
|
|
if (b < 0x20 && b != '\n' && b != '\r' && b != '\t') odd++;
|
|
}
|
|
return odd * 20 <= len; // a stray control character or two is still text
|
|
}
|
|
|
|
RmArgs parseRm(const std::string& args) {
|
|
RmArgs out;
|
|
size_t at = 0;
|
|
while (at < args.size()) {
|
|
while (at < args.size() && args[at] == ' ') at++;
|
|
if (at >= args.size() || args[at] != '-') break;
|
|
size_t end = args.find(' ', at);
|
|
std::string flags = args.substr(at + 1, end == std::string::npos ? std::string::npos : end - at - 1);
|
|
bool known = !flags.empty();
|
|
for (char c : flags) known = known && (c == 'r' || c == 'R' || c == 'f');
|
|
if (!known) break; // a name that starts with a dash
|
|
for (char c : flags) (c == 'f' ? out.force : out.recursive) = true;
|
|
at = end == std::string::npos ? args.size() : end;
|
|
}
|
|
out.path = at < args.size() ? args.substr(at) : "";
|
|
while (!out.path.empty() && out.path.back() == ' ') out.path.pop_back();
|
|
return out;
|
|
}
|
|
|
|
bool hasGlob(const std::string& text) { return text.find_first_of("*?") != std::string::npos; }
|
|
|
|
bool globMatch(const std::string& pattern, const std::string& name) {
|
|
auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? static_cast<char>(c - 'A' + 'a') : c; };
|
|
size_t p = 0, n = 0, star = std::string::npos, mark = 0;
|
|
while (n < name.size()) {
|
|
if (p < pattern.size() && (pattern[p] == '?' || lower(pattern[p]) == lower(name[n]))) {
|
|
p++;
|
|
n++;
|
|
} else if (p < pattern.size() && pattern[p] == '*') {
|
|
star = p++; // try it as nothing first; come back here to let it take one more
|
|
mark = n;
|
|
} else if (star != std::string::npos) {
|
|
p = star + 1;
|
|
n = ++mark;
|
|
} else return false;
|
|
}
|
|
while (p < pattern.size() && pattern[p] == '*') p++;
|
|
return p == pattern.size();
|
|
}
|
|
|
|
bool splitGlob(const std::string& path, std::string& folder, std::string& pattern) {
|
|
size_t slash = path.rfind('/');
|
|
if (slash == std::string::npos) return false;
|
|
pattern = path.substr(slash + 1);
|
|
folder = slash == 0 ? "/" : path.substr(0, slash);
|
|
return hasGlob(pattern) && !hasGlob(folder);
|
|
}
|
|
|
|
} // namespace roro::files
|