From fdc00a85746924930d5db1cf4c47262fb97a10f9 Mon Sep 17 00:00:00 2001 From: luna Date: Fri, 28 Aug 2026 07:35:06 +0000 Subject: [PATCH] [verified] feat(quickshell): add dense application launcher --- dotfiles/quickshell/CLAUDE.md | 2 +- dotfiles/quickshell/open_launcher.sh | 2 +- dotfiles/quickshell/shell.qml | 4 +- .../tests/ApplicationLauncherHeadless.qml | 62 ++ .../widgets/launcher/ApplicationLauncher.qml | 101 +++ .../launcher/ApplicationLauncherContent.qml | 602 ++++++++++++++++++ hosts/terra/home/hyprland.nix | 2 +- 7 files changed, 770 insertions(+), 5 deletions(-) create mode 100644 dotfiles/quickshell/tests/ApplicationLauncherHeadless.qml create mode 100644 dotfiles/quickshell/widgets/launcher/ApplicationLauncher.qml create mode 100644 dotfiles/quickshell/widgets/launcher/ApplicationLauncherContent.qml diff --git a/dotfiles/quickshell/CLAUDE.md b/dotfiles/quickshell/CLAUDE.md index e1f938a..1d5c6cb 100644 --- a/dotfiles/quickshell/CLAUDE.md +++ b/dotfiles/quickshell/CLAUDE.md @@ -27,7 +27,7 @@ Quickshell hot-reloads QML on file save when already running, so for most edits **Directory layout**: - `widgets/bar/` — `Bar.qml` is the main sidebar (right-anchored, full height) hosting the module stack (date, clock, tray, decorative dividers); `BarTop.qml`/`BarBottom.qml` are thin accent-colored strips anchored to the top/bottom edges. - `widgets/bar/modules/` — individual bar widgets (`Clock`, `Date`, `Tray`/`TrayItem`, `Volume`) built on the shared `BarWidget` base component. -- `widgets/launcher/` — app launcher panel (`Launcher` → `LauncherPanel` → `SearchBar`/`TextField`), currently a WIP skeleton (search box has no backing logic yet, `Launcher.qml` is instantiated with `visible: false`). +- `widgets/launcher/` — shared `AppModel` search/execution plus eight launcher variants. `ApplicationLauncher` (variant 8 and the primary `SUPER` launcher) keeps its visual core in the headlessly renderable `ApplicationLauncherContent`; variants 1–7 remain available on `SUPER CTRL 1–7` for comparison. - `widgets/decoration/` — reusable QtQuick `Shape`-based visual accents (angled panel edges, slashes) used to give bar panels their non-rectangular look. `Dummy.qml` is a placeholder/test rectangle. - `widgets/input/` — thin wrappers around `QtQuick.Controls` inputs (currently just `TextField`). - `widgets/layout/` — `HorizontalStack`/`VerticalStack`: `RowLayout`/`ColumnLayout` wrappers that expose `default property alias content` for terser call sites, with a trailing filler `Item` that soaks up remaining space. diff --git a/dotfiles/quickshell/open_launcher.sh b/dotfiles/quickshell/open_launcher.sh index a4ec215..8329ea9 100755 --- a/dotfiles/quickshell/open_launcher.sh +++ b/dotfiles/quickshell/open_launcher.sh @@ -1,3 +1,3 @@ #!/bin/bash -hyprctl dispatch 'hl.dsp.global ("quickshell:launcher7")' +hyprctl dispatch 'hl.dsp.global ("quickshell:launcher8")' diff --git a/dotfiles/quickshell/shell.qml b/dotfiles/quickshell/shell.qml index c1dc319..fb2222a 100644 --- a/dotfiles/quickshell/shell.qml +++ b/dotfiles/quickshell/shell.qml @@ -17,8 +17,7 @@ Scope { SideBar {} BarBottom {} - // App launcher variants — toggled via Hyprland global shortcuts - // (SUPER CTRL 1/2/3). Try each and keep the one you like. + // App launcher variants — SUPER CTRL 1–8; variant 8 is the primary HUD. LauncherStack {} // 1 — left vertical list LauncherGrid {} // 2 — centered icon grid LauncherSpotlight {} // 3 — top-center command bar @@ -26,6 +25,7 @@ Scope { LauncherDock {} // 5 — deck rising from the bottom bar LauncherSlant {} // 6 — angular / sheared panel LauncherCorner {} // 7 — Slant (V6) copy + floating power panel (shutdown/reboot) + ApplicationLauncher {} // 8 — dense HUD command index (primary) Notifications {} VolumeOsd {} diff --git a/dotfiles/quickshell/tests/ApplicationLauncherHeadless.qml b/dotfiles/quickshell/tests/ApplicationLauncherHeadless.qml new file mode 100644 index 0000000..71ca1d9 --- /dev/null +++ b/dotfiles/quickshell/tests/ApplicationLauncherHeadless.qml @@ -0,0 +1,62 @@ +import QtQuick +import qs.widgets.launcher + +ApplicationLauncherContent { + width: 1080 + height: 620 + + query: "term" + selectedIndex: 1 + showIcons: false + apps: [ + { + name: "Alacritty", + genericName: "Terminal Emulator", + comment: "GPU accelerated command interface", + icon: "utilities-terminal", + categories: ["System", "TerminalEmulator"] + }, + { + name: "Ghostty", + genericName: "Terminal", + comment: "Fast native terminal for local operations", + icon: "com.mitchellh.ghostty", + categories: ["System", "TerminalEmulator"] + }, + { + name: "Kitty", + genericName: "Terminal Emulator", + comment: "GPU based terminal with multiplexing", + icon: "kitty", + categories: ["System", "TerminalEmulator"] + }, + { + name: "Vivaldi", + genericName: "Web Browser", + comment: "Access network and web applications", + icon: "vivaldi", + categories: ["Network", "WebBrowser"] + }, + { + name: "Cosmic Files", + genericName: "File Manager", + comment: "Browse and manage local storage", + icon: "com.system76.CosmicFiles", + categories: ["System", "FileManager"] + }, + { + name: "Obsidian", + genericName: "Knowledge Base", + comment: "Local-first markdown workspace", + icon: "obsidian", + categories: ["Office"] + }, + { + name: "Steam", + genericName: "Game Platform", + comment: "Launch and manage games", + icon: "steam", + categories: ["Game"] + } + ] +} diff --git a/dotfiles/quickshell/widgets/launcher/ApplicationLauncher.qml b/dotfiles/quickshell/widgets/launcher/ApplicationLauncher.qml new file mode 100644 index 0000000..cc1a063 --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/ApplicationLauncher.qml @@ -0,0 +1,101 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import QtQuick +import qs.widgets.theme + +// Primary application launcher (variant 8). The full-screen layer-shell adapter +// owns focus, DesktopEntries and execution; ApplicationLauncherContent remains +// an Item so the complete visual state can be rendered headlessly. +Scope { + id: root + + property bool active: false + + function toggle() { root.active = !root.active; } + + GlobalShortcut { + name: "launcher8" + description: "Toggle dense application command index" + onPressed: root.toggle() + } + + PanelWindow { + id: win + + visible: root.active + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + exclusionMode: ExclusionMode.Ignore + color: Theme.textAlpha(0) + + anchors { + top: true + left: true + right: true + bottom: true + } + + property int selectedIndex: 0 + + function clampSelection(index) { + if (model.apps.length === 0) + return 0; + return Math.max(0, Math.min(model.apps.length - 1, index)); + } + + function move(delta) { + const count = model.apps.length; + if (count === 0) + return; + selectedIndex = ((selectedIndex + delta) % count + count) % count; + } + + function launch(index) { + if (model.launch(index)) + root.active = false; + } + + onVisibleChanged: { + if (visible) { + content.clearSearch(); + selectedIndex = 0; + content.focusSearch(); + } + } + + AppModel { + id: model + search: content.query + } + + Rectangle { + anchors.fill: parent + color: Theme.surface + opacity: 0.72 + + MouseArea { + anchors.fill: parent + onClicked: root.active = false + } + } + + ApplicationLauncherContent { + id: content + anchors.centerIn: parent + width: 1080 + height: 620 + scale: Math.min(1, (parent.width - 64) / width, (parent.height - 64) / height) + transformOrigin: Item.Center + apps: model.apps + selectedIndex: win.selectedIndex + + onSelectionRequested: index => win.selectedIndex = win.clampSelection(index) + onMoveRequested: delta => win.move(delta) + onLaunchRequested: index => win.launch(index) + onDismissRequested: root.active = false + } + } +} diff --git a/dotfiles/quickshell/widgets/launcher/ApplicationLauncherContent.qml b/dotfiles/quickshell/widgets/launcher/ApplicationLauncherContent.qml new file mode 100644 index 0000000..cb11326 --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/ApplicationLauncherContent.qml @@ -0,0 +1,602 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import qs.widgets.bar +import qs.widgets.theme + +// Headlessly renderable visual core for the primary application launcher. +// Runtime concerns (DesktopEntries, layer shell, launching) stay in +// ApplicationLauncher.qml; this component only renders state and emits intent. +Item { + id: root + + property var apps: [] + property int selectedIndex: 0 + property bool showIcons: true + property alias query: searchInput.text + + readonly property var selectedApp: apps.length > 0 && selectedIndex >= 0 && selectedIndex < apps.length + ? apps[selectedIndex] : null + + signal selectionRequested(int index) + signal moveRequested(int delta) + signal launchRequested(int index) + signal dismissRequested + + function focusSearch() { searchInput.forceActiveFocus(); } + function clearSearch() { searchInput.text = ""; } + + implicitWidth: 1080 + implicitHeight: 620 + + component MicroText: Text { + color: Theme.muted + font.family: Theme.microFont + font.pixelSize: 7 + font.letterSpacing: 0.9 + elide: Text.ElideRight + } + + component KeyHint: Row { + property string keyText: "" + property string actionText: "" + spacing: 6 + + Rectangle { + width: keyLabel.implicitWidth + 10 + height: 18 + color: Theme.accentAlpha(0.14) + border.width: 1 + border.color: Theme.accent + + Text { + id: keyLabel + anchors.centerIn: parent + text: parent.parent.keyText + color: Theme.accent + font.family: Theme.microFont + font.pixelSize: 7 + font.bold: true + } + } + + MicroText { + anchors.verticalCenter: parent.verticalCenter + text: parent.actionText + } + } + + StatusBarPanel { + anchors.fill: parent + panelId: "008" + title: "APPLICATION COMMAND INDEX" + meta: root.apps.length + " TARGETS" + chamfer: 18 + + // Query module. + StatusBarPanel { + id: queryPanel + x: 18 + y: 34 + width: 670 + height: 76 + panelId: "QRY" + title: "SEARCH VECTOR" + meta: "FUZZY / PREFIX" + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.topMargin: 31 + height: 34 + spacing: 12 + + Text { + text: ">_" + color: Theme.accent + font.family: Theme.displayFont + font.pixelSize: 20 + font.bold: true + } + + TextInput { + id: searchInput + Layout.fillWidth: true + Layout.fillHeight: true + verticalAlignment: TextInput.AlignVCenter + color: Theme.text + selectionColor: Theme.accent + selectedTextColor: Theme.surface + font.family: Theme.displayFont + font.pixelSize: 18 + font.letterSpacing: 1 + clip: true + + onTextChanged: root.selectionRequested(0) + + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Down: + case Qt.Key_Tab: + root.moveRequested(1); + event.accepted = true; + break; + case Qt.Key_Up: + case Qt.Key_Backtab: + root.moveRequested(-1); + event.accepted = true; + break; + case Qt.Key_PageDown: + root.moveRequested(5); + event.accepted = true; + break; + case Qt.Key_PageUp: + root.moveRequested(-5); + event.accepted = true; + break; + case Qt.Key_Return: + case Qt.Key_Enter: + root.launchRequested(root.selectedIndex); + event.accepted = true; + break; + case Qt.Key_Escape: + root.dismissRequested(); + event.accepted = true; + break; + } + } + + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + visible: searchInput.text.length === 0 + text: "TYPE APPLICATION DESIGNATION" + color: Theme.muted + font: searchInput.font + } + } + + Rectangle { + Layout.preferredWidth: 84 + Layout.preferredHeight: 22 + color: Theme.accentAlpha(0.12) + border.width: 1 + border.color: Theme.hair + + Text { + anchors.centerIn: parent + text: "LIVE INDEX" + color: Theme.accent + font.family: Theme.microFont + font.pixelSize: 7 + font.bold: true + font.letterSpacing: 0.8 + } + } + } + } + + // Search result table. + StatusBarPanel { + id: resultPanel + x: 18 + y: 118 + width: 670 + height: 424 + panelId: "IDX" + title: "APPLICATION TABLE" + meta: root.query.length > 0 ? "FILTER ACTIVE" : "A–Z / LOCAL" + + ListView { + id: appList + x: 9 + y: 29 + width: parent.width - 18 + height: parent.height - 38 + clip: true + spacing: 3 + model: root.apps + currentIndex: root.selectedIndex + boundsBehavior: Flickable.StopAtBounds + onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain) + + delegate: MouseArea { + id: row + required property int index + required property var modelData + + width: ListView.view.width + height: 48 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + readonly property bool selected: index === root.selectedIndex + + onEntered: root.selectionRequested(index) + onClicked: root.launchRequested(index) + + Shape { + id: rowShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: row.selected ? Theme.selection : Theme.textAlpha(0.025) + strokeColor: row.selected ? Theme.accent : Theme.textAlpha(0.10) + strokeWidth: 1 + startX: 9; startY: 0 + PathLine { x: rowShape.width; y: 0 } + PathLine { x: rowShape.width - 9; y: rowShape.height } + PathLine { x: 0; y: rowShape.height } + PathLine { x: 9; y: 0 } + } + + ShapePath { + fillColor: row.selected ? Theme.accent : Theme.textAlpha(0.12) + strokeWidth: 0 + startX: 9; startY: 0 + PathLine { x: 13; y: 0 } + PathLine { x: 4; y: rowShape.height } + PathLine { x: 0; y: rowShape.height } + PathLine { x: 9; y: 0 } + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 18 + anchors.rightMargin: 18 + spacing: 12 + + Text { + Layout.preferredWidth: 28 + text: String(row.index + 1).padStart(2, "0") + color: row.selected ? Theme.accent : Theme.muted + font.family: Theme.microFont + font.pixelSize: 8 + font.bold: row.selected + } + + Rectangle { + Layout.preferredWidth: 34 + Layout.preferredHeight: 34 + color: row.selected ? Theme.accentAlpha(0.14) : Theme.textAlpha(0.035) + border.width: 1 + border.color: row.selected ? Theme.accent : Theme.hair + + IconImage { + visible: root.showIcons + anchors.centerIn: parent + implicitSize: 24 + source: root.showIcons + ? Quickshell.iconPath(row.modelData.icon || "application-x-executable", "application-x-executable") + : "" + } + + Text { + visible: !root.showIcons + anchors.centerIn: parent + text: String(row.modelData.name || "?").charAt(0).toUpperCase() + color: row.selected ? Theme.accent : Theme.text + font.family: Theme.displayFont + font.pixelSize: 14 + font.bold: true + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 1 + + Text { + Layout.fillWidth: true + text: row.modelData.name || "UNKNOWN TARGET" + color: row.selected ? Theme.accent : Theme.text + font.family: Theme.displayFont + font.pixelSize: 11 + font.bold: row.selected + font.letterSpacing: 0.5 + elide: Text.ElideRight + } + + MicroText { + Layout.fillWidth: true + text: row.modelData.genericName || row.modelData.comment || "DESKTOP APPLICATION" + } + } + + MicroText { + Layout.preferredWidth: 100 + horizontalAlignment: Text.AlignRight + text: row.index === root.selectedIndex ? "LOCKED" : "AVAILABLE" + color: row.selected ? Theme.accent : Theme.muted + } + + Rectangle { + Layout.preferredWidth: 38 + Layout.preferredHeight: 6 + color: Theme.textAlpha(0.05) + border.width: 1 + border.color: Theme.hair + + Rectangle { + width: row.selected ? parent.width : Math.max(5, parent.width * (1 - row.index / Math.max(1, root.apps.length))) + height: parent.height + color: row.selected ? Theme.accent : Theme.textAlpha(0.20) + } + } + } + } + + Text { + anchors.centerIn: parent + visible: root.apps.length === 0 + text: "NO EXECUTABLE TARGETS MATCH QUERY" + color: Theme.muted + font.family: Theme.microFont + font.pixelSize: 9 + font.letterSpacing: 1.2 + } + } + } + + // Selected application inspector. + StatusBarPanel { + id: inspector + x: 700 + y: 34 + width: 362 + height: 508 + panelId: "SEL" + title: "TARGET INSPECTOR" + meta: root.selectedApp ? "LOCKED" : "NO TARGET" + + Item { + id: reticle + anchors.horizontalCenter: parent.horizontalCenter + y: 39 + width: 126 + height: 126 + + Rectangle { + anchors.centerIn: parent + width: 112 + height: 112 + radius: 56 + color: Theme.textAlpha(0.015) + border.width: 1 + border.color: Theme.accentAlpha(0.52) + } + Rectangle { + anchors.centerIn: parent + width: 84 + height: 84 + radius: 42 + color: Theme.textAlpha(0) + border.width: 1 + border.color: Theme.hair + } + Rectangle { anchors.centerIn: parent; width: 126; height: 1; color: Theme.hair } + Rectangle { anchors.centerIn: parent; width: 1; height: 126; color: Theme.hair } + + Rectangle { + anchors.centerIn: parent + width: 60 + height: 60 + color: Theme.surface + border.width: 1 + border.color: Theme.accent + + IconImage { + visible: root.showIcons + anchors.centerIn: parent + implicitSize: 44 + source: root.showIcons && root.selectedApp + ? Quickshell.iconPath(root.selectedApp.icon || "application-x-executable", "application-x-executable") + : "" + } + + Text { + visible: !root.showIcons + anchors.centerIn: parent + text: root.selectedApp ? String(root.selectedApp.name || "?").charAt(0).toUpperCase() : "?" + color: Theme.accent + font.family: Theme.displayFont + font.pixelSize: 28 + font.bold: true + } + } + + Repeater { + model: 4 + Rectangle { + required property int index + width: 5; height: 5 + x: index % 2 === 0 ? 8 : reticle.width - 13 + y: index < 2 ? 8 : reticle.height - 13 + color: Theme.accent + } + } + } + + Text { + anchors.top: reticle.bottom + anchors.topMargin: 14 + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 34 + horizontalAlignment: Text.AlignHCenter + text: root.selectedApp ? root.selectedApp.name : "NO TARGET" + color: Theme.text + font.family: Theme.displayFont + font.pixelSize: 19 + font.bold: true + font.letterSpacing: 1 + elide: Text.ElideRight + } + + MicroText { + id: genericLabel + anchors.top: reticle.bottom + anchors.topMargin: 42 + x: 18 + width: parent.width - 36 + horizontalAlignment: Text.AlignHCenter + text: root.selectedApp ? (root.selectedApp.genericName || "DESKTOP APPLICATION") : "AWAITING SEARCH VECTOR" + color: Theme.accent + } + + Rectangle { + x: 18 + anchors.top: genericLabel.bottom + anchors.topMargin: 12 + width: parent.width - 36 + height: 1 + color: Theme.hair + } + + MicroText { + id: description + x: 22 + anchors.top: genericLabel.bottom + anchors.topMargin: 27 + width: parent.width - 44 + height: 42 + text: root.selectedApp ? (root.selectedApp.comment || "No application description supplied by desktop entry.") : "Enter a designation to acquire an executable target." + wrapMode: Text.Wrap + elide: Text.ElideRight + maximumLineCount: 3 + horizontalAlignment: Text.AlignHCenter + } + + GridLayout { + id: telemetryGrid + x: 18 + anchors.top: description.bottom + anchors.topMargin: 15 + width: parent.width - 36 + columns: 2 + rowSpacing: 7 + columnSpacing: 7 + + Repeater { + model: [ + { label: "ENTRY", value: "DESKTOP" }, + { label: "MODE", value: "DETACHED" }, + { label: "AUTH", value: "LOCAL USER" }, + { label: "INDEX", value: String(Math.max(0, root.selectedIndex + 1)).padStart(3, "0") } + ] + + Rectangle { + required property var modelData + Layout.fillWidth: true + Layout.preferredHeight: 46 + color: Theme.textAlpha(0.025) + border.width: 1 + border.color: Theme.hair + + MicroText { x: 8; y: 7; width: parent.width - 16; text: modelData.label } + Text { + x: 8; y: 22; width: parent.width - 16 + text: modelData.value + color: Theme.text + font.family: Theme.displayFont + font.pixelSize: 9 + font.bold: true + font.letterSpacing: 0.6 + elide: Text.ElideRight + } + } + } + } + + Item { + x: 18 + anchors.bottom: executeTile.top + anchors.bottomMargin: 12 + width: parent.width - 36 + height: 18 + + Row { + spacing: 5 + Repeater { + model: 24 + Rectangle { + required property int index + width: 7; height: 3 + y: index % 3 === 0 ? 0 : 4 + color: index < 16 ? Theme.accent : Theme.hair + transform: Rotation { angle: -35 } + } + } + } + } + + MouseArea { + id: executeTile + x: 18 + anchors.bottom: parent.bottom + anchors.bottomMargin: 14 + width: parent.width - 36 + height: 42 + enabled: root.selectedApp !== null + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: root.launchRequested(root.selectedIndex) + + Rectangle { + anchors.fill: parent + color: executeTile.containsMouse ? Theme.accent : Theme.accentAlpha(0.16) + border.width: 1 + border.color: Theme.accent + + Text { + anchors.centerIn: parent + text: root.selectedApp ? "EXECUTE SELECTED TARGET" : "NO TARGET ACQUIRED" + color: executeTile.containsMouse ? Theme.surface : Theme.accent + font.family: Theme.displayFont + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1.2 + } + } + } + } + + // Dense command footer. + StatusBarPanel { + x: 18 + y: 550 + width: 1044 + height: 52 + showHeader: false + chamfer: 9 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 16 + anchors.rightMargin: 16 + spacing: 18 + + MicroText { text: "INPUT CHANNEL // KEYBOARD"; color: Theme.accent } + Rectangle { Layout.preferredWidth: 1; Layout.preferredHeight: 22; color: Theme.hair } + KeyHint { keyText: "↑ ↓"; actionText: "SELECT" } + KeyHint { keyText: "ENTER"; actionText: "EXECUTE" } + KeyHint { keyText: "ESC"; actionText: "ABORT" } + Item { Layout.fillWidth: true } + MicroText { text: "QRY:" + (root.query.length > 0 ? "ACTIVE" : "IDLE") } + MicroText { text: "CRC // A7F2" } + Rectangle { + Layout.preferredWidth: 76 + Layout.preferredHeight: 4 + color: Theme.accent + } + } + } + } +} diff --git a/hosts/terra/home/hyprland.nix b/hosts/terra/home/hyprland.nix index 5dd1d16..593ce5e 100644 --- a/hosts/terra/home/hyprland.nix +++ b/hosts/terra/home/hyprland.nix @@ -198,7 +198,7 @@ in # quickshell app-launcher variants (evaluating — pick one) ] - ++ (map (n: bind "SUPER + CTRL + ${toString n}" (dsp.global "quickshell:launcher${toString n}")) (lib.range 1 7)) + ++ (map (n: bind "SUPER + CTRL + ${toString n}" (dsp.global "quickshell:launcher${toString n}")) (lib.range 1 8)) ++ [ # restart quickshell (also starts it if not running) (bind "SUPER + CTRL + 0" (dsp.exec "qs kill; sleep 0.3; qs"))