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/ui")
|
||||
include_directories("${CMAKE_SOURCE_DIR}/lib/plugins")
|
||||
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")
|
||||
target_sources(waycast PRIVATE ${SRC_FILES} ${LIB_UI_FILES} src/main.cpp)
|
||||
target_include_directories(waycast PRIVATE ${CMAKE_SOURCE_DIR}/lib)
|
||||
file(GLOB_RECURSE PLUGIN_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/src/plugins/**/*.cpp")
|
||||
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})
|
||||
|
||||
# Bundle QML into the app (no runtime QML path headaches)
|
||||
@ -62,7 +64,7 @@ endif()
|
||||
|
||||
# Add -fmodules-ts where needed
|
||||
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")
|
||||
target_compile_options(waycast PRIVATE -fmodules-ts)
|
||||
target_compile_options(waycast PRIVATE -fmodules-ts -mavx2)
|
||||
endif()
|
@ -7,6 +7,7 @@
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
|
||||
namespace files
|
||||
{
|
||||
@ -40,6 +41,61 @@ namespace files
|
||||
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)
|
||||
{
|
||||
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 "DesktopAppListItem.hpp"
|
||||
#include "../plugins/PluginManager.hpp"
|
||||
#include <QDebug>
|
||||
|
||||
#undef signals
|
||||
@ -58,8 +58,9 @@ void AppListModel::loadItems()
|
||||
beginResetModel();
|
||||
m_items.clear();
|
||||
|
||||
// Add desktop applications
|
||||
addDesktopApps();
|
||||
// Get items from all registered plugins
|
||||
auto& pluginManager = plugins::PluginManager::instance();
|
||||
m_items = pluginManager.getAllItems();
|
||||
|
||||
updateFilteredItems();
|
||||
endResetModel();
|
||||
@ -91,20 +92,19 @@ void AppListModel::setSearchText(const QString &searchText)
|
||||
emit searchTextChanged();
|
||||
|
||||
beginResetModel();
|
||||
|
||||
// Use plugin manager for search
|
||||
auto& pluginManager = plugins::PluginManager::instance();
|
||||
m_items = pluginManager.search(searchText);
|
||||
updateFilteredItems();
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void AppListModel::addDesktopApps()
|
||||
{
|
||||
dmenu::DEVec apps = dmenu::get_dmenu_app_data();
|
||||
if (!apps)
|
||||
return;
|
||||
|
||||
for (const auto &entry : *apps) {
|
||||
auto item = std::make_shared<DesktopAppListItem>(entry);
|
||||
m_items.push_back(item);
|
||||
}
|
||||
// This method is deprecated - plugins now handle data loading
|
||||
// Kept for backward compatibility but does nothing
|
||||
}
|
||||
|
||||
void AppListModel::addItems(const std::vector<ListItemPtr> &items)
|
||||
@ -118,11 +118,8 @@ void AppListModel::updateFilteredItems()
|
||||
{
|
||||
m_filteredIndexes.clear();
|
||||
|
||||
// Since PluginManager now handles filtering, just show all items
|
||||
for (size_t i = 0; i < m_items.size(); ++i) {
|
||||
const ListItemPtr &item = m_items[i];
|
||||
|
||||
if (item->matches(m_searchText)) {
|
||||
m_filteredIndexes.push_back(static_cast<int>(i));
|
||||
}
|
||||
m_filteredIndexes.push_back(static_cast<int>(i));
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ public:
|
||||
Q_INVOKABLE void executeItem(int index);
|
||||
|
||||
// Add items from different sources
|
||||
void addDesktopApps();
|
||||
void addDesktopApps(); // Deprecated - use PluginManager instead
|
||||
void addItems(const std::vector<ListItemPtr> &items);
|
||||
|
||||
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"
|
||||
#define signals public
|
||||
|
||||
#include "fuzzy.hpp"
|
||||
|
||||
#include "ui/AppListModel.hpp"
|
||||
#include "plugins/PluginManager.hpp"
|
||||
#include "plugins/DesktopAppPlugin.hpp"
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
@ -18,67 +22,67 @@
|
||||
#include <sstream>
|
||||
#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[])
|
||||
{
|
||||
// dmenu::DEVec apps = dmenu::get_dmenu_app_data();
|
||||
// for (auto &app : *apps.get())
|
||||
// {
|
||||
// std::cout << app.iconPath() << std::endl;
|
||||
// // std::cout << std::format("---\nName: {}\nID: {}\nIcon: {}\nExec: {}\nDisp: {}\n---\n", app.id, app.name, app.iconPath(), app.exec, app.display ? "yes" : "no");
|
||||
// }
|
||||
|
||||
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())
|
||||
fuzzy::FuzzyFinder fuzzy;
|
||||
std::string home = std::getenv("HOME");
|
||||
std::cout << "Home Directory: " << home << '\n';
|
||||
auto projects = files::findAllFiles(home.append("/Documents/wallpapers"), 1);
|
||||
auto pathMatches = fuzzy.find(projects, "sun", 10);
|
||||
std::cout << "Path fuzzy search for 'anime':\n";
|
||||
for (const auto &match : pathMatches)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
std::cout << " " << match.text << " (score: " << match.score << ")\n";
|
||||
}
|
||||
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
|
||||
|
||||
#include "ListItem.hpp"
|
||||
#include "../../../lib/ui/ListItem.hpp"
|
||||
|
||||
#undef signals
|
||||
#include "../dmenu.hpp"
|
||||
#include "../../../lib/dmenu.hpp"
|
||||
#define signals public
|
||||
|
||||
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