From cf99dde4805f1792d6a56156b6a0338aa684005c Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 16 Oct 2026 21:08:04 +0200 Subject: notes: an empty note body gets a file, and the second click writes Reported from use: a note created with only a title then saved left a db.json entry with no file beside it, and reopening it said the file was missing. Cause: FileView.setText("") on a path that view has not loaded creates no file at all and reports no error. The index write (always non-empty JSON) was fine, so only the body was lost, which is why it looked like a missing file rather than a failed save. Bodies are now written by a Process running cat > with the text on stdin: verified against empty and non-empty payloads, consecutive writes on one Process, and a store with no db.json at all. The exit code is the real outcome, so a refused write reports it. Second report in the same flow: the editor stays editable after the first click, so typing more and then pressing Commit & push dropped the edit, the push carrying only what the first click wrote. The second click now writes the current title and body before it commits and pushes. --- desktop/modules/notes/NotesModule.qml | 45 ++++++++++++++++++++++------------- desktop/modules/notes/NotesPage.qml | 25 ++++++++++++------- desktop/modules/notes/README.md | 15 +++++++----- 3 files changed, 54 insertions(+), 31 deletions(-) (limited to 'desktop') diff --git a/desktop/modules/notes/NotesModule.qml b/desktop/modules/notes/NotesModule.qml index 082f02c..992cbfa 100644 --- a/desktop/modules/notes/NotesModule.qml +++ b/desktop/modules/notes/NotesModule.qml @@ -150,27 +150,38 @@ Module { if (done) done(ok, ok ? "" : mod.dbView.saveError); } - // A note body. notes.sh keeps the title out of the file, so this is the - // body and nothing else. setText writes the path directly and needs no - // prior load; reading the file back is what would need one, and nothing - // here reads it back. - property FileView bodyView: FileView { - blockWrites: true - printErrors: false - property string saveError: "" - onSaved: saveError = "" - onSaveFailed: error => { - console.warn("notes: note body write failed:", error); - saveError = "the note's file could not be written"; + // A note body, written through a Process rather than a FileView. + // FileView.setText() with EMPTY text on a path that view has not loaded + // writes no file at all and reports no error, which is how a note created + // with an empty body ended up in db.json with no file beside it. cat reads + // stdin, so any content and any length works, including none, and the exit + // code is the real outcome. Clearing stdinEnabled after the write is what + // gives cat its EOF. + property Process bodyWriter: Process { + stdinEnabled: true + property string data: "" + property var done: null + property string err: "" + onStarted: { + mod.bodyWriter.write(mod.bodyWriter.data); + mod.bodyWriter.stdinEnabled = false; + } + onExited: code => { + const d = mod.bodyWriter.done; + mod.bodyWriter.done = null; + if (d) d(code === 0, code === 0 ? "" : (mod.bodyWriter.err || "the note's file could not be written")); } + stderr: StdioCollector { onStreamFinished: mod.bodyWriter.err = this.text } } function writeBody(path, text, done) { - mod.bodyView.path = path; - mod.bodyView.saveError = ""; - mod.bodyView.setText(text); - const ok = mod.bodyView.saveError === ""; - if (done) done(ok, ok ? "" : mod.bodyView.saveError); + mod.bodyWriter.done = done; + mod.bodyWriter.data = text; + mod.bodyWriter.err = ""; + mod.bodyWriter.command = ["sh", "-c", 'cat > "$1"', "sh", path]; + mod.bodyWriter.stdinEnabled = true; + mod.bodyWriter.running = false; + mod.bodyWriter.running = true; } // The path of a note's body, "" when the entry's file is not a plain diff --git a/desktop/modules/notes/NotesPage.qml b/desktop/modules/notes/NotesPage.qml index 585fcfb..1a82254 100644 --- a/desktop/modules/notes/NotesPage.qml +++ b/desktop/modules/notes/NotesPage.qml @@ -19,9 +19,11 @@ import "Notes.js" as Notes // // Save is two clicks and each click does one thing. The first writes the note // and, when that has finished, arms the button as "Commit & push?"; the second -// commits and pushes and returns to the list. Arming the second click only -// after the write completes is what stops a quick second click from committing -// a note the first click has not written yet. +// writes whatever the editor holds now, then commits and pushes, then returns +// to the list. The second click writes as well because the editor stays +// editable in between: pushing without writing would throw away an edit made +// after the first click. Arming only after the first write completes is what +// stops a quick second click from committing a note that is not written yet. // // Nothing is remembered between visits: leaving the editor with text that was // not saved drops it, which is the clipboard page's own rule. @@ -78,12 +80,19 @@ Item { // The note is already on disk and only the push is owed, so this click // pushes rather than writing a second note. if (page.awaiting !== "") { - const id = page.awaiting === "create" ? page.owner.lastCreatedId : page.editing.id; + // The editor stays editable after the first click, so whatever was + // typed since is written before it is pushed: the push alone would + // leave that edit behind. + const kind = page.awaiting; + const id = kind === "create" ? page.owner.lastCreatedId : page.editing.id; page.busy = true; - page.owner.commitPush(page.awaiting, id, (ok, msg) => { - page.busy = false; - if (!ok) { page.owner.lastError = msg; return; } - page.toList(); + page.owner.update(id, titleField.text, bodyEdit.text, (ok, msg) => { + if (!ok) { page.busy = false; page.owner.lastError = msg; return; } + page.owner.commitPush(kind, id, (ok2, msg2) => { + page.busy = false; + if (!ok2) { page.owner.lastError = msg2; return; } + page.toList(); + }); }); return; } diff --git a/desktop/modules/notes/README.md b/desktop/modules/notes/README.md index 0f642b8..42f989f 100644 --- a/desktop/modules/notes/README.md +++ b/desktop/modules/notes/README.md @@ -14,9 +14,9 @@ per note, named by its creation time in epoch seconds. lists, links, inline code and quotes render; tables and fenced-code highlighting do not. - **Save** is two clicks: the first writes the note, the second reads - **Commit & push?** and pushes it. **Delete** asks first, then removes the - note and pushes the removal. With git off there is no second half, so Save - writes and returns in one. + **Commit & push?**, writes whatever you have typed since, and pushes it. + **Delete** asks first, then removes the note and pushes the removal. With git + off there is no second half, so Save writes and returns in one. - Leaving the editor with **Cancel**, the back arrow, Escape or a click outside drops unsaved text. The save is the explicit act. - **Update list** runs `git pull` and stamps `git.lastpull`. @@ -38,9 +38,12 @@ git to resolve instead. A change made on two machines at once conflicts here for the same reason it conflicts for `notes.sh`: every machine appends to the one `db.json`. -A write that fails is reported rather than assumed: the writes block, so a -refused or failing `setText` is known when it returns, and a failed commit or -push shows git's own message. +A write that fails is reported rather than assumed. A body is written by a +`Process` running `cat > ` with the text on stdin, not by a `FileView`: +`setText` with an empty string, on a path that view has not loaded, creates no +file and reports no error, which once put an empty note into `db.json` with no +file beside it. `cat` takes any content at any length, including none, and its +exit code is the outcome; a failed commit or push shows git's own message. **Git order.** `notes.sh` pulls before it writes and commits after, and the page keeps that order: creating a note pulls first, then writes on what the -- cgit v1.2.3