533 lines
23 KiB
QML
533 lines
23 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import QtQuick.Effects
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import Quickshell.Widgets
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
import "../../Helpers/FuzzySort.js" as Fuzzysort
|
|
import "../../Helpers/MathHelper.js" as MathHelper
|
|
|
|
NLoader {
|
|
id: appLauncher
|
|
isLoaded: false
|
|
// Clipboard state is persisted in Services/Clipboard.qml
|
|
content: Component {
|
|
NPanel {
|
|
id: appLauncherPanel
|
|
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
|
|
|
// No local timer/processes; use persistent Clipboard service
|
|
|
|
// Removed local clipboard processes; handled by Clipboard service
|
|
|
|
// Copy helpers via simple exec; avoid keeping processes alive locally
|
|
function copyImageBase64(mime, base64) {
|
|
Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`])
|
|
}
|
|
|
|
function copyText(text) {
|
|
Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`])
|
|
}
|
|
|
|
function updateClipboardHistory() {
|
|
Clipboard.refresh()
|
|
}
|
|
|
|
function selectNext() {
|
|
if (filteredEntries.length > 0) {
|
|
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1)
|
|
}
|
|
}
|
|
|
|
function selectPrev() {
|
|
if (filteredEntries.length > 0) {
|
|
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
}
|
|
}
|
|
|
|
function activateSelected() {
|
|
if (filteredEntries.length === 0)
|
|
return
|
|
|
|
var modelData = filteredEntries[selectedIndex]
|
|
if (modelData && modelData.execute) {
|
|
if (modelData.isCommand) {
|
|
modelData.execute()
|
|
return
|
|
} else {
|
|
modelData.execute()
|
|
}
|
|
appLauncherPanel.hide()
|
|
}
|
|
}
|
|
|
|
property var desktopEntries: DesktopEntries.applications.values
|
|
property string searchText: ""
|
|
property int selectedIndex: 0
|
|
property var filteredEntries: {
|
|
console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0)
|
|
if (!desktopEntries || desktopEntries.length === 0) {
|
|
console.log("[AppLauncher] No desktop entries available")
|
|
return []
|
|
}
|
|
|
|
// Filter out entries that shouldn't be displayed
|
|
var visibleEntries = desktopEntries.filter(entry => {
|
|
if (!entry || entry.noDisplay) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
console.log("[AppLauncher] Visible entries:", visibleEntries.length)
|
|
|
|
var query = searchText ? searchText.toLowerCase() : ""
|
|
var results = []
|
|
|
|
// Handle special commands
|
|
if (query === ">") {
|
|
results.push({
|
|
"isCommand": true,
|
|
"name": ">calc",
|
|
"content": "Calculator - evaluate mathematical expressions",
|
|
"icon": "calculate",
|
|
"execute": function () {
|
|
searchText = ">calc "
|
|
searchInput.cursorPosition = searchText.length
|
|
}
|
|
})
|
|
|
|
results.push({
|
|
"isCommand": true,
|
|
"name": ">clip",
|
|
"content": "Clipboard history - browse and restore clipboard items",
|
|
"icon": "content_paste",
|
|
"execute": function () {
|
|
searchText = ">clip "
|
|
searchInput.cursorPosition = searchText.length
|
|
}
|
|
})
|
|
|
|
return results
|
|
}
|
|
|
|
// Handle clipboard history
|
|
if (query.startsWith(">clip")) {
|
|
if (!Clipboard.initialized) {
|
|
Clipboard.refresh()
|
|
}
|
|
const searchTerm = query.slice(5).trim()
|
|
|
|
Clipboard.history.forEach(function (clip, index) {
|
|
let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip
|
|
|
|
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) {
|
|
let entry
|
|
if (clip.type === 'image') {
|
|
entry = {
|
|
"isClipboard": true,
|
|
"name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
|
|
"content": "Image: " + clip.mimeType,
|
|
"icon": "image",
|
|
"type": 'image',
|
|
"data": clip.data,
|
|
"execute": function () {
|
|
const base64Data = clip.data.split(',')[1]
|
|
copyImageBase64(clip.mimeType, base64Data)
|
|
Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType])
|
|
}
|
|
}
|
|
} else {
|
|
const textContent = clip.content || clip
|
|
let displayContent = textContent
|
|
let previewContent = ""
|
|
|
|
displayContent = displayContent.replace(/\s+/g, ' ').trim()
|
|
|
|
if (displayContent.length > 50) {
|
|
previewContent = displayContent
|
|
displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."
|
|
}
|
|
|
|
entry = {
|
|
"isClipboard": true,
|
|
"name": displayContent,
|
|
"content": previewContent || textContent,
|
|
"icon": "content_paste",
|
|
"execute": function () {
|
|
Quickshell.clipboardText = String(textContent)
|
|
copyText(String(textContent))
|
|
var preview = (textContent.length > 50) ? textContent.slice(0, 50) + "…" : textContent
|
|
Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview])
|
|
}
|
|
}
|
|
}
|
|
results.push(entry)
|
|
}
|
|
})
|
|
|
|
if (results.length === 0) {
|
|
results.push({
|
|
"isClipboard": true,
|
|
"name": "No clipboard history",
|
|
"content": "No matching clipboard entries found",
|
|
"icon": "content_paste_off"
|
|
})
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// Handle calculator
|
|
if (query.startsWith(">calc")) {
|
|
var expr = searchText.slice(5).trim()
|
|
if (expr && isMathExpression(expr)) {
|
|
var value = safeEval(expr)
|
|
if (value !== null && value !== undefined && value !== "") {
|
|
var formattedResult = MathHelper.MathHelper.formatResult(value)
|
|
results.push({
|
|
"isCalculator": true,
|
|
"name": `Calculator: ${expr} = ${formattedResult}`,
|
|
"result": value,
|
|
"expr": expr,
|
|
"icon": "calculate",
|
|
"execute": function () {
|
|
Quickshell.clipboardText = String(formattedResult)
|
|
clipboardTextCopyProcess.copyText(String(formattedResult))
|
|
Quickshell.execDetached(
|
|
["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// Regular app search
|
|
if (!query) {
|
|
results = results.concat(visibleEntries.sort(function (a, b) {
|
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
|
}))
|
|
} else {
|
|
var fuzzyResults = Fuzzysort.go(query, visibleEntries, {
|
|
"keys": ["name", "comment", "genericName"]
|
|
})
|
|
results = results.concat(fuzzyResults.map(function (r) {
|
|
return r.obj
|
|
}))
|
|
}
|
|
|
|
console.log("[AppLauncher] Filtered entries:", results.length)
|
|
return results
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
console.log("[AppLauncher] Component completed")
|
|
console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined')
|
|
if (typeof DesktopEntries !== 'undefined') {
|
|
console.log("[AppLauncher] DesktopEntries.entries:",
|
|
DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
|
|
}
|
|
// Start clipboard refresh immediately on open
|
|
updateClipboardHistory()
|
|
}
|
|
|
|
function isMathExpression(str) {
|
|
// Allow more characters for enhanced math functions
|
|
return /^[-+*/().0-9\s\w]+$/.test(str)
|
|
}
|
|
|
|
function safeEval(expr) {
|
|
return MathHelper.MathHelper.evaluate(expr)
|
|
}
|
|
|
|
// Main content container
|
|
Rectangle {
|
|
anchors.centerIn: parent
|
|
width: Math.min(700 * scaling, parent.width * 0.75)
|
|
height: Math.min(550 * scaling, parent.height * 0.8)
|
|
radius: 32 * scaling
|
|
color: Colors.colorSurface
|
|
border.color: Colors.colorOutline
|
|
border.width: Style.borderThin * scaling
|
|
|
|
// Subtle gradient background
|
|
gradient: Gradient {
|
|
GradientStop {
|
|
position: 0.0
|
|
color: Qt.lighter(Colors.colorSurface, 1.02)
|
|
}
|
|
GradientStop {
|
|
position: 1.0
|
|
color: Qt.darker(Colors.colorSurface, 1.1)
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginLarge * scaling
|
|
spacing: Style.marginMedium * scaling
|
|
|
|
// Search bar
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40 * scaling
|
|
Layout.bottomMargin: Style.marginMedium * scaling
|
|
radius: 20 * scaling
|
|
color: Colors.colorSurface
|
|
border.color: searchInput.activeFocus ? Colors.colorPrimary : Colors.colorOutline
|
|
border.width: searchInput.activeFocus ? 2 : 1
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.margins: 12 * scaling
|
|
spacing: 10 * scaling
|
|
|
|
Text {
|
|
text: "search"
|
|
font.family: "Material Symbols Outlined"
|
|
font.pointSize: 16 * scaling
|
|
color: searchInput.activeFocus ? Colors.colorPrimary : Colors.colorOnSurface
|
|
}
|
|
|
|
TextField {
|
|
id: searchInput
|
|
placeholderText: "Search applications..."
|
|
color: Colors.colorOnSurface
|
|
placeholderTextColor: Colors.colorOnSurface
|
|
background: null
|
|
font.pointSize: 13 * scaling
|
|
Layout.fillWidth: true
|
|
onTextChanged: {
|
|
searchText = text
|
|
selectedIndex = 0 // Reset selection when search changes
|
|
}
|
|
selectedTextColor: Colors.colorOnSurface
|
|
selectionColor: Colors.colorPrimary
|
|
padding: 0
|
|
verticalAlignment: TextInput.AlignVCenter
|
|
leftPadding: 0
|
|
rightPadding: 0
|
|
topPadding: 0
|
|
bottomPadding: 0
|
|
font.bold: true
|
|
Component.onCompleted: {
|
|
contentItem.cursorColor = Colors.colorOnSurface
|
|
contentItem.verticalAlignment = TextInput.AlignVCenter
|
|
// Focus the search bar by default
|
|
Qt.callLater(() => {
|
|
searchInput.forceActiveFocus()
|
|
})
|
|
}
|
|
onActiveFocusChanged: contentItem.cursorColor = Colors.colorOnSurface
|
|
|
|
Keys.onDownPressed: selectNext()
|
|
Keys.onUpPressed: selectPrev()
|
|
Keys.onEnterPressed: activateSelected()
|
|
Keys.onReturnPressed: activateSelected()
|
|
Keys.onEscapePressed: appLauncherPanel.hide()
|
|
}
|
|
}
|
|
|
|
Behavior on border.color {
|
|
ColorAnimation {
|
|
duration: 120
|
|
}
|
|
}
|
|
|
|
Behavior on border.width {
|
|
NumberAnimation {
|
|
duration: 120
|
|
}
|
|
}
|
|
}
|
|
|
|
// Applications list
|
|
ScrollView {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
|
|
|
ListView {
|
|
id: appsList
|
|
anchors.fill: parent
|
|
spacing: 4 * scaling
|
|
model: filteredEntries
|
|
currentIndex: selectedIndex
|
|
|
|
delegate: Rectangle {
|
|
width: appsList.width - Style.marginSmall * scaling
|
|
height: 56 * scaling
|
|
radius: 16 * scaling
|
|
property bool isSelected: index === selectedIndex
|
|
color: (appCardArea.containsMouse || isSelected) ? Qt.darker(
|
|
Colors.colorPrimary,
|
|
1.1) : Colors.colorSurface
|
|
border.color: (appCardArea.containsMouse
|
|
|| isSelected) ? Colors.colorPrimary : "transparent"
|
|
border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: 150
|
|
}
|
|
}
|
|
|
|
Behavior on border.color {
|
|
ColorAnimation {
|
|
duration: 150
|
|
}
|
|
}
|
|
|
|
Behavior on border.width {
|
|
NumberAnimation {
|
|
duration: 150
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Style.marginMedium * scaling
|
|
spacing: Style.marginMedium * scaling
|
|
|
|
// App icon with background
|
|
Rectangle {
|
|
Layout.preferredWidth: 40 * scaling
|
|
Layout.preferredHeight: 40 * scaling
|
|
radius: 14 * scaling
|
|
color: appCardArea.containsMouse ? Qt.darker(Colors.colorPrimary,
|
|
1.1) : Colors.colorSurfaceVariant
|
|
property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard
|
|
|| modelData.isCommand) || (iconImg.status === Image.Ready
|
|
&& iconImg.source !== ""
|
|
&& iconImg.status !== Image.Error
|
|
&& iconImg.source !== "")
|
|
visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode
|
|
|
|
// Clipboard image display
|
|
Image {
|
|
id: clipboardImage
|
|
anchors.fill: parent
|
|
anchors.margins: 6 * scaling
|
|
visible: modelData.type === 'image'
|
|
source: modelData.data || ""
|
|
fillMode: Image.PreserveAspectCrop
|
|
asynchronous: true
|
|
cache: true
|
|
}
|
|
|
|
IconImage {
|
|
id: iconImg
|
|
anchors.fill: parent
|
|
anchors.margins: 6 * scaling
|
|
asynchronous: true
|
|
source: modelData.isCalculator ? "calculate" : modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "")
|
|
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand
|
|
|| parent.iconLoaded) && modelData.type !== 'image'
|
|
}
|
|
|
|
// Fallback icon container
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.margins: 6 * scaling
|
|
radius: 10 * scaling
|
|
color: Colors.colorPrimary
|
|
opacity: 0.3
|
|
visible: !parent.iconLoaded
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard
|
|
|| modelData.isCommand)
|
|
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
|
|
font.pointSize: 18 * scaling
|
|
font.weight: Font.Bold
|
|
color: Colors.colorPrimary
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: 150
|
|
}
|
|
}
|
|
}
|
|
|
|
// App info
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 2 * scaling
|
|
|
|
NText {
|
|
text: modelData.name || "Unknown"
|
|
font.pointSize: 14 * scaling
|
|
font.weight: Font.Bold
|
|
color: Colors.colorOnSurface
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
NText {
|
|
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "")
|
|
font.pointSize: 11 * scaling
|
|
color: (appCardArea.containsMouse
|
|
|| isSelected) ? Colors.colorOnSurface : Colors.colorOnSurface
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
visible: text !== ""
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: appCardArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
|
|
onClicked: {
|
|
selectedIndex = index
|
|
activateSelected()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// No results message
|
|
NText {
|
|
text: searchText.trim() !== "" ? "No applications found" : "No applications available"
|
|
font.pointSize: Style.fontSizeLarge * scaling
|
|
color: Colors.colorOnSurface
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Layout.fillWidth: true
|
|
visible: filteredEntries.length === 0
|
|
}
|
|
|
|
// Results count
|
|
NText {
|
|
text: searchText.startsWith(
|
|
">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length
|
|
!== 1 ? 's' : ''}` : searchText.startsWith(
|
|
">calc") ? `${filteredEntries.length} result${filteredEntries.length
|
|
!== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}`
|
|
font.pointSize: Style.fontSizeSmall * scaling
|
|
color: Colors.colorOnSurface
|
|
horizontalAlignment: Text.AlignHCenter
|
|
Layout.fillWidth: true
|
|
visible: searchText.trim() !== ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|