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,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
|
||||
Reference in New Issue
Block a user