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
|
|
@ -55,7 +55,6 @@ in
|
||||||
networkmanagerapplet
|
networkmanagerapplet
|
||||||
pavucontrol
|
pavucontrol
|
||||||
polkit_gnome
|
polkit_gnome
|
||||||
anyrun
|
|
||||||
zenity
|
zenity
|
||||||
libcanberra-gtk3
|
libcanberra-gtk3
|
||||||
];
|
];
|
||||||
|
|
@ -167,30 +166,6 @@ in
|
||||||
|
|
||||||
extraConfig =
|
extraConfig =
|
||||||
let
|
let
|
||||||
powerMenu = pkgs.writeShellScript "power-menu" ''
|
|
||||||
# Stop the daemon so standalone stdin mode can run cleanly.
|
|
||||||
# systemd restarts it automatically afterwards (Restart=on-failure).
|
|
||||||
systemctl --user stop anyrun.service 2>/dev/null || true
|
|
||||||
choice=$(printf '%s\n' \
|
|
||||||
$'\uf023 Lock' \
|
|
||||||
$'\uf08b Logout' \
|
|
||||||
$'\uf01e Reboot' \
|
|
||||||
$'\uf011 Shutdown' \
|
|
||||||
| ${pkgs.anyrun}/bin/anyrun \
|
|
||||||
--plugins "${pkgs.anyrun}/lib/libstdin.so" \
|
|
||||||
--show-results-immediately true \
|
|
||||||
--hide-plugin-info true \
|
|
||||||
--close-on-click true)
|
|
||||||
# Restart the daemon service (reset-failed clears the start-rate limiter).
|
|
||||||
systemctl --user reset-failed anyrun.service 2>/dev/null
|
|
||||||
systemctl --user start anyrun.service 2>/dev/null
|
|
||||||
case "$choice" in
|
|
||||||
*Lock) ${pkgs.hyprlock}/bin/hyprlock ;;
|
|
||||||
*Logout) hyprctl dispatch exit ;;
|
|
||||||
*Reboot) systemctl reboot ;;
|
|
||||||
*Shutdown) systemctl poweroff ;;
|
|
||||||
esac
|
|
||||||
'';
|
|
||||||
kbdBrightUp = pkgs.writeShellScript "kbd-bright-up" ''
|
kbdBrightUp = pkgs.writeShellScript "kbd-bright-up" ''
|
||||||
${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight set +10%
|
${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight set +10%
|
||||||
brightness=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight get)
|
brightness=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight get)
|
||||||
|
|
@ -260,7 +235,7 @@ in
|
||||||
-- Apps
|
-- Apps
|
||||||
hl.bind(mod .. " + T", hl.dsp.exec_cmd("ghostty"))
|
hl.bind(mod .. " + T", hl.dsp.exec_cmd("ghostty"))
|
||||||
hl.bind(mod .. " + E", hl.dsp.exec_cmd("nemo"))
|
hl.bind(mod .. " + E", hl.dsp.exec_cmd("nemo"))
|
||||||
hl.bind(mod .. " + R", hl.dsp.exec_cmd("hyprctl layers -j | grep -q anyrun && anyrun close || anyrun"))
|
hl.bind(mod .. " + R", hl.dsp.exec_cmd("${pkgs.quickshell}/bin/qs ipc call launcher toggle"))
|
||||||
hl.bind(mod .. " + Q", hl.dsp.window.close())
|
hl.bind(mod .. " + Q", hl.dsp.window.close())
|
||||||
hl.bind(mod .. " + SHIFT + E", hl.dsp.exit())
|
hl.bind(mod .. " + SHIFT + E", hl.dsp.exit())
|
||||||
|
|
||||||
|
|
@ -279,8 +254,8 @@ in
|
||||||
hl.bind(mod .. " + K", hl.dsp.focus({ direction = "up" }))
|
hl.bind(mod .. " + K", hl.dsp.focus({ direction = "up" }))
|
||||||
hl.bind(mod .. " + J", hl.dsp.focus({ direction = "down" }))
|
hl.bind(mod .. " + J", hl.dsp.focus({ direction = "down" }))
|
||||||
|
|
||||||
-- Power menu — dismiss launcher if open, then show menu
|
-- Power menu (quickshell launcher in power mode)
|
||||||
hl.bind(mod .. " + L", hl.dsp.exec_cmd("anyrun close 2>/dev/null; ${powerMenu}"))
|
hl.bind(mod .. " + L", hl.dsp.exec_cmd("${pkgs.quickshell}/bin/qs ipc call launcher powermenu"))
|
||||||
|
|
||||||
-- Move windows
|
-- Move windows
|
||||||
hl.bind(mod .. " + SHIFT + left", hl.dsp.window.move({ direction = "left" }))
|
hl.bind(mod .. " + SHIFT + left", hl.dsp.window.move({ direction = "left" }))
|
||||||
|
|
@ -407,83 +382,6 @@ in
|
||||||
# Hyprland session so they don't crash-loop in a GNOME session.
|
# Hyprland session so they don't crash-loop in a GNOME session.
|
||||||
wayland.systemd.target = "hyprland-session.target";
|
wayland.systemd.target = "hyprland-session.target";
|
||||||
|
|
||||||
systemd.user.services.anyrun = {
|
|
||||||
Unit = {
|
|
||||||
Description = "Anyrun launcher daemon";
|
|
||||||
PartOf = [ "graphical-session.target" ];
|
|
||||||
After = [ "graphical-session.target" ];
|
|
||||||
};
|
|
||||||
Service = {
|
|
||||||
ExecStart = "${pkgs.anyrun}/bin/anyrun daemon";
|
|
||||||
Restart = "on-failure";
|
|
||||||
RestartSec = 2;
|
|
||||||
};
|
|
||||||
Install.WantedBy = [ "hyprland-session.target" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
xdg.configFile = {
|
|
||||||
# anyrun config — written manually since HM 26.05 has no anyrun module.
|
|
||||||
"anyrun/config.ron".text = ''
|
|
||||||
Config(
|
|
||||||
x: Fraction(0.5),
|
|
||||||
y: Fraction(0.25),
|
|
||||||
width: Absolute(350),
|
|
||||||
height: Absolute(0),
|
|
||||||
hide_icons: false,
|
|
||||||
ignore_exclusive_zones: false,
|
|
||||||
layer: Overlay,
|
|
||||||
hide_plugin_info: true,
|
|
||||||
close_on_click: true,
|
|
||||||
max_entries: Some(8),
|
|
||||||
plugins: [
|
|
||||||
"${pkgs.anyrun}/lib/libapplications.so",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
"anyrun/style.css".text = ''
|
|
||||||
* { all: unset; font-family: "FiraMono Nerd Font", monospace; font-size: 13px; }
|
|
||||||
window { background: transparent; }
|
|
||||||
box.main {
|
|
||||||
background: #${c.base00};
|
|
||||||
border: 1px solid #${c.base03};
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
margin: 16px;
|
|
||||||
}
|
|
||||||
text {
|
|
||||||
background: #${c.base01};
|
|
||||||
color: #${c.base05};
|
|
||||||
caret-color: #${c.base0D};
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
list.plugin { background: transparent; }
|
|
||||||
.matches { background: transparent; }
|
|
||||||
.match {
|
|
||||||
padding: 4px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #${c.base05};
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.match:selected {
|
|
||||||
background: #${c.base02};
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
label.match.description { color: #${c.base04}; font-size: 11px; }
|
|
||||||
'';
|
|
||||||
"anyrun/applications.ron".text = ''
|
|
||||||
(
|
|
||||||
desktop_actions: false,
|
|
||||||
max_entries: 8,
|
|
||||||
terminal: Some((
|
|
||||||
command: "ghostty",
|
|
||||||
args: "-e {}",
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ in
|
||||||
singleton Theme 1.0 Theme.qml
|
singleton Theme 1.0 Theme.qml
|
||||||
singleton Commands 1.0 Commands.qml
|
singleton Commands 1.0 Commands.qml
|
||||||
Bar 1.0 Bar.qml
|
Bar 1.0 Bar.qml
|
||||||
|
Launcher 1.0 Launcher.qml
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -87,6 +88,8 @@ in
|
||||||
readonly property string wifiConnect: "${wifiConnectScript}"
|
readonly property string wifiConnect: "${wifiConnectScript}"
|
||||||
readonly property string powerprofilesctl: "${powerprofilesctl}"
|
readonly property string powerprofilesctl: "${powerprofilesctl}"
|
||||||
readonly property string notifSound: "${pkgs.libcanberra-gtk3}/bin/canberra-gtk-play"
|
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 = ''
|
text = ''
|
||||||
//@ pragma UseQApplication
|
//@ pragma UseQApplication
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Notifications
|
import Quickshell.Services.Notifications
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
|
|
@ -104,6 +108,17 @@ in
|
||||||
property var latestNotification: null
|
property var latestNotification: null
|
||||||
signal notificationReceived()
|
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 {
|
NotificationServer {
|
||||||
id: _notifServer
|
id: _notifServer
|
||||||
bodySupported: true
|
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" = {
|
"quickshell/Bar.qml" = {
|
||||||
onChange = qsRestart;
|
onChange = qsRestart;
|
||||||
text = ''
|
text = ''
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue