[verified] feat(quickshell): add blade launcher variant

This commit is contained in:
2026-08-28 20:48:23 +00:00
parent fdc00a8574
commit 5ecc73f084
5 changed files with 576 additions and 2 deletions
+2 -1
View File
@@ -17,7 +17,7 @@ Scope {
SideBar {}
BarBottom {}
// App launcher variants — SUPER CTRL 18; variant 8 is the primary HUD.
// App launcher variants — SUPER CTRL 19; 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 {}
@@ -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"] }
]
}
@@ -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
}
}
}
@@ -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" }
}