diff --git a/dotfiles/quickshell/shell.qml b/dotfiles/quickshell/shell.qml index fb2222a..278e5ff 100644 --- a/dotfiles/quickshell/shell.qml +++ b/dotfiles/quickshell/shell.qml @@ -17,7 +17,7 @@ Scope { SideBar {} BarBottom {} - // App launcher variants — SUPER CTRL 1–8; variant 8 is the primary HUD. + // App launcher variants — SUPER CTRL 1–9; variant 8 remains the primary HUD. LauncherStack {} // 1 — left vertical list LauncherGrid {} // 2 — centered icon grid LauncherSpotlight {} // 3 — top-center command bar @@ -26,6 +26,7 @@ Scope { LauncherSlant {} // 6 — angular / sheared panel LauncherCorner {} // 7 — Slant (V6) copy + floating power panel (shutdown/reboot) ApplicationLauncher {} // 8 — dense HUD command index (primary) + BladeLauncher {} // 9 — asymmetric blade matrix Notifications {} VolumeOsd {} diff --git a/dotfiles/quickshell/tests/BladeLauncherHeadless.qml b/dotfiles/quickshell/tests/BladeLauncherHeadless.qml new file mode 100644 index 0000000..509f9fe --- /dev/null +++ b/dotfiles/quickshell/tests/BladeLauncherHeadless.qml @@ -0,0 +1,20 @@ +import QtQuick +import qs.widgets.launcher + +BladeLauncherContent { + width: 1120 + height: 640 + query: "dev" + selectedIndex: 2 + showIcons: false + apps: [ + { name: "Visual Studio Code", genericName: "Code Editor", comment: "Edit and debug software projects", icon: "visual-studio-code", categories: ["Development"] }, + { name: "GitKraken", genericName: "Git Client", comment: "Inspect branches and repository history", icon: "gitkraken", categories: ["Development"] }, + { name: "OpenCode", genericName: "AI Development", comment: "Agentic terminal coding environment", icon: "utilities-terminal", categories: ["Development", "Utility"] }, + { name: "Alacritty", genericName: "Terminal Emulator", comment: "GPU accelerated command interface", icon: "utilities-terminal", categories: ["System"] }, + { name: "Vivaldi", genericName: "Web Browser", comment: "Access network and web applications", icon: "vivaldi", categories: ["Network"] }, + { name: "Obsidian", genericName: "Knowledge Base", comment: "Local-first markdown workspace", icon: "obsidian", categories: ["Office"] }, + { name: "Blender", genericName: "3D Creation", comment: "Model, animate and render 3D scenes", icon: "blender", categories: ["Graphics"] }, + { name: "Steam", genericName: "Game Platform", comment: "Launch and manage games", icon: "steam", categories: ["Game"] } + ] +} diff --git a/dotfiles/quickshell/widgets/launcher/BladeLauncher.qml b/dotfiles/quickshell/widgets/launcher/BladeLauncher.qml new file mode 100644 index 0000000..c29d7b5 --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/BladeLauncher.qml @@ -0,0 +1,80 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import QtQuick +import qs.widgets.theme + +// Variant 9 — asymmetric Blade application launcher. +Scope { + id: root + + property bool active: false + function toggle() { root.active = !root.active; } + + GlobalShortcut { + name: "launcher9" + description: "Toggle app launcher (Blade matrix)" + 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) { + return model.apps.length === 0 ? 0 : 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.78 + MouseArea { anchors.fill: parent; onClicked: root.active = false } + } + + BladeLauncherContent { + id: content + anchors.centerIn: parent + width: 1120 + height: 640 + scale: Math.min(1, (parent.width - 56) / width, (parent.height - 56) / 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/BladeLauncherContent.qml b/dotfiles/quickshell/widgets/launcher/BladeLauncherContent.qml new file mode 100644 index 0000000..3521d90 --- /dev/null +++ b/dotfiles/quickshell/widgets/launcher/BladeLauncherContent.qml @@ -0,0 +1,473 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import qs.widgets.theme + +// Variant 9 visual core: asymmetric "Blade" composition. It shares the shell +// Theme and AppModel contract, but intentionally does not reuse StatusBarPanel +// or the nested-panel structure of ApplicationLauncherContent. +Item { + id: root + + property var apps: [] + property int selectedIndex: 0 + property bool showIcons: true + property alias query: commandInput.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() { commandInput.forceActiveFocus(); } + function clearSearch() { commandInput.text = ""; } + + implicitWidth: 1120 + implicitHeight: 640 + + component MicroText: Text { + color: Theme.muted + font.family: Theme.microFont + font.pixelSize: 7 + font.letterSpacing: 0.9 + elide: Text.ElideRight + } + + Rectangle { anchors.fill: parent; color: Theme.surface } + + // Sparse circuit traces behind the active surfaces. + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: Theme.textAlpha(0) + strokeColor: Theme.accentAlpha(0.24) + strokeWidth: 1 + startX: 24; startY: 92 + PathLine { x: 115; y: 92 } + PathLine { x: 138; y: 69 } + PathLine { x: 480; y: 69 } + PathLine { x: 494; y: 55 } + PathLine { x: 840; y: 55 } + } + ShapePath { + fillColor: Theme.textAlpha(0) + strokeColor: Theme.hair + strokeWidth: 1 + startX: 115; startY: 570 + PathLine { x: 146; y: 601 } + PathLine { x: 775; y: 601 } + PathLine { x: 801; y: 575 } + PathLine { x: 1095; y: 575 } + } + } + + // Top command mast — an open angular frame rather than panel chrome. + Shape { + id: mast + x: 26; y: 18 + width: parent.width - 52; height: 72 + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: Theme.textAlpha(0.018) + strokeColor: Theme.hair + strokeWidth: 1 + startX: 0; startY: 17 + PathLine { x: 17; y: 0 } + PathLine { x: mast.width - 110; y: 0 } + PathLine { x: mast.width - 88; y: 22 } + PathLine { x: mast.width; y: 22 } + PathLine { x: mast.width; y: mast.height } + PathLine { x: 0; y: mast.height } + PathLine { x: 0; y: 17 } + } + ShapePath { + fillColor: Theme.accent + strokeWidth: 0 + startX: 0; startY: 17 + PathLine { x: 17; y: 0 } + PathLine { x: 122; y: 0 } + PathLine { x: 116; y: 4 } + PathLine { x: 20; y: 4 } + PathLine { x: 4; y: 20 } + PathLine { x: 4; y: mast.height } + PathLine { x: 0; y: mast.height } + PathLine { x: 0; y: 17 } + } + } + + Text { + x: 54; y: 31 + text: "BLADE // EXECUTION MATRIX" + color: Theme.text + font.family: Theme.displayFont + font.pixelSize: 16 + font.bold: true + font.letterSpacing: 1.4 + } + MicroText { x: 56; y: 57; text: "09 / APPLICATION ROUTER / LOCAL DESKTOP ENTRIES"; color: Theme.accent } + + Row { + anchors.right: parent.right + anchors.rightMargin: 48 + y: 37 + spacing: 4 + Repeater { + model: 16 + Rectangle { + required property int index + width: 9; height: index % 4 === 0 ? 16 : 8 + y: index % 4 === 0 ? 0 : 8 + color: index < Math.min(16, root.apps.length + 6) ? Theme.accent : Theme.hair + transform: Rotation { angle: -28 } + } + } + } + + // Left category rotor and mode spine. + Item { + id: rotorZone + x: 26; y: 108 + width: 188; height: 444 + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: Theme.textAlpha(0.018) + strokeColor: Theme.hair + strokeWidth: 1 + startX: 0; startY: 0 + PathLine { x: 154; y: 0 } + PathLine { x: 188; y: 34 } + PathLine { x: 188; y: 408 } + PathLine { x: 152; y: 444 } + PathLine { x: 0; y: 444 } + PathLine { x: 0; y: 0 } + } + ShapePath { + fillColor: Theme.accent + strokeWidth: 0 + startX: 0; startY: 0 + PathLine { x: 7; y: 0 } + PathLine { x: 7; y: 444 } + PathLine { x: 0; y: 444 } + PathLine { x: 0; y: 0 } + } + } + + Text { + x: 20; y: 18 + text: "VECTOR" + color: Theme.accent + font.family: Theme.displayFont + font.pixelSize: 11 + font.bold: true + font.letterSpacing: 1.4 + } + MicroText { x: 20; y: 38; text: "CATEGORY BUS" } + + Item { + id: rotor + anchors.horizontalCenter: parent.horizontalCenter + y: 70; width: 126; height: 126 + Rectangle { anchors.centerIn: parent; width: 116; height: 116; radius: 58; color: Theme.textAlpha(0); border.width: 1; border.color: Theme.accentAlpha(0.55) } + Rectangle { anchors.centerIn: parent; width: 82; height: 82; radius: 41; 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: 32; height: 32; color: Theme.accentAlpha(0.18); border.width: 1; border.color: Theme.accent; transform: Rotation { angle: 45 } } + Text { anchors.centerIn: parent; text: "A"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 16; font.bold: true } + Repeater { + model: 4 + Rectangle { + required property int index + width: 6; height: 6; radius: 3 + x: [16, 101, 101, 16][index] + y: [16, 16, 101, 101][index] + color: Theme.accent + } + } + } + + Column { + x: 18; y: 218 + width: parent.width - 38 + spacing: 7 + Repeater { + model: ["ALL TARGETS", "SYSTEM", "DEVELOP", "NETWORK", "MEDIA"] + Rectangle { + required property int index + required property string modelData + width: parent.width - index * 5 + height: 31 + x: index * 5 + color: index === 0 ? Theme.accentAlpha(0.14) : Theme.textAlpha(0.025) + border.width: 1 + border.color: index === 0 ? Theme.accent : Theme.hair + Text { + x: 9; anchors.verticalCenter: parent.verticalCenter + text: String(index).padStart(2, "0") + " " + modelData + color: index === 0 ? Theme.accent : Theme.muted + font.family: Theme.microFont + font.pixelSize: 7 + font.bold: index === 0 + font.letterSpacing: 0.7 + } + } + } + } + + MicroText { x: 18; anchors.bottom: parent.bottom; anchors.bottomMargin: 14; text: "BUS // UNFILTERED"; color: Theme.accent } + } + + // Central command spine. + Item { + id: commandZone + x: 230; y: 108 + width: 594; height: 486 + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: Theme.textAlpha(0.012) + strokeColor: Theme.hair + strokeWidth: 1 + startX: 20; startY: 0 + PathLine { x: commandZone.width; y: 0 } + PathLine { x: commandZone.width - 20; y: commandZone.height } + PathLine { x: 0; y: commandZone.height } + PathLine { x: 20; y: 0 } + } + } + + // Search blade. + Shape { + id: searchBlade + x: 12; y: 12; width: parent.width - 32; height: 58 + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: Theme.accentAlpha(0.10) + strokeColor: Theme.accent + strokeWidth: 1 + startX: 18; startY: 0 + PathLine { x: searchBlade.width; y: 0 } + PathLine { x: searchBlade.width - 18; y: searchBlade.height } + PathLine { x: 0; y: searchBlade.height } + PathLine { x: 18; y: 0 } + } + } + Text { x: 32; y: 28; text: ">"; color: Theme.accent; font.family: Theme.displayFont; font.pixelSize: 20; font.bold: true } + TextInput { + id: commandInput + x: 62; y: 22; width: 430; height: 38 + verticalAlignment: TextInput.AlignVCenter + color: Theme.text + selectionColor: Theme.accent + selectedTextColor: Theme.surface + font.family: Theme.displayFont + font.pixelSize: 17 + 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: commandInput.text.length === 0; text: "ACQUIRE TARGET"; color: Theme.muted; font: commandInput.font } + } + MicroText { anchors.right: parent.right; anchors.rightMargin: 38; y: 35; text: root.apps.length + " HIT"; color: Theme.accent } + + ListView { + id: bladeList + x: 10; y: 84; width: parent.width - 26; height: 382 + model: root.apps + currentIndex: root.selectedIndex + clip: true + spacing: 4 + boundsBehavior: Flickable.StopAtBounds + onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain) + + delegate: MouseArea { + id: blade + required property int index + required property var modelData + readonly property bool selected: index === root.selectedIndex + width: ListView.view.width - (index % 2 === 0 ? 0 : 16) + height: 43 + x: index % 2 === 0 ? 0 : 16 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: root.selectionRequested(index) + onClicked: root.launchRequested(index) + + Shape { + id: bladeShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: blade.selected ? Theme.selection : Theme.textAlpha(0.025) + strokeColor: blade.selected ? Theme.accent : Theme.textAlpha(0.12) + strokeWidth: 1 + startX: 16; startY: 0 + PathLine { x: bladeShape.width; y: 0 } + PathLine { x: bladeShape.width - 24; y: bladeShape.height } + PathLine { x: 0; y: bladeShape.height } + PathLine { x: 16; y: 0 } + } + ShapePath { + fillColor: blade.selected ? Theme.accent : Theme.textAlpha(0.12) + strokeWidth: 0 + startX: 16; startY: 0 + PathLine { x: 22; y: 0 } + PathLine { x: 6; y: bladeShape.height } + PathLine { x: 0; y: bladeShape.height } + PathLine { x: 16; y: 0 } + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 26 + anchors.rightMargin: 28 + spacing: 10 + Text { Layout.preferredWidth: 26; text: String(blade.index + 1).padStart(2, "0"); color: blade.selected ? Theme.accent : Theme.muted; font.family: Theme.microFont; font.pixelSize: 7 } + Rectangle { + Layout.preferredWidth: 28; Layout.preferredHeight: 28 + color: blade.selected ? Theme.accentAlpha(0.14) : Theme.textAlpha(0.025) + border.width: 1; border.color: blade.selected ? Theme.accent : Theme.hair + IconImage { visible: root.showIcons; anchors.centerIn: parent; implicitSize: 20; source: root.showIcons ? Quickshell.iconPath(blade.modelData.icon || "application-x-executable", "application-x-executable") : "" } + Text { visible: !root.showIcons; anchors.centerIn: parent; text: String(blade.modelData.name || "?").charAt(0).toUpperCase(); color: blade.selected ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: 11; font.bold: true } + } + Text { Layout.fillWidth: true; text: blade.modelData.name || "UNKNOWN"; color: blade.selected ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: 10; font.bold: blade.selected; elide: Text.ElideRight } + MicroText { Layout.preferredWidth: 125; horizontalAlignment: Text.AlignRight; text: blade.modelData.genericName || "APPLICATION" } + Rectangle { Layout.preferredWidth: blade.selected ? 32 : 12; Layout.preferredHeight: 3; color: blade.selected ? Theme.accent : Theme.hair } + } + } + + Text { anchors.centerIn: parent; visible: root.apps.length === 0; text: "NO TARGET VECTOR"; color: Theme.muted; font.family: Theme.microFont; font.pixelSize: 9; font.letterSpacing: 1.2 } + } + } + + // Right dossier: open brackets and data lines, deliberately not a panel. + Item { + id: dossier + x: 842; y: 108 + width: 252; height: 444 + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: Theme.textAlpha(0) + strokeColor: Theme.accent + strokeWidth: 2 + startX: 36; startY: 0 + PathLine { x: 0; y: 0 } + PathLine { x: 0; y: 86 } + } + ShapePath { + fillColor: Theme.textAlpha(0) + strokeColor: Theme.accent + strokeWidth: 2 + startX: dossier.width - 36; startY: dossier.height + PathLine { x: dossier.width; y: dossier.height } + PathLine { x: dossier.width; y: dossier.height - 86 } + } + ShapePath { + fillColor: Theme.textAlpha(0) + strokeColor: Theme.hair + strokeWidth: 1 + startX: 20; startY: 20 + PathLine { x: dossier.width; y: 20 } + PathLine { x: dossier.width; y: dossier.height - 20 } + PathLine { x: 0; y: dossier.height - 20 } + PathLine { x: 0; y: 104 } + } + } + + MicroText { x: 18; y: 12; text: "ACTIVE DOSSIER"; color: Theme.accent } + Text { + x: 18; y: 45; width: parent.width - 36 + text: root.selectedApp ? root.selectedApp.name : "NO TARGET" + color: Theme.text + font.family: Theme.displayFont + font.pixelSize: 18 + font.bold: true + font.letterSpacing: 0.8 + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideRight + } + MicroText { x: 18; y: 94; width: parent.width - 36; text: root.selectedApp ? (root.selectedApp.genericName || "DESKTOP APPLICATION") : "AWAITING ACQUISITION"; color: Theme.accent } + + Rectangle { x: 18; y: 119; width: parent.width - 36; height: 1; color: Theme.hair } + MicroText { x: 18; y: 137; width: parent.width - 36; height: 52; wrapMode: Text.Wrap; maximumLineCount: 4; text: root.selectedApp ? (root.selectedApp.comment || "No metadata supplied by desktop entry.") : "Enter a search vector and select an executable target." } + + Column { + x: 18; y: 210; width: parent.width - 36; spacing: 8 + Repeater { + model: [ + { k: "INDEX", v: String(Math.max(0, root.selectedIndex + 1)).padStart(3, "0") }, + { k: "TYPE", v: "DESKTOP ENTRY" }, + { k: "ROUTE", v: "DETACHED" }, + { k: "STATE", v: root.selectedApp ? "ARMED" : "IDLE" } + ] + Item { + required property var modelData + width: parent.width; height: 27 + Rectangle { x: 0; y: parent.height - 1; width: parent.width; height: 1; color: Theme.hair } + MicroText { x: 0; anchors.verticalCenter: parent.verticalCenter; text: modelData.k } + Text { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter; text: modelData.v; color: modelData.k === "STATE" ? Theme.accent : Theme.text; font.family: Theme.displayFont; font.pixelSize: 8; font.bold: true; font.letterSpacing: 0.5 } + } + } + } + + MouseArea { + id: fire + x: 18; anchors.bottom: parent.bottom; anchors.bottomMargin: 34 + width: parent.width - 36; height: 44 + enabled: root.selectedApp !== null + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: root.launchRequested(root.selectedIndex) + Shape { + id: fireShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + ShapePath { + fillColor: fire.containsMouse ? Theme.accent : Theme.accentAlpha(0.16) + strokeColor: Theme.accent + strokeWidth: 1 + startX: 14; startY: 0 + PathLine { x: fireShape.width; y: 0 } + PathLine { x: fireShape.width - 14; y: fireShape.height } + PathLine { x: 0; y: fireShape.height } + PathLine { x: 14; y: 0 } + } + } + Text { anchors.centerIn: parent; text: "LAUNCH VECTOR"; color: fire.containsMouse ? Theme.surface : Theme.accent; font.family: Theme.displayFont; font.pixelSize: 10; font.bold: true; font.letterSpacing: 1.2 } + } + } + + // Bottom caution lane and key map. + Row { + x: 28; y: 612; spacing: 4 + Repeater { model: 30; Rectangle { required property int index; width: 12; height: 4; color: index % 3 === 0 ? Theme.accent : Theme.hair; transform: Rotation { angle: -32 } } } + } + MicroText { x: 420; y: 606; text: "↑↓ SELECT // ENTER LAUNCH // ESC ABORT // PGUP/PGDN STEP"; color: Theme.accent } + MicroText { anchors.right: parent.right; anchors.rightMargin: 28; y: 606; text: "BLADE-09 / CRC A7F2" } +} diff --git a/hosts/terra/home/hyprland.nix b/hosts/terra/home/hyprland.nix index 593ce5e..e0042b4 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 8)) + ++ (map (n: bind "SUPER + CTRL + ${toString n}" (dsp.global "quickshell:launcher${toString n}")) (lib.range 1 9)) ++ [ # restart quickshell (also starts it if not running) (bind "SUPER + CTRL + 0" (dsp.exec "qs kill; sleep 0.3; qs"))