#pragma once #include "PluginInterface.hpp" #include "GenericListItem.hpp" #include "files.hpp" #include "fuzzy.hpp" #include #include #include #include #include #include #include #include #include namespace plugins { class FileSearchPlugin : public SearchPlugin { public: // Constructor with configurable search and ignore directories explicit FileSearchPlugin( const std::vector &searchDirectories = getDefaultSearchDirs(), int maxDepth = 3, size_t maxFiles = 1000) : m_searchDirectories(searchDirectories), m_maxDepth(maxDepth), m_maxFiles(maxFiles) { // Initialize ignore directory names set m_ignoreDirNames = { "node_modules", "vendor", ".git", ".svn", ".hg", "build", "dist", "target", ".cache", "__pycache__", ".pytest_cache", ".mypy_cache", "coverage", ".coverage", ".tox", "venv", ".venv", "env", ".env" }; // Pre-load all files from search directories loadFiles(); } std::vector search(const QString &query) override { std::vector results; if (query.length() < 2) { return results; // Require at least 2 characters to avoid huge result sets } // Convert file paths to strings for fuzzy matching std::vector filePaths; filePaths.reserve(m_allFiles.size()); for (const auto &file : m_allFiles) { filePaths.push_back(file.filename().string()); // Match against filename only } // Use fuzzy finder to get matches auto fuzzyMatches = m_fuzzyFinder.find(filePaths, query.toStdString(), 50); // Convert fuzzy matches back to ListItems results.reserve(fuzzyMatches.size()); for (const auto &match : fuzzyMatches) { // Find the original file path that corresponds to this match auto it = std::find_if(m_allFiles.begin(), m_allFiles.end(), [&match](const std::filesystem::path &path) { return path.filename().string() == match.text; }); if (it != m_allFiles.end()) { auto item = createFileListItem(*it, match.score); results.push_back(item); } } return results; } std::vector getAllItems() override { // Don't return all files by default (too many) // Could return recent files or empty list return {}; } QString pluginName() const override { return "File Search"; } QString pluginDescription() const override { return QString("Searches files in specified directories (currently monitoring %1 files)") .arg(m_allFiles.size()); } int priority() const override { return 25; // Lower priority than applications, higher than examples } // Configuration methods void addSearchDirectory(const std::string &directory) { m_searchDirectories.push_back(directory); loadFiles(); // Reload files } void addIgnoreDirName(const std::string &dirName) { m_ignoreDirNames.insert(dirName); loadFiles(); // Reload files } size_t getFileCount() const { return m_allFiles.size(); } private: // Default search directories static std::vector getDefaultSearchDirs() { std::vector dirs; // Add common user directories QString home = QDir::homePath(); dirs.push_back((home + "/Documents").toStdString()); dirs.push_back((home + "/Desktop").toStdString()); dirs.push_back((home + "/Downloads").toStdString()); // Add XDG user directories if they exist auto xdgDirs = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation); for (const QString &dir : xdgDirs) { if (QDir(dir).exists()) { dirs.push_back(dir.toStdString()); } } return dirs; } void loadFiles() { m_allFiles.clear(); for (const auto &searchDir : m_searchDirectories) { if (!std::filesystem::exists(searchDir)) { continue; } try { // Get all files from this directory auto files = files::findAllFiles(searchDir, m_maxDepth); for (const auto &file : files) { // Check if this file should be ignored if (shouldIgnoreFile(file)) { continue; } m_allFiles.push_back(file); // Limit total files to prevent memory issues if (m_allFiles.size() >= m_maxFiles) { return; } } } catch (const std::filesystem::filesystem_error &e) { // Silently skip directories we can't access continue; } } } bool shouldIgnoreFile(const std::filesystem::path &file) const { try { // Check if file is in any ignored directory (walk up the path) auto current = file.parent_path(); while (!current.empty() && current != current.root_path()) { std::string dirName = current.filename().string(); // Ignore any directory that starts with . if (!dirName.empty() && dirName[0] == '.') { return true; } // Ignore directories by name if (m_ignoreDirNames.contains(dirName)) { return true; } current = current.parent_path(); } // Ignore hidden files (starting with .) std::string fileName = file.filename().string(); if (!fileName.empty() && fileName[0] == '.') { return true; } // Ignore common temporary file extensions auto extension = file.extension().string(); std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); if (extension == ".tmp" || extension == ".temp" || extension == ".log" || extension == ".cache") { return true; } return false; } catch (const std::filesystem::filesystem_error &) { return true; // Ignore files we can't access } } ListItemPtr createFileListItem(const std::filesystem::path &filePath, int fuzzyScore) { QString fileName = QString::fromStdString(filePath.filename().string()); QString fullPath = QString::fromStdString(filePath.string()); QString parentDir = QString::fromStdString(filePath.parent_path().filename().string()); // Create description showing parent directory QString description = parentDir.isEmpty() ? fullPath : QString("%1/.../%2").arg(parentDir, fileName); // Simple icon based on file extension QUrl icon = getFileIcon(filePath); return ListItems::createFile(fileName, fullPath, icon); } QUrl getFileIcon(const std::filesystem::path &filePath) const { QString ext = QString::fromStdString(filePath.extension().string()).toLower(); QString iconName; // Use freedesktop.org standard icon names that respect user themes if (ext == ".txt" || ext == ".md" || ext == ".rst" || ext == ".readme") { iconName = "text-x-generic"; } else if (ext == ".pdf") { iconName = "application-pdf"; } else if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".bmp" || ext == ".svg" || ext == ".webp" || ext == ".tiff") { iconName = "image-x-generic"; } else if (ext == ".mp3" || ext == ".wav" || ext == ".ogg" || ext == ".flac" || ext == ".m4a" || ext == ".aac" || ext == ".wma") { iconName = "audio-x-generic"; } else if (ext == ".mp4" || ext == ".avi" || ext == ".mkv" || ext == ".webm" || ext == ".mov" || ext == ".wmv" || ext == ".flv" || ext == ".m4v") { iconName = "video-x-generic"; } else if (ext == ".zip" || ext == ".tar" || ext == ".gz" || ext == ".bz2" || ext == ".xz" || ext == ".7z" || ext == ".rar") { iconName = "package-x-generic"; } else if (ext == ".cpp" || ext == ".hpp" || ext == ".c" || ext == ".h") { iconName = "text-x-c++src"; } else if (ext == ".py") { iconName = "text-x-python"; } else if (ext == ".js" || ext == ".ts" || ext == ".json") { iconName = "text-x-javascript"; } else if (ext == ".html" || ext == ".htm" || ext == ".css") { iconName = "text-html"; } else if (ext == ".xml" || ext == ".xsl" || ext == ".xsd") { iconName = "text-xml"; } else if (ext == ".sh" || ext == ".bash" || ext == ".zsh") { iconName = "text-x-script"; } else if (ext == ".doc" || ext == ".docx" || ext == ".odt") { iconName = "application-vnd.oasis.opendocument.text"; } else if (ext == ".xls" || ext == ".xlsx" || ext == ".ods") { iconName = "application-vnd.oasis.opendocument.spreadsheet"; } else if (ext == ".ppt" || ext == ".pptx" || ext == ".odp") { iconName = "application-vnd.oasis.opendocument.presentation"; } else { iconName = "text-x-generic"; } // Use Qt's icon theme system to find the actual icon file return resolveThemeIcon(iconName); } QUrl resolveThemeIcon(const QString& iconName) const { // First try Qt's theme system QIcon icon = QIcon::fromTheme(iconName); if (!icon.isNull()) { // Try to find the actual file path by searching standard locations QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); // Icon subdirectories in order of preference QStringList iconSubDirs = { "icons/hicolor/scalable/mimetypes", "icons/hicolor/48x48/mimetypes", "icons/hicolor/32x32/mimetypes", "icons/hicolor/24x24/mimetypes", "icons/hicolor/16x16/mimetypes", "icons/Adwaita/scalable/mimetypes", "icons/Adwaita/48x48/mimetypes", "icons/Adwaita/32x32/mimetypes", "icons/breeze/mimetypes/22", // KDE Plasma "icons/breeze-dark/mimetypes/22", "icons/Papirus/48x48/mimetypes", // Popular icon theme "icons/elementary/mimetypes/48", // Elementary OS }; QStringList extensions = {".svg", ".png", ".xpm"}; for (const QString& dataDir : dataDirs) { for (const QString& iconSubDir : iconSubDirs) { QString basePath = dataDir + "/" + iconSubDir + "/"; for (const QString& ext : extensions) { QString fullPath = basePath + iconName + ext; if (QFile::exists(fullPath)) { return QUrl::fromLocalFile(fullPath); } } } } } // Fallback to a generic file icon if nothing found QIcon fallbackIcon = QIcon::fromTheme("text-x-generic"); if (!fallbackIcon.isNull()) { // Try to resolve the fallback icon the same way QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); for (const QString& dataDir : dataDirs) { QString path = dataDir + "/icons/hicolor/48x48/mimetypes/text-x-generic.png"; if (QFile::exists(path)) { return QUrl::fromLocalFile(path); } } } // Ultimate fallback - return empty URL and let QML handle with default return QUrl(); } // Member variables std::vector m_searchDirectories; std::unordered_set m_ignoreDirNames; int m_maxDepth; size_t m_maxFiles; std::vector m_allFiles; fuzzy::FuzzyFinder m_fuzzyFinder; }; }