quickshell: native launcher + power menu, drop anyrun
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9671dfb793
commit
77fca92c5c
2 changed files with 240 additions and 105 deletions
|
|
@ -49,6 +49,7 @@ in
|
|||
singleton Theme 1.0 Theme.qml
|
||||
singleton Commands 1.0 Commands.qml
|
||||
Bar 1.0 Bar.qml
|
||||
Launcher 1.0 Launcher.qml
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
@ -87,6 +88,8 @@ in
|
|||
readonly property string wifiConnect: "${wifiConnectScript}"
|
||||
readonly property string powerprofilesctl: "${powerprofilesctl}"
|
||||
readonly property string notifSound: "${pkgs.libcanberra-gtk3}/bin/canberra-gtk-play"
|
||||
readonly property string hyprlock: "${pkgs.hyprlock}/bin/hyprlock"
|
||||
readonly property string systemctl: "${pkgs.systemd}/bin/systemctl"
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
|
@ -96,6 +99,7 @@ in
|
|||
text = ''
|
||||
//@ pragma UseQApplication
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
|
||||
|
|
@ -104,6 +108,17 @@ in
|
|||
property var latestNotification: null
|
||||
signal notificationReceived()
|
||||
|
||||
Launcher {
|
||||
id: launcher
|
||||
}
|
||||
|
||||
// Bound in hyprland.nix: Super+R → toggle, Super+L → powermenu
|
||||
IpcHandler {
|
||||
target: "launcher"
|
||||
function toggle(): void { launcher.toggleMode("apps"); }
|
||||
function powermenu(): void { launcher.toggleMode("power"); }
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: _notifServer
|
||||
bodySupported: true
|
||||
|
|
@ -130,6 +145,228 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
# App launcher + power menu (replaces anyrun). Full-screen transparent
|
||||
# overlay with exclusive keyboard focus while open; Esc / click-outside
|
||||
# closes. Apps come from Quickshell's DesktopEntries service.
|
||||
"quickshell/Launcher.qml" = {
|
||||
onChange = qsRestart;
|
||||
text = ''
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
// "apps" (Super+R) or "power" (Super+L)
|
||||
property string mode: "apps"
|
||||
|
||||
visible: false
|
||||
screen: Quickshell.screens[0]
|
||||
WlrLayershell.namespace: "quickshell-launcher"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
function toggleMode(m) {
|
||||
if (visible && mode === m) { close(); return; }
|
||||
mode = m;
|
||||
search.text = "";
|
||||
list.currentIndex = 0;
|
||||
visible = true;
|
||||
search.forceActiveFocus();
|
||||
}
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
// Power actions go through Hyprland's exec dispatcher so they
|
||||
// are NOT children of quickshell — a quickshell restart must
|
||||
// never kill a running hyprlock.
|
||||
readonly property var powerActions: [
|
||||
{ name: "Lock", glyph: "", dispatch: "exec " + Commands.hyprlock },
|
||||
{ name: "Logout", glyph: "", dispatch: "exit" },
|
||||
{ name: "Reboot", glyph: "", dispatch: "exec " + Commands.systemctl + " reboot" },
|
||||
{ name: "Shutdown", glyph: "", dispatch: "exec " + Commands.systemctl + " poweroff" }
|
||||
]
|
||||
|
||||
function score(name, extra, q) {
|
||||
let n = name.toLowerCase();
|
||||
if (n.startsWith(q)) return 3;
|
||||
if (n.includes(" " + q)) return 2;
|
||||
if (n.includes(q)) return 1;
|
||||
if (extra && extra.toLowerCase().includes(q)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
property var entries: {
|
||||
let q = search.text.toLowerCase().trim();
|
||||
if (mode === "power") {
|
||||
return powerActions.filter(a => q === "" || score(a.name, "", q) > 0);
|
||||
}
|
||||
let apps = DesktopEntries.applications.values.filter(a => !a.noDisplay);
|
||||
if (q === "") {
|
||||
apps.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return apps.slice(0, 8);
|
||||
}
|
||||
let scored = [];
|
||||
for (let i = 0; i < apps.length; i++) {
|
||||
let s = score(apps[i].name, apps[i].genericName + " " + apps[i].comment, q);
|
||||
if (s > 0) scored.push({ app: apps[i], s: s });
|
||||
}
|
||||
scored.sort((a, b) => b.s - a.s || a.app.name.localeCompare(b.app.name));
|
||||
return scored.slice(0, 8).map(x => x.app);
|
||||
}
|
||||
|
||||
function activate(item) {
|
||||
if (!item) return;
|
||||
if (mode === "power") {
|
||||
Hyprland.dispatch(item.dispatch);
|
||||
} else {
|
||||
item.execute();
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
// Click outside the box closes
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: box
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round(parent.height * 0.25)
|
||||
width: 350
|
||||
height: col.height + 16
|
||||
radius: 10
|
||||
color: Theme.base00
|
||||
border.width: 1
|
||||
border.color: Theme.base03
|
||||
|
||||
// Swallow clicks inside the box
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
Column {
|
||||
id: col
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 8
|
||||
spacing: 6
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 36
|
||||
radius: 6
|
||||
color: Theme.base01
|
||||
|
||||
TextInput {
|
||||
id: search
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
color: Theme.base05
|
||||
font.family: "FiraMono Nerd Font"
|
||||
font.pixelSize: 13
|
||||
clip: true
|
||||
onTextChanged: list.currentIndex = 0
|
||||
|
||||
Keys.onEscapePressed: root.close()
|
||||
Keys.onUpPressed: list.currentIndex = Math.max(0, list.currentIndex - 1)
|
||||
Keys.onDownPressed: list.currentIndex = Math.min(root.entries.length - 1, list.currentIndex + 1)
|
||||
Keys.onReturnPressed: root.activate(root.entries[list.currentIndex])
|
||||
Keys.onEnterPressed: root.activate(root.entries[list.currentIndex])
|
||||
Keys.onTabPressed: list.currentIndex = (list.currentIndex + 1) % Math.max(1, root.entries.length)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: search
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: search.text === ""
|
||||
text: root.mode === "power" ? "Power" : "Search"
|
||||
color: Theme.base03
|
||||
font.family: "FiraMono Nerd Font"
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
width: parent.width
|
||||
height: contentHeight
|
||||
interactive: false
|
||||
model: root.entries
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: list.width
|
||||
height: 32
|
||||
radius: 6
|
||||
color: list.currentIndex === index ? Theme.base02 : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
spacing: 10
|
||||
|
||||
Image {
|
||||
visible: root.mode === "apps" && source != ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 18
|
||||
height: 18
|
||||
sourceSize.width: 18
|
||||
sourceSize.height: 18
|
||||
source: root.mode === "apps" ? Quickshell.iconPath(modelData.icon, true) : ""
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.mode === "power"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.mode === "power" ? modelData.glyph : ""
|
||||
color: Theme.base0D
|
||||
font.family: "FiraMono Nerd Font"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.name
|
||||
color: Theme.base05
|
||||
font.family: "FiraMono Nerd Font"
|
||||
font.pixelSize: 13
|
||||
elide: Text.ElideRight
|
||||
width: 270
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: list.currentIndex = index
|
||||
onClicked: root.activate(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
"quickshell/Bar.qml" = {
|
||||
onChange = qsRestart;
|
||||
text = ''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue