Public Access
Notes: edit a text file of any size (#47)
The editor held the whole note in memory and stopped at 16 KB. It now keeps a window of the file around the cursor, and the rest on the card as a list of pieces (notes::NoteDocument). Memory with a note open is what it was. Up to 64 KB a save rewrites the file, as before. Above, the five-second save appends what changed to <note>.edit, and the file is rewritten on leaving the note, with a progress bar. After a power cut, opening the note picks the edit up where it was saved; a rewrite cut short is finished or dropped, never half applied. Also: Ctrl with Fn+Up/Down go to the start and end of the note; the consoles' `key` command takes ctrl-, alt- and shift-; the Storage App's `e` no longer refuses a big file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,622 @@
|
||||
#include "note_document.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace roro::notes {
|
||||
|
||||
namespace {
|
||||
// The side file: this line, the note's size and two checksums of it (its first and last
|
||||
// kilobyte), then, in any order, text that left a window and snapshots of the list of pieces.
|
||||
// snapshot: "RSNP" cursor count { src at len }... crc32 length "PNSR" (numbers: 32 bits, low byte first)
|
||||
// The newest snapshot that checks out is the note as it was last saved. kDone at the very end:
|
||||
// the rewrite this file was for is complete in `<note>.tmp`, and only has to take the note's place.
|
||||
const char kMagic[] = "roro9stack note edits 1\n";
|
||||
constexpr size_t kMagicLen = sizeof(kMagic) - 1;
|
||||
constexpr size_t kHeaderLen = kMagicLen + 12;
|
||||
const char kSnap[] = "RSNP", kSnapEnd[] = "PNSR", kDone[] = "RDONE1\n\n";
|
||||
constexpr size_t kDoneLen = 8;
|
||||
constexpr size_t kCheck = 1024; // of each end of the note, in the header
|
||||
constexpr uint32_t kStepBytes = 64 * 1024; // a rewrite's step
|
||||
constexpr size_t kBlock = 4096;
|
||||
constexpr uint32_t kSeekNewline = 1024;
|
||||
constexpr size_t kMaxSnapshot = 12 + 9 * 4096 + 12;
|
||||
|
||||
bool continuation(int c) { return (c & 0xC0) == 0x80; }
|
||||
|
||||
uint32_t crc32(uint32_t crc, const uint8_t* data, size_t len) {
|
||||
crc = ~crc;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (int k = 0; k < 8; k++) crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1)));
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
void put32(std::string& s, uint32_t v) {
|
||||
for (int i = 0; i < 4; i++) s += static_cast<char>((v >> (8 * i)) & 0xFF);
|
||||
}
|
||||
|
||||
uint32_t get32(const uint8_t* p) { return p[0] | (p[1] << 8) | (p[2] << 16) | (static_cast<uint32_t>(p[3]) << 24); }
|
||||
|
||||
const uint8_t* bytes(const std::string& s) { return reinterpret_cast<const uint8_t*>(s.data()); }
|
||||
} // namespace
|
||||
|
||||
NoteDocument::NoteDocument(NoteCard& card, int cols, int rows) : card_(card), text_(cols, rows) { openNew(); }
|
||||
|
||||
void NoteDocument::reset() {
|
||||
path_.clear();
|
||||
sidePath_.clear();
|
||||
pieces_.clear();
|
||||
loaded_.clear();
|
||||
win_ = 0;
|
||||
windowLoaded_ = false;
|
||||
before_ = after_ = 0;
|
||||
droppedAtLoad_ = newlinesAtLoad_ = 0;
|
||||
flushedSinceSave_ = sidePending_ = false;
|
||||
sideSize_ = 0;
|
||||
windowSaved_.valid = false;
|
||||
rw_.active = false;
|
||||
std::vector<uint8_t>().swap(rw_.block);
|
||||
}
|
||||
|
||||
void NoteDocument::openNew() {
|
||||
reset();
|
||||
text_.buffer().clear();
|
||||
text_.refilled(0, 0);
|
||||
windowLoaded_ = true;
|
||||
loadedRevision_ = savedRevision_ = text_.revision();
|
||||
}
|
||||
|
||||
std::string NoteDocument::open(const std::string& path, std::string* told) {
|
||||
openNew();
|
||||
path_ = path;
|
||||
sidePath_ = side();
|
||||
uint32_t sideSize = 0, fileSize = 0, other = 0;
|
||||
bool hasSide = card_.size(sidePath_, sideSize);
|
||||
if (hasSide && sideIsDone(sideSize)) { // a rewrite was cut after its last write: finish it
|
||||
if (card_.size(tmp(), other)) {
|
||||
if (card_.size(path_, fileSize)) card_.remove(path_);
|
||||
card_.rename(tmp(), path_);
|
||||
}
|
||||
card_.remove(sidePath_);
|
||||
hasSide = false;
|
||||
}
|
||||
if (!card_.size(path_, fileSize)) {
|
||||
card_.done();
|
||||
openNew();
|
||||
return "The card refused to open it";
|
||||
}
|
||||
if (fileSize > NoteText::kMaxBytes && card_.freeBytes() < static_cast<uint64_t>(fileSize) + 16 * 1024) {
|
||||
card_.done();
|
||||
openNew();
|
||||
return "Not enough room on the card: saving it needs a second copy";
|
||||
}
|
||||
uint32_t cursor = 0;
|
||||
bool resumed = false;
|
||||
if (hasSide) {
|
||||
if (card_.size(tmp(), other)) card_.remove(tmp()); // a rewrite that didn't get that far
|
||||
sideSize_ = sideSize;
|
||||
Resume r = resume(fileSize, cursor);
|
||||
if (r == Resume::Ok) {
|
||||
resumed = sidePending_ = true;
|
||||
if (told) *told = "Your unsaved changes are back";
|
||||
} else {
|
||||
sideSize_ = 0;
|
||||
pieces_.clear();
|
||||
if (r == Resume::Mismatch) { // typed text is never thrown away without a word (Q227)
|
||||
std::string lost = sidePath_ + ".lost";
|
||||
card_.remove(lost);
|
||||
card_.rename(sidePath_, lost);
|
||||
if (told) *told = "The file changed: unsaved edits kept as .edit.lost";
|
||||
} else {
|
||||
card_.remove(sidePath_);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!resumed && fileSize) pieces_.push_back({0, 0, fileSize});
|
||||
windowLoaded_ = false;
|
||||
bool ok = load(cursor, cursor ? 1000 : 0, -1); // an edit picked up: its last lines above the cursor
|
||||
card_.done();
|
||||
if (!ok) {
|
||||
openNew();
|
||||
return "The card refused to read it";
|
||||
}
|
||||
savedRevision_ = text_.revision();
|
||||
return "";
|
||||
}
|
||||
|
||||
uint32_t NoteDocument::piecesBytes() const {
|
||||
uint32_t n = 0;
|
||||
for (const Piece& p : pieces_) n += p.len;
|
||||
return n;
|
||||
}
|
||||
|
||||
int NoteDocument::percent() const {
|
||||
uint32_t all = size();
|
||||
return all ? static_cast<int>(static_cast<uint64_t>(before_ + text_.top()) * 100 / all) : 0;
|
||||
}
|
||||
|
||||
size_t NoteDocument::readDoc(uint32_t at, uint8_t* into, size_t len) {
|
||||
size_t got = 0;
|
||||
uint32_t pos = 0;
|
||||
for (const Piece& p : pieces_) {
|
||||
if (got == len) break;
|
||||
if (at < pos + p.len) {
|
||||
uint32_t skip = at - pos;
|
||||
size_t n = std::min<size_t>(len - got, p.len - skip);
|
||||
size_t r = card_.read(fileOf(p), p.at + skip, into + got, n);
|
||||
got += r;
|
||||
at += static_cast<uint32_t>(r);
|
||||
if (r != n) break;
|
||||
}
|
||||
pos += p.len;
|
||||
}
|
||||
return got;
|
||||
}
|
||||
|
||||
int NoteDocument::byteAt(uint32_t at) {
|
||||
uint8_t b;
|
||||
return readDoc(at, &b, 1) == 1 ? b : -1;
|
||||
}
|
||||
|
||||
size_t NoteDocument::splitAt(uint32_t at) {
|
||||
uint32_t pos = 0;
|
||||
for (size_t i = 0; i < pieces_.size(); i++) {
|
||||
if (at == pos) return i;
|
||||
Piece& p = pieces_[i];
|
||||
if (at < pos + p.len) {
|
||||
uint32_t first = at - pos;
|
||||
Piece rest{p.src, p.at + first, p.len - first};
|
||||
p.len = first;
|
||||
pieces_.insert(pieces_.begin() + static_cast<long>(i) + 1, rest);
|
||||
return i + 1;
|
||||
}
|
||||
pos += p.len;
|
||||
}
|
||||
return pieces_.size();
|
||||
}
|
||||
|
||||
void NoteDocument::merge() {
|
||||
size_t kept = 0;
|
||||
for (size_t i = 0; i < pieces_.size(); i++) {
|
||||
const Piece p = pieces_[i];
|
||||
if (!p.len) continue;
|
||||
if (kept && pieces_[kept - 1].src == p.src && pieces_[kept - 1].at + pieces_[kept - 1].len == p.at) pieces_[kept - 1].len += p.len;
|
||||
else pieces_[kept++] = p;
|
||||
}
|
||||
pieces_.resize(kept);
|
||||
}
|
||||
|
||||
// The window dropped the CRs of the file's CRLFs when it was read. If it's put back untouched,
|
||||
// the cursor is further along in the file than in the window: by one for each line before it.
|
||||
uint32_t NoteDocument::noteCursor() const {
|
||||
size_t c = text_.cursor();
|
||||
if (windowLoaded_ && droppedAtLoad_ && text_.revision() == loadedRevision_) {
|
||||
const std::string& t = text_.text();
|
||||
if (droppedAtLoad_ == newlinesAtLoad_) c += static_cast<size_t>(std::count(t.begin(), t.begin() + static_cast<long>(c), '\n'));
|
||||
else if (!t.empty()) c += droppedAtLoad_ * c / t.size(); // a file of both kinds of line: near enough
|
||||
}
|
||||
return before_ + static_cast<uint32_t>(c);
|
||||
}
|
||||
|
||||
bool NoteDocument::headerFor(std::string& header) {
|
||||
uint32_t fileSize = 0;
|
||||
if (path_.empty() || !card_.size(path_, fileSize)) return false;
|
||||
std::vector<uint8_t> buf(kCheck);
|
||||
size_t n = std::min<size_t>(kCheck, fileSize);
|
||||
if (card_.read(path_, 0, buf.data(), n) != n) return false;
|
||||
uint32_t head = crc32(0, buf.data(), n);
|
||||
if (card_.read(path_, fileSize - static_cast<uint32_t>(n), buf.data(), n) != n) return false;
|
||||
uint32_t tail = crc32(0, buf.data(), n);
|
||||
header.assign(kMagic, kMagicLen);
|
||||
put32(header, fileSize);
|
||||
put32(header, head);
|
||||
put32(header, tail);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteDocument::ensureSide(std::string& why) {
|
||||
if (sideSize_) return true;
|
||||
std::string header;
|
||||
if (!headerFor(header)) {
|
||||
why = path_.empty() ? "the note has no file yet" : "the card refused to read the note";
|
||||
return false;
|
||||
}
|
||||
if (!card_.create(sidePath_) || !card_.append(sidePath_, bytes(header), header.size())) {
|
||||
card_.remove(sidePath_);
|
||||
why = "the card refused a write";
|
||||
return false;
|
||||
}
|
||||
sideSize_ = static_cast<uint32_t>(header.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteDocument::putBack(std::string& why) {
|
||||
if (!windowLoaded_) return true;
|
||||
if (text_.revision() == loadedRevision_) {
|
||||
pieces_.insert(pieces_.begin() + static_cast<long>(win_), loaded_.begin(), loaded_.end());
|
||||
} else {
|
||||
Piece p{1, 0, static_cast<uint32_t>(text_.size())};
|
||||
if (windowSaved_.valid && windowSaved_.revision == text_.revision()) {
|
||||
p.at = windowSaved_.at;
|
||||
} else if (p.len) {
|
||||
if (!ensureSide(why)) return false;
|
||||
if (!card_.append(sidePath_, bytes(text_.text()), p.len)) {
|
||||
card_.done();
|
||||
if (!card_.size(sidePath_, sideSize_)) sideSize_ = 0;
|
||||
why = "the card refused a write";
|
||||
return false;
|
||||
}
|
||||
p.at = sideSize_;
|
||||
sideSize_ += p.len;
|
||||
}
|
||||
if (p.len) pieces_.insert(pieces_.begin() + static_cast<long>(win_), p);
|
||||
flushedSinceSave_ = sidePending_ = true;
|
||||
}
|
||||
windowLoaded_ = false;
|
||||
loaded_.clear();
|
||||
windowSaved_.valid = false;
|
||||
before_ = after_ = 0;
|
||||
merge();
|
||||
return true;
|
||||
}
|
||||
|
||||
// The window's start is where a line starts on screen whenever that can be known: after a
|
||||
// newline, or where the window before had a line start. Otherwise the same text could wrap
|
||||
// differently from one window to the next.
|
||||
bool NoteDocument::load(uint32_t cursor, int row, int64_t startHint) {
|
||||
uint32_t total = piecesBytes();
|
||||
cursor = std::min(cursor, total);
|
||||
uint32_t s = 0, e = total;
|
||||
if (total > NoteText::kMaxBytes - kEdge) {
|
||||
uint32_t c = cursor > kHalf ? cursor - kHalf : 0;
|
||||
if (c == 0) {
|
||||
s = 0;
|
||||
} else if (startHint >= 0 && startHint <= static_cast<int64_t>(c)) {
|
||||
s = static_cast<uint32_t>(startHint);
|
||||
} else {
|
||||
uint8_t buf[128];
|
||||
uint32_t at = c, limit = std::min(c + kSeekNewline, cursor);
|
||||
bool found = false;
|
||||
while (at < limit && !found) {
|
||||
size_t n = readDoc(at, buf, std::min<size_t>(sizeof buf, limit - at));
|
||||
if (!n) break;
|
||||
for (size_t i = 0; i < n && !found; i++)
|
||||
if (buf[i] == '\n') {
|
||||
s = at + static_cast<uint32_t>(i) + 1;
|
||||
found = true;
|
||||
}
|
||||
at += static_cast<uint32_t>(n);
|
||||
}
|
||||
if (!found) {
|
||||
s = c;
|
||||
for (int k = 0; k < 3 && s < cursor && continuation(byteAt(s)); k++) s++;
|
||||
}
|
||||
}
|
||||
e = std::min(total, cursor + kHalf);
|
||||
for (int k = 0; k < 3 && e < total && continuation(byteAt(e)); k++) e++;
|
||||
if (e < total && e > 0 && byteAt(e) == '\n' && byteAt(e - 1) == '\r') e++;
|
||||
e = std::min<uint32_t>(e, s + NoteText::kMaxBytes);
|
||||
}
|
||||
size_t i0 = splitAt(s), i1 = splitAt(e);
|
||||
loaded_.assign(pieces_.begin() + static_cast<long>(i0), pieces_.begin() + static_cast<long>(i1));
|
||||
pieces_.erase(pieces_.begin() + static_cast<long>(i0), pieces_.begin() + static_cast<long>(i1));
|
||||
win_ = i0;
|
||||
before_ = s;
|
||||
after_ = total - e;
|
||||
std::string& b = text_.buffer();
|
||||
b.resize(e - s);
|
||||
size_t got = 0;
|
||||
bool ok = true;
|
||||
for (const Piece& p : loaded_) {
|
||||
size_t n = card_.read(fileOf(p), p.at, reinterpret_cast<uint8_t*>(&b[got]), p.len);
|
||||
got += n;
|
||||
if (n != p.len) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) { // the note is whole in its pieces: stand on an empty window where the cursor was
|
||||
pieces_.insert(pieces_.begin() + static_cast<long>(i0), loaded_.begin(), loaded_.end());
|
||||
loaded_.clear();
|
||||
win_ = splitAt(cursor);
|
||||
before_ = cursor;
|
||||
after_ = total - cursor;
|
||||
s = cursor;
|
||||
b.clear();
|
||||
}
|
||||
droppedAtLoad_ = text_.refilled(cursor - s, row);
|
||||
newlinesAtLoad_ = static_cast<size_t>(std::count(b.begin(), b.end(), '\n'));
|
||||
loadedRevision_ = text_.revision();
|
||||
windowLoaded_ = true;
|
||||
windowSaved_.valid = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool NoteDocument::wantsMove() const {
|
||||
if (!windowLoaded_) return true;
|
||||
size_t n = text_.size(), c = text_.cursor();
|
||||
if (n + kSpare >= NoteText::kMaxBytes) return true;
|
||||
if (before_ && c < kEdge) return true;
|
||||
return after_ && n - c < kEdge;
|
||||
}
|
||||
|
||||
bool NoteDocument::move(std::string& why) {
|
||||
uint32_t cursor = noteCursor();
|
||||
int row = text_.cursorRow();
|
||||
int64_t hint = -1;
|
||||
if (windowLoaded_ && !droppedAtLoad_ && cursor > kHalf) {
|
||||
uint32_t c = cursor - kHalf;
|
||||
if (c >= before_ && c < before_ + text_.size()) hint = static_cast<int64_t>(before_) + static_cast<int64_t>(text_.startOfLine(c - before_));
|
||||
}
|
||||
if (!putBack(why)) return false;
|
||||
bool ok = load(cursor, row, hint);
|
||||
card_.done();
|
||||
if (!ok) why = "the card refused to read";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool NoteDocument::jump(uint32_t to, std::string& why) {
|
||||
to = std::min(to, size());
|
||||
if (windowLoaded_ && to == 0 && !before_) return text_.toStart(), true;
|
||||
if (windowLoaded_ && to == size() && !after_) return text_.toEnd(), true;
|
||||
if (!putBack(why)) return false;
|
||||
bool ok = load(to, to ? 1000 : 0, -1);
|
||||
card_.done();
|
||||
if (!ok) why = "the card refused to read";
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool NoteDocument::wantsRewrite() const { return size() <= kWholeLimit || sideSize_ > kSideLimit || pieces_.size() > kManyPieces; }
|
||||
|
||||
bool NoteDocument::journal(std::string& why) {
|
||||
if (path_.empty()) {
|
||||
why = "the note has no file yet";
|
||||
return false;
|
||||
}
|
||||
bool modified = text_.revision() != loadedRevision_;
|
||||
Piece w{1, 0, static_cast<uint32_t>(text_.size())};
|
||||
uint32_t cursor = noteCursor();
|
||||
if (!ensureSide(why)) return false;
|
||||
bool wrote = true;
|
||||
if (modified && w.len) {
|
||||
if (windowSaved_.valid && windowSaved_.revision == text_.revision()) {
|
||||
w.at = windowSaved_.at;
|
||||
} else if ((wrote = card_.append(sidePath_, bytes(text_.text()), w.len))) {
|
||||
w.at = sideSize_;
|
||||
sideSize_ += w.len;
|
||||
windowSaved_.valid = true;
|
||||
windowSaved_.at = w.at;
|
||||
windowSaved_.len = w.len;
|
||||
windowSaved_.revision = text_.revision();
|
||||
}
|
||||
}
|
||||
if (wrote) {
|
||||
std::vector<Piece> all(pieces_.begin(), pieces_.begin() + static_cast<long>(win_));
|
||||
if (!modified) all.insert(all.end(), loaded_.begin(), loaded_.end());
|
||||
else if (w.len) all.push_back(w);
|
||||
all.insert(all.end(), pieces_.begin() + static_cast<long>(win_), pieces_.end());
|
||||
std::string rec(kSnap, 4);
|
||||
put32(rec, cursor);
|
||||
put32(rec, static_cast<uint32_t>(all.size()));
|
||||
for (const Piece& p : all) {
|
||||
rec += static_cast<char>(p.src);
|
||||
put32(rec, p.at);
|
||||
put32(rec, p.len);
|
||||
}
|
||||
put32(rec, crc32(0, bytes(rec), rec.size()));
|
||||
put32(rec, static_cast<uint32_t>(rec.size()) + 8);
|
||||
rec.append(kSnapEnd, 4);
|
||||
wrote = card_.append(sidePath_, bytes(rec), rec.size());
|
||||
if (wrote) sideSize_ += static_cast<uint32_t>(rec.size());
|
||||
}
|
||||
card_.done();
|
||||
if (!wrote) {
|
||||
windowSaved_.valid = false;
|
||||
if (!card_.size(sidePath_, sideSize_)) sideSize_ = 0;
|
||||
why = "the card refused a write";
|
||||
return false;
|
||||
}
|
||||
savedRevision_ = text_.revision();
|
||||
flushedSinceSave_ = false;
|
||||
sidePending_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteDocument::rewriteStart(std::string& why) {
|
||||
if (path_.empty()) {
|
||||
why = "the note has no file yet";
|
||||
return false;
|
||||
}
|
||||
if (card_.freeBytes() < static_cast<uint64_t>(size()) + 16 * 1024) {
|
||||
why = "the card is full";
|
||||
return false;
|
||||
}
|
||||
if (!card_.create(tmp())) {
|
||||
why = "the card refused to open a file";
|
||||
return false;
|
||||
}
|
||||
rw_.active = true;
|
||||
rw_.stage = 0;
|
||||
rw_.index = 0;
|
||||
rw_.offset = rw_.done = rw_.wrote = rw_.wroteBefore = rw_.wroteAfter = 0;
|
||||
rw_.total = size();
|
||||
rw_.revision = text_.revision();
|
||||
rw_.carry = false;
|
||||
rw_.block.resize(kBlock);
|
||||
return true;
|
||||
}
|
||||
|
||||
int NoteDocument::rewriteStep(std::string& why) {
|
||||
if (!rw_.active) return -1;
|
||||
auto failed = [&](const char* what) {
|
||||
card_.done();
|
||||
card_.remove(tmp());
|
||||
rw_.active = false;
|
||||
std::vector<uint8_t>().swap(rw_.block);
|
||||
why = what;
|
||||
return -1;
|
||||
};
|
||||
auto out = [&](const uint8_t* data, size_t len) {
|
||||
if (!len) return true;
|
||||
if (!card_.append(tmp(), data, len)) return false;
|
||||
rw_.wrote += static_cast<uint32_t>(len);
|
||||
if (rw_.stage == 0) rw_.wroteBefore += static_cast<uint32_t>(len);
|
||||
if (rw_.stage == 2) rw_.wroteAfter += static_cast<uint32_t>(len);
|
||||
return true;
|
||||
};
|
||||
const uint8_t cr = '\r';
|
||||
uint32_t budget = kStepBytes;
|
||||
while (budget > 0 && rw_.stage < 3) {
|
||||
if (rw_.stage == 1) {
|
||||
uint32_t left = static_cast<uint32_t>(text_.size()) - rw_.offset;
|
||||
if (!left) {
|
||||
rw_.stage = 2;
|
||||
rw_.index = win_;
|
||||
rw_.offset = 0;
|
||||
continue;
|
||||
}
|
||||
uint32_t n = std::min(left, budget);
|
||||
if (!out(bytes(text_.text()) + rw_.offset, n)) return failed("the card refused a write");
|
||||
rw_.offset += n;
|
||||
rw_.done += n;
|
||||
budget -= n;
|
||||
continue;
|
||||
}
|
||||
size_t end = rw_.stage == 0 ? win_ : pieces_.size();
|
||||
bool pieceOver = rw_.index < end && rw_.offset >= pieces_[rw_.index].len;
|
||||
if (rw_.index >= end || pieceOver) {
|
||||
if (rw_.carry && !out(&cr, 1)) return failed("the card refused a write"); // a CR that ended its piece stays
|
||||
rw_.carry = false;
|
||||
rw_.offset = 0;
|
||||
if (pieceOver) rw_.index++;
|
||||
else rw_.stage++;
|
||||
continue;
|
||||
}
|
||||
const Piece& p = pieces_[rw_.index];
|
||||
uint32_t n = std::min<uint32_t>(std::min<uint32_t>(p.len - rw_.offset, budget), static_cast<uint32_t>(rw_.block.size()));
|
||||
uint8_t* b = rw_.block.data();
|
||||
if (card_.read(fileOf(p), p.at + rw_.offset, b, n) != n) return failed("the card refused to read the note");
|
||||
size_t m = n;
|
||||
if (p.src == 0) { // CRLF becomes LF (Q148, Q230), in the file's own text: what was typed has none
|
||||
if (rw_.carry && b[0] != '\n' && !out(&cr, 1)) return failed("the card refused a write");
|
||||
rw_.carry = false;
|
||||
m = 0;
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
if (b[i] == '\r') {
|
||||
if (i + 1 == n) {
|
||||
rw_.carry = true;
|
||||
continue;
|
||||
}
|
||||
if (b[i + 1] == '\n') continue;
|
||||
}
|
||||
b[m++] = b[i];
|
||||
}
|
||||
}
|
||||
if (!out(b, m)) return failed("the card refused a write");
|
||||
rw_.offset += n;
|
||||
rw_.done += n;
|
||||
budget -= n;
|
||||
}
|
||||
if (rw_.stage < 3) return std::min(99, static_cast<int>(static_cast<uint64_t>(rw_.done) * 100 / std::max<uint32_t>(1, rw_.total)));
|
||||
|
||||
// All of it is in the temporary file. From the mark in the side file on, the rewrite counts as
|
||||
// done: whatever is cut after that, opening the note finishes it.
|
||||
card_.done();
|
||||
uint32_t written = 0, old = 0;
|
||||
if (!card_.size(tmp(), written) || written != rw_.wrote) return failed("the card refused a write");
|
||||
if (sideSize_) {
|
||||
if (!card_.append(sidePath_, reinterpret_cast<const uint8_t*>(kDone), kDoneLen)) return failed("the card refused a write");
|
||||
sideSize_ += kDoneLen;
|
||||
card_.done();
|
||||
}
|
||||
rw_.active = false;
|
||||
std::vector<uint8_t>().swap(rw_.block);
|
||||
// FAT can't rename onto a file. Between these two lines only the temporary file exists: the
|
||||
// Notes list puts such a file back under its name.
|
||||
if ((card_.size(path_, old) && !card_.remove(path_)) || !card_.rename(tmp(), path_)) {
|
||||
card_.done();
|
||||
why = "the card refused to replace the note";
|
||||
return -1;
|
||||
}
|
||||
if (sideSize_) card_.remove(sidePath_);
|
||||
card_.done();
|
||||
sideSize_ = 0;
|
||||
uint32_t window = static_cast<uint32_t>(text_.size());
|
||||
pieces_.clear();
|
||||
loaded_.clear();
|
||||
if (rw_.wroteBefore) pieces_.push_back({0, 0, rw_.wroteBefore});
|
||||
win_ = pieces_.size();
|
||||
if (rw_.wroteAfter) pieces_.push_back({0, rw_.wroteBefore + window, rw_.wroteAfter});
|
||||
if (window) loaded_.push_back({0, rw_.wroteBefore, window});
|
||||
before_ = rw_.wroteBefore;
|
||||
after_ = rw_.wroteAfter;
|
||||
loadedRevision_ = savedRevision_ = rw_.revision;
|
||||
droppedAtLoad_ = 0;
|
||||
windowLoaded_ = true;
|
||||
flushedSinceSave_ = sidePending_ = false;
|
||||
windowSaved_.valid = false;
|
||||
return 100;
|
||||
}
|
||||
|
||||
bool NoteDocument::sideIsDone(uint32_t sideSize) {
|
||||
uint8_t tail[kDoneLen];
|
||||
return sideSize >= kHeaderLen + kDoneLen && card_.read(sidePath_, sideSize - kDoneLen, tail, kDoneLen) == kDoneLen &&
|
||||
std::memcmp(tail, kDone, kDoneLen) == 0;
|
||||
}
|
||||
|
||||
NoteDocument::Resume NoteDocument::resume(uint32_t fileSize, uint32_t& cursor) {
|
||||
uint8_t header[kHeaderLen];
|
||||
if (sideSize_ < kHeaderLen || card_.read(sidePath_, 0, header, kHeaderLen) != kHeaderLen || std::memcmp(header, kMagic, kMagicLen) != 0)
|
||||
return Resume::Nothing;
|
||||
|
||||
// The newest snapshot that checks out, looking back from the end: after it there may be text
|
||||
// that left a window, or a write the power cut short.
|
||||
auto snapshotEndingAt = [&](uint32_t end) {
|
||||
uint8_t lenBytes[4];
|
||||
if (end < kHeaderLen + 24 || card_.read(sidePath_, end - 8, lenBytes, 4) != 4) return false;
|
||||
uint32_t len = get32(lenBytes);
|
||||
if (len < 24 || len > kMaxSnapshot || len > end - kHeaderLen || (len - 24) % 9) return false;
|
||||
std::string rec(len, '\0');
|
||||
if (card_.read(sidePath_, end - len, reinterpret_cast<uint8_t*>(&rec[0]), len) != len) return false;
|
||||
const uint8_t* r = bytes(rec);
|
||||
if (std::memcmp(r, kSnap, 4) != 0 || get32(r + len - 12) != crc32(0, r, len - 12)) return false;
|
||||
uint32_t count = get32(r + 8);
|
||||
if (count != (len - 24) / 9) return false;
|
||||
std::vector<Piece> list;
|
||||
list.reserve(count);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
const uint8_t* q = r + 12 + 9 * i;
|
||||
Piece p{q[0], get32(q + 1), get32(q + 5)};
|
||||
uint64_t stop = static_cast<uint64_t>(p.at) + p.len;
|
||||
if (p.src > 1 || !p.len) return false;
|
||||
if (p.src == 1 && (p.at < kHeaderLen || stop > end - len)) return false;
|
||||
list.push_back(p);
|
||||
}
|
||||
pieces_.swap(list);
|
||||
cursor = get32(r + 4);
|
||||
return true;
|
||||
};
|
||||
bool found = false;
|
||||
std::vector<uint8_t> buf(1024 + 3);
|
||||
for (uint32_t end = sideSize_; end > kHeaderLen && !found;) {
|
||||
uint32_t a = end > 1024 + kHeaderLen ? end - 1024 : static_cast<uint32_t>(kHeaderLen);
|
||||
size_t n = card_.read(sidePath_, a, buf.data(), std::min<size_t>(buf.size(), sideSize_ - a));
|
||||
for (size_t i = n >= 4 ? n - 4 + 1 : 0; i-- > 0 && !found;)
|
||||
if (std::memcmp(buf.data() + i, kSnapEnd, 4) == 0) found = snapshotEndingAt(a + static_cast<uint32_t>(i) + 4);
|
||||
end = a;
|
||||
}
|
||||
if (!found) return Resume::Nothing;
|
||||
std::string expect;
|
||||
if (!headerFor(expect) || std::memcmp(header, expect.data(), kHeaderLen) != 0) {
|
||||
pieces_.clear();
|
||||
return Resume::Mismatch;
|
||||
}
|
||||
for (const Piece& p : pieces_)
|
||||
if (p.src == 0 && static_cast<uint64_t>(p.at) + p.len > fileSize) return pieces_.clear(), Resume::Mismatch;
|
||||
merge();
|
||||
return Resume::Ok;
|
||||
}
|
||||
|
||||
} // namespace roro::notes
|
||||
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "note_text.h"
|
||||
|
||||
namespace roro::notes {
|
||||
|
||||
// The card, as a note needs it. On the device every call is made on the storage task.
|
||||
class NoteCard {
|
||||
public:
|
||||
virtual ~NoteCard() = default;
|
||||
virtual bool size(const std::string& path, uint32_t& size) = 0; // false: no such file
|
||||
virtual size_t read(const std::string& path, uint32_t at, uint8_t* into, size_t len) = 0;
|
||||
virtual bool create(const std::string& path) = 0; // an empty file, in place of what was there
|
||||
virtual bool append(const std::string& path, const uint8_t* data, size_t len) = 0;
|
||||
virtual bool remove(const std::string& path) = 0;
|
||||
virtual bool rename(const std::string& from, const std::string& to) = 0;
|
||||
virtual uint64_t freeBytes() = 0;
|
||||
virtual void done() {} // what was appended is on the card now (files kept open are closed)
|
||||
};
|
||||
|
||||
// A text file of any size, edited (issue #47, F1 Q223-Q232). The file stays on the card; what is
|
||||
// in memory is one window of it, a NoteText of up to 16 KB around the cursor, and a list of pieces
|
||||
// saying what the rest is made of: runs of bytes of the file, and runs of the side file
|
||||
// `<note>.edit`, where a window that was changed is written when the cursor leaves it.
|
||||
//
|
||||
// the note = pieces before the window + the window + pieces after it
|
||||
//
|
||||
// Saving comes in two kinds. `journal` appends the window and the list of pieces to the side
|
||||
// file: quick whatever the note's size, and enough to pick the edit up after a power cut.
|
||||
// `rewrite` streams the whole note into `<note>.tmp` and puts it in the note's place: the file is
|
||||
// then the note again, and the side file goes. A note of up to 64 KB is always rewritten.
|
||||
//
|
||||
// Every method marked [card] reads or writes the card.
|
||||
class NoteDocument {
|
||||
public:
|
||||
static constexpr uint32_t kHalf = 4096; // loaded on each side of the cursor
|
||||
static constexpr uint32_t kEdge = 2048; // this near an end of the window, it moves
|
||||
static constexpr uint32_t kSpare = 256; // this near full, it moves
|
||||
static constexpr uint32_t kWholeLimit = 64 * 1024; // up to here a save is a rewrite
|
||||
static constexpr uint32_t kSideLimit = 1024 * 1024; // a side file this big asks for a rewrite
|
||||
static constexpr size_t kManyPieces = 256; // and so does a list this long
|
||||
|
||||
NoteDocument(NoteCard& card, int cols, int rows);
|
||||
|
||||
// [card] "" or why not. `told`: something the user should read (an edit picked up, or set aside).
|
||||
std::string open(const std::string& path, std::string* told = nullptr);
|
||||
void openNew(); // nothing on the card until the first rewrite
|
||||
const std::string& path() const { return path_; }
|
||||
void setPath(const std::string& path) { // a new note's, before its first rewrite
|
||||
path_ = path;
|
||||
sidePath_ = path.empty() ? "" : side();
|
||||
}
|
||||
|
||||
NoteText& text() { return text_; }
|
||||
const NoteText& text() const { return text_; }
|
||||
uint32_t size() const { return before_ + static_cast<uint32_t>(text_.size()) + after_; }
|
||||
uint32_t cursor() const { return before_ + static_cast<uint32_t>(text_.cursor()); } // in the note
|
||||
int percent() const;
|
||||
bool windowed() const { return before_ || after_; } // the note is more than its window
|
||||
|
||||
// After each key: the cursor is near an end of the window that isn't an end of the note, or
|
||||
// the window is nearly full.
|
||||
bool wantsMove() const;
|
||||
bool move(std::string& why); // [card] the window, around the cursor
|
||||
bool jump(uint32_t to, std::string& why); // [card] the cursor, anywhere in the note
|
||||
|
||||
bool dirty() const { return text_.revision() != savedRevision_ || flushedSinceSave_; } // the card doesn't have it
|
||||
bool filePending() const { return sidePending_; } // saved, but in the side file: a rewrite is owed
|
||||
bool wantsRewrite() const; // the next save should be a rewrite
|
||||
bool journal(std::string& why); // [card]
|
||||
bool rewriteStart(std::string& why); // [card]
|
||||
int rewriteStep(std::string& why); // [card] percent done; 100: the file is the note; -1: failed
|
||||
bool rewriting() const { return rw_.active; }
|
||||
|
||||
private:
|
||||
struct Piece {
|
||||
uint8_t src; // 0: the note's file, 1: the side file
|
||||
uint32_t at, len;
|
||||
};
|
||||
enum class Resume { Ok, Mismatch, Nothing };
|
||||
|
||||
std::string side() const { return path_ + ".edit"; }
|
||||
std::string tmp() const { return path_ + ".tmp"; }
|
||||
const std::string& fileOf(const Piece& p) const { return p.src ? sidePath_ : path_; }
|
||||
uint32_t piecesBytes() const;
|
||||
size_t readDoc(uint32_t at, uint8_t* into, size_t len); // from the pieces: the window is put back first
|
||||
int byteAt(uint32_t at);
|
||||
size_t splitAt(uint32_t at); // the index of the piece that starts there
|
||||
void merge();
|
||||
uint32_t noteCursor() const; // where the cursor is among the pieces once the window is put back
|
||||
bool putBack(std::string& why);
|
||||
bool load(uint32_t cursor, int row, int64_t startHint); // false: the card refused, and the window is empty
|
||||
bool ensureSide(std::string& why);
|
||||
bool headerFor(std::string& header);
|
||||
Resume resume(uint32_t fileSize, uint32_t& cursor);
|
||||
bool sideIsDone(uint32_t sideSize);
|
||||
void reset();
|
||||
|
||||
NoteCard& card_;
|
||||
NoteText text_;
|
||||
std::string path_, sidePath_;
|
||||
std::vector<Piece> pieces_; // without the window while it's loaded
|
||||
std::vector<Piece> loaded_; // what the window was read from
|
||||
size_t win_ = 0; // the window sits before pieces_[win_]
|
||||
bool windowLoaded_ = false;
|
||||
uint32_t before_ = 0, after_ = 0;
|
||||
uint32_t loadedRevision_ = 0, savedRevision_ = 0;
|
||||
size_t droppedAtLoad_ = 0, newlinesAtLoad_ = 0;
|
||||
bool flushedSinceSave_ = false, sidePending_ = false;
|
||||
uint32_t sideSize_ = 0; // 0: no side file
|
||||
struct {
|
||||
bool valid = false;
|
||||
uint32_t at = 0, len = 0, revision = 0;
|
||||
} windowSaved_; // the window as the side file already has it
|
||||
struct {
|
||||
bool active = false;
|
||||
int stage = 0; // 0: pieces before, 1: the window, 2: pieces after
|
||||
size_t index = 0;
|
||||
uint32_t offset = 0, done = 0, total = 0, wrote = 0, wroteBefore = 0, wroteAfter = 0, revision = 0;
|
||||
bool carry = false; // a CR at the end of a block, waiting to see what follows
|
||||
std::vector<uint8_t> block;
|
||||
} rw_;
|
||||
};
|
||||
|
||||
} // namespace roro::notes
|
||||
@@ -16,11 +16,24 @@ NoteText::NoteText(int cols, int rows, std::string&& text) : cols_(std::max(1, c
|
||||
text_.reserve(kMaxBytes);
|
||||
}
|
||||
|
||||
void NoteText::dropCarriageReturns() {
|
||||
size_t kept = 0;
|
||||
for (size_t i = 0; i < text_.size(); i++)
|
||||
if (!(text_[i] == '\r' && i + 1 < text_.size() && text_[i + 1] == '\n')) text_[kept++] = text_[i];
|
||||
size_t NoteText::dropCarriageReturns(size_t* follow) {
|
||||
size_t kept = 0, size = text_.size(), place = follow ? *follow : 0;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
if (follow && i == place) *follow = kept;
|
||||
if (!(text_[i] == '\r' && i + 1 < size && text_[i + 1] == '\n')) text_[kept++] = text_[i];
|
||||
}
|
||||
if (follow && place >= size) *follow = kept;
|
||||
text_.resize(kept);
|
||||
return size - kept;
|
||||
}
|
||||
|
||||
size_t NoteText::refilled(size_t cursor, int row) {
|
||||
size_t dropped = dropCarriageReturns(&cursor);
|
||||
cursor_ = std::min(cursor, text_.size());
|
||||
while (cursor_ > 0 && cursor_ < text_.size() && continuation(text_[cursor_])) cursor_--;
|
||||
top_ = lineOf(cursor_);
|
||||
for (int i = 0; i < row && top_ > 0; i++) top_ = lineOf(top_ - 1);
|
||||
return dropped;
|
||||
}
|
||||
|
||||
bool NoteText::setText(const std::string& text) {
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
namespace roro::notes {
|
||||
|
||||
// The text of a note while it's edited (F1, Q144, Q145): UTF-8 held whole in memory, a cursor, and
|
||||
// the part of it on screen. Lines wrap at spaces, `cols` characters wide; a line owns the space or
|
||||
// The text of a note while it's edited (F1, Q144, Q145): UTF-8 held in memory, a cursor, and the
|
||||
// part of it on screen. Up to 16 KB: a longer note is edited through NoteDocument (note_document.h),
|
||||
// which keeps this as its window on the file. Lines wrap at spaces, `cols` characters wide; a line owns the space or
|
||||
// the newline it ends with, so every byte of the text belongs to exactly one line. No index of
|
||||
// lines is kept (a note of newlines alone would need twice its size): where a line starts is
|
||||
// worked out from the start of its paragraph, which is never far.
|
||||
@@ -27,8 +28,17 @@ class NoteText {
|
||||
bool setText(const std::string& text);
|
||||
const std::string& text() const { return text_; }
|
||||
size_t cursor() const { return cursor_; }
|
||||
size_t size() const { return text_.size(); }
|
||||
uint32_t revision() const { return revision_; } // changes with every edit: is it saved?
|
||||
|
||||
// For a window on a longer text (issue #47): the caller refills the buffer, then says where
|
||||
// the cursor is in it and which row of the screen it should be on. CRLF becomes LF as in
|
||||
// setText; returns how many CRs went. The revision doesn't change: nothing was edited.
|
||||
std::string& buffer() { return text_; }
|
||||
size_t refilled(size_t cursor, int row);
|
||||
size_t startOfLine(size_t pos) const { return lineOf(pos); }
|
||||
size_t top() const { return top_; }
|
||||
|
||||
bool insert(uint32_t codePoint); // false: the note is full
|
||||
bool insertText(const std::string& s); // all of it or nothing
|
||||
void backspace();
|
||||
@@ -61,7 +71,7 @@ class NoteText {
|
||||
bool hasLineAfter(size_t start) const;
|
||||
void moved(bool keepGoal = false);
|
||||
void follow(); // scrolls so the cursor is on screen
|
||||
void dropCarriageReturns();
|
||||
size_t dropCarriageReturns(size_t* follow = nullptr); // how many; `follow` is a place in the text, kept on its character
|
||||
|
||||
int cols_, rows_;
|
||||
std::string text_;
|
||||
|
||||
Reference in New Issue
Block a user