From 906556a08d44f9a1f0983d6490622fb6e163ce0e Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 10:27:12 +0100 Subject: [PATCH 1/9] chore: fix CMake globs and ignore Qt autogen (.qt) artifacts --- .gitignore | 3 +++ CMakeLists.txt | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 23baf6a..7a4b81d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ Makefile # Misc tags compile_commands.json + +# Qt auto-gen artifacts (created by build in repo root) +.qt/ diff --git a/CMakeLists.txt b/CMakeLists.txt index c2169b1..a557418 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,10 +14,13 @@ set(CMAKE_AUTORCC ON) find_package(Qt6 COMPONENTS Widgets REQUIRED) # Collect sources and headers only from source tree +# Glob only implementation sources in src; add top-level main.cpp explicitly file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/src/*.cpp - ${CMAKE_SOURCE_DIR}/*.cpp ) +if(EXISTS ${CMAKE_SOURCE_DIR}/main.cpp) + list(APPEND SOURCES ${CMAKE_SOURCE_DIR}/main.cpp) +endif() # Remove duplicate sources that may exist in multiple places list(REMOVE_ITEM SOURCES "${CMAKE_SOURCE_DIR}/src/utils/readApps.cpp") From 2b8c1510fccbab9c5856e958ed8caa0200aec1d9 Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 10:40:55 +0100 Subject: [PATCH 2/9] chore: Add --dump, cache save and ensure dirs are created; refactor app reader storage --- CMakeLists.txt | 21 ++++++- main.cpp | 11 ++++ src/core/apps/readApps.cpp | 110 +++++++++++++++++++++++++++++++++++-- src/core/apps/readApps.h | 7 ++- src/utils/json.hpp | 9 ++- 5 files changed, 148 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a557418..a59838a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,13 @@ project(DLauncher LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Make compile commands available for tools (clang-tidy, language servers) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Development options +option(BUILD_TESTS "Build unit tests" ON) +option(ENABLE_STRICT_WARNINGS "Enable -Wall -Wextra for developer builds" ON) + # Qt settings set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) @@ -61,6 +68,14 @@ target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/src/utils ) -# Test target for menu parser -add_executable(menu_parser_test test/menu_parser_test.cpp src/utils/menu_parser.cpp) -target_include_directories(menu_parser_test PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/test) +if(BUILD_TESTS) + # Test target for menu parser + add_executable(menu_parser_test test/menu_parser_test.cpp src/utils/menu_parser.cpp) + target_include_directories(menu_parser_test PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/test) + target_link_libraries(menu_parser_test PRIVATE Qt6::Widgets) + add_test(NAME menu_parser_test COMMAND menu_parser_test) +endif() + +if(ENABLE_STRICT_WARNINGS) + target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) +endif() diff --git a/main.cpp b/main.cpp index ce681a1..5d43287 100644 --- a/main.cpp +++ b/main.cpp @@ -49,11 +49,16 @@ int main(int argc, char *argv[]) // By default don't include NoDisplay/Hidden entries; allow override via CLI flag bool includeHidden = false; + bool dumpMode = false; bool menuMode = false; std::vector> menuItems; // label, command for (int i = 1; i < argc; ++i) { std::string arg(argv[i]); if (arg == "--include-hidden" || arg == "--show-hidden") includeHidden = true; + else if (arg == "--dump") { + // Diagnostic mode: print each .desktop path and whether it would be included + dumpMode = true; + } else if (arg == "--menu-file" && i + 1 < argc) { menuMode = true; std::string path = argv[++i]; @@ -101,6 +106,12 @@ int main(int argc, char *argv[]) } appReader.LoadApps(includeHidden); + if (dumpMode) { + // Print diagnostics and exit + appReader.DumpAndPrint(includeHidden); + return 0; + } + std::string searchTerm = ""; globalKbListener.registerKeyCallback(Qt::Key_Escape, [&]() { diff --git a/src/core/apps/readApps.cpp b/src/core/apps/readApps.cpp index 2607742..f8f7e54 100644 --- a/src/core/apps/readApps.cpp +++ b/src/core/apps/readApps.cpp @@ -28,6 +28,8 @@ static void ensureCacheDir() void AppReader::LoadApps(bool includeHidden) { + // Ensure cache directory exists before attempting to read cache + ensureCacheDir(); const char* home = getenv("HOME"); std::string homeStr = home ? std::string(home) : std::string(); @@ -65,7 +67,7 @@ void AppReader::LoadApps(bool includeHidden) app.hidden = (parts[5] == "1"); std::string cats = json_util::percent_decode(parts[6]); if (!cats.empty()) app.categories = toStringArray(cats, ";"); - allAps.push_back(app); + allApps.push_back(app); } return; } @@ -94,7 +96,7 @@ void AppReader::LoadApps(bool includeHidden) continue; if (seenNames.find(key_name) == seenNames.end()) { - allAps.push_back(app); + allApps.push_back(app); seenNames.insert(key_name); } } @@ -107,13 +109,99 @@ void AppReader::LoadApps(bool includeHidden) } } +void AppReader::DumpAndPrint(bool includeHidden) +{ + const char* home = getenv("HOME"); + std::string homeStr = home ? std::string(home) : std::string(); + std::vector priorityDirs = { + homeStr + "/.local/share/applications", + "/usr/local/share/applications", + "/usr/share/applications"}; + + for (const auto &appDir : priorityDirs) + { + if (!std::filesystem::exists(appDir)) + continue; + try + { + for (const auto &entry : std::filesystem::directory_iterator(appDir)) + { + if (entry.path().extension() != ".desktop") + continue; + DesktopApp app = parseDesktopApp(entry.path()); + bool skipped = false; + std::string reason; + if (!includeHidden && app.noDisplay) + { + skipped = true; + reason = "NoDisplay"; + } + else if (!includeHidden && app.hidden) + { + skipped = true; + reason = "Hidden"; + } + if (skipped) + { + std::cout << entry.path().string() << "\tSKIPPED\t" << reason << std::endl; + } + else + { + std::cout << entry.path().string() << "\tINCLUDED\t" << app.name << std::endl; + } + } + } + catch (const std::filesystem::filesystem_error &e) + { + std::cerr << "Filesystem error: " << e.what() << std::endl; + continue; + } + } + // After scanning from disk, persist to cache for faster startup next time + if (useCache) + { + try { + SaveCache(); + } catch(...) {} + } +} + +void AppReader::SaveCache() +{ + try { + std::filesystem::path p = cachePath(); + std::filesystem::create_directories(p.parent_path()); + std::ofstream f(p, std::ios::trunc); + for (const auto &app : allApps) + { + std::string name = json_util::percent_encode(app.name); + std::string exec = json_util::percent_encode(app.exec); + std::string icon = app.icon ? json_util::percent_encode(*app.icon) : std::string(); + std::string comment = app.comment ? json_util::percent_encode(*app.comment) : std::string(); + std::string noDisplay = app.noDisplay ? "1" : "0"; + std::string hidden = app.hidden ? "1" : "0"; + std::string cats; + if (!app.categories.empty()) { + std::ostringstream oss; + for (size_t i = 0; i < app.categories.size(); ++i) { + if (i) oss << ";"; + oss << app.categories[i]; + } + cats = json_util::percent_encode(oss.str()); + } + f << name << "\t" << exec << "\t" << icon << "\t" << comment << "\t" << noDisplay << "\t" << hidden << "\t" << cats << "\n"; + } + f.close(); + } catch(...) {} +} + std::vector AppReader::ReadDesktopApps(int limit, const std::string &searchTerm) { std::vector filtered; std::string searchLower = toLower(searchTerm); - for (const auto &app : allAps) + for (const auto &app : allApps) { if (searchTerm.empty() || toLower(app.name).find(searchLower) != std::string::npos) @@ -130,7 +218,7 @@ AppReader::ReadDesktopApps(int limit, const std::string &searchTerm) return filtered; } -std::vector AppReader::GetAllApps() { return allAps; } +std::vector AppReader::GetAllApps() { return allApps; } std::vector AppReader::SearchApps(std::string searchTerm, int limit, bool isFuzzy) @@ -141,7 +229,7 @@ std::vector AppReader::SearchApps(std::string searchTerm, int limit, if (isFuzzy) { std::set> seen; - for (const auto &app : allAps) + for (const auto &app : allApps) { std::string appNameLower = toLower(app.name); appNameLower.erase(remove_if(appNameLower.begin(), appNameLower.end(), ::isspace), appNameLower.end()); @@ -253,6 +341,18 @@ DesktopApp AppReader::parseDesktopApp(const std::filesystem::path &path) result.hidden = (v == "true" || v == "1"); } } + // If name is empty, fall back to desktop filename (without extension) + if (result.name.empty()) + { + try { + std::string fname = path.filename().string(); + if (!fname.empty()) { + auto pos = fname.rfind('.'); + if (pos != std::string::npos) fname = fname.substr(0, pos); + result.name = fname; + } + } catch (...) {} + } return result; } diff --git a/src/core/apps/readApps.h b/src/core/apps/readApps.h index f43fe36..cabafdf 100644 --- a/src/core/apps/readApps.h +++ b/src/core/apps/readApps.h @@ -23,6 +23,11 @@ class AppReader // includeHidden: if true, do not skip entries marked NoDisplay or Hidden void LoadApps(bool includeHidden = false); + // Dump scan diagnostics to stdout: for each .desktop file print path and + // whether it was included or skipped (with reason). Useful for --dump CLI. + void DumpAndPrint(bool includeHidden = false); + // Save currently loaded apps to the on-disk cache (creates directories as needed) + void SaveCache(); std::vector ReadDesktopApps(int limit = -1, const std::string &searchTerm = ""); @@ -47,6 +52,6 @@ class AppReader "/var/lib/flatpak/applications", "/var/lib/flatpak/exports/share/applications"}; - std::vector allAps; + std::vector allApps; bool useCache = true; }; diff --git a/src/utils/json.hpp b/src/utils/json.hpp index 690d554..3fadbef 100644 --- a/src/utils/json.hpp +++ b/src/utils/json.hpp @@ -82,8 +82,14 @@ inline std::map load_freq(const std::string &path) inline void save_freq(const std::string &path, const std::map &freq) { + // Ensure parent directory exists + std::filesystem::path p(path); + std::filesystem::path parent = p.parent_path(); + if (!parent.empty()) + std::filesystem::create_directories(parent); + // Write atomically: write to temp file then rename - std::filesystem::path tmp = std::filesystem::path(path).parent_path() / (std::filesystem::path(path).filename().string() + ".tmp"); + std::filesystem::path tmp = parent / (p.filename().string() + ".tmp"); std::ofstream f(tmp, std::ios::trunc); for (const auto &p : freq) { @@ -96,6 +102,7 @@ inline void save_freq(const std::string &path, const std::map if (ec) { // best-effort fallback: try overwrite + std::filesystem::create_directories(parent); std::ofstream f2(path, std::ios::trunc); for (const auto &p : freq) { From 8201da3b491bb0bf27b7afd1971cb83c9f7d6b27 Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 10:45:01 +0100 Subject: [PATCH 3/9] chore: Remove duplicate app reader sources and update includes to canonical core implementation --- src/apps/readApps.cpp | 190 -------------------------------- src/apps/readApps.h | 44 -------- src/components/appRow/appRow.h | 2 +- src/utils/readApps.cpp | 191 --------------------------------- src/utils/readApps.h | 43 -------- 5 files changed, 1 insertion(+), 469 deletions(-) delete mode 100644 src/apps/readApps.cpp delete mode 100644 src/apps/readApps.h delete mode 100644 src/utils/readApps.cpp delete mode 100644 src/utils/readApps.h diff --git a/src/apps/readApps.cpp b/src/apps/readApps.cpp deleted file mode 100644 index f7e6989..0000000 --- a/src/apps/readApps.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#include "readApps.h" -#include "../utils/utils.h" -#include -#include -#include -#include -#include -#include -#include - -AppReader::AppReader() = default; -AppReader::~AppReader() = default; - -void AppReader::LoadApps() -{ - - std::vector priorityDirs = { - std::string(getenv("HOME")) + "/.local/share/applications", - "/usr/local/share/applications", - "/usr/share/applications"}; - std::set seenNames; - for (const auto &appDir : priorityDirs) - { - if (!std::filesystem::exists(appDir)) - continue; - try - { - for (const auto &entry : std::filesystem::directory_iterator(appDir)) - { - if (entry.path().extension() != ".desktop") - continue; - DesktopApp app = parseDesktopApp(entry.path()); - std::string key_name = toLower(app.name); - key_name.erase(remove_if(key_name.begin(), key_name.end(), ::isspace), key_name.end()); - if (key_name.empty()) - continue; - if (seenNames.find(key_name) == seenNames.end()) - { - allAps.push_back(app); - seenNames.insert(key_name); - } - } - } - catch (const std::filesystem::filesystem_error &e) - { - std::cerr << "Filesystem error: " << e.what() << std::endl; - continue; - } - } -} - -std::vector -AppReader::ReadDesktopApps(int limit, const std::string &searchTerm) -{ - std::vector filtered; - std::string searchLower = toLower(searchTerm); - - for (const auto &app : allAps) - { - if (searchTerm.empty() || - toLower(app.name).find(searchLower) != std::string::npos) - { - filtered.push_back(app); - } - } - - if (limit > 0 && filtered.size() > static_cast(limit)) - { - filtered.resize(limit); - } - - return filtered; -} - -std::vector AppReader::GetAllApps() { return allAps; } - -std::vector AppReader::SearchApps(std::string searchTerm, int limit, - bool isFuzzy) -{ - std::vector results; - std::string searchLower = toLower(searchTerm); - - if (isFuzzy) - { - std::set> seen; - for (const auto &app : allAps) - { - std::string appNameLower = toLower(app.name); - appNameLower.erase(remove_if(appNameLower.begin(), appNameLower.end(), ::isspace), appNameLower.end()); - std::string exec = toLower(app.exec); - size_t percent = exec.find('%'); - if (percent != std::string::npos) - exec = exec.substr(0, percent); - exec.erase(remove_if(exec.begin(), exec.end(), ::isspace), exec.end()); - if (similarity(appNameLower, searchLower) > 0.4 || - contains(exec, searchLower, false) != std::string::npos) - { - auto key = std::make_pair(appNameLower, exec); - if (seen.find(key) == seen.end()) - { - results.push_back(app); - seen.insert(key); - } - } - } - if (limit > 0 && results.size() > static_cast(limit)) - { - results.resize(limit); - } - } - return results; -} - -DesktopApp AppReader::parseDesktopApp(const std::filesystem::path &path) -{ - DesktopApp result; - - std::ifstream f(path); - if (!f.is_open()) - return result; - - std::stringstream buffer; - buffer << f.rdbuf(); - std::string contents = buffer.str(); - - auto lines = toStringArray(contents, "\n"); - - bool inDesktopEntry = false; - - for (const auto &line : lines) - { - std::string trimmed = trim(line); - - if (trimmed.empty() || trimmed[0] == '#') - continue; - - if (trimmed.front() == '[' && trimmed.back() == ']') - { - std::string section = toLower(trimmed); - if (section == "[desktop entry]") - { - inDesktopEntry = true; - continue; - } - else - { - - break; - } - } - - if (!inDesktopEntry) - continue; - - auto equalsIndex = line.find('='); - if (equalsIndex == std::string::npos) - continue; - - std::string arg = trim(line.substr(0, equalsIndex)); - std::string value = trim(line.substr(equalsIndex + 1)); - std::string argLower = toLower(arg); - - if (argLower == "name") - { - result.name = value; - } - else if (argLower.rfind("name[", 0) == 0 && result.name.empty()) - { - result.name = value; - } - else if (argLower == "exec") - { - result.exec = value; - } - else if (argLower == "icon") - { - result.icon = value; - } - else if (argLower == "comment") - { - result.comment = value; - } - else if (argLower == "categories") - { - result.categories = toStringArray(value, ";"); - } - } - - return result; -} diff --git a/src/apps/readApps.h b/src/apps/readApps.h deleted file mode 100644 index 771a701..0000000 --- a/src/apps/readApps.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once -#include -#include -#include -#include - -struct DesktopApp -{ - std::string name; - std::string exec; - std::optional icon; - std::optional comment; - std::vector categories; -}; - -class AppReader -{ -public: - AppReader(); - ~AppReader(); - - void LoadApps(); - - std::vector ReadDesktopApps(int limit = -1, - const std::string &searchTerm = ""); - std::vector GetAllApps(); - std::vector SearchApps(std::string searchTerm, int limit = -1, - bool isFuzzy = true); - -private: - DesktopApp parseDesktopApp(const std::filesystem::path &path); - - const std::vector desktopAppPaths = { - "/usr/share/applications", - "/usr/local/share/applications", - "~/.local/share/applications", - "~/.local/share/flatpak/exports/share/applications", - "/var/lib/flatpak/exports/share/applications", - "/var/lib/snapd/desktop/applications", - "/var/lib/flatpak/applications", - "/var/lib/flatpak/exports/share/applications"}; - - std::vector allAps; -}; diff --git a/src/components/appRow/appRow.h b/src/components/appRow/appRow.h index 8f1b1b7..b12d5a8 100644 --- a/src/components/appRow/appRow.h +++ b/src/components/appRow/appRow.h @@ -2,7 +2,7 @@ #ifndef APPROW_H #define APPROW_H -#include "readApps.h" +#include "core/apps/readApps.h" #include #include diff --git a/src/utils/readApps.cpp b/src/utils/readApps.cpp deleted file mode 100644 index cc79c62..0000000 --- a/src/utils/readApps.cpp +++ /dev/null @@ -1,191 +0,0 @@ - -#include "readApps.h" -#include "utils.h" -#include -#include -#include -#include -#include -#include -#include - -AppReader::AppReader() = default; -AppReader::~AppReader() = default; - -void AppReader::LoadApps() -{ - - std::vector priorityDirs = { - std::string(getenv("HOME")) + "/.local/share/applications", - "/usr/local/share/applications", - "/usr/share/applications"}; - std::set seenNames; - for (const auto &appDir : priorityDirs) - { - if (!std::filesystem::exists(appDir)) - continue; - try - { - for (const auto &entry : std::filesystem::directory_iterator(appDir)) - { - if (entry.path().extension() != ".desktop") - continue; - DesktopApp app = parseDesktopApp(entry.path()); - std::string key_name = toLower(app.name); - key_name.erase(remove_if(key_name.begin(), key_name.end(), ::isspace), key_name.end()); - if (key_name.empty()) - continue; - if (seenNames.find(key_name) == seenNames.end()) - { - allAps.push_back(app); - seenNames.insert(key_name); - } - } - } - catch (const std::filesystem::filesystem_error &e) - { - std::cerr << "Filesystem error: " << e.what() << std::endl; - continue; - } - } -} - -std::vector -AppReader::ReadDesktopApps(int limit, const std::string &searchTerm) -{ - std::vector filtered; - std::string searchLower = toLower(searchTerm); - - for (const auto &app : allAps) - { - if (searchTerm.empty() || - toLower(app.name).find(searchLower) != std::string::npos) - { - filtered.push_back(app); - } - } - - if (limit > 0 && filtered.size() > static_cast(limit)) - { - filtered.resize(limit); - } - - return filtered; -} - -std::vector AppReader::GetAllApps() { return allAps; } - -std::vector AppReader::SearchApps(std::string searchTerm, int limit, - bool isFuzzy) -{ - std::vector results; - std::string searchLower = toLower(searchTerm); - - if (isFuzzy) - { - std::set> seen; - for (const auto &app : allAps) - { - std::string appNameLower = toLower(app.name); - appNameLower.erase(remove_if(appNameLower.begin(), appNameLower.end(), ::isspace), appNameLower.end()); - std::string exec = toLower(app.exec); - size_t percent = exec.find('%'); - if (percent != std::string::npos) - exec = exec.substr(0, percent); - exec.erase(remove_if(exec.begin(), exec.end(), ::isspace), exec.end()); - if (similarity(appNameLower, searchLower) > 0.4 || - contains(exec, searchLower, false) != std::string::npos) - { - auto key = std::make_pair(appNameLower, exec); - if (seen.find(key) == seen.end()) - { - results.push_back(app); - seen.insert(key); - } - } - } - if (limit > 0 && results.size() > static_cast(limit)) - { - results.resize(limit); - } - } - return results; -} - -DesktopApp AppReader::parseDesktopApp(const std::filesystem::path &path) -{ - DesktopApp result; - - std::ifstream f(path); - if (!f.is_open()) - return result; - - std::stringstream buffer; - buffer << f.rdbuf(); - std::string contents = buffer.str(); - - auto lines = toStringArray(contents, "\n"); - - bool inDesktopEntry = false; - - for (const auto &line : lines) - { - std::string trimmed = trim(line); - - if (trimmed.empty() || trimmed[0] == '#') - continue; - - if (trimmed.front() == '[' && trimmed.back() == ']') - { - std::string section = toLower(trimmed); - if (section == "[desktop entry]") - { - inDesktopEntry = true; - continue; - } - else - { - - break; - } - } - - if (!inDesktopEntry) - continue; - - auto equalsIndex = line.find('='); - if (equalsIndex == std::string::npos) - continue; - - std::string arg = trim(line.substr(0, equalsIndex)); - std::string value = trim(line.substr(equalsIndex + 1)); - std::string argLower = toLower(arg); - - if (argLower == "name") - { - result.name = value; - } - else if (argLower.rfind("name[", 0) == 0 && result.name.empty()) - { - result.name = value; - } - else if (argLower == "exec") - { - result.exec = value; - } - else if (argLower == "icon") - { - result.icon = value; - } - else if (argLower == "comment") - { - result.comment = value; - } - else if (argLower == "categories") - { - result.categories = toStringArray(value, ";"); - } - } - - return result; -} diff --git a/src/utils/readApps.h b/src/utils/readApps.h deleted file mode 100644 index 80d7024..0000000 --- a/src/utils/readApps.h +++ /dev/null @@ -1,43 +0,0 @@ - -#pragma once -#include -#include -#include -#include - -struct DesktopApp { - std::string name; - std::string exec; - std::optional icon; - std::optional comment; - std::vector categories; -}; - -class AppReader { -public: - AppReader(); - ~AppReader(); - - void LoadApps(); - - std::vector ReadDesktopApps(int limit = -1, - const std::string &searchTerm = ""); - std::vector GetAllApps(); - std::vector SearchApps(std::string searchTerm, int limit = -1, - bool isFuzzy = true); - -private: - DesktopApp parseDesktopApp(const std::filesystem::path &path); - - const std::vector desktopAppPaths = { - "/usr/share/applications", - "/usr/local/share/applications", - "~/.local/share/applications", - "~/.local/share/flatpak/exports/share/applications", - "/var/lib/flatpak/exports/share/applications", - "/var/lib/snapd/desktop/applications", - "/var/lib/flatpak/applications", - "/var/lib/flatpak/exports/share/applications"}; - - std::vector allAps; -}; From 0c6155349b5b3eeda001ead01f3c87735a22db8a Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 10:45:40 +0100 Subject: [PATCH 4/9] chore: Unify include paths to canonical src/ layout (use core/... and ui/...) --- main.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/main.cpp b/main.cpp index 5d43287..de3e142 100644 --- a/main.cpp +++ b/main.cpp @@ -2,15 +2,15 @@ #include #include "LockedLineEdit.h" -#include "src/ui/components/list.h" -#include "src/core/listeners/kb.h" -#include "src/ui/components/appRow.h" -#include "src/core/apps/readApps.h" +#include "ui/components/list.h" +#include "core/listeners/kb.h" +#include "ui/components/appRow.h" +#include "core/apps/readApps.h" #include "utils.h" -#include "src/theme/theme.h" -#include "src/utils/utils.h" -#include "src/utils/debug.h" -#include "src/utils/json.hpp" +#include "theme/theme.h" +#include "utils/utils.h" +#include "utils/debug.h" +#include "utils/json.hpp" #include #include #include From 541c3b9af6366a68f492305cdce119dedc4d370b Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 10:51:09 +0100 Subject: [PATCH 5/9] feat(core): add XDG helpers and FrequencyStore; use in main to centralize config/cache paths --- main.cpp | 30 +++++++++++----------- src/core/frequency_store.cpp | 34 +++++++++++++++++++++++++ src/core/frequency_store.h | 18 +++++++++++++ src/core/xdg.cpp | 49 ++++++++++++++++++++++++++++++++++++ src/core/xdg.h | 18 +++++++++++++ 5 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 src/core/frequency_store.cpp create mode 100644 src/core/frequency_store.h create mode 100644 src/core/xdg.cpp create mode 100644 src/core/xdg.h diff --git a/main.cpp b/main.cpp index de3e142..7379526 100644 --- a/main.cpp +++ b/main.cpp @@ -11,6 +11,8 @@ #include "utils/utils.h" #include "utils/debug.h" #include "utils/json.hpp" +#include "core/xdg.h" +#include "core/frequency_store.h" #include #include #include @@ -31,14 +33,12 @@ Qt::WindowFlags devFlags() int main(int argc, char *argv[]) { - std::string configDir = std::string(getenv("HOME")) + "/.config"; - std::string freqPath = configDir + "/dlauncher_freq.txt"; - std::filesystem::create_directories(configDir); - { - std::ofstream f(freqPath, std::ios::app); - f.close(); - } - std::map appFrequency = json_util::load_freq(freqPath); + // Use XDG helpers and FrequencyStore + std::string freqPath = xdg::configPath("dlauncher_freq.txt").string(); + // ensure dir exists + xdg::ensureDir(xdg::configPath("")); + // frequency store + FrequencyStore freqStore(freqPath); Debug::log("Debug run string: " + Debug::generateRandomString()); AppReader appReader; @@ -198,9 +198,9 @@ int main(int argc, char *argv[]) list->listWidget->clear(); if (search.empty()) { std::vector allApps = appReader.ReadDesktopApps(64, ""); - std::sort(allApps.begin(), allApps.end(), [&](const DesktopApp &a, const DesktopApp &b) { - int fa = appFrequency[a.exec]; - int fb = appFrequency[b.exec]; + std::sort(allApps.begin(), allApps.end(), [&](const DesktopApp &a, const DesktopApp &b) { + int fa = freqStore.get(a.exec); + int fb = freqStore.get(b.exec); if (fa == fb) { return a.name < b.name; } @@ -222,8 +222,8 @@ int main(int argc, char *argv[]) } std::sort(filteredApps.begin(), filteredApps.end(), [&](const DesktopApp &a, const DesktopApp &b) { - int fa = appFrequency[a.exec]; - int fb = appFrequency[b.exec]; + int fa = freqStore.get(a.exec); + int fb = freqStore.get(b.exec); if (fa == fb) { return a.name < b.name; } @@ -254,8 +254,8 @@ int main(int argc, char *argv[]) std::cout << "Launching: " << program.toStdString(); for (const auto &arg : args) std::cout << " " << arg.toStdString(); std::cout << std::endl; - appFrequency[appRow->app.exec]++; - json_util::save_freq(freqPath, appFrequency); + freqStore.inc(appRow->app.exec); + freqStore.save(); if (envAssignments.isEmpty()) { QProcess::startDetached(program, args); } else { diff --git a/src/core/frequency_store.cpp b/src/core/frequency_store.cpp new file mode 100644 index 0000000..64ea39a --- /dev/null +++ b/src/core/frequency_store.cpp @@ -0,0 +1,34 @@ +#include "frequency_store.h" +#include "../utils/json.hpp" +#include "xdg.h" +#include + +FrequencyStore::FrequencyStore(const std::string &p) + : path(p) +{ + store = json_util::load_freq(path); + dirtyFlag = false; +} + +int FrequencyStore::get(const std::string &key) const +{ + auto it = store.find(key); + if (it == store.end()) return 0; + return it->second; +} + +void FrequencyStore::inc(const std::string &key, int by) +{ + store[key] += by; + dirtyFlag = true; +} + +void FrequencyStore::save() +{ + if (!dirtyFlag) return; + // ensure parent dir exists + std::filesystem::path p(path); + if (!p.parent_path().empty()) xdg::ensureDir(p.parent_path()); + json_util::save_freq(path, store); + dirtyFlag = false; +} diff --git a/src/core/frequency_store.h b/src/core/frequency_store.h new file mode 100644 index 0000000..e03319a --- /dev/null +++ b/src/core/frequency_store.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +class FrequencyStore +{ +public: + FrequencyStore(const std::string &path); + int get(const std::string &key) const; + void inc(const std::string &key, int by = 1); + void save(); + bool dirty() const { return dirtyFlag; } + +private: + std::string path; + std::map store; + bool dirtyFlag = false; +}; diff --git a/src/core/xdg.cpp b/src/core/xdg.cpp new file mode 100644 index 0000000..cf19e4f --- /dev/null +++ b/src/core/xdg.cpp @@ -0,0 +1,49 @@ +#include "xdg.h" +#include + +namespace xdg { + +std::filesystem::path configHome() +{ + const char *xdg = getenv("XDG_CONFIG_HOME"); + if (xdg && xdg[0] != '\0') return std::filesystem::path(xdg); + const char *home = getenv("HOME"); + if (home && home[0] != '\0') return std::filesystem::path(home) / ".config"; + return std::filesystem::path("."); +} + +std::filesystem::path cacheHome() +{ + const char *xdg = getenv("XDG_CACHE_HOME"); + if (xdg && xdg[0] != '\0') return std::filesystem::path(xdg); + const char *home = getenv("HOME"); + if (home && home[0] != '\0') return std::filesystem::path(home) / ".cache"; + return std::filesystem::path("."); +} + +std::filesystem::path home() +{ + const char *h = getenv("HOME"); + if (h && h[0] != '\0') return std::filesystem::path(h); + return std::filesystem::path("."); +} + +void ensureDir(const std::filesystem::path &p) +{ + try { + if (p.empty()) return; + std::filesystem::create_directories(p); + } catch(...) {} +} + +std::filesystem::path configPath(const std::string &relPath) +{ + return configHome() / relPath; +} + +std::filesystem::path cachePath(const std::string &relPath) +{ + return cacheHome() / relPath; +} + +} // namespace xdg diff --git a/src/core/xdg.h b/src/core/xdg.h new file mode 100644 index 0000000..30df73b --- /dev/null +++ b/src/core/xdg.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +namespace xdg { +// Returns XDG config home (XDG_CONFIG_HOME or $HOME/.config) +std::filesystem::path configHome(); +// Returns XDG cache home (XDG_CACHE_HOME or $HOME/.cache) +std::filesystem::path cacheHome(); +// Return $HOME directory path (or '.' fallback) +std::filesystem::path home(); +// Ensure a directory exists (creates parents as needed) +void ensureDir(const std::filesystem::path &p); +// Helper to build a config path under XDG config +std::filesystem::path configPath(const std::string &relPath); +// Helper to build a cache path under XDG cache +std::filesystem::path cachePath(const std::string &relPath); +} // namespace xdg From 2a28200012bcba2259059593479da4214e8a3186 Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 10:51:27 +0100 Subject: [PATCH 6/9] chore(docs): add supported commit types (bugfix, type) to CONTRIBUTING.md --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3819eaa..a1b7b90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,13 @@ type(scope): short description - test: Adding or correcting tests - chore: Maintenance +We also accept these concise aliases/types and encourage their use in commit messages: + +- `bugfix:` (alias for `fix:`) — use when clarifying a bug-fix type +- `type:` — small project-specific type to indicate the change relates to "type" metadata + +Preferred top-level types to use: `feat:`, `fix:`/`bugfix:`, `chore:`, `type:`. + **Examples:** - `feat(launcher): add frequency-based sorting` - `fix(theme): correct color parsing` From 89835fdaad0288eb2f6fc4284066968bf2c652d5 Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 10:52:41 +0100 Subject: [PATCH 7/9] chore(ci): add commit-lint workflow enforcing Conventional Commits (allow bugfix/type) --- .github/workflows/commitlint.yml | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/commitlint.yml diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..cbd31a3 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,76 @@ +name: Commit Lint + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + lint-commits: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Gather commits range + id: range + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "pull_request" ]; then + base_ref=${{ github.event.pull_request.base.ref }} + # fetch base ref and compute range + git fetch origin "$base_ref":refs/remotes/origin/$base_ref || true + RANGE="origin/$base_ref..HEAD" + else + # push event: use before..after + before=${{ github.event.before }} + after=${{ github.sha }} + if [ "$before" = "0000000000000000000000000000000000000000" ]; then + # initial commit range: just check latest + RANGE="$after" + else + RANGE="$before..$after" + fi + fi + echo "range=$RANGE" >> $GITHUB_OUTPUT + + - name: Check commit subjects for Conventional Commits + env: + RANGE: ${{ steps.range.outputs.range }} + run: | + set -euo pipefail + echo "Checking commits in range: $RANGE" + # gather commits; if RANGE is a single sha, handle that + if git rev-parse --verify "$RANGE" >/dev/null 2>&1; then + commits=$(git rev-list --no-merges $RANGE) + else + # fallback: recent 100 commits + commits=$(git rev-list --no-merges HEAD -n 100) + fi + + if [ -z "$commits" ]; then + echo "No commits to check." + exit 0 + fi + + # Conventional commit regex (allowing bugfix and type aliases) + re='^(feat|fix|bugfix|docs|style|refactor|perf|test|chore|type)(\([^)]*\))?: .+' + + failed=0 + for c in $commits; do + subj=$(git log --format=%s -n 1 $c) + if ! echo "$subj" | grep -Eq "$re"; then + echo "Invalid commit message subject for $c: '$subj'" + failed=1 + fi + done + + if [ $failed -ne 0 ]; then + echo "One or more commit messages do not follow Conventional Commits." >&2 + exit 1 + fi + + echo "All commit subjects look good." From 89b9ddf469c13a158cfd6d49a5212cfcfa706fe8 Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 11:02:46 +0100 Subject: [PATCH 8/9] feat(core): add string interning for exec strings to reduce duplicated memory --- CMakeLists.txt | 17 +++++++------- main.cpp | 44 ++++++++++++++++++------------------ src/core/apps/readApps.cpp | 26 +++++++++++++++++---- src/core/apps/readApps.h | 8 ++++++- src/core/frequency_store.cpp | 12 ++++++++-- src/core/frequency_store.h | 4 ++-- src/core/intern.cpp | 20 ++++++++++++++++ src/core/intern.h | 6 +++++ 8 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 src/core/intern.cpp create mode 100644 src/core/intern.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a59838a..2a895db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,14 +59,15 @@ add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets) -target_include_directories(${PROJECT_NAME} PRIVATE - ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/src/ui - ${CMAKE_SOURCE_DIR}/src/core - ${CMAKE_SOURCE_DIR}/src/services - ${CMAKE_SOURCE_DIR}/src/components - ${CMAKE_SOURCE_DIR}/src/utils -) + target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/ui + ${CMAKE_SOURCE_DIR}/src/core + ${CMAKE_SOURCE_DIR}/src/services + ${CMAKE_SOURCE_DIR}/src/components + ${CMAKE_SOURCE_DIR}/src/utils + ${CMAKE_SOURCE_DIR}/src/core + ) if(BUILD_TESTS) # Test target for menu parser diff --git a/main.cpp b/main.cpp index 7379526..c90af14 100644 --- a/main.cpp +++ b/main.cpp @@ -164,21 +164,21 @@ int main(int argc, char *argv[]) layout->addWidget(input); - std::vector allApps; - if (menuMode) { - for (auto &p : menuItems) { - DesktopApp a; - a.name = p.first; - a.exec = p.second; - allApps.push_back(a); + // Create rows lazily from pointers into the canonical app list + if (menuMode) { + for (auto &p : menuItems) { + DesktopApp a; + a.name = p.first; + a.exec = p.second; + list->addRow(new AppRow(list, a)); + } + } else { + auto &master = appReader.GetAllApps(); + size_t count = master.size(); + for (size_t i = 0; i < count && i < 64; ++i) { + list->addRow(new AppRow(list, master[i])); + } } - } else { - allApps = appReader.ReadDesktopApps(64, ""); - } - - for (const auto &app : allApps) { - list->addRow(new AppRow(list, app)); - } QObject::connect(list->listWidget, &QListWidget::currentRowChanged, [&](int row) { @@ -214,25 +214,25 @@ int main(int argc, char *argv[]) return; } - std::vector filteredApps; + std::vector filteredAppsPtrs; for (const auto &app : appReader.GetAllApps()) { if (similarity(search, app.name) > 0.5 || contains(app.name, search, false) != std::string::npos) { - filteredApps.push_back(app); + filteredAppsPtrs.push_back(&app); } } - std::sort(filteredApps.begin(), filteredApps.end(), [&](const DesktopApp &a, const DesktopApp &b) { - int fa = freqStore.get(a.exec); - int fb = freqStore.get(b.exec); + std::sort(filteredAppsPtrs.begin(), filteredAppsPtrs.end(), [&](const DesktopApp *a, const DesktopApp *b) { + int fa = freqStore.get(a->exec); + int fb = freqStore.get(b->exec); if (fa == fb) { - return a.name < b.name; + return a->name < b->name; } if (fa == 0) return false; if (fb == 0) return true; return fa > fb; }); - for (const auto &app : filteredApps) { - list->addRow(new AppRow(list, app)); + for (const auto *appPtr : filteredAppsPtrs) { + list->addRow(new AppRow(list, *appPtr)); } }); QObject::connect(input, &QLineEdit::returnPressed, [&]() diff --git a/src/core/apps/readApps.cpp b/src/core/apps/readApps.cpp index f8f7e54..71a4ae6 100644 --- a/src/core/apps/readApps.cpp +++ b/src/core/apps/readApps.cpp @@ -10,6 +10,7 @@ #include "../../utils/json.hpp" #include #include +#include "intern.h" AppReader::AppReader() = default; AppReader::~AppReader() = default; @@ -67,7 +68,16 @@ void AppReader::LoadApps(bool includeHidden) app.hidden = (parts[5] == "1"); std::string cats = json_util::percent_decode(parts[6]); if (!cats.empty()) app.categories = toStringArray(cats, ";"); - allApps.push_back(app); + // Precompute lower-case and exec-normalized values + app.name_lc = toLower(app.name); + app.exec_lc = toLower(app.exec); + size_t pct = app.exec_lc.find('%'); + if (pct != std::string::npos) app.exec_lc = app.exec_lc.substr(0, pct); + // trim spaces + app.exec_lc.erase(remove_if(app.exec_lc.begin(), app.exec_lc.end(), ::isspace), app.exec_lc.end()); + // intern exec string + app.exec_intern = intern::intern_string(app.exec); + allApps.push_back(std::move(app)); } return; } @@ -90,13 +100,21 @@ void AppReader::LoadApps(bool includeHidden) // Skip entries marked NoDisplay or Hidden unless requested if (!includeHidden && (app.noDisplay || app.hidden)) continue; - std::string key_name = toLower(app.name); + // Precompute normalized keys + app.name_lc = toLower(app.name); + app.exec_lc = toLower(app.exec); + size_t pct = app.exec_lc.find('%'); + if (pct != std::string::npos) app.exec_lc = app.exec_lc.substr(0, pct); + app.exec_lc.erase(remove_if(app.exec_lc.begin(), app.exec_lc.end(), ::isspace), app.exec_lc.end()); + app.exec_intern = intern::intern_string(app.exec); + + std::string key_name = app.name_lc; key_name.erase(remove_if(key_name.begin(), key_name.end(), ::isspace), key_name.end()); if (key_name.empty()) continue; if (seenNames.find(key_name) == seenNames.end()) { - allApps.push_back(app); + allApps.push_back(std::move(app)); seenNames.insert(key_name); } } @@ -218,7 +236,7 @@ AppReader::ReadDesktopApps(int limit, const std::string &searchTerm) return filtered; } -std::vector AppReader::GetAllApps() { return allApps; } +const std::vector &AppReader::GetAllApps() const { return allApps; } std::vector AppReader::SearchApps(std::string searchTerm, int limit, bool isFuzzy) diff --git a/src/core/apps/readApps.h b/src/core/apps/readApps.h index cabafdf..e17e90c 100644 --- a/src/core/apps/readApps.h +++ b/src/core/apps/readApps.h @@ -8,6 +8,12 @@ struct DesktopApp { std::string name; std::string exec; + // Precomputed lower-cased name for fast search + std::string name_lc; + // Precomputed normalized exec string (lower-cased, trimmed, up to '%' placeholder) + std::string exec_lc; + // Interned pointer to canonical exec string (stored in intern pool) + const std::string *exec_intern = nullptr; std::optional icon; std::optional comment; bool noDisplay = false; @@ -31,7 +37,7 @@ class AppReader std::vector ReadDesktopApps(int limit = -1, const std::string &searchTerm = ""); - std::vector GetAllApps(); + const std::vector &GetAllApps() const; std::vector SearchApps(std::string searchTerm, int limit = -1, bool isFuzzy = true); diff --git a/src/core/frequency_store.cpp b/src/core/frequency_store.cpp index 64ea39a..c9e097c 100644 --- a/src/core/frequency_store.cpp +++ b/src/core/frequency_store.cpp @@ -6,7 +6,12 @@ FrequencyStore::FrequencyStore(const std::string &p) : path(p) { - store = json_util::load_freq(path); + // load into temporary ordered map then populate unordered_map + try { + std::map tmp = json_util::load_freq(path); + store.reserve(tmp.size() + 64); + for (const auto &p : tmp) store.emplace(p.first, p.second); + } catch(...) {} dirtyFlag = false; } @@ -29,6 +34,9 @@ void FrequencyStore::save() // ensure parent dir exists std::filesystem::path p(path); if (!p.parent_path().empty()) xdg::ensureDir(p.parent_path()); - json_util::save_freq(path, store); + // json_util expects a std::map; build one from unordered_map + std::map out; + out.insert(store.begin(), store.end()); + json_util::save_freq(path, out); dirtyFlag = false; } diff --git a/src/core/frequency_store.h b/src/core/frequency_store.h index e03319a..2baa15a 100644 --- a/src/core/frequency_store.h +++ b/src/core/frequency_store.h @@ -1,6 +1,6 @@ #pragma once #include -#include +#include class FrequencyStore { @@ -13,6 +13,6 @@ class FrequencyStore private: std::string path; - std::map store; + std::unordered_map store; bool dirtyFlag = false; }; diff --git a/src/core/intern.cpp b/src/core/intern.cpp new file mode 100644 index 0000000..5c3af02 --- /dev/null +++ b/src/core/intern.cpp @@ -0,0 +1,20 @@ +#include "intern.h" +#include +#include +#include + +namespace intern { +static std::unordered_set pool; +static std::mutex pool_mtx; + +const std::string *intern_string(const std::string &s) +{ + std::lock_guard lk(pool_mtx); + auto it = pool.find(s); + if (it != pool.end()) return &*it; + // insert and return pointer + auto res = pool.insert(s); + return &*res.first; +} + +} diff --git a/src/core/intern.h b/src/core/intern.h new file mode 100644 index 0000000..478db13 --- /dev/null +++ b/src/core/intern.h @@ -0,0 +1,6 @@ +#pragma once +#include + +namespace intern { +const std::string *intern_string(const std::string &s); +} From d9b008f5849f19b4c6c6e4f0db94b1ace9904793 Mon Sep 17 00:00:00 2001 From: dayvster Date: Sat, 14 Feb 2026 11:21:53 +0100 Subject: [PATCH 9/9] feat: show system apps behind --show-system flag; persist OnlyShowIn/NotShowIn in cache; add discovery test; debug logging for list population --- CMakeLists.txt | 6 ++ main.cpp | 18 ++++- src/core/apps/readApps.cpp | 148 +++++++++++++++++++++++++++++-------- src/core/apps/readApps.h | 11 ++- src/ui/components/list.cpp | 4 + test/discovery_test.cpp | 37 ++++++++++ 6 files changed, 186 insertions(+), 38 deletions(-) create mode 100644 test/discovery_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a895db..7093d10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,11 +70,17 @@ target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets) ) if(BUILD_TESTS) + enable_testing() # Test target for menu parser add_executable(menu_parser_test test/menu_parser_test.cpp src/utils/menu_parser.cpp) target_include_directories(menu_parser_test PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/test) target_link_libraries(menu_parser_test PRIVATE Qt6::Widgets) add_test(NAME menu_parser_test COMMAND menu_parser_test) + + add_executable(discovery_test test/discovery_test.cpp src/core/apps/readApps.cpp src/core/intern.cpp src/utils/utils.cpp) + target_include_directories(discovery_test PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/test) + target_link_libraries(discovery_test PRIVATE Qt6::Widgets) + add_test(NAME discovery_test COMMAND discovery_test) endif() if(ENABLE_STRICT_WARNINGS) diff --git a/main.cpp b/main.cpp index c90af14..14305d6 100644 --- a/main.cpp +++ b/main.cpp @@ -47,14 +47,16 @@ int main(int argc, char *argv[]) const Theme &theme = themeManager.currentTheme(); GlobalEventListener globalKbListener(app); - // By default don't include NoDisplay/Hidden entries; allow override via CLI flag + // By default include all .desktop entries (don't skip NoDisplay/Hidden/OnlyShowIn) bool includeHidden = false; bool dumpMode = false; bool menuMode = false; + bool showSystem = false; std::vector> menuItems; // label, command for (int i = 1; i < argc; ++i) { std::string arg(argv[i]); if (arg == "--include-hidden" || arg == "--show-hidden") includeHidden = true; + else if (arg == "--show-system") showSystem = true; else if (arg == "--dump") { // Diagnostic mode: print each .desktop path and whether it would be included dumpMode = true; @@ -104,11 +106,11 @@ int main(int argc, char *argv[]) if (!label.empty() && !cmd.empty()) menuItems.emplace_back(label, cmd); } } - appReader.LoadApps(includeHidden); + appReader.LoadApps(includeHidden, showSystem); if (dumpMode) { // Print diagnostics and exit - appReader.DumpAndPrint(includeHidden); + appReader.DumpAndPrint(includeHidden, showSystem); return 0; } @@ -285,5 +287,15 @@ int main(int argc, char *argv[]) window.show(); + // Safety: ensure initial rows are populated (some environments/layout timing + // issues can leave the list empty). If list is empty, populate from master. + if (!menuMode && list->listWidget->count() == 0) { + auto &master = appReader.GetAllApps(); + size_t count = master.size(); + for (size_t i = 0; i < count && i < 64; ++i) { + list->addRow(new AppRow(list, master[i])); + } + } + return app.exec(); } diff --git a/src/core/apps/readApps.cpp b/src/core/apps/readApps.cpp index 71a4ae6..9742492 100644 --- a/src/core/apps/readApps.cpp +++ b/src/core/apps/readApps.cpp @@ -10,7 +10,7 @@ #include "../../utils/json.hpp" #include #include -#include "intern.h" +#include "core/intern.h" AppReader::AppReader() = default; AppReader::~AppReader() = default; @@ -27,17 +27,32 @@ static void ensureCacheDir() std::filesystem::create_directories(cachePath().parent_path()); } -void AppReader::LoadApps(bool includeHidden) +void AppReader::LoadApps(bool includeHidden, bool showSystem) { // Ensure cache directory exists before attempting to read cache ensureCacheDir(); const char* home = getenv("HOME"); std::string homeStr = home ? std::string(home) : std::string(); - std::vector priorityDirs = { - homeStr + "/.local/share/applications", - "/usr/local/share/applications", - "/usr/share/applications"}; + + // Build scan directories from configured desktopAppPaths, expanding '~' + std::vector priorityDirs; + for (const auto &p : desktopAppPaths) { + if (!p.empty() && p[0] == '~') { + if (!homeStr.empty()) priorityDirs.push_back(homeStr + p.substr(1)); + } else { + priorityDirs.push_back(p); + } + } + // Deduplicate while preserving order + { + std::set seen; + std::vector dedup; + for (auto &d : priorityDirs) { + if (seen.insert(d).second) dedup.push_back(d); + } + priorityDirs.swap(dedup); + } // Attempt to load from cache if enabled and present if (useCache) @@ -51,10 +66,13 @@ void AppReader::LoadApps(bool includeHidden) if (f.is_open()) { std::string line; + size_t loaded = 0; while (std::getline(f, line)) { if (line.empty()) continue; - // Format: name\texec\ticon\tcomment\tnoDisplay\thidden\tcategories + // Format (backwards compatible): + // older: name\texec\ticon\tcomment\tnoDisplay\thidden\tcategories + // newer: name\texec\ticon\tcomment\tnoDisplay\thidden\tcategories\tonlyshowin\tnotshowin std::vector parts = toStringArray(line, "\t"); if (parts.size() < 7) continue; DesktopApp app; @@ -68,6 +86,12 @@ void AppReader::LoadApps(bool includeHidden) app.hidden = (parts[5] == "1"); std::string cats = json_util::percent_decode(parts[6]); if (!cats.empty()) app.categories = toStringArray(cats, ";"); + if (parts.size() >= 9) { + std::string onlysi = json_util::percent_decode(parts[7]); + if (!onlysi.empty()) app.onlyShowIn = toStringArray(onlysi, ";"); + std::string notsi = json_util::percent_decode(parts[8]); + if (!notsi.empty()) app.notShowIn = toStringArray(notsi, ";"); + } // Precompute lower-case and exec-normalized values app.name_lc = toLower(app.name); app.exec_lc = toLower(app.exec); @@ -77,9 +101,15 @@ void AppReader::LoadApps(bool includeHidden) app.exec_lc.erase(remove_if(app.exec_lc.begin(), app.exec_lc.end(), ::isspace), app.exec_lc.end()); // intern exec string app.exec_intern = intern::intern_string(app.exec); + // Optionally hide obvious system/config utilities unless showSystem is true + std::string nm = toLower(app.name); + bool likely_system = (nm.find("settings") != std::string::npos || nm.find("configuration") != std::string::npos || nm.find("system") != std::string::npos || nm.find("bluetooth") != std::string::npos || nm.find("printer") != std::string::npos || nm.find("authentication") != std::string::npos || nm.find("kcm_") != std::string::npos || nm.find("kcm ") != std::string::npos || nm.find("app permissions") != std::string::npos); + if (!showSystem && likely_system) continue; allApps.push_back(std::move(app)); + ++loaded; } - return; + // Only use cache if it actually contained apps; otherwise fall through to scanning + if (loaded > 0) return; } } } @@ -92,11 +122,14 @@ void AppReader::LoadApps(bool includeHidden) continue; try { - for (const auto &entry : std::filesystem::directory_iterator(appDir)) - { - if (entry.path().extension() != ".desktop") - continue; - DesktopApp app = parseDesktopApp(entry.path()); + for (const auto &entry : std::filesystem::directory_iterator(appDir)) + { + if (entry.path().extension() != ".desktop") + continue; + // Allow symlinked desktop files and follow them + std::filesystem::path p = entry.path(); + try { p = std::filesystem::canonical(entry.path()); } catch(...) {} + DesktopApp app = parseDesktopApp(p); // Skip entries marked NoDisplay or Hidden unless requested if (!includeHidden && (app.noDisplay || app.hidden)) continue; @@ -108,6 +141,11 @@ void AppReader::LoadApps(bool includeHidden) app.exec_lc.erase(remove_if(app.exec_lc.begin(), app.exec_lc.end(), ::isspace), app.exec_lc.end()); app.exec_intern = intern::intern_string(app.exec); + // Optionally hide system/config utilities by name + std::string nm = toLower(app.name); + bool likely_system = (nm.find("settings") != std::string::npos || nm.find("configuration") != std::string::npos || nm.find("system") != std::string::npos || nm.find("bluetooth") != std::string::npos || nm.find("printer") != std::string::npos || nm.find("authentication") != std::string::npos || nm.find("kcm_") != std::string::npos || nm.find("kcm ") != std::string::npos || nm.find("app permissions") != std::string::npos); + if (!showSystem && likely_system) continue; + std::string key_name = app.name_lc; key_name.erase(remove_if(key_name.begin(), key_name.end(), ::isspace), key_name.end()); if (key_name.empty()) @@ -127,7 +165,7 @@ void AppReader::LoadApps(bool includeHidden) } } -void AppReader::DumpAndPrint(bool includeHidden) +void AppReader::DumpAndPrint(bool includeHidden, bool showSystem) { const char* home = getenv("HOME"); std::string homeStr = home ? std::string(home) : std::string(); @@ -146,7 +184,9 @@ void AppReader::DumpAndPrint(bool includeHidden) { if (entry.path().extension() != ".desktop") continue; - DesktopApp app = parseDesktopApp(entry.path()); + std::filesystem::path p = entry.path(); + try { p = std::filesystem::canonical(entry.path()); } catch(...) {} + DesktopApp app = parseDesktopApp(p); bool skipped = false; std::string reason; if (!includeHidden && app.noDisplay) @@ -159,6 +199,24 @@ void AppReader::DumpAndPrint(bool includeHidden) skipped = true; reason = "Hidden"; } + else if (!includeHidden && !app.onlyShowIn.empty()) + { + skipped = true; + reason = "OnlyShowIn"; + } + else if (!includeHidden && !app.notShowIn.empty()) + { + skipped = true; + reason = "NotShowIn"; + } + // system/config heuristics + std::string nm = toLower(app.name); + bool likely_system = (nm.find("settings") != std::string::npos || nm.find("configuration") != std::string::npos || nm.find("system") != std::string::npos || nm.find("bluetooth") != std::string::npos || nm.find("printer") != std::string::npos || nm.find("authentication") != std::string::npos || nm.find("kcm_") != std::string::npos || nm.find("kcm ") != std::string::npos || nm.find("app permissions") != std::string::npos); + if (!showSystem && likely_system) + { + skipped = true; + reason = "System"; + } if (skipped) { std::cout << entry.path().string() << "\tSKIPPED\t" << reason << std::endl; @@ -190,24 +248,42 @@ void AppReader::SaveCache() std::filesystem::path p = cachePath(); std::filesystem::create_directories(p.parent_path()); std::ofstream f(p, std::ios::trunc); - for (const auto &app : allApps) - { - std::string name = json_util::percent_encode(app.name); - std::string exec = json_util::percent_encode(app.exec); - std::string icon = app.icon ? json_util::percent_encode(*app.icon) : std::string(); - std::string comment = app.comment ? json_util::percent_encode(*app.comment) : std::string(); - std::string noDisplay = app.noDisplay ? "1" : "0"; - std::string hidden = app.hidden ? "1" : "0"; - std::string cats; - if (!app.categories.empty()) { - std::ostringstream oss; - for (size_t i = 0; i < app.categories.size(); ++i) { - if (i) oss << ";"; - oss << app.categories[i]; + for (const auto &app : allApps) + { + std::string name = json_util::percent_encode(app.name); + std::string exec = json_util::percent_encode(app.exec); + std::string icon = app.icon ? json_util::percent_encode(*app.icon) : std::string(); + std::string comment = app.comment ? json_util::percent_encode(*app.comment) : std::string(); + std::string noDisplay = app.noDisplay ? "1" : "0"; + std::string hidden = app.hidden ? "1" : "0"; + std::string cats; + if (!app.categories.empty()) { + std::ostringstream oss; + for (size_t i = 0; i < app.categories.size(); ++i) { + if (i) oss << ";"; + oss << app.categories[i]; + } + cats = json_util::percent_encode(oss.str()); } - cats = json_util::percent_encode(oss.str()); - } - f << name << "\t" << exec << "\t" << icon << "\t" << comment << "\t" << noDisplay << "\t" << hidden << "\t" << cats << "\n"; + std::string onlysi; + if (!app.onlyShowIn.empty()) { + std::ostringstream oss2; + for (size_t i = 0; i < app.onlyShowIn.size(); ++i) { + if (i) oss2 << ";"; + oss2 << app.onlyShowIn[i]; + } + onlysi = json_util::percent_encode(oss2.str()); + } + std::string notsi; + if (!app.notShowIn.empty()) { + std::ostringstream oss3; + for (size_t i = 0; i < app.notShowIn.size(); ++i) { + if (i) oss3 << ";"; + oss3 << app.notShowIn[i]; + } + notsi = json_util::percent_encode(oss3.str()); + } + f << name << "\t" << exec << "\t" << icon << "\t" << comment << "\t" << noDisplay << "\t" << hidden << "\t" << cats << "\t" << onlysi << "\t" << notsi << "\n"; } f.close(); } catch(...) {} @@ -348,6 +424,14 @@ DesktopApp AppReader::parseDesktopApp(const std::filesystem::path &path) { result.categories = toStringArray(value, ";"); } + else if (argLower == "onlyshowin") + { + result.onlyShowIn = toStringArray(value, ";"); + } + else if (argLower == "notshowin") + { + result.notShowIn = toStringArray(value, ";"); + } else if (argLower == "nodisplay") { std::string v = toLower(value); diff --git a/src/core/apps/readApps.h b/src/core/apps/readApps.h index e17e90c..deb401b 100644 --- a/src/core/apps/readApps.h +++ b/src/core/apps/readApps.h @@ -19,6 +19,8 @@ struct DesktopApp bool noDisplay = false; bool hidden = false; std::vector categories; + std::vector onlyShowIn; + std::vector notShowIn; }; class AppReader @@ -28,10 +30,11 @@ class AppReader ~AppReader(); // includeHidden: if true, do not skip entries marked NoDisplay or Hidden - void LoadApps(bool includeHidden = false); + // showSystem: if false (default) hide system/config apps; use --show-system to reveal them + void LoadApps(bool includeHidden = false, bool showSystem = false); // Dump scan diagnostics to stdout: for each .desktop file print path and // whether it was included or skipped (with reason). Useful for --dump CLI. - void DumpAndPrint(bool includeHidden = false); + void DumpAndPrint(bool includeHidden = false, bool showSystem = false); // Save currently loaded apps to the on-disk cache (creates directories as needed) void SaveCache(); @@ -44,11 +47,13 @@ class AppReader // Cache management: Load apps from cache when possible to speed startup. // Cache is stored in XDG cache dir (or ~/.cache) as 'dlauncher/apps.cache'. void SetCacheEnabled(bool enabled) { useCache = enabled; } + // Allow tests to override scan paths (default list below) + void SetDesktopAppPaths(const std::vector &paths) { desktopAppPaths = paths; } private: DesktopApp parseDesktopApp(const std::filesystem::path &path); - const std::vector desktopAppPaths = { + std::vector desktopAppPaths = { "/usr/share/applications", "/usr/local/share/applications", "~/.local/share/applications", diff --git a/src/ui/components/list.cpp b/src/ui/components/list.cpp index b641394..e7223fc 100644 --- a/src/ui/components/list.cpp +++ b/src/ui/components/list.cpp @@ -1,4 +1,5 @@ #include "list.h" +#include #include "list.h" QString StyleSheet = R"( @@ -57,6 +58,8 @@ void ListView::addRow(QWidget *row) listWidget->addItem(item); listWidget->setItemWidget(item, row); count++; + // Debug: log when rows are added to help diagnose empty UI issues + std::cout << "[ListView] addRow: now has " << count << " rows" << std::endl; } void ListView::removeAllRows() @@ -65,6 +68,7 @@ void ListView::removeAllRows() { listWidget->clear(); count = 0; + std::cout << "[ListView] removeAllRows: cleared" << std::endl; } } diff --git a/test/discovery_test.cpp b/test/discovery_test.cpp new file mode 100644 index 0000000..c4485ac --- /dev/null +++ b/test/discovery_test.cpp @@ -0,0 +1,37 @@ +// Simple integration test: create a temp directory with a .desktop file and +// verify AppReader discovers it when scanning. +#include +#include +#include +#include "../src/core/apps/readApps.h" +#include "../src/core/intern.h" + +int main() { + namespace fs = std::filesystem; + fs::path tmp = fs::temp_directory_path() / "dlauncher_test_apps"; + fs::create_directories(tmp); + fs::path desktop = tmp / "TestDmenu.desktop"; + std::ofstream f(desktop); + f << "[Desktop Entry]\n"; + f << "Name=TestDmenu\n"; + f << "Exec=/bin/echo hello\n"; + f << "NoDisplay=true\n"; // typical dmenu helper might set this + f.close(); + + AppReader r; + // point scanner at our temp dir + r.SetDesktopAppPaths({ tmp.string() }); + r.SetCacheEnabled(false); + r.LoadApps(true); // include hidden by default to accept NoDisplay + auto apps = r.GetAllApps(); + bool found = false; + for (const auto &a : apps) { + if (a.name == "TestDmenu" && a.exec == "/bin/echo hello") { found = true; break; } + } + assert(found && "Test desktop app should be discovered"); + + // cleanup + fs::remove(desktop); + fs::remove(tmp); + return 0; +}