From afae6710a1a082209750c9c403d005c4d0dcfcb9 Mon Sep 17 00:00:00 2001
From: kellyson <109356794+kellyson71@users.noreply.github.com>
Date: Wed, 25 Feb 2026 02:15:29 -0300
Subject: [PATCH 1/5] feat(config): add obsidianEnabled property to
PerformanceAdapter
---
config/Config.qml | 1 +
1 file changed, 1 insertion(+)
diff --git a/config/Config.qml b/config/Config.qml
index 9f159e8e..7527d8f7 100644
--- a/config/Config.qml
+++ b/config/Config.qml
@@ -727,6 +727,7 @@ Singleton {
property bool rotateCoverArt: true
property bool dashboardPersistTabs: true
property int dashboardMaxPersistentTabs: 2
+ property bool obsidianEnabled: true
}
}
From 5e2717038901d7ccf4c1732ebfb4db1acc293388 Mon Sep 17 00:00:00 2001
From: kellyson <109356794+kellyson71@users.noreply.github.com>
Date: Wed, 25 Feb 2026 02:15:38 -0300
Subject: [PATCH 2/5] feat(dashboard): add Dashboard settings section with
Obsidian integration toggle
---
.../widgets/dashboard/controls/ShellPanel.qml | 33 +++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/modules/widgets/dashboard/controls/ShellPanel.qml b/modules/widgets/dashboard/controls/ShellPanel.qml
index 76796878..01110550 100644
--- a/modules/widgets/dashboard/controls/ShellPanel.qml
+++ b/modules/widgets/dashboard/controls/ShellPanel.qml
@@ -664,6 +664,10 @@ Item {
text: "System"
sectionId: "system"
}
+ SectionButton {
+ text: "Dashboard"
+ sectionId: "dashboard"
+ }
}
// ═══════════════════════════════════════════════════════════════
@@ -1850,6 +1854,35 @@ Item {
}
}
}
+
+ // ═══════════════════════════════════════════════════════════════
+ // DASHBOARD SECTION
+ // ═══════════════════════════════════════════════════════════════
+ ColumnLayout {
+ visible: root.currentSection === "dashboard"
+ Layout.fillWidth: true
+ spacing: 8
+
+ Text {
+ text: "Notes"
+ font.family: Config.theme.font
+ font.pixelSize: Styling.fontSize(-1)
+ font.weight: Font.Medium
+ color: Colors.overSurfaceVariant
+ Layout.bottomMargin: -4
+ }
+
+ ToggleRow {
+ label: "Obsidian Integration"
+ checked: Config.performance.obsidianEnabled ?? true
+ onToggled: value => {
+ if (value !== Config.performance.obsidianEnabled) {
+ GlobalStates.markShellChanged();
+ Config.performance.obsidianEnabled = value;
+ }
+ }
+ }
+ }
}
}
}
From 7248631f3e02ffba58672dfdadf5bde4448b2357 Mon Sep 17 00:00:00 2001
From: kellyson <109356794+kellyson71@users.noreply.github.com>
Date: Wed, 25 Feb 2026 02:15:48 -0300
Subject: [PATCH 3/5] feat(notes): add python backend for Obsidian vault
scanning
---
.../dashboard/notes/obsidian_backend.py | 51 +++++++++++++++++++
1 file changed, 51 insertions(+)
create mode 100644 modules/widgets/dashboard/notes/obsidian_backend.py
diff --git a/modules/widgets/dashboard/notes/obsidian_backend.py b/modules/widgets/dashboard/notes/obsidian_backend.py
new file mode 100644
index 00000000..850e4ac1
--- /dev/null
+++ b/modules/widgets/dashboard/notes/obsidian_backend.py
@@ -0,0 +1,51 @@
+import os
+import sys
+import json
+import time
+
+def scan_vault(vault_path):
+ if not os.path.exists(vault_path):
+ return {"order": [], "notes": {}}
+
+ notes_data = {}
+ order = []
+
+ # Scan for markdown files
+ for root, _, files in os.walk(vault_path):
+ for file in files:
+ if file.endswith('.md'):
+ file_path = os.path.join(root, file)
+
+ # Use filename without extension as title and ID
+ title = file[:-3]
+ note_id = title # Using title as ID for easier reading in QML
+
+ try:
+ stats = os.stat(file_path)
+ created = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(stats.st_ctime))
+ modified = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime(stats.st_mtime))
+
+ notes_data[note_id] = {
+ "id": note_id,
+ "title": title,
+ "created": created,
+ "modified": modified,
+ "isMarkdown": True
+ }
+ order.append(note_id)
+ except Exception:
+ continue
+
+ # Sort order by modified date descending (newest first)
+ order.sort(key=lambda x: notes_data[x]["modified"], reverse=True)
+
+ return {"order": order, "notes": notes_data}
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print(json.dumps({"order": [], "notes": {}}))
+ sys.exit(1)
+
+ vault_path = sys.argv[1]
+ result = scan_vault(vault_path)
+ print(json.dumps(result))
From c0dd40ae33f0cdc5fbe3eaaf2020e94cb2aacd62 Mon Sep 17 00:00:00 2001
From: kellyson <109356794+kellyson71@users.noreply.github.com>
Date: Wed, 25 Feb 2026 02:15:57 -0300
Subject: [PATCH 4/5] feat(notes): implement Obsidian Vault integration and
dynamic path selection
---
modules/widgets/dashboard/notes/NotesTab.qml | 237 ++++++++++++-------
1 file changed, 156 insertions(+), 81 deletions(-)
diff --git a/modules/widgets/dashboard/notes/NotesTab.qml b/modules/widgets/dashboard/notes/NotesTab.qml
index 01e120bc..e89f595c 100644
--- a/modules/widgets/dashboard/notes/NotesTab.qml
+++ b/modules/widgets/dashboard/notes/NotesTab.qml
@@ -21,11 +21,61 @@ Item {
property int leftPanelWidth: 0
- // Notes directory configuration
- property string notesDir: (Quickshell.env("XDG_DATA_HOME") || (Quickshell.env("HOME") + "/.local/share")) + "/ambxst-notes"
- property string indexPath: notesDir + "/index.json"
- property string notesPath: notesDir + "/notes"
- property string noteExtension: ".html" // Store as HTML for rich text (Markdown uses .md)
+ // Notes directory configuration - persistent via file
+ property string defaultVault: (Quickshell.env("XDG_DATA_HOME") || (Quickshell.env("HOME") + "/.local/share")) + "/ambxst/notes"
+ property string vaultPathFile: (Quickshell.env("XDG_DATA_HOME") || (Quickshell.env("HOME") + "/.local/share")) + "/ambxst-obsidian-path.txt"
+ property string notesDir: defaultVault // Will be overwritten on load
+
+ // Process to read saved vault path
+ Process {
+ id: readVaultPathProcess
+ command: ["cat", vaultPathFile]
+ stdout: SplitParser {
+ onRead: data => {
+ if (data.trim() !== "") {
+ root.notesDir = data.trim();
+ }
+ }
+ }
+ onExited: {
+ root.refreshNotes()
+ }
+ }
+
+ Component.onCompleted: {
+ readVaultPathProcess.running = true;
+ initDirProcess.running = true;
+ }
+
+ Connections {
+ target: Config.performance
+ function onObsidianEnabledChanged() {
+ root.refreshNotes();
+ }
+ }
+
+ // Process to pick and save vault path
+ Process {
+ id: pickVaultProcess
+ command: ["zenity", "--file-selection", "--directory", "--title", "Select Obsidian Vault"]
+ stdout: SplitParser {
+ onRead: data => {
+ if (data.trim() !== "") {
+ root.notesDir = data.trim();
+ saveVaultPathProcess.command = ["bash", "-c", "echo '" + data.trim() + "' > '" + vaultPathFile + "'"];
+ saveVaultPathProcess.running = true;
+ root.refreshNotes();
+ }
+ }
+ }
+ }
+
+ Process {
+ id: saveVaultPathProcess
+ }
+ property string indexPath: (Quickshell.env("XDG_DATA_HOME") || (Quickshell.env("HOME") + "/.local/share")) + "/ambxst-notes-index.json"
+ property string notesPath: notesDir
+ property string noteExtension: ".md" // Use Markdown for Obsidian
// Search and selection state
property string searchText: ""
@@ -534,6 +584,17 @@ Item {
noteNameToCreate: noteNameToCreate,
icon: "plus"
});
+
+ // Add vault config button at the end if enabled
+ if (Config.performance.obsidianEnabled) {
+ newFilteredNotes.push({
+ id: "__vault__",
+ title: "Vault: " + notesDir.split("/").pop(),
+ isVaultButton: true,
+ isCreateButton: false,
+ icon: "folder"
+ });
+ }
}
filteredNotes = newFilteredNotes;
@@ -599,16 +660,7 @@ Item {
function confirmDeleteNote() {
if (noteToDelete) {
- // Find if note is markdown to use correct extension
- var isMarkdown = false;
- for (var i = 0; i < allNotes.length; i++) {
- if (allNotes[i].id === noteToDelete) {
- isMarkdown = allNotes[i].isMarkdown || false;
- break;
- }
- }
- var extension = isMarkdown ? ".md" : noteExtension;
- // Store the ID before cancelDeleteMode clears it
+ var extension = ".md";
deleteNoteProcess.deletedNoteId = noteToDelete;
deleteNoteProcess.command = ["rm", "-f", notesPath + "/" + noteToDelete + extension];
deleteNoteProcess.running = true;
@@ -658,16 +710,19 @@ Item {
}
function createNewNote(title, isMarkdown) {
- var noteId = NotesUtils.generateUUID();
+ if (isMarkdown === undefined) isMarkdown = true; // Force true for Obsidian
+
+ // Remove uuid generation, directly use title
var noteTitle = title || "Untitled Note";
- var extension = isMarkdown ? ".md" : noteExtension;
+ var noteId = noteTitle; // ID is the title directly
+ var extension = ".md";
// Create the note file with appropriate content
- var initialContent = isMarkdown ? "# " + noteTitle + "\n\n" : "
" + noteTitle + "
";
+ var initialContent = "# " + noteTitle + "\n\n";
createNoteProcess.noteId = noteId;
createNoteProcess.noteTitle = noteTitle;
- createNoteProcess.noteIsMarkdown = isMarkdown || false;
+ createNoteProcess.noteIsMarkdown = isMarkdown;
createNoteProcess.command = ["sh", "-c", "mkdir -p '" + notesPath + "' && printf '%s' '" + initialContent.replace(/'/g, "'\\''") + "' > '" + notesPath + "/" + noteId + extension + "'"];
createNoteProcess.running = true;
}
@@ -716,11 +771,11 @@ Item {
}
function updateNoteTitle(noteId, newTitle) {
- // Read index, update title, save
- readIndexForUpdateProcess.noteId = noteId;
- readIndexForUpdateProcess.newTitle = newTitle;
- readIndexForUpdateProcess.command = ["cat", indexPath];
- readIndexForUpdateProcess.running = true;
+ // Just rename the file in shell
+ renameNoteProcess.oldNoteId = noteId;
+ renameNoteProcess.newNoteId = newTitle;
+ renameNoteProcess.command = ["mv", notesPath + "/" + noteId + ".md", notesPath + "/" + newTitle + ".md"];
+ renameNoteProcess.running = true;
}
function updateNoteModified(noteId) {
@@ -824,9 +879,7 @@ Item {
saveIndexProcess.running = true;
}
- Component.onCompleted: {
- initDirProcess.running = true;
- }
+
// --- Processes ---
@@ -840,10 +893,11 @@ Item {
}
}
- // Read index.json
+ // Read notes from python backend
Process {
id: readIndexProcess
- command: ["cat", indexPath]
+ // We pass the notesDir which is the Vault path
+ command: ["python3", Quickshell.env("HOME") + "/.local/src/ambxst/modules/widgets/dashboard/notes/obsidian_backend.py", root.notesDir]
stdout: SplitParser {
onRead: data => readIndexProcess.stdoutData += data + "\n"
}
@@ -863,7 +917,7 @@ Item {
title: noteMeta.title || "Untitled",
created: noteMeta.created || "",
modified: noteMeta.modified || "",
- isMarkdown: noteMeta.isMarkdown || false,
+ isMarkdown: noteMeta.isMarkdown !== undefined ? noteMeta.isMarkdown : true,
isCreateButton: false
});
}
@@ -976,64 +1030,41 @@ Item {
onExited: code => {}
}
- // Read index for title update
+ // Read index for title update (removed because mv command handles this now)
Process {
- id: readIndexForUpdateProcess
- property string noteId: ""
- property string newTitle: ""
- stdout: SplitParser {
- onRead: data => readIndexForUpdateProcess.stdoutData += data + "\n"
- }
- property string stdoutData: ""
+ id: renameNoteProcess // Name changed to match updateNoteTitle script call
+ property string oldNoteId: ""
+ property string newNoteId: ""
onExited: code => {
- var indexData = NotesUtils.parseIndex(stdoutData.trim());
- stdoutData = "";
-
- if (indexData.notes[noteId]) {
- indexData.notes[noteId].title = newTitle;
- indexData.notes[noteId].modified = NotesUtils.getCurrentTimestamp();
- }
-
- // Update local allNotes
+ // Update local allNotes to reflect rename without reloading python purely for UX speed
for (var i = 0; i < allNotes.length; i++) {
- if (allNotes[i].id === noteId) {
- allNotes[i].title = newTitle;
+ if (allNotes[i].id === oldNoteId) {
+ allNotes[i].title = newNoteId;
+ allNotes[i].id = newNoteId;
allNotes[i].modified = NotesUtils.getCurrentTimestamp();
break;
}
}
+ updateFilteredNotes();
- var jsonContent = NotesUtils.serializeIndex(indexData);
- saveIndexProcess.command = ["sh", "-c", "printf '%s' '" + jsonContent.replace(/'/g, "'\\''") + "' > '" + indexPath + "'"];
- saveIndexProcess.running = true;
+ // Refocus if it was the currently open note
+ if (currentNoteId === oldNoteId) {
+ currentNoteId = newNoteId;
+ currentNoteTitle = newNoteId;
+ }
- updateFilteredNotes();
- noteId = "";
- newTitle = "";
+ oldNoteId = "";
+ newNoteId = "";
}
}
- // Read index for modified timestamp update
+ // Read index for modified timestamp update (removed because python parses it natively)
Process {
id: readIndexForModifiedProcess
property string noteId: ""
- stdout: SplitParser {
- onRead: data => readIndexForModifiedProcess.stdoutData += data + "\n"
- }
- property string stdoutData: ""
onExited: code => {
- var indexData = NotesUtils.parseIndex(stdoutData.trim());
- stdoutData = "";
-
- if (indexData.notes[noteId]) {
- indexData.notes[noteId].modified = NotesUtils.getCurrentTimestamp();
- }
-
- var jsonContent = NotesUtils.serializeIndex(indexData);
- saveIndexProcess.command = ["sh", "-c", "printf '%s' '" + jsonContent.replace(/'/g, "'\\''") + "' > '" + indexPath + "'"];
- saveIndexProcess.running = true;
noteId = "";
}
}
@@ -1105,6 +1136,21 @@ Item {
root.expandedItemIndex = -1;
createOptions[root.selectedOptionIndex]();
}
+ } else if (note.isVaultButton) {
+ // Vault config options
+ let vaultOptions = [function () {
+ pickVaultProcess.running = true;
+ }, function () {
+ root.notesDir = root.defaultVault;
+ saveVaultPathProcess.command = ["bash", "-c", "echo '" + root.defaultVault + "' > '" + vaultPathFile + "'"];
+ saveVaultPathProcess.running = true;
+ root.refreshNotes();
+ }
+ ];
+ if (root.selectedOptionIndex >= 0 && root.selectedOptionIndex < vaultOptions.length) {
+ root.expandedItemIndex = -1;
+ vaultOptions[root.selectedOptionIndex]();
+ }
} else {
// Note options
let options = [function () {
@@ -1126,8 +1172,8 @@ Item {
if (root.selectedIndex >= 0 && root.selectedIndex < filteredNotes.length) {
let note = filteredNotes[root.selectedIndex];
- if (note.isCreateButton || note.isCreateSpecificButton) {
- // Expand to show create options instead of creating directly
+ if (note.isCreateButton || note.isCreateSpecificButton || note.isVaultButton) {
+ // Expand to show options
root.expandedItemIndex = root.selectedIndex;
root.selectedOptionIndex = 0;
root.keyboardNavigation = true;
@@ -1185,9 +1231,10 @@ Item {
return;
}
if (root.expandedItemIndex >= 0) {
- // Max options: 2 for create button, 3 for notes
- var isCreateBtn = root.expandedItemIndex < filteredNotes.length && filteredNotes[root.expandedItemIndex].isCreateButton;
- var maxIndex = isCreateBtn ? 1 : 2;
+ // Max options: 2 for create/vault buttons, 3 for notes
+ var expandedNote = root.expandedItemIndex < filteredNotes.length ? filteredNotes[root.expandedItemIndex] : null;
+ var isSmallMenu = expandedNote && (expandedNote.isCreateButton || expandedNote.isVaultButton);
+ var maxIndex = isSmallMenu ? 1 : 2;
if (root.selectedOptionIndex < maxIndex) {
root.selectedOptionIndex++;
root.keyboardNavigation = true;
@@ -1242,7 +1289,7 @@ Item {
}
}
}
- }
+ } // SearchInput end
// Results list
ListView {
@@ -1369,8 +1416,8 @@ Item {
height: {
let baseHeight = 48;
if (index === root.expandedItemIndex && !isInDeleteMode && !isInRenameMode) {
- // 2 options for create button, 3 for regular notes
- var optionCount = modelData.isCreateButton ? 2 : 3;
+ // 2 options for create/vault buttons, 3 for regular notes
+ var optionCount = (modelData.isCreateButton || modelData.isVaultButton) ? 2 : 3;
var listHeight = 36 * optionCount;
return baseHeight + 4 + listHeight + 8;
}
@@ -1434,7 +1481,7 @@ Item {
}
if (!root.deleteMode && !isExpanded) {
- if (modelData.isCreateButton || modelData.isCreateSpecificButton) {
+ if (modelData.isCreateButton || modelData.isCreateSpecificButton || modelData.isVaultButton) {
// Show create menu instead of creating directly
if (root.expandedItemIndex === index) {
root.expandedItemIndex = -1;
@@ -1673,6 +1720,8 @@ Item {
return Icons.edit;
} else if (modelData.isCreateButton) {
return Icons.plus;
+ } else if (modelData.isVaultButton) {
+ return Icons.folder;
} else if (modelData.isMarkdown) {
return Icons.markdown;
} else {
@@ -2007,9 +2056,35 @@ Item {
}
]
+ property var vaultOptions: [
+ {
+ text: "Selecionar Pasta",
+ icon: Icons.folder,
+ highlightColor: Styling.srItem("overprimary"),
+ textColor: Styling.srItem("primary"),
+ action: function () {
+ root.expandedItemIndex = -1;
+ pickVaultProcess.running = true;
+ }
+ },
+ {
+ text: "Restaurar Padrão",
+ icon: Icons.arrowCounterClockwise,
+ highlightColor: Colors.secondary,
+ textColor: Styling.srItem("secondary"),
+ action: function () {
+ root.expandedItemIndex = -1;
+ root.notesDir = root.defaultVault;
+ saveVaultPathProcess.command = ["bash", "-c", "echo '" + root.defaultVault + "' > '" + vaultPathFile + "'"];
+ saveVaultPathProcess.running = true;
+ root.refreshNotes();
+ }
+ }
+ ]
+
ClippingRectangle {
Layout.fillWidth: true
- Layout.preferredHeight: 36 * (modelData.isCreateButton ? 2 : 3)
+ Layout.preferredHeight: 36 * ((modelData.isCreateButton || modelData.isVaultButton) ? 2 : 3)
color: Colors.background
radius: Styling.radius(0)
@@ -2027,7 +2102,7 @@ Item {
clip: true
interactive: false
boundsBehavior: Flickable.StopAtBounds
- model: modelData.isCreateButton ? expandedOptionsLayout.createOptions : expandedOptionsLayout.noteOptions
+ model: modelData.isCreateButton ? expandedOptionsLayout.createOptions : (modelData.isVaultButton ? expandedOptionsLayout.vaultOptions : expandedOptionsLayout.noteOptions)
currentIndex: root.selectedOptionIndex
highlightFollowsCurrentItem: true
highlightRangeMode: ListView.ApplyRange
From 10aab547bd6ce05410021c7df4954880ee1250c6 Mon Sep 17 00:00:00 2001
From: kellyson <109356794+kellyson71@users.noreply.github.com>
Date: Wed, 25 Feb 2026 02:16:06 -0300
Subject: [PATCH 5/5] feat(binds): add button to open keybinds JSON manually
---
modules/widgets/dashboard/controls/BindsPanel.qml | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/modules/widgets/dashboard/controls/BindsPanel.qml b/modules/widgets/dashboard/controls/BindsPanel.qml
index 3befcabc..b1676628 100644
--- a/modules/widgets/dashboard/controls/BindsPanel.qml
+++ b/modules/widgets/dashboard/controls/BindsPanel.qml
@@ -579,6 +579,13 @@ Item {
onClicked: function () {
Config.keybindsLoader.reload();
}
+ },
+ {
+ icon: Icons.file,
+ tooltip: "Open JSON",
+ onClicked: function () {
+ Qt.openUrlExternally("file://" + Config.keybindsPath);
+ }
}
]
}