From fc1c1c5c8cd0595a3eb29374802c5a66d5d373e8 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 15:22:56 +0200 Subject: [PATCH 01/13] initial commit --- zettelkasten/ZkLinkDialog.qml | 216 ++++++++++++++++++++++++++++++++++ zettelkasten/info.json | 10 ++ zettelkasten/zettelkasten.qml | 142 ++++++++++++++++++++++ 3 files changed, 368 insertions(+) create mode 100644 zettelkasten/ZkLinkDialog.qml create mode 100644 zettelkasten/info.json create mode 100644 zettelkasten/zettelkasten.qml diff --git a/zettelkasten/ZkLinkDialog.qml b/zettelkasten/ZkLinkDialog.qml new file mode 100644 index 0000000..90c2e29 --- /dev/null +++ b/zettelkasten/ZkLinkDialog.qml @@ -0,0 +1,216 @@ +import QtQuick 2.0 +import QtQuick.Window 2.0 + +Window { + id: root + title: "Insert Zettelkasten link" + width: 620 + height: 420 + minimumWidth: 480 + minimumHeight: 300 + modality: Qt.ApplicationModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint + + property var entries: [] + signal linkSelected(string linkTarget, string zkId) + property var filtered: [] + + SystemPalette { id: pal } + + Component.onCompleted: { + applyFilter(); + searchInput.forceActiveFocus(); + } + + // ── Search field ────────────────────────────────────────────────────────── + + Rectangle { + id: searchBox + anchors { top: parent.top; left: parent.left; right: parent.right; margins: 10 } + height: 30 + radius: 3 + color: pal.base + border.color: searchInput.activeFocus ? "#1cb27e" : pal.mid + border.width: 1 + + Text { + anchors { fill: parent; leftMargin: 8 } + verticalAlignment: Text.AlignVCenter + text: "Filter by name…" + color: pal.mid + font.pixelSize: 13 + visible: searchInput.text === "" + } + + TextInput { + id: searchInput + anchors { fill: parent; margins: 8 } + verticalAlignment: TextInput.AlignVCenter + font.pixelSize: 13 + color: pal.text + clip: true + onTextChanged: applyFilter() + Keys.onReturnPressed: acceptSelection() + Keys.onDownPressed: moveSelection(1) + Keys.onUpPressed: moveSelection(-1) + } + } + + // ── Note list ───────────────────────────────────────────────────────────── + + Rectangle { + id: listBox + anchors { + top: searchBox.bottom; topMargin: 6 + left: parent.left; right: parent.right; margins: 10 + bottom: bottomBar.top; bottomMargin: 6 + } + radius: 3 + color: pal.base + border.color: pal.mid + border.width: 1 + clip: true + + ListView { + id: resultList + anchors { fill: parent; margins: 1; rightMargin: scrollBar.visible ? 8 : 1 } + model: filtered + currentIndex: 0 + clip: true + boundsBehavior: Flickable.StopAtBounds + + delegate: Item { + width: resultList.width + height: 28 + + Rectangle { + anchors.fill: parent + color: index === resultList.currentIndex + ? "#1cb27e" + : (rowMouse.containsMouse ? "#e4f5ef" : "transparent") + } + + Text { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left; right: parent.right + margins: 8 + } + text: modelData.label + color: index === resultList.currentIndex ? "white" : pal.text + font.pixelSize: 13 + elide: Text.ElideRight + } + + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + onClicked: resultList.currentIndex = index + onDoubleClicked: acceptSelection() + } + } + } + + // Minimal scrollbar + Rectangle { + id: scrollBar + visible: resultList.contentHeight > resultList.height + width: 5 + anchors { right: parent.right; top: parent.top; bottom: parent.bottom; margins: 1 } + color: "transparent" + + Rectangle { + width: parent.width + radius: 2 + color: pal.mid + height: Math.max(24, resultList.height * resultList.height / Math.max(resultList.contentHeight, 1)) + y: resultList.height > 0 + ? resultList.contentY / Math.max(resultList.contentHeight - resultList.height, 1) + * (resultList.height - height) + : 0 + } + } + + Text { + anchors.centerIn: parent + visible: filtered.length === 0 + text: "No matching note found." + color: pal.mid + font.pixelSize: 13 + } + } + + // ── Bottom bar ──────────────────────────────────────────────────────────── + + Item { + id: bottomBar + anchors { bottom: parent.bottom; left: parent.left; right: parent.right; margins: 10 } + height: 34 + + Text { + anchors { verticalCenter: parent.verticalCenter; left: parent.left } + text: filtered.length + " note(s)" + color: pal.mid + font.pixelSize: 12 + } + + // Cancel + Rectangle { + id: cancelBtn + anchors { verticalCenter: parent.verticalCenter; right: insertBtn.left; rightMargin: 8 } + width: 76; height: 26 + radius: 4 + color: cancelMouse.pressed ? pal.dark : pal.button + border.color: pal.mid + border.width: 1 + + Text { anchors.centerIn: parent; text: "Cancel"; color: pal.buttonText; font.pixelSize: 13 } + MouseArea { id: cancelMouse; anchors.fill: parent; onClicked: root.close() } + } + + // Insert + Rectangle { + id: insertBtn + anchors { verticalCenter: parent.verticalCenter; right: parent.right } + width: 76; height: 26 + radius: 4 + opacity: (resultList.currentIndex >= 0 && filtered.length > 0) ? 1.0 : 0.4 + color: insertMouse.pressed ? "#15896b" : "#1cb27e" + + Text { anchors.centerIn: parent; text: "Insert"; color: "white"; font.pixelSize: 13 } + MouseArea { + id: insertMouse + anchors.fill: parent + enabled: resultList.currentIndex >= 0 && filtered.length > 0 + onClicked: acceptSelection() + } + } + } + + // ── Logic ───────────────────────────────────────────────────────────────── + + function moveSelection(delta) { + var next = resultList.currentIndex + delta; + if (next >= 0 && next < resultList.count) + resultList.currentIndex = next; + } + + function applyFilter() { + var f = searchInput.text ? searchInput.text.toLowerCase() : ""; + var result = []; + for (var i = 0; i < entries.length; i++) { + if (!f || entries[i].label.toLowerCase().indexOf(f) >= 0) + result.push(entries[i]); + } + filtered = result; + resultList.currentIndex = result.length > 0 ? 0 : -1; + } + + function acceptSelection() { + var idx = resultList.currentIndex; + if (idx < 0 || idx >= filtered.length) return; + linkSelected(filtered[idx].linkTarget, filtered[idx].zkId); + root.close(); + } +} diff --git a/zettelkasten/info.json b/zettelkasten/info.json new file mode 100644 index 0000000..ac87211 --- /dev/null +++ b/zettelkasten/info.json @@ -0,0 +1,10 @@ +{ + "name": "Zettelkasten", + "identifier": "luginf-zettelkasten", + "version": "0.1.0", + "script": "zettelkasten.qml", + "authors": ["alan"], + "platforms": ["linux", "macos", "windows"], + "minAppVersion": "22.12.0", + "description": "Zettelkasten support: generates 14-digit timestamp IDs and inserts [[filename|ID]] wiki-links to notes detected by a configurable ECMAScript ID pattern." +} diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml new file mode 100644 index 0000000..222fde5 --- /dev/null +++ b/zettelkasten/zettelkasten.qml @@ -0,0 +1,142 @@ +// Zettelkasten support for QOwnNotes. +// +// Two actions: +// • Insert ZK ID — inserts an ID built from a configurable format string +// • Insert ZK link — filters notes by name, picks one, inserts [[filename|ID]] +// +// ID format tokens: %Y year %M month %D day %h hour %m minute %s second +// Example format: id%Y%M%Dx%h%m%s → id20260430x143012 +// +// IDs are detected via a configurable ECMAScript regex (default: \d{14}). +// The regex is tested first against the note filename, then full content — +// first match only. +// +// Link format: [[nom_fichier_sur_disque.md|20260430143012]] +// QOwnNotes resolves the filename part as a wiki-link (Ctrl+click to open). + +import QtQml 2.0 +import QOwnNotesTypes 1.0 + +Script { + property string idRegex + property string idFormat + + property variant settingsVariables: [ + { + "identifier": "idRegex", + "name": "ID detection pattern (ECMAScript regex)", + "description": "Pattern used to detect Zettelkasten IDs in note filenames and content.\nDefault matches 14-digit timestamps: \\d{14}", + "type": "string", + "default": "\\d{14}" + }, + { + "identifier": "idFormat", + "name": "ID generation format", + "description": "Format string for generating new IDs.\nTokens: %Y=year %M=month %D=day %h=hour %m=minute %s=second\nLiteral characters are kept as-is.\n\nExamples:\n %Y%M%D%h%m%s → 20260430143012\n id%Y%M%Dx%h%m%s → id20260430x143012\n %Y-%M-%D → 2026-04-30", + "type": "string", + "default": "%Y%M%D%h%m%s" + } + ] + + function init() { + script.registerCustomAction("zkInsertId", "Insert Zettelkasten ID", "ZK-ID", "", false, false, true); + script.registerCustomAction("zkInsertLink", "Insert Zettelkasten link", "ZK-Link", "", false, false, true); + } + + function customActionInvoked(identifier) { + if (identifier === "zkInsertId") { + insertZkId(); + } else if (identifier === "zkInsertLink") { + insertZkLink(); + } + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + function generateId() { + var fmt = (idFormat || "").trim() || "%Y%M%D%h%m%s"; + var d = new Date(); + var p = function(n) { return n < 10 ? "0" + n : String(n); }; + return fmt + .replace(/%Y/g, String(d.getFullYear())) + .replace(/%M/g, p(d.getMonth() + 1)) + .replace(/%D/g, p(d.getDate())) + .replace(/%h/g, p(d.getHours())) + .replace(/%m/g, p(d.getMinutes())) + .replace(/%s/g, p(d.getSeconds())); + } + + function extractId(text) { + try { + var re = new RegExp(idRegex || "\\d{14}"); + var m = text.match(re); + return m ? m[0] : null; + } catch (e) { + script.log("zettelkasten: invalid ID regex — " + e); + return null; + } + } + + // ── Actions ─────────────────────────────────────────────────────────────── + + function insertZkId() { + script.noteTextEditWrite(generateId()); + } + + function insertZkLink() { + var noteIds = script.fetchNoteIdsByNoteTextPart(""); + var entries = []; + + for (var i = 0; i < noteIds.length; i++) { + var note = script.fetchNoteById(noteIds[i]); + if (!note || !note.fileName) continue; + + // Check filename first, then full content — first match only + var zkId = extractId(note.fileName); + if (!zkId) zkId = extractId(note.noteText); + if (!zkId) continue; + + // Strip .txt extension — QOwnNotes wiki-links don't resolve it + var linkTarget = /\.txt$/i.test(note.fileName) + ? note.fileName.slice(0, note.fileName.length - 4) + : note.fileName; + + entries.push({ + label: zkId + " — " + note.name, + linkTarget: linkTarget, + zkId: zkId + }); + } + + if (entries.length === 0) { + script.informationMessageBox( + "No note with a Zettelkasten ID was found.\nPattern: " + (idRegex || "\\d{14}"), + "Zettelkasten" + ); + return; + } + + // Most recent first + entries.sort(function(a, b) { + return b.zkId > a.zkId ? 1 : b.zkId < a.zkId ? -1 : 0; + }); + + var component = Qt.createComponent(Qt.resolvedUrl("ZkLinkDialog.qml")); + if (component.status !== Component.Ready) { + script.informationMessageBox( + "Failed to load ZkLinkDialog:\n" + component.errorString(), "Zettelkasten"); + return; + } + var dialog = component.createObject(null, { entries: entries }); + if (!dialog) { + script.informationMessageBox("Failed to instantiate ZkLinkDialog.", "Zettelkasten"); + return; + } + dialog.linkSelected.connect(function(linkTarget, zkId) { + script.noteTextEditWrite("[[" + linkTarget + "|" + zkId + "]]"); + }); + dialog.show(); + dialog.raise(); + dialog.requestActivate(); + } +} From db3154f2b2f4dbb0de7c0d29ae03346db883f9c3 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 15:40:25 +0200 Subject: [PATCH 02/13] repair ztl --- zettelkasten/info.json | 2 +- zettelkasten/zettelkasten.qml | 181 +++++++++++++++++++++++++++++++--- 2 files changed, 171 insertions(+), 12 deletions(-) diff --git a/zettelkasten/info.json b/zettelkasten/info.json index ac87211..074ad0b 100644 --- a/zettelkasten/info.json +++ b/zettelkasten/info.json @@ -1,7 +1,7 @@ { "name": "Zettelkasten", "identifier": "luginf-zettelkasten", - "version": "0.1.0", + "version": "0.2.0", "script": "zettelkasten.qml", "authors": ["alan"], "platforms": ["linux", "macos", "windows"], diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml index 222fde5..2f1bd04 100644 --- a/zettelkasten/zettelkasten.qml +++ b/zettelkasten/zettelkasten.qml @@ -1,8 +1,9 @@ // Zettelkasten support for QOwnNotes. // -// Two actions: -// • Insert ZK ID — inserts an ID built from a configurable format string -// • Insert ZK link — filters notes by name, picks one, inserts [[filename|ID]] +// Actions: +// • Insert ZK ID — inserts an ID built from a configurable format string +// • Insert ZK link — filters notes by name, picks one, inserts [[filename|ID]] +// • Repair ZK links — scans all notes and fixes [[oldName|ID]] → [[newName|ID]] // // ID format tokens: %Y year %M month %D day %h hour %m minute %s second // Example format: id%Y%M%Dx%h%m%s → id20260430x143012 @@ -13,6 +14,11 @@ // // Link format: [[nom_fichier_sur_disque.md|20260430143012]] // QOwnNotes resolves the filename part as a wiki-link (Ctrl+click to open). +// +// Rename resilience: when a note carrying a ZK ID is opened, any backlinks in +// other notes that still reference its old filename are silently rewritten to +// use the current filename. A manual "Repair ZK links" action (ZK-Fix toolbar +// button) performs the same scan across the entire vault in one pass. import QtQml 2.0 import QOwnNotesTypes 1.0 @@ -20,6 +26,10 @@ import QOwnNotesTypes 1.0 Script { property string idRegex property string idFormat + property bool autoRepairLinks: true + + // Runtime state — not a user setting + property string notesDir: "" property variant settingsVariables: [ { @@ -35,12 +45,20 @@ Script { "description": "Format string for generating new IDs.\nTokens: %Y=year %M=month %D=day %h=hour %m=minute %s=second\nLiteral characters are kept as-is.\n\nExamples:\n %Y%M%D%h%m%s → 20260430143012\n id%Y%M%Dx%h%m%s → id20260430x143012\n %Y-%M-%D → 2026-04-30", "type": "string", "default": "%Y%M%D%h%m%s" + }, + { + "identifier": "autoRepairLinks", + "name": "Auto-repair backlinks on note open", + "description": "When a note with a ZK ID is opened, automatically rewrite any backlinks in other notes that still use an outdated filename for that ID.", + "type": "boolean", + "default": true } ] function init() { - script.registerCustomAction("zkInsertId", "Insert Zettelkasten ID", "ZK-ID", "", false, false, true); - script.registerCustomAction("zkInsertLink", "Insert Zettelkasten link", "ZK-Link", "", false, false, true); + script.registerCustomAction("zkInsertId", "Insert Zettelkasten ID", "ZK-ID", "", false, false, true); + script.registerCustomAction("zkInsertLink", "Insert Zettelkasten link", "ZK-Link","", false, false, true); + script.registerCustomAction("zkRepairLinks", "Repair Zettelkasten links","ZK-Fix", "", false, false, false); } function customActionInvoked(identifier) { @@ -48,6 +66,15 @@ Script { insertZkId(); } else if (identifier === "zkInsertLink") { insertZkLink(); + } else if (identifier === "zkRepairLinks") { + repairAllLinks(); + } + } + + function noteOpenedHook(note) { + resolveNotesDir(); + if (autoRepairLinks !== false) { + repairBacklinksFor(note); } } @@ -77,6 +104,143 @@ Script { } } + // Returns the link target string for [[target|id]] given a note object. + function noteLinkTarget(note) { + return /\.txt$/i.test(note.fileName) + ? note.fileName.slice(0, note.fileName.length - 4) + : note.fileName; + } + + function regEscape(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + // Populate notesDir — tries several API approaches. + function resolveNotesDir() { + if (notesDir) return; + + // Strategy 1: NoteFolder.localPath + try { + var folder = script.currentNoteFolder(); + script.log("zettelkasten: currentNoteFolder=" + JSON.stringify(folder) + + " localPath=" + (folder ? folder.localPath : "n/a")); + if (folder && folder.localPath) { + notesDir = folder.localPath; + return; + } + } catch (e) { + script.log("zettelkasten: currentNoteFolder() error: " + e); + } + + // Strategy 2: current note's full file path + try { + var note = script.currentNote(); + script.log("zettelkasten: currentNote.fullNoteFilePath=" + + (note ? note.fullNoteFilePath : "n/a")); + if (note && note.fullNoteFilePath) { + notesDir = note.fullNoteFilePath.replace(/[\/\\][^\/\\]+$/, ""); + return; + } + } catch (e) { + script.log("zettelkasten: currentNote() error: " + e); + } + + script.log("zettelkasten: could not resolve notes directory"); + } + + // ── Backlink repair ─────────────────────────────────────────────────────── + + // When a note is opened, find all notes that link to its ZK ID with a + // stale filename and rewrite those links to use the current filename. + function repairBacklinksFor(note) { + if (!note || !note.fileName || !notesDir) return; + + var zkId = extractId(note.fileName); + if (!zkId) zkId = extractId(note.noteText); + if (!zkId) return; + + var currentTarget = noteLinkTarget(note); + + // Fetch notes that contain the literal string "|zkId]]" + var candidates = script.fetchNoteIdsByNoteTextPart("|" + zkId + "]]"); + + var pattern = new RegExp( + "\\[\\[([^\\]|]*)\\|" + regEscape(zkId) + "\\]\\]", "g" + ); + + for (var i = 0; i < candidates.length; i++) { + var n = script.fetchNoteById(candidates[i]); + if (!n || !n.noteText) continue; + + var changed = false; + var newText = n.noteText.replace(pattern, function(match, oldTarget) { + if (oldTarget === currentTarget) return match; + changed = true; + return "[[" + currentTarget + "|" + zkId + "]]"; + }); + + if (changed) { + script.writeToFile(notesDir + "/" + n.fileName, newText, false); + script.log("zettelkasten: repaired backlink in \"" + n.fileName + + "\" → [[" + currentTarget + "|" + zkId + "]]"); + } + } + } + + // Full vault scan: build an id→currentTarget map, then rewrite every + // [[staleTarget|id]] in every note. + function repairAllLinks() { + resolveNotesDir(); + if (!notesDir) { + script.informationMessageBox( + "Notes directory not available.\nPlease open a note first.", "Zettelkasten"); + return; + } + + var allIds = script.fetchNoteIdsByNoteTextPart(""); + + // Build zkId → correct link target + var idMap = {}; + for (var i = 0; i < allIds.length; i++) { + var note = script.fetchNoteById(allIds[i]); + if (!note || !note.fileName) continue; + var zkId = extractId(note.fileName); + if (!zkId) zkId = extractId(note.noteText); + if (!zkId) continue; + idMap[zkId] = noteLinkTarget(note); + } + + var pattern = /\[\[([^\]|]*)\|([^\]]*)\]\]/g; + var repairedLinks = 0; + var repairedNotes = 0; + + for (var j = 0; j < allIds.length; j++) { + var n = script.fetchNoteById(allIds[j]); + if (!n || !n.noteText) continue; + + var changed = false; + var newText = n.noteText.replace(pattern, function(match, linkTarget, linkId) { + var correct = idMap[linkId]; + if (!correct || correct === linkTarget) return match; + changed = true; + repairedLinks++; + return "[[" + correct + "|" + linkId + "]]"; + }); + + if (changed) { + script.writeToFile(notesDir + "/" + n.fileName, newText, false); + repairedNotes++; + } + } + + script.informationMessageBox( + repairedLinks > 0 + ? "Repaired " + repairedLinks + " link(s) in " + repairedNotes + " note(s)." + : "All Zettelkasten links are up to date.", + "Zettelkasten" + ); + } + // ── Actions ─────────────────────────────────────────────────────────────── function insertZkId() { @@ -96,14 +260,9 @@ Script { if (!zkId) zkId = extractId(note.noteText); if (!zkId) continue; - // Strip .txt extension — QOwnNotes wiki-links don't resolve it - var linkTarget = /\.txt$/i.test(note.fileName) - ? note.fileName.slice(0, note.fileName.length - 4) - : note.fileName; - entries.push({ label: zkId + " — " + note.name, - linkTarget: linkTarget, + linkTarget: noteLinkTarget(note), zkId: zkId }); } From 10f5df54e0d70aace0ee63120bd6a08e713388cd Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 15:56:24 +0200 Subject: [PATCH 03/13] adding readme + fix --- zettelkasten/README.md | 76 +++++++++++++++++++++++++++++++++++ zettelkasten/zettelkasten.qml | 14 +++---- 2 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 zettelkasten/README.md diff --git a/zettelkasten/README.md b/zettelkasten/README.md new file mode 100644 index 0000000..3dfbce8 --- /dev/null +++ b/zettelkasten/README.md @@ -0,0 +1,76 @@ +# Zettelkasten for QOwnNotes + +A QOwnNotes script that adds [Zettelkasten](https://en.wikipedia.org/wiki/Zettelkasten) support: unique IDs for notes and permanent wiki-links that survive note renames. + +## Concept + +In the Zettelkasten method, each note carries a **permanent unique ID** embedded in its content or filename. Links between notes are based on this ID, not on the filename. This means a note can be renamed freely without breaking any link pointing to it. + +This script implements that principle inside QOwnNotes using the native `[[filename|id]]` wiki-link format. + +## Link format + +``` +[[MyNote|20260430143012]] +``` + +- The left part (`MyNote`) is what QOwnNotes uses to resolve the link (Ctrl+click to open). +- The right part (`20260430143012`) is the permanent ZK ID, used by this script to repair links when the filename changes. + +## Actions + +Three toolbar buttons are registered: + +| Button | Action | +|--------|--------| +| **ZK-ID** | Insert a new unique ZK ID at the cursor position | +| **ZK-Link** | Open a searchable dialog to pick a note and insert a `[[filename\|id]]` link | +| **ZK-Fix** | Scan all notes and repair every link whose filename is out of date | + +## ID format + +IDs are generated from the current date and time using a configurable format string. + +Available tokens: + +| Token | Value | +|-------|-------| +| `%Y` | 4-digit year | +| `%M` | 2-digit month | +| `%D` | 2-digit day | +| `%h` | 2-digit hour | +| `%m` | 2-digit minute | +| `%s` | 2-digit second | + +Examples: + +| Format | Result | +|--------|--------| +| `%Y%M%D%h%m%s` *(default)* | `20260430143012` | +| `id%Y%M%Dx%h%m%s` | `id20260430x143012` | +| `%Y-%M-%D` | `2026-04-30` | + +## ID detection + +When searching for a note's ZK ID, the script checks the **filename** first, then the full **note body**. Only the first match is used. + +The detection pattern is a configurable ECMAScript regex. The default `\d{14}` matches any 14-digit timestamp. If you use a custom format with a prefix (e.g. `id%Y%M%Dx%h%m%s`), update the regex accordingly — for example `id\d{8}x\d{6}`. + +## Rename resilience + +When you rename a note in QOwnNotes, any `[[oldName|id]]` links in other notes become stale. This script fixes them automatically in two ways: + +- **Automatic** — when you open the renamed note, the script silently rewrites every backlink pointing to it with the new filename. This happens in the background with no interruption. +- **Manual** — click **ZK-Fix** at any time to repair all stale links across the entire vault in one pass. + +> **Note:** QOwnNotes may show a native dialog asking whether to replace link occurrences after a rename. That dialog does not understand the `[[filename|id]]` format and will not change anything. You can safely click *No* and let this script handle it, or disable the dialog entirely in *Settings → Notes*. + +## Settings + +All settings are accessible in *Settings → Scripting → Zettelkasten*: + +| Setting | Default | Description | +|---------|---------|-------------| +| ID generation format | `%Y%M%D%h%m%s` | Format string for new IDs | +| ID detection pattern | `\d{14}` | ECMAScript regex to locate IDs in filenames and content | +| Auto-repair backlinks on note open | enabled | Automatically fix stale backlinks when a note with a ZK ID is opened | diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml index 2f1bd04..f58ac96 100644 --- a/zettelkasten/zettelkasten.qml +++ b/zettelkasten/zettelkasten.qml @@ -32,13 +32,6 @@ Script { property string notesDir: "" property variant settingsVariables: [ - { - "identifier": "idRegex", - "name": "ID detection pattern (ECMAScript regex)", - "description": "Pattern used to detect Zettelkasten IDs in note filenames and content.\nDefault matches 14-digit timestamps: \\d{14}", - "type": "string", - "default": "\\d{14}" - }, { "identifier": "idFormat", "name": "ID generation format", @@ -46,6 +39,13 @@ Script { "type": "string", "default": "%Y%M%D%h%m%s" }, + { + "identifier": "idRegex", + "name": "ID detection pattern (ECMAScript regex)", + "description": "Pattern used to detect Zettelkasten IDs in note filenames and content.\nDefault matches 14-digit timestamps: \\d{14}", + "type": "string", + "default": "\\d{14}" + }, { "identifier": "autoRepairLinks", "name": "Auto-repair backlinks on note open", From 2009bbf526f7c1771d76c49399feaaee3f2d88ea Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 16:09:11 +0200 Subject: [PATCH 04/13] updating info.json --- zettelkasten/info.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zettelkasten/info.json b/zettelkasten/info.json index 074ad0b..c57e53d 100644 --- a/zettelkasten/info.json +++ b/zettelkasten/info.json @@ -1,10 +1,10 @@ { "name": "Zettelkasten", - "identifier": "luginf-zettelkasten", + "identifier": "zettelkasten", "version": "0.2.0", "script": "zettelkasten.qml", - "authors": ["alan"], + "authors": ["@luginf"], "platforms": ["linux", "macos", "windows"], - "minAppVersion": "22.12.0", + "minAppVersion": "26.4.11", "description": "Zettelkasten support: generates 14-digit timestamp IDs and inserts [[filename|ID]] wiki-links to notes detected by a configurable ECMAScript ID pattern." } From 098bb9aff7e297ed889bd8161dfa46782627d9d3 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 16:45:04 +0200 Subject: [PATCH 05/13] fix format with qmlformat --- zettelkasten/README.md | 4 + zettelkasten/ZkLinkDialog.qml | 116 +++++++++++++++------- zettelkasten/zettelkasten.qml | 178 ++++++++++++++-------------------- 3 files changed, 159 insertions(+), 139 deletions(-) diff --git a/zettelkasten/README.md b/zettelkasten/README.md index 3dfbce8..fe53992 100644 --- a/zettelkasten/README.md +++ b/zettelkasten/README.md @@ -74,3 +74,7 @@ All settings are accessible in *Settings → Scripting → Zettelkasten*: | ID generation format | `%Y%M%D%h%m%s` | Format string for new IDs | | ID detection pattern | `\d{14}` | ECMAScript regex to locate IDs in filenames and content | | Auto-repair backlinks on note open | enabled | Automatically fix stale backlinks when a note with a ZK ID is opened | + + +fix with qmlformat + diff --git a/zettelkasten/ZkLinkDialog.qml b/zettelkasten/ZkLinkDialog.qml index 90c2e29..bd1be68 100644 --- a/zettelkasten/ZkLinkDialog.qml +++ b/zettelkasten/ZkLinkDialog.qml @@ -15,7 +15,9 @@ Window { signal linkSelected(string linkTarget, string zkId) property var filtered: [] - SystemPalette { id: pal } + SystemPalette { + id: pal + } Component.onCompleted: { applyFilter(); @@ -23,10 +25,14 @@ Window { } // ── Search field ────────────────────────────────────────────────────────── - Rectangle { id: searchBox - anchors { top: parent.top; left: parent.left; right: parent.right; margins: 10 } + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: 10 + } height: 30 radius: 3 color: pal.base @@ -34,7 +40,10 @@ Window { border.width: 1 Text { - anchors { fill: parent; leftMargin: 8 } + anchors { + fill: parent + leftMargin: 8 + } verticalAlignment: Text.AlignVCenter text: "Filter by name…" color: pal.mid @@ -44,7 +53,10 @@ Window { TextInput { id: searchInput - anchors { fill: parent; margins: 8 } + anchors { + fill: parent + margins: 8 + } verticalAlignment: TextInput.AlignVCenter font.pixelSize: 13 color: pal.text @@ -52,18 +64,21 @@ Window { onTextChanged: applyFilter() Keys.onReturnPressed: acceptSelection() Keys.onDownPressed: moveSelection(1) - Keys.onUpPressed: moveSelection(-1) + Keys.onUpPressed: moveSelection(-1) } } // ── Note list ───────────────────────────────────────────────────────────── - Rectangle { id: listBox anchors { - top: searchBox.bottom; topMargin: 6 - left: parent.left; right: parent.right; margins: 10 - bottom: bottomBar.top; bottomMargin: 6 + top: searchBox.bottom + topMargin: 6 + left: parent.left + right: parent.right + margins: 10 + bottom: bottomBar.top + bottomMargin: 6 } radius: 3 color: pal.base @@ -73,7 +88,11 @@ Window { ListView { id: resultList - anchors { fill: parent; margins: 1; rightMargin: scrollBar.visible ? 8 : 1 } + anchors { + fill: parent + margins: 1 + rightMargin: scrollBar.visible ? 8 : 1 + } model: filtered currentIndex: 0 clip: true @@ -85,15 +104,14 @@ Window { Rectangle { anchors.fill: parent - color: index === resultList.currentIndex - ? "#1cb27e" - : (rowMouse.containsMouse ? "#e4f5ef" : "transparent") + color: index === resultList.currentIndex ? "#1cb27e" : (rowMouse.containsMouse ? "#e4f5ef" : "transparent") } Text { anchors { verticalCenter: parent.verticalCenter - left: parent.left; right: parent.right + left: parent.left + right: parent.right margins: 8 } text: modelData.label @@ -106,7 +124,7 @@ Window { id: rowMouse anchors.fill: parent hoverEnabled: true - onClicked: resultList.currentIndex = index + onClicked: resultList.currentIndex = index onDoubleClicked: acceptSelection() } } @@ -117,7 +135,12 @@ Window { id: scrollBar visible: resultList.contentHeight > resultList.height width: 5 - anchors { right: parent.right; top: parent.top; bottom: parent.bottom; margins: 1 } + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 1 + } color: "transparent" Rectangle { @@ -125,10 +148,7 @@ Window { radius: 2 color: pal.mid height: Math.max(24, resultList.height * resultList.height / Math.max(resultList.contentHeight, 1)) - y: resultList.height > 0 - ? resultList.contentY / Math.max(resultList.contentHeight - resultList.height, 1) - * (resultList.height - height) - : 0 + y: resultList.height > 0 ? resultList.contentY / Math.max(resultList.contentHeight - resultList.height, 1) * (resultList.height - height) : 0 } } @@ -142,14 +162,21 @@ Window { } // ── Bottom bar ──────────────────────────────────────────────────────────── - Item { id: bottomBar - anchors { bottom: parent.bottom; left: parent.left; right: parent.right; margins: 10 } + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + margins: 10 + } height: 34 Text { - anchors { verticalCenter: parent.verticalCenter; left: parent.left } + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + } text: filtered.length + " note(s)" color: pal.mid font.pixelSize: 12 @@ -158,27 +185,50 @@ Window { // Cancel Rectangle { id: cancelBtn - anchors { verticalCenter: parent.verticalCenter; right: insertBtn.left; rightMargin: 8 } - width: 76; height: 26 + anchors { + verticalCenter: parent.verticalCenter + right: insertBtn.left + rightMargin: 8 + } + width: 76 + height: 26 radius: 4 color: cancelMouse.pressed ? pal.dark : pal.button border.color: pal.mid border.width: 1 - Text { anchors.centerIn: parent; text: "Cancel"; color: pal.buttonText; font.pixelSize: 13 } - MouseArea { id: cancelMouse; anchors.fill: parent; onClicked: root.close() } + Text { + anchors.centerIn: parent + text: "Cancel" + color: pal.buttonText + font.pixelSize: 13 + } + MouseArea { + id: cancelMouse + anchors.fill: parent + onClicked: root.close() + } } // Insert Rectangle { id: insertBtn - anchors { verticalCenter: parent.verticalCenter; right: parent.right } - width: 76; height: 26 + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + width: 76 + height: 26 radius: 4 opacity: (resultList.currentIndex >= 0 && filtered.length > 0) ? 1.0 : 0.4 color: insertMouse.pressed ? "#15896b" : "#1cb27e" - Text { anchors.centerIn: parent; text: "Insert"; color: "white"; font.pixelSize: 13 } + Text { + anchors.centerIn: parent + text: "Insert" + color: "white" + font.pixelSize: 13 + } MouseArea { id: insertMouse anchors.fill: parent @@ -189,7 +239,6 @@ Window { } // ── Logic ───────────────────────────────────────────────────────────────── - function moveSelection(delta) { var next = resultList.currentIndex + delta; if (next >= 0 && next < resultList.count) @@ -209,7 +258,8 @@ Window { function acceptSelection() { var idx = resultList.currentIndex; - if (idx < 0 || idx >= filtered.length) return; + if (idx < 0 || idx >= filtered.length) + return; linkSelected(filtered[idx].linkTarget, filtered[idx].zkId); root.close(); } diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml index f58ac96..2657981 100644 --- a/zettelkasten/zettelkasten.qml +++ b/zettelkasten/zettelkasten.qml @@ -1,25 +1,19 @@ // Zettelkasten support for QOwnNotes. -// // Actions: // • Insert ZK ID — inserts an ID built from a configurable format string // • Insert ZK link — filters notes by name, picks one, inserts [[filename|ID]] // • Repair ZK links — scans all notes and fixes [[oldName|ID]] → [[newName|ID]] -// // ID format tokens: %Y year %M month %D day %h hour %m minute %s second // Example format: id%Y%M%Dx%h%m%s → id20260430x143012 -// // IDs are detected via a configurable ECMAScript regex (default: \d{14}). // The regex is tested first against the note filename, then full content — // first match only. -// // Link format: [[nom_fichier_sur_disque.md|20260430143012]] // QOwnNotes resolves the filename part as a wiki-link (Ctrl+click to open). -// // Rename resilience: when a note carrying a ZK ID is opened, any backlinks in // other notes that still reference its old filename are silently rewritten to // use the current filename. A manual "Repair ZK links" action (ZK-Fix toolbar // button) performs the same scan across the entire vault in one pass. - import QtQml 2.0 import QOwnNotesTypes 1.0 @@ -31,34 +25,30 @@ Script { // Runtime state — not a user setting property string notesDir: "" - property variant settingsVariables: [ - { + property variant settingsVariables: [{ "identifier": "idFormat", "name": "ID generation format", "description": "Format string for generating new IDs.\nTokens: %Y=year %M=month %D=day %h=hour %m=minute %s=second\nLiteral characters are kept as-is.\n\nExamples:\n %Y%M%D%h%m%s → 20260430143012\n id%Y%M%Dx%h%m%s → id20260430x143012\n %Y-%M-%D → 2026-04-30", "type": "string", "default": "%Y%M%D%h%m%s" - }, - { + }, { "identifier": "idRegex", "name": "ID detection pattern (ECMAScript regex)", "description": "Pattern used to detect Zettelkasten IDs in note filenames and content.\nDefault matches 14-digit timestamps: \\d{14}", "type": "string", "default": "\\d{14}" - }, - { + }, { "identifier": "autoRepairLinks", "name": "Auto-repair backlinks on note open", "description": "When a note with a ZK ID is opened, automatically rewrite any backlinks in other notes that still use an outdated filename for that ID.", "type": "boolean", "default": true - } - ] + }] function init() { - script.registerCustomAction("zkInsertId", "Insert Zettelkasten ID", "ZK-ID", "", false, false, true); - script.registerCustomAction("zkInsertLink", "Insert Zettelkasten link", "ZK-Link","", false, false, true); - script.registerCustomAction("zkRepairLinks", "Repair Zettelkasten links","ZK-Fix", "", false, false, false); + script.registerCustomAction("zkInsertId", "Insert Zettelkasten ID", "ZK-ID", "", false, false, true); + script.registerCustomAction("zkInsertLink", "Insert Zettelkasten link", "ZK-Link", "", false, false, true); + script.registerCustomAction("zkRepairLinks", "Repair Zettelkasten links", "ZK-Fix", "", false, false, false); } function customActionInvoked(identifier) { @@ -79,24 +69,19 @@ Script { } // ── Helpers ─────────────────────────────────────────────────────────────── - function generateId() { var fmt = (idFormat || "").trim() || "%Y%M%D%h%m%s"; var d = new Date(); - var p = function(n) { return n < 10 ? "0" + n : String(n); }; - return fmt - .replace(/%Y/g, String(d.getFullYear())) - .replace(/%M/g, p(d.getMonth() + 1)) - .replace(/%D/g, p(d.getDate())) - .replace(/%h/g, p(d.getHours())) - .replace(/%m/g, p(d.getMinutes())) - .replace(/%s/g, p(d.getSeconds())); + var p = function (n) { + return n < 10 ? "0" + n : String(n); + }; + return fmt.replace(/%Y/g, String(d.getFullYear())).replace(/%M/g, p(d.getMonth() + 1)).replace(/%D/g, p(d.getDate())).replace(/%h/g, p(d.getHours())).replace(/%m/g, p(d.getMinutes())).replace(/%s/g, p(d.getSeconds())); } function extractId(text) { try { var re = new RegExp(idRegex || "\\d{14}"); - var m = text.match(re); + var m = text.match(re); return m ? m[0] : null; } catch (e) { script.log("zettelkasten: invalid ID regex — " + e); @@ -106,9 +91,7 @@ Script { // Returns the link target string for [[target|id]] given a note object. function noteLinkTarget(note) { - return /\.txt$/i.test(note.fileName) - ? note.fileName.slice(0, note.fileName.length - 4) - : note.fileName; + return /\.txt$/i.test(note.fileName) ? note.fileName.slice(0, note.fileName.length - 4) : note.fileName; } function regEscape(s) { @@ -117,13 +100,13 @@ Script { // Populate notesDir — tries several API approaches. function resolveNotesDir() { - if (notesDir) return; + if (notesDir) + return; // Strategy 1: NoteFolder.localPath try { var folder = script.currentNoteFolder(); - script.log("zettelkasten: currentNoteFolder=" + JSON.stringify(folder) - + " localPath=" + (folder ? folder.localPath : "n/a")); + script.log("zettelkasten: currentNoteFolder=" + JSON.stringify(folder) + " localPath=" + (folder ? folder.localPath : "n/a")); if (folder && folder.localPath) { notesDir = folder.localPath; return; @@ -135,8 +118,7 @@ Script { // Strategy 2: current note's full file path try { var note = script.currentNote(); - script.log("zettelkasten: currentNote.fullNoteFilePath=" - + (note ? note.fullNoteFilePath : "n/a")); + script.log("zettelkasten: currentNote.fullNoteFilePath=" + (note ? note.fullNoteFilePath : "n/a")); if (note && note.fullNoteFilePath) { notesDir = note.fullNoteFilePath.replace(/[\/\\][^\/\\]+$/, ""); return; @@ -144,7 +126,6 @@ Script { } catch (e) { script.log("zettelkasten: currentNote() error: " + e); } - script.log("zettelkasten: could not resolve notes directory"); } @@ -153,36 +134,32 @@ Script { // When a note is opened, find all notes that link to its ZK ID with a // stale filename and rewrite those links to use the current filename. function repairBacklinksFor(note) { - if (!note || !note.fileName || !notesDir) return; - + if (!note || !note.fileName || !notesDir) + return; var zkId = extractId(note.fileName); - if (!zkId) zkId = extractId(note.noteText); - if (!zkId) return; - + if (!zkId) + zkId = extractId(note.noteText); + if (!zkId) + return; var currentTarget = noteLinkTarget(note); // Fetch notes that contain the literal string "|zkId]]" var candidates = script.fetchNoteIdsByNoteTextPart("|" + zkId + "]]"); - - var pattern = new RegExp( - "\\[\\[([^\\]|]*)\\|" + regEscape(zkId) + "\\]\\]", "g" - ); - + var pattern = new RegExp("\\[\\[([^\\]|]*)\\|" + regEscape(zkId) + "\\]\\]", "g"); for (var i = 0; i < candidates.length; i++) { var n = script.fetchNoteById(candidates[i]); - if (!n || !n.noteText) continue; - + if (!n || !n.noteText) + continue; var changed = false; - var newText = n.noteText.replace(pattern, function(match, oldTarget) { - if (oldTarget === currentTarget) return match; - changed = true; - return "[[" + currentTarget + "|" + zkId + "]]"; - }); - + var newText = n.noteText.replace(pattern, function (match, oldTarget) { + if (oldTarget === currentTarget) + return match; + changed = true; + return "[[" + currentTarget + "|" + zkId + "]]"; + }); if (changed) { script.writeToFile(notesDir + "/" + n.fileName, newText, false); - script.log("zettelkasten: repaired backlink in \"" + n.fileName - + "\" → [[" + currentTarget + "|" + zkId + "]]"); + script.log("zettelkasten: repaired backlink in \"" + n.fileName + "\" → [[" + currentTarget + "|" + zkId + "]]"); } } } @@ -192,57 +169,49 @@ Script { function repairAllLinks() { resolveNotesDir(); if (!notesDir) { - script.informationMessageBox( - "Notes directory not available.\nPlease open a note first.", "Zettelkasten"); + script.informationMessageBox("Notes directory not available.\nPlease open a note first.", "Zettelkasten"); return; } - var allIds = script.fetchNoteIdsByNoteTextPart(""); // Build zkId → correct link target var idMap = {}; for (var i = 0; i < allIds.length; i++) { var note = script.fetchNoteById(allIds[i]); - if (!note || !note.fileName) continue; + if (!note || !note.fileName) + continue; var zkId = extractId(note.fileName); - if (!zkId) zkId = extractId(note.noteText); - if (!zkId) continue; + if (!zkId) + zkId = extractId(note.noteText); + if (!zkId) + continue; idMap[zkId] = noteLinkTarget(note); } - var pattern = /\[\[([^\]|]*)\|([^\]]*)\]\]/g; var repairedLinks = 0; var repairedNotes = 0; - for (var j = 0; j < allIds.length; j++) { var n = script.fetchNoteById(allIds[j]); - if (!n || !n.noteText) continue; - + if (!n || !n.noteText) + continue; var changed = false; - var newText = n.noteText.replace(pattern, function(match, linkTarget, linkId) { - var correct = idMap[linkId]; - if (!correct || correct === linkTarget) return match; - changed = true; - repairedLinks++; - return "[[" + correct + "|" + linkId + "]]"; - }); - + var newText = n.noteText.replace(pattern, function (match, linkTarget, linkId) { + var correct = idMap[linkId]; + if (!correct || correct === linkTarget) + return match; + changed = true; + repairedLinks++; + return "[[" + correct + "|" + linkId + "]]"; + }); if (changed) { script.writeToFile(notesDir + "/" + n.fileName, newText, false); repairedNotes++; } } - - script.informationMessageBox( - repairedLinks > 0 - ? "Repaired " + repairedLinks + " link(s) in " + repairedNotes + " note(s)." - : "All Zettelkasten links are up to date.", - "Zettelkasten" - ); + script.informationMessageBox(repairedLinks > 0 ? "Repaired " + repairedLinks + " link(s) in " + repairedNotes + " note(s)." : "All Zettelkasten links are up to date.", "Zettelkasten"); } // ── Actions ─────────────────────────────────────────────────────────────── - function insertZkId() { script.noteTextEditWrite(generateId()); } @@ -250,50 +219,47 @@ Script { function insertZkLink() { var noteIds = script.fetchNoteIdsByNoteTextPart(""); var entries = []; - for (var i = 0; i < noteIds.length; i++) { var note = script.fetchNoteById(noteIds[i]); - if (!note || !note.fileName) continue; + if (!note || !note.fileName) + continue; // Check filename first, then full content — first match only var zkId = extractId(note.fileName); - if (!zkId) zkId = extractId(note.noteText); - if (!zkId) continue; - + if (!zkId) + zkId = extractId(note.noteText); + if (!zkId) + continue; entries.push({ - label: zkId + " — " + note.name, - linkTarget: noteLinkTarget(note), - zkId: zkId - }); + "label": zkId + " — " + note.name, + "linkTarget": noteLinkTarget(note), + "zkId": zkId + }); } - if (entries.length === 0) { - script.informationMessageBox( - "No note with a Zettelkasten ID was found.\nPattern: " + (idRegex || "\\d{14}"), - "Zettelkasten" - ); + script.informationMessageBox("No note with a Zettelkasten ID was found.\nPattern: " + (idRegex || "\\d{14}"), "Zettelkasten"); return; } // Most recent first - entries.sort(function(a, b) { - return b.zkId > a.zkId ? 1 : b.zkId < a.zkId ? -1 : 0; - }); - + entries.sort(function (a, b) { + return b.zkId > a.zkId ? 1 : b.zkId < a.zkId ? -1 : 0; + }); var component = Qt.createComponent(Qt.resolvedUrl("ZkLinkDialog.qml")); if (component.status !== Component.Ready) { - script.informationMessageBox( - "Failed to load ZkLinkDialog:\n" + component.errorString(), "Zettelkasten"); + script.informationMessageBox("Failed to load ZkLinkDialog:\n" + component.errorString(), "Zettelkasten"); return; } - var dialog = component.createObject(null, { entries: entries }); + var dialog = component.createObject(null, { + "entries": entries + }); if (!dialog) { script.informationMessageBox("Failed to instantiate ZkLinkDialog.", "Zettelkasten"); return; } - dialog.linkSelected.connect(function(linkTarget, zkId) { - script.noteTextEditWrite("[[" + linkTarget + "|" + zkId + "]]"); - }); + dialog.linkSelected.connect(function (linkTarget, zkId) { + script.noteTextEditWrite("[[" + linkTarget + "|" + zkId + "]]"); + }); dialog.show(); dialog.raise(); dialog.requestActivate(); From 8778f8fe07c17f761703d2160954f48d671c8907 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 17:26:59 +0200 Subject: [PATCH 06/13] use prettier --- zettelkasten/README.md | 50 ++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/zettelkasten/README.md b/zettelkasten/README.md index fe53992..f233808 100644 --- a/zettelkasten/README.md +++ b/zettelkasten/README.md @@ -21,11 +21,11 @@ This script implements that principle inside QOwnNotes using the native `[[filen Three toolbar buttons are registered: -| Button | Action | -|--------|--------| -| **ZK-ID** | Insert a new unique ZK ID at the cursor position | +| Button | Action | +| ----------- | ---------------------------------------------------------------------------- | +| **ZK-ID** | Insert a new unique ZK ID at the cursor position | | **ZK-Link** | Open a searchable dialog to pick a note and insert a `[[filename\|id]]` link | -| **ZK-Fix** | Scan all notes and repair every link whose filename is out of date | +| **ZK-Fix** | Scan all notes and repair every link whose filename is out of date | ## ID format @@ -33,22 +33,22 @@ IDs are generated from the current date and time using a configurable format str Available tokens: -| Token | Value | -|-------|-------| -| `%Y` | 4-digit year | -| `%M` | 2-digit month | -| `%D` | 2-digit day | -| `%h` | 2-digit hour | -| `%m` | 2-digit minute | -| `%s` | 2-digit second | +| Token | Value | +| ----- | -------------- | +| `%Y` | 4-digit year | +| `%M` | 2-digit month | +| `%D` | 2-digit day | +| `%h` | 2-digit hour | +| `%m` | 2-digit minute | +| `%s` | 2-digit second | Examples: -| Format | Result | -|--------|--------| -| `%Y%M%D%h%m%s` *(default)* | `20260430143012` | -| `id%Y%M%Dx%h%m%s` | `id20260430x143012` | -| `%Y-%M-%D` | `2026-04-30` | +| Format | Result | +| -------------------------- | ------------------- | +| `%Y%M%D%h%m%s` _(default)_ | `20260430143012` | +| `id%Y%M%Dx%h%m%s` | `id20260430x143012` | +| `%Y-%M-%D` | `2026-04-30` | ## ID detection @@ -63,18 +63,16 @@ When you rename a note in QOwnNotes, any `[[oldName|id]]` links in other notes b - **Automatic** — when you open the renamed note, the script silently rewrites every backlink pointing to it with the new filename. This happens in the background with no interruption. - **Manual** — click **ZK-Fix** at any time to repair all stale links across the entire vault in one pass. -> **Note:** QOwnNotes may show a native dialog asking whether to replace link occurrences after a rename. That dialog does not understand the `[[filename|id]]` format and will not change anything. You can safely click *No* and let this script handle it, or disable the dialog entirely in *Settings → Notes*. +> **Note:** QOwnNotes may show a native dialog asking whether to replace link occurrences after a rename. That dialog does not understand the `[[filename|id]]` format and will not change anything. You can safely click _No_ and let this script handle it, or disable the dialog entirely in _Settings → Notes_. ## Settings -All settings are accessible in *Settings → Scripting → Zettelkasten*: - -| Setting | Default | Description | -|---------|---------|-------------| -| ID generation format | `%Y%M%D%h%m%s` | Format string for new IDs | -| ID detection pattern | `\d{14}` | ECMAScript regex to locate IDs in filenames and content | -| Auto-repair backlinks on note open | enabled | Automatically fix stale backlinks when a note with a ZK ID is opened | +All settings are accessible in _Settings → Scripting → Zettelkasten_: +| Setting | Default | Description | +| ---------------------------------- | -------------- | -------------------------------------------------------------------- | +| ID generation format | `%Y%M%D%h%m%s` | Format string for new IDs | +| ID detection pattern | `\d{14}` | ECMAScript regex to locate IDs in filenames and content | +| Auto-repair backlinks on note open | enabled | Automatically fix stale backlinks when a note with a ZK ID is opened | fix with qmlformat - From cab0bb308d0452096e4894fad519a2f96f90d48b Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 17:58:52 +0200 Subject: [PATCH 07/13] trying to fix formatting, one more time --- zettelkasten/zettelkasten.qml | 56 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml index 2657981..3f0e145 100644 --- a/zettelkasten/zettelkasten.qml +++ b/zettelkasten/zettelkasten.qml @@ -25,25 +25,29 @@ Script { // Runtime state — not a user setting property string notesDir: "" - property variant settingsVariables: [{ + property variant settingsVariables: [ + { "identifier": "idFormat", "name": "ID generation format", "description": "Format string for generating new IDs.\nTokens: %Y=year %M=month %D=day %h=hour %m=minute %s=second\nLiteral characters are kept as-is.\n\nExamples:\n %Y%M%D%h%m%s → 20260430143012\n id%Y%M%Dx%h%m%s → id20260430x143012\n %Y-%M-%D → 2026-04-30", "type": "string", "default": "%Y%M%D%h%m%s" - }, { + }, + { "identifier": "idRegex", "name": "ID detection pattern (ECMAScript regex)", "description": "Pattern used to detect Zettelkasten IDs in note filenames and content.\nDefault matches 14-digit timestamps: \\d{14}", "type": "string", "default": "\\d{14}" - }, { + }, + { "identifier": "autoRepairLinks", "name": "Auto-repair backlinks on note open", "description": "When a note with a ZK ID is opened, automatically rewrite any backlinks in other notes that still use an outdated filename for that ID.", "type": "boolean", "default": true - }] + } + ] function init() { script.registerCustomAction("zkInsertId", "Insert Zettelkasten ID", "ZK-ID", "", false, false, true); @@ -152,11 +156,11 @@ Script { continue; var changed = false; var newText = n.noteText.replace(pattern, function (match, oldTarget) { - if (oldTarget === currentTarget) - return match; - changed = true; - return "[[" + currentTarget + "|" + zkId + "]]"; - }); + if (oldTarget === currentTarget) + return match; + changed = true; + return "[[" + currentTarget + "|" + zkId + "]]"; + }); if (changed) { script.writeToFile(notesDir + "/" + n.fileName, newText, false); script.log("zettelkasten: repaired backlink in \"" + n.fileName + "\" → [[" + currentTarget + "|" + zkId + "]]"); @@ -196,13 +200,13 @@ Script { continue; var changed = false; var newText = n.noteText.replace(pattern, function (match, linkTarget, linkId) { - var correct = idMap[linkId]; - if (!correct || correct === linkTarget) - return match; - changed = true; - repairedLinks++; - return "[[" + correct + "|" + linkId + "]]"; - }); + var correct = idMap[linkId]; + if (!correct || correct === linkTarget) + return match; + changed = true; + repairedLinks++; + return "[[" + correct + "|" + linkId + "]]"; + }); if (changed) { script.writeToFile(notesDir + "/" + n.fileName, newText, false); repairedNotes++; @@ -231,10 +235,10 @@ Script { if (!zkId) continue; entries.push({ - "label": zkId + " — " + note.name, - "linkTarget": noteLinkTarget(note), - "zkId": zkId - }); + "label": zkId + " — " + note.name, + "linkTarget": noteLinkTarget(note), + "zkId": zkId + }); } if (entries.length === 0) { script.informationMessageBox("No note with a Zettelkasten ID was found.\nPattern: " + (idRegex || "\\d{14}"), "Zettelkasten"); @@ -243,23 +247,23 @@ Script { // Most recent first entries.sort(function (a, b) { - return b.zkId > a.zkId ? 1 : b.zkId < a.zkId ? -1 : 0; - }); + return b.zkId > a.zkId ? 1 : b.zkId < a.zkId ? -1 : 0; + }); var component = Qt.createComponent(Qt.resolvedUrl("ZkLinkDialog.qml")); if (component.status !== Component.Ready) { script.informationMessageBox("Failed to load ZkLinkDialog:\n" + component.errorString(), "Zettelkasten"); return; } var dialog = component.createObject(null, { - "entries": entries - }); + "entries": entries + }); if (!dialog) { script.informationMessageBox("Failed to instantiate ZkLinkDialog.", "Zettelkasten"); return; } dialog.linkSelected.connect(function (linkTarget, zkId) { - script.noteTextEditWrite("[[" + linkTarget + "|" + zkId + "]]"); - }); + script.noteTextEditWrite("[[" + linkTarget + "|" + zkId + "]]"); + }); dialog.show(); dialog.raise(); dialog.requestActivate(); From 4a0722b96209cba072ad495315ea9b336efe66e9 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 23:05:15 +0200 Subject: [PATCH 08/13] update info.json --- zettelkasten/info.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zettelkasten/info.json b/zettelkasten/info.json index c57e53d..e620ba9 100644 --- a/zettelkasten/info.json +++ b/zettelkasten/info.json @@ -6,5 +6,6 @@ "authors": ["@luginf"], "platforms": ["linux", "macos", "windows"], "minAppVersion": "26.4.11", - "description": "Zettelkasten support: generates 14-digit timestamp IDs and inserts [[filename|ID]] wiki-links to notes detected by a configurable ECMAScript ID pattern." + "description": "Zettelkasten support: generates 14-digit timestamp IDs and inserts [[filename|ID]] wiki-links to notes detected by a configurable ECMAScript ID pattern.", + "resources": ["ZkLinkDialog.qml"] } From 0f80d2805455845a77c03d962b00d4f36bbaefb0 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 23:05:29 +0200 Subject: [PATCH 09/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- zettelkasten/zettelkasten.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml index 3f0e145..c266bf5 100644 --- a/zettelkasten/zettelkasten.qml +++ b/zettelkasten/zettelkasten.qml @@ -95,6 +95,16 @@ Script { // Returns the link target string for [[target|id]] given a note object. function noteLinkTarget(note) { + var name = note && note.name ? note.name : ""; + if (name) { + var relativeDir = note.relativeNoteFileDirPath || ""; + if (relativeDir) { + relativeDir = relativeDir.replace(/[\/\\]+$/, ""); + return relativeDir ? relativeDir + "/" + name : name; + } + return name; + } + return /\.txt$/i.test(note.fileName) ? note.fileName.slice(0, note.fileName.length - 4) : note.fileName; } From 8ceaf1a601252ba5edc37cefa29d3700efa70f69 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 23:07:14 +0200 Subject: [PATCH 10/13] update readme --- zettelkasten/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/zettelkasten/README.md b/zettelkasten/README.md index f233808..d40a044 100644 --- a/zettelkasten/README.md +++ b/zettelkasten/README.md @@ -75,4 +75,3 @@ All settings are accessible in _Settings → Scripting → Zettelkasten_: | ID detection pattern | `\d{14}` | ECMAScript regex to locate IDs in filenames and content | | Auto-repair backlinks on note open | enabled | Automatically fix stale backlinks when a note with a ZK ID is opened | -fix with qmlformat From 9daa31c010c19367ec96869b58ea5da0f63e1e39 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 23:14:04 +0200 Subject: [PATCH 11/13] removing debug logs --- zettelkasten/zettelkasten.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml index c266bf5..9233ba0 100644 --- a/zettelkasten/zettelkasten.qml +++ b/zettelkasten/zettelkasten.qml @@ -120,7 +120,6 @@ Script { // Strategy 1: NoteFolder.localPath try { var folder = script.currentNoteFolder(); - script.log("zettelkasten: currentNoteFolder=" + JSON.stringify(folder) + " localPath=" + (folder ? folder.localPath : "n/a")); if (folder && folder.localPath) { notesDir = folder.localPath; return; @@ -132,7 +131,6 @@ Script { // Strategy 2: current note's full file path try { var note = script.currentNote(); - script.log("zettelkasten: currentNote.fullNoteFilePath=" + (note ? note.fullNoteFilePath : "n/a")); if (note && note.fullNoteFilePath) { notesDir = note.fullNoteFilePath.replace(/[\/\\][^\/\\]+$/, ""); return; From 75a45e278556346a1432e2f84fd2b300148ed58c Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 23:23:33 +0200 Subject: [PATCH 12/13] fixed script.writeToFile(...) is being called with 3 arguments --- zettelkasten/info.json | 2 +- zettelkasten/zettelkasten.qml | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/zettelkasten/info.json b/zettelkasten/info.json index e620ba9..23078b3 100644 --- a/zettelkasten/info.json +++ b/zettelkasten/info.json @@ -6,6 +6,6 @@ "authors": ["@luginf"], "platforms": ["linux", "macos", "windows"], "minAppVersion": "26.4.11", - "description": "Zettelkasten support: generates 14-digit timestamp IDs and inserts [[filename|ID]] wiki-links to notes detected by a configurable ECMAScript ID pattern.", + "description": "Zettelkasten support: generates configurable note IDs and inserts [[filename|ID]] wiki-links to notes detected by a configurable ECMAScript ID pattern.", "resources": ["ZkLinkDialog.qml"] } diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml index 9233ba0..0371841 100644 --- a/zettelkasten/zettelkasten.qml +++ b/zettelkasten/zettelkasten.qml @@ -85,7 +85,9 @@ Script { function extractId(text) { try { var re = new RegExp(idRegex || "\\d{14}"); - var m = text.match(re); + // Strip [[target|id]] links before matching so a linked ID is never + // mistaken for the note's own ID (which would corrupt idMap). + var m = text.replace(/\[\[[^\]|]*\|[^\]]*\]\]/g, "").match(re); return m ? m[0] : null; } catch (e) { script.log("zettelkasten: invalid ID regex — " + e); @@ -170,7 +172,7 @@ Script { return "[[" + currentTarget + "|" + zkId + "]]"; }); if (changed) { - script.writeToFile(notesDir + "/" + n.fileName, newText, false); + script.writeToFile(notesDir + "/" + n.fileName, newText); script.log("zettelkasten: repaired backlink in \"" + n.fileName + "\" → [[" + currentTarget + "|" + zkId + "]]"); } } @@ -216,7 +218,7 @@ Script { return "[[" + correct + "|" + linkId + "]]"; }); if (changed) { - script.writeToFile(notesDir + "/" + n.fileName, newText, false); + script.writeToFile(notesDir + "/" + n.fileName, newText); repairedNotes++; } } From 86bc1ddb91c45e79ae76fa6f896b9c823972f8b5 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 23:27:26 +0200 Subject: [PATCH 13/13] fixed last remaining copilot insights --- zettelkasten/zettelkasten.qml | 48 ++++------------------------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/zettelkasten/zettelkasten.qml b/zettelkasten/zettelkasten.qml index 0371841..557671d 100644 --- a/zettelkasten/zettelkasten.qml +++ b/zettelkasten/zettelkasten.qml @@ -22,9 +22,6 @@ Script { property string idFormat property bool autoRepairLinks: true - // Runtime state — not a user setting - property string notesDir: "" - property variant settingsVariables: [ { "identifier": "idFormat", @@ -66,7 +63,6 @@ Script { } function noteOpenedHook(note) { - resolveNotesDir(); if (autoRepairLinks !== false) { repairBacklinksFor(note); } @@ -114,41 +110,12 @@ Script { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } - // Populate notesDir — tries several API approaches. - function resolveNotesDir() { - if (notesDir) - return; - - // Strategy 1: NoteFolder.localPath - try { - var folder = script.currentNoteFolder(); - if (folder && folder.localPath) { - notesDir = folder.localPath; - return; - } - } catch (e) { - script.log("zettelkasten: currentNoteFolder() error: " + e); - } - - // Strategy 2: current note's full file path - try { - var note = script.currentNote(); - if (note && note.fullNoteFilePath) { - notesDir = note.fullNoteFilePath.replace(/[\/\\][^\/\\]+$/, ""); - return; - } - } catch (e) { - script.log("zettelkasten: currentNote() error: " + e); - } - script.log("zettelkasten: could not resolve notes directory"); - } - // ── Backlink repair ─────────────────────────────────────────────────────── // When a note is opened, find all notes that link to its ZK ID with a // stale filename and rewrite those links to use the current filename. function repairBacklinksFor(note) { - if (!note || !note.fileName || !notesDir) + if (!note || !note.fileName) return; var zkId = extractId(note.fileName); if (!zkId) @@ -162,7 +129,7 @@ Script { var pattern = new RegExp("\\[\\[([^\\]|]*)\\|" + regEscape(zkId) + "\\]\\]", "g"); for (var i = 0; i < candidates.length; i++) { var n = script.fetchNoteById(candidates[i]); - if (!n || !n.noteText) + if (!n || !n.noteText || !n.fullNoteFilePath) continue; var changed = false; var newText = n.noteText.replace(pattern, function (match, oldTarget) { @@ -172,7 +139,7 @@ Script { return "[[" + currentTarget + "|" + zkId + "]]"; }); if (changed) { - script.writeToFile(notesDir + "/" + n.fileName, newText); + script.writeToFile(n.fullNoteFilePath, newText); script.log("zettelkasten: repaired backlink in \"" + n.fileName + "\" → [[" + currentTarget + "|" + zkId + "]]"); } } @@ -181,11 +148,6 @@ Script { // Full vault scan: build an id→currentTarget map, then rewrite every // [[staleTarget|id]] in every note. function repairAllLinks() { - resolveNotesDir(); - if (!notesDir) { - script.informationMessageBox("Notes directory not available.\nPlease open a note first.", "Zettelkasten"); - return; - } var allIds = script.fetchNoteIdsByNoteTextPart(""); // Build zkId → correct link target @@ -206,7 +168,7 @@ Script { var repairedNotes = 0; for (var j = 0; j < allIds.length; j++) { var n = script.fetchNoteById(allIds[j]); - if (!n || !n.noteText) + if (!n || !n.noteText || !n.fullNoteFilePath) continue; var changed = false; var newText = n.noteText.replace(pattern, function (match, linkTarget, linkId) { @@ -218,7 +180,7 @@ Script { return "[[" + correct + "|" + linkId + "]]"; }); if (changed) { - script.writeToFile(notesDir + "/" + n.fileName, newText); + script.writeToFile(n.fullNoteFilePath, newText); repairedNotes++; } }