396 lines
14 KiB
C++
396 lines
14 KiB
C++
#pragma once
|
|
|
|
#include "PluginInterface.hpp"
|
|
#include "GenericListItem.hpp"
|
|
#include "files.hpp"
|
|
#include "fuzzy.hpp"
|
|
#include <QUrl>
|
|
#include <QProcess>
|
|
#include <QDir>
|
|
#include <QStandardPaths>
|
|
#include <QIcon>
|
|
#include <QFile>
|
|
#include <filesystem>
|
|
#include <unordered_set>
|
|
#include <algorithm>
|
|
|
|
namespace plugins
|
|
{
|
|
class FileSearchPlugin : public SearchPlugin
|
|
{
|
|
public:
|
|
// Constructor with configurable search and ignore directories
|
|
explicit FileSearchPlugin(
|
|
const std::vector<std::string> &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<ListItemPtr> search(const QString &query) override
|
|
{
|
|
std::vector<ListItemPtr> 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<std::string> 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<ListItemPtr> 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<std::string> getDefaultSearchDirs()
|
|
{
|
|
std::vector<std::string> 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<std::string> m_searchDirectories;
|
|
std::unordered_set<std::string> m_ignoreDirNames;
|
|
int m_maxDepth;
|
|
size_t m_maxFiles;
|
|
|
|
std::vector<std::filesystem::path> m_allFiles;
|
|
fuzzy::FuzzyFinder m_fuzzyFinder;
|
|
};
|
|
} |