Fuzzy finder
This commit is contained in:
parent
60f2be7f50
commit
244ff7cb4d
@ -28,10 +28,12 @@ qt_add_executable(waycast)
|
|||||||
|
|
||||||
include_directories("${CMAKE_SOURCE_DIR}/lib")
|
include_directories("${CMAKE_SOURCE_DIR}/lib")
|
||||||
include_directories("${CMAKE_SOURCE_DIR}/lib/ui")
|
include_directories("${CMAKE_SOURCE_DIR}/lib/ui")
|
||||||
|
include_directories("${CMAKE_SOURCE_DIR}/lib/plugins")
|
||||||
file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/src/*.cpp")
|
file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/src/*.cpp")
|
||||||
file(GLOB_RECURSE LIB_UI_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/lib/ui/*.cpp")
|
file(GLOB_RECURSE LIB_UI_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/lib/ui/*.cpp")
|
||||||
target_sources(waycast PRIVATE ${SRC_FILES} ${LIB_UI_FILES} src/main.cpp)
|
file(GLOB_RECURSE PLUGIN_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/src/plugins/**/*.cpp")
|
||||||
target_include_directories(waycast PRIVATE ${CMAKE_SOURCE_DIR}/lib)
|
target_sources(waycast PRIVATE ${SRC_FILES} ${LIB_UI_FILES} ${PLUGIN_FILES} src/main.cpp)
|
||||||
|
target_include_directories(waycast PRIVATE ${CMAKE_SOURCE_DIR}/lib ${CMAKE_SOURCE_DIR}/lib/plugins ${CMAKE_SOURCE_DIR}/src)
|
||||||
# target_include_directories(waycast PRIVATE ${CMAKE_SOURCE_DIR}/lib ${CMAKE_CURRENT_BINARY_DIR})
|
# target_include_directories(waycast PRIVATE ${CMAKE_SOURCE_DIR}/lib ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
# Bundle QML into the app (no runtime QML path headaches)
|
# Bundle QML into the app (no runtime QML path headaches)
|
||||||
@ -62,7 +64,7 @@ endif()
|
|||||||
|
|
||||||
# Add -fmodules-ts where needed
|
# Add -fmodules-ts where needed
|
||||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
target_compile_options(waycast PRIVATE -fmodules-ts)
|
target_compile_options(waycast PRIVATE -fmodules-ts -mavx2)
|
||||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
target_compile_options(waycast PRIVATE -fmodules-ts)
|
target_compile_options(waycast PRIVATE -fmodules-ts -mavx2)
|
||||||
endif()
|
endif()
|
@ -7,6 +7,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace files
|
namespace files
|
||||||
{
|
{
|
||||||
@ -40,6 +41,61 @@ namespace files
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::vector<fs::path> findAllFiles(const std::string& path, int max_depth = -1)
|
||||||
|
{
|
||||||
|
std::vector<fs::path> out;
|
||||||
|
std::error_code ec;
|
||||||
|
fs::path p(path);
|
||||||
|
|
||||||
|
if (!fs::exists(p, ec) || !fs::is_directory(p, ec))
|
||||||
|
return out;
|
||||||
|
|
||||||
|
// Use recursive_directory_iterator with depth control
|
||||||
|
auto options = fs::directory_options::skip_permission_denied;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (max_depth < 0) {
|
||||||
|
// No depth limit - use unlimited recursion
|
||||||
|
for (const auto& entry : fs::recursive_directory_iterator(p, options, ec)) {
|
||||||
|
if (ec) {
|
||||||
|
ec.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.is_regular_file(ec) && !ec) {
|
||||||
|
out.push_back(entry.path());
|
||||||
|
}
|
||||||
|
ec.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Limited depth recursion
|
||||||
|
std::function<void(const fs::path&, int)> recurse = [&](const fs::path& dir_path, int current_depth) {
|
||||||
|
if (current_depth > max_depth) return;
|
||||||
|
|
||||||
|
for (const auto& entry : fs::directory_iterator(dir_path, options, ec)) {
|
||||||
|
if (ec) {
|
||||||
|
ec.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.is_regular_file(ec) && !ec) {
|
||||||
|
out.push_back(entry.path());
|
||||||
|
} else if (entry.is_directory(ec) && !ec && current_depth < max_depth) {
|
||||||
|
recurse(entry.path(), current_depth + 1);
|
||||||
|
}
|
||||||
|
ec.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recurse(p, 0);
|
||||||
|
}
|
||||||
|
} catch (const fs::filesystem_error&) {
|
||||||
|
// Handle any remaining filesystem errors silently
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
inline std::string readFile(const std::string &filename)
|
inline std::string readFile(const std::string &filename)
|
||||||
{
|
{
|
||||||
std::ifstream in(filename);
|
std::ifstream in(filename);
|
||||||
|
326
lib/fuzzy.hpp
Normal file
326
lib/fuzzy.hpp
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
#include <string>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <immintrin.h> // AVX/AVX2 intrinsics
|
||||||
|
|
||||||
|
namespace fuzzy
|
||||||
|
{
|
||||||
|
struct FuzzyMatch
|
||||||
|
{
|
||||||
|
std::string text;
|
||||||
|
int score;
|
||||||
|
std::vector<size_t> matchPositions;
|
||||||
|
|
||||||
|
bool operator<(const FuzzyMatch &other) const
|
||||||
|
{
|
||||||
|
return score > other.score; // Higher score = better match
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class FuzzyFinder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Find method for string candidates
|
||||||
|
std::vector<FuzzyMatch> find(const std::vector<std::string> &candidates,
|
||||||
|
const std::string &query,
|
||||||
|
int limit = 10)
|
||||||
|
{
|
||||||
|
return findInternal(candidates, query, limit,
|
||||||
|
[](const std::string &candidate) -> std::string
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find method for filesystem path candidates
|
||||||
|
std::vector<FuzzyMatch> find(const std::vector<std::filesystem::path> &candidates,
|
||||||
|
const std::string &query,
|
||||||
|
int limit = 10)
|
||||||
|
{
|
||||||
|
return findInternal(candidates, query, limit,
|
||||||
|
[](const std::filesystem::path &candidate) -> std::string
|
||||||
|
{
|
||||||
|
// For now, just use filename for matching
|
||||||
|
// In the future, could implement path-specific scoring
|
||||||
|
return candidate.filename().string();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Generic internal find method using a converter function
|
||||||
|
template <typename T, typename Converter>
|
||||||
|
std::vector<FuzzyMatch> findInternal(const std::vector<T> &candidates,
|
||||||
|
const std::string &query,
|
||||||
|
int limit,
|
||||||
|
Converter converter)
|
||||||
|
{
|
||||||
|
if (query.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FuzzyMatch> matches;
|
||||||
|
matches.reserve(candidates.size());
|
||||||
|
|
||||||
|
for (const auto &candidate : candidates)
|
||||||
|
{
|
||||||
|
std::string searchText = converter(candidate);
|
||||||
|
auto match = calculateMatch(searchText, query);
|
||||||
|
if (match.score > 0)
|
||||||
|
{
|
||||||
|
// Store the original candidate as text for display
|
||||||
|
if constexpr (std::is_same_v<T, std::filesystem::path>)
|
||||||
|
{
|
||||||
|
match.text = candidate.string(); // Full path for display
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
match.text = searchText; // Use converted text
|
||||||
|
}
|
||||||
|
matches.push_back(std::move(match));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by score (highest first)
|
||||||
|
std::sort(matches.begin(), matches.end());
|
||||||
|
|
||||||
|
// Limit results
|
||||||
|
if (limit > 0 && matches.size() > static_cast<size_t>(limit))
|
||||||
|
{
|
||||||
|
matches.resize(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if AVX2 is available at runtime
|
||||||
|
bool hasAVX2() const
|
||||||
|
{
|
||||||
|
return __builtin_cpu_supports("avx2");
|
||||||
|
}
|
||||||
|
|
||||||
|
// AVX2-accelerated character search - finds next occurrence of query_char in text
|
||||||
|
size_t findNextCharAVX2(const std::string &text, char query_char, size_t start_pos) const
|
||||||
|
{
|
||||||
|
const size_t text_len = text.length();
|
||||||
|
if (start_pos >= text_len)
|
||||||
|
return std::string::npos;
|
||||||
|
|
||||||
|
const char *text_ptr = text.c_str() + start_pos;
|
||||||
|
const size_t remaining = text_len - start_pos;
|
||||||
|
|
||||||
|
// Process 32 characters at a time with AVX2
|
||||||
|
const size_t avx_chunks = remaining / 32;
|
||||||
|
const __m256i query_vec = _mm256_set1_epi8(query_char);
|
||||||
|
|
||||||
|
for (size_t chunk = 0; chunk < avx_chunks; ++chunk)
|
||||||
|
{
|
||||||
|
const __m256i text_vec = _mm256_loadu_si256(reinterpret_cast<const __m256i *>(text_ptr + chunk * 32));
|
||||||
|
const __m256i cmp_result = _mm256_cmpeq_epi8(query_vec, text_vec);
|
||||||
|
const uint32_t match_mask = _mm256_movemask_epi8(cmp_result);
|
||||||
|
|
||||||
|
if (match_mask != 0)
|
||||||
|
{
|
||||||
|
// Found a match - find the first set bit
|
||||||
|
const int first_match = __builtin_ctz(match_mask);
|
||||||
|
return start_pos + chunk * 32 + first_match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle remaining characters with scalar code
|
||||||
|
const size_t remaining_start = start_pos + avx_chunks * 32;
|
||||||
|
for (size_t i = remaining_start; i < text_len; ++i)
|
||||||
|
{
|
||||||
|
if (text[i] == query_char)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
FuzzyMatch calculateMatch(const std::string &text, const std::string &query)
|
||||||
|
{
|
||||||
|
// Choose algorithm based on AVX2 availability and string length
|
||||||
|
if (hasAVX2() && text.length() > 64 && query.length() > 2)
|
||||||
|
{
|
||||||
|
return calculateMatchAVX2(text, query);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return calculateMatchScalar(text, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AVX2-accelerated fuzzy matching
|
||||||
|
FuzzyMatch calculateMatchAVX2(const std::string &text, const std::string &query)
|
||||||
|
{
|
||||||
|
FuzzyMatch match;
|
||||||
|
match.text = text;
|
||||||
|
match.score = 0;
|
||||||
|
|
||||||
|
if (query.empty() || text.empty())
|
||||||
|
{
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lowerText = toLower(text);
|
||||||
|
std::string lowerQuery = toLower(query);
|
||||||
|
|
||||||
|
size_t textIdx = 0;
|
||||||
|
size_t queryIdx = 0;
|
||||||
|
int consecutiveBonus = 0;
|
||||||
|
bool inConsecutiveMatch = false;
|
||||||
|
|
||||||
|
// Use AVX2 to accelerate character searching
|
||||||
|
while (queryIdx < lowerQuery.length())
|
||||||
|
{
|
||||||
|
char queryChar = lowerQuery[queryIdx];
|
||||||
|
|
||||||
|
// Use AVX2 to find next occurrence of query character
|
||||||
|
size_t foundPos = findNextCharAVX2(lowerText, queryChar, textIdx);
|
||||||
|
|
||||||
|
if (foundPos == std::string::npos)
|
||||||
|
{
|
||||||
|
break; // Character not found
|
||||||
|
}
|
||||||
|
|
||||||
|
textIdx = foundPos;
|
||||||
|
match.matchPositions.push_back(textIdx);
|
||||||
|
|
||||||
|
// Calculate score (same logic as before)
|
||||||
|
int charScore = 10;
|
||||||
|
|
||||||
|
if (inConsecutiveMatch && foundPos == match.matchPositions[match.matchPositions.size() - 2] + 1)
|
||||||
|
{
|
||||||
|
consecutiveBonus += 5;
|
||||||
|
charScore += consecutiveBonus;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
consecutiveBonus = 0;
|
||||||
|
inConsecutiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textIdx == 0 || !std::isalnum(lowerText[textIdx - 1]))
|
||||||
|
{
|
||||||
|
charScore += 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text[textIdx] == query[queryIdx])
|
||||||
|
{
|
||||||
|
charScore += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
match.score += charScore;
|
||||||
|
queryIdx++;
|
||||||
|
textIdx++; // Move past current match
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply penalties and validation
|
||||||
|
if (queryIdx < lowerQuery.length())
|
||||||
|
{
|
||||||
|
match.score -= (lowerQuery.length() - queryIdx) * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryIdx < lowerQuery.length())
|
||||||
|
{
|
||||||
|
match.score = 0;
|
||||||
|
match.matchPositions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original scalar implementation as fallback
|
||||||
|
FuzzyMatch calculateMatchScalar(const std::string &text, const std::string &query)
|
||||||
|
{
|
||||||
|
FuzzyMatch match;
|
||||||
|
match.text = text;
|
||||||
|
match.score = 0;
|
||||||
|
|
||||||
|
if (query.empty() || text.empty())
|
||||||
|
{
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lowerText = toLower(text);
|
||||||
|
std::string lowerQuery = toLower(query);
|
||||||
|
|
||||||
|
// Simple fuzzy matching algorithm
|
||||||
|
size_t textIdx = 0;
|
||||||
|
size_t queryIdx = 0;
|
||||||
|
int consecutiveBonus = 0;
|
||||||
|
bool inConsecutiveMatch = false;
|
||||||
|
|
||||||
|
while (textIdx < lowerText.length() && queryIdx < lowerQuery.length())
|
||||||
|
{
|
||||||
|
if (lowerText[textIdx] == lowerQuery[queryIdx])
|
||||||
|
{
|
||||||
|
match.matchPositions.push_back(textIdx);
|
||||||
|
|
||||||
|
// Base score for character match
|
||||||
|
int charScore = 10;
|
||||||
|
|
||||||
|
// Bonus for consecutive characters
|
||||||
|
if (inConsecutiveMatch)
|
||||||
|
{
|
||||||
|
consecutiveBonus += 5;
|
||||||
|
charScore += consecutiveBonus;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
consecutiveBonus = 0;
|
||||||
|
inConsecutiveMatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus for word boundaries (start of word)
|
||||||
|
if (textIdx == 0 || !std::isalnum(lowerText[textIdx - 1]))
|
||||||
|
{
|
||||||
|
charScore += 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus for exact case match
|
||||||
|
if (text[textIdx] == query[queryIdx])
|
||||||
|
{
|
||||||
|
charScore += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
match.score += charScore;
|
||||||
|
queryIdx++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
inConsecutiveMatch = false;
|
||||||
|
consecutiveBonus = 0;
|
||||||
|
}
|
||||||
|
textIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Penalty for unmatched characters in query
|
||||||
|
if (queryIdx < lowerQuery.length())
|
||||||
|
{
|
||||||
|
match.score -= (lowerQuery.length() - queryIdx) * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't match all query characters, score is 0
|
||||||
|
if (queryIdx < lowerQuery.length())
|
||||||
|
{
|
||||||
|
match.score = 0;
|
||||||
|
match.matchPositions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string toLower(const std::string &str)
|
||||||
|
{
|
||||||
|
std::string result = str;
|
||||||
|
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
33
lib/plugins/PluginInterface.hpp
Normal file
33
lib/plugins/PluginInterface.hpp
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../ui/ListItem.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace plugins
|
||||||
|
{
|
||||||
|
class SearchPlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~SearchPlugin() = default;
|
||||||
|
|
||||||
|
// Search for items based on query
|
||||||
|
virtual std::vector<ListItemPtr> search(const QString &query) = 0;
|
||||||
|
|
||||||
|
// Get all items (used for initial display or empty query)
|
||||||
|
virtual std::vector<ListItemPtr> getAllItems() = 0;
|
||||||
|
|
||||||
|
// Plugin identification
|
||||||
|
virtual QString pluginName() const = 0;
|
||||||
|
virtual QString pluginDescription() const = 0;
|
||||||
|
|
||||||
|
// Priority for ordering results (higher = higher priority)
|
||||||
|
virtual int priority() const { return 0; }
|
||||||
|
|
||||||
|
// Whether this plugin should be active by default
|
||||||
|
virtual bool isEnabled() const { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
using SearchPluginPtr = std::unique_ptr<SearchPlugin>;
|
||||||
|
}
|
77
lib/plugins/PluginManager.hpp
Normal file
77
lib/plugins/PluginManager.hpp
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "PluginInterface.hpp"
|
||||||
|
#include "../ui/ListItem.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace plugins
|
||||||
|
{
|
||||||
|
class PluginManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static PluginManager& instance()
|
||||||
|
{
|
||||||
|
static PluginManager instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerPlugin(SearchPluginPtr plugin)
|
||||||
|
{
|
||||||
|
if (plugin && plugin->isEnabled())
|
||||||
|
{
|
||||||
|
m_plugins.push_back(std::move(plugin));
|
||||||
|
|
||||||
|
// Sort plugins by priority (higher priority first)
|
||||||
|
std::sort(m_plugins.begin(), m_plugins.end(),
|
||||||
|
[](const SearchPluginPtr& a, const SearchPluginPtr& b) {
|
||||||
|
return a->priority() > b->priority();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListItemPtr> search(const QString& query)
|
||||||
|
{
|
||||||
|
std::vector<ListItemPtr> allResults;
|
||||||
|
|
||||||
|
for (const auto& plugin : m_plugins)
|
||||||
|
{
|
||||||
|
if (query.isEmpty())
|
||||||
|
{
|
||||||
|
auto items = plugin->getAllItems();
|
||||||
|
allResults.insert(allResults.end(), items.begin(), items.end());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto results = plugin->search(query);
|
||||||
|
allResults.insert(allResults.end(), results.begin(), results.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListItemPtr> getAllItems()
|
||||||
|
{
|
||||||
|
std::vector<ListItemPtr> allItems;
|
||||||
|
|
||||||
|
for (const auto& plugin : m_plugins)
|
||||||
|
{
|
||||||
|
auto items = plugin->getAllItems();
|
||||||
|
allItems.insert(allItems.end(), items.begin(), items.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
return allItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<SearchPluginPtr>& getPlugins() const
|
||||||
|
{
|
||||||
|
return m_plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
PluginManager() = default;
|
||||||
|
std::vector<SearchPluginPtr> m_plugins;
|
||||||
|
};
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
#include "AppListModel.hpp"
|
#include "AppListModel.hpp"
|
||||||
#include "DesktopAppListItem.hpp"
|
#include "../plugins/PluginManager.hpp"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
#undef signals
|
#undef signals
|
||||||
@ -58,8 +58,9 @@ void AppListModel::loadItems()
|
|||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_items.clear();
|
m_items.clear();
|
||||||
|
|
||||||
// Add desktop applications
|
// Get items from all registered plugins
|
||||||
addDesktopApps();
|
auto& pluginManager = plugins::PluginManager::instance();
|
||||||
|
m_items = pluginManager.getAllItems();
|
||||||
|
|
||||||
updateFilteredItems();
|
updateFilteredItems();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
@ -91,20 +92,19 @@ void AppListModel::setSearchText(const QString &searchText)
|
|||||||
emit searchTextChanged();
|
emit searchTextChanged();
|
||||||
|
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|
||||||
|
// Use plugin manager for search
|
||||||
|
auto& pluginManager = plugins::PluginManager::instance();
|
||||||
|
m_items = pluginManager.search(searchText);
|
||||||
updateFilteredItems();
|
updateFilteredItems();
|
||||||
|
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppListModel::addDesktopApps()
|
void AppListModel::addDesktopApps()
|
||||||
{
|
{
|
||||||
dmenu::DEVec apps = dmenu::get_dmenu_app_data();
|
// This method is deprecated - plugins now handle data loading
|
||||||
if (!apps)
|
// Kept for backward compatibility but does nothing
|
||||||
return;
|
|
||||||
|
|
||||||
for (const auto &entry : *apps) {
|
|
||||||
auto item = std::make_shared<DesktopAppListItem>(entry);
|
|
||||||
m_items.push_back(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppListModel::addItems(const std::vector<ListItemPtr> &items)
|
void AppListModel::addItems(const std::vector<ListItemPtr> &items)
|
||||||
@ -118,11 +118,8 @@ void AppListModel::updateFilteredItems()
|
|||||||
{
|
{
|
||||||
m_filteredIndexes.clear();
|
m_filteredIndexes.clear();
|
||||||
|
|
||||||
|
// Since PluginManager now handles filtering, just show all items
|
||||||
for (size_t i = 0; i < m_items.size(); ++i) {
|
for (size_t i = 0; i < m_items.size(); ++i) {
|
||||||
const ListItemPtr &item = m_items[i];
|
m_filteredIndexes.push_back(static_cast<int>(i));
|
||||||
|
|
||||||
if (item->matches(m_searchText)) {
|
|
||||||
m_filteredIndexes.push_back(static_cast<int>(i));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,7 +28,7 @@ public:
|
|||||||
Q_INVOKABLE void executeItem(int index);
|
Q_INVOKABLE void executeItem(int index);
|
||||||
|
|
||||||
// Add items from different sources
|
// Add items from different sources
|
||||||
void addDesktopApps();
|
void addDesktopApps(); // Deprecated - use PluginManager instead
|
||||||
void addItems(const std::vector<ListItemPtr> &items);
|
void addItems(const std::vector<ListItemPtr> &items);
|
||||||
|
|
||||||
QString searchText() const;
|
QString searchText() const;
|
||||||
|
89
lib/ui/GenericListItem.hpp
Normal file
89
lib/ui/GenericListItem.hpp
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ListItem.hpp"
|
||||||
|
#include <functional>
|
||||||
|
#include <QProcess>
|
||||||
|
|
||||||
|
// Generic implementation that plugin developers can use without writing boilerplate
|
||||||
|
class GenericListItem : public ListItem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GenericListItem(const QString& name,
|
||||||
|
const QString& description,
|
||||||
|
const QUrl& iconUrl,
|
||||||
|
const QString& itemType,
|
||||||
|
std::function<void()> executeFunc = nullptr)
|
||||||
|
: m_name(name)
|
||||||
|
, m_description(description)
|
||||||
|
, m_iconUrl(iconUrl)
|
||||||
|
, m_itemType(itemType)
|
||||||
|
, m_executeFunc(executeFunc)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QString name() const override { return m_name; }
|
||||||
|
QString description() const override { return m_description; }
|
||||||
|
QUrl iconUrl() const override { return m_iconUrl; }
|
||||||
|
QString itemType() const override { return m_itemType; }
|
||||||
|
|
||||||
|
void execute() override
|
||||||
|
{
|
||||||
|
if (m_executeFunc) {
|
||||||
|
m_executeFunc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow updating properties after construction
|
||||||
|
void setName(const QString& name) { m_name = name; }
|
||||||
|
void setDescription(const QString& description) { m_description = description; }
|
||||||
|
void setIconUrl(const QUrl& iconUrl) { m_iconUrl = iconUrl; }
|
||||||
|
void setExecuteFunction(std::function<void()> func) { m_executeFunc = func; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_name;
|
||||||
|
QString m_description;
|
||||||
|
QUrl m_iconUrl;
|
||||||
|
QString m_itemType;
|
||||||
|
std::function<void()> m_executeFunc;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convenience factory functions for common use cases
|
||||||
|
namespace ListItems {
|
||||||
|
|
||||||
|
// Create an application-style item
|
||||||
|
inline ListItemPtr createApplication(const QString& name,
|
||||||
|
const QString& exec,
|
||||||
|
const QUrl& icon = QUrl())
|
||||||
|
{
|
||||||
|
return std::make_shared<GenericListItem>(
|
||||||
|
name, exec, icon, "app",
|
||||||
|
[exec]() {
|
||||||
|
// Basic app execution - plugins can override for complex logic
|
||||||
|
QProcess::startDetached("/bin/sh", QStringList() << "-c" << exec);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a file-style item
|
||||||
|
inline ListItemPtr createFile(const QString& filename,
|
||||||
|
const QString& path,
|
||||||
|
const QUrl& icon = QUrl())
|
||||||
|
{
|
||||||
|
return std::make_shared<GenericListItem>(
|
||||||
|
filename, path, icon, "file",
|
||||||
|
[path]() {
|
||||||
|
QProcess::startDetached("xdg-open", QStringList() << path);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a generic item with custom action
|
||||||
|
inline ListItemPtr createItem(const QString& name,
|
||||||
|
const QString& description,
|
||||||
|
const QString& itemType,
|
||||||
|
std::function<void()> action = nullptr,
|
||||||
|
const QUrl& icon = QUrl())
|
||||||
|
{
|
||||||
|
return std::make_shared<GenericListItem>(name, description, icon, itemType, action);
|
||||||
|
}
|
||||||
|
}
|
122
src/main.cpp
122
src/main.cpp
@ -9,7 +9,11 @@
|
|||||||
#include "files.hpp"
|
#include "files.hpp"
|
||||||
#define signals public
|
#define signals public
|
||||||
|
|
||||||
|
#include "fuzzy.hpp"
|
||||||
|
|
||||||
#include "ui/AppListModel.hpp"
|
#include "ui/AppListModel.hpp"
|
||||||
|
#include "plugins/PluginManager.hpp"
|
||||||
|
#include "plugins/DesktopAppPlugin.hpp"
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@ -18,67 +22,67 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
std::vector<std::string> split(std::string s, char delimiter)
|
|
||||||
{
|
|
||||||
std::vector<std::string> items;
|
|
||||||
std::string line;
|
|
||||||
std::stringstream ss(s);
|
|
||||||
|
|
||||||
while (std::getline(ss, line, delimiter))
|
|
||||||
{
|
|
||||||
items.push_back(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
// dmenu::DEVec apps = dmenu::get_dmenu_app_data();
|
fuzzy::FuzzyFinder fuzzy;
|
||||||
// for (auto &app : *apps.get())
|
std::string home = std::getenv("HOME");
|
||||||
// {
|
std::cout << "Home Directory: " << home << '\n';
|
||||||
// std::cout << app.iconPath() << std::endl;
|
auto projects = files::findAllFiles(home.append("/Documents/wallpapers"), 1);
|
||||||
// // std::cout << std::format("---\nName: {}\nID: {}\nIcon: {}\nExec: {}\nDisp: {}\n---\n", app.id, app.name, app.iconPath(), app.exec, app.display ? "yes" : "no");
|
auto pathMatches = fuzzy.find(projects, "sun", 10);
|
||||||
// }
|
std::cout << "Path fuzzy search for 'anime':\n";
|
||||||
|
for (const auto &match : pathMatches)
|
||||||
QGuiApplication app(argc, argv);
|
|
||||||
QCoreApplication::setApplicationName("waycast");
|
|
||||||
|
|
||||||
// Enable system theme support
|
|
||||||
app.setDesktopSettingsAware(true);
|
|
||||||
|
|
||||||
// Ensure we follow system color scheme
|
|
||||||
app.setAttribute(Qt::AA_UseStyleSheetPropagationInWidgetStyles, true);
|
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
|
||||||
|
|
||||||
// Register the AppListModel type with QML
|
|
||||||
qmlRegisterType<AppListModel>("WayCast", 1, 0, "AppListModel");
|
|
||||||
|
|
||||||
// Set up layer shell before creating any windows
|
|
||||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []()
|
|
||||||
{ QCoreApplication::exit(-1); });
|
|
||||||
|
|
||||||
engine.loadFromModule("WayCast", "Main");
|
|
||||||
|
|
||||||
// Get the root objects and configure layer shell
|
|
||||||
auto rootObjects = engine.rootObjects();
|
|
||||||
if (!rootObjects.isEmpty())
|
|
||||||
{
|
{
|
||||||
QWindow *window = qobject_cast<QWindow *>(rootObjects.first());
|
std::cout << " " << match.text << " (score: " << match.score << ")\n";
|
||||||
if (window)
|
|
||||||
{
|
|
||||||
LayerShellQt::Window *layerWindow = LayerShellQt::Window::get(window);
|
|
||||||
if (layerWindow)
|
|
||||||
{
|
|
||||||
layerWindow->setLayer(LayerShellQt::Window::LayerTop);
|
|
||||||
layerWindow->setAnchors({});
|
|
||||||
layerWindow->setKeyboardInteractivity(LayerShellQt::Window::KeyboardInteractivityOnDemand);
|
|
||||||
|
|
||||||
// Now show the window after layer shell is configured
|
|
||||||
window->show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return app.exec();
|
std::cout << "\nAll files found:\n";
|
||||||
|
for (const auto &p : projects)
|
||||||
|
{
|
||||||
|
std::cout << " " << p.string() << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACTUAL APP CODE
|
||||||
|
// QGuiApplication app(argc, argv);
|
||||||
|
// QCoreApplication::setApplicationName("waycast");
|
||||||
|
|
||||||
|
// // Initialize plugin system
|
||||||
|
// auto& pluginManager = plugins::PluginManager::instance();
|
||||||
|
// pluginManager.registerPlugin(std::make_unique<plugins::DesktopAppPlugin>());
|
||||||
|
|
||||||
|
// // Enable system theme support
|
||||||
|
// app.setDesktopSettingsAware(true);
|
||||||
|
|
||||||
|
// // Ensure we follow system color scheme
|
||||||
|
// app.setAttribute(Qt::AA_UseStyleSheetPropagationInWidgetStyles, true);
|
||||||
|
|
||||||
|
// QQmlApplicationEngine engine;
|
||||||
|
|
||||||
|
// // Register the AppListModel type with QML
|
||||||
|
// qmlRegisterType<AppListModel>("WayCast", 1, 0, "AppListModel");
|
||||||
|
|
||||||
|
// // Set up layer shell before creating any windows
|
||||||
|
// QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []()
|
||||||
|
// { QCoreApplication::exit(-1); });
|
||||||
|
|
||||||
|
// engine.loadFromModule("WayCast", "Main");
|
||||||
|
|
||||||
|
// // Get the root objects and configure layer shell
|
||||||
|
// auto rootObjects = engine.rootObjects();
|
||||||
|
// if (!rootObjects.isEmpty())
|
||||||
|
// {
|
||||||
|
// QWindow *window = qobject_cast<QWindow *>(rootObjects.first());
|
||||||
|
// if (window)
|
||||||
|
// {
|
||||||
|
// LayerShellQt::Window *layerWindow = LayerShellQt::Window::get(window);
|
||||||
|
// if (layerWindow)
|
||||||
|
// {
|
||||||
|
// layerWindow->setLayer(LayerShellQt::Window::LayerTop);
|
||||||
|
// layerWindow->setAnchors({});
|
||||||
|
// layerWindow->setKeyboardInteractivity(LayerShellQt::Window::KeyboardInteractivityOnDemand);
|
||||||
|
|
||||||
|
// // Now show the window after layer shell is configured
|
||||||
|
// window->show();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return app.exec();
|
||||||
}
|
}
|
||||||
|
67
src/plugins/DesktopAppPlugin.hpp
Normal file
67
src/plugins/DesktopAppPlugin.hpp
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../lib/plugins/PluginInterface.hpp"
|
||||||
|
#include "../../lib/dmenu.hpp"
|
||||||
|
#include "DesktopAppPlugin/DesktopAppListItem.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace plugins
|
||||||
|
{
|
||||||
|
class DesktopAppPlugin : public SearchPlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DesktopAppPlugin()
|
||||||
|
{
|
||||||
|
loadDesktopEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListItemPtr> search(const QString &query) override
|
||||||
|
{
|
||||||
|
std::vector<ListItemPtr> results;
|
||||||
|
|
||||||
|
for (const auto &item : m_allItems)
|
||||||
|
{
|
||||||
|
if (item->matches(query))
|
||||||
|
{
|
||||||
|
results.push_back(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListItemPtr> getAllItems() override
|
||||||
|
{
|
||||||
|
return m_allItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString pluginName() const override
|
||||||
|
{
|
||||||
|
return "Desktop Applications";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString pluginDescription() const override
|
||||||
|
{
|
||||||
|
return "Searches installed desktop applications";
|
||||||
|
}
|
||||||
|
|
||||||
|
int priority() const override
|
||||||
|
{
|
||||||
|
return 100; // High priority for applications
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void loadDesktopEntries()
|
||||||
|
{
|
||||||
|
auto desktopEntries = dmenu::get_dmenu_app_data();
|
||||||
|
|
||||||
|
for (const auto &entry : *desktopEntries)
|
||||||
|
{
|
||||||
|
auto listItem = std::make_shared<DesktopAppListItem>(entry);
|
||||||
|
m_allItems.push_back(listItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListItemPtr> m_allItems;
|
||||||
|
};
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ListItem.hpp"
|
#include "../../../lib/ui/ListItem.hpp"
|
||||||
|
|
||||||
#undef signals
|
#undef signals
|
||||||
#include "../dmenu.hpp"
|
#include "../../../lib/dmenu.hpp"
|
||||||
#define signals public
|
#define signals public
|
||||||
|
|
||||||
class DesktopAppListItem : public ListItem
|
class DesktopAppListItem : public ListItem
|
54
src/plugins/ExamplePlugin.hpp
Normal file
54
src/plugins/ExamplePlugin.hpp
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../lib/plugins/PluginInterface.hpp"
|
||||||
|
#include "../../lib/ui/GenericListItem.hpp"
|
||||||
|
|
||||||
|
namespace plugins
|
||||||
|
{
|
||||||
|
// Example of how simple it is to create a new plugin
|
||||||
|
class ExamplePlugin : public SearchPlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::vector<ListItemPtr> search(const QString& query) override
|
||||||
|
{
|
||||||
|
std::vector<ListItemPtr> results;
|
||||||
|
|
||||||
|
// Example: Create some sample items that match any query
|
||||||
|
if (query.contains("test", Qt::CaseInsensitive)) {
|
||||||
|
// Using the convenient factory function
|
||||||
|
results.push_back(ListItems::createItem(
|
||||||
|
"Test Item 1",
|
||||||
|
"This is a test item",
|
||||||
|
"example",
|
||||||
|
[]() { /* custom action */ }
|
||||||
|
));
|
||||||
|
|
||||||
|
// Or create directly with GenericListItem
|
||||||
|
results.push_back(std::make_shared<GenericListItem>(
|
||||||
|
"Test Item 2",
|
||||||
|
"Another test item",
|
||||||
|
QUrl(), // no icon
|
||||||
|
"example",
|
||||||
|
[]() {
|
||||||
|
// Custom execute action
|
||||||
|
qDebug() << "Test item executed!";
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListItemPtr> getAllItems() override
|
||||||
|
{
|
||||||
|
// Return some default items
|
||||||
|
return {
|
||||||
|
ListItems::createItem("Example Item", "Always visible", "example")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString pluginName() const override { return "Example Plugin"; }
|
||||||
|
QString pluginDescription() const override { return "Demonstrates easy plugin creation"; }
|
||||||
|
int priority() const override { return 10; } // Low priority
|
||||||
|
};
|
||||||
|
}
|
77
src/plugins/FileSystemPlugin.hpp
Normal file
77
src/plugins/FileSystemPlugin.hpp
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../lib/plugins/PluginInterface.hpp"
|
||||||
|
#include "../../lib/files.hpp"
|
||||||
|
#include "../../lib/ui/GenericListItem.hpp"
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QDir>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace plugins
|
||||||
|
{
|
||||||
|
|
||||||
|
class FileSystemPlugin : public SearchPlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FileSystemPlugin(const QString& searchPath = QDir::homePath())
|
||||||
|
: m_searchPath(searchPath.toStdString())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListItemPtr> search(const QString& query) override
|
||||||
|
{
|
||||||
|
std::vector<ListItemPtr> results;
|
||||||
|
|
||||||
|
if (query.length() < 2) // Avoid expensive searches for short queries
|
||||||
|
return results;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(m_searchPath)) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
auto filename = QString::fromStdString(entry.path().filename().string());
|
||||||
|
if (filename.contains(query, Qt::CaseInsensitive)) {
|
||||||
|
auto item = ListItems::createFile(
|
||||||
|
filename,
|
||||||
|
QString::fromStdString(entry.path().string())
|
||||||
|
);
|
||||||
|
results.push_back(item);
|
||||||
|
|
||||||
|
if (results.size() >= 50) // Limit results to avoid performance issues
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::filesystem::filesystem_error&) {
|
||||||
|
// Handle permission errors silently
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ListItemPtr> getAllItems() override
|
||||||
|
{
|
||||||
|
// For file system, don't return all files (too many)
|
||||||
|
// Instead return an empty list - files only show up on search
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString pluginName() const override
|
||||||
|
{
|
||||||
|
return "File System";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString pluginDescription() const override
|
||||||
|
{
|
||||||
|
return "Searches files in the home directory";
|
||||||
|
}
|
||||||
|
|
||||||
|
int priority() const override
|
||||||
|
{
|
||||||
|
return 50; // Lower priority than applications
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_searchPath;
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user