feat: app launcher

This commit is contained in:
2026-02-05 18:43:54 +02:00
parent 887b477c5e
commit 65b87cb9c6
6 changed files with 440 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import "Windows/QuickSettings" import "Windows/QuickSettings"
import "Windows/AppLauncher"
PanelWindow { PanelWindow {
id: overlay id: overlay
@@ -70,4 +71,9 @@ PanelWindow {
id: quickSettingsWindow id: quickSettingsWindow
manager: ShellStateManager manager: ShellStateManager
} }
AppLauncher {
id: appLauncherWindow
manager: ShellStateManager
}
} }

View File

@@ -30,7 +30,11 @@ QtObject {
if (!ShellStateManager.shellOpen) if (!ShellStateManager.shellOpen)
return false; return false;
var context = ShellStateManager.quickSettingsOpen ? "quickSettings" : "topbar"; var context = "topbar";
if (ShellStateManager.appLauncherOpen)
context = "appLauncher";
else if (ShellStateManager.quickSettingsOpen)
context = "quickSettings";
var handler = handlers[context]; var handler = handlers[context];
if (handler) { if (handler) {
var handled = handler(key); var handled = handler(key);
@@ -39,7 +43,9 @@ QtObject {
} }
if (key === Qt.Key_Escape || key === Qt.Key_Shift || key === Qt.Key_X) { if (key === Qt.Key_Escape || key === Qt.Key_Shift || key === Qt.Key_X) {
if (context === "quickSettings") { if (context === "appLauncher") {
ShellStateManager.closeAppLauncher();
} else if (context === "quickSettings") {
ShellStateManager.closeQuickSettings(); ShellStateManager.closeQuickSettings();
} else if (context === "topbar") { } else if (context === "topbar") {
ShellStateManager.closeShell(); ShellStateManager.closeShell();

View File

@@ -1,12 +1,21 @@
pragma Singleton pragma Singleton
import QtQuick import QtQuick
import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Io
QtObject { QtObject {
id: manager id: manager
property bool shellOpen: false property bool shellOpen: false
property bool quickSettingsOpen: false property bool quickSettingsOpen: false
property bool appLauncherOpen: false
property var appUsageCounts: ({})
property string appUsagePath: {
var homeDir = Quickshell.env("HOME");
return homeDir ? homeDir + "/.local/share/deltarunequickshell/apps.json" : "";
}
property bool appUsageLoaded: false
property var windowRequests: ({}) property var windowRequests: ({})
property var quickSettingsPayload: ({}) property var quickSettingsPayload: ({})
property var globals: ({}) property var globals: ({})
@@ -15,6 +24,8 @@ QtObject {
signal shellClosed signal shellClosed
signal quickSettingsOpened signal quickSettingsOpened
signal quickSettingsClosed signal quickSettingsClosed
signal appLauncherOpened
signal appLauncherClosed
signal windowRequested(string name, var payload) signal windowRequested(string name, var payload)
function openShell() { function openShell() {
@@ -33,6 +44,9 @@ QtObject {
if (quickSettingsOpen) { if (quickSettingsOpen) {
closeQuickSettings(); closeQuickSettings();
} }
if (appLauncherOpen) {
closeAppLauncher();
}
shellOpen = false; shellOpen = false;
shellClosed(); shellClosed();
} }
@@ -55,6 +69,9 @@ QtObject {
var normalizedPayload = payload || ({}); var normalizedPayload = payload || ({});
quickSettingsPayload = normalizedPayload; quickSettingsPayload = normalizedPayload;
console.log("ShellStateManager: openQuickSettings", normalizedPayload); console.log("ShellStateManager: openQuickSettings", normalizedPayload);
if (appLauncherOpen) {
closeAppLauncher();
}
if (!quickSettingsOpen) { if (!quickSettingsOpen) {
quickSettingsOpen = true; quickSettingsOpen = true;
quickSettingsOpened(); quickSettingsOpened();
@@ -74,6 +91,72 @@ QtObject {
quickSettingsOpen ? closeQuickSettings() : openQuickSettings(payload); quickSettingsOpen ? closeQuickSettings() : openQuickSettings(payload);
} }
function openAppLauncher(payload) {
var normalizedPayload = payload || ({});
console.log("ShellStateManager: openAppLauncher", normalizedPayload);
if (quickSettingsOpen) {
closeQuickSettings();
}
if (!appLauncherOpen) {
appLauncherOpen = true;
appLauncherOpened();
}
requestWindow("appLauncher", normalizedPayload);
}
function closeAppLauncher() {
if (appLauncherOpen) {
console.log("ShellStateManager: closeAppLauncher");
appLauncherOpen = false;
appLauncherClosed();
}
}
function toggleAppLauncher(payload) {
appLauncherOpen ? closeAppLauncher() : openAppLauncher(payload);
}
function bumpAppUsage(id) {
if (!id)
return;
var updated = Object.assign({}, appUsageCounts);
updated[id] = (updated[id] || 0) + 1;
appUsageCounts = updated;
}
function persistAppUsage() {
if (!appUsageLoaded)
return;
if (!appUsageFile || !appUsageAdapter)
return;
appUsageAdapter.counts = appUsageCounts || ({});
appUsageFile.writeAdapter();
}
onAppUsageCountsChanged: persistAppUsage()
property FileView appUsageFile: FileView {
id: appUsageFile
path: manager.appUsagePath
watchChanges: true
onFileChanged: reload()
onLoaded: {
if (appUsageAdapter) {
manager.appUsageCounts = appUsageAdapter.counts || ({});
}
manager.appUsageLoaded = true;
}
onLoadFailed: {
manager.appUsageLoaded = true;
manager.persistAppUsage();
}
JsonAdapter {
id: appUsageAdapter
property var counts: ({})
}
}
function setGlobal(key, value) { function setGlobal(key, value) {
globals[key] = value; globals[key] = value;
} }

View File

@@ -49,6 +49,12 @@ Item {
}); });
return true; return true;
} }
if (iconSources[selectedIndex] === "item.png") {
ShellStateManager.openAppLauncher({
source: "Topbar"
});
return true;
}
} }
return false; return false;
} }
@@ -84,7 +90,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
selected: topbar.selectedIndex == repeatitem.index selected: topbar.selectedIndex == repeatitem.index
iconSource: topbar.iconSources[repeatitem.index] iconSource: topbar.iconSources[repeatitem.index]
showSoul: !topbar.manager.quickSettingsOpen showSoul: !topbar.manager.quickSettingsOpen && !topbar.manager.appLauncherOpen
} }
MouseArea { MouseArea {
@@ -96,6 +102,11 @@ Item {
source: "Topbar" source: "Topbar"
}); });
} }
if (topbar.iconSources[repeatitem.index] === "item.png") {
ShellStateManager.openAppLauncher({
source: "Topbar"
});
}
} }
} }
} }

View File

@@ -0,0 +1,44 @@
import QtQuick
import "../.."
import "../../Window" as ShellWindow
ShellWindow.Window {
id: appLauncherWindow
property var manager: ShellStateManager
property var appLauncherApp: null
width: 1217 + 52
height: 767 + 52
visible: manager ? manager.appLauncherOpen : false
anchors.centerIn: parent
Loader {
id: appLauncherLoader
anchors.fill: parent
asynchronous: true
source: "./AppLauncherApp.qml"
onLoaded: {
appLauncherApp = item;
if (appLauncherApp)
appLauncherApp.manager = manager;
}
}
onManagerChanged: {
if (appLauncherApp)
appLauncherApp.manager = manager;
}
QtObject {
id: appLauncherKeyHandler
function handle(event) {
if (appLauncherApp && appLauncherApp.handleKey)
return appLauncherApp.handleKey(event.key);
return false;
}
Component.onCompleted: ShellInputManager.registerHandler("appLauncher", handle)
Component.onDestruction: ShellInputManager.unregisterHandler("appLauncher")
}
}

View File

@@ -0,0 +1,287 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import "../.."
Item {
id: root
width: parent ? parent.width : 1280
height: parent ? parent.height : 820
focus: true
/* ------------------------------
PIXEL CONSTANTS (DO NOT TOUCH)
------------------------------ */
property int menuLeft: 64
property int menuTop: 140
property int lineHeight: 38 + 40 + 1
property int nameFontSize: 32
property int stateFontSize: 28
property int stateColumnX: 824
property int soulOffsetX: -36 - 32
property int soulOffsetY: -26
/* ------------------------------ */
property int iconSize: 36
property int textStartX: 239
property int textRightPadding: 96
property int visibleRows: Math.max(1, Math.floor((height - menuTop - 64) / lineHeight))
property int scrollOffset: 0
property ShellStateManager manager: null
property int activeSelection: 0
property bool isSelected: true
ListModel {
id: appEntriesModel
}
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
function menuLength() {
return appEntriesModel.count;
}
function menuAt(index) {
if (index < 0 || index >= appEntriesModel.count)
return null;
var row = appEntriesModel.get(index);
return row ? row.entry : null;
}
function wrapIndex(i) {
const length = menuLength();
if (length === 0)
return 0;
return (i + length) % length;
}
function clampSelection() {
const length = menuLength();
if (length === 0) {
activeSelection = 0;
return;
}
activeSelection = clamp(activeSelection, 0, length - 1);
}
function ensureVisible() {
const length = menuLength();
if (length === 0)
return;
if (activeSelection < scrollOffset)
scrollOffset = activeSelection;
else if (activeSelection >= scrollOffset + visibleRows)
scrollOffset = Math.max(0, activeSelection - visibleRows + 1);
}
function appName(entry) {
if (!entry)
return "";
if (entry.name && entry.name.length > 0)
return entry.name;
return entry.id || "";
}
function appUsageCount(entry) {
if (!entry || !manager || !manager.appUsageCounts)
return 0;
var key = entry.id || entry.name;
return manager.appUsageCounts[key] || 0;
}
function applicationsToArray() {
var apps = DesktopEntries.applications;
var list = [];
if (!apps)
return list;
if (apps.values && apps.values.length !== undefined) {
for (var i = 0; i < apps.values.length; i++) {
list.push(apps.values[i]);
}
} else if (apps.length !== undefined) {
for (var j = 0; j < apps.length; j++) {
list.push(apps[j]);
}
} else if (apps.count !== undefined && apps.get) {
for (var k = 0; k < apps.count; k++) {
list.push(apps.get(k));
}
}
return list;
}
function rebuildAppEntries() {
var list = applicationsToArray();
console.log("AppLauncher: rebuilding entries", list.length);
var filtered = [];
for (var i = 0; i < list.length; i++) {
var entry = list[i];
if (!entry)
continue;
if (entry.noDisplay)
continue;
filtered.push(entry);
}
console.log("AppLauncher: filtered entries", filtered.length);
filtered.sort(function (a, b) {
var usageA = appUsageCount(a);
var usageB = appUsageCount(b);
if (usageA !== usageB)
return usageB - usageA;
var nameA = appName(a);
var nameB = appName(b);
return nameA.localeCompare(nameB);
});
appEntriesModel.clear();
for (var i = 0; i < filtered.length; i++) {
appEntriesModel.append({
entry: filtered[i]
});
}
clampSelection();
ensureVisible();
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
rebuildAppEntries();
}
}
Connections {
target: manager
ignoreUnknownSignals: true
function onAppUsageCountsChanged() {
rebuildAppEntries();
}
function onAppLauncherOpened() {
activeSelection = 0;
isSelected = true;
clampSelection();
}
}
Text {
text: "APPS"
font.family: "8bitoperator JVE"
font.pixelSize: 71
renderType: Text.NativeRendering
font.hintingPreference: Font.PreferNoHinting
smooth: false
antialiasing: false
anchors.horizontalCenter: parent.horizontalCenter
color: "#ffffff"
y: 32
}
Repeater {
model: appEntriesModel
delegate: Item {
width: root.width
height: lineHeight
x: 0
y: menuTop + (index - root.scrollOffset) * lineHeight
visible: index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows
Text {
x: root.textStartX
y: 0
text: root.appName(model.entry)
width: root.width - root.textStartX - root.textRightPadding
font.family: "8bitoperator JVE"
font.pixelSize: 71
font.letterSpacing: 1
renderType: Text.NativeRendering
font.hintingPreference: Font.PreferNoHinting
smooth: false
antialiasing: false
wrapMode: Text.NoWrap
elide: Text.ElideRight
color: (root.activeSelection == index && root.isSelected == true) ? "#fefe00" : "#ffffff"
}
IconImage {
id: appIcon
x: 182
y: 8 + 14
implicitSize: root.iconSize
source: Quickshell.iconPath(model.entry && model.entry.icon ? model.entry.icon : "")
asynchronous: true
}
Image {
source: "../QuickSettings/soul.png"
width: 36
height: 36
x: appIcon.x
y: appIcon.y
opacity: 0.7
visible: root.activeSelection == index
}
}
}
/* ------------------------------
INPUT HANDLING
------------------------------ */
function handleKey(key) {
switch (key) {
case Qt.Key_Up:
activeSelection = wrapIndex(activeSelection - 1);
ensureVisible();
return true;
case Qt.Key_Down:
activeSelection = wrapIndex(activeSelection + 1);
ensureVisible();
return true;
case Qt.Key_Z:
case Qt.Key_Return:
case Qt.Key_Enter:
{
var entry = menuAt(activeSelection);
if (entry) {
if (manager && manager.bumpAppUsage) {
manager.bumpAppUsage(entry.id || entry.name);
}
entry.execute();
}
if (manager && manager.closeShell) {
manager.closeShell();
}
return true;
}
case Qt.Key_X:
case Qt.Key_Shift:
case Qt.Key_Escape:
if (manager && manager.closeAppLauncher) {
manager.closeAppLauncher();
}
return true;
}
return false;
}
Component.onCompleted: {
activeSelection = 0;
isSelected = true;
scrollOffset = 0;
rebuildAppEntries();
Qt.callLater(rebuildAppEntries);
ShellInputManager.registerHandler("appLauncher", handleKey);
}
Component.onDestruction: {
ShellInputManager.unregisterHandler("appLauncher");
}
}