app launcher

This commit is contained in:
Zacharias-Brohn
2025-11-10 01:40:56 +01:00
parent b2a270ed0b
commit 6aedefc4d7
11 changed files with 2441 additions and 31 deletions
+7
View File
@@ -0,0 +1,7 @@
import QtQuick
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
+72
View File
@@ -0,0 +1,72 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import qs
Item {
id: root
required property DesktopEntry modelData
implicitHeight: 48
anchors.left: parent?.left
anchors.right: parent?.right
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: event => onClicked(event)
function onClicked(): void {
Search.launch(root.modelData);
}
}
Item {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.margins: 4
IconImage {
id: icon
source: Quickshell.iconPath( root.modelData?.icon, "image-missing" )
implicitSize: parent.height * 0.8
anchors.verticalCenter: parent.verticalCenter
}
Item {
anchors.left: icon.right
anchors.leftMargin: 8
anchors.verticalCenter: icon.verticalCenter
implicitWidth: parent.width - icon.width
implicitHeight: name.implicitHeight + comment.implicitHeight
Text {
id: name
text: root.modelData?.name || qsTr("Unknown Application")
font.pointSize: 12
color: mouseArea.containsMouse ? "#ffffff" : "#cccccc"
elide: Text.ElideRight
}
Text {
id: comment
text: ( root.modelData?.comment || root.modelData?.genericName || root.modelData?.name ) ?? ""
font.pointSize: 10
color: mouseArea.containsMouse ? "#dddddd" : "#888888"
elide: Text.ElideRight
width: root.width - icon.width - 4 * 2
anchors.top: name.bottom
}
}
}
}
+210
View File
@@ -0,0 +1,210 @@
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs
Scope {
id: root
PanelWindow {
id: launcherWindow
anchors {
top: true
left: true
right: true
bottom: true
}
color: "transparent"
visible: false
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
HyprlandFocusGrab {
id: focusGrab
windows: [ searchInput ]
}
onVisibleChanged: {
if ( !visible ) {
searchInput.text = "";
}
}
GlobalShortcut {
appid: "z-cast"
name: "toggle-launcher"
onPressed: {
launcherWindow.visible = !launcherWindow.visible;
focusGrab.active = true;
searchInput.forceActiveFocus();
}
}
mask: Region { item: backgroundRect }
Rectangle {
id: backgroundRect
anchors.top: parent.top
anchors.topMargin: 200
implicitHeight: 800
implicitWidth: 600
x: Math.round(( parent.width - width ) / 2 )
color: "#801a1a1a"
radius: 8
border.color: "#444444"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
TextInput {
id: searchInput
Layout.fillWidth: true
Layout.preferredHeight: 30
Layout.leftMargin: 5
font.pixelSize: 20
color: "white"
horizontalAlignment: Text.AlignLeft
echoMode: TextInput.Normal
cursorDelegate: Rectangle {
id: cursor
property bool disableBlink
implicitWidth: 2
color: "white"
radius: 2
Connections {
target: searchInput
function onCursorPositionChanged(): void {
if ( searchInput.activeFocus && searchInput.cursorVisible ) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
}
Timer {
id: enableBlink
interval: 100
onTriggered: cursor.disableBlink = false
}
Timer {
running: searchInput.activeFocus && searchInput.cursorVisible && !cursor.disableBlink
repeat: true
triggeredOnStart: true
interval: 500
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
when: !searchInput.activeFocus || !searchInput.cursorVisible
cursor.opacity: 0
}
Behavior on opacity {
Anim {
duration: 200
}
}
}
Keys.onPressed: {
if ( event.key === Qt.Key_Down ) {
appListView.incrementCurrentIndex();
event.accepted = true;
} else if ( event.key === Qt.Key_Up ) {
appListView.decrementCurrentIndex();
event.accepted = true;
} else if ( event.key === Qt.Key_Return || event.key === Qt.Key_Enter ) {
if ( appListView.currentItem ) {
Search.launch(appListView.currentItem.modelData);
launcherWindow.visible = false;
}
event.accepted = true;
} else if ( event.key === Qt.Key_Escape ) {
launcherWindow.visible = false;
event.accepted = true;
}
}
}
Rectangle {
id: separator
Layout.fillWidth: true
Layout.preferredHeight: 1
color: "#444444"
}
Rectangle {
id: appListRect
Layout.fillWidth: true
Layout.fillHeight: true
color: "transparent"
clip: true
ListView {
id: appListView
anchors.fill: parent
model: ScriptModel {
id: appModel
onValuesChanged: {
appListView.currentIndex = 0;
}
}
preferredHighlightBegin: 0
preferredHighlightEnd: appListRect.height
highlightFollowsCurrentItem: false
highlightRangeMode: ListView.ApplyRange
focus: true
highlight: Rectangle {
radius: 4
color: "#FFFFFF"
opacity: 0.08
y: appListView.currentItem?.y
implicitWidth: appListView.width
implicitHeight: appListView.currentItem?.implicitHeight ?? 0
Behavior on y {
Anim {
duration: MaterialEasing.expressiveDefaultSpatialTime
easing.bezierCurve: MaterialEasing.expressiveFastSpatial
}
}
}
state: "apps"
states: [
State {
name: "apps"
PropertyChanges {
appModel.values: Search.search(searchInput.text)
appListView.delegate: appItem
}
}
]
Component {
id: appItem
AppItem {
}
}
}
}
}
}
}
}
+4 -2
View File
@@ -63,7 +63,7 @@ PanelWindow {
Rectangle {
id: backgroundRect
implicitWidth: 400
implicitHeight: 80
implicitHeight: 90
x: root.centerX - implicitWidth - 20
y: 34 + 20 + ( root.index * ( implicitHeight + 10 ))
color: "#801a1a1a"
@@ -80,6 +80,7 @@ PanelWindow {
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
IconImage {
source: root.notif.image
Layout.preferredWidth: 64
@@ -107,6 +108,7 @@ PanelWindow {
text: root.notif.summary
color: "white"
font.pointSize: 10
font.bold: true
elide: Text.ElideRight
wrapMode: Text.WordWrap
Layout.fillWidth: true
@@ -115,7 +117,7 @@ PanelWindow {
Text {
text: root.notif.body
color: "#dddddd"
font.pointSize: 8
font.pointSize: 10
elide: Text.ElideRight
wrapMode: Text.WordWrap
Layout.fillWidth: true
+9 -29
View File
@@ -275,39 +275,13 @@ PanelWindow {
required property var modelData
required property var index
width: parent.width
height: groupColumn.isExpanded ? ( modelData.actions.length > 1 ? 130 : 80 ) : 80
height: groupColumn.isExpanded ? ( modelData.actions.length > 1 ? 130 : 80 ) : ( groupColumn.notifications.length === 1 ? ( modelData.actions.length > 1 ? 130 : 80 ) : 80 )
color: "#801a1a1a"
border.color: "#555555"
border.width: 1
radius: 8
visible: groupColumn.notifications[0].id === modelData.id || groupColumn.isExpanded
// NumberAnimation {
// id: expandAnim
// target: groupHeader
// property: "y"
// duration: 300
// easing.type: Easing.OutCubic
// from: (( groupHeader.height / 2 ) * index )
// to: (( groupHeader.height + 60 ) * index )
// onStarted: {
// groupColumn.shouldShow = true;
// }
// }
//
// NumberAnimation {
// id: collapseAnim
// target: groupHeader
// property: "y"
// duration: 300
// easing.type: Easing.OutCubic
// from: (( groupHeader.height + 60 ) * index )
// to: (( groupHeader.height / 2 ) * index )
// onStopped: {
// groupColumn.isExpanded = false;
// }
// }
Connections {
target: groupColumn
function onShouldShowChanged() {
@@ -320,13 +294,15 @@ PanelWindow {
onVisibleChanged: {
if ( visible ) {
// expandAnim.start();
} else {
groupColumn.isExpanded = false;
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if ( groupColumn.isExpanded ) {
if ( groupColumn.isExpanded || groupColumn.notifications.length === 1 ) {
if ( groupHeader.modelData.actions.length === 1 ) {
groupHeader.modelData.actions[0].invoke();
}
@@ -371,6 +347,10 @@ PanelWindow {
font.pointSize: 10
color: "#dddddd"
elide: Text.ElideRight
lineHeightMode: Text.FixedHeight
lineHeight: 14
wrapMode: Text.WordWrap
maximumLineCount: 3
Layout.fillWidth: true
Layout.fillHeight: true
}
@@ -387,7 +367,7 @@ PanelWindow {
RowLayout {
spacing: 2
visible: groupColumn.isExpanded && groupHeader.modelData.actions.length > 1
visible: groupColumn.isExpanded ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : ( groupColumn.notifications.length === 1 ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : false )
height: 15
width: parent.width
+44
View File
@@ -0,0 +1,44 @@
pragma Singleton
import Caelestia
import Quickshell
import Quickshell.Io
Searcher {
id: root
readonly property string home: Quickshell.env("HOME")
function launch(entry: DesktopEntry): void {
appDb.incrementFrequency(entry.id);
console.log( "Search command:", entry.command );
Quickshell.execDetached({
command: ["app2unit", "--", ...entry.command],
workingDirectory: entry.workingDirectory
});
}
function search(search: string): list<var> {
keys = ["name"];
weights = [1];
const results = query(search).map(e => e.entry);
return results;
}
function selector(item: var): string {
return keys.map(k => item[k]).join(" ");
}
list: appDb.apps
useFuzzy: true
AppDb {
id: appDb
path: `${root.home}/.local/share/z-cast-qt/apps.sqlite`
entries: DesktopEntries.applications.values
}
}
+55
View File
@@ -0,0 +1,55 @@
import Quickshell
import "../scripts/fzf.js" as Fzf
import "../scripts/fuzzysort.js" as Fuzzy
import QtQuick
Singleton {
required property list<QtObject> list
property string key: "name"
property bool useFuzzy: false
property var extraOpts: ({})
// Extra stuff for fuzzy
property list<string> keys: [key]
property list<real> weights: [1]
readonly property var fzf: useFuzzy ? [] : new Fzf.Finder(list, Object.assign({
selector
}, extraOpts))
readonly property list<var> fuzzyPrepped: useFuzzy ? list.map(e => {
const obj = {
_item: e
};
for (const k of keys)
obj[k] = Fuzzy.prepare(e[k]);
return obj;
}) : []
function transformSearch(search: string): string {
return search;
}
function selector(item: var): string {
// Only for fzf
return item[key];
}
function query(search: string): list<var> {
search = transformSearch(search);
if (!search)
return [...list];
if (useFuzzy)
return Fuzzy.go(search, fuzzyPrepped, Object.assign({
all: true,
keys,
scoreFn: r => weights.reduce((a, w, i) => a + r[i].score * w, 0)
}, extraOpts)).map(r => r.obj._item);
return fzf.find(search).sort((a, b) => {
if (a.score === b.score)
return selector(a.item).trim().length - selector(b.item).trim().length;
return b.score - a.score;
}).map(r => r.item);
}
}