From ed9af263fde269ac45e000afa20f58909bafb470 Mon Sep 17 00:00:00 2001 From: bbedward Date: Sat, 12 Jul 2025 21:47:02 -0400 Subject: [PATCH] Recently used apps --- Common/Prefs.qml | 38 ++++++++++++-- Services/PreferencesService.qml | 15 +++++- Widgets/AppLauncher.qml | 78 ++++++++++++++++++++++++++++- Widgets/SpotlightLauncher.qml | 88 +++++++++++++++++++++++++++++++-- 4 files changed, 208 insertions(+), 11 deletions(-) diff --git a/Common/Prefs.qml b/Common/Prefs.qml index 9c6fbd2a..7d89cd1b 100644 --- a/Common/Prefs.qml +++ b/Common/Prefs.qml @@ -120,24 +120,52 @@ Singleton { } function addRecentApp(app) { + if (!app) return + + var execProp = app.execString || app.exec || "" + if (!execProp) return + var existingIndex = -1 for (var i = 0; i < recentlyUsedApps.length; i++) { - if (recentlyUsedApps[i].exec === app.exec) { + if (recentlyUsedApps[i].exec === execProp) { existingIndex = i break } } if (existingIndex >= 0) { - recentlyUsedApps.splice(existingIndex, 1) + // App exists, increment usage count + recentlyUsedApps[existingIndex].usageCount = (recentlyUsedApps[existingIndex].usageCount || 1) + 1 + recentlyUsedApps[existingIndex].lastUsed = Date.now() + } else { + // New app, create entry + var appData = { + name: app.name || "", + exec: execProp, + icon: app.icon || "application-x-executable", + comment: app.comment || "", + usageCount: 1, + lastUsed: Date.now() + } + recentlyUsedApps.push(appData) } - recentlyUsedApps.unshift(app) + // Sort by usage count (descending), then alphabetically by name + var sortedApps = recentlyUsedApps.sort(function(a, b) { + if (a.usageCount !== b.usageCount) { + return b.usageCount - a.usageCount // Higher usage count first + } + return a.name.localeCompare(b.name) // Alphabetical tiebreaker + }) - if (recentlyUsedApps.length > 10) { - recentlyUsedApps = recentlyUsedApps.slice(0, 10) + // Limit to 5 apps + if (sortedApps.length > 5) { + sortedApps = sortedApps.slice(0, 5) } + // Reassign to trigger property change signal + recentlyUsedApps = sortedApps + saveSettings() } diff --git a/Services/PreferencesService.qml b/Services/PreferencesService.qml index 3ee1b000..386ae118 100644 --- a/Services/PreferencesService.qml +++ b/Services/PreferencesService.qml @@ -50,7 +50,20 @@ Singleton { } function saveRecentApps() { - recentAppsFileView.text = JSON.stringify(recentApps, null, 2) + var jsonData = JSON.stringify(recentApps, null, 2) + var process = Qt.createQmlObject(' + import Quickshell.Io + Process { + command: ["sh", "-c", "echo \'' + jsonData.replace(/'/g, "'\"'\"'") + '\' > \'' + root.recentAppsFile + '\'"] + running: true + onExited: { + if (exitCode !== 0) { + console.warn("Failed to save recent apps:", exitCode) + } + destroy() + } + } + ', root) } function addRecentApp(app) { diff --git a/Widgets/AppLauncher.qml b/Widgets/AppLauncher.qml index 2596187d..233925d3 100644 --- a/Widgets/AppLauncher.qml +++ b/Widgets/AppLauncher.qml @@ -33,7 +33,7 @@ PanelWindow { // App management property var categories: AppSearchService.getAllCategories() property string selectedCategory: "All" - property var recentApps: [] + property var recentApps: Prefs.getRecentApps() property var pinnedApps: ["firefox", "code", "terminal", "file-manager"] property bool showCategories: false property string viewMode: "list" // "list" or "grid" @@ -84,6 +84,13 @@ PanelWindow { } } + Connections { + target: Prefs + function onRecentlyUsedAppsChanged() { + recentApps = Prefs.getRecentApps() + } + } + function updateFilteredModel() { filteredModel.clear() @@ -383,6 +390,7 @@ PanelWindow { if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && filteredModel.count) { var firstApp = filteredModel.get(0) if (firstApp.desktopEntry) { + Prefs.addRecentApp(firstApp.desktopEntry) AppSearchService.launchApp(firstApp.desktopEntry) } else { launcher.launchApp(firstApp.exec) @@ -398,6 +406,69 @@ PanelWindow { } } + // Recent apps section + Column { + width: parent.width + spacing: Theme.spacingS + visible: recentApps.length > 0 && searchField.text.length === 0 + + Text { + text: "Recently Used" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + Row { + width: parent.width + spacing: Theme.spacingM + + Repeater { + model: Math.min(recentApps.length, 5) + + Rectangle { + width: 56 + height: 56 + radius: Theme.cornerRadius + color: recentAppMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) + border.width: 1 + + IconImage { + anchors.fill: parent + anchors.margins: 8 + source: recentApps[index] ? Quickshell.iconPath(recentApps[index].icon, "") : "" + smooth: true + asynchronous: true + } + + MouseArea { + id: recentAppMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (recentApps[index]) { + var recentApp = recentApps[index] + // Find the desktop entry for this recent app + var foundApp = AppSearchService.getAppByExec(recentApp.exec) + if (foundApp) { + Prefs.addRecentApp(foundApp) + AppSearchService.launchApp(foundApp) + } else { + // Fallback to direct execution + var cleanExec = recentApp.exec.replace(/%[fFuU]/g, "").trim() + Quickshell.execDetached(["sh", "-c", cleanExec]) + } + launcher.hide() + } + } + } + } + } + } + } + // Category filter and view mode controls Row { width: parent.width @@ -521,6 +592,7 @@ PanelWindow { // Calculate more precise remaining height let usedHeight = 40 + Theme.spacingL // Header usedHeight += 52 + Theme.spacingL // Search container + usedHeight += (recentApps.length > 0 && searchField.text.length === 0 ? 56 + Theme.spacingS + Theme.spacingL : 0) // Recent apps when visible usedHeight += (searchField.text.length === 0 ? 40 + Theme.spacingL : 0) // Category/controls when visible return parent.height - usedHeight } @@ -784,6 +856,7 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { if (model.desktopEntry) { + Prefs.addRecentApp(model.desktopEntry) AppSearchService.launchApp(model.desktopEntry) } else { launcher.launchApp(model.exec) @@ -845,6 +918,7 @@ PanelWindow { cursorShape: Qt.PointingHandCursor onClicked: { if (model.desktopEntry) { + Prefs.addRecentApp(model.desktopEntry) AppSearchService.launchApp(model.desktopEntry) } else { launcher.launchApp(model.exec) @@ -870,6 +944,7 @@ PanelWindow { function show() { launcher.isVisible = true + recentApps = Prefs.getRecentApps() // Refresh recent apps Qt.callLater(function() { searchField.forceActiveFocus() }) @@ -894,5 +969,6 @@ PanelWindow { categories = AppSearchService.getAllCategories() updateFilteredModel() } + recentApps = Prefs.getRecentApps() // Load recent apps on startup } } \ No newline at end of file diff --git a/Widgets/SpotlightLauncher.qml b/Widgets/SpotlightLauncher.qml index 72169d9f..0580ee3c 100644 --- a/Widgets/SpotlightLauncher.qml +++ b/Widgets/SpotlightLauncher.qml @@ -70,7 +70,7 @@ PanelWindow { } function loadRecentApps() { - recentApps = PreferencesService.getRecentApps() + recentApps = Prefs.getRecentApps() } function updateFilteredApps() { @@ -154,7 +154,7 @@ PanelWindow { } function launchApp(app) { - PreferencesService.addRecentApp(app) + Prefs.addRecentApp(app) if (app.desktopEntry) { AppSearchService.launchApp(app.desktopEntry) } else { @@ -221,6 +221,13 @@ PanelWindow { } } + Connections { + target: Prefs + function onRecentlyUsedAppsChanged() { + recentApps = Prefs.getRecentApps() + } + } + // Dimmed overlay background Rectangle { anchors.fill: parent @@ -236,13 +243,18 @@ PanelWindow { width: 600 height: { // Calculate dynamic height based on content - let baseHeight = Theme.spacingXL * 2 + Theme.spacingL * 3 // Margins and spacing + let baseHeight = Theme.spacingXL * 2 + Theme.spacingL * 4 // Margins and spacing // Add category section height if visible if (categories.length > 1 || filteredModel.count > 0) { baseHeight += 36 * 2 + Theme.spacingS + Theme.spacingM // Categories (2 rows) } + // Add recent apps section height if visible + if (recentApps.length > 0 && searchField.text.length === 0) { + baseHeight += 56 + Theme.spacingS + Theme.fontSizeMedium + Theme.spacingL // Recent apps + } + // Add search field height baseHeight += 56 @@ -367,6 +379,73 @@ PanelWindow { } } + // Recent apps section + Column { + width: parent.width + spacing: Theme.spacingS + visible: recentApps.length > 0 && searchField.text.length === 0 + + Text { + text: "Recently Used" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM + + Repeater { + model: Math.min(recentApps.length, 5) + + Rectangle { + width: 56 + height: 56 + radius: Theme.cornerRadius + color: recentSpotlightMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) + border.width: 1 + + IconImage { + anchors.fill: parent + anchors.margins: 8 + source: recentApps[index] ? Quickshell.iconPath(recentApps[index].icon, "") : "" + smooth: true + asynchronous: true + } + + MouseArea { + id: recentSpotlightMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (recentApps[index]) { + var recentApp = recentApps[index] + // Find the desktop entry for this recent app + var foundApp = AppSearchService.getAppByExec(recentApp.exec) + if (foundApp) { + launchApp({ + name: foundApp.name, + exec: foundApp.execString, + icon: foundApp.icon, + comment: foundApp.comment, + desktopEntry: foundApp + }) + } else { + // Fallback to direct execution + launchApp(recentApp) + } + } + } + } + } + } + } + } + // Search field with view toggle buttons Row { width: parent.width @@ -531,7 +610,7 @@ PanelWindow { } delegate: Rectangle { - width: parent.width + width: resultsList.width height: 60 radius: Theme.cornerRadius color: ListView.isCurrentItem ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : @@ -730,5 +809,6 @@ PanelWindow { if (AppSearchService.ready) { categories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science") } + loadRecentApps() // Load recent apps on startup } } \ No newline at end of file