1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-03 20:32:07 -04:00

launcher/dsearch: support for folder search and extra filters

This commit is contained in:
bbedward
2026-02-19 18:19:29 -05:00
parent e931829411
commit e67f1f79bc
7 changed files with 393 additions and 75 deletions

View File

@@ -162,6 +162,11 @@ Item {
} }
] ]
property string fileSearchType: "all"
property string fileSearchExt: ""
property string fileSearchFolder: ""
property string fileSearchSort: "score"
property string pluginFilter: "" property string pluginFilter: ""
property string activePluginName: "" property string activePluginName: ""
property var activePluginCategories: [] property var activePluginCategories: []
@@ -346,6 +351,10 @@ Item {
previousSearchMode = "all"; previousSearchMode = "all";
autoSwitchedToFiles = false; autoSwitchedToFiles = false;
isFileSearching = false; isFileSearching = false;
fileSearchType = "all";
fileSearchExt = "";
fileSearchFolder = "";
fileSearchSort = "score";
sections = []; sections = [];
flatModel = []; flatModel = [];
selectedFlatIndex = 0; selectedFlatIndex = 0;
@@ -399,6 +408,34 @@ Item {
performSearch(); performSearch();
} }
function setFileSearchType(type) {
if (fileSearchType === type)
return;
fileSearchType = type;
performFileSearch();
}
function setFileSearchExt(ext) {
if (fileSearchExt === ext)
return;
fileSearchExt = ext;
performFileSearch();
}
function setFileSearchFolder(folder) {
if (fileSearchFolder === folder)
return;
fileSearchFolder = folder;
performFileSearch();
}
function setFileSearchSort(sort) {
if (fileSearchSort === sort)
return;
fileSearchSort = sort;
performFileSearch();
}
function clearPluginFilter() { function clearPluginFilter() {
if (pluginFilter) { if (pluginFilter) {
pluginFilter = ""; pluginFilter = "";
@@ -832,10 +869,20 @@ Item {
var params = { var params = {
limit: 20, limit: 20,
fuzzy: true, fuzzy: true,
sort: "score", sort: fileSearchSort || "score",
desc: true desc: true
}; };
if (DSearchService.supportsTypeFilter) {
params.type = (fileSearchType && fileSearchType !== "all") ? fileSearchType : "all";
}
if (fileSearchExt) {
params.ext = fileSearchExt;
}
if (fileSearchFolder) {
params.folder = fileSearchFolder;
}
DSearchService.search(fileQuery, params, function (response) { DSearchService.search(fileQuery, params, function (response) {
isFileSearching = false; isFileSearching = false;
if (response.error) if (response.error)
@@ -845,34 +892,73 @@ Item {
for (var i = 0; i < hits.length; i++) { for (var i = 0; i < hits.length; i++) {
var hit = hits[i]; var hit = hits[i];
var docTypes = hit.locations?.doc_type;
var isDir = docTypes ? !!docTypes["dir"] : false;
fileItems.push(transformFileResult({ fileItems.push(transformFileResult({
path: hit.id || "", path: hit.id || "",
score: hit.score || 0 score: hit.score || 0,
is_dir: isDir
})); }));
} }
var fileSection = { var fileSections = [];
id: "files", var showType = fileSearchType || "all";
title: I18n.tr("Files"),
icon: "folder", if (showType === "all" && DSearchService.supportsTypeFilter) {
priority: 4, var onlyFiles = [];
items: fileItems, var onlyDirs = [];
collapsed: collapsedSections["files"] || false, for (var j = 0; j < fileItems.length; j++) {
flatStartIndex: 0 if (fileItems[j].data?.is_dir)
}; onlyDirs.push(fileItems[j]);
else
onlyFiles.push(fileItems[j]);
}
if (onlyFiles.length > 0) {
fileSections.push({
id: "files",
title: I18n.tr("Files"),
icon: "insert_drive_file",
priority: 4,
items: onlyFiles,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
});
}
if (onlyDirs.length > 0) {
fileSections.push({
id: "folders",
title: I18n.tr("Folders"),
icon: "folder",
priority: 4.1,
items: onlyDirs,
collapsed: collapsedSections["folders"] || false,
flatStartIndex: 0
});
}
} else {
var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
if (fileItems.length > 0) {
fileSections.push({
id: "files",
title: filesTitle,
icon: filesIcon,
priority: 4,
items: fileItems,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
});
}
}
var newSections; var newSections;
if (searchMode === "files") { if (searchMode === "files") {
newSections = fileItems.length > 0 ? [fileSection] : []; newSections = fileSections;
} else { } else {
var existingNonFile = sections.filter(function (s) { var existingNonFile = sections.filter(function (s) {
return s.id !== "files"; return s.id !== "files" && s.id !== "folders";
}); });
if (fileItems.length > 0) { newSections = existingNonFile.concat(fileSections);
newSections = existingNonFile.concat([fileSection]);
} else {
newSections = existingNonFile;
}
} }
newSections.sort(function (a, b) { newSections.sort(function (a, b) {
return a.priority - b.priority; return a.priority - b.priority;
@@ -918,7 +1004,7 @@ Item {
} }
function transformFileResult(file) { function transformFileResult(file) {
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path")); return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal"));
} }
function detectTrigger(query) { function detectTrigger(query) {
@@ -1586,6 +1672,9 @@ Item {
case "copy_path": case "copy_path":
copyToClipboard(item.data.path); copyToClipboard(item.data.path);
break; break;
case "open_terminal":
openTerminal(item.data.path);
break;
case "copy": case "copy":
copyToClipboard(item.name); copyToClipboard(item.name);
break; break;
@@ -1667,6 +1756,16 @@ Item {
Qt.openUrlExternally("file://" + folder); Qt.openUrlExternally("file://" + folder);
} }
function openTerminal(path) {
if (!path)
return;
var terminal = Quickshell.env("TERMINAL") || "xterm";
Quickshell.execDetached({
command: [terminal],
workingDirectory: path
});
}
function copyToClipboard(text) { function copyToClipboard(text) {
if (!text) if (!text)
return; return;

View File

@@ -107,6 +107,10 @@ Item {
spotlightContent.controller.activePluginId = ""; spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = ""; spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = ""; spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all";
spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score";
spotlightContent.controller.collapsedSections = {}; spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0; spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null; spotlightContent.controller.selectedItem = null;

View File

@@ -116,31 +116,43 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
}; };
} }
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel) { function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel, openTerminalLabel) {
var filename = file.path ? file.path.split("/").pop() : ""; var filename = file.path ? file.path.split("/").pop() : "";
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : ""; var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
var isDir = file.is_dir || false;
var actions = [];
if (isDir) {
if (openTerminalLabel) {
actions.push({
name: openTerminalLabel,
icon: "terminal",
action: "open_terminal"
});
}
} else {
actions.push({
name: openFolderLabel,
icon: "folder_open",
action: "open_folder"
});
}
actions.push({
name: copyPathLabel,
icon: "content_copy",
action: "copy_path"
});
return { return {
id: file.path || "", id: file.path || "",
type: "file", type: "file",
name: filename, name: filename,
subtitle: dirname, subtitle: dirname,
icon: Utils.getFileIcon(filename), icon: isDir ? "folder" : Utils.getFileIcon(filename),
iconType: "material", iconType: "material",
section: "files", section: "files",
data: file, data: file,
actions: [ actions: actions,
{
name: openFolderLabel,
icon: "folder_open",
action: "open_folder"
},
{
name: copyPathLabel,
icon: "content_copy",
action: "copy_path"
}
],
primaryAction: { primaryAction: {
name: openLabel, name: openLabel,
icon: "open_in_new", icon: "open_in_new",

View File

@@ -549,8 +549,151 @@ FocusScope {
} }
Item { Item {
id: fileFilterRow
width: parent.width width: parent.width
height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 2) height: showFileFilters ? fileFilterContent.height : 0
visible: showFileFilters
readonly property bool showFileFilters: controller.searchMode === "files"
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
id: fileFilterContent
width: parent.width
spacing: Theme.spacingS
Row {
id: typeChips
anchors.verticalCenter: parent.verticalCenter
spacing: 2
visible: DSearchService.supportsTypeFilter
Repeater {
model: [
{
id: "all",
label: I18n.tr("All"),
icon: "search"
},
{
id: "file",
label: I18n.tr("Files"),
icon: "insert_drive_file"
},
{
id: "dir",
label: I18n.tr("Folders"),
icon: "folder"
}
]
Rectangle {
required property var modelData
required property int index
width: chipContent.width + Theme.spacingM * 2
height: sortDropdown.height
radius: Theme.cornerRadius
color: controller.fileSearchType === modelData.id || chipArea.containsMouse ? Theme.primaryContainer : "transparent"
Row {
id: chipContent
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: modelData.icon
size: 14
color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceVariantText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceText
}
}
MouseArea {
id: chipArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: controller.setFileSearchType(modelData.id)
}
}
}
}
Rectangle {
width: 1
height: 20
anchors.verticalCenter: parent.verticalCenter
color: Theme.outlineMedium
visible: typeChips.visible
}
DankDropdown {
id: sortDropdown
anchors.verticalCenter: parent.verticalCenter
width: Math.min(130, parent.width / 3)
compactMode: true
dropdownWidth: 130
popupWidth: 150
maxPopupHeight: 200
currentValue: {
switch (controller.fileSearchSort) {
case "score":
return I18n.tr("Score");
case "name":
return I18n.tr("Name");
case "modified":
return I18n.tr("Modified");
case "size":
return I18n.tr("Size");
default:
return I18n.tr("Score");
}
}
options: [I18n.tr("Score"), I18n.tr("Name"), I18n.tr("Modified"), I18n.tr("Size")]
onValueChanged: value => {
var sortMap = {};
sortMap[I18n.tr("Score")] = "score";
sortMap[I18n.tr("Name")] = "name";
sortMap[I18n.tr("Modified")] = "modified";
sortMap[I18n.tr("Size")] = "size";
controller.setFileSearchSort(sortMap[value] || "score");
}
}
DankTextField {
id: extFilterField
anchors.verticalCenter: parent.verticalCenter
width: Math.min(100, parent.width / 4)
height: sortDropdown.height
placeholderText: I18n.tr("ext")
font.pixelSize: Theme.fontSizeSmall
showClearButton: text.length > 0
onTextChanged: {
controller.setFileSearchExt(text.trim());
}
}
}
}
Item {
width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: root.parentModal?.isClosing ? 0 : 1 opacity: root.parentModal?.isClosing ? 0 : 1
ResultsList { ResultsList {
@@ -586,6 +729,9 @@ FocusScope {
function onSearchQueryRequested(query) { function onSearchQueryRequested(query) {
searchField.text = query; searchField.text = query;
} }
function onModeChanged() {
extFilterField.text = "";
}
} }
FocusScope { FocusScope {

View File

@@ -113,6 +113,7 @@ Rectangle {
font.family: Theme.fontFamily font.family: Theme.fontFamily
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
clip: true
visible: (root.item?.subtitle ?? "").length > 0 visible: (root.item?.subtitle ?? "").length > 0
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
} }
@@ -181,7 +182,7 @@ Rectangle {
case "plugin": case "plugin":
return I18n.tr("Plugin"); return I18n.tr("Plugin");
case "file": case "file":
return I18n.tr("File"); return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
default: default:
return ""; return "";
} }

View File

@@ -435,7 +435,15 @@ Item {
var mode = root.controller?.searchMode ?? "all"; var mode = root.controller?.searchMode ?? "all";
switch (mode) { switch (mode) {
case "files": case "files":
return "folder_open"; var fileType = root.controller?.fileSearchType ?? "all";
switch (fileType) {
case "dir":
return "folder_open";
case "file":
return "insert_drive_file";
default:
return "folder_open";
}
case "plugins": case "plugins":
return "extension"; return "extension";
case "apps": case "apps":
@@ -465,7 +473,15 @@ Item {
return I18n.tr("Type to search files"); return I18n.tr("Type to search files");
if (root.controller.searchQuery.length < 2) if (root.controller.searchQuery.length < 2)
return I18n.tr("Type at least 2 characters"); return I18n.tr("Type at least 2 characters");
return I18n.tr("No files found"); var fileType = root.controller?.fileSearchType ?? "all";
switch (fileType) {
case "dir":
return I18n.tr("No folders found");
case "file":
return I18n.tr("No files found");
default:
return I18n.tr("No results found");
}
case "plugins": case "plugins":
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins"); return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
case "apps": case "apps":

View File

@@ -1,8 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
@@ -13,6 +11,9 @@ Singleton {
property bool dsearchAvailable: false property bool dsearchAvailable: false
property int searchIdCounter: 0 property int searchIdCounter: 0
property int indexVersion: 0
property bool supportsTypeFilter: false
property bool versionChecked: false
signal searchResultsReceived(var results) signal searchResultsReceived(var results)
signal statsReceived(var stats) signal statsReceived(var stats)
@@ -26,118 +27,157 @@ Singleton {
stdout: SplitParser { stdout: SplitParser {
onRead: line => { onRead: line => {
if (line && line.trim().length > 0) { if (line && line.trim().length > 0) {
root.dsearchAvailable = true root.dsearchAvailable = true;
} }
} }
} }
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
root.dsearchAvailable = false root.dsearchAvailable = false;
} else {
root._checkVersion();
} }
} }
} }
function _checkVersion() {
Proc.runCommand("dsearch-version", ["dsearch", "version", "--json"], (stdout, exitCode) => {
root.versionChecked = true;
if (exitCode !== 0)
return;
const response = JSON.parse(stdout);
root.indexVersion = response.index_schema || 0;
root.supportsTypeFilter = root.indexVersion >= 2;
});
}
function ping(callback) { function ping(callback) {
if (!dsearchAvailable) { if (!dsearchAvailable) {
if (callback) { if (callback) {
callback({ "error": "dsearch not available" }) callback({
"error": "dsearch not available"
});
} }
return return;
} }
Proc.runCommand("dsearch-ping", ["dsearch", "ping", "--json"], (stdout, exitCode) => { Proc.runCommand("dsearch-ping", ["dsearch", "ping", "--json"], (stdout, exitCode) => {
if (callback) { if (callback) {
if (exitCode === 0) { if (exitCode === 0) {
try { try {
const response = JSON.parse(stdout) const response = JSON.parse(stdout);
callback({ "result": response }) callback({
"result": response
});
} catch (e) { } catch (e) {
callback({ "error": "failed to parse ping response" }) callback({
"error": "failed to parse ping response"
});
} }
} else { } else {
callback({ "error": "ping failed" }) callback({
"error": "ping failed"
});
} }
} }
}) });
} }
function search(query, params, callback) { function search(query, params, callback) {
if (!query || query.length === 0) { if (!query || query.length === 0) {
if (callback) { if (callback) {
callback({ "error": "query is required" }) callback({
"error": "query is required"
});
} }
return return;
} }
if (!dsearchAvailable) { if (!dsearchAvailable) {
if (callback) { if (callback) {
callback({ "error": "dsearch not available" }) callback({
"error": "dsearch not available"
});
} }
return return;
} }
const args = ["dsearch", "search", query, "--json"] const args = ["dsearch", "search", query, "--json"];
if (params) { if (params) {
if (params.limit !== undefined) { if (params.limit !== undefined) {
args.push("-n", String(params.limit)) args.push("-n", String(params.limit));
}
if (params.type) {
args.push("-t", params.type);
} }
if (params.ext) { if (params.ext) {
args.push("-e", params.ext) args.push("-e", params.ext);
}
if (params.folder) {
args.push("--folder", params.folder);
} }
if (params.field) { if (params.field) {
args.push("-f", params.field) args.push("-f", params.field);
} }
if (params.fuzzy) { if (params.fuzzy) {
args.push("--fuzzy") args.push("--fuzzy");
} }
if (params.sort) { if (params.sort) {
args.push("--sort", params.sort) args.push("--sort", params.sort);
} }
if (params.desc !== undefined) { if (params.desc !== undefined) {
args.push("--desc=" + (params.desc ? "true" : "false")) args.push("--desc=" + (params.desc ? "true" : "false"));
} }
if (params.minSize !== undefined) { if (params.minSize !== undefined) {
args.push("--min-size", String(params.minSize)) args.push("--min-size", String(params.minSize));
} }
if (params.maxSize !== undefined) { if (params.maxSize !== undefined) {
args.push("--max-size", String(params.maxSize)) args.push("--max-size", String(params.maxSize));
} }
} }
Proc.runCommand("dsearch-search", args, (stdout, exitCode) => { Proc.runCommand("dsearch-search", args, (stdout, exitCode) => {
if (exitCode === 0) { if (exitCode === 0) {
try { try {
const response = JSON.parse(stdout) const response = JSON.parse(stdout);
searchResultsReceived(response) searchResultsReceived(response);
if (callback) { if (callback) {
callback({ "result": response }) callback({
"result": response
});
} }
} catch (e) { } catch (e) {
const error = "failed to parse search response" const error = "failed to parse search response";
errorOccurred(error) errorOccurred(error);
if (callback) { if (callback) {
callback({ "error": error }) callback({
"error": error
});
} }
} }
} else if (exitCode === 124) { } else if (exitCode === 124) {
const error = "search timed out" const error = "search timed out";
errorOccurred(error) errorOccurred(error);
if (callback) { if (callback) {
callback({ "error": error }) callback({
"error": error
});
} }
} else { } else {
const error = "search failed" const error = "search failed";
errorOccurred(error) errorOccurred(error);
if (callback) { if (callback) {
callback({ "error": error }) callback({
"error": error
});
} }
} }
}, 100, 5000) }, 100, 5000);
} }
function rediscover() { function rediscover() {
checkProcess.running = true checkProcess.running = true;
} }
} }