Public Access
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
131 lines
6.4 KiB
C++
131 lines
6.4 KiB
C++
#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
|