Files
roro9stack/lib/notes/src/note_text.h
T
twislaandClaude Opus 5.5 e8a654a15f 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
2026-10-06 10:08:10 +02:00

82 lines
3.3 KiB
C++

#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace roro::notes {
// The text of a note while it's edited (F1, Q144, Q145): UTF-8 held whole in memory, a cursor, and
// the part of it on screen. Lines wrap at spaces, `cols` characters wide; a line owns the space or
// the newline it ends with, so every byte of the text belongs to exactly one line. No index of
// lines is kept (a note of newlines alone would need twice its size): where a line starts is
// worked out from the start of its paragraph, which is never far.
class NoteText {
public:
static constexpr size_t kMaxBytes = 16 * 1024;
// Room for a full note is reserved once, so typing never has to find a bigger block of memory:
// on the device a failed allocation is the end. The second form takes over a string the
// caller filled (and reserved): a note is never in memory twice.
NoteText(int cols, int rows);
NoteText(int cols, int rows, std::string&& text);
// Copied into the buffer already held. CRLF becomes LF (Q148). False, and nothing changes, if
// it's over kMaxBytes.
bool setText(const std::string& text);
const std::string& text() const { return text_; }
size_t cursor() const { return cursor_; }
uint32_t revision() const { return revision_; } // changes with every edit: is it saved?
bool insert(uint32_t codePoint); // false: the note is full
bool insertText(const std::string& s); // all of it or nothing
void backspace();
void left();
void right();
void up();
void down();
void pageUp();
void pageDown();
void lineStart();
void lineEnd();
void toStart();
void toEnd();
// The screen, kept around the cursor: its rows (tabs as spaces, the ending newline left out),
// and where the cursor is on it, in rows and characters.
std::vector<std::string> rows();
int cursorRow();
int cursorCol();
int percent() const { return text_.empty() ? 0 : static_cast<int>(top_ * 100 / text_.size()); }
std::string firstLine() const; // without its newline, for the title and the file's name
private:
size_t nextLine(size_t start) const; // where the line after the one at `start` starts
size_t lineOf(size_t pos) const; // the start of the line `pos` is on
size_t lastSpot(size_t start) const; // the last place the cursor can be on that line
size_t atColumn(size_t start, int col) const;
int columnOf(size_t start, size_t pos) const;
bool hasLineAfter(size_t start) const;
void moved(bool keepGoal = false);
void follow(); // scrolls so the cursor is on screen
void dropCarriageReturns();
int cols_, rows_;
std::string text_;
size_t cursor_ = 0, top_ = 0;
int goal_ = -1; // the column Up and Down aim for, across short lines
uint32_t revision_ = 0;
};
// The file a new note is saved as (Q142): from its first line, "Shopping list!" -> "shopping-list",
// or "note-<stamp>" when that gives nothing. No extension, no folder.
std::string nameFromFirstLine(const std::string& firstLine, const std::string& stamp);
// A row's title in the Notes list, from the first bytes of a file: its first line that isn't
// blank, cut to `maxChars`; "" if there's none.
std::string titleFrom(const std::string& head, size_t maxChars);
} // namespace roro::notes