Files
roro9stack/lib/files/src/file_list.cpp
T
twislaandClaude Opus 5.5 b7aed8e91c F1 steps 2-6: the Storage App, its viewers, and Maintenance moved in (#3)
The Storage App browses the SD card: folders first with sizes and dates,
three sorts, one item at a time with a clipboard (c, x, v), rename, delete
after counting what's inside, new folder, details. A listing holds 256
entries and says when a folder has more.

FileOps does the card's work for the App and the console alike, one
operation at a time on the storage task in turns of about 150 ms, so Logs
and Captures are still written during a long copy. A copy shows progress,
can be cancelled (what it wrote is taken back) and compares sizes after.
The read-only rules are checked there: the firmware's top-level folders,
/gemini/cache, and files being written (a Track, a Capture, an upload,
today's IRC Logs). A listing reads the folder straight from FatFs: through
the Arduino File, 329 entries took over two seconds.

Viewers by type: text read a screen at a time whatever the file's size
(logs open at the end), a hex dump, a Capture's packets as the LoRa Scanner
lists them, a Track's summary, and an Update File checked as an install
would check it, without writing anything. Tab shows any file as hex or text.

Settings > Storage is gone: usage, Storage Clean-up and Erase are the App's
Maintenance, behind a warning. The Storage Warning points there.

The Clock sets the system time whatever its source, so files are dated
correctly with a GNSS Fix alone (Q137).

Console: cp, mv, mkdir, du, cancel; rm takes folders and follows the rules;
ls shows dates; Debug Builds get `sd fill`.

424 host tests. Checked on the device: docs/milestones/F1.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-06 08:45:43 +02:00

138 lines
5.0 KiB
C++

#include "file_list.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <ctime>
namespace roro::files {
namespace {
// Names compare letters only, whatever the case; ties by the bytes, so the order is total.
int compareNames(const char* a, const char* b) {
for (const char *x = a, *y = b;; ++x, ++y) {
int cx = std::tolower(static_cast<unsigned char>(*x)), cy = std::tolower(static_cast<unsigned char>(*y));
if (cx != cy) return cx < cy ? -1 : 1;
if (!cx) break;
}
return std::strcmp(a, b);
}
constexpr uint32_t kYear2020 = 1577836800;
std::string sizeText(uint32_t bytes) {
char s[16];
if (bytes < 1024) std::snprintf(s, sizeof s, "%u B", static_cast<unsigned>(bytes));
else if (bytes < 10 * 1024) std::snprintf(s, sizeof s, "%.1f KB", bytes / 1024.0);
else if (bytes < 1024 * 1024) std::snprintf(s, sizeof s, "%u KB", static_cast<unsigned>(bytes / 1024));
else if (bytes < 10u * 1024 * 1024) std::snprintf(s, sizeof s, "%.1f MB", bytes / 1048576.0);
else if (bytes < 1024u * 1024 * 1024) std::snprintf(s, sizeof s, "%u MB", static_cast<unsigned>(bytes / 1048576));
else std::snprintf(s, sizeof s, "%.1f GB", bytes / 1073741824.0);
return s;
}
} // namespace
void FileList::clear() {
std::vector<Entry>().swap(entries_);
std::vector<uint16_t>().swap(order_);
std::vector<char>().swap(names_);
more_ = wasted_ = 0;
}
bool FileList::before(const Entry& a, const Entry& b, FileSort by) const {
if (a.folder != b.folder) return a.folder;
if (!a.folder) {
if (by == FileSort::Date && a.modified != b.modified) return a.modified > b.modified;
if (by == FileSort::Size && a.size != b.size) return a.size > b.size;
}
return compareNames(names_.data() + a.name, names_.data() + b.name) < 0;
}
void FileList::add(const char* name, uint32_t size, uint32_t modified, bool folder) {
size_t len = std::strlen(name) + 1;
if (entries_.size() >= kMax) {
// Full: the new entry takes the place of the one that sorts last by name, if it sorts
// before it. The old name's bytes stay in the buffer until there's enough waste to pack.
more_++;
size_t last = 0;
for (size_t i = 1; i < entries_.size(); i++)
if (before(entries_[last], entries_[i], FileSort::Name)) last = i;
Entry candidate{static_cast<uint32_t>(names_.size()), size, modified, folder};
names_.insert(names_.end(), name, name + len);
if (!before(candidate, entries_[last], FileSort::Name)) {
names_.resize(names_.size() - len);
return;
}
wasted_ += std::strlen(names_.data() + entries_[last].name) + 1;
entries_[last] = candidate;
if (wasted_ > 2048) compact();
return;
}
if (entries_.empty()) {
entries_.reserve(32);
names_.reserve(512);
}
entries_.push_back({static_cast<uint32_t>(names_.size()), size, modified, folder});
names_.insert(names_.end(), name, name + len);
order_.push_back(static_cast<uint16_t>(order_.size()));
}
void FileList::compact() {
std::vector<char> packed;
packed.reserve(names_.size() - wasted_);
for (Entry& e : entries_) {
const char* n = names_.data() + e.name;
e.name = static_cast<uint32_t>(packed.size());
packed.insert(packed.end(), n, n + std::strlen(n) + 1);
}
names_.swap(packed);
wasted_ = 0;
}
void FileList::sort(FileSort by) {
if (wasted_) compact();
names_.shrink_to_fit();
entries_.shrink_to_fit();
order_.resize(entries_.size());
for (size_t i = 0; i < order_.size(); i++) order_[i] = static_cast<uint16_t>(i);
std::sort(order_.begin(), order_.end(), [&](uint16_t a, uint16_t b) { return before(entries_[a], entries_[b], by); });
}
int FileList::find(const std::string& name) const {
for (size_t i = 0; i < order_.size(); i++)
if (name == this->name(i)) return static_cast<int>(i);
return -1;
}
size_t FileList::bytes() const {
return entries_.capacity() * sizeof(Entry) + order_.capacity() * sizeof(uint16_t) + names_.capacity();
}
std::string formatStamp(uint32_t modified) {
if (modified < kYear2020) return "-";
time_t t = static_cast<time_t>(modified);
struct tm local;
localtime_r(&t, &local);
char s[20];
std::snprintf(s, sizeof s, "%04d-%02d-%02d %02d:%02d", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, local.tm_hour,
local.tm_min);
return s;
}
std::string rowDetail(bool folder, uint32_t size, uint32_t modified) {
if (folder) return "folder";
std::string stamp = formatStamp(modified);
return sizeText(size) + " " + (stamp == "-" ? stamp : stamp.substr(0, 10));
}
std::string fitName(const std::string& name, size_t maxChars) {
if (name.size() <= maxChars || maxChars < 8) return name.substr(0, maxChars);
size_t tail = std::min<size_t>(6, maxChars / 3), head = maxChars - tail - 2;
return name.substr(0, head) + ".." + name.substr(name.size() - tail);
}
} // namespace roro::files