Notes: plain text notes on the SD card, with an editor that saves by itself (#19)

The Notes App lists the files of /notes by their first line, newest first:
n starts a note, Enter opens it, r renames its file, d deletes it after
asking, s sorts by name. A new note's file is named after its first line.

The editor wraps at spaces, 38 columns by 8 rows; Fn+arrows move through
the wrapped text, Ctrl+A and Ctrl+E go to the ends of the line. There is no
save key: the note is written five seconds after the last key, on Back, on
leaving the App, when the screen turns off and before the device powers
off. A save writes a temporary file and puts it in the note's place; a save
cut short is put back, or offered, the next time.

A note is up to 16 KB, held in one buffer reserved when it's opened: the
file is read straight into it and typing never makes it grow. A failed
allocation aborts on this device, and with IRC connected the largest free
block is about 31 KB: a first version that copied the note once on loading
restarted the device when a full note was opened with IRC connected.
Editing files of any size is #47.

The Storage App's text viewer gets `e`, which edits a text file up to 16 KB
with the same editor unless the file is read-only.

439 host tests. Checked on the device: docs/milestones/F1.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-06 10:08:10 +02:00
co-authored by Claude Opus 5.5
parent c868977f1c
commit e8a654a15f
17 changed files with 1599 additions and 9 deletions
+31 -1
View File
@@ -51,6 +51,7 @@ void StorageApp::onExit() {
ops_.cancel();
wait_ = Wait::None;
}
noteEditor_.close(); // saves
viewer_.close();
wantList_ = false;
loaded_ = false;
@@ -105,7 +106,8 @@ void StorageApp::update(uint32_t nowMs) {
wantList_ = present;
clip_ = Item();
if (view_ == View::Viewer) viewer_.close();
if (view_ == View::Details || view_ == View::Name || view_ == View::Viewer) view_ = View::Browse;
if (view_ == View::Editor) noteEditor_.close();
if (view_ == View::Details || view_ == View::Name || view_ == View::Viewer || view_ == View::Editor) view_ = View::Browse;
requestRedraw();
}
if (wantList_ && present && wait_ == Wait::None && !ops_.busy() && ops_.list(want_).empty()) {
@@ -114,6 +116,7 @@ void StorageApp::update(uint32_t nowMs) {
waitSinceMs_ = nowMs;
}
if (view_ == View::Viewer && viewer_.update(nowMs)) requestRedraw();
if (view_ == View::Editor && noteEditor_.update(nowMs)) requestRedraw();
FileOps::Status s;
if (wait_ != Wait::None && ops_.finished(s)) {
onFinished(s);
@@ -280,6 +283,32 @@ bool StorageApp::onKey(const KeyEvent& e) {
}
return true;
}
if (view_ == View::Editor) {
if (!noteEditor_.onKey(e)) {
std::string name = baseName(noteEditor_.path());
noteEditor_.close();
view_ = View::Browse;
open(cwd_, name); // its size and date have changed
}
return true;
}
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);
if (why.empty()) view_ = View::Editor;
else {
view_ = View::Browse;
say(why);
}
} else {
viewer_.say(why);
}
return true;
}
if (view_ == View::Viewer) {
if (!viewer_.onKey(e)) {
viewer_.close();
@@ -453,6 +482,7 @@ void StorageApp::draw(Canvas& c) {
return;
case View::Maintenance: maintenance_.draw(c); return;
case View::Viewer: viewer_.draw(c); return;
case View::Editor: noteEditor_.draw(c); return;
case View::Details: {
std::vector<std::string> lines;
auto measure = widgets::bodyMeasure(c);