From 43ca66779b7d267cc6c52bdea75d3ff8eb60132f Mon Sep 17 00:00:00 2001
From: Tuur Vanhoutte <4633209+zjeffer@users.noreply.github.com>
Date: Fri, 27 Dec 2024 15:40:46 +0100
Subject: [PATCH] hyprpm: use glaze to parse hyprctl plugin list (#8812)

* Use std::filesystem::path in hyprpm DataState to avoid concatenating strings with (folder + "/" + file)
* Added getPluginStates helper method in DataState
* Small clang-tidy improvements
---
 .github/actions/setup_base/action.yml |   9 +++
 hyprpm/CMakeLists.txt                 |   3 +-
 hyprpm/src/core/DataState.cpp         | 107 ++++++++++++--------------
 hyprpm/src/core/DataState.hpp         |  22 +++---
 hyprpm/src/core/PluginManager.cpp     |  48 +++++-------
 hyprpm/src/meson.build                |   1 +
 nix/default.nix                       |   4 +
 7 files changed, 97 insertions(+), 97 deletions(-)

diff --git a/.github/actions/setup_base/action.yml b/.github/actions/setup_base/action.yml
index c510bb74f..6df442b32 100644
--- a/.github/actions/setup_base/action.yml
+++ b/.github/actions/setup_base/action.yml
@@ -63,6 +63,15 @@ runs:
           librsvg \
           re2
 
+    - name: Get glaze
+      shell: bash
+      run: |
+        git clone https://github.com/stephenberry/glaze.git
+        cd glaze
+        cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build
+        cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF`
+        cmake --install build
+
     - name: Get hyprwayland-scanner-git
       shell: bash
       run: |
diff --git a/hyprpm/CMakeLists.txt b/hyprpm/CMakeLists.txt
index 22cb8caaa..8eb325fde 100644
--- a/hyprpm/CMakeLists.txt
+++ b/hyprpm/CMakeLists.txt
@@ -10,10 +10,11 @@ file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
 set(CMAKE_CXX_STANDARD 23)
 
 pkg_check_modules(hyprpm_deps REQUIRED IMPORTED_TARGET tomlplusplus hyprutils>=0.2.4)
+find_package(glaze REQUIRED)
 
 add_executable(hyprpm ${SRCFILES})
 
-target_link_libraries(hyprpm PUBLIC PkgConfig::hyprpm_deps)
+target_link_libraries(hyprpm PUBLIC PkgConfig::hyprpm_deps glaze::glaze)
 
 # binary
 install(TARGETS hyprpm)
diff --git a/hyprpm/src/core/DataState.cpp b/hyprpm/src/core/DataState.cpp
index fb8679d62..55f148a4d 100644
--- a/hyprpm/src/core/DataState.cpp
+++ b/hyprpm/src/core/DataState.cpp
@@ -1,11 +1,10 @@
 #include "DataState.hpp"
 #include <toml++/toml.hpp>
 #include <print>
-#include <filesystem>
 #include <fstream>
 #include "PluginManager.hpp"
 
-std::string DataState::getDataStatePath() {
+std::filesystem::path DataState::getDataStatePath() {
     const auto HOME = getenv("HOME");
     if (!HOME) {
         std::println(stderr, "DataState: no $HOME");
@@ -16,12 +15,29 @@ std::string DataState::getDataStatePath() {
     const auto XDG_DATA_HOME = getenv("XDG_DATA_HOME");
 
     if (XDG_DATA_HOME)
-        return std::string{XDG_DATA_HOME} + "/hyprpm";
-    return std::string{HOME} + "/.local/share/hyprpm";
+        return std::filesystem::path{XDG_DATA_HOME} / "hyprpm";
+    return std::filesystem::path{HOME} / ".local/share/hyprpm";
 }
 
 std::string DataState::getHeadersPath() {
-    return getDataStatePath() + "/headersRoot";
+    return getDataStatePath() / "headersRoot";
+}
+
+std::vector<std::filesystem::path> DataState::getPluginStates() {
+    ensureStateStoreExists();
+
+    std::vector<std::filesystem::path> states;
+    for (const auto& entry : std::filesystem::directory_iterator(getDataStatePath())) {
+        if (!entry.is_directory() || entry.path().stem() == "headersRoot")
+            continue;
+
+        const auto stateFile = entry.path() / "state.toml";
+        if (!std::filesystem::exists(stateFile))
+            continue;
+
+        states.emplace_back(stateFile);
+    }
+    return states;
 }
 
 void DataState::ensureStateStoreExists() {
@@ -37,7 +53,7 @@ void DataState::ensureStateStoreExists() {
 void DataState::addNewPluginRepo(const SPluginRepository& repo) {
     ensureStateStoreExists();
 
-    const auto PATH = getDataStatePath() + "/" + repo.name;
+    const auto PATH = getDataStatePath() / repo.name;
 
     std::filesystem::create_directories(PATH);
     // clang-format off
@@ -50,19 +66,21 @@ void DataState::addNewPluginRepo(const SPluginRepository& repo) {
         }}
     };
     for (auto const& p : repo.plugins) {
+        const auto filename = p.name + ".so";
+
         // copy .so to the good place
         if (std::filesystem::exists(p.filename))
-            std::filesystem::copy_file(p.filename, PATH + "/" + p.name + ".so");
+            std::filesystem::copy_file(p.filename, PATH / filename);
 
         DATA.emplace(p.name, toml::table{
-            {"filename", p.name + ".so"},
+            {"filename", filename},
             {"enabled", p.enabled},
             {"failed", p.failed}
         });
     }
     // clang-format on
 
-    std::ofstream ofs(PATH + "/state.toml", std::ios::trunc);
+    std::ofstream ofs(PATH / "state.toml", std::ios::trunc);
     ofs << DATA;
     ofs.close();
 }
@@ -72,17 +90,10 @@ bool DataState::pluginRepoExists(const std::string& urlOrName) {
 
     const auto PATH = getDataStatePath();
 
-    for (const auto& entry : std::filesystem::directory_iterator(PATH)) {
-        if (!entry.is_directory() || entry.path().stem() == "headersRoot")
-            continue;
-
-        if (!std::filesystem::exists(entry.path().string() + "/state.toml"))
-            continue;
-
-        auto       STATE = toml::parse_file(entry.path().string() + "/state.toml");
-
-        const auto NAME = STATE["repository"]["name"].value_or("");
-        const auto URL  = STATE["repository"]["url"].value_or("");
+    for (const auto& stateFile : getPluginStates()) {
+        const auto STATE = toml::parse_file(stateFile.c_str());
+        const auto NAME  = STATE["repository"]["name"].value_or("");
+        const auto URL   = STATE["repository"]["url"].value_or("");
 
         if (URL == urlOrName || NAME == urlOrName)
             return true;
@@ -96,29 +107,22 @@ void DataState::removePluginRepo(const std::string& urlOrName) {
 
     const auto PATH = getDataStatePath();
 
-    for (const auto& entry : std::filesystem::directory_iterator(PATH)) {
-        if (!entry.is_directory() || entry.path().stem() == "headersRoot")
-            continue;
-
-        if (!std::filesystem::exists(entry.path().string() + "/state.toml"))
-            continue;
-
-        auto       STATE = toml::parse_file(entry.path().string() + "/state.toml");
-
-        const auto NAME = STATE["repository"]["name"].value_or("");
-        const auto URL  = STATE["repository"]["url"].value_or("");
+    for (const auto& stateFile : getPluginStates()) {
+        const auto STATE = toml::parse_file(stateFile.c_str());
+        const auto NAME  = STATE["repository"]["name"].value_or("");
+        const auto URL   = STATE["repository"]["url"].value_or("");
 
         if (URL == urlOrName || NAME == urlOrName) {
 
             // unload the plugins!!
-            for (const auto& file : std::filesystem::directory_iterator(entry.path())) {
+            for (const auto& file : std::filesystem::directory_iterator(stateFile.parent_path())) {
                 if (!file.path().string().ends_with(".so"))
                     continue;
 
                 g_pPluginManager->loadUnloadPlugin(std::filesystem::absolute(file.path()), false);
             }
 
-            std::filesystem::remove_all(entry.path());
+            std::filesystem::remove_all(stateFile.parent_path());
             return;
         }
     }
@@ -139,7 +143,7 @@ void DataState::updateGlobalState(const SGlobalState& state) {
     };
     // clang-format on
 
-    std::ofstream ofs(PATH + "/state.toml", std::ios::trunc);
+    std::ofstream ofs(PATH / "state.toml", std::ios::trunc);
     ofs << DATA;
     ofs.close();
 }
@@ -147,12 +151,12 @@ void DataState::updateGlobalState(const SGlobalState& state) {
 SGlobalState DataState::getGlobalState() {
     ensureStateStoreExists();
 
-    const auto PATH = getDataStatePath();
+    const auto stateFile = getDataStatePath() / "state.toml";
 
-    if (!std::filesystem::exists(PATH + "/state.toml"))
+    if (!std::filesystem::exists(stateFile))
         return SGlobalState{};
 
-    auto         DATA = toml::parse_file(PATH + "/state.toml");
+    auto         DATA = toml::parse_file(stateFile.c_str());
 
     SGlobalState state;
     state.headersHashCompiled = DATA["state"]["hash"].value_or("");
@@ -167,15 +171,8 @@ std::vector<SPluginRepository> DataState::getAllRepositories() {
     const auto                     PATH = getDataStatePath();
 
     std::vector<SPluginRepository> repos;
-
-    for (const auto& entry : std::filesystem::directory_iterator(PATH)) {
-        if (!entry.is_directory() || entry.path().stem() == "headersRoot")
-            continue;
-
-        if (!std::filesystem::exists(entry.path().string() + "/state.toml"))
-            continue;
-
-        auto              STATE = toml::parse_file(entry.path().string() + "/state.toml");
+    for (const auto& stateFile : getPluginStates()) {
+        const auto        STATE = toml::parse_file(stateFile.c_str());
 
         const auto        NAME = STATE["repository"]["name"].value_or("");
         const auto        URL  = STATE["repository"]["url"].value_or("");
@@ -210,15 +207,8 @@ bool DataState::setPluginEnabled(const std::string& name, bool enabled) {
 
     const auto PATH = getDataStatePath();
 
-    for (const auto& entry : std::filesystem::directory_iterator(PATH)) {
-        if (!entry.is_directory() || entry.path().stem() == "headersRoot")
-            continue;
-
-        if (!std::filesystem::exists(entry.path().string() + "/state.toml"))
-            continue;
-
-        auto STATE = toml::parse_file(entry.path().string() + "/state.toml");
-
+    for (const auto& stateFile : getPluginStates()) {
+        const auto STATE = toml::parse_file(stateFile.c_str());
         for (const auto& [key, val] : STATE) {
             if (key == "repository")
                 continue;
@@ -231,10 +221,11 @@ bool DataState::setPluginEnabled(const std::string& name, bool enabled) {
             if (FAILED)
                 return false;
 
-            (*STATE[key].as_table()).insert_or_assign("enabled", enabled);
+            auto modifiedState = STATE;
+            (*modifiedState[key].as_table()).insert_or_assign("enabled", enabled);
 
-            std::ofstream state(entry.path().string() + "/state.toml", std::ios::trunc);
-            state << STATE;
+            std::ofstream state(stateFile, std::ios::trunc);
+            state << modifiedState;
             state.close();
 
             return true;
diff --git a/hyprpm/src/core/DataState.hpp b/hyprpm/src/core/DataState.hpp
index 70788f72b..ebb550ba9 100644
--- a/hyprpm/src/core/DataState.hpp
+++ b/hyprpm/src/core/DataState.hpp
@@ -1,4 +1,5 @@
 #pragma once
+#include <filesystem>
 #include <string>
 #include <vector>
 #include "Plugin.hpp"
@@ -9,14 +10,15 @@ struct SGlobalState {
 };
 
 namespace DataState {
-    std::string                    getDataStatePath();
-    std::string                    getHeadersPath();
-    void                           ensureStateStoreExists();
-    void                           addNewPluginRepo(const SPluginRepository& repo);
-    void                           removePluginRepo(const std::string& urlOrName);
-    bool                           pluginRepoExists(const std::string& urlOrName);
-    void                           updateGlobalState(const SGlobalState& state);
-    SGlobalState                   getGlobalState();
-    bool                           setPluginEnabled(const std::string& name, bool enabled);
-    std::vector<SPluginRepository> getAllRepositories();
+    std::filesystem::path              getDataStatePath();
+    std::string                        getHeadersPath();
+    std::vector<std::filesystem::path> getPluginStates();
+    void                               ensureStateStoreExists();
+    void                               addNewPluginRepo(const SPluginRepository& repo);
+    void                               removePluginRepo(const std::string& urlOrName);
+    bool                               pluginRepoExists(const std::string& urlOrName);
+    void                               updateGlobalState(const SGlobalState& state);
+    SGlobalState                       getGlobalState();
+    bool                               setPluginEnabled(const std::string& name, bool enabled);
+    std::vector<SPluginRepository>     getAllRepositories();
 };
\ No newline at end of file
diff --git a/hyprpm/src/core/PluginManager.cpp b/hyprpm/src/core/PluginManager.cpp
index 48b108c84..a3bd5a098 100644
--- a/hyprpm/src/core/PluginManager.cpp
+++ b/hyprpm/src/core/PluginManager.cpp
@@ -7,10 +7,8 @@
 
 #include <cstdio>
 #include <iostream>
-#include <array>
 #include <filesystem>
 #include <print>
-#include <thread>
 #include <fstream>
 #include <algorithm>
 #include <format>
@@ -21,6 +19,7 @@
 #include <unistd.h>
 
 #include <toml++/toml.hpp>
+#include <glaze/glaze.hpp>
 
 #include <hyprutils/string/String.hpp>
 #include <hyprutils/os/Process.hpp>
@@ -83,13 +82,13 @@ SHyprlandVersion CPluginManager::getHyprlandVersion(bool running) {
     hlbranch             = hlbranch.substr(0, hlbranch.find(" at commit "));
 
     std::string hldate = HLVERCALL.substr(HLVERCALL.find("Date: ") + 6);
-    hldate             = hldate.substr(0, hldate.find("\n"));
+    hldate             = hldate.substr(0, hldate.find('\n'));
 
     std::string hlcommits;
 
     if (HLVERCALL.contains("commits:")) {
         hlcommits = HLVERCALL.substr(HLVERCALL.find("commits:") + 9);
-        hlcommits = hlcommits.substr(0, hlcommits.find(" "));
+        hlcommits = hlcommits.substr(0, hlcommits.find(' '));
     }
 
     int commits = 0;
@@ -378,7 +377,7 @@ eHeadersErrors CPluginManager::headersValid() {
 
     // find headers commit
     const std::string& cmd     = std::format("PKG_CONFIG_PATH=\"{}/share/pkgconfig\" pkgconf --cflags --keep-system-cflags hyprland", DataState::getHeadersPath());
-    auto               headers = execAndGet(cmd.c_str());
+    auto               headers = execAndGet(cmd);
 
     if (!headers.contains("-I/"))
         return HEADERS_MISSING;
@@ -790,35 +789,28 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState() {
     const auto HOME = getenv("HOME");
     const auto HIS  = getenv("HYPRLAND_INSTANCE_SIGNATURE");
     if (!HOME || !HIS) {
-        std::println(stderr, "PluginManager: no $HOME or HIS");
+        std::println(stderr, "PluginManager: no $HOME or $HYPRLAND_INSTANCE_SIGNATURE");
         return LOADSTATE_FAIL;
     }
-    const auto               HYPRPMPATH = DataState::getDataStatePath() + "/";
+    const auto HYPRPMPATH = DataState::getDataStatePath();
 
-    auto                     pluginLines = execAndGet("hyprctl plugins list | grep Plugin");
+    const auto json = glz::read_json<glz::json_t::array_t>(execAndGet("hyprctl plugins list -j"));
+    if (!json) {
+        std::println(stderr, "PluginManager: couldn't parse hyprctl output");
+        return LOADSTATE_FAIL;
+    }
 
     std::vector<std::string> loadedPlugins;
+    for (const auto& plugin : json.value()) {
+        if (!plugin.is_object() || !plugin.contains("name")) {
+            std::println(stderr, "PluginManager: couldn't parse plugin object");
+            return LOADSTATE_FAIL;
+        }
+        loadedPlugins.emplace_back(plugin["name"].get<std::string>());
+    }
 
     std::println("{}", successString("Ensuring plugin load state"));
 
-    // iterate line by line
-    while (!pluginLines.empty()) {
-        auto plLine = pluginLines.substr(0, pluginLines.find('\n'));
-
-        if (pluginLines.find('\n') != std::string::npos)
-            pluginLines = pluginLines.substr(pluginLines.find('\n') + 1);
-        else
-            pluginLines = "";
-
-        if (plLine.back() != ':')
-            continue;
-
-        plLine = plLine.substr(7);
-        plLine = plLine.substr(0, plLine.find(" by "));
-
-        loadedPlugins.push_back(plLine);
-    }
-
     // get state
     const auto REPOS = DataState::getAllRepositories();
 
@@ -853,7 +845,7 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState() {
     for (auto const& p : loadedPlugins) {
         if (!enabled(p)) {
             // unload
-            if (!loadUnloadPlugin(HYPRPMPATH + repoForName(p) + "/" + p + ".so", false)) {
+            if (!loadUnloadPlugin(HYPRPMPATH / repoForName(p) / (p + ".so"), false)) {
                 std::println("{}", infoString("{} will be unloaded after restarting Hyprland", p));
                 hyprlandVersionMismatch = true;
             } else
@@ -870,7 +862,7 @@ ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState() {
             if (std::find_if(loadedPlugins.begin(), loadedPlugins.end(), [&](const auto& other) { return other == p.name; }) != loadedPlugins.end())
                 continue;
 
-            if (!loadUnloadPlugin(HYPRPMPATH + repoForName(p.name) + "/" + p.filename, true)) {
+            if (!loadUnloadPlugin(HYPRPMPATH / repoForName(p.name) / p.filename, true)) {
                 std::println("{}", infoString("{} will be loaded after restarting Hyprland", p.name));
                 hyprlandVersionMismatch = true;
             } else
diff --git a/hyprpm/src/meson.build b/hyprpm/src/meson.build
index 2ef6c3238..fd914f9d2 100644
--- a/hyprpm/src/meson.build
+++ b/hyprpm/src/meson.build
@@ -8,6 +8,7 @@ executable(
     dependency('hyprutils', version: '>= 0.1.1'),
     dependency('threads'),
     dependency('tomlplusplus'),
+    dependency('glaze', method: 'cmake'),
   ],
   install: true,
 )
diff --git a/nix/default.nix b/nix/default.nix
index 9293a35c7..8e3af31d1 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -5,12 +5,14 @@
   pkg-config,
   pkgconf,
   makeWrapper,
+  cmake,
   meson,
   ninja,
   aquamarine,
   binutils,
   cairo,
   git,
+  glaze,
   hyprcursor,
   hyprgraphics,
   hyprland-protocols,
@@ -102,6 +104,7 @@ in
         makeWrapper
         meson
         ninja
+        cmake # needed for glaze
         pkg-config
       ];
 
@@ -116,6 +119,7 @@ in
           aquamarine
           cairo
           git
+          glaze
           hyprcursor
           hyprgraphics
           hyprland-protocols