1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

spotlight: danksearch integration (indexed file search)

This commit is contained in:
bbedward
2025-10-27 09:49:17 -04:00
parent 0ac5b7bc87
commit 79b23ca829
8 changed files with 1210 additions and 57 deletions

View File

@@ -0,0 +1,237 @@
import QtQuick
import Quickshell.Io
import qs.Services
Item {
id: controller
property string searchQuery: ""
property alias model: fileModel
property int selectedIndex: 0
property bool keyboardNavigationActive: false
property bool isSearching: false
property int totalResults: 0
property string searchField: "filename"
signal searchCompleted
ListModel {
id: fileModel
}
function performSearch() {
if (!DSearchService.isConnected) {
model.clear()
totalResults = 0
isSearching = false
return
}
if (searchQuery.length === 0) {
model.clear()
totalResults = 0
isSearching = false
return
}
isSearching = true
const params = {
"limit": 50,
"fuzzy": true,
"sort_by": "score",
"sort_desc": true
}
if (searchField && searchField !== "all") {
params.field = searchField
}
DSearchService.search(searchQuery, params, response => {
if (response.error) {
model.clear()
totalResults = 0
isSearching = false
return
}
if (response.result) {
updateModel(response.result)
}
isSearching = false
searchCompleted()
})
}
function updateModel(result) {
model.clear()
totalResults = result.total_hits || 0
selectedIndex = 0
keyboardNavigationActive = true
if (!result.hits || result.hits.length === 0) {
selectedIndex = -1
keyboardNavigationActive = false
return
}
for (var i = 0; i < result.hits.length; i++) {
const hit = result.hits[i]
const filePath = hit.id || ""
const fileName = getFileName(filePath)
const fileExt = getFileExtension(fileName)
const fileType = determineFileType(fileName, filePath)
const dirPath = getDirPath(filePath)
model.append({
"filePath": filePath,
"fileName": fileName,
"fileExtension": fileExt,
"fileType": fileType,
"dirPath": dirPath,
"score": hit.score || 0
})
}
}
function getFileName(path) {
const parts = path.split('/')
return parts[parts.length - 1] || path
}
function getFileExtension(fileName) {
const parts = fileName.split('.')
if (parts.length > 1) {
return parts[parts.length - 1].toLowerCase()
}
return ""
}
function getDirPath(path) {
const lastSlash = path.lastIndexOf('/')
if (lastSlash > 0) {
return path.substring(0, lastSlash)
}
return ""
}
function determineFileType(fileName, filePath) {
const ext = getFileExtension(fileName)
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"]
if (imageExts.includes(ext)) {
return "image"
}
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"]
if (videoExts.includes(ext)) {
return "video"
}
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"]
if (audioExts.includes(ext)) {
return "audio"
}
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"]
if (codeExts.includes(ext)) {
return "code"
}
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"]
if (docExts.includes(ext)) {
return "document"
}
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"]
if (archiveExts.includes(ext)) {
return "archive"
}
if (!ext || fileName.indexOf('.') === -1) {
return "binary"
}
return "file"
}
function selectNext() {
if (model.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, model.count - 1)
}
function selectPrevious() {
if (model.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0)
}
signal fileOpened
function openFile(filePath) {
if (!filePath || filePath.length === 0) {
return
}
let url = filePath
if (!url.startsWith("file://")) {
url = "file://" + filePath
}
Qt.openUrlExternally(url)
fileOpened()
}
function openFolder(filePath) {
if (!filePath || filePath.length === 0) {
return
}
const lastSlash = filePath.lastIndexOf('/')
if (lastSlash <= 0) {
return
}
const dirPath = filePath.substring(0, lastSlash)
let url = dirPath
if (!url.startsWith("file://")) {
url = "file://" + dirPath
}
Qt.openUrlExternally(url)
fileOpened()
}
function openSelected() {
if (model.count === 0 || selectedIndex < 0 || selectedIndex >= model.count) {
return
}
const item = model.get(selectedIndex)
if (item && item.filePath) {
openFile(item.filePath)
}
}
function reset() {
searchQuery = ""
model.clear()
selectedIndex = -1
keyboardNavigationActive = false
isSearching = false
totalResults = 0
}
onSearchQueryChanged: {
performSearch()
}
onSearchFieldChanged: {
performSearch()
}
}

View File

@@ -0,0 +1,155 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: entry
required property string filePath
required property string fileName
required property string fileExtension
required property string fileType
required property string dirPath
required property bool isSelected
required property int itemIndex
signal clicked()
readonly property int iconSize: 40
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: iconSize
height: iconSize
anchors.verticalCenter: parent.verticalCenter
Image {
id: imagePreview
anchors.fill: parent
source: fileType === "image" ? `file://${filePath}` : ""
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
asynchronous: true
visible: fileType === "image" && status === Image.Ready
sourceSize.width: 128
sourceSize.height: 128
}
MultiEffect {
anchors.fill: parent
source: imagePreview
maskEnabled: true
maskSource: imageMask
visible: fileType === "image" && imagePreview.status === Image.Ready
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: imageMask
width: iconSize
height: iconSize
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: getFileTypeColor()
visible: fileType !== "image" || imagePreview.status !== Image.Ready
StyledText {
anchors.centerIn: parent
text: getFileIconText()
font.pixelSize: fileExtension.length > 0 ? (fileExtension.length > 3 ? Theme.fontSizeSmall - 2 : Theme.fontSizeSmall) : Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: fileName
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideMiddle
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
width: parent.width
text: dirPath
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 1
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: entry.clicked()
}
function getFileTypeColor() {
switch (fileType) {
case "code":
return Theme.codeFileColor || Theme.primarySelected
case "document":
return Theme.docFileColor || Theme.secondarySelected
case "video":
return Theme.videoFileColor || Theme.tertiarySelected
case "audio":
return Theme.audioFileColor || Theme.errorSelected
case "archive":
return Theme.archiveFileColor || Theme.warningSelected
case "binary":
return Theme.binaryFileColor || Theme.surfaceDim
default:
return Theme.surfaceLight
}
}
function getFileIconText() {
if (fileType === "binary") {
return "bin"
}
if (fileExtension.length > 0) {
return fileExtension
}
return fileName.charAt(0).toUpperCase()
}
}

View File

@@ -0,0 +1,214 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: resultsContainer
property var fileSearchController: null
function resetScroll() {
filesList.contentY = 0
}
color: "transparent"
clip: true
DankListView {
id: filesList
property int itemHeight: 60
property int itemSpacing: Theme.spacingS
property bool keyboardNavigationActive: fileSearchController ? fileSearchController.keyboardNavigationActive : false
function ensureVisible(index) {
if (index < 0 || index >= count)
return
const itemY = index * (itemHeight + itemSpacing)
const itemBottom = itemY + itemHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.margins: Theme.spacingS
model: fileSearchController ? fileSearchController.model : null
currentIndex: fileSearchController ? fileSearchController.selectedIndex : -1
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
delegate: Rectangle {
required property int index
required property string filePath
required property string fileName
required property string fileExtension
required property string fileType
required property string dirPath
width: ListView.view.width
height: filesList.itemHeight
radius: Theme.cornerRadius
color: ListView.isCurrentItem ? Theme.primaryPressed : fileMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceContainerHigh
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: 40
height: 40
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: iconBackground
anchors.fill: parent
radius: width / 2
color: Theme.surfaceLight
visible: fileType !== "image"
Loader {
anchors.centerIn: parent
active: fileName.toLowerCase().startsWith("dockerfile")
sourceComponent: DankNFIcon {
name: "docker"
size: Theme.fontSizeLarge
color: Theme.surfaceText
}
}
StyledText {
anchors.centerIn: parent
text: fileExtension ? (fileExtension.length > 4 ? fileExtension.substring(0, 4) : fileExtension) : "?"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Bold
visible: !fileName.toLowerCase().startsWith("dockerfile")
}
}
Loader {
anchors.fill: parent
active: fileType === "image"
sourceComponent: Image {
anchors.fill: parent
source: "file://" + filePath
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: 40
height: 40
radius: 20
}
}
}
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 40 - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: fileName || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideMiddle
maximumLineCount: 1
}
StyledText {
width: parent.width
text: dirPath || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
maximumLineCount: 1
}
}
}
MouseArea {
id: fileMouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
z: 10
onEntered: () => {
if (fileSearchController && filesList.keyboardNavigationActive) {
fileSearchController.keyboardNavigationActive = false
}
filesList.currentIndex = index
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (fileSearchController)
fileSearchController.openFile(filePath)
} else if (mouse.button === Qt.RightButton) {
if (fileSearchController)
fileSearchController.openFolder(filePath)
}
}
}
}
}
Item {
anchors.fill: parent
visible: !fileSearchController || !fileSearchController.model || fileSearchController.model.count === 0
StyledText {
property string displayText: {
if (!fileSearchController) {
return ""
}
if (!DSearchService.isConnected) {
return I18n.tr("DankSearch not available")
}
if (fileSearchController.isSearching) {
return I18n.tr("Searching...")
}
if (fileSearchController.searchQuery.length === 0) {
return I18n.tr("Enter a search query")
}
if (!fileSearchController.model || fileSearchController.model.count === 0) {
return I18n.tr("No files found")
}
return ""
}
text: displayText
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: displayText.length > 0
}
}
}

View File

@@ -11,10 +11,40 @@ Item {
property alias appLauncher: appLauncher
property alias searchField: searchField
property alias fileSearchController: fileSearchController
property var parentModal: null
property string searchMode: "apps"
function resetScroll() {
resultsView.resetScroll()
if (searchMode === "apps") {
resultsView.resetScroll()
} else {
fileSearchResults.resetScroll()
}
}
function updateSearchMode() {
if (searchField.text.startsWith("/")) {
if (searchMode !== "files") {
searchMode = "files"
}
const query = searchField.text.substring(1)
fileSearchController.searchQuery = query
} else {
if (searchMode !== "apps") {
searchMode = "apps"
fileSearchController.reset()
appLauncher.searchQuery = searchField.text
}
}
}
onSearchModeChanged: {
if (searchMode === "files") {
appLauncher.keyboardNavigationActive = false
} else {
fileSearchController.keyboardNavigationActive = false
}
}
anchors.fill: parent
@@ -27,59 +57,95 @@ Item {
event.accepted = true
} else if (event.key === Qt.Key_Down) {
appLauncher.selectNext()
if (searchMode === "apps") {
appLauncher.selectNext()
} else {
fileSearchController.selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious()
if (searchMode === "apps") {
appLauncher.selectPrevious()
} else {
fileSearchController.selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") {
} else if (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
event.accepted = true
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") {
} else if (event.key === Qt.Key_Left && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
event.accepted = true
} else if (event.key == Qt.Key_J && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext()
if (searchMode === "apps") {
appLauncher.selectNext()
} else {
fileSearchController.selectNext()
}
event.accepted = true
} else if (event.key == Qt.Key_K && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious()
if (searchMode === "apps") {
appLauncher.selectPrevious()
} else {
fileSearchController.selectPrevious()
}
event.accepted = true
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
} else if (event.key == Qt.Key_L && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
event.accepted = true
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && appLauncher.viewMode === "grid") {
} else if (event.key == Qt.Key_H && event.modifiers & Qt.ControlModifier && searchMode === "apps" && appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
event.accepted = true
} else if (event.key === Qt.Key_Tab) {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
} else {
appLauncher.selectNext()
}
} else {
appLauncher.selectNext()
fileSearchController.selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_Backtab) {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
} else {
appLauncher.selectPrevious()
}
} else {
appLauncher.selectPrevious()
fileSearchController.selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectNextInRow()
} else {
appLauncher.selectNext()
}
} else {
appLauncher.selectNext()
fileSearchController.selectNext()
}
event.accepted = true
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
if (searchMode === "apps") {
if (appLauncher.viewMode === "grid") {
appLauncher.selectPreviousInRow()
} else {
appLauncher.selectPrevious()
}
} else {
appLauncher.selectPrevious()
fileSearchController.selectPrevious()
}
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
appLauncher.launchSelected()
if (searchMode === "apps") {
appLauncher.launchSelected()
} else if (searchMode === "files") {
fileSearchController.openSelected()
}
event.accepted = true
}
}
@@ -98,6 +164,15 @@ Item {
}
}
FileSearchController {
id: fileSearchController
onFileOpened: () => {
if (parentModal)
parentModal.hide()
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
@@ -118,7 +193,7 @@ Item {
backgroundColor: Theme.surfaceContainerHigh
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconName: searchMode === "files" ? "folder" : "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
@@ -126,14 +201,18 @@ Item {
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: parentModal ? parentModal.spotlightOpen : true
placeholderText: ""
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
placeholderText: searchMode === "files" ? "Search files..." : "Search apps..."
ignoreLeftRightKeys: searchMode === "files" || appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [spotlightKeyHandler]
text: appLauncher.searchQuery
onTextEdited: () => {
appLauncher.searchQuery = text
}
onTextChanged: {
if (searchMode === "apps") {
appLauncher.searchQuery = text
}
}
onTextEdited: {
updateSearchMode()
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
if (parentModal)
@@ -141,12 +220,18 @@ Item {
event.accepted = true
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected()
else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0))
if (searchMode === "apps") {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected()
else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0))
} else if (searchMode === "files") {
if (fileSearchController.model.count > 0)
fileSearchController.openSelected()
}
event.accepted = true
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key
=== Qt.Key_Backtab || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false
}
}
@@ -154,7 +239,7 @@ Item {
Row {
spacing: Theme.spacingXS
visible: appLauncher.model.count > 0
visible: searchMode === "apps" && appLauncher.model.count > 0
anchors.verticalCenter: parent.verticalCenter
Rectangle {
@@ -207,12 +292,116 @@ Item {
}
}
}
Row {
spacing: Theme.spacingXS
visible: searchMode === "files"
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: filenameFilterButton
width: 36
height: 36
radius: Theme.cornerRadius
color: fileSearchController.searchField === "filename" ? Theme.primaryHover : filenameFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "title"
size: 18
color: fileSearchController.searchField === "filename" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: filenameFilterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
fileSearchController.searchField = "filename"
}
onEntered: {
filenameTooltipLoader.active = true
Qt.callLater(() => {
if (filenameTooltipLoader.item) {
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
filenameTooltipLoader.item.show(I18n.tr("Search filenames"), p.x, p.y, null)
}
})
}
onExited: {
if (filenameTooltipLoader.item)
filenameTooltipLoader.item.hide()
filenameTooltipLoader.active = false
}
}
}
Rectangle {
id: contentFilterButton
width: 36
height: 36
radius: Theme.cornerRadius
color: fileSearchController.searchField === "body" ? Theme.primaryHover : contentFilterArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "description"
size: 18
color: fileSearchController.searchField === "body" ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: contentFilterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
fileSearchController.searchField = "body"
}
onEntered: {
contentTooltipLoader.active = true
Qt.callLater(() => {
if (contentTooltipLoader.item) {
const p = mapToItem(null, width / 2, height + Theme.spacingXS)
contentTooltipLoader.item.show(I18n.tr("Search file contents"), p.x, p.y, null)
}
})
}
onExited: {
if (contentTooltipLoader.item)
contentTooltipLoader.item.hide()
contentTooltipLoader.active = false
}
}
}
}
}
SpotlightResults {
id: resultsView
appLauncher: spotlightKeyHandler.appLauncher
contextMenu: contextMenu
Item {
width: parent.width
height: parent.height - y
SpotlightResults {
id: resultsView
anchors.fill: parent
appLauncher: spotlightKeyHandler.appLauncher
contextMenu: contextMenu
visible: searchMode === "apps"
}
FileSearchResults {
id: fileSearchResults
anchors.fill: parent
fileSearchController: spotlightKeyHandler.fileSearchController
visible: searchMode === "files"
}
}
}
@@ -233,7 +422,6 @@ Item {
MouseArea {
// Prevent closing when clicking on the menu itself
x: contextMenu.x
y: contextMenu.y
width: contextMenu.width
@@ -241,4 +429,18 @@ Item {
onClicked: () => {}
}
}
Loader {
id: filenameTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Loader {
id: contentTooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

View File

@@ -20,10 +20,10 @@ DankModal {
open()
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
function showWithQuery(query) {
@@ -40,10 +40,10 @@ DankModal {
open()
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
function hide() {
@@ -58,6 +58,9 @@ DankModal {
spotlightContent.appLauncher.selectedIndex = 0
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
}
if (spotlightContent.fileSearchController) {
spotlightContent.fileSearchController.reset()
}
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll()
}
@@ -111,17 +114,17 @@ DankModal {
}
IpcHandler {
function open(): string {
function open(): string {
spotlightModal.show()
return "SPOTLIGHT_OPEN_SUCCESS"
}
function close(): string {
function close(): string {
spotlightModal.hide()
return "SPOTLIGHT_CLOSE_SUCCESS"
}
function toggle(): string {
function toggle(): string {
spotlightModal.toggle()
return "SPOTLIGHT_TOGGLE_SUCCESS"
}

View File

@@ -7,10 +7,6 @@ import qs.Widgets
Rectangle {
id: resultsContainer
// DEVELOPER NOTE: This component renders the Spotlight launcher (accessed via Mod+Space).
// Changes to launcher behavior, especially item rendering, filtering, or model structure,
// likely require corresponding updates in Modules/AppDrawer/AppLauncher.qml and vice versa.
property var appLauncher: null
property var contextMenu: null
@@ -19,8 +15,6 @@ Rectangle {
resultsGrid.contentY = 0
}
width: parent.width
height: parent.height - y
radius: Theme.cornerRadius
color: "transparent"
clip: true

311
Services/DSearchService.qml Normal file
View File

@@ -0,0 +1,311 @@
pragma Singleton
pragma ComponentBehavior
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property bool dsearchAvailable: false
property bool isConnected: false
property bool isConnecting: false
property int apiVersion: 0
readonly property int expectedApiVersion: 1
property bool handshakeReceived: false
property var pendingRequests: ({})
property int requestIdCounter: 0
signal connectionStateChanged
signal searchResultsReceived(var results)
signal statsReceived(var stats)
signal errorOccurred(string error)
signal apiVersionReceived(int version)
Component.onCompleted: {
discoverAndConnect()
}
function discoverAndConnect() {
if (requestSocket.connected) {
requestSocket.connected = false
Qt.callLater(() => {
performDiscovery()
})
return
}
performDiscovery()
}
function performDiscovery() {
dsearchAvailable = false
isConnecting = false
isConnected = false
requestSocket.connected = false
requestSocket.path = ""
const xdgRuntimeDir = Quickshell.env("XDG_RUNTIME_DIR")
if (xdgRuntimeDir && xdgRuntimeDir.length > 0) {
findSocketProcess.searchPath = xdgRuntimeDir
findSocketProcess.running = true
} else {
findSocketProcess.searchPath = "/tmp"
findSocketProcess.running = true
}
}
Process {
id: findSocketProcess
property string searchPath: ""
command: ["find", searchPath, "-maxdepth", "1", "-type", "s", "-name", "danksearch-*.sock"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split('\n').filter(line => line.length > 0)
if (lines.length > 0) {
const socketPath = lines[0]
testSocketProcess.socketPath = socketPath
testSocketProcess.running = true
} else {
if (findSocketProcess.searchPath !== "/tmp") {
findSocketProcess.searchPath = "/tmp"
findSocketProcess.running = true
} else {
root.dsearchAvailable = false
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.dsearchAvailable = false
}
}
}
Process {
id: testSocketProcess
property string socketPath: ""
command: ["test", "-S", socketPath]
running: false
onExited: exitCode => {
if (exitCode === 0) {
root.dsearchAvailable = true
requestSocket.path = socketPath
connectSocket()
} else {
root.dsearchAvailable = false
}
}
}
function connectSocket() {
if (!dsearchAvailable || isConnected || isConnecting) {
return
}
if (!requestSocket.path || requestSocket.path.length === 0) {
return
}
isConnecting = true
handshakeReceived = false
requestSocket.connected = true
}
DankSocket {
id: requestSocket
path: ""
connected: false
onConnectionStateChanged: {
if (!connected) {
root.isConnected = false
root.isConnecting = false
root.apiVersion = 0
root.handshakeReceived = false
root.dsearchAvailable = false
root.pendingRequests = {}
requestSocket.connected = false
requestSocket.path = ""
root.connectionStateChanged()
}
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0) {
return
}
try {
const message = JSON.parse(line)
if (!root.handshakeReceived && message.apiVersion !== undefined) {
handleHandshake(message)
} else {
handleResponse(message)
}
} catch (e) {
console.warn("DSearchService: Failed to parse message:", e)
}
}
}
}
function handleHandshake(message) {
handshakeReceived = true
apiVersion = message.apiVersion || 0
isConnected = true
isConnecting = false
connectionStateChanged()
apiVersionReceived(apiVersion)
}
function sendRequest(method, params, callback) {
if (!isConnected) {
if (callback) {
callback({
"error": "not connected to dsearch socket"
})
}
return
}
requestIdCounter++
const id = Date.now() + requestIdCounter
const request = {
"id": id,
"method": method
}
if (params && Object.keys(params).length > 0) {
request.params = params
}
if (callback) {
pendingRequests[id] = callback
}
requestSocket.send(request)
}
function handleResponse(response) {
const callback = pendingRequests[response.id]
if (callback) {
delete pendingRequests[response.id]
if (response.error) {
errorOccurred(response.error)
}
callback(response)
}
}
function ping(callback) {
sendRequest("ping", null, callback)
}
function search(query, params, callback) {
if (!query || query.length === 0) {
if (callback) {
callback({
"error": "query is required"
})
}
return
}
if (!isConnected) {
discoverAndConnect()
if (callback) {
callback({
"error": "not connected - attempting reconnection"
})
}
return
}
const searchParams = {
"query": query
}
if (params) {
for (const key in params) {
searchParams[key] = params[key]
}
}
sendRequest("search", searchParams, response => {
if (response.result) {
searchResultsReceived(response.result)
}
if (callback) {
callback(response)
}
})
}
function getStats(callback) {
sendRequest("stats", null, response => {
if (response.result) {
statsReceived(response.result)
}
if (callback) {
callback(response)
}
})
}
function sync(callback) {
sendRequest("sync", null, callback)
}
function reindex(callback) {
sendRequest("reindex", null, callback)
}
function watchStart(callback) {
sendRequest("watch.start", null, callback)
}
function watchStop(callback) {
sendRequest("watch.stop", null, callback)
}
function watchStatus(callback) {
sendRequest("watch.status", null, callback)
}
function rediscover() {
if (isConnected) {
requestSocket.connected = false
}
discoverAndConnect()
}
function disconnect() {
if (isConnected || isConnecting) {
requestSocket.connected = false
}
}
}

37
Widgets/DankNFIcon.qml Normal file
View File

@@ -0,0 +1,37 @@
import QtQuick
import qs.Common
Item {
id: root
property string name: ""
property alias size: icon.font.pixelSize
property alias color: icon.color
implicitWidth: icon.implicitWidth
implicitHeight: icon.implicitHeight
readonly property var iconMap: ({
"docker": "\uf21f"
})
FontLoader {
id: firaCodeFont
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
}
StyledText {
id: icon
anchors.fill: parent
font.family: firaCodeFont.name
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
text: root.iconMap[root.name] || ""
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
antialiasing: true
}
}