-
Notifications
You must be signed in to change notification settings - Fork 99
Zettelkasten support #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Zettelkasten support #286
Changes from 7 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
fc1c1c5
initial commit
db3154f
repair ztl
10f5df5
adding readme + fix
2009bbf
updating info.json
098bb9a
fix format with qmlformat
8778f8f
use prettier
cab0bb3
trying to fix formatting, one more time
4a0722b
update info.json
0f80d28
Potential fix for pull request finding
luginf 8ceaf1a
update readme
9ee3de2
Merge branch 'zettelkasten' of github.com:luginf/qownnotes-scripts in…
9daa31c
removing debug logs
75a45e2
fixed script.writeToFile(...) is being called with 3 arguments
86bc1dd
fixed last remaining copilot insights
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # 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 | | ||
|
|
||
| fix with qmlformat | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,266 @@ | ||
| 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(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "name": "Zettelkasten", | ||
| "identifier": "zettelkasten", | ||
| "version": "0.2.0", | ||
| "script": "zettelkasten.qml", | ||
| "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." | ||
|
pbek marked this conversation as resolved.
Outdated
|
||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.