fuzzel could only answer "which app", so launching Brave at a URL meant picking the app, waiting for it, then typing the address somewhere else. The launcher now takes a verb and an object in one line: type `gitlab.enqs.de` and every app that can open it sorts to the top, default browser first, ready for Enter — or arrow/hjkl down to Brave's New Incognito Window, which is offered as a peer of Brave itself rather than hidden behind a submenu. services/Apps.qml is the brain and holds no UI: it flattens desktop entries and their actions into one index, classifies the typed argument (url / mail / path), ranks by fuzzy match plus handler affinity plus frecency, and launches. Launching through `uwsm app` keeps each app in its own scope; entries whose Exec declares no field code (Brave's `--incognito` action, among others) are launched by tokenising Exec per the Desktop Entry spec and appending the argument, never via `sh -c`. Four prefixes reuse the same window, so there is one picker and one set of keys: `>` runs a command, `=` evaluates arithmetic, `:` is clipboard history (cliphist, which had been a second fuzzel invocation), and a dmenu mode is driven over IPC. quickshell/scripts/takemi-dmenu is the bridge for that last one — a `fuzzel --dmenu` drop-in for scripts that want a picker, symlinked into ~/.local/bin. fluxo's bt menu now goes through it. Living in the Quickshell process means the window is on screen on the frame the bind fires, with no process to spawn. Removes fuzzel entirely: its config, its theme submodule, the package, and the last consumers (the bar sigil, fluxo's menu_command). Also drops hypr/scripts/powermenu.sh, which was fuzzel-based and bound to nothing.
133 lines
4.3 KiB
QML
133 lines
4.3 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import "root:/config"
|
|
import "root:/components"
|
|
|
|
// One line in the result list. Rest is bare — no fill, no border — so a list of
|
|
// forty apps reads as a column of text rather than forty boxes. Only the
|
|
// selected row gets a slab, and it takes the full inverted treatment: white
|
|
// fill, black text. That is the strongest contrast the palette has, which is
|
|
// what the row you are about to launch should be wearing.
|
|
Item {
|
|
id: root
|
|
|
|
required property var entry
|
|
required property bool selected
|
|
property bool hovered: false
|
|
property string badge: ""
|
|
|
|
signal activated
|
|
signal pinRequested
|
|
|
|
implicitHeight: 46
|
|
|
|
// Selection slab. Leans like everything else; drawn as a Skew so the
|
|
// diagonals get analytic antialiasing instead of a pixel staircase.
|
|
Skew {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: -Theme.padS
|
|
anchors.rightMargin: -Theme.padS
|
|
color: root.selected ? Theme.treatmentSelectedFill : "transparent"
|
|
opacity: root.selected ? 1 : 0
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutQuad }
|
|
}
|
|
}
|
|
|
|
// Hover is a crimson tick in the gutter rather than a second slab, so
|
|
// pointing at a row cannot be mistaken for selecting it.
|
|
Skew {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
x: -Theme.padS
|
|
width: 3
|
|
height: root.hovered && !root.selected ? parent.height - 10 : 0
|
|
color: Theme.accent
|
|
|
|
Behavior on height {
|
|
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutBack }
|
|
}
|
|
}
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.padM
|
|
anchors.rightMargin: Theme.padM
|
|
spacing: Theme.padM
|
|
|
|
IconImage {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
implicitSize: 26
|
|
asynchronous: true
|
|
source: root.entry.icon ? Quickshell.iconPath(root.entry.icon, true) : ""
|
|
visible: source !== ""
|
|
}
|
|
|
|
// Entries with no icon still need to occupy the same gutter, or their
|
|
// labels wander left and the column stops being a column.
|
|
Icon {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 26
|
|
text: ""
|
|
color: root.selected ? Theme.treatmentSelectedText : Theme.muted
|
|
visible: !root.entry.icon && root.entry.kind !== "dmenu"
|
|
}
|
|
|
|
Column {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width - 26 - Theme.padM * 2 - trailing.width
|
|
spacing: -1
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: root.entry.name
|
|
elide: Text.ElideRight
|
|
color: root.selected ? Theme.treatmentSelectedText : Theme.text
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fsBody
|
|
font.weight: root.selected ? Font.Bold : Theme.weightBody
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: root.entry.sub
|
|
visible: text !== ""
|
|
elide: Text.ElideRight
|
|
color: root.selected ? Theme.alpha(Theme.ink, 0.6) : Theme.muted
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fsMicro
|
|
renderType: Text.NativeRendering
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trailing hint: the key that fires this row. Only the selected row shows
|
|
// one, because a column of ⏎ glyphs tells you nothing.
|
|
Text {
|
|
id: trailing
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.padM
|
|
text: root.selected ? root.badge : ""
|
|
color: Theme.alpha(Theme.ink, 0.55)
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fsMicro
|
|
font.letterSpacing: 2
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
HoverHandler {
|
|
onHoveredChanged: root.hovered = hovered
|
|
}
|
|
|
|
TapHandler {
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
onTapped: (point, button) => {
|
|
if (button === Qt.RightButton) root.pinRequested();
|
|
else root.activated();
|
|
}
|
|
}
|
|
}
|