From d36055054651442db19df046cbdd0697b805c541 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:41:09 +0000 Subject: [PATCH] hyprpm: Add hyprpm, a Hyprland Plugin Manager (#4072) --- .github/workflows/ci.yaml | 6 +- .github/workflows/security-checks.yml | 2 +- CMakeLists.txt | 3 +- Makefile | 2 + hyprpm/CMakeLists.txt | 14 + hyprpm/src/core/DataState.cpp | 215 ++++++++ hyprpm/src/core/DataState.hpp | 21 + hyprpm/src/core/Manifest.cpp | 104 ++++ hyprpm/src/core/Manifest.hpp | 32 ++ hyprpm/src/core/Plugin.hpp | 17 + hyprpm/src/core/PluginManager.cpp | 690 ++++++++++++++++++++++++++ hyprpm/src/core/PluginManager.hpp | 59 +++ hyprpm/src/helpers/Colors.hpp | 11 + hyprpm/src/main.cpp | 144 ++++++ hyprpm/src/meson.build | 10 + hyprpm/src/progress/CProgressBar.cpp | 80 +++ hyprpm/src/progress/CProgressBar.hpp | 17 + meson.build | 1 + nix/default.nix | 8 +- 19 files changed, 1428 insertions(+), 8 deletions(-) create mode 100644 hyprpm/CMakeLists.txt create mode 100644 hyprpm/src/core/DataState.cpp create mode 100644 hyprpm/src/core/DataState.hpp create mode 100644 hyprpm/src/core/Manifest.cpp create mode 100644 hyprpm/src/core/Manifest.hpp create mode 100644 hyprpm/src/core/Plugin.hpp create mode 100644 hyprpm/src/core/PluginManager.cpp create mode 100644 hyprpm/src/core/PluginManager.hpp create mode 100644 hyprpm/src/helpers/Colors.hpp create mode 100644 hyprpm/src/main.cpp create mode 100644 hyprpm/src/meson.build create mode 100644 hyprpm/src/progress/CProgressBar.cpp create mode 100644 hyprpm/src/progress/CProgressBar.hpp diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54248e3a..e68dc2a8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,7 +12,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers xcb-util-errors xcb-util-renderutil xcb-util-wm xorg-fonts-encodings xorg-server-common xorg-setxkbmap xorg-xkbcomp xorg-xwayland git cmake go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd python libliftoff + pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers xcb-util-errors xcb-util-renderutil xcb-util-wm xorg-fonts-encodings xorg-server-common xorg-setxkbmap xorg-xkbcomp xorg-xwayland git cmake go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd python libliftoff tomlplusplus - name: Set up user run: | useradd -m githubuser @@ -61,7 +61,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers git go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd cmake jq python libliftoff + pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers git go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd cmake jq python libliftoff tomlplusplus - name: Set up user run: | useradd -m githubuser @@ -90,7 +90,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers git cmake go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd libliftoff + pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers git cmake go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd libliftoff tomlplusplus - name: Set up user run: | useradd -m githubuser diff --git a/.github/workflows/security-checks.yml b/.github/workflows/security-checks.yml index 6b7d71e5..644aa29d 100644 --- a/.github/workflows/security-checks.yml +++ b/.github/workflows/security-checks.yml @@ -54,7 +54,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers xcb-util-errors xcb-util-renderutil xcb-util-wm xorg-fonts-encodings xorg-server-common xorg-setxkbmap xorg-xkbcomp xorg-xwayland git cmake go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd python libliftoff + pacman --noconfirm --noprogressbar -Sy glslang libepoxy libfontenc libxcvt libxfont2 libxkbfile vulkan-headers vulkan-validation-layers xcb-util-errors xcb-util-renderutil xcb-util-wm xorg-fonts-encodings xorg-server-common xorg-setxkbmap xorg-xkbcomp xorg-xwayland git cmake go clang lld libc++ pkgconf meson ninja wayland wayland-protocols libinput libxkbcommon pixman glm libdrm libglvnd cairo pango systemd scdoc base-devel seatd python libliftoff tomlplusplus useradd -m githubuser echo -e "root ALL=(ALL:ALL) ALL\ngithubuser ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers su githubuser -c "cd ~ && git clone https://aur.archlinux.org/libdisplay-info.git && cd ./libdisplay-info && makepkg -si --skippgpcheck --noconfirm --noprogressbar" diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f52c554..bedb7bbf 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -245,5 +245,6 @@ protocol("staging/tearing-control/tearing-control-v1.xml" "tearing-control-v1" f protocol("unstable/text-input/text-input-unstable-v1.xml" "text-input-unstable-v1" false) protocol("staging/cursor-shape/cursor-shape-v1.xml" "cursor-shape-v1" false) -# hyprctl +# tools add_subdirectory(hyprctl) +add_subdirectory(hyprpm) diff --git a/Makefile b/Makefile index 6213102e..f03931fe 100644 --- a/Makefile +++ b/Makefile @@ -38,8 +38,10 @@ install: mkdir -p ${PREFIX}/bin cp -f ./build/Hyprland ${PREFIX}/bin cp -f ./build/hyprctl/hyprctl ${PREFIX}/bin + cp -f ./build/hyprpm/hyprpm ${PREFIX}/bin chmod 755 ${PREFIX}/bin/Hyprland chmod 755 ${PREFIX}/bin/hyprctl + chmod 755 ${PREFIX}/bin/hyprpm if [ ! -f ${PREFIX}/share/wayland-sessions/hyprland.desktop ]; then cp ./example/hyprland.desktop ${PREFIX}/share/wayland-sessions; fi mkdir -p ${PREFIX}/share/hyprland cp ./assets/wall_* ${PREFIX}/share/hyprland diff --git a/hyprpm/CMakeLists.txt b/hyprpm/CMakeLists.txt new file mode 100644 index 00000000..e9cbefa1 --- /dev/null +++ b/hyprpm/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.19) + +project( + hyprpm + DESCRIPTION "A Hyprland Plugin Manager" +) + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") + +set(CMAKE_CXX_STANDARD 23) + +pkg_check_modules(deps REQUIRED IMPORTED_TARGET tomlplusplus) + +add_executable(hyprpm ${SRCFILES}) diff --git a/hyprpm/src/core/DataState.cpp b/hyprpm/src/core/DataState.cpp new file mode 100644 index 00000000..d95f10bb --- /dev/null +++ b/hyprpm/src/core/DataState.cpp @@ -0,0 +1,215 @@ +#include "DataState.hpp" +#include +#include +#include +#include +#include "PluginManager.hpp" + +std::string DataState::getDataStatePath() { + const auto HOME = getenv("HOME"); + if (!HOME) { + std::cerr << "DataState: no $HOME\n"; + throw std::runtime_error("no $HOME"); + return ""; + } + + 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"; +} + +void DataState::ensureStateStoreExists() { + const auto PATH = getDataStatePath(); + + if (!std::filesystem::exists(PATH)) + std::filesystem::create_directories(PATH); +} + +void DataState::addNewPluginRepo(const SPluginRepository& repo) { + ensureStateStoreExists(); + + const auto PATH = getDataStatePath() + "/" + repo.name; + + std::filesystem::create_directories(PATH); + // clang-format off + auto DATA = toml::table{ + {"repository", toml::table{ + {"name", repo.name}, + {"hash", repo.hash}, + {"url", repo.url} + }} + }; + for (auto& p : repo.plugins) { + // copy .so to the good place + std::filesystem::copy_file(p.filename, PATH + "/" + p.name + ".so"); + + DATA.emplace(p.name, toml::table{ + {"filename", p.name + ".so"}, + {"enabled", p.enabled} + }); + } + // clang-format on + + std::ofstream ofs(PATH + "/state.toml", std::ios::trunc); + ofs << DATA; + ofs.close(); +} + +bool DataState::pluginRepoExists(const std::string& urlOrName) { + ensureStateStoreExists(); + + const auto PATH = getDataStatePath(); + + for (const auto& entry : std::filesystem::directory_iterator(PATH)) { + if (!entry.is_directory()) + 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(""); + + if (URL == urlOrName || NAME == urlOrName) + return true; + } + + return false; +} + +void DataState::removePluginRepo(const std::string& urlOrName) { + ensureStateStoreExists(); + + const auto PATH = getDataStatePath(); + + for (const auto& entry : std::filesystem::directory_iterator(PATH)) { + if (!entry.is_directory()) + 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(""); + + if (URL == urlOrName || NAME == urlOrName) { + + // unload the plugins!! + for (const auto& file : std::filesystem::directory_iterator(entry.path())) { + if (!file.path().string().ends_with(".so")) + continue; + + g_pPluginManager->loadUnloadPlugin(std::filesystem::absolute(file.path()), false); + } + + std::filesystem::remove_all(entry.path()); + return; + } + } +} + +void DataState::updateGlobalState(const SGlobalState& state) { + ensureStateStoreExists(); + + const auto PATH = getDataStatePath(); + + std::filesystem::create_directories(PATH); + // clang-format off + auto DATA = toml::table{ + {"state", toml::table{ + {"hash", state.headersHashCompiled}, + {"dont_warn_install", state.dontWarnInstall} + }} + }; + // clang-format on + + std::ofstream ofs(PATH + "/state.toml", std::ios::trunc); + ofs << DATA; + ofs.close(); +} + +SGlobalState DataState::getGlobalState() { + ensureStateStoreExists(); + + const auto PATH = getDataStatePath(); + + if (!std::filesystem::exists(PATH + "/state.toml")) + return SGlobalState{}; + + auto DATA = toml::parse_file(PATH + "/state.toml"); + + SGlobalState state; + state.headersHashCompiled = DATA["state"]["hash"].value_or(""); + state.dontWarnInstall = DATA["state"]["dont_warn_install"].value_or(false); + + return state; +} + +std::vector DataState::getAllRepositories() { + ensureStateStoreExists(); + + const auto PATH = getDataStatePath(); + + std::vector repos; + + for (const auto& entry : std::filesystem::directory_iterator(PATH)) { + if (!entry.is_directory()) + 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(""); + const auto HASH = STATE["repository"]["hash"].value_or(""); + + SPluginRepository repo; + repo.hash = HASH; + repo.name = NAME; + repo.url = URL; + + for (const auto& [key, val] : STATE) { + if (key == "repository") + continue; + + const auto ENABLED = STATE[key]["enabled"].value_or(false); + const auto FILENAME = STATE[key]["filename"].value_or(""); + + repo.plugins.push_back(SPlugin{std::string{key.str()}, FILENAME, ENABLED}); + } + + repos.push_back(repo); + } + + return repos; +} + +bool DataState::setPluginEnabled(const std::string& name, bool enabled) { + ensureStateStoreExists(); + + const auto PATH = getDataStatePath(); + + for (const auto& entry : std::filesystem::directory_iterator(PATH)) { + if (!entry.is_directory()) + continue; + + auto STATE = toml::parse_file(entry.path().string() + "/state.toml"); + + for (const auto& [key, val] : STATE) { + if (key == "repository") + continue; + + if (key.str() != name) + continue; + + (*STATE[key].as_table()).insert_or_assign("enabled", enabled); + + std::ofstream state(entry.path().string() + "/state.toml", std::ios::trunc); + state << STATE; + state.close(); + + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/hyprpm/src/core/DataState.hpp b/hyprpm/src/core/DataState.hpp new file mode 100644 index 00000000..ac81dae1 --- /dev/null +++ b/hyprpm/src/core/DataState.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include "Plugin.hpp" + +struct SGlobalState { + std::string headersHashCompiled = ""; + bool dontWarnInstall = false; +}; + +namespace DataState { + std::string getDataStatePath(); + 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 getAllRepositories(); +}; \ No newline at end of file diff --git a/hyprpm/src/core/Manifest.cpp b/hyprpm/src/core/Manifest.cpp new file mode 100644 index 00000000..27ad058a --- /dev/null +++ b/hyprpm/src/core/Manifest.cpp @@ -0,0 +1,104 @@ +#include "Manifest.hpp" +#include +#include + +CManifest::CManifest(const eManifestType type, const std::string& path) { + auto manifest = toml::parse_file(path); + + if (type == MANIFEST_HYPRLOAD) { + for (auto& [key, val] : manifest) { + if (key.str().ends_with(".build")) + continue; + + CManifest::SManifestPlugin plugin; + plugin.name = key; + m_vPlugins.push_back(plugin); + } + + for (auto& plugin : m_vPlugins) { + plugin.description = manifest[plugin.name]["description"].value_or("?"); + plugin.version = manifest[plugin.name]["version"].value_or("?"); + plugin.output = manifest[plugin.name]["build"]["output"].value_or("?"); + auto authors = manifest[plugin.name]["authors"].as_array(); + if (authors) { + for (auto&& a : *authors) { + plugin.authors.push_back(a.as_string()->value_or("?")); + } + } else { + auto author = manifest[plugin.name]["author"].value_or(""); + if (!std::string{author}.empty()) + plugin.authors.push_back(author); + } + auto buildSteps = manifest[plugin.name]["build"]["steps"].as_array(); + if (buildSteps) { + for (auto&& s : *buildSteps) { + plugin.buildSteps.push_back(s.as_string()->value_or("?")); + } + } + + if (plugin.output.empty() || plugin.buildSteps.empty()) { + m_bGood = false; + return; + } + } + } else if (type == MANIFEST_HYPRPM) { + m_sRepository.name = manifest["repository"]["name"].value_or(""); + auto authors = manifest["repository"]["authors"].as_array(); + if (authors) { + for (auto&& a : *authors) { + m_sRepository.authors.push_back(a.as_string()->value_or("?")); + } + } else { + auto author = manifest["repository"]["author"].value_or(""); + if (!std::string{author}.empty()) + m_sRepository.authors.push_back(author); + } + + auto pins = manifest["repository"]["commit_pins"].as_array(); + if (pins) { + for (auto&& pin : *pins) { + auto pinArr = pin.as_array(); + if (pinArr && pinArr->get(1)) + m_sRepository.commitPins.push_back(std::make_pair<>(pinArr->get(0)->as_string()->get(), pinArr->get(1)->as_string()->get())); + } + } + + for (auto& [key, val] : manifest) { + if (key.str() == "repository") + continue; + + CManifest::SManifestPlugin plugin; + plugin.name = key; + m_vPlugins.push_back(plugin); + } + + for (auto& plugin : m_vPlugins) { + plugin.description = manifest[plugin.name]["description"].value_or("?"); + plugin.output = manifest[plugin.name]["output"].value_or("?"); + auto authors = manifest[plugin.name]["authors"].as_array(); + if (authors) { + for (auto&& a : *authors) { + plugin.authors.push_back(a.as_string()->value_or("?")); + } + } else { + auto author = manifest[plugin.name]["author"].value_or(""); + if (!std::string{author}.empty()) + plugin.authors.push_back(author); + } + auto buildSteps = manifest[plugin.name]["build"].as_array(); + if (buildSteps) { + for (auto&& s : *buildSteps) { + plugin.buildSteps.push_back(s.as_string()->value_or("?")); + } + } + + if (plugin.output.empty() || plugin.buildSteps.empty()) { + m_bGood = false; + return; + } + } + } else { + // ??? + m_bGood = false; + } +} \ No newline at end of file diff --git a/hyprpm/src/core/Manifest.hpp b/hyprpm/src/core/Manifest.hpp new file mode 100644 index 00000000..63e1791f --- /dev/null +++ b/hyprpm/src/core/Manifest.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +enum eManifestType { + MANIFEST_HYPRLOAD, + MANIFEST_HYPRPM +}; + +class CManifest { + public: + CManifest(const eManifestType type, const std::string& path); + + struct SManifestPlugin { + std::string name; + std::string description; + std::string version; + std::vector authors; + std::vector buildSteps; + std::string output; + }; + + struct { + std::string name; + std::vector authors; + std::vector> commitPins; + } m_sRepository; + + std::vector m_vPlugins; + bool m_bGood = true; +}; \ No newline at end of file diff --git a/hyprpm/src/core/Plugin.hpp b/hyprpm/src/core/Plugin.hpp new file mode 100644 index 00000000..32c02a49 --- /dev/null +++ b/hyprpm/src/core/Plugin.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +struct SPlugin { + std::string name; + std::string filename; + bool enabled; +}; + +struct SPluginRepository { + std::string url; + std::string name; + std::vector plugins; + std::string hash; +}; \ No newline at end of file diff --git a/hyprpm/src/core/PluginManager.cpp b/hyprpm/src/core/PluginManager.cpp new file mode 100644 index 00000000..8efcdf98 --- /dev/null +++ b/hyprpm/src/core/PluginManager.cpp @@ -0,0 +1,690 @@ +#include "PluginManager.hpp" +#include "../helpers/Colors.hpp" +#include "../progress/CProgressBar.hpp" +#include "Manifest.hpp" +#include "DataState.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +std::string execAndGet(std::string cmd) { + cmd += " 2>&1"; + std::array buffer; + std::string result; + const std::unique_ptr pipe(popen(cmd.c_str(), "r"), pclose); + if (!pipe) + return ""; + + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; +} + +SHyprlandVersion CPluginManager::getHyprlandVersion() { + static SHyprlandVersion ver; + static bool once = false; + + if (once) + return ver; + + once = true; + const auto HLVERCALL = execAndGet("hyprctl version"); + if (m_bVerbose) + std::cout << Colors::BLUE << "[v] " << Colors::RESET << "version returned: " << HLVERCALL << "\n"; + + if (!HLVERCALL.contains("Tag:")) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " You don't seem to be running Hyprland."; + return SHyprlandVersion{}; + } + + std::string hlcommit = HLVERCALL.substr(HLVERCALL.find("at commit") + 10); + hlcommit = hlcommit.substr(0, hlcommit.find_first_of(' ')); + + std::string hlbranch = HLVERCALL.substr(HLVERCALL.find("from branch") + 12); + hlbranch = hlbranch.substr(0, hlbranch.find(" at commit ")); + + if (m_bVerbose) + std::cout << Colors::BLUE << "[v] " << Colors::RESET << "parsed commit " << hlcommit << " at branch " << hlbranch << "\n"; + + ver = SHyprlandVersion{hlbranch, hlcommit}; + return ver; +} + +bool CPluginManager::addNewPluginRepo(const std::string& url) { + + if (DataState::pluginRepoExists(url)) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Could not clone the plugin repository. Repository already installed.\n"; + return false; + } + + auto GLOBALSTATE = DataState::getGlobalState(); + if (!GLOBALSTATE.dontWarnInstall) { + std::cout << Colors::YELLOW << "!" << Colors::RED << " Disclaimer:\n " << Colors::RESET + << "plugins, especially not official, have no guarantee of stability, availablity or security.\n Run them at your own risk.\n " + << "This message will not appear again.\n"; + GLOBALSTATE.dontWarnInstall = true; + DataState::updateGlobalState(GLOBALSTATE); + } + + std::cout << Colors::GREEN << "✔" << Colors::RESET << Colors::RED << " adding a new plugin repository " << Colors::RESET << "from " << url << "\n " << Colors::RED + << "MAKE SURE" << Colors::RESET << " that you trust the authors. " << Colors::RED << "DO NOT" << Colors::RESET + << " install random plugins without verifying the code and author.\n " + << "Are you sure? [Y/n] "; + std::fflush(stdout); + std::string input; + std::getline(std::cin, input); + + if (input.size() > 0 && input[0] != 'Y' && input[0] != 'y') { + std::cout << "Aborting.\n"; + return false; + } + + CProgressBar progress; + progress.m_iMaxSteps = 5; + progress.m_iSteps = 0; + progress.m_szCurrentMessage = "Cloning the plugin repository"; + + progress.print(); + + if (!std::filesystem::exists("/tmp/hyprpm")) { + std::filesystem::create_directory("/tmp/hyprpm"); + std::filesystem::permissions("/tmp/hyprpm", std::filesystem::perms::all, std::filesystem::perm_options::replace); + } + + if (std::filesystem::exists("/tmp/hyprpm/new")) { + progress.printMessageAbove(std::string{Colors::YELLOW} + "!" + Colors::RESET + " old plugin repo build files found in temp directory, removing."); + std::filesystem::remove_all("/tmp/hyprpm/new"); + } + + progress.printMessageAbove(std::string{Colors::RESET} + " → Cloning " + url); + + std::string ret = execAndGet("cd /tmp/hyprpm && git clone " + url + " new"); + + if (!std::filesystem::exists("/tmp/hyprpm/new")) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Could not clone the plugin repository. shell returned:\n" << ret << "\n"; + return false; + } + + progress.m_iSteps = 1; + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " cloned"); + progress.m_szCurrentMessage = "Reading the manifest"; + progress.print(); + + std::unique_ptr pManifest; + + if (std::filesystem::exists("/tmp/hyprpm/new/hyprpm.toml")) { + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " found hyprpm manifest"); + pManifest = std::make_unique(MANIFEST_HYPRPM, "/tmp/hyprpm/new/hyprpm.toml"); + } else if (std::filesystem::exists("/tmp/hyprpm/new/hyprload.toml")) { + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " found hyprload manifest"); + pManifest = std::make_unique(MANIFEST_HYPRLOAD, "/tmp/hyprpm/new/hyprload.toml"); + } + + if (!pManifest) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " The provided plugin repository does not have a valid manifest\n"; + return false; + } + + if (!pManifest->m_bGood) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " The provided plugin repository has a corrupted manifest\n"; + return false; + } + + progress.m_iSteps = 2; + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " parsed manifest, found " + std::to_string(pManifest->m_vPlugins.size()) + " plugins:"); + for (auto& pl : pManifest->m_vPlugins) { + std::string message = std::string{Colors::RESET} + " → " + pl.name + " by "; + for (auto& a : pl.authors) { + message += a + ", "; + } + if (pl.authors.size() > 0) { + message.pop_back(); + message.pop_back(); + } + message += " version " + pl.version; + progress.printMessageAbove(message); + } + progress.m_szCurrentMessage = "Verifying headers"; + progress.print(); + + const auto HEADERSSTATUS = headersValid(); + + if (HEADERSSTATUS != HEADERS_OK) { + + switch (HEADERSSTATUS) { + case HEADERS_CORRUPTED: std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Headers corrupted. Please run hyprpm update to fix those.\n"; break; + case HEADERS_MISMATCHED: std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Headers version mismatch. Please run hyprpm update to fix those.\n"; break; + case HEADERS_NOT_HYPRLAND: std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " It doesn't seem you are running on hyprland.\n"; break; + case HEADERS_MISSING: std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Headers missing. Please run hyprpm update to fix those.\n"; break; + default: break; + } + + return false; + } + + progress.m_iSteps = 3; + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " Hyprland headers OK"); + progress.m_szCurrentMessage = "Building plugin(s)"; + progress.print(); + + for (auto& p : pManifest->m_vPlugins) { + std::string out; + + progress.printMessageAbove(std::string{Colors::RESET} + " → Building " + p.name); + + for (auto& bs : p.buildSteps) { + out += execAndGet("cd /tmp/hyprpm/new && " + bs) + "\n"; + } + + if (!std::filesystem::exists("/tmp/hyprpm/new/" + p.output)) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Plugin " << p.name << " failed to build.\n"; + + if (m_bVerbose) + std::cout << Colors::BLUE << "[v] " << Colors::RESET << "shell returned: " << out << "\n"; + + return false; + } + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " built " + p.name + " into " + p.output); + } + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " all plugins built"); + progress.m_iSteps = 4; + progress.m_szCurrentMessage = "Installing repository"; + progress.print(); + + // add repo toml to DataState + SPluginRepository repo; + std::string repohash = execAndGet("cd /tmp/hyprpm/new/ && git rev-parse HEAD"); + if (repohash.length() > 0) + repohash.pop_back(); + repo.name = pManifest->m_sRepository.name.empty() ? url.substr(url.find_last_of('/') + 1) : pManifest->m_sRepository.name; + repo.url = url; + repo.hash = repohash; + for (auto& p : pManifest->m_vPlugins) { + repo.plugins.push_back(SPlugin{p.name, "/tmp/hyprpm/new/" + p.output, false}); + } + DataState::addNewPluginRepo(repo); + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " installed repository"); + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " you can now enable the plugin(s) with hyprpm enable"); + progress.m_iSteps = 5; + progress.m_szCurrentMessage = "Done!"; + progress.print(); + + std::cout << "\n"; + + // remove build files + std::filesystem::remove_all("/tmp/hyprpm/new"); + + return true; +} + +bool CPluginManager::removePluginRepo(const std::string& urlOrName) { + if (!DataState::pluginRepoExists(urlOrName)) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Could not remove the repository. Repository is not installed.\n"; + return false; + } + + std::cout << Colors::YELLOW << "!" << Colors::RESET << Colors::RED << " removing a plugin repository: " << Colors::RESET << urlOrName << "\n " + << "Are you sure? [Y/n] "; + std::fflush(stdout); + std::string input; + std::getline(std::cin, input); + + if (input.size() > 0 && input[0] != 'Y' && input[0] != 'y') { + std::cout << "Aborting.\n"; + return false; + } + + DataState::removePluginRepo(urlOrName); + + return true; +} + +eHeadersErrors CPluginManager::headersValid() { + const auto HLVER = getHyprlandVersion(); + + // find headers commit + auto headers = execAndGet("pkg-config --cflags hyprland"); + + if (!headers.contains("-I/")) + return HEADERS_MISSING; + + headers.pop_back(); // pop newline + + std::string verHeader = ""; + + while (!headers.empty()) { + const auto PATH = headers.substr(0, headers.find(" -I/", 3)); + + if (headers.find(" -I/", 3) != std::string::npos) + headers = headers.substr(headers.find("-I/", 3)); + else + headers = ""; + + if (PATH.ends_with("protocols") || PATH.ends_with("wlroots")) + continue; + + verHeader = PATH.substr(2) + "/hyprland/src/version.h"; + break; + } + + if (verHeader.empty()) + return HEADERS_CORRUPTED; + + // read header + std::ifstream ifs(verHeader); + if (!ifs.good()) + return HEADERS_CORRUPTED; + + std::string verHeaderContent((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); + ifs.close(); + + std::string hash = verHeaderContent.substr(verHeaderContent.find("#define GIT_COMMIT_HASH") + 23); + hash = hash.substr(0, hash.find_first_of('\n')); + hash = hash.substr(hash.find_first_of('"') + 1); + hash = hash.substr(0, hash.find_first_of('"')); + + if (hash != HLVER.hash) + return HEADERS_MISMATCHED; + + return HEADERS_OK; +} + +bool CPluginManager::updateHeaders() { + + const auto HLVER = getHyprlandVersion(); + + if (!std::filesystem::exists("/tmp/hyprpm")) { + std::filesystem::create_directory("/tmp/hyprpm"); + std::filesystem::permissions("/tmp/hyprpm", std::filesystem::perms::all, std::filesystem::perm_options::replace); + } + + if (headersValid() == HEADERS_OK) { + std::cout << "\n" << std::string{Colors::GREEN} + "✔" + Colors::RESET + " Your headers are already up-to-date.\n"; + auto GLOBALSTATE = DataState::getGlobalState(); + GLOBALSTATE.headersHashCompiled = HLVER.hash; + DataState::updateGlobalState(GLOBALSTATE); + return true; + } + + CProgressBar progress; + progress.m_iMaxSteps = 5; + progress.m_iSteps = 0; + progress.m_szCurrentMessage = "Cloning the hyprland repository"; + progress.print(); + + if (std::filesystem::exists("/tmp/hyprpm/hyprland")) { + progress.printMessageAbove(std::string{Colors::YELLOW} + "!" + Colors::RESET + " old hyprland source files found in temp directory, removing."); + std::filesystem::remove_all("/tmp/hyprpm/hyprland"); + } + + progress.printMessageAbove(std::string{Colors::YELLOW} + "!" + Colors::RESET + " Cloning https://github.com/hyprwm/hyprland, this might take a moment."); + + std::string ret = execAndGet("cd /tmp/hyprpm && git clone --recursive https://github.com/hyprwm/hyprland hyprland"); + + if (!std::filesystem::exists("/tmp/hyprpm/hyprland")) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Could not clone the hyprland repository. shell returned:\n" << ret << "\n"; + return false; + } + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " cloned"); + progress.m_iSteps = 2; + progress.m_szCurrentMessage = "Checking out sources"; + progress.print(); + + ret = + execAndGet("cd /tmp/hyprpm/hyprland && git checkout " + HLVER.branch + " 2>&1 && git submodule update --init 2>&1 && git reset --hard --recurse-submodules " + HLVER.hash); + + if (m_bVerbose) + progress.printMessageAbove(std::string{Colors::BLUE} + "[v] " + Colors::RESET + "git returned: " + ret); + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " checked out to running ver"); + progress.m_iSteps = 3; + progress.m_szCurrentMessage = "Building Hyprland"; + progress.print(); + + progress.printMessageAbove(std::string{Colors::YELLOW} + "!" + Colors::RESET + " configuring Hyprland"); + + ret = execAndGet("cd /tmp/hyprpm/hyprland && cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -S . -B ./build -G Ninja"); + // le hack. Wlroots has to generate its build/include + ret = execAndGet("cd /tmp/hyprpm/hyprland/subprojects/wlroots && meson setup -Drenderers=gles2 -Dexamples=false build"); + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " configured Hyprland"); + progress.m_iSteps = 4; + progress.m_szCurrentMessage = "Installing sources"; + progress.print(); + + progress.printMessageAbove( + std::string{Colors::YELLOW} + "!" + Colors::RESET + + " in order to install the sources, you will need to input your password.\n If nothing pops up, make sure you have polkit and an authentication daemon running."); + + ret = execAndGet("pkexec sh \"-c\" \"cd /tmp/hyprpm/hyprland && make installheaders\""); + + if (m_bVerbose) + std::cout << Colors::BLUE << "[v] " << Colors::RESET << "pkexec returned: " << ret << "\n"; + + // remove build files + std::filesystem::remove_all("/tmp/hyprpm/hyprland"); + + auto HEADERSVALID = headersValid(); + if (HEADERSVALID == HEADERS_OK) { + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " installed headers"); + progress.m_iSteps = 5; + progress.m_szCurrentMessage = "Done!"; + progress.print(); + + auto GLOBALSTATE = DataState::getGlobalState(); + GLOBALSTATE.headersHashCompiled = HLVER.hash; + DataState::updateGlobalState(GLOBALSTATE); + + std::cout << "\n"; + } else { + progress.printMessageAbove(std::string{Colors::RED} + "✖" + Colors::RESET + " failed to install headers with error code " + std::to_string((int)HEADERSVALID)); + progress.m_iSteps = 5; + progress.m_szCurrentMessage = "Failed"; + progress.print(); + + std::cout << "\n"; + + return false; + } + + return true; +} + +bool CPluginManager::updatePlugins(bool forceUpdateAll) { + if (headersValid() != HEADERS_OK) { + std::cout << "\n" << std::string{Colors::RED} + "✖" + Colors::RESET + " headers are not up-to-date, please run hyprpm update.\n"; + return false; + } + + const auto REPOS = DataState::getAllRepositories(); + + if (REPOS.size() < 1) { + std::cout << "\n" << std::string{Colors::RED} + "✖" + Colors::RESET + " No repos to update.\n"; + return true; + } + + const auto HLVER = getHyprlandVersion(); + + CProgressBar progress; + progress.m_iMaxSteps = REPOS.size() * 2 + 1; + progress.m_iSteps = 0; + progress.m_szCurrentMessage = "Updating repositories"; + progress.print(); + + for (auto& repo : REPOS) { + bool update = forceUpdateAll; + + progress.m_iSteps++; + progress.m_szCurrentMessage = "Updating " + repo.name; + progress.print(); + + progress.printMessageAbove(std::string{Colors::RESET} + " → checking for updates for " + repo.name); + + if (std::filesystem::exists("/tmp/hyprpm/update")) { + progress.printMessageAbove(std::string{Colors::YELLOW} + "!" + Colors::RESET + " old update build files found in temp directory, removing."); + std::filesystem::remove_all("/tmp/hyprpm/update"); + } + + progress.printMessageAbove(std::string{Colors::RESET} + " → Cloning " + repo.url); + + std::string ret = execAndGet("cd /tmp/hyprpm && git clone " + repo.url + " update"); + + if (!std::filesystem::exists("/tmp/hyprpm/update")) { + std::cout << "\n" << std::string{Colors::RED} + "✖" + Colors::RESET + " could not clone repo: shell returned:\n" + ret; + return false; + } + + if (!update) { + // check if git has updates + std::string hash = execAndGet("cd /tmp/hyprpm/update && git rev-parse HEAD"); + if (!hash.empty()) + hash.pop_back(); + + update = update || hash != repo.hash; + } + + if (!update) { + std::filesystem::remove_all("/tmp/hyprpm/update"); + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " repository " + repo.name + " is up-to-date."); + progress.m_iSteps++; + progress.print(); + continue; + } + + // we need to update + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " repository " + repo.name + " has updates."); + progress.printMessageAbove(std::string{Colors::RESET} + " → Building " + repo.name); + progress.m_iSteps++; + progress.print(); + + std::unique_ptr pManifest; + + if (std::filesystem::exists("/tmp/hyprpm/update/hyprpm.toml")) { + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " found hyprpm manifest"); + pManifest = std::make_unique(MANIFEST_HYPRPM, "/tmp/hyprpm/update/hyprpm.toml"); + } else if (std::filesystem::exists("/tmp/hyprpm/update/hyprload.toml")) { + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " found hyprload manifest"); + pManifest = std::make_unique(MANIFEST_HYPRLOAD, "/tmp/hyprpm/update/hyprload.toml"); + } + + if (!pManifest) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " The provided plugin repository does not have a valid manifest\n"; + continue; + } + + if (!pManifest->m_bGood) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " The provided plugin repository has a corrupted manifest\n"; + continue; + } + + if (!pManifest->m_sRepository.commitPins.empty()) { + // check commit pins + + progress.printMessageAbove(std::string{Colors::RESET} + " → Manifest has " + std::to_string(pManifest->m_sRepository.commitPins.size()) + " pins, checking"); + + for (auto& [hl, plugin] : pManifest->m_sRepository.commitPins) { + if (hl != HLVER.hash) + continue; + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " commit pin " + plugin + " matched hl, resetting"); + + execAndGet("cd /tmp/hyprpm/update/ && git reset --hard --recurse-submodules " + plugin); + } + } + + bool failed = false; + for (auto& p : pManifest->m_vPlugins) { + std::string out; + + progress.printMessageAbove(std::string{Colors::RESET} + " → Building " + p.name); + + for (auto& bs : p.buildSteps) { + out += execAndGet("cd /tmp/hyprpm/update && " + bs) + "\n"; + } + + if (!std::filesystem::exists("/tmp/hyprpm/update/" + p.output)) { + std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " Plugin " << p.name << " failed to build.\n"; + failed = true; + if (m_bVerbose) + std::cout << Colors::BLUE << "[v] " << Colors::RESET << "shell returned: " << out << "\n"; + break; + } + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " built " + p.name + " into " + p.output); + } + + if (failed) + continue; + + // add repo toml to DataState + SPluginRepository newrepo = repo; + newrepo.plugins.clear(); + execAndGet( + "cd /tmp/hyprpm/update/ && git pull --recurse-submodules && git reset --hard --recurse-submodules"); // repo hash in the state.toml has to match head and not any pin + std::string repohash = execAndGet("git rev-parse HEAD"); + if (repohash.length() > 0) + repohash.pop_back(); + newrepo.hash = repohash; + for (auto& p : pManifest->m_vPlugins) { + const auto OLDPLUGINIT = std::find_if(repo.plugins.begin(), repo.plugins.end(), [&](const auto& other) { return other.name == p.name; }); + newrepo.plugins.push_back(SPlugin{p.name, "/tmp/hyprpm/update/" + p.output, OLDPLUGINIT != repo.plugins.end() ? OLDPLUGINIT->enabled : false}); + } + DataState::removePluginRepo(newrepo.name); + DataState::addNewPluginRepo(newrepo); + + std::filesystem::remove_all("/tmp/hyprpm/update"); + + progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " updated " + repo.name); + } + + progress.m_iSteps++; + progress.m_szCurrentMessage = "Done!"; + progress.print(); + + std::cout << "\n"; + + return true; +} + +bool CPluginManager::enablePlugin(const std::string& name) { + bool ret = DataState::setPluginEnabled(name, true); + if (ret) + std::cout << Colors::GREEN << "✔" << Colors::RESET << " Enabled " << name << "\n"; + return ret; +} + +bool CPluginManager::disablePlugin(const std::string& name) { + bool ret = DataState::setPluginEnabled(name, false); + if (ret) + std::cout << Colors::GREEN << "✔" << Colors::RESET << " Disabled " << name << "\n"; + return ret; +} + +ePluginLoadStateReturn CPluginManager::ensurePluginsLoadState() { + if (headersValid() != HEADERS_OK) { + std::cerr << "\n" << std::string{Colors::RED} + "✖" + Colors::RESET + " headers are not up-to-date, please run hyprpm update.\n"; + return LOADSTATE_HEADERS_OUTDATED; + } + + const auto HOME = getenv("HOME"); + const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + if (!HOME || !HIS) { + std::cerr << "PluginManager: no $HOME or HIS\n"; + return LOADSTATE_FAIL; + } + const auto HYPRPMPATH = DataState::getDataStatePath() + "/"; + + auto pluginLines = execAndGet("hyprctl plugins list | grep Plugin"); + + std::vector loadedPlugins; + + std::cout << Colors::GREEN << "✔" << Colors::RESET << " Ensuring plugin load state\n"; + + // 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(); + + auto enabled = [REPOS](const std::string& plugin) -> bool { + for (auto& r : REPOS) { + for (auto& p : r.plugins) { + if (p.name == plugin && p.enabled) + return true; + } + } + + return false; + }; + + auto repoForName = [REPOS](const std::string& name) -> std::string { + for (auto& r : REPOS) { + for (auto& p : r.plugins) { + if (p.name == name) + return r.name; + } + } + + return ""; + }; + + // unload disabled plugins + for (auto& p : loadedPlugins) { + if (!enabled(p)) { + // unload + loadUnloadPlugin(HYPRPMPATH + repoForName(p) + "/" + p + ".so", false); + std::cout << Colors::GREEN << "✔" << Colors::RESET << " Unloaded " << p << "\n"; + } + } + + // load enabled plugins + for (auto& r : REPOS) { + for (auto& p : r.plugins) { + if (!p.enabled) + continue; + + if (std::find_if(loadedPlugins.begin(), loadedPlugins.end(), [&](const auto& other) { return other == p.name; }) != loadedPlugins.end()) + continue; + + loadUnloadPlugin(HYPRPMPATH + repoForName(p.name) + "/" + p.filename, true); + std::cout << Colors::GREEN << "✔" << Colors::RESET << " Loaded " << p.name << "\n"; + } + } + + std::cout << Colors::GREEN << "✔" << Colors::RESET << " Plugin load state ensured\n"; + + return LOADSTATE_OK; +} + +bool CPluginManager::loadUnloadPlugin(const std::string& path, bool load) { + if (load) + execAndGet("hyprctl plugin load " + path); + else + execAndGet("hyprctl plugin unload " + path); + + return true; +} + +void CPluginManager::listAllPlugins() { + const auto REPOS = DataState::getAllRepositories(); + + for (auto& r : REPOS) { + std::cout << std::string{Colors::RESET} + " → Repository " + r.name + ":\n"; + + for (auto& p : r.plugins) { + std::cout << std::string{Colors::RESET} + " │ Plugin " + p.name + "\n └─ enabled: " << (p.enabled ? Colors::GREEN : Colors::RED) << (p.enabled ? "true" : "false") + << Colors::RESET << "\n"; + } + } +} + +void CPluginManager::notify(const eNotifyIcons icon, uint32_t color, int durationMs, const std::string& message) { + execAndGet("hyprctl notify " + std::to_string((int)icon) + " " + std::to_string(durationMs) + " " + std::to_string(color) + " " + message); +} \ No newline at end of file diff --git a/hyprpm/src/core/PluginManager.hpp b/hyprpm/src/core/PluginManager.hpp new file mode 100644 index 00000000..dcf890d6 --- /dev/null +++ b/hyprpm/src/core/PluginManager.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +enum eHeadersErrors { + HEADERS_OK = 0, + HEADERS_NOT_HYPRLAND, + HEADERS_MISSING, + HEADERS_CORRUPTED, + HEADERS_MISMATCHED, +}; + +enum eNotifyIcons { + ICON_WARNING = 0, + ICON_INFO, + ICON_HINT, + ICON_ERROR, + ICON_CONFUSED, + ICON_OK, + ICON_NONE +}; + +enum ePluginLoadStateReturn { + LOADSTATE_OK = 0, + LOADSTATE_FAIL, + LOADSTATE_PARTIAL_FAIL, + LOADSTATE_HEADERS_OUTDATED +}; + +struct SHyprlandVersion { + std::string branch; + std::string hash; +}; + +class CPluginManager { + public: + bool addNewPluginRepo(const std::string& url); + bool removePluginRepo(const std::string& urlOrName); + + eHeadersErrors headersValid(); + bool updateHeaders(); + bool updatePlugins(bool forceUpdateAll); + + void listAllPlugins(); + + bool enablePlugin(const std::string& name); + bool disablePlugin(const std::string& name); + ePluginLoadStateReturn ensurePluginsLoadState(); + + bool loadUnloadPlugin(const std::string& path, bool load); + SHyprlandVersion getHyprlandVersion(); + + void notify(const eNotifyIcons icon, uint32_t color, int durationMs, const std::string& message); + + bool m_bVerbose = false; +}; + +inline std::unique_ptr g_pPluginManager; \ No newline at end of file diff --git a/hyprpm/src/helpers/Colors.hpp b/hyprpm/src/helpers/Colors.hpp new file mode 100644 index 00000000..f3298bdd --- /dev/null +++ b/hyprpm/src/helpers/Colors.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace Colors { + constexpr const char* RED = "\x1b[31m"; + constexpr const char* GREEN = "\x1b[32m"; + constexpr const char* YELLOW = "\x1b[33m"; + constexpr const char* BLUE = "\x1b[34m"; + constexpr const char* MAGENTA = "\x1b[35m"; + constexpr const char* CYAN = "\x1b[36m"; + constexpr const char* RESET = "\x1b[0m"; +}; \ No newline at end of file diff --git a/hyprpm/src/main.cpp b/hyprpm/src/main.cpp new file mode 100644 index 00000000..e6191e66 --- /dev/null +++ b/hyprpm/src/main.cpp @@ -0,0 +1,144 @@ +#include "progress/CProgressBar.hpp" +#include "helpers/Colors.hpp" +#include "core/PluginManager.hpp" + +#include +#include +#include +#include +#include + +const std::string HELP = R"#(┏ hyprpm, a Hyprland Plugin Manager +┃ +┣ add [url] → Install a new plugin repository from git +┣ remove [url/name] → Remove an installed plugin repository +┣ enable [name] → Enable a plugin +┣ disable [name] → Disable a plugin +┣ update → Check and update all plugins if needed +┣ reload → Reload hyprpm state. Ensure all enabled plugins are loaded. +┣ list → List all installed plugins +┃ +┣ Flags: +┃ +┣ --notify | -n → Send a hyprland notification for important events (e.g. load fail) +┣ --help | -h → Show this menu +┣ --verbose | -v → Enable too much logging +┗ +)#"; + +int main(int argc, char** argv, char** envp) { + std::vector ARGS{argc}; + for (int i = 0; i < argc; ++i) { + ARGS[i] = std::string{argv[i]}; + } + + if (ARGS.size() < 2) { + std::cout << HELP; + return 1; + } + + std::vector command; + bool notify = false, verbose = false; + + for (int i = 1; i < argc; ++i) { + if (ARGS[i].starts_with("-")) { + if (ARGS[i] == "--help" || ARGS[i] == "-h") { + std::cout << HELP; + return 0; + } else if (ARGS[i] == "--notify" || ARGS[i] == "-n") { + notify = true; + } else if (ARGS[i] == "--verbose" || ARGS[i] == "-v") { + verbose = true; + } else { + std::cerr << "Unrecognized option " << ARGS[i]; + return 1; + } + } else { + command.push_back(ARGS[i]); + } + } + + g_pPluginManager = std::make_unique(); + g_pPluginManager->m_bVerbose = verbose; + + if (command[0] == "add") { + if (command.size() < 2) { + std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args for add.\n"; + return 1; + } + + return g_pPluginManager->addNewPluginRepo(command[1]) ? 0 : 1; + } else if (command[0] == "remove") { + if (ARGS.size() < 2) { + std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args for remove.\n"; + return 1; + } + + return g_pPluginManager->removePluginRepo(command[1]) ? 0 : 1; + } else if (command[0] == "update") { + bool headersValid = g_pPluginManager->headersValid() == HEADERS_OK; + bool headers = g_pPluginManager->updateHeaders(); + if (headers) { + bool ret1 = g_pPluginManager->updatePlugins(!headersValid); + + if (!ret1) + return 1; + + auto ret2 = g_pPluginManager->ensurePluginsLoadState(); + + if (ret2 != LOADSTATE_OK) + return 1; + } else if (notify) + g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Couldn't update headers"); + } else if (command[0] == "enable") { + if (ARGS.size() < 2) { + std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args for enable.\n"; + return 1; + } + + if (!g_pPluginManager->enablePlugin(command[1])) { + std::cerr << Colors::RED << "✖" << Colors::RESET << " Couldn't enable plugin (missing?)\n"; + return 1; + } + + auto ret = g_pPluginManager->ensurePluginsLoadState(); + if (ret != LOADSTATE_OK) + return 1; + } else if (command[0] == "disable") { + if (command.size() < 2) { + std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args for disable.\n"; + return 1; + } + + if (!g_pPluginManager->disablePlugin(command[1])) { + std::cerr << Colors::RED << "✖" << Colors::RESET << " Couldn't disable plugin (missing?)\n"; + return 1; + } + + auto ret = g_pPluginManager->ensurePluginsLoadState(); + if (ret != LOADSTATE_OK) + return 1; + } else if (command[0] == "reload") { + auto ret = g_pPluginManager->ensurePluginsLoadState(); + + if (ret != LOADSTATE_OK && notify) { + switch (ret) { + case LOADSTATE_FAIL: + case LOADSTATE_PARTIAL_FAIL: g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Failed to load plugins"); break; + case LOADSTATE_HEADERS_OUTDATED: + g_pPluginManager->notify(ICON_ERROR, 0, 10000, "[hyprpm] Failed to load plugins: Outdated headers. Please run hyprpm update manually."); + break; + default: break; + } + } else if (notify) { + g_pPluginManager->notify(ICON_OK, 0, 4000, "[hyprpm] Loaded plugins"); + } + } else if (command[0] == "list") { + g_pPluginManager->listAllPlugins(); + } else { + std::cout << HELP; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/hyprpm/src/meson.build b/hyprpm/src/meson.build new file mode 100644 index 00000000..0adae7aa --- /dev/null +++ b/hyprpm/src/meson.build @@ -0,0 +1,10 @@ +globber = run_command('sh', '-c', 'find . -name "*.cpp" | sort', check: true) +src = globber.stdout().strip().split('\n') + +executable('hyprpm', src, + dependencies: [ + dependency('threads'), + dependency('tomlplusplus') + ], + install : true +) diff --git a/hyprpm/src/progress/CProgressBar.cpp b/hyprpm/src/progress/CProgressBar.cpp new file mode 100644 index 00000000..69f51dd2 --- /dev/null +++ b/hyprpm/src/progress/CProgressBar.cpp @@ -0,0 +1,80 @@ +#include "CProgressBar.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include "../helpers/Colors.hpp" + +void CProgressBar::printMessageAbove(const std::string& msg) { + struct winsize w; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + + std::string spaces; + for (size_t i = 0; i < w.ws_col; ++i) { + spaces += ' '; + } + + std::cout << "\r" << spaces << "\r" << msg << "\n"; + print(); +} + +void CProgressBar::print() { + struct winsize w; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); + + if (m_bFirstPrint) + std::cout << "\n"; + m_bFirstPrint = false; + + std::string spaces; + for (size_t i = 0; i < w.ws_col; ++i) { + spaces += ' '; + } + + std::cout << "\r" << spaces << "\r"; + + std::string message = ""; + + float percentDone = 0; + if (m_fPercentage >= 0) + percentDone = m_fPercentage; + else + percentDone = (float)m_iSteps / (float)m_iMaxSteps; + + const auto BARWIDTH = std::clamp(w.ws_col - m_szCurrentMessage.length() - 2, 0UL, 50UL); + + // draw bar + message += std::string{" "} + Colors::GREEN; + size_t i = 0; + for (; i < std::floor(percentDone * BARWIDTH); ++i) { + message += "━"; + } + + if (i < BARWIDTH) { + i++; + + message += std::string{"╍"} + Colors::RESET; + + for (; i < BARWIDTH; ++i) { + message += "━"; + } + } else + message += Colors::RESET; + + // draw progress + if (m_fPercentage >= 0) + message += " " + std::format("{}%", static_cast(percentDone * 100.0)) + " "; + else + message += " " + std::format("{} / {}", m_iSteps, m_iMaxSteps) + " "; + + // draw message + std::cout << message + " " + m_szCurrentMessage; + + std::fflush(stdout); +} \ No newline at end of file diff --git a/hyprpm/src/progress/CProgressBar.hpp b/hyprpm/src/progress/CProgressBar.hpp new file mode 100644 index 00000000..6ac18f21 --- /dev/null +++ b/hyprpm/src/progress/CProgressBar.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +class CProgressBar { + public: + void print(); + void printMessageAbove(const std::string& msg); + + std::string m_szCurrentMessage = ""; + size_t m_iSteps = 0; + size_t m_iMaxSteps = 0; + float m_fPercentage = -1; // if != -1, use percentage + + private: + bool m_bFirstPrint = true; +}; \ No newline at end of file diff --git a/meson.build b/meson.build index de29528e..81d6e403 100644 --- a/meson.build +++ b/meson.build @@ -80,6 +80,7 @@ endforeach subdir('protocols') subdir('src') subdir('hyprctl') +subdir('hyprpm/src') subdir('assets') subdir('example') subdir('docs') diff --git a/nix/default.nix b/nix/default.nix index 7ddeae94..68b1bb22 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -20,6 +20,7 @@ pango, pciutils, systemd, + tomlplusplus, udis86, wayland, wayland-protocols, @@ -81,19 +82,20 @@ assert lib.assertMsg (!hidpiXWayland) "The option `hidpiXWayland` has been remov buildInputs = [ - git cairo + git hyprland-protocols - libGL libdrm_2_4_118 + libGL libinput libxkbcommon mesa pango + pciutils + tomlplusplus udis86 wayland wayland-protocols - pciutils wlroots ] ++ lib.optionals enableXWayland [libxcb xcbutilwm xwayland]