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"); } }