diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 20:39:40 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 20:39:40 +0200 |
| commit | fe7dcb389e0df86aadbe481646ca9202dfcca9dc (patch) | |
| tree | 04762a63a6ec69b1b7d4169a02d7761fd627a5b3 /desktop/modules | |
| parent | 635df81d36b3102c3ac574bc865e7bfa5795a046 (diff) | |
| download | quickshell-fe7dcb389e0df86aadbe481646ca9202dfcca9dc.tar.gz quickshell-fe7dcb389e0df86aadbe481646ca9202dfcca9dc.zip | |
notes: drawer tile with count and the note list
The module resolves notes.sh's own paths by sourcing its rc, so a changed
BASEDIR moves the page with it, and notes.sh's git test is the same one the
page uses. A Module is a plain QtObject with no default property, so the
config Process and the db.json FileView are typed properties, the shape
UpdatesModule uses for its own Process.
db.json is watched rather than read on open: the tile's count has to move
when a note is added from either side, the CLI included, which is the same
call mail makes for its unread count.
The list reads db.json.notes rather than walking notes/, because that is
where the titles are; entries sort newest first on the file timestamp and
a title search filters them.
Diffstat (limited to 'desktop/modules')
| -rw-r--r-- | desktop/modules/notes/NotesModule.qml | 122 | ||||
| -rw-r--r-- | desktop/modules/notes/NotesPage.qml | 121 |
2 files changed, 243 insertions, 0 deletions
diff --git a/desktop/modules/notes/NotesModule.qml b/desktop/modules/notes/NotesModule.qml new file mode 100644 index 0000000..a2d54e5 --- /dev/null +++ b/desktop/modules/notes/NotesModule.qml @@ -0,0 +1,122 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell.Io +import QtQuick +import "../.." +import "Notes.js" as Notes + +// A drawer tile and page for bash-notes. The store is notes.sh's own, under +// BASEDIR (default ~/.local/share/bash-notes): db.json is the index and +// notes/ holds one file per note, named by its creation time in epoch +// seconds. The page writes notes in exactly the shape notes.sh reads, so both +// tools work on the same notes. +// +// A Module is a plain QtObject and has no default property, so the Process and +// the FileView below are declared as typed properties, as UpdatesModule does +// for its Process. +Module { + id: mod + + name: "notes" + label: "Notes" + alwaysActive: true + + // fa-sticky_note, present in Inconsolata Nerd Font. + icon: "\uf249" + + // Resolved from notes.sh's own rc, so a changed BASEDIR moves the page + // with it. Empty until the shell below returns. + property string baseDir: "" + property string dbPath: "" + property string notesDir: "" + property bool gitEnabled: false + property string client: "" + + // The whole parsed db.json, so a write can replace .notes and carry + // params and git through untouched. + property var db: ({ notes: [] }) + property bool ready: false + property string lastError: "" + + readonly property var notes: Notes.sorted(mod.db.notes) + readonly property int count: Array.isArray(mod.db.notes) ? mod.db.notes.length : 0 + + // One shell, sourcing the rc the way notes.sh sources it, so ${BASEDIR} + // expands with ~ and notes.sh's own defaults apply when there is no rc. + // Single-quoted JS strings: a template literal would interpolate ${...}. + // Output is one field per line: base, db, notes dir, git on, client. + property Process cfg: Process { + running: true + command: ["sh", "-c", + '[ -r "$HOME/.config/bash-notes.rc" ] && . "$HOME/.config/bash-notes.rc"\n' + + 'base="${BASEDIR:-$HOME/.local/share/bash-notes}"\n' + + 'git=0; [ "${USEGIT:-false}" = "true" ] && [ -n "${GITREMOTE:-}" ] && git=1\n' + + 'printf "%s\\n" "$base" "${DB:-$base/db.json}" "${NOTESDIR:-$base/notes}" "$git" "${GITCLIENT:-$(hostname)}"'] + stdout: StdioCollector { onStreamFinished: mod.applyConfig(this.text) } + } + + function applyConfig(text) { + const l = String(text).split("\n"); + mod.baseDir = l[0] || ""; + mod.dbPath = l[1] || ""; + mod.notesDir = l[2] || ""; + mod.gitEnabled = (l[3] || "") === "1"; + mod.client = l[4] || ""; + } + + // The store, watched rather than read on open: the tile's count has to + // move when a note is added from either side, the CLI included. A missing + // file is an empty store, not an error worth logging. + property FileView dbView: FileView { + path: mod.dbPath + atomicWrites: true + watchChanges: true + printErrors: false + onFileChanged: mod.reparse() + onLoaded: mod.reparse() + onLoadFailed: mod.reparse() + } + + // Nothing to read until the config has resolved, so the load failed on the + // empty path does not mark the store ready. + function reparse() { + if (mod.dbPath === "") return; + mod.db = Notes.parse(mod.dbView.text()); + mod.ready = true; + } + + tileContent: Component { + Text { + width: parent ? parent.width : implicitWidth + elide: Text.ElideRight + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + color: Theme.subtext + text: !mod.ready ? "…" + : mod.count === 0 ? "empty" + : mod.count === 1 ? "1 note" : mod.count + " notes" + } + } + + page: Component { + Page { + id: pg + title: "Notes" + // 57 is the page header plus its rule, the reading ClipboardModule + // takes of the same number. + NotesPage { + width: parent.width + height: pg.height - 57 + owner: mod + } + } + } +} diff --git a/desktop/modules/notes/NotesPage.qml b/desktop/modules/notes/NotesPage.qml new file mode 100644 index 0000000..d73bea2 --- /dev/null +++ b/desktop/modules/notes/NotesPage.qml @@ -0,0 +1,121 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import QtQuick +import QtQuick.Controls +import "../.." +import "Notes.js" as Notes + +// The notes list, newest first, with a title search. Reads the module's parsed +// db.json; nothing here touches the store or git. +Item { + id: page + + // The notes module: the store, the resolved paths and the writes. + required property QtObject owner + + Row { + id: bar + anchors { top: parent.top; left: parent.left; right: parent.right } + spacing: 8 + + TextField { + id: search + width: parent.width + height: 32 + placeholderText: "Search" + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + background: Rectangle { + radius: 6 + color: Qt.alpha(Theme.surface, 0.9) + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.15) + } + Component.onCompleted: forceActiveFocus() + onTextChanged: list.currentIndex = 0 + } + } + + ListView { + id: list + anchors { + top: bar.bottom; topMargin: 10 + left: parent.left; right: parent.right + bottom: errorLine.top; bottomMargin: 6 + } + clip: true + spacing: 4 + model: Notes.filter(page.owner.notes, search.text) + boundsBehavior: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar {} + + Text { + anchors.centerIn: parent + visible: list.count === 0 + text: page.owner.ready ? "No notes" : "Loading…" + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + } + + delegate: Rectangle { + id: row + required property var modelData + required property int index + + width: list.width + height: 36 + radius: 8 + color: rowArea.containsMouse || ListView.isCurrentItem + ? Qt.alpha(Theme.accent, 0.18) : Qt.alpha(Theme.surface, 0.35) + + MouseArea { + id: rowArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + + Text { + anchors { + left: parent.left; leftMargin: 10 + right: when.left; rightMargin: 8 + verticalCenter: parent.verticalCenter + } + elide: Text.ElideRight + maximumLineCount: 1 + textFormat: Text.PlainText + text: row.modelData.title + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + } + + Text { + id: when + anchors { right: parent.right; rightMargin: 10; verticalCenter: parent.verticalCenter } + text: Notes.formatDate(row.modelData.file) + color: Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + } + } + + Text { + id: errorLine + anchors { left: parent.left; right: parent.right; bottom: parent.bottom } + height: visible ? implicitHeight : 0 + visible: page.owner.lastError !== "" + text: page.owner.lastError + wrapMode: Text.WordWrap + color: Theme.red + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } +} |
