app launcher
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user