import QtQuick import Quickshell import Quickshell.Io import qs.Commons Item { id: root property var pluginApi: null // Provider metadata property string name: pluginApi?.tr("provider.name") property var launcher: null property bool handleSearch: false property string supportedLayouts: "list" property bool supportsAutoPaste: false // Search state property var currentResults: [] property string currentQuery: "" property bool searching: false property int nextRequestId: 0 property int activeRequestId: 0 property int fileProcessRequestId: 0 property int dirProcessRequestId: 0 property int pendingProcessCount: 0 property bool currentRequestFailed: false property var pendingResultsByType: ({ "files": [], "dirs": [] }) property string fdCommandPath: "" property bool fdAvailable: false // Settings shortcuts property var cfg: pluginApi?.pluginSettings || ({}) property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) property bool showHidden: cfg.showHidden ?? defaults.showHidden ?? false property int maxResults: cfg.maxResults ?? defaults.maxResults ?? 0 property string fileOpener: cfg.fileOpener ?? defaults.fileOpener ?? "xdg-open" property string fdCommand: cfg.fdCommand ?? defaults.fdCommand ?? "fd" property string searchDirectory: cfg.searchDirectory ?? defaults.searchDirectory ?? "~" Process { id: fileSearchProcess running: false stdout: StdioCollector { id: fileStdoutCollector } stderr: StdioCollector { id: fileStderrCollector } onExited: function(exitCode) { root.handleSearchProcessExit("files", fileProcessRequestId, exitCode, fileStdoutCollector.text, fileStderrCollector.text); } } Process { id: dirSearchProcess running: false stdout: StdioCollector { id: dirStdoutCollector } stderr: StdioCollector { id: dirStderrCollector } onExited: function(exitCode) { root.handleSearchProcessExit("dirs", dirProcessRequestId, exitCode, dirStdoutCollector.text, dirStderrCollector.text); } } // Debounce timer for search Timer { id: searchDebouncer interval: 300 repeat: false onTriggered: root.executeSearch(root.currentQuery) } function init() { Logger.i("FileSearch", "Initializing plugin"); fdCommandPath = fdCommand; fdAvailable = true; Logger.i("FileSearch", "Using fd command:", fdCommandPath); } function handleCommand(searchText) { return searchText.startsWith(">file"); } function commands() { return [{ "name": ">file", "description": pluginApi?.tr("launcher.command.description"), "icon": "file-search", "isTablerIcon": true, "isImage": false, "onActivate": function() { launcher.setSearchText(">file "); } }]; } function getResults(searchText) { if (!searchText.startsWith(">file")) { return []; } if (!fdAvailable) { return [{ "name": pluginApi?.tr("launcher.errors.fdNotFound.title"), "description": pluginApi?.tr("launcher.errors.fdNotFound.description"), "icon": "alert-circle", "isTablerIcon": true, "isImage": false, "onActivate": function() {} }]; } var query = searchText.slice(5).trim(); if (query === "") { return [{ "name": pluginApi?.tr("launcher.prompts.emptyQuery.title"), "description": pluginApi?.tr("launcher.prompts.emptyQuery.description", { "root": displaySearchDirectory() }), "icon": "file-search", "isTablerIcon": true, "isImage": false, "onActivate": function() {} }]; } if (query !== currentQuery) { currentQuery = query; activeRequestId = 0; currentRequestFailed = false; searching = true; searchDebouncer.restart(); return [{ "name": pluginApi?.tr("launcher.prompts.searching.title"), "description": pluginApi?.tr("launcher.prompts.searching.description", { "query": query }), "icon": "refresh", "isTablerIcon": true, "isImage": false, "onActivate": function() {} }]; } if (searching) { return [{ "name": pluginApi?.tr("launcher.prompts.searching.title"), "description": pluginApi?.tr("launcher.prompts.searching.description", { "query": query }), "icon": "refresh", "isTablerIcon": true, "isImage": false, "onActivate": function() {} }]; } return currentResults; } function executeSearch(query) { if (!fdAvailable || query === "") { return; } Logger.d("FileSearch", "Executing search for:", query); if (fileSearchProcess.running) { fileSearchProcess.running = false; } if (dirSearchProcess.running) { dirSearchProcess.running = false; } var expandedDir = expandHomePath(searchDirectory); nextRequestId += 1; var requestId = nextRequestId; activeRequestId = requestId; currentRequestFailed = false; pendingProcessCount = 2; pendingResultsByType = ({ "files": [], "dirs": [] }); var commonArgs = []; if (showHidden) { commonArgs.push("--hidden"); } if (maxResults > 0) { commonArgs.push("--max-results", maxResults.toString()); } commonArgs.push("--base-directory", expandedDir); commonArgs.push("--absolute-path"); commonArgs.push("--color", "never"); commonArgs.push(query); var fileArgs = ["--type", "f"].concat(commonArgs); var dirArgs = ["--type", "d"].concat(commonArgs); fileProcessRequestId = requestId; fileSearchProcess.command = [fdCommandPath].concat(fileArgs); fileSearchProcess.running = true; dirProcessRequestId = requestId; dirSearchProcess.command = [fdCommandPath].concat(dirArgs); dirSearchProcess.running = true; Logger.d("FileSearch", "Running file command:", fdCommandPath, fileArgs.join(" ")); Logger.d("FileSearch", "Running dir command:", fdCommandPath, dirArgs.join(" ")); } function handleSearchProcessExit(kind, requestId, exitCode, stdoutText, stderrText) { if (requestId !== activeRequestId || currentRequestFailed) { return; } if (exitCode !== 0) { currentRequestFailed = true; searching = false; pendingProcessCount = 0; Logger.e("FileSearch", "fd command failed with exit code:", exitCode); Logger.e("FileSearch", "stderr:", stderrText); currentResults = [{ "name": pluginApi?.tr("launcher.errors.fdNotFound.title"), "description": pluginApi?.tr("launcher.errors.fdNotFound.description"), "icon": "alert-circle", "isTablerIcon": true, "onActivate": function() {} }]; if (launcher) { launcher.updateResults(); } return; } pendingResultsByType[kind] = parseRawPaths(stdoutText); pendingProcessCount -= 1; if (pendingProcessCount <= 0) { finalizeSearchResults(requestId); } } function finalizeSearchResults(requestId) { if (requestId !== activeRequestId || currentRequestFailed) { return; } var results = []; for (var i = 0; i < pendingResultsByType.dirs.length; i++) { results.push(formatFileEntry(pendingResultsByType.dirs[i], true)); } for (var j = 0; j < pendingResultsByType.files.length; j++) { results.push(formatFileEntry(pendingResultsByType.files[j], false)); } results = sortResults(results, currentQuery); if (maxResults > 0 && results.length > maxResults) { results = results.slice(0, maxResults); } if (results.length === 0) { results.push({ "name": pluginApi?.tr("launcher.prompts.noResults.title"), "description": pluginApi?.tr("launcher.prompts.noResults.description", { "query": currentQuery }), "icon": "file-off", "isTablerIcon": true, "isImage": false, "onActivate": function() {} }); searching = false; currentResults = results; if (launcher) { launcher.updateResults(); } return; } searching = false; currentResults = results; Logger.d("FileSearch", "Found", results.length, "results"); if (launcher) { launcher.updateResults(); } } function parseRawPaths(output) { var trimmed = output.trim(); if (trimmed === "") { return []; } return trimmed.split("\n").filter(function(line) { return line.trim() !== ""; }); } function expandHomePath(pathValue) { var expandedPath = pathValue; if (expandedPath.startsWith("~")) { expandedPath = Quickshell.env("HOME") + expandedPath.substring(1); } return expandedPath; } function displaySearchDirectory() { var expandedPath = expandHomePath(searchDirectory); var homeDir = Quickshell.env("HOME"); if (expandedPath.startsWith(homeDir)) { return "~" + expandedPath.slice(homeDir.length); } return expandedPath; } function sortResults(results, query) { var queryLower = query.toLowerCase(); results.sort(function(a, b) { var rankA = resultRank(a, queryLower); var rankB = resultRank(b, queryLower); if (rankA !== rankB) { return rankA - rankB; } var nameA = (a.name || "").toLowerCase(); var nameB = (b.name || "").toLowerCase(); if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } return (a.description || "").length - (b.description || "").length; }); return results; } function resultRank(result, queryLower) { var name = (result.name || "").toLowerCase(); var description = (result.description || "").toLowerCase(); var fullPath = description + "/" + name; if (name === queryLower) { return 0; } if (name.startsWith(queryLower)) { return 1; } if (name.indexOf(queryLower) !== -1) { return 2; } if (fullPath.indexOf(queryLower) !== -1) { return 3; } return 4; } function formatFileEntry(filePath, forcedIsDirectory) { var normalizedPath = filePath; while (normalizedPath.length > 1 && normalizedPath.endsWith("/")) { normalizedPath = normalizedPath.slice(0, -1); } var isDirectory = (forcedIsDirectory !== undefined) ? forcedIsDirectory : normalizedPath !== filePath; var parts = normalizedPath.split("/"); var filename = parts[parts.length - 1]; var parentPath = parts.slice(0, -1).join("/"); if (filename === "") { filename = normalizedPath; } var homeDir = Quickshell.env("HOME"); if (parentPath.startsWith(homeDir)) { parentPath = "~" + parentPath.slice(homeDir.length); } return { "name": filename, "description": parentPath, "icon": isDirectory ? "folder" : getFileIcon(filename), "isTablerIcon": true, "isImage": false, "singleLine": false, "onActivate": function() { root.openFile(normalizedPath); } }; } function getFileIcon(filename) { var ext = filename.split(".").pop().toLowerCase(); // Images if (["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp", "ico"].indexOf(ext) !== -1) { return "photo"; } // Documents if (["txt", "md", "pdf", "doc", "docx", "odt", "rtf"].indexOf(ext) !== -1) { return "file-text"; } // Code files if (["js", "ts", "py", "java", "cpp", "c", "h", "qml", "rs", "go", "rb", "php", "html", "css", "json", "xml", "yaml", "yml"].indexOf(ext) !== -1) { return "code"; } // Archives if (["zip", "tar", "gz", "bz2", "xz", "7z", "rar"].indexOf(ext) !== -1) { return "file-zip"; } // Audio if (["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"].indexOf(ext) !== -1) { return "music"; } // Video if (["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm"].indexOf(ext) !== -1) { return "video"; } // Spreadsheets if (["xls", "xlsx", "ods", "csv"].indexOf(ext) !== -1) { return "table"; } // Presentations if (["ppt", "pptx", "odp"].indexOf(ext) !== -1) { return "presentation"; } // Default return "file"; } function openFile(filePath) { Logger.i("FileSearch", "Opening file:", filePath); Quickshell.execDetached([fileOpener, filePath]); launcher.close(); } }