first commit
This commit is contained in:
393
launcher/Launcher.qml
Normal file
393
launcher/Launcher.qml
Normal file
@@ -0,0 +1,393 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../config" as Cfg
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool isTop: true
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
readonly property int launcherGap: 12
|
||||
|
||||
GlobalShortcut {
|
||||
id: launcherShortcut
|
||||
name: "toggle"
|
||||
onPressed: root.toggle()
|
||||
}
|
||||
|
||||
property bool _isOpen: false
|
||||
readonly property bool isOpen: _isOpen
|
||||
property string mode: "drun"
|
||||
property bool _keyboardNav: false
|
||||
property var allItems: []
|
||||
property bool loading: false
|
||||
property bool proxyMode: false
|
||||
|
||||
readonly property var filteredItems: {
|
||||
const q = searchInput.text.toLowerCase()
|
||||
if (!q) return allItems.slice(0, 80)
|
||||
return allItems.filter(i => i.name.toLowerCase().includes(q))
|
||||
}
|
||||
|
||||
function toggle() { _isOpen ? close() : open() }
|
||||
|
||||
function open() {
|
||||
closeTimer.stop()
|
||||
visible = true
|
||||
_isOpen = true
|
||||
searchInput.text = ""
|
||||
resultsList.currentIndex = -1
|
||||
loadItems()
|
||||
Qt.callLater(() => searchInput.forceActiveFocus())
|
||||
}
|
||||
|
||||
function close() {
|
||||
_isOpen = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onActiveChanged() {
|
||||
if (!root.active && root._isOpen) root.close()
|
||||
}
|
||||
}
|
||||
|
||||
function loadItems() {
|
||||
loading = true
|
||||
readCache()
|
||||
updateProc.command = [Cfg.Config.launcherScript, "--update-only"]
|
||||
updateProc.running = true
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
const path = Cfg.Config.launcherCacheDir + "/" + (mode === "drun" ? "drun.txt" : "run.txt")
|
||||
dataProc.rawOutput = ""
|
||||
dataProc.command = ["bash", "-c", "cat " + path]
|
||||
dataProc.running = true
|
||||
}
|
||||
|
||||
function launch(item) {
|
||||
if (!item || !item.exec) return
|
||||
|
||||
const cleanExec = item.exec.replace(/%[fFuUik]/g, "").trim()
|
||||
const finalExec = root.proxyMode ? "proxychains4 " + cleanExec : cleanExec
|
||||
const logPath = "/tmp/qml_launch.log"
|
||||
|
||||
launchProc.command = ["sh", "-c", finalExec + " > " + logPath + " 2>&1 &"]
|
||||
launchProc.running = true
|
||||
|
||||
console.log("Launching: " + finalExec)
|
||||
if (typeof close === "function") close()
|
||||
}
|
||||
|
||||
onModeChanged: if (_isOpen) loadItems()
|
||||
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "main-shell-launcher"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: _isOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
mask: Region { item: clipContainer }
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: {
|
||||
root.visible = false
|
||||
root.mode = "drun"
|
||||
root.allItems = []
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updateProc
|
||||
onRunningChanged: if (!running) root.readCache()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dataProc
|
||||
property string rawOutput: ""
|
||||
stdout: SplitParser { onRead: data => dataProc.rawOutput += data + "\n" }
|
||||
onRunningChanged: {
|
||||
if (!running && rawOutput) {
|
||||
const lines = rawOutput.trim().split("\n")
|
||||
const items = []
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const p = lines[i].split("\t")
|
||||
if (p.length >= 3) items.push({ name: p[0], exec: p[1], id: p[2], icon: p[3] || "" })
|
||||
}
|
||||
root.allItems = items
|
||||
root.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: launchProc }
|
||||
|
||||
Item {
|
||||
id: clipContainer
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 460
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + root.launcherGap) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + root.launcherGap)
|
||||
height: parent.height - root.barHeight - root.launcherGap
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: bubble
|
||||
width: parent.width
|
||||
property int targetHeight: Math.min(layout.implicitHeight + 28, 540)
|
||||
height: targetHeight
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: root._isOpen ? 0 : (root.isTop ? -bubble.height : bubble.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: root.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: root._isOpen ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
radius: Cfg.Config.popupRadius
|
||||
color: t.launcherBg
|
||||
border.color: t.launcherBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
|
||||
focus: root._isOpen
|
||||
Keys.onEscapePressed: root.close()
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 44
|
||||
radius: 10
|
||||
color: t.launcherInput
|
||||
border.color: searchInput.activeFocus ? t.launcherInputBorderFocus : t.launcherBorder
|
||||
|
||||
Behavior on color { NumberAnimation { duration: 180 } }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 8
|
||||
spacing: 10
|
||||
|
||||
TextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
color: t.launcherText
|
||||
font.pixelSize: 15
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
selectionColor: t.launcherAccent
|
||||
|
||||
onTextChanged: resultsList.currentIndex = -1
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (root.filteredItems.length > 0) {
|
||||
let idx = resultsList.currentIndex;
|
||||
if (idx === -1) idx = 0;
|
||||
root.launch(root.filteredItems[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
root._keyboardNav = true
|
||||
if (resultsList.currentIndex < 0) {
|
||||
resultsList.currentIndex = 0;
|
||||
} else {
|
||||
resultsList.currentIndex = Math.min(resultsList.currentIndex + 1, resultsList.count - 1)
|
||||
}
|
||||
resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
root._keyboardNav = true
|
||||
if (resultsList.currentIndex < 0) {
|
||||
resultsList.currentIndex = resultsList.count - 1;
|
||||
} else {
|
||||
resultsList.currentIndex = Math.max(resultsList.currentIndex - 1, 0)
|
||||
}
|
||||
resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
Keys.onTabPressed: root.mode = root.mode === "drun" ? "run" : "drun"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 28
|
||||
width: modeRow.implicitWidth + 10
|
||||
radius: 8
|
||||
color: t.launcherPill
|
||||
border.color: t.launcherBorder
|
||||
|
||||
RowLayout {
|
||||
id: modeRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: ["drun", "run"]
|
||||
delegate: Rectangle {
|
||||
height: 22
|
||||
width: label.implicitWidth + 14
|
||||
radius: 6
|
||||
color: root.mode === modelData ? t.launcherModeActiveBg : "transparent"
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: 10
|
||||
font.bold: root.mode === modelData
|
||||
color: root.mode === modelData ? t.launcherModeActiveText : t.launcherDim
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.mode = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: proxyBtn
|
||||
height: 28
|
||||
width: 110
|
||||
radius: 8
|
||||
color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.22) : t.launcherPill
|
||||
border.color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.65) : t.launcherBorder
|
||||
border.width: 1
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
RowLayout {
|
||||
id: proxyBtnRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
Text {
|
||||
text: ""
|
||||
font.pixelSize: 11
|
||||
color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
Text {
|
||||
text: "proxychains4"
|
||||
font.pixelSize: 10
|
||||
font.bold: root.proxyMode
|
||||
color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.proxyMode = !root.proxyMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: resultsList
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.min(contentHeight, 400)
|
||||
clip: true
|
||||
model: root.filteredItems
|
||||
currentIndex: -1
|
||||
spacing: 2
|
||||
|
||||
onModelChanged: currentIndex = -1
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
delegate: Rectangle {
|
||||
width: resultsList.width
|
||||
height: 38
|
||||
radius: 8
|
||||
property bool hovered: false
|
||||
|
||||
color: hovered ? t.launcherHover
|
||||
: (resultsList.currentIndex === index && index !== -1
|
||||
? t.launcherSelected : "transparent")
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 12
|
||||
|
||||
Item {
|
||||
width: 22; height: 22
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Image {
|
||||
id: appIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? "image://icon/" + modelData.icon : ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !appIcon.visible
|
||||
text: root.mode === "drun" ? "" : ""
|
||||
color: resultsList.currentIndex === index && index !== -1 ? t.launcherAccent : t.launcherDim
|
||||
font.pixelSize: 16
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
color: t.launcherText
|
||||
font.pixelSize: 14
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: parent.hovered = true
|
||||
onExited: parent.hovered = false
|
||||
onClicked: root.launch(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user