aboutsummaryrefslogtreecommitdiff
path: root/dotfiles/.config/noctalia/plugins/file-search/LauncherProvider.qml
diff options
context:
space:
mode:
Diffstat (limited to 'dotfiles/.config/noctalia/plugins/file-search/LauncherProvider.qml')
-rw-r--r--dotfiles/.config/noctalia/plugins/file-search/LauncherProvider.qml448
1 files changed, 448 insertions, 0 deletions
diff --git a/dotfiles/.config/noctalia/plugins/file-search/LauncherProvider.qml b/dotfiles/.config/noctalia/plugins/file-search/LauncherProvider.qml
new file mode 100644
index 0000000..3ca5e88
--- /dev/null
+++ b/dotfiles/.config/noctalia/plugins/file-search/LauncherProvider.qml
@@ -0,0 +1,448 @@
+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();
+ }
+}