#pragma once #include #include #include #include #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 // `.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 `.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(text_.size()) + after_; } uint32_t cursor() const { return before_ + static_cast(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 pieces_; // without the window while it's loaded std::vector 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 block; } rw_; }; } // namespace roro::notes