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:
+183
-78
@@ -9,24 +9,108 @@
|
||||
#include "cleanup_plan.h"
|
||||
#include "file_list.h"
|
||||
#include "file_names.h"
|
||||
#include "platform/console.h"
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/theme.h"
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
using notes::NoteDocument;
|
||||
using notes::NoteText;
|
||||
|
||||
std::function<void(const std::string&, int)> NoteEditor::onProgress;
|
||||
|
||||
// The SD card for a NoteDocument. Every call is made on the storage task. The file being read and
|
||||
// the file being appended to stay open between calls (a window is read in a few pieces, a rewrite
|
||||
// appends some five hundred blocks to the megabyte), until done().
|
||||
class NoteEditor::Card : public notes::NoteCard {
|
||||
public:
|
||||
explicit Card(StorageService& storage) : storage_(storage) {}
|
||||
|
||||
bool size(const std::string& path, uint32_t& size) override {
|
||||
close(path);
|
||||
File f = SD.open(path.c_str(), FILE_READ);
|
||||
if (!f || f.isDirectory()) return false;
|
||||
size = static_cast<uint32_t>(f.size());
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
size_t read(const std::string& path, uint32_t at, uint8_t* into, size_t len) override {
|
||||
if (appendPath_ == path) closeAppend(); // what was appended has to be there to read
|
||||
if (readPath_ != path || !read_) {
|
||||
closeRead();
|
||||
read_ = SD.open(path.c_str(), FILE_READ);
|
||||
if (!read_) return 0;
|
||||
readPath_ = path;
|
||||
}
|
||||
if (!read_.seek(at)) return 0;
|
||||
int n = read_.read(into, len);
|
||||
return n > 0 ? static_cast<size_t>(n) : 0;
|
||||
}
|
||||
bool create(const std::string& path) override {
|
||||
close(path);
|
||||
File f = SD.open(path.c_str(), FILE_WRITE);
|
||||
if (!f) return false;
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
bool append(const std::string& path, const uint8_t* data, size_t len) override {
|
||||
if (readPath_ == path) closeRead();
|
||||
if (appendPath_ != path || !append_) {
|
||||
closeAppend();
|
||||
append_ = SD.open(path.c_str(), FILE_APPEND);
|
||||
if (!append_) return false;
|
||||
appendPath_ = path;
|
||||
}
|
||||
return append_.write(data, len) == len;
|
||||
}
|
||||
bool remove(const std::string& path) override {
|
||||
close(path);
|
||||
return SD.remove(path.c_str());
|
||||
}
|
||||
bool rename(const std::string& from, const std::string& to) override {
|
||||
done();
|
||||
return SD.rename(from.c_str(), to.c_str());
|
||||
}
|
||||
uint64_t freeBytes() override {
|
||||
StorageState s = storage_.state();
|
||||
return s.totalBytes > s.usedBytes ? s.totalBytes - s.usedBytes : 0;
|
||||
}
|
||||
void done() override {
|
||||
closeRead();
|
||||
closeAppend();
|
||||
}
|
||||
|
||||
private:
|
||||
void close(const std::string& path) {
|
||||
if (readPath_ == path) closeRead();
|
||||
if (appendPath_ == path) closeAppend();
|
||||
}
|
||||
void closeRead() {
|
||||
if (read_) read_.close();
|
||||
readPath_.clear();
|
||||
}
|
||||
void closeAppend() {
|
||||
if (append_) append_.close();
|
||||
appendPath_.clear();
|
||||
}
|
||||
|
||||
StorageService& storage_;
|
||||
File read_, append_;
|
||||
std::string readPath_, appendPath_;
|
||||
};
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kMessageMs = 4000;
|
||||
|
||||
// One block the size of a full note, and something left: without it, nothing is opened. Free
|
||||
// One block the size of the window, and something left: without it, nothing is opened. Free
|
||||
// memory in total isn't the measure: with IRC connected the largest free block is about 31 KB.
|
||||
constexpr size_t kRoomWanted = NoteText::kMaxBytes + 8 * 1024;
|
||||
const char* const kNoRoom = "Not enough memory to edit: close IRC or a Gemini page";
|
||||
|
||||
// On the storage task. Reads a file of up to `limit` bytes into a string that has that capacity
|
||||
// already; false if it can't be read whole.
|
||||
// On the storage task. Reads a file of up to `limit` bytes into a string; false if it can't be
|
||||
// read whole.
|
||||
bool readWhole(const std::string& path, std::string& into, size_t limit) {
|
||||
File f = SD.open(path.c_str(), FILE_READ);
|
||||
if (!f) return false;
|
||||
@@ -51,42 +135,43 @@ void NoteEditor::say(const std::string& text) {
|
||||
std::string NoteEditor::open(const std::string& path) {
|
||||
close();
|
||||
if (ESP.getMaxAllocHeap() < kRoomWanted) return kNoRoom;
|
||||
std::string body, left, why;
|
||||
body.reserve(NoteText::kMaxBytes); // the note's own buffer from here on: read into, then handed over
|
||||
card_ = std::make_shared<Card>(storage_);
|
||||
doc_.reset(new NoteDocument(*card_, kCols, kRows));
|
||||
std::string left, why, told;
|
||||
bool ran = storage_.runAndWait([&]() {
|
||||
File f = SD.open(path.c_str(), FILE_READ);
|
||||
if (!f) {
|
||||
why = "The card refused to open it";
|
||||
return;
|
||||
}
|
||||
size_t size = f.size();
|
||||
f.close();
|
||||
if (size > NoteText::kMaxBytes) why = "Too big to edit: 16 KB at most";
|
||||
else if (!readWhole(path, body, NoteText::kMaxBytes)) why = "The card refused to read it";
|
||||
if (!why.empty()) return;
|
||||
// A save that never finished: its temporary file is offered back (Q143), if there's the
|
||||
// memory to look at it now. If not, it stays for the next time.
|
||||
// A save of a small note that never finished: its temporary file is offered back (Q143),
|
||||
// if there's the memory to look at it now. A bigger note's unfinished saves are in its
|
||||
// side file, and the document picks them up by itself.
|
||||
std::string tmp = path + ".tmp";
|
||||
File t = SD.open(tmp.c_str(), FILE_READ);
|
||||
if (!t) return;
|
||||
size_t tmpSize = t.size();
|
||||
t.close();
|
||||
if (tmpSize > 0 && tmpSize <= NoteText::kMaxBytes && ESP.getMaxAllocHeap() < tmpSize + 8 * 1024) return;
|
||||
left.reserve(tmpSize <= NoteText::kMaxBytes ? tmpSize : 0);
|
||||
if (!readWhole(tmp, left, NoteText::kMaxBytes) || left == body || left.empty()) {
|
||||
size_t tmpSize = t ? t.size() : 0;
|
||||
bool hasTmp = static_cast<bool>(t);
|
||||
if (t) t.close();
|
||||
bool hasSide = SD.exists((path + ".edit").c_str());
|
||||
if (hasTmp && !hasSide && tmpSize > 0 && tmpSize <= NoteText::kMaxBytes && ESP.getMaxAllocHeap() >= kRoomWanted + tmpSize) {
|
||||
left.reserve(tmpSize);
|
||||
if (!readWhole(tmp, left, NoteText::kMaxBytes)) std::string().swap(left);
|
||||
}
|
||||
why = doc_->open(path, &told);
|
||||
if (!why.empty()) return;
|
||||
if (hasTmp && !hasSide && (left.empty() || doc_->windowed() || left == doc_->text().text())) {
|
||||
std::string().swap(left);
|
||||
SD.remove(tmp.c_str());
|
||||
}
|
||||
});
|
||||
if (!ran) return "No SD card";
|
||||
if (!why.empty()) return why;
|
||||
text_.reset(new NoteText(kCols, kRows, std::move(body)));
|
||||
if (!ran) why = "No SD card";
|
||||
if (!why.empty()) {
|
||||
doc_.reset();
|
||||
card_.reset();
|
||||
return why;
|
||||
}
|
||||
path_ = path;
|
||||
folder_ = files::parentOf(path);
|
||||
savedRevision_ = text_->revision();
|
||||
problem_.clear();
|
||||
message_.clear();
|
||||
givenUp_ = false;
|
||||
lastKeyMs_ = millis();
|
||||
if (!told.empty()) say(told);
|
||||
if (!left.empty()) {
|
||||
recovered_ = std::move(left);
|
||||
ask_ = Ask::Recover;
|
||||
@@ -98,36 +183,38 @@ std::string NoteEditor::open(const std::string& path) {
|
||||
std::string NoteEditor::openNew(const std::string& folder) {
|
||||
close();
|
||||
if (ESP.getMaxAllocHeap() < kRoomWanted) return kNoRoom;
|
||||
text_.reset(new NoteText(kCols, kRows));
|
||||
card_ = std::make_shared<Card>(storage_);
|
||||
doc_.reset(new NoteDocument(*card_, kCols, kRows));
|
||||
path_.clear();
|
||||
folder_ = folder;
|
||||
savedRevision_ = text_->revision();
|
||||
problem_.clear();
|
||||
message_.clear();
|
||||
givenUp_ = false;
|
||||
lastKeyMs_ = millis();
|
||||
return "";
|
||||
}
|
||||
|
||||
// Leaving rewrites the file, however long the note (Q225). Not when the device is powering off:
|
||||
// then a long note's edits go to its side file, which is quick, and are picked up the next time.
|
||||
void NoteEditor::close() {
|
||||
if (dirty()) save();
|
||||
text_.reset();
|
||||
if (doc_ && !givenUp_ && owed()) save(!power_.poweringOff());
|
||||
doc_.reset();
|
||||
card_.reset();
|
||||
dialog_.reset();
|
||||
ask_ = Ask::None;
|
||||
std::string().swap(recovered_);
|
||||
}
|
||||
|
||||
// On the main loop, waiting for the storage task: no second copy of the note is made, and at
|
||||
// 16 KB the wait is a fraction of a second, when nobody has typed for five.
|
||||
bool NoteEditor::save() {
|
||||
if (!text_) return true;
|
||||
// On the main loop, waiting for the storage task: no second copy of the text is made. A note of
|
||||
// up to 64 KB is rewritten in a fraction of a second, when nobody has typed for five; a longer
|
||||
// one takes a second for each 400 KB or so, and shows how far it is.
|
||||
bool NoteEditor::save(bool whole) {
|
||||
if (!doc_) return true;
|
||||
lastTryMs_ = millis();
|
||||
const std::string& body = text_->text();
|
||||
if (path_.empty() && body.empty()) { // a new note nothing was typed in: no file
|
||||
savedRevision_ = text_->revision();
|
||||
return true;
|
||||
}
|
||||
if (path_.empty() && doc_->size() == 0) return true; // a new note nothing was typed in: no file
|
||||
std::string path = path_, why;
|
||||
if (path.empty()) {
|
||||
bool fresh = path_.empty();
|
||||
if (fresh) {
|
||||
char stamp[20] = "new";
|
||||
int64_t now = clock_.utcNow();
|
||||
if (now >= 0) {
|
||||
@@ -137,10 +224,15 @@ bool NoteEditor::save() {
|
||||
std::snprintf(stamp, sizeof stamp, "%04d%02d%02d-%02d%02d", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, local.tm_hour,
|
||||
local.tm_min);
|
||||
}
|
||||
path = files::joinPath(folder_, notes::nameFromFirstLine(text_->firstLine(), stamp) + ".txt");
|
||||
path = files::joinPath(folder_, notes::nameFromFirstLine(doc_->text().firstLine(), stamp) + ".txt");
|
||||
}
|
||||
bool fresh = path_.empty();
|
||||
bool rewrite = whole || fresh || doc_->wantsRewrite();
|
||||
int percent = 0;
|
||||
bool ran = storage_.runAndWait([&]() {
|
||||
if (!rewrite) {
|
||||
doc_->journal(why);
|
||||
return;
|
||||
}
|
||||
if (!SD.exists(folder_.c_str()) && !SD.mkdir(folder_.c_str())) {
|
||||
why = "the card refused to make " + folder_;
|
||||
return;
|
||||
@@ -148,48 +240,59 @@ bool NoteEditor::save() {
|
||||
if (fresh) { // a name nothing has yet: "list (2).txt"
|
||||
std::string name = files::baseName(path);
|
||||
for (int n = 2; n < 100 && SD.exists(path.c_str()); n++) path = files::joinPath(folder_, files::copyName(name, n));
|
||||
doc_->setPath(path);
|
||||
}
|
||||
std::string tmp = path + ".tmp";
|
||||
File f = SD.open(tmp.c_str(), FILE_WRITE);
|
||||
if (!f) {
|
||||
why = "the card refused to open a file";
|
||||
return;
|
||||
}
|
||||
size_t wrote = body.empty() ? 0 : f.write(reinterpret_cast<const uint8_t*>(body.data()), body.size());
|
||||
f.close();
|
||||
File check = SD.open(tmp.c_str(), FILE_READ);
|
||||
bool whole = wrote == body.size() && check && check.size() == body.size();
|
||||
if (check) check.close();
|
||||
if (!whole) {
|
||||
SD.remove(tmp.c_str());
|
||||
why = "the card refused a write";
|
||||
return;
|
||||
}
|
||||
// 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 (SD.exists(path.c_str())) SD.remove(path.c_str());
|
||||
if (!SD.rename(tmp.c_str(), path.c_str())) why = "the card refused to rename the file";
|
||||
if (doc_->rewriteStart(why)) percent = doc_->rewriteStep(why);
|
||||
if (fresh && !why.empty()) doc_->setPath("");
|
||||
});
|
||||
bool show = doc_->size() > NoteDocument::kWholeLimit;
|
||||
uint32_t started = millis();
|
||||
while (ran && rewrite && why.empty() && percent >= 0 && percent < 100) {
|
||||
if (show && onProgress) onProgress(files::baseName(path), percent);
|
||||
ran = storage_.runAndWait([&]() { percent = doc_->rewriteStep(why); });
|
||||
}
|
||||
if (!ran) why = "no SD card";
|
||||
if (!why.empty()) {
|
||||
if (problem_ != why) say("Not saved: " + why);
|
||||
problem_ = why;
|
||||
redraw_ = true;
|
||||
return false;
|
||||
}
|
||||
if (rewrite && show) console.printf("notes: rewrote %s, %u bytes in %.1f s\n", path.c_str(), (unsigned)doc_->size(), (millis() - started) / 1000.0);
|
||||
path_ = path;
|
||||
problem_.clear();
|
||||
savedRevision_ = text_->revision();
|
||||
redraw_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void NoteEditor::settle() {
|
||||
if (!doc_ || !doc_->wantsMove()) return;
|
||||
// A window that leaves memory needs a file to belong to: a new note is saved first.
|
||||
if (path_.empty() && !save(true)) return;
|
||||
std::string why;
|
||||
bool ran = storage_.runAndWait([&]() { doc_->move(why); });
|
||||
if (!ran) why = "no SD card";
|
||||
if (!why.empty() && problem_ != why) say("The card: " + why);
|
||||
if (!why.empty()) problem_ = why;
|
||||
}
|
||||
|
||||
bool NoteEditor::jump(bool toEnd) {
|
||||
std::string why;
|
||||
uint32_t to = toEnd ? doc_->size() : 0;
|
||||
bool ran = storage_.runAndWait([&]() { doc_->jump(to, why); });
|
||||
if (!ran) why = "no SD card";
|
||||
if (!why.empty()) say("The card: " + why);
|
||||
return why.empty();
|
||||
}
|
||||
|
||||
void NoteEditor::help(std::vector<KeyHelp>& out) const {
|
||||
if (dialog_) return keys::add(out, keys::kDialog);
|
||||
keys::add(out, keys::kNotesEditor);
|
||||
}
|
||||
|
||||
bool NoteEditor::onKey(const KeyEvent& e) {
|
||||
if (!text_) return false;
|
||||
if (!doc_) return false;
|
||||
NoteText* text_ = &doc_->text();
|
||||
redraw_ = true;
|
||||
if (dialog_) {
|
||||
dialog_->onKey(e);
|
||||
@@ -210,7 +313,7 @@ bool NoteEditor::onKey(const KeyEvent& e) {
|
||||
return true;
|
||||
}
|
||||
if (asked == Ask::LeaveUnsaved && result == 1) {
|
||||
savedRevision_ = text_->revision(); // given up on
|
||||
givenUp_ = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -225,37 +328,38 @@ bool NoteEditor::onKey(const KeyEvent& e) {
|
||||
else if (lower == 'e') text_->lineEnd();
|
||||
break;
|
||||
}
|
||||
if (!text_->insert(e.ch)) say("This note is full: 16 KB");
|
||||
if (!text_->insert(e.ch)) say("Can't type: " + problem_);
|
||||
break;
|
||||
}
|
||||
case Key::Select:
|
||||
if (!text_->insert('\n')) say("This note is full: 16 KB");
|
||||
if (!text_->insert('\n')) say("Can't type: " + problem_);
|
||||
break;
|
||||
case Key::Tab:
|
||||
if (!text_->insertText(" ")) say("This note is full: 16 KB");
|
||||
if (!text_->insertText(" ")) say("Can't type: " + problem_);
|
||||
break;
|
||||
case Key::Delete: text_->backspace(); break;
|
||||
case Key::Left: text_->left(); break;
|
||||
case Key::Right: text_->right(); break;
|
||||
case Key::Up: page ? text_->pageUp() : text_->up(); break;
|
||||
case Key::Down: page ? text_->pageDown() : text_->down(); break;
|
||||
case Key::Up: e.ctrl ? void(jump(false)) : page ? text_->pageUp() : text_->up(); break;
|
||||
case Key::Down: e.ctrl ? void(jump(true)) : page ? text_->pageDown() : text_->down(); break;
|
||||
case Key::Back:
|
||||
if (!dirty() || save()) return false;
|
||||
if (!owed() || save(true)) return false;
|
||||
ask_ = Ask::LeaveUnsaved;
|
||||
dialog_.reset(new DialogModel({"Stay", "Leave"}));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
settle();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NoteEditor::update(uint32_t) {
|
||||
if (!text_) return false;
|
||||
if (!doc_) return false;
|
||||
uint32_t now = millis();
|
||||
// A save that failed is tried again every five seconds, not at every pass.
|
||||
if (dirty() && !dialog_ && now - lastTryMs_ >= kSaveAfterMs &&
|
||||
(now - lastKeyMs_ >= kSaveAfterMs || power_.screen() == ScreenState::Off))
|
||||
save();
|
||||
save(false);
|
||||
if (!message_.empty() && now - messageMs_ >= kMessageMs) {
|
||||
message_.clear();
|
||||
redraw_ = true;
|
||||
@@ -266,7 +370,8 @@ bool NoteEditor::update(uint32_t) {
|
||||
}
|
||||
|
||||
void NoteEditor::draw(Canvas& c) {
|
||||
if (!text_) return;
|
||||
if (!doc_) return;
|
||||
NoteText* text_ = &doc_->text();
|
||||
const auto& area = theme::kContent;
|
||||
c.setTextDatum(top_left);
|
||||
|
||||
@@ -275,7 +380,7 @@ void NoteEditor::draw(Canvas& c) {
|
||||
c.setTextColor(theme::kMuted);
|
||||
std::string name = path_.empty() ? "New note" : files::fitName(files::baseName(path_), 26);
|
||||
c.drawString(name.c_str(), 4, area.y + 1);
|
||||
std::string state = formatBytes(text_->text().size()) + (dirty() ? (problem_.empty() ? ", typing" : ", NOT SAVED") : ", saved");
|
||||
std::string state = formatBytes(doc_->size()) + (dirty() ? (problem_.empty() ? ", typing" : ", NOT SAVED") : ", saved");
|
||||
if (path_.empty() && !dirty()) state = "empty";
|
||||
c.setTextDatum(top_right);
|
||||
c.setTextColor(dirty() && !problem_.empty() ? theme::kWarning : theme::kMuted);
|
||||
@@ -288,9 +393,9 @@ void NoteEditor::draw(Canvas& c) {
|
||||
c.setTextColor(theme::kText);
|
||||
for (size_t i = 0; i < rows.size(); i++) c.drawString(rows[i].c_str(), 4, top + 1 + static_cast<int>(i) * theme::kLineHeight);
|
||||
c.fillRect(3 + text_->cursorCol() * 6, top + text_->cursorRow() * theme::kLineHeight, 1, theme::kLineHeight, theme::kAccent);
|
||||
if (text_->text().size() > static_cast<size_t>(kCols * kRows)) { // more than a screen: where we are in it
|
||||
if (doc_->size() > static_cast<uint32_t>(kCols * kRows)) { // more than a screen: where we are in it
|
||||
int h = kRows * theme::kLineHeight, barH = 12;
|
||||
c.fillRect(area.w - 2, top + (h - barH) * text_->percent() / 100, 2, barH, theme::kMuted);
|
||||
c.fillRect(area.w - 2, top + (h - barH) * doc_->percent() / 100, 2, barH, theme::kMuted);
|
||||
}
|
||||
|
||||
c.setFont(&fonts::small);
|
||||
|
||||
+26
-10
@@ -7,7 +7,9 @@
|
||||
#include "dialog_model.h"
|
||||
#include "key_help.h"
|
||||
#include "key_event.h"
|
||||
#include "note_text.h"
|
||||
#include <functional>
|
||||
|
||||
#include "note_document.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/power_service.h"
|
||||
#include "services/storage_service.h"
|
||||
@@ -15,10 +17,12 @@
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Edits one text file of up to 16 KB (F1, Q143-Q145): the Notes App's editor, and the Storage
|
||||
// App's for `e`. It saves by itself: five seconds after the last key, when the screen turns off,
|
||||
// and on close. A save writes `<file>.tmp`, then puts it in the note's place, so the note on the
|
||||
// card is always a whole one.
|
||||
// Edits one text file of any size (F1, Q143-Q145; issue #47, Q223-Q232): the Notes App's editor,
|
||||
// and the Storage App's for `e`. The text is a notes::NoteDocument: a window of the file in
|
||||
// memory, the rest on the card. It saves by itself: five seconds after the last key, when the
|
||||
// screen turns off, and on close. Up to 64 KB a save writes `<file>.tmp` and puts it in the
|
||||
// note's place; a bigger note's saves go to `<file>.edit`, and the file is rewritten on leaving.
|
||||
// Either way the note on the card is always a whole one.
|
||||
class NoteEditor {
|
||||
public:
|
||||
static constexpr int kCols = 38, kRows = 8;
|
||||
@@ -30,9 +34,13 @@ class NoteEditor {
|
||||
std::string open(const std::string& path); // "" or why it can't be edited
|
||||
std::string openNew(const std::string& folder); // the same; no file until there's something to save (Q142)
|
||||
void close(); // saves what isn't yet
|
||||
bool isOpen() const { return static_cast<bool>(text_); }
|
||||
bool isOpen() const { return static_cast<bool>(doc_); }
|
||||
const std::string& path() const { return path_; } // "" for a new note nothing was typed in
|
||||
|
||||
// A long rewrite shows how far it is: the editor is waiting for the card meanwhile, so the
|
||||
// screen is drawn from here (set once, in main).
|
||||
static std::function<void(const std::string& name, int percent)> onProgress;
|
||||
|
||||
bool onKey(const KeyEvent& e); // false: done, and saved
|
||||
void help(std::vector<KeyHelp>& out) const;
|
||||
bool update(uint32_t nowMs); // true: draw again
|
||||
@@ -41,16 +49,24 @@ class NoteEditor {
|
||||
private:
|
||||
enum class Ask { None, Recover, LeaveUnsaved };
|
||||
|
||||
bool dirty() const { return text_ && text_->revision() != savedRevision_; }
|
||||
bool save(); // true if the card has it now (or there was nothing to save)
|
||||
class Card;
|
||||
bool dirty() const { return doc_ && doc_->dirty(); }
|
||||
bool owed() const { return doc_ && (doc_->dirty() || doc_->filePending()); } // the file isn't the note yet
|
||||
// True if the card has it now (or there was nothing to save). `whole`: the file itself is
|
||||
// rewritten; otherwise a note over 64 KB only gets its side file written, which is quick.
|
||||
bool save(bool whole);
|
||||
void settle(); // after a key: moves the window if the cursor is near an end of it
|
||||
bool jump(bool toEnd);
|
||||
void say(const std::string& text);
|
||||
|
||||
StorageService& storage_;
|
||||
ClockService& clock_;
|
||||
PowerService& power_;
|
||||
std::unique_ptr<notes::NoteText> text_;
|
||||
std::shared_ptr<Card> card_;
|
||||
std::unique_ptr<notes::NoteDocument> doc_;
|
||||
std::string path_, folder_;
|
||||
uint32_t savedRevision_ = 0, lastKeyMs_ = 0, lastTryMs_ = 0;
|
||||
uint32_t lastKeyMs_ = 0, lastTryMs_ = 0;
|
||||
bool givenUp_ = false; // "Leave" after a save that failed
|
||||
std::string recovered_; // what a temporary file left behind holds, until the user has chosen
|
||||
Ask ask_ = Ask::None;
|
||||
std::unique_ptr<DialogModel> dialog_;
|
||||
|
||||
+10
-1
@@ -25,6 +25,10 @@ bool endsWith(const std::string& s, const char* tail) {
|
||||
size_t n = std::strlen(tail);
|
||||
return s.size() >= n && s.compare(s.size() - n, n, tail) == 0;
|
||||
}
|
||||
|
||||
// Beside a note, and not one: a save cut short (.tmp), a long note's unsaved edits (.edit), and
|
||||
// edits that no longer fit their file, kept for whoever wants to look (.edit.lost).
|
||||
bool notANote(const std::string& name) { return endsWith(name, ".tmp") || endsWith(name, ".edit") || endsWith(name, ".edit.lost"); }
|
||||
} // namespace
|
||||
|
||||
void NotesApp::onEnter() {
|
||||
@@ -125,6 +129,7 @@ void NotesApp::onFinished(const FileOps::Status& s) {
|
||||
if (list_.find(name.substr(0, name.size() - 4)) < 0) orphans.push_back(name);
|
||||
continue;
|
||||
}
|
||||
if (notANote(name)) continue;
|
||||
notes_.push_back(static_cast<uint16_t>(i));
|
||||
}
|
||||
if (!orphans.empty() && !mended_) {
|
||||
@@ -202,6 +207,8 @@ bool NotesApp::onKey(const KeyEvent& e) {
|
||||
selectIndex_ = rows_.selected();
|
||||
selectAfter_.clear();
|
||||
std::string why = ops_.remove(target_, false);
|
||||
std::string side = target_ + ".edit"; // a long note's unsaved edits go with it (issue #47)
|
||||
if (why.empty()) storage_.runJob([side]() { SD.remove(side.c_str()); });
|
||||
if (why.empty()) wait_ = Wait::Work;
|
||||
else say(why);
|
||||
}
|
||||
@@ -225,6 +232,8 @@ bool NotesApp::onNameKey(const KeyEvent& e) {
|
||||
if (why.empty() && name != baseName(target_)) {
|
||||
why = ops_.move(target_, false, joinPath(kFolder, name));
|
||||
if (why.empty()) {
|
||||
std::string side = target_ + ".edit", sideTo = joinPath(kFolder, name) + ".edit";
|
||||
storage_.runJob([side, sideTo]() { SD.rename(side.c_str(), sideTo.c_str()); });
|
||||
wait_ = Wait::Work;
|
||||
selectAfter_ = name;
|
||||
}
|
||||
@@ -268,7 +277,7 @@ bool NotesApp::onListKey(const KeyEvent& e) {
|
||||
list_.sort(sort_);
|
||||
notes_.clear();
|
||||
for (size_t i = 0; i < list_.count(); i++)
|
||||
if (!list_.folder(i) && !endsWith(list_.name(i), ".tmp")) notes_.push_back(static_cast<uint16_t>(i));
|
||||
if (!list_.folder(i) && !notANote(list_.name(i))) notes_.push_back(static_cast<uint16_t>(i));
|
||||
for (size_t i = 0; i < notes_.size(); i++)
|
||||
if (keep == list_.name(notes_[i])) rows_.select(static_cast<int>(i));
|
||||
titlesFrom_ = -1;
|
||||
|
||||
@@ -322,7 +322,6 @@ bool StorageApp::onKey(const KeyEvent& e) {
|
||||
if (view_ == View::Viewer && viewer_.showingText() && e.key == Key::Char && (e.ch == 'e' || e.ch == 'E')) {
|
||||
// Edit it (Q146), if the rules and its size allow.
|
||||
std::string path = viewer_.path(), why = ops_.whyReadOnly(path, false);
|
||||
if (why.empty() && viewer_.size() > notes::NoteText::kMaxBytes) why = "Too big to edit: 16 KB at most";
|
||||
if (why.empty()) {
|
||||
viewer_.close();
|
||||
why = noteEditor_.open(path);
|
||||
|
||||
Reference in New Issue
Block a user