From f7f5760caa079dbec536c45d82e439041faeee35 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 18:06:35 +0200 Subject: [PATCH 1/5] first commit --- snippets/ManageSnippetsDialog.qml | 292 ++++++++++++++++++++++++++++++ snippets/README.md | 66 +++++++ snippets/info.json | 10 + snippets/snippets.json | 14 ++ snippets/snippets.qml | 164 +++++++++++++++++ 5 files changed, 546 insertions(+) create mode 100644 snippets/ManageSnippetsDialog.qml create mode 100644 snippets/README.md create mode 100644 snippets/info.json create mode 100644 snippets/snippets.json create mode 100644 snippets/snippets.qml diff --git a/snippets/ManageSnippetsDialog.qml b/snippets/ManageSnippetsDialog.qml new file mode 100644 index 0000000..010c41a --- /dev/null +++ b/snippets/ManageSnippetsDialog.qml @@ -0,0 +1,292 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.0 + +Window { + id: root + title: "Manage Snippets" + width: 720 + height: 480 + minimumWidth: 520 + minimumHeight: 320 + modality: Qt.ApplicationModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint + + // Input / output + property var snippets: [] + signal snippetsSaved(var updatedSnippets) + + // Internal state + property var items: [] + property bool updating: false + property bool isDirty: false + + SystemPalette { id: palette } + + Window { + id: helpWindow + title: "Snippets — Placeholders" + width: 640 + height: 460 + minimumWidth: 500 + minimumHeight: 360 + flags: Qt.Dialog | Qt.WindowCloseButtonHint + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + TextArea { + Layout.fillWidth: true + readOnly: true + selectByMouse: true + background: null + font.family: "monospace" + font.pointSize: Qt.application.font.pointSize - 1 + text: + "Date & time\n" + + " $CURRENT_YEAR four-digit year 2026\n" + + " $CURRENT_YEAR_SHORT two-digit year 26\n" + + " $CURRENT_MONTH two-digit month 04\n" + + " $CURRENT_MONTH_NAME full month name April\n" + + " $CURRENT_MONTH_NAME_SHORT short month name Apr\n" + + " $CURRENT_DATE two-digit day 29\n" + + " $CURRENT_HOUR hour 00–23 14\n" + + " $CURRENT_MINUTE two-digit minute 07\n" + + " $CURRENT_SECOND two-digit second 03\n" + + " $CURRENT_SECONDS_UNIX Unix timestamp 1745920023\n\n" + + "Identifiers\n" + + " $UUID UUID v4 550e8400-e29b-41d4-a716-446655440000\n\n" + + "Note context\n" + + " $NOTE_TITLE current note title My note\n" + + " $NOTE_FILENAME current note filename my-note.md\n\n" + + "System\n" + + " $OS_NAME operating system Linux / macOS / Windows\n\n" + + "Zettelkasten (format configurable in settings)\n" + + " $ZK_ID Zettelkasten ID 20260430143012" + } + + Item { Layout.fillHeight: true } + + RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + Button { + text: "Close" + onClicked: helpWindow.close() + } + } + } + } + + Component.onCompleted: { + items = JSON.parse(JSON.stringify(snippets)); + } + + // ── Layout ─────────────────────────────────────────────────────────────── + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 10 + + // ── Left panel: list + +/- buttons ─────────────────────────────── + ColumnLayout { + Layout.preferredWidth: 200 + Layout.minimumWidth: 140 + Layout.fillHeight: true + spacing: 4 + + RowLayout { + spacing: 4 + + Button { + text: "+" + implicitWidth: 32 + implicitHeight: 28 + onClicked: { + var copy = items.slice(); + copy.push({ name: "New snippet", content: "" }); + items = copy; + var idx = items.length - 1; + snippetList.currentIndex = idx; + loadItem(idx); + nameField.selectAll(); + nameField.forceActiveFocus(); + } + } + + Button { + text: "−" + implicitWidth: 32 + implicitHeight: 28 + enabled: snippetList.currentIndex >= 0 + onClicked: { + var idx = snippetList.currentIndex; + var copy = items.slice(); + copy.splice(idx, 1); + items = copy; + snippetsSaved(items); + var next = Math.min(idx, items.length - 1); + snippetList.currentIndex = next; + if (next >= 0) loadItem(next); + else clearEditor(); + } + } + + Item { Layout.fillWidth: true } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + border.color: palette.mid + border.width: 1 + radius: 3 + clip: true + + ListView { + id: snippetList + anchors.fill: parent + anchors.margins: 1 + model: items + currentIndex: -1 + clip: true + + delegate: ItemDelegate { + width: snippetList.width + text: modelData.name + highlighted: ListView.isCurrentItem + onClicked: { + snippetList.currentIndex = index; + loadItem(index); + } + } + + ScrollBar.vertical: ScrollBar {} + } + } + } + + // ── Right panel: editor ────────────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 6 + enabled: snippetList.currentIndex >= 0 + + RowLayout { + spacing: 6 + Label { text: "Name:" } + TextField { + id: nameField + Layout.fillWidth: true + placeholderText: "Snippet name" + selectByMouse: true + onTextChanged: if (!updating) isDirty = true + } + } + + Label { + text: "Content" + color: palette.dark + font.pointSize: Qt.application.font.pointSize - 1 + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + border.color: contentArea.activeFocus ? palette.highlight : palette.mid + border.width: 1 + radius: 3 + color: palette.base + clip: true + + Flickable { + id: contentFlick + anchors.fill: parent + anchors.margins: 1 + contentWidth: width + contentHeight: contentArea.implicitHeight + flickableDirection: Flickable.VerticalFlick + clip: true + + TextArea { + id: contentArea + width: contentFlick.width + height: Math.max(contentFlick.height, implicitHeight) + wrapMode: Text.Wrap + selectByMouse: true + placeholderText: "Snippet content…" + background: null + onTextChanged: if (!updating) isDirty = true + } + + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + } + } + + RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + Button { + text: "Save" + enabled: isDirty && snippetList.currentIndex >= 0 + onClicked: saveCurrentItem() + } + } + } + } + + // ── Bottom bar ─────────────────────────────────────────────────────── + RowLayout { + Layout.fillWidth: true + Button { + text: "Placeholders…" + onClicked: helpWindow.show() + } + Item { Layout.fillWidth: true } + Button { + text: "Close" + onClicked: root.close() + } + } + } + + // ── Functions ───────────────────────────────────────────────────────────── + + function loadItem(idx) { + if (idx < 0 || idx >= items.length) return; + updating = true; + nameField.text = items[idx].name; + contentArea.text = items[idx].content; + updating = false; + isDirty = false; + } + + function clearEditor() { + updating = true; + nameField.text = ""; + contentArea.text = ""; + updating = false; + isDirty = false; + } + + function saveCurrentItem() { + var idx = snippetList.currentIndex; + if (idx < 0) return; + var copy = items.slice(); + copy[idx] = { name: nameField.text, content: contentArea.text }; + items = copy; + snippetList.currentIndex = idx; + isDirty = false; + snippetsSaved(items); + } +} diff --git a/snippets/README.md b/snippets/README.md new file mode 100644 index 0000000..5098392 --- /dev/null +++ b/snippets/README.md @@ -0,0 +1,66 @@ +# Text Snippets + +Insert reusable text snippets into your notes via a selection dialog. + +## Usage + +- **Scripting › Insert snippet** — choose a snippet from the list and insert it at the cursor +- **Scripting › Manage snippets** — add, edit, or delete snippets + +## Placeholders + +Placeholders are replaced at insertion time. + +### Date & time + +| Placeholder | Example output | +|---|---| +| `$CURRENT_YEAR` | `2026` | +| `$CURRENT_YEAR_SHORT` | `26` | +| `$CURRENT_MONTH` | `04` | +| `$CURRENT_MONTH_NAME` | `April` (locale système) | +| `$CURRENT_MONTH_NAME_SHORT` | `Apr` (locale système) | +| `$CURRENT_DATE` | `29` | +| `$CURRENT_HOUR` | `14` | +| `$CURRENT_MINUTE` | `07` | +| `$CURRENT_SECOND` | `03` | +| `$CURRENT_SECONDS_UNIX` | `1745920023` | + +### Identifiants + +| Placeholder | Exemple | +|---|---| +| `$UUID` | `550e8400-e29b-41d4-a716-446655440000` | +| `$ZK_ID` | `20260430143012` (format configurable) | + +### Note context + +| Placeholder | Example output | +|---|---| +| `$NOTE_TITLE` | `My note` | +| `$NOTE_FILENAME` | `my-note.md` | + +## Zettelkasten integration + +`$ZK_ID` generates a Zettelkasten ID using the same format string syntax as the +[Zettelkasten extension](../zettelkasten/). Configure **Zettelkasten ID format** +in _Settings → Scripting → Text Snippets_ to match the format you use there. + +| 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 | + +## Exemple + +``` +## $CURRENT_MONTH_NAME $CURRENT_DATE, $CURRENT_YEAR + +$ZK_ID + +#tag +``` diff --git a/snippets/info.json b/snippets/info.json new file mode 100644 index 0000000..45e39a6 --- /dev/null +++ b/snippets/info.json @@ -0,0 +1,10 @@ +{ + "name": "Text Snippets", + "identifier": "snippets", + "version": "0.1.0", + "script": "snippets.qml", + "authors": ["@luginf"], + "platforms": ["linux", "macos", "windows"], + "minAppVersion": "26.4.11", + "description": "Insert and manage reusable text snippets with placeholders ($CURRENT_YEAR, $CURRENT_DATE, $UUID…) into notes." +} diff --git a/snippets/snippets.json b/snippets/snippets.json new file mode 100644 index 0000000..ccab6ef --- /dev/null +++ b/snippets/snippets.json @@ -0,0 +1,14 @@ +[ + { + "name": "Reference", + "content": "## Reference\n\nid$CURRENT_YEAR$CURRENT_MONTH$CURRENT_DATEx$CURRENT_HOUR$CURRENT_MINUTE$CURRENT_SECOND\n\n\nSee also:\n-\n-\n\n#tag" + }, + { + "name": "test", + "content": "just a test" + }, + { + "name": "Zettelkasten_id", + "content": "id$CURRENT_YEAR$CURRENT_MONTH$CURRENT_DATEx$CURRENT_HOUR$CURRENT_MINUTE$CURRENT_SECOND" + } +] \ No newline at end of file diff --git a/snippets/snippets.qml b/snippets/snippets.qml new file mode 100644 index 0000000..dc63cb9 --- /dev/null +++ b/snippets/snippets.qml @@ -0,0 +1,164 @@ +// Text Snippets — insert reusable text snippets with placeholders into notes. +// +// Placeholders (date & time): +// $CURRENT_YEAR four-digit year 2026 +// $CURRENT_YEAR_SHORT two-digit year 26 +// $CURRENT_MONTH two-digit month 04 +// $CURRENT_MONTH_NAME full month name April (system locale) +// $CURRENT_MONTH_NAME_SHORT short month name Apr (system locale) +// $CURRENT_DATE two-digit day 29 +// $CURRENT_HOUR hour 00–23 14 +// $CURRENT_MINUTE two-digit minute 07 +// $CURRENT_SECOND two-digit second 03 +// $CURRENT_SECONDS_UNIX Unix timestamp 1745920023 +// +// Placeholders (identifiers): +// $UUID UUID v4 550e8400-e29b-41d4-a716-446655440000 +// +// Placeholders (note context): +// $NOTE_TITLE current note title My note +// $NOTE_FILENAME current note filename my-note.md +// +// Placeholders (system): +// $OS_NAME operating system name Linux + +import QtQml 2.0 +import QOwnNotesTypes 1.0 + +Script { + property string scriptDirPath + property string zkIdFormat + + property variant settingsVariables: [ + { + "identifier": "zkIdFormat", + "name": "Zettelkasten ID format", + "description": "Format string used by the $ZK_ID placeholder. Uses the same tokens as the Zettelkasten extension — set both to the same value to keep IDs consistent.\nTokens: %Y=year %M=month %D=day %h=hour %m=minute %s=second\n\nExamples:\n %Y%M%D%h%m%s → 20260430143012\n id%Y%M%Dx%h%m%s → id20260430x143012", + "type": "string", + "default": "%Y%M%D%h%m%s" + } + ] + + function init() { + script.registerCustomAction("insertSnippet", "Insert snippet", "Snippet", "", false, false, false); + script.registerCustomAction("manageSnippets", "Manage snippets", "", "", false, true, false); + } + + function customActionInvoked(identifier) { + if (identifier === "insertSnippet") { + insertSnippet(); + } else if (identifier === "manageSnippets") { + manageSnippets(); + } + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + function pad(n) { + return n < 10 ? "0" + n : String(n); + } + + function generateZkId() { + var fmt = (zkIdFormat || "").trim() || "%Y%M%D%h%m%s"; + var now = new Date(); + return fmt + .replace(/%Y/g, String(now.getFullYear())) + .replace(/%M/g, pad(now.getMonth() + 1)) + .replace(/%D/g, pad(now.getDate())) + .replace(/%h/g, pad(now.getHours())) + .replace(/%m/g, pad(now.getMinutes())) + .replace(/%s/g, pad(now.getSeconds())); + } + + function generateUUID() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0; + return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + + function processPlaceholders(text) { + var now = new Date(); + var loc = Qt.locale(); + var note = script.currentNote(); + var osMap = { "linux": "Linux", "osx": "macOS", "windows": "Windows", "unix": "Unix" }; + return text + .replace(/\$CURRENT_YEAR_SHORT/g, String(now.getFullYear()).slice(-2)) + .replace(/\$CURRENT_YEAR/g, String(now.getFullYear())) + .replace(/\$CURRENT_MONTH_NAME_SHORT/g, loc.monthName(now.getMonth(), 1)) + .replace(/\$CURRENT_MONTH_NAME/g, loc.monthName(now.getMonth(), 0)) + .replace(/\$CURRENT_MONTH/g, pad(now.getMonth() + 1)) + .replace(/\$CURRENT_DATE/g, pad(now.getDate())) + .replace(/\$CURRENT_HOUR/g, pad(now.getHours())) + .replace(/\$CURRENT_MINUTE/g, pad(now.getMinutes())) + .replace(/\$CURRENT_SECOND/g, pad(now.getSeconds())) + .replace(/\$CURRENT_SECONDS_UNIX/g, String(Math.floor(now.getTime() / 1000))) + .replace(/\$UUID/g, generateUUID()) + .replace(/\$NOTE_TITLE/g, note ? note.name : "") + .replace(/\$NOTE_FILENAME/g, note ? note.fileName : "") + .replace(/\$OS_NAME/g, osMap[Qt.platform.os] || Qt.platform.os) + .replace(/\$ZK_ID/g, generateZkId()); + } + + function snippetsFilePath() { + return scriptDirPath + "/snippets.json"; + } + + function loadSnippets() { + var path = snippetsFilePath(); + if (!script.fileExists(path)) return []; + try { + return JSON.parse(script.readFromFile(path, "UTF-8")); + } catch (e) { + script.log("snippets: JSON read error — " + e); + return []; + } + } + + function saveSnippets(snippets) { + script.writeToFile(snippetsFilePath(), JSON.stringify(snippets, null, 2), false); + } + + // ── Actions ─────────────────────────────────────────────────────────────── + + function insertSnippet() { + var snippets = loadSnippets(); + if (snippets.length === 0) { + script.informationMessageBox( + "No snippets defined yet.\nUse Scripting › Manage snippets to create one.", + "Snippets" + ); + return; + } + + var names = []; + for (var i = 0; i < snippets.length; i++) names.push(snippets[i].name); + + var selected = script.inputDialogGetItem("Insert snippet", "Snippet:", names, 0, false); + if (!selected) return; + + for (var j = 0; j < snippets.length; j++) { + if (snippets[j].name === selected) { + script.noteTextEditWrite(processPlaceholders(snippets[j].content)); + return; + } + } + } + + function manageSnippets() { + var component = Qt.createComponent(Qt.resolvedUrl("ManageSnippetsDialog.qml")); + if (component.status !== Component.Ready) { + script.log("snippets: failed to load dialog — " + component.errorString()); + return; + } + var dialog = component.createObject(null, { snippets: loadSnippets() }); + if (!dialog) { + script.log("snippets: failed to instantiate dialog"); + return; + } + dialog.snippetsSaved.connect(function(updated) { + saveSnippets(updated); + }); + dialog.show(); + } +} From 629c9ad0344870c7c9757ea26dd1ce384ef2e8fa Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 21:11:02 +0200 Subject: [PATCH 2/5] linter --- snippets/ManageSnippetsDialog.qml | 659 ++++++++++++++++++++---------- snippets/README.md | 56 +-- snippets/snippets.json | 10 +- snippets/snippets.qml | 39 +- 4 files changed, 506 insertions(+), 258 deletions(-) mode change 100644 => 100755 snippets/snippets.json diff --git a/snippets/ManageSnippetsDialog.qml b/snippets/ManageSnippetsDialog.qml index 010c41a..f23a218 100644 --- a/snippets/ManageSnippetsDialog.qml +++ b/snippets/ManageSnippetsDialog.qml @@ -1,6 +1,4 @@ import QtQuick 2.0 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 import QtQuick.Window 2.0 Window { @@ -13,277 +11,508 @@ Window { modality: Qt.ApplicationModal flags: Qt.Dialog | Qt.WindowCloseButtonHint - // Input / output property var snippets: [] signal snippetsSaved(var updatedSnippets) - // Internal state property var items: [] property bool updating: false property bool isDirty: false + property bool showHelp: false - SystemPalette { id: palette } + readonly property int baseWidth: 720 + readonly property int helpPanelWidth: 320 - Window { - id: helpWindow - title: "Snippets — Placeholders" - width: 640 - height: 460 - minimumWidth: 500 - minimumHeight: 360 - flags: Qt.Dialog | Qt.WindowCloseButtonHint + SystemPalette { + id: pal + } - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 12 + Component.onCompleted: { + items = JSON.parse(JSON.stringify(snippets)); + } - TextArea { - Layout.fillWidth: true - readOnly: true - selectByMouse: true - background: null - font.family: "monospace" - font.pointSize: Qt.application.font.pointSize - 1 - text: - "Date & time\n" + - " $CURRENT_YEAR four-digit year 2026\n" + - " $CURRENT_YEAR_SHORT two-digit year 26\n" + - " $CURRENT_MONTH two-digit month 04\n" + - " $CURRENT_MONTH_NAME full month name April\n" + - " $CURRENT_MONTH_NAME_SHORT short month name Apr\n" + - " $CURRENT_DATE two-digit day 29\n" + - " $CURRENT_HOUR hour 00–23 14\n" + - " $CURRENT_MINUTE two-digit minute 07\n" + - " $CURRENT_SECOND two-digit second 03\n" + - " $CURRENT_SECONDS_UNIX Unix timestamp 1745920023\n\n" + - "Identifiers\n" + - " $UUID UUID v4 550e8400-e29b-41d4-a716-446655440000\n\n" + - "Note context\n" + - " $NOTE_TITLE current note title My note\n" + - " $NOTE_FILENAME current note filename my-note.md\n\n" + - "System\n" + - " $OS_NAME operating system Linux / macOS / Windows\n\n" + - "Zettelkasten (format configurable in settings)\n" + - " $ZK_ID Zettelkasten ID 20260430143012" - } + // ── Bottom bar ──────────────────────────────────────────────────────────── + Item { + id: bottomBar + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + margins: 10 + } + height: 34 - Item { Layout.fillHeight: true } + Rectangle { + id: placeholdersBtn + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + } + width: 110 + height: 26 + radius: 4 + color: phMouse.pressed ? pal.dark : pal.button + border.color: pal.mid + border.width: 1 + + Text { + anchors.centerIn: parent + text: showHelp ? "Hide" : "Placeholders…" + color: pal.buttonText + font.pixelSize: 13 + } - RowLayout { - Layout.fillWidth: true - Item { Layout.fillWidth: true } - Button { - text: "Close" - onClicked: helpWindow.close() + MouseArea { + id: phMouse + anchors.fill: parent + onClicked: { + showHelp = !showHelp; + root.width = showHelp ? baseWidth + helpPanelWidth + 16 : baseWidth; } } } - } - Component.onCompleted: { - items = JSON.parse(JSON.stringify(snippets)); + Rectangle { + id: closeBtn + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + width: 76 + height: 26 + radius: 4 + color: closeMouse.pressed ? pal.dark : pal.button + border.color: pal.mid + border.width: 1 + + Text { + anchors.centerIn: parent + text: "Close" + color: pal.buttonText + font.pixelSize: 13 + } + + MouseArea { + id: closeMouse + anchors.fill: parent + onClicked: root.close() + } + } } - // ── Layout ─────────────────────────────────────────────────────────────── - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - spacing: 8 - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 10 - - // ── Left panel: list + +/- buttons ─────────────────────────────── - ColumnLayout { - Layout.preferredWidth: 200 - Layout.minimumWidth: 140 - Layout.fillHeight: true - spacing: 4 - - RowLayout { - spacing: 4 - - Button { - text: "+" - implicitWidth: 32 - implicitHeight: 28 - onClicked: { - var copy = items.slice(); - copy.push({ name: "New snippet", content: "" }); - items = copy; - var idx = items.length - 1; - snippetList.currentIndex = idx; - loadItem(idx); - nameField.selectAll(); - nameField.forceActiveFocus(); - } - } + // ── Left panel: list ────────────────────────────────────────────────────── - Button { - text: "−" - implicitWidth: 32 - implicitHeight: 28 - enabled: snippetList.currentIndex >= 0 - onClicked: { - var idx = snippetList.currentIndex; - var copy = items.slice(); - copy.splice(idx, 1); - items = copy; - snippetsSaved(items); - var next = Math.min(idx, items.length - 1); - snippetList.currentIndex = next; - if (next >= 0) loadItem(next); - else clearEditor(); - } - } + // +/- buttons + Item { + id: listButtons + anchors { + top: parent.top + topMargin: 10 + left: parent.left + leftMargin: 10 + } + width: 210 + height: 30 + + Rectangle { + id: addBtn + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + } + width: 32 + height: 26 + radius: 4 + color: addMouse.pressed ? pal.dark : pal.button + border.color: pal.mid + border.width: 1 + + Text { + anchors.centerIn: parent + text: "+" + color: pal.buttonText + font.pixelSize: 16 + } - Item { Layout.fillWidth: true } + MouseArea { + id: addMouse + anchors.fill: parent + onClicked: { + var copy = items.slice(); + copy.push({ + "name": "New snippet", + "content": "" + }); + items = copy; + var idx = items.length - 1; + snippetList.currentIndex = idx; + loadItem(idx); + nameInput.selectAll(); + nameInput.forceActiveFocus(); } + } + } - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - border.color: palette.mid - border.width: 1 - radius: 3 - clip: true - - ListView { - id: snippetList - anchors.fill: parent - anchors.margins: 1 - model: items - currentIndex: -1 - clip: true - - delegate: ItemDelegate { - width: snippetList.width - text: modelData.name - highlighted: ListView.isCurrentItem - onClicked: { - snippetList.currentIndex = index; - loadItem(index); - } - } - - ScrollBar.vertical: ScrollBar {} - } - } + Rectangle { + id: removeBtn + anchors { + verticalCenter: parent.verticalCenter + left: addBtn.right + leftMargin: 4 + } + width: 32 + height: 26 + radius: 4 + opacity: snippetList.currentIndex >= 0 ? 1.0 : 0.4 + color: removeMouse.pressed ? pal.dark : pal.button + border.color: pal.mid + border.width: 1 + + Text { + anchors.centerIn: parent + text: "−" + color: pal.buttonText + font.pixelSize: 16 } - // ── Right panel: editor ────────────────────────────────────────── - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: 6 + MouseArea { + id: removeMouse + anchors.fill: parent enabled: snippetList.currentIndex >= 0 - - RowLayout { - spacing: 6 - Label { text: "Name:" } - TextField { - id: nameField - Layout.fillWidth: true - placeholderText: "Snippet name" - selectByMouse: true - onTextChanged: if (!updating) isDirty = true - } + onClicked: { + var idx = snippetList.currentIndex; + var copy = items.slice(); + copy.splice(idx, 1); + items = copy; + snippetsSaved(items); + var next = Math.min(idx, items.length - 1); + snippetList.currentIndex = next; + if (next >= 0) + loadItem(next); + else + clearEditor(); } + } + } + } - Label { - text: "Content" - color: palette.dark - font.pointSize: Qt.application.font.pointSize - 1 - } + Rectangle { + id: listPanel + anchors { + top: listButtons.bottom + topMargin: 4 + left: parent.left + leftMargin: 10 + bottom: bottomBar.top + bottomMargin: 6 + } + width: 210 + radius: 3 + color: pal.base + border.color: pal.mid + border.width: 1 + clip: true + + ListView { + id: snippetList + anchors { + fill: parent + margins: 1 + rightMargin: listScroll.visible ? 8 : 1 + } + model: items + currentIndex: -1 + clip: true + boundsBehavior: Flickable.StopAtBounds + + delegate: Item { + width: snippetList.width + height: 28 Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - border.color: contentArea.activeFocus ? palette.highlight : palette.mid - border.width: 1 - radius: 3 - color: palette.base - clip: true - - Flickable { - id: contentFlick - anchors.fill: parent - anchors.margins: 1 - contentWidth: width - contentHeight: contentArea.implicitHeight - flickableDirection: Flickable.VerticalFlick - clip: true - - TextArea { - id: contentArea - width: contentFlick.width - height: Math.max(contentFlick.height, implicitHeight) - wrapMode: Text.Wrap - selectByMouse: true - placeholderText: "Snippet content…" - background: null - onTextChanged: if (!updating) isDirty = true - } - - ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + anchors.fill: parent + color: index === snippetList.currentIndex ? "#1cb27e" : (rowMouse.containsMouse ? "#e4f5ef" : "transparent") + } + + Text { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + margins: 8 } + text: modelData.name + color: index === snippetList.currentIndex ? "white" : pal.text + font.pixelSize: 13 + elide: Text.ElideRight } - RowLayout { - Layout.fillWidth: true - Item { Layout.fillWidth: true } - Button { - text: "Save" - enabled: isDirty && snippetList.currentIndex >= 0 - onClicked: saveCurrentItem() + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + onClicked: { + snippetList.currentIndex = index; + loadItem(index); } } } } - // ── Bottom bar ─────────────────────────────────────────────────────── - RowLayout { - Layout.fillWidth: true - Button { - text: "Placeholders…" - onClicked: helpWindow.show() + Rectangle { + id: listScroll + visible: snippetList.contentHeight > snippetList.height + width: 5 + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 1 } - Item { Layout.fillWidth: true } - Button { - text: "Close" - onClicked: root.close() + color: "transparent" + + Rectangle { + width: parent.width + radius: 2 + color: pal.mid + height: Math.max(24, snippetList.height * snippetList.height / Math.max(snippetList.contentHeight, 1)) + y: snippetList.height > 0 ? snippetList.contentY / Math.max(snippetList.contentHeight - snippetList.height, 1) * (snippetList.height - height) : 0 } } } - // ── Functions ───────────────────────────────────────────────────────────── + // ── Right panel: editor ─────────────────────────────────────────────────── + property bool editorEnabled: snippetList.currentIndex >= 0 + + // Name row + Text { + id: nameLabel + anchors { + verticalCenter: listButtons.verticalCenter + left: listPanel.right + leftMargin: 16 + } + text: "Name:" + color: editorEnabled ? pal.text : pal.mid + font.pixelSize: 13 + } + + Rectangle { + id: nameBox + anchors { + verticalCenter: listButtons.verticalCenter + left: nameLabel.right + leftMargin: 6 + right: showHelp ? helpSep.left : parent.right + rightMargin: 10 + } + height: 28 + radius: 3 + color: pal.base + border.color: nameInput.activeFocus ? "#1cb27e" : pal.mid + border.width: 1 + opacity: editorEnabled ? 1.0 : 0.5 + + TextInput { + id: nameInput + anchors { + fill: parent + margins: 6 + } + verticalAlignment: TextInput.AlignVCenter + font.pixelSize: 13 + color: pal.text + clip: true + enabled: editorEnabled + onTextChanged: { + if (!updating) + isDirty = true; + } + } + } + + // Content label + Text { + id: contentLabel + anchors { + top: nameBox.bottom + topMargin: 8 + left: listPanel.right + leftMargin: 16 + } + text: "Content" + color: editorEnabled ? pal.text : pal.mid + font.pixelSize: 13 + } + + // Save button + Rectangle { + id: saveBtn + anchors { + verticalCenter: contentLabel.verticalCenter + right: showHelp ? helpSep.left : parent.right + rightMargin: 10 + } + width: 76 + height: 26 + radius: 4 + opacity: isDirty && editorEnabled ? 1.0 : 0.4 + color: saveMouse.pressed ? "#15896b" : "#1cb27e" + + Text { + anchors.centerIn: parent + text: "Save" + color: "white" + font.pixelSize: 13 + } + + MouseArea { + id: saveMouse + anchors.fill: parent + enabled: isDirty && editorEnabled + onClicked: saveCurrentItem() + } + } + + // Content editor + Rectangle { + id: contentBox + anchors { + top: contentLabel.bottom + topMargin: 4 + left: listPanel.right + leftMargin: 10 + right: showHelp ? helpSep.left : parent.right + rightMargin: 10 + bottom: bottomBar.top + bottomMargin: 6 + } + radius: 3 + color: editorEnabled ? pal.base : pal.window + border.color: contentEdit.activeFocus ? "#1cb27e" : pal.mid + border.width: 1 + clip: true + + Flickable { + id: contentFlick + anchors { + fill: parent + margins: 8 + rightMargin: contentScroll.visible ? 14 : 8 + } + contentWidth: width + contentHeight: contentEdit.implicitHeight + flickableDirection: Flickable.VerticalFlick + clip: true + + TextEdit { + id: contentEdit + width: contentFlick.width + height: Math.max(contentFlick.height, implicitHeight) + wrapMode: Text.Wrap + selectByMouse: true + font.pixelSize: 13 + font.family: "monospace" + color: pal.text + enabled: editorEnabled + onTextChanged: { + if (!updating) + isDirty = true; + } + } + } + + Rectangle { + id: contentScroll + visible: contentFlick.contentHeight > contentFlick.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, contentFlick.height * contentFlick.height / Math.max(contentFlick.contentHeight, 1)) + y: contentFlick.height > 0 ? contentFlick.contentY / Math.max(contentFlick.contentHeight - contentFlick.height, 1) * (contentFlick.height - height) : 0 + } + } + } + + // ── Help panel ──────────────────────────────────────────────────────────── + Rectangle { + id: helpSep + visible: showHelp + width: 1 + color: pal.mid + anchors { + top: parent.top + topMargin: 10 + bottom: bottomBar.top + bottomMargin: 6 + right: helpPanel.left + rightMargin: 8 + } + } + + Rectangle { + id: helpPanel + visible: showHelp + width: helpPanelWidth + anchors { + top: parent.top + topMargin: 10 + right: parent.right + rightMargin: 10 + bottom: bottomBar.top + bottomMargin: 6 + } + color: "transparent" + TextEdit { + anchors.fill: parent + readOnly: true + selectByMouse: true + wrapMode: Text.NoWrap + font.pixelSize: 12 + font.family: "monospace" + color: pal.text + text: "Date & time\n" + " $CURRENT_YEAR four-digit year 2026\n" + " $CURRENT_YEAR_SHORT two-digit year 26\n" + " $CURRENT_MONTH month 04\n" + " $CURRENT_MONTH_NAME full name April\n" + " $CURRENT_MONTH_NAME_SHORT short name Apr\n" + " $CURRENT_DATE day 29\n" + " $CURRENT_HOUR hour 00–23 14\n" + " $CURRENT_MINUTE minute 07\n" + " $CURRENT_SECOND second 03\n" + " $CURRENT_SECONDS_UNIX Unix timestamp 1745920023\n\n" + "Identifiers\n" + " $UUID UUID v4\n\n" + "Note context\n" + " $NOTE_TITLE note title\n" + " $NOTE_FILENAME note filename\n\n" + "System\n" + " $OS_NAME Linux / macOS / Windows\n\n" + "Zettelkasten\n" + " $ZK_ID ID (format in settings)" + } + } + + // ── Functions ───────────────────────────────────────────────────────────── function loadItem(idx) { - if (idx < 0 || idx >= items.length) return; + if (idx < 0 || idx >= items.length) + return; updating = true; - nameField.text = items[idx].name; - contentArea.text = items[idx].content; + nameInput.text = items[idx].name; + contentEdit.text = items[idx].content; updating = false; isDirty = false; } function clearEditor() { updating = true; - nameField.text = ""; - contentArea.text = ""; + nameInput.text = ""; + contentEdit.text = ""; updating = false; isDirty = false; } function saveCurrentItem() { var idx = snippetList.currentIndex; - if (idx < 0) return; + if (idx < 0) + return; var copy = items.slice(); - copy[idx] = { name: nameField.text, content: contentArea.text }; + copy[idx] = { + "name": nameInput.text, + "content": contentEdit.text + }; items = copy; snippetList.currentIndex = idx; isDirty = false; diff --git a/snippets/README.md b/snippets/README.md index 5098392..8079f08 100644 --- a/snippets/README.md +++ b/snippets/README.md @@ -13,32 +13,32 @@ Placeholders are replaced at insertion time. ### Date & time -| Placeholder | Example output | -|---|---| -| `$CURRENT_YEAR` | `2026` | -| `$CURRENT_YEAR_SHORT` | `26` | -| `$CURRENT_MONTH` | `04` | -| `$CURRENT_MONTH_NAME` | `April` (locale système) | -| `$CURRENT_MONTH_NAME_SHORT` | `Apr` (locale système) | -| `$CURRENT_DATE` | `29` | -| `$CURRENT_HOUR` | `14` | -| `$CURRENT_MINUTE` | `07` | -| `$CURRENT_SECOND` | `03` | -| `$CURRENT_SECONDS_UNIX` | `1745920023` | +| Placeholder | Example output | +| --------------------------- | ------------------------ | +| `$CURRENT_YEAR` | `2026` | +| `$CURRENT_YEAR_SHORT` | `26` | +| `$CURRENT_MONTH` | `04` | +| `$CURRENT_MONTH_NAME` | `April` (locale système) | +| `$CURRENT_MONTH_NAME_SHORT` | `Apr` (locale système) | +| `$CURRENT_DATE` | `29` | +| `$CURRENT_HOUR` | `14` | +| `$CURRENT_MINUTE` | `07` | +| `$CURRENT_SECOND` | `03` | +| `$CURRENT_SECONDS_UNIX` | `1745920023` | ### Identifiants -| Placeholder | Exemple | -|---|---| -| `$UUID` | `550e8400-e29b-41d4-a716-446655440000` | -| `$ZK_ID` | `20260430143012` (format configurable) | +| Placeholder | Exemple | +| ----------- | -------------------------------------- | +| `$UUID` | `550e8400-e29b-41d4-a716-446655440000` | +| `$ZK_ID` | `20260430143012` (format configurable) | ### Note context -| Placeholder | Example output | -|---|---| -| `$NOTE_TITLE` | `My note` | -| `$NOTE_FILENAME` | `my-note.md` | +| Placeholder | Example output | +| ---------------- | -------------- | +| `$NOTE_TITLE` | `My note` | +| `$NOTE_FILENAME` | `my-note.md` | ## Zettelkasten integration @@ -46,14 +46,14 @@ Placeholders are replaced at insertion time. [Zettelkasten extension](../zettelkasten/). Configure **Zettelkasten ID format** in _Settings → Scripting → Text Snippets_ to match the format you use there. -| 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 | ## Exemple diff --git a/snippets/snippets.json b/snippets/snippets.json old mode 100644 new mode 100755 index ccab6ef..23c2e25 --- a/snippets/snippets.json +++ b/snippets/snippets.json @@ -1,14 +1,14 @@ [ { - "name": "Reference", - "content": "## Reference\n\nid$CURRENT_YEAR$CURRENT_MONTH$CURRENT_DATEx$CURRENT_HOUR$CURRENT_MINUTE$CURRENT_SECOND\n\n\nSee also:\n-\n-\n\n#tag" + "content": "## Reference\n\nid$CURRENT_YEAR$CURRENT_MONTH$CURRENT_DATEx$CURRENT_HOUR$CURRENT_MINUTE$CURRENT_SECOND\n\n\nSee also:\n- \n- \n\n#tag", + "name": "Reference" }, { "name": "test", - "content": "just a test" + "content": "just a test " }, { - "name": "Zettelkasten_id", - "content": "id$CURRENT_YEAR$CURRENT_MONTH$CURRENT_DATEx$CURRENT_HOUR$CURRENT_MINUTE$CURRENT_SECOND" + "content": "id$CURRENT_YEAR$CURRENT_MONTH$CURRENT_DATEx$CURRENT_HOUR$CURRENT_MINUTE$CURRENT_SECOND", + "name": "Zettelkasten_id" } ] \ No newline at end of file diff --git a/snippets/snippets.qml b/snippets/snippets.qml index dc63cb9..ea46196 100644 --- a/snippets/snippets.qml +++ b/snippets/snippets.qml @@ -131,18 +131,37 @@ Script { return; } - var names = []; - for (var i = 0; i < snippets.length; i++) names.push(snippets[i].name); - - var selected = script.inputDialogGetItem("Insert snippet", "Snippet:", names, 0, false); - if (!selected) return; + // Build entries with a preview (placeholders resolved at dialog-open time) + // and originalIndex so we can re-resolve at the moment of insertion. + var entries = []; + for (var i = 0; i < snippets.length; i++) { + entries.push({ + name: snippets[i].name, + preview: processPlaceholders(snippets[i].content), + originalIndex: i + }); + } - for (var j = 0; j < snippets.length; j++) { - if (snippets[j].name === selected) { - script.noteTextEditWrite(processPlaceholders(snippets[j].content)); - return; - } + var component = Qt.createComponent(Qt.resolvedUrl("InsertSnippetDialog.qml")); + if (component.status !== Component.Ready) { + script.log("snippets: failed to load InsertSnippetDialog — " + component.errorString()); + return; } + var dialog = component.createObject(null, { entries: entries }); + if (!dialog) { + script.log("snippets: failed to instantiate InsertSnippetDialog"); + return; + } + // Re-process placeholders at insertion time so timestamps are fresh. + dialog.snippetChosen.connect(function(originalIndex) { + script.noteTextEditWrite(processPlaceholders(snippets[originalIndex].content)); + }); + dialog.manageRequested.connect(function() { + manageSnippets(); + }); + dialog.show(); + dialog.raise(); + dialog.requestActivate(); } function manageSnippets() { From 7563256893e925e9278c60dc29204432746d1255 Mon Sep 17 00:00:00 2001 From: luginf Date: Thu, 30 Apr 2026 23:04:54 +0200 Subject: [PATCH 3/5] lint again --- snippets/ManageSnippetsDialog.qml | 6 +-- snippets/info.json | 7 +++- snippets/snippets.qml | 70 +++++++++++-------------------- 3 files changed, 33 insertions(+), 50 deletions(-) diff --git a/snippets/ManageSnippetsDialog.qml b/snippets/ManageSnippetsDialog.qml index f23a218..111be00 100644 --- a/snippets/ManageSnippetsDialog.qml +++ b/snippets/ManageSnippetsDialog.qml @@ -139,9 +139,9 @@ Window { onClicked: { var copy = items.slice(); copy.push({ - "name": "New snippet", - "content": "" - }); + "name": "New snippet", + "content": "" + }); items = copy; var idx = items.length - 1; snippetList.currentIndex = idx; diff --git a/snippets/info.json b/snippets/info.json index 45e39a6..334aed6 100644 --- a/snippets/info.json +++ b/snippets/info.json @@ -6,5 +6,10 @@ "authors": ["@luginf"], "platforms": ["linux", "macos", "windows"], "minAppVersion": "26.4.11", - "description": "Insert and manage reusable text snippets with placeholders ($CURRENT_YEAR, $CURRENT_DATE, $UUID…) into notes." + "description": "Insert and manage reusable text snippets with placeholders ($CURRENT_YEAR, $CURRENT_DATE, $UUID…) into notes.", + "resources": [ + "InsertSnippetDialog.qml", + "ManageSnippetsDialog.qml", + "snippets.json" + ] } diff --git a/snippets/snippets.qml b/snippets/snippets.qml index ea46196..cb14548 100644 --- a/snippets/snippets.qml +++ b/snippets/snippets.qml @@ -1,5 +1,4 @@ // Text Snippets — insert reusable text snippets with placeholders into notes. -// // Placeholders (date & time): // $CURRENT_YEAR four-digit year 2026 // $CURRENT_YEAR_SHORT two-digit year 26 @@ -11,17 +10,13 @@ // $CURRENT_MINUTE two-digit minute 07 // $CURRENT_SECOND two-digit second 03 // $CURRENT_SECONDS_UNIX Unix timestamp 1745920023 -// // Placeholders (identifiers): // $UUID UUID v4 550e8400-e29b-41d4-a716-446655440000 -// // Placeholders (note context): // $NOTE_TITLE current note title My note // $NOTE_FILENAME current note filename my-note.md -// // Placeholders (system): // $OS_NAME operating system name Linux - import QtQml 2.0 import QOwnNotesTypes 1.0 @@ -53,7 +48,6 @@ Script { } // ── Helpers ─────────────────────────────────────────────────────────────── - function pad(n) { return n < 10 ? "0" + n : String(n); } @@ -61,17 +55,11 @@ Script { function generateZkId() { var fmt = (zkIdFormat || "").trim() || "%Y%M%D%h%m%s"; var now = new Date(); - return fmt - .replace(/%Y/g, String(now.getFullYear())) - .replace(/%M/g, pad(now.getMonth() + 1)) - .replace(/%D/g, pad(now.getDate())) - .replace(/%h/g, pad(now.getHours())) - .replace(/%m/g, pad(now.getMinutes())) - .replace(/%s/g, pad(now.getSeconds())); + return fmt.replace(/%Y/g, String(now.getFullYear())).replace(/%M/g, pad(now.getMonth() + 1)).replace(/%D/g, pad(now.getDate())).replace(/%h/g, pad(now.getHours())).replace(/%m/g, pad(now.getMinutes())).replace(/%s/g, pad(now.getSeconds())); } function generateUUID() { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0; return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); }); @@ -81,23 +69,13 @@ Script { var now = new Date(); var loc = Qt.locale(); var note = script.currentNote(); - var osMap = { "linux": "Linux", "osx": "macOS", "windows": "Windows", "unix": "Unix" }; - return text - .replace(/\$CURRENT_YEAR_SHORT/g, String(now.getFullYear()).slice(-2)) - .replace(/\$CURRENT_YEAR/g, String(now.getFullYear())) - .replace(/\$CURRENT_MONTH_NAME_SHORT/g, loc.monthName(now.getMonth(), 1)) - .replace(/\$CURRENT_MONTH_NAME/g, loc.monthName(now.getMonth(), 0)) - .replace(/\$CURRENT_MONTH/g, pad(now.getMonth() + 1)) - .replace(/\$CURRENT_DATE/g, pad(now.getDate())) - .replace(/\$CURRENT_HOUR/g, pad(now.getHours())) - .replace(/\$CURRENT_MINUTE/g, pad(now.getMinutes())) - .replace(/\$CURRENT_SECOND/g, pad(now.getSeconds())) - .replace(/\$CURRENT_SECONDS_UNIX/g, String(Math.floor(now.getTime() / 1000))) - .replace(/\$UUID/g, generateUUID()) - .replace(/\$NOTE_TITLE/g, note ? note.name : "") - .replace(/\$NOTE_FILENAME/g, note ? note.fileName : "") - .replace(/\$OS_NAME/g, osMap[Qt.platform.os] || Qt.platform.os) - .replace(/\$ZK_ID/g, generateZkId()); + var osMap = { + "linux": "Linux", + "osx": "macOS", + "windows": "Windows", + "unix": "Unix" + }; + return text.replace(/\$CURRENT_YEAR_SHORT/g, String(now.getFullYear()).slice(-2)).replace(/\$CURRENT_YEAR/g, String(now.getFullYear())).replace(/\$CURRENT_MONTH_NAME_SHORT/g, loc.monthName(now.getMonth(), 1)).replace(/\$CURRENT_MONTH_NAME/g, loc.monthName(now.getMonth(), 0)).replace(/\$CURRENT_MONTH/g, pad(now.getMonth() + 1)).replace(/\$CURRENT_DATE/g, pad(now.getDate())).replace(/\$CURRENT_HOUR/g, pad(now.getHours())).replace(/\$CURRENT_MINUTE/g, pad(now.getMinutes())).replace(/\$CURRENT_SECOND/g, pad(now.getSeconds())).replace(/\$CURRENT_SECONDS_UNIX/g, String(Math.floor(now.getTime() / 1000))).replace(/\$UUID/g, generateUUID()).replace(/\$NOTE_TITLE/g, note ? note.name : "").replace(/\$NOTE_FILENAME/g, note ? note.fileName : "").replace(/\$OS_NAME/g, osMap[Qt.platform.os] || Qt.platform.os).replace(/\$ZK_ID/g, generateZkId()); } function snippetsFilePath() { @@ -106,7 +84,8 @@ Script { function loadSnippets() { var path = snippetsFilePath(); - if (!script.fileExists(path)) return []; + if (!script.fileExists(path)) + return []; try { return JSON.parse(script.readFromFile(path, "UTF-8")); } catch (e) { @@ -120,14 +99,10 @@ Script { } // ── Actions ─────────────────────────────────────────────────────────────── - function insertSnippet() { var snippets = loadSnippets(); if (snippets.length === 0) { - script.informationMessageBox( - "No snippets defined yet.\nUse Scripting › Manage snippets to create one.", - "Snippets" - ); + script.informationMessageBox("No snippets defined yet.\nUse Scripting › Manage snippets to create one.", "Snippets"); return; } @@ -136,27 +111,28 @@ Script { var entries = []; for (var i = 0; i < snippets.length; i++) { entries.push({ - name: snippets[i].name, - preview: processPlaceholders(snippets[i].content), - originalIndex: i + "name": snippets[i].name, + "preview": processPlaceholders(snippets[i].content), + "originalIndex": i }); } - var component = Qt.createComponent(Qt.resolvedUrl("InsertSnippetDialog.qml")); if (component.status !== Component.Ready) { script.log("snippets: failed to load InsertSnippetDialog — " + component.errorString()); return; } - var dialog = component.createObject(null, { entries: entries }); + var dialog = component.createObject(null, { + "entries": entries + }); if (!dialog) { script.log("snippets: failed to instantiate InsertSnippetDialog"); return; } // Re-process placeholders at insertion time so timestamps are fresh. - dialog.snippetChosen.connect(function(originalIndex) { + dialog.snippetChosen.connect(function (originalIndex) { script.noteTextEditWrite(processPlaceholders(snippets[originalIndex].content)); }); - dialog.manageRequested.connect(function() { + dialog.manageRequested.connect(function () { manageSnippets(); }); dialog.show(); @@ -170,12 +146,14 @@ Script { script.log("snippets: failed to load dialog — " + component.errorString()); return; } - var dialog = component.createObject(null, { snippets: loadSnippets() }); + var dialog = component.createObject(null, { + "snippets": loadSnippets() + }); if (!dialog) { script.log("snippets: failed to instantiate dialog"); return; } - dialog.snippetsSaved.connect(function(updated) { + dialog.snippetsSaved.connect(function (updated) { saveSnippets(updated); }); dialog.show(); From d2f0911e85d6b47d2505108152f7d8a0962997f4 Mon Sep 17 00:00:00 2001 From: luginf Date: Fri, 1 May 2026 10:53:33 +0200 Subject: [PATCH 4/5] add missing file --- snippets/InsertSnippetDialog.qml | 374 +++++++++++++++++++++++++++++++ snippets/snippets.qml | 2 +- 2 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 snippets/InsertSnippetDialog.qml diff --git a/snippets/InsertSnippetDialog.qml b/snippets/InsertSnippetDialog.qml new file mode 100644 index 0000000..f43fa4f --- /dev/null +++ b/snippets/InsertSnippetDialog.qml @@ -0,0 +1,374 @@ +import QtQuick 2.0 +import QtQuick.Window 2.0 + +Window { + id: root + title: "Insert Snippet" + width: 580 + height: 200 + minimumWidth: 400 + minimumHeight: 160 + modality: Qt.ApplicationModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint + + // entries: [{name, preview, originalIndex}] + property var entries: [] + signal snippetChosen(int index) + signal manageRequested + 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: 8 + } + height: 26 + radius: 3 + color: pal.base + border.color: searchInput.activeFocus ? "#1cb27e" : pal.mid + border.width: 1 + + Text { + anchors { + fill: parent + leftMargin: 7 + } + verticalAlignment: Text.AlignVCenter + text: "Filter…" + color: pal.mid + font.pixelSize: 12 + visible: searchInput.text === "" + } + + TextInput { + id: searchInput + anchors { + fill: parent + margins: 7 + } + verticalAlignment: TextInput.AlignVCenter + font.pixelSize: 12 + color: pal.text + clip: true + onTextChanged: applyFilter() + Keys.onReturnPressed: acceptSelection() + Keys.onDownPressed: moveSelection(1) + Keys.onUpPressed: moveSelection(-1) + } + } + + // ── Bottom bar ──────────────────────────────────────────────────────────── + Item { + id: bottomBar + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + margins: 8 + } + height: 30 + + // Manage button (left side) + Rectangle { + id: manageBtn + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + } + width: 70 + height: 24 + radius: 4 + color: manageMouse.pressed ? pal.dark : pal.button + border.color: pal.mid + border.width: 1 + + Text { + anchors.centerIn: parent + text: "Manage…" + color: pal.buttonText + font.pixelSize: 12 + } + + MouseArea { + id: manageMouse + anchors.fill: parent + onClicked: { + manageRequested(); + root.close(); + } + } + } + + Rectangle { + id: cancelBtn + anchors { + verticalCenter: parent.verticalCenter + right: insertBtn.left + rightMargin: 6 + } + width: 64 + height: 24 + 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: 12 + } + + MouseArea { + id: cancelMouse + anchors.fill: parent + onClicked: root.close() + } + } + + Rectangle { + id: insertBtn + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + } + width: 64 + height: 24 + radius: 4 + opacity: snippetList.currentIndex >= 0 && filtered.length > 0 ? 1.0 : 0.4 + color: insertMouse.pressed ? "#15896b" : "#1cb27e" + + Text { + anchors.centerIn: parent + text: "Insert" + color: "white" + font.pixelSize: 12 + } + + MouseArea { + id: insertMouse + anchors.fill: parent + enabled: snippetList.currentIndex >= 0 && filtered.length > 0 + onClicked: acceptSelection() + } + } + } + + // ── Left panel: snippet list ────────────────────────────────────────────── + Rectangle { + id: listPanel + anchors { + top: searchBox.bottom + topMargin: 5 + left: parent.left + leftMargin: 8 + bottom: bottomBar.top + bottomMargin: 5 + } + width: 180 + radius: 3 + color: pal.base + border.color: pal.mid + border.width: 1 + clip: true + + ListView { + id: snippetList + anchors { + fill: parent + margins: 1 + rightMargin: listScroll.visible ? 8 : 1 + } + model: filtered + currentIndex: filtered.length > 0 ? 0 : -1 + clip: true + boundsBehavior: Flickable.StopAtBounds + + delegate: Item { + width: snippetList.width + height: 24 + + Rectangle { + anchors.fill: parent + color: index === snippetList.currentIndex ? "#1cb27e" : (rowMouse.containsMouse ? "#e4f5ef" : "transparent") + } + + Text { + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.right + margins: 7 + } + text: modelData.name + color: index === snippetList.currentIndex ? "white" : pal.text + font.pixelSize: 12 + elide: Text.ElideRight + } + + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + onClicked: snippetList.currentIndex = index + onDoubleClicked: acceptSelection() + } + } + + onCurrentIndexChanged: { + if (currentIndex >= 0 && currentIndex < filtered.length) + previewEdit.text = filtered[currentIndex].preview; + else + previewEdit.text = ""; + } + } + + Rectangle { + id: listScroll + visible: snippetList.contentHeight > snippetList.height + width: 4 + 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(20, snippetList.height * snippetList.height / Math.max(snippetList.contentHeight, 1)) + y: snippetList.height > 0 ? snippetList.contentY / Math.max(snippetList.contentHeight - snippetList.height, 1) * (snippetList.height - height) : 0 + } + } + + Text { + anchors.centerIn: parent + visible: filtered.length === 0 + text: "No match." + color: pal.mid + font.pixelSize: 12 + } + } + + // ── Right panel: preview ────────────────────────────────────────────────── + Text { + id: previewLabel + anchors { + top: searchBox.bottom + topMargin: 8 + left: listPanel.right + leftMargin: 10 + } + text: "Preview" + color: pal.mid + font.pixelSize: 10 + } + + Rectangle { + id: previewPanel + anchors { + top: previewLabel.bottom + topMargin: 2 + left: listPanel.right + leftMargin: 8 + right: parent.right + rightMargin: 8 + bottom: bottomBar.top + bottomMargin: 5 + } + radius: 3 + color: pal.base + border.color: pal.mid + border.width: 1 + clip: true + + Flickable { + id: previewFlick + anchors { + fill: parent + margins: 7 + rightMargin: previewScroll.visible ? 12 : 7 + } + contentWidth: width + contentHeight: previewEdit.implicitHeight + flickableDirection: Flickable.VerticalFlick + clip: true + + TextEdit { + id: previewEdit + width: previewFlick.width + height: Math.max(previewFlick.height, implicitHeight) + readOnly: true + selectByMouse: true + wrapMode: Text.Wrap + font.pixelSize: 12 + font.family: "monospace" + color: pal.text + } + } + + Rectangle { + id: previewScroll + visible: previewFlick.contentHeight > previewFlick.height + width: 4 + 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(20, previewFlick.height * previewFlick.height / Math.max(previewFlick.contentHeight, 1)) + y: previewFlick.height > 0 ? previewFlick.contentY / Math.max(previewFlick.contentHeight - previewFlick.height, 1) * (previewFlick.height - height) : 0 + } + } + } + + // ── Logic ───────────────────────────────────────────────────────────────── + function moveSelection(delta) { + var next = snippetList.currentIndex + delta; + if (next >= 0 && next < snippetList.count) + snippetList.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].name.toLowerCase().indexOf(f) >= 0) + result.push(entries[i]); + } + filtered = result; + snippetList.currentIndex = result.length > 0 ? 0 : -1; + } + + function acceptSelection() { + var idx = snippetList.currentIndex; + if (idx < 0 || idx >= filtered.length) + return; + snippetChosen(filtered[idx].originalIndex); + root.close(); + } +} diff --git a/snippets/snippets.qml b/snippets/snippets.qml index cb14548..1eada65 100644 --- a/snippets/snippets.qml +++ b/snippets/snippets.qml @@ -95,7 +95,7 @@ Script { } function saveSnippets(snippets) { - script.writeToFile(snippetsFilePath(), JSON.stringify(snippets, null, 2), false); + script.writeToFile(snippetsFilePath(), JSON.stringify(snippets, null, 2)); } // ── Actions ─────────────────────────────────────────────────────────────── From 11b62647498841b81309d74ba2be1ddb7b1e96e0 Mon Sep 17 00:00:00 2001 From: luginf Date: Fri, 1 May 2026 13:48:30 +0200 Subject: [PATCH 5/5] fix various copilot suggestions --- snippets/ManageSnippetsDialog.qml | 26 ++++++++++++++------------ snippets/README.md | 6 +++--- snippets/snippets.json | 4 ---- snippets/snippets.qml | 21 ++++++++++++++++++++- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/snippets/ManageSnippetsDialog.qml b/snippets/ManageSnippetsDialog.qml index 111be00..752f757 100644 --- a/snippets/ManageSnippetsDialog.qml +++ b/snippets/ManageSnippetsDialog.qml @@ -17,6 +17,7 @@ Window { property var items: [] property bool updating: false property bool isDirty: false + property bool listDirty: false property bool showHelp: false readonly property int baseWidth: 720 @@ -183,7 +184,7 @@ Window { var copy = items.slice(); copy.splice(idx, 1); items = copy; - snippetsSaved(items); + listDirty = true; var next = Math.min(idx, items.length - 1); snippetList.currentIndex = next; if (next >= 0) @@ -355,7 +356,7 @@ Window { width: 76 height: 26 radius: 4 - opacity: isDirty && editorEnabled ? 1.0 : 0.4 + opacity: (isDirty && editorEnabled) || listDirty ? 1.0 : 0.4 color: saveMouse.pressed ? "#15896b" : "#1cb27e" Text { @@ -368,7 +369,7 @@ Window { MouseArea { id: saveMouse anchors.fill: parent - enabled: isDirty && editorEnabled + enabled: (isDirty && editorEnabled) || listDirty onClicked: saveCurrentItem() } } @@ -506,16 +507,17 @@ Window { function saveCurrentItem() { var idx = snippetList.currentIndex; - if (idx < 0) - return; - var copy = items.slice(); - copy[idx] = { - "name": nameInput.text, - "content": contentEdit.text - }; - items = copy; - snippetList.currentIndex = idx; + if (idx >= 0) { + var copy = items.slice(); + copy[idx] = { + "name": nameInput.text, + "content": contentEdit.text + }; + items = copy; + snippetList.currentIndex = idx; + } isDirty = false; + listDirty = false; snippetsSaved(items); } } diff --git a/snippets/README.md b/snippets/README.md index 8079f08..2512408 100644 --- a/snippets/README.md +++ b/snippets/README.md @@ -42,9 +42,9 @@ Placeholders are replaced at insertion time. ## Zettelkasten integration -`$ZK_ID` generates a Zettelkasten ID using the same format string syntax as the -[Zettelkasten extension](../zettelkasten/). Configure **Zettelkasten ID format** -in _Settings → Scripting → Text Snippets_ to match the format you use there. +`$ZK_ID` generates a Zettelkasten ID using the format string syntax documented +below. Configure **Zettelkasten ID format** in _Settings → Scripting → Text +Snippets_ to match the token format you use for your Zettelkasten IDs. | Token | Value | | ----- | -------------- | diff --git a/snippets/snippets.json b/snippets/snippets.json index 23c2e25..5e3e5ca 100755 --- a/snippets/snippets.json +++ b/snippets/snippets.json @@ -3,10 +3,6 @@ "content": "## Reference\n\nid$CURRENT_YEAR$CURRENT_MONTH$CURRENT_DATEx$CURRENT_HOUR$CURRENT_MINUTE$CURRENT_SECOND\n\n\nSee also:\n- \n- \n\n#tag", "name": "Reference" }, - { - "name": "test", - "content": "just a test " - }, { "content": "id$CURRENT_YEAR$CURRENT_MONTH$CURRENT_DATEx$CURRENT_HOUR$CURRENT_MINUTE$CURRENT_SECOND", "name": "Zettelkasten_id" diff --git a/snippets/snippets.qml b/snippets/snippets.qml index 1eada65..6e693ff 100644 --- a/snippets/snippets.qml +++ b/snippets/snippets.qml @@ -75,7 +75,26 @@ Script { "windows": "Windows", "unix": "Unix" }; - return text.replace(/\$CURRENT_YEAR_SHORT/g, String(now.getFullYear()).slice(-2)).replace(/\$CURRENT_YEAR/g, String(now.getFullYear())).replace(/\$CURRENT_MONTH_NAME_SHORT/g, loc.monthName(now.getMonth(), 1)).replace(/\$CURRENT_MONTH_NAME/g, loc.monthName(now.getMonth(), 0)).replace(/\$CURRENT_MONTH/g, pad(now.getMonth() + 1)).replace(/\$CURRENT_DATE/g, pad(now.getDate())).replace(/\$CURRENT_HOUR/g, pad(now.getHours())).replace(/\$CURRENT_MINUTE/g, pad(now.getMinutes())).replace(/\$CURRENT_SECOND/g, pad(now.getSeconds())).replace(/\$CURRENT_SECONDS_UNIX/g, String(Math.floor(now.getTime() / 1000))).replace(/\$UUID/g, generateUUID()).replace(/\$NOTE_TITLE/g, note ? note.name : "").replace(/\$NOTE_FILENAME/g, note ? note.fileName : "").replace(/\$OS_NAME/g, osMap[Qt.platform.os] || Qt.platform.os).replace(/\$ZK_ID/g, generateZkId()); + var placeholders = { + "$CURRENT_SECONDS_UNIX": String(Math.floor(now.getTime() / 1000)), + "$CURRENT_YEAR_SHORT": String(now.getFullYear()).slice(-2), + "$CURRENT_YEAR": String(now.getFullYear()), + "$CURRENT_MONTH_NAME_SHORT": loc.monthName(now.getMonth(), 1), + "$CURRENT_MONTH_NAME": loc.monthName(now.getMonth(), 0), + "$CURRENT_MONTH": pad(now.getMonth() + 1), + "$CURRENT_DATE": pad(now.getDate()), + "$CURRENT_HOUR": pad(now.getHours()), + "$CURRENT_MINUTE": pad(now.getMinutes()), + "$CURRENT_SECOND": pad(now.getSeconds()), + "$UUID": generateUUID(), + "$NOTE_TITLE": note ? note.name : "", + "$NOTE_FILENAME": note ? note.fileName : "", + "$OS_NAME": osMap[Qt.platform.os] || Qt.platform.os, + "$ZK_ID": generateZkId() + }; + return text.replace(/\$(?:CURRENT_SECONDS_UNIX|CURRENT_YEAR_SHORT|CURRENT_YEAR|CURRENT_MONTH_NAME_SHORT|CURRENT_MONTH_NAME|CURRENT_MONTH|CURRENT_DATE|CURRENT_HOUR|CURRENT_MINUTE|CURRENT_SECOND|UUID|NOTE_TITLE|NOTE_FILENAME|OS_NAME|ZK_ID)/g, function (match) { + return placeholders[match]; + }); } function snippetsFilePath() {