diff --git a/CMakeLists.txt b/CMakeLists.txt index e3146be..e38718f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,19 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(cpptrace) +FetchContent_Declare( + inotify-cpp + GIT_REPOSITORY https://github.com/erikzenker/inotify-cpp.git + GIT_TAG "v1.0.0" + GIT_SHALLOW 1 +) +set(BUILD_EXAMPLE OFF) +set(BUILD_SHARED_LIBS OFF) +set(BUILD_STATIC_LIBS ON) +set(BUILD_TEST OFF) +set(USE_BOOST_FILESYSTEM OFF) +FetchContent_MakeAvailable(inotify-cpp) + add_subdirectory(vendor) find_program(WAYLAND_SCANNER wayland-scanner REQUIRED) @@ -222,6 +235,7 @@ target_link_libraries(waylight PRIVATE msdfgen::msdfgen-ext lunasvg::lunasvg SQLiteCpp + inotify-cpp-static m dl diff --git a/flake.nix b/flake.nix index 16eca43..f6a1a93 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,7 @@ harfbuzz sqlite zenity + boost ]; buildInputs = with pkgs; [ cmake diff --git a/src/App.cpp b/src/App.cpp index fd45731..b5e0d79 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -170,7 +170,11 @@ App::App() path TEXT, comment TEXT, dbus_activatable BOOL NOT NULL - ); + ) + )") + .exec(); + + SQLite::Statement(*m_db, R"( CREATE TABLE IF NOT EXISTS ApplicationActionCache ( id INTEGER PRIMARY KEY NOT NULL, id_app INTEGER NOT NULL, @@ -178,11 +182,11 @@ App::App() exec TEXT, icon TEXT, FOREIGN KEY (id_app) REFERENCES ApplicationCache(id) - ); + ) )") .exec(); - m_cache = Waylight::Cache(m_db); + m_cache.emplace(m_db); } App::~App() diff --git a/src/Cache.cpp b/src/Cache.cpp index ce0865b..90dea83 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -12,6 +12,18 @@ #include "common.hpp" +void replace_all( + std::string &str, std::string const &from, std::string const &to) +{ + if (from.empty()) + return; + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.size(), to); + pos += to.size(); + } +} + namespace Waylight { Cache::Cache(std::shared_ptr db) @@ -74,11 +86,32 @@ Cache::Cache(std::shared_ptr db) std::format(" Exec: {}", *action.exec).c_str()); } } + + for (auto const &dir : m_app_dirs) { + m_inot.watchPathRecursively(dir); + } + + auto events = { + inotify::Event::open | inotify::Event::is_dir, + inotify::Event::create, + inotify::Event::modify, + inotify::Event::remove, + inotify::Event::move, + }; + m_inot.onEvents(events, [this](auto notification) { rescan(); }); + + m_inot_th = std::thread([this]() { m_inot.run(); }); +} + +Cache::~Cache() +{ + m_inot.stop(); + m_inot_th.join(); } void Cache::rescan() { - m_app_dirs.clear(); + m_apps.clear(); int id = 0; for (auto const &dir : m_app_dirs) { @@ -93,40 +126,76 @@ void Cache::rescan() mINI::INIStructure ini; ini_file.read(ini); - constexpr auto read_action = - [&](mINI::INIMap const §ion) { - return ApplicationCache::Action { - .name = section.get("Name"), - .exec = [&]() -> std::optional { - if (section.has("Exec")) { - auto const s = section.get("Exec"); - if (!s.empty()) - return s; - } - return std::nullopt; - }(), - .icon = [&]() -> std::optional> { - if (section.has("Icon")) { - auto const icon_name = section.get("Icon"); - if (!icon_name.empty()) { - if (icon_name[0] == '/') { - return std::filesystem::path(icon_name); - } else { - return icon_name; - } - } - } - return std::nullopt; - }(), - }; - }; + constexpr auto read_action = [&](std::string desktop_file_uri, + mINI::INIMap const + §ion) { + auto const name = section.get("Name"); + auto const icon = [&]() + -> std::optional< + std::variant> { + if (section.has("Icon")) { + auto const icon_name = section.get("Icon"); + if (!icon_name.empty()) { + if (icon_name[0] == '/') { + return std::filesystem::path(icon_name); + } else { + return icon_name; + } + } + } + return std::nullopt; + }(); - ApplicationCache app { - .id = id++, - .desktop_entry_path = file.path(), - .type = - [&]() { + return ApplicationCache::Action { + .name = name, + .exec = [&]() -> std::optional { + if (section.has("Exec")) { + auto s = section.get("Exec"); + if (!s.empty()) { + // Either deprecated or not used... + replace_all(s, "%f", ""); + replace_all(s, "%F", ""); + replace_all(s, "%u", ""); + replace_all(s, "%U", ""); + replace_all(s, "%d", ""); + replace_all(s, "%D", ""); + replace_all(s, "%n", ""); + replace_all(s, "%N", ""); + replace_all(s, "%v", ""); + replace_all(s, "%M", ""); + + replace_all(s, "%c", name); + if (icon) { + if (auto const p + = std::get_if( + &*icon)) { + replace_all(s, "%i", + "--icon '" + p->string() + "'"); + } else if (auto const n + = std::get_if(&*icon)) { + replace_all( + s, "%i", "--icon '" + *n + "'"); + } else { + replace_all(s, "%i", ""); + } + } else { + replace_all(s, "%i", ""); + } + replace_all(s, "%k", "'" + desktop_file_uri); + return s; + } + } + return std::nullopt; + }(), + .icon = icon, + }; + }; + + m_apps.push_back({ + .id = id++, + .desktop_entry_path = file.path(), + .type = + [&]() { auto const type_str { ini["Desktop Entry"].get( "Type") }; auto type { ApplicationCache::Type::Application }; @@ -137,51 +206,54 @@ void Cache::rescan() else if (type_str == "Directory") type = ApplicationCache::Type::Directory; return type; - }(), - .terminal = - [&]() { + }(), + .terminal = + [&]() { if (ini["Desktop Entry"].has("Terminal")) { return ini["Desktop Entry"]["Terminal"] == "true" ? true : false; } return false; - }(), - .no_display = - [&]() { + }(), + .no_display = + [&]() { if (ini["Desktop Entry"].has("NoDisplay")) { return ini["Desktop Entry"]["NoDisplay"] == "true" ? true : false; } return false; - }(), - .path = [&]() -> std::optional { + }(), + .path = [&]() -> std::optional { if (ini["Desktop Entry"].has("Path")) { return ini["Desktop Entry"]["Path"]; } return std::nullopt; - }(), - .comment = [&]() -> std::optional { + }(), + .comment = [&]() -> std::optional { if (ini["Desktop Entry"].has("Comment")) { return ini["Desktop Entry"]["Comment"]; } return std::nullopt; - }(), - .actions = - [&]() { + }(), + .actions = + [&]() { std::vector actions; for (auto const &[_, v] : ini) { try { - auto const action = read_action(v); + auto const action = read_action( + std::filesystem::canonical(file.path()) + .string(), + v); actions.push_back(action); } catch (...) { } } return actions; - }(), - .dbus_activatable = - [&]() { + }(), + .dbus_activatable = + [&]() { if (ini["Desktop Entry"].has("DBusActivatable")) { return ini["Desktop Entry"]["DBusActivatable"] == "true" @@ -189,10 +261,12 @@ void Cache::rescan() : false; } return false; - }(), - }; + }(), + }); } } + + dump(); } void Cache::dump() @@ -221,6 +295,7 @@ void Cache::dump() "(?,?,?,?)"); for (auto &app : m_apps) { + ins_app.reset(); ins_app.clearBindings(); ins_app.bind(1, static_cast(app.type)); ins_app.bind(2, app.desktop_entry_path.string()); @@ -240,6 +315,7 @@ void Cache::dump() app.id = m_db->getLastInsertRowid(); for (auto const &action : app.actions) { + ins_act.reset(); ins_act.clearBindings(); ins_act.bind(1, app.id); ins_act.bind(2, action.name); @@ -271,34 +347,34 @@ void Cache::load() { m_apps.clear(); - SQLite::Statement getApps(*m_db, + SQLite::Statement get_apps(*m_db, "SELECT id, type, desktop_entry_path, terminal, no_display, path, " "comment, dbus_activatable " "FROM ApplicationCache"); std::unordered_map id_to_index; - while (getApps.executeStep()) { + while (get_apps.executeStep()) { ApplicationCache app {}; - app.id = getApps.getColumn(0).getInt64(); + app.id = get_apps.getColumn(0).getInt64(); app.type = static_cast( - getApps.getColumn(1).getInt()); + get_apps.getColumn(1).getInt()); app.desktop_entry_path - = std::filesystem::path(getApps.getColumn(2).getString()); - app.terminal = getApps.getColumn(3).getInt() != 0; - app.no_display = getApps.getColumn(4).getInt() != 0; + = std::filesystem::path(get_apps.getColumn(2).getString()); + app.terminal = get_apps.getColumn(3).getInt() != 0; + app.no_display = get_apps.getColumn(4).getInt() != 0; - if (!getApps.getColumn(5).isNull()) - app.path = std::string(getApps.getColumn(5).getString()); + if (!get_apps.getColumn(5).isNull()) + app.path = std::string(get_apps.getColumn(5).getString()); else app.path.reset(); - if (!getApps.getColumn(6).isNull()) - app.comment = std::string(getApps.getColumn(6).getString()); + if (!get_apps.getColumn(6).isNull()) + app.comment = std::string(get_apps.getColumn(6).getString()); else app.comment.reset(); - app.dbus_activatable = getApps.getColumn(7).getInt() != 0; + app.dbus_activatable = get_apps.getColumn(7).getInt() != 0; id_to_index.emplace(app.id, m_apps.size()); m_apps.push_back(std::move(app)); @@ -307,27 +383,27 @@ void Cache::load() if (m_apps.empty()) return; - SQLite::Statement getActions(*m_db, + SQLite::Statement get_actions(*m_db, "SELECT id_app, name, exec, icon " "FROM ApplicationActionCache " "ORDER BY id_app"); - while (getActions.executeStep()) { - auto id_app = getActions.getColumn(0).getInt64(); + while (get_actions.executeStep()) { + auto id_app = get_actions.getColumn(0).getInt64(); auto it = id_to_index.find(id_app); if (it == id_to_index.end()) continue; ApplicationCache::Action action {}; - action.name = std::string(getActions.getColumn(1).getString()); + action.name = std::string(get_actions.getColumn(1).getString()); - if (!getActions.getColumn(2).isNull()) - action.exec = std::string(getActions.getColumn(2).getString()); + if (!get_actions.getColumn(2).isNull()) + action.exec = std::string(get_actions.getColumn(2).getString()); else action.exec.reset(); - if (!getActions.getColumn(3).isNull()) { - auto const str = getActions.getColumn(3).getString(); + if (!get_actions.getColumn(3).isNull()) { + auto const str = get_actions.getColumn(3).getString(); if (str.at(0) == '/') { action.icon = std::filesystem::canonical(std::filesystem::path(str)); diff --git a/src/Cache.hpp b/src/Cache.hpp index 4f297fd..ecd03b8 100644 --- a/src/Cache.hpp +++ b/src/Cache.hpp @@ -3,10 +3,12 @@ #include #include #include +#include #include #include #include +#include namespace Waylight { @@ -46,6 +48,7 @@ struct ApplicationCache { struct Cache { explicit Cache(std::shared_ptr db); + ~Cache(); void rescan(); void dump(); @@ -55,6 +58,9 @@ private: std::vector m_apps; std::vector m_app_dirs; std::shared_ptr m_db; + + inotify::NotifierBuilder m_inot; + std::thread m_inot_th; }; } // namespace Cache