aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorVaxry <[email protected]>2023-12-06 16:31:37 +0000
committerVaxry <[email protected]>2023-12-06 23:31:03 +0000
commit1540f6c627381bb60f4fd8fbde65fe5470040889 (patch)
tree3d6ebd29edf20491a0998b3392284a52208535ac
parent5489f9f07a73c6b5b97702731a5092463a01fb5b (diff)
downloadHyprland-1540f6c627381bb60f4fd8fbde65fe5470040889.tar.gz
Hyprland-1540f6c627381bb60f4fd8fbde65fe5470040889.zip
add hyprpm
-rw-r--r--.gitmodules3
-rwxr-xr-xCMakeLists.txt3
-rw-r--r--hyprpm/CMakeLists.txt13
-rw-r--r--hyprpm/src/core/DataState.cpp183
-rw-r--r--hyprpm/src/core/DataState.hpp17
-rw-r--r--hyprpm/src/core/Manifest.cpp104
-rw-r--r--hyprpm/src/core/Manifest.hpp33
-rw-r--r--hyprpm/src/core/Plugin.hpp17
-rw-r--r--hyprpm/src/core/PluginManager.cpp623
-rw-r--r--hyprpm/src/core/PluginManager.hpp29
-rw-r--r--hyprpm/src/helpers/Colors.hpp11
-rw-r--r--hyprpm/src/main.cpp72
-rw-r--r--hyprpm/src/progress/CProgressBar.cpp80
-rw-r--r--hyprpm/src/progress/CProgressBar.hpp17
14 files changed, 1204 insertions, 1 deletions
diff --git a/.gitmodules b/.gitmodules
index c62f5b53..cdb427c7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -10,3 +10,6 @@
[submodule "subprojects/tracy"]
path = subprojects/tracy
url = https://github.com/wolfpld/tracy
+[submodule "hyprpm/subprojects/tomlpp"]
+ path = hyprpm/subprojects/tomlpp
+ url = https://github.com/marzer/tomlplusplus
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/hyprpm/CMakeLists.txt b/hyprpm/CMakeLists.txt
new file mode 100644
index 00000000..038a38a7
--- /dev/null
+++ b/hyprpm/CMakeLists.txt
@@ -0,0 +1,13 @@
+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)
+
+add_executable(hyprpm ${SRCFILES})
+target_include_directories(hyprpm PUBLIC "." "subprojects/tomlpp/include")
diff --git a/hyprpm/src/core/DataState.cpp b/hyprpm/src/core/DataState.cpp
new file mode 100644
index 00000000..7b29b862
--- /dev/null
+++ b/hyprpm/src/core/DataState.cpp
@@ -0,0 +1,183 @@
+#include "DataState.hpp"
+#include <toml++/toml.hpp>
+#include <iostream>
+#include <filesystem>
+#include <fstream>
+
+void DataState::addNewPluginRepo(const SPluginRepository& repo) {
+ const auto HOME = getenv("HOME");
+ if (!HOME) {
+ std::cerr << "DataState: no $HOME\n";
+ return;
+ }
+ const auto PATH = std::string(HOME) + "/.hyprpm/" + 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) {
+ const auto HOME = getenv("HOME");
+ if (!HOME) {
+ std::cerr << "DataState: no $HOME\n";
+ return false;
+ }
+ const auto PATH = std::string(HOME) + "/.hyprpm/";
+
+ 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) {
+ const auto HOME = getenv("HOME");
+ if (!HOME) {
+ std::cerr << "DataState: no $HOME\n";
+ return;
+ }
+ const auto PATH = std::string(HOME) + "/.hyprpm/";
+
+ 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) {
+ std::filesystem::remove_all(entry.path());
+ return;
+ }
+ }
+}
+
+void DataState::updateGlobalState(const SGlobalState& state) {
+ const auto HOME = getenv("HOME");
+ if (!HOME) {
+ std::cerr << "DataState: no $HOME\n";
+ return;
+ }
+ const auto PATH = std::string(HOME) + "/.hyprpm/";
+
+ std::filesystem::create_directories(PATH);
+ // clang-format off
+ auto DATA = toml::table{
+ {"state", toml::table{
+ {"hash", state.headersHashCompiled},
+ }}
+ };
+ // clang-format on
+
+ std::ofstream ofs(PATH + "/state.toml", std::ios::trunc);
+ ofs << DATA;
+ ofs.close();
+}
+
+std::vector<SPluginRepository> DataState::getAllRepositories() {
+ const auto HOME = getenv("HOME");
+ if (!HOME) {
+ std::cerr << "DataState: no $HOME\n";
+ return {};
+ }
+ const auto PATH = std::string(HOME) + "/.hyprpm/";
+
+ std::vector<SPluginRepository> 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) {
+ const auto HOME = getenv("HOME");
+ if (!HOME) {
+ std::cerr << "DataState: no $HOME\n";
+ return false;
+ }
+ const auto PATH = std::string(HOME) + "/.hyprpm/";
+
+ 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..be8792bb
--- /dev/null
+++ b/hyprpm/src/core/DataState.hpp
@@ -0,0 +1,17 @@
+#pragma once
+#include <string>
+#include <vector>
+#include "Plugin.hpp"
+
+struct SGlobalState {
+ std::string headersHashCompiled;
+};
+
+namespace DataState {
+ void addNewPluginRepo(const SPluginRepository& repo);
+ void removePluginRepo(const std::string& urlOrName);
+ bool pluginRepoExists(const std::string& urlOrName);
+ void updateGlobalState(const SGlobalState& state);
+ bool setPluginEnabled(const std::string& name, bool enabled);
+ std::vector<SPluginRepository> 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 <toml++/toml.hpp>
+#include <iostream>
+
+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..6411451e
--- /dev/null
+++ b/hyprpm/src/core/Manifest.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+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<std::string> authors;
+ std::vector<std::string> buildSteps;
+ std::string output;
+ };
+
+ struct {
+ std::string name;
+ std::vector<std::string> authors;
+ std::vector<std::pair<std::string, std::string>> commitPins;
+ } m_sRepository;
+
+ std::vector<SManifestPlugin> 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 <string>
+#include <vector>
+
+struct SPlugin {
+ std::string name;
+ std::string filename;
+ bool enabled;
+};
+
+struct SPluginRepository {
+ std::string url;
+ std::string name;
+ std::vector<SPlugin> 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..4cba72bd
--- /dev/null
+++ b/hyprpm/src/core/PluginManager.cpp
@@ -0,0 +1,623 @@
+#include "PluginManager.hpp"
+#include "../helpers/Colors.hpp"
+#include "../progress/CProgressBar.hpp"
+#include "Manifest.hpp"
+#include "DataState.hpp"
+
+#include <iostream>
+#include <array>
+#include <filesystem>
+#include <thread>
+#include <fstream>
+#include <algorithm>
+
+#include <toml++/toml.hpp>
+
+std::string execAndGet(std::string cmd) {
+ cmd += " 2>&1";
+ std::array<char, 128> buffer;
+ std::string result;
+ const std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
+ if (!pipe)
+ return "";
+
+ while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
+ result += buffer.data();
+ }
+ return result;
+}
+
+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;
+ }
+
+ 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<CManifest> 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<CManifest>(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<CManifest>(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";
+ 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 ~/.hyprpm
+ SPluginRepository repo;
+ std::string repohash = execAndGet("cd /tmp/hyprpm/new/ && git rev-parse HEAD");
+ if (repohash.length() > 0)
+ repohash.pop_back();
+ repo.name = url.substr(url.find_last_of('/') + 1);
+ 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 HLVERCALL = execAndGet("hyprctl version");
+
+ if (!HLVERCALL.contains("Tag:"))
+ return HEADERS_NOT_HYPRLAND;
+
+ std::string hlcommit = HLVERCALL.substr(HLVERCALL.find("at commit") + 10);
+ hlcommit = hlcommit.substr(0, hlcommit.find_first_of(' '));
+
+ // 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<char>(ifs)), (std::istreambuf_iterator<char>()));
+ 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 != hlcommit)
+ return HEADERS_MISMATCHED;
+
+ return HEADERS_OK;
+}
+
+bool CPluginManager::updateHeaders() {
+
+ const auto HLVERCALL = execAndGet("hyprctl version");
+
+ if (!HLVERCALL.contains("Tag:")) {
+ std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " You don't seem to be running Hyprland.";
+ return false;
+ }
+
+ std::string hlcommit = HLVERCALL.substr(HLVERCALL.find("at commit") + 10);
+ hlcommit = hlcommit.substr(0, hlcommit.find_first_of(' '));
+
+ 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";
+ DataState::updateGlobalState(SGlobalState{hlcommit});
+ 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 reset --hard --recurse-submodules " + hlcommit);
+
+ 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");
+
+ 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\"");
+
+ // remove build files
+ std::filesystem::remove_all("/tmp/hyprpm/hyprland");
+
+ if (headersValid() == HEADERS_OK) {
+ progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " installed headers");
+ progress.m_iSteps = 5;
+ progress.m_szCurrentMessage = "Done!";
+ progress.print();
+
+ DataState::updateGlobalState(SGlobalState{hlcommit});
+
+ std::cout << "\n";
+ } else {
+ progress.printMessageAbove(std::string{Colors::RED} + "✖" + Colors::RESET + " failed to install headers");
+ 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.";
+ return false;
+ }
+
+ const auto REPOS = DataState::getAllRepositories();
+
+ if (REPOS.size() < 1) {
+ std::cout << "\n" << std::string{Colors::RED} + "✖" + Colors::RESET + " No repos to update.";
+ return true;
+ }
+
+ const auto HLVERCALL = execAndGet("hyprctl version");
+
+ if (!HLVERCALL.contains("Tag:")) {
+ std::cerr << "\n" << Colors::RED << "✖" << Colors::RESET << " You don't seem to be running Hyprland.";
+ return false;
+ }
+
+ std::string hlcommit = HLVERCALL.substr(HLVERCALL.find("at commit") + 10);
+ hlcommit = hlcommit.substr(0, hlcommit.find_first_of(' '));
+
+ 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<CManifest> 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<CManifest>(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<CManifest>(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 != hlcommit)
+ 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;
+ break;
+ }
+
+ progress.printMessageAbove(std::string{Colors::GREEN} + "✔" + Colors::RESET + " built " + p.name + " into " + p.output);
+ }
+
+ if (failed)
+ continue;
+
+ // add repo toml to ~/.hyprpm
+ SPluginRepository newrepo = repo;
+ newrepo.plugins.clear();
+ std::string repohash = execAndGet(
+ "cd /tmp/hyprpm/update/ && git pull --recurse-submodules && git reset --hard --recurse-submodules && git rev-parse HEAD"); // repo hash in the state.toml has to match head and not any pin
+ if (repohash.length() > 0)
+ repohash.pop_back();
+ newrepo.hash = repohash;
+ for (auto& p : pManifest->m_vPlugins) {
+ newrepo.plugins.push_back(SPlugin{p.name, "/tmp/hyprpm/update/" + p.output, 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;
+}
+
+void 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.";
+ return;
+ }
+
+ const auto HOME = getenv("HOME");
+ const auto HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE");
+ if (!HOME || !HIS) {
+ std::cerr << "PluginManager: no $HOME or HIS\n";
+ return;
+ }
+ const auto HYPRPMPATH = std::string(HOME) + "/.hyprpm/";
+
+ auto pluginLines = execAndGet("hyprctl plugins list | grep Plugin");
+
+ std::vector<std::string> 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
+ execAndGet("hyprctl plugin unload " + HYPRPMPATH + repoForName(p) + "/" + p + ".so");
+ 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;
+
+ execAndGet("hyprctl plugin load " + HYPRPMPATH + repoForName(p.name) + "/" + p.filename);
+ std::cout << Colors::GREEN << "✔" << Colors::RESET << " Loaded " << p.name << "\n";
+ }
+ }
+
+ std::cout << Colors::GREEN << "✔" << Colors::RESET << " Plugin load state ensured\n";
+} \ 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..9cd6ddfa
--- /dev/null
+++ b/hyprpm/src/core/PluginManager.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <memory>
+#include <string>
+
+enum eHeadersErrors
+{
+ HEADERS_OK,
+ HEADERS_NOT_HYPRLAND,
+ HEADERS_MISSING,
+ HEADERS_CORRUPTED,
+ HEADERS_MISMATCHED,
+};
+
+class CPluginManager {
+ public:
+ bool addNewPluginRepo(const std::string& url);
+ bool removePluginRepo(const std::string& urlOrName);
+
+ eHeadersErrors headersValid();
+ bool updateHeaders();
+ bool updatePlugins(bool forceUpdateAll);
+
+ bool enablePlugin(const std::string& name);
+ bool disablePlugin(const std::string& name);
+ void ensurePluginsLoadState();
+};
+
+inline std::unique_ptr<CPluginManager> 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..c6f083a8
--- /dev/null
+++ b/hyprpm/src/main.cpp
@@ -0,0 +1,72 @@
+#include "progress/CProgressBar.hpp"
+#include "helpers/Colors.hpp"
+#include "core/PluginManager.hpp"
+
+#include <iostream>
+#include <vector>
+#include <string>
+#include <chrono>
+#include <thread>
+
+int main(int argc, char** argv, char** envp) {
+ std::vector<std::string> ARGS{argc};
+ for (int i = 0; i < argc; ++i) {
+ ARGS[i] = std::string{argv[i]};
+ }
+
+ if (ARGS.size() < 2) {
+ std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args.\n";
+ return 1;
+ }
+
+ g_pPluginManager = std::make_unique<CPluginManager>();
+
+ if (ARGS[1] == "add") {
+ if (ARGS.size() < 3) {
+ std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args for add.\n";
+ return 1;
+ }
+
+ return g_pPluginManager->addNewPluginRepo(ARGS[2]) ? 0 : 1;
+ } else if (ARGS[1] == "remove") {
+ if (ARGS.size() < 3) {
+ std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args for remove.\n";
+ return 1;
+ }
+
+ return g_pPluginManager->removePluginRepo(ARGS[2]) ? 0 : 1;
+ } else if (ARGS[1] == "update") {
+ bool headersValid = g_pPluginManager->headersValid() == HEADERS_OK;
+ bool headers = g_pPluginManager->updateHeaders();
+ if (headers)
+ g_pPluginManager->updatePlugins(!headersValid);
+ } else if (ARGS[1] == "enable") {
+ if (ARGS.size() < 3) {
+ std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args for enable.\n";
+ return 1;
+ }
+
+ if (!g_pPluginManager->enablePlugin(ARGS[2])) {
+ std::cerr << Colors::RED << "✖" << Colors::RESET << " Couldn't enable plugin (missing?)\n";
+ return 1;
+ }
+
+ g_pPluginManager->ensurePluginsLoadState();
+ } else if (ARGS[1] == "disable") {
+ if (ARGS.size() < 3) {
+ std::cerr << Colors::RED << "✖" << Colors::RESET << " Not enough args for disable.\n";
+ return 1;
+ }
+
+ if (!g_pPluginManager->disablePlugin(ARGS[2])) {
+ std::cerr << Colors::RED << "✖" << Colors::RESET << " Couldn't disable plugin (missing?)\n";
+ return 1;
+ }
+
+ g_pPluginManager->ensurePluginsLoadState();
+ } else if (ARGS[1] == "load") {
+ g_pPluginManager->ensurePluginsLoadState();
+ }
+
+ return 0;
+} \ No newline at end of file
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 <iostream>
+#include <algorithm>
+#include <cmath>
+#include <format>
+
+#include <sys/ioctl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#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<int>(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 <string>
+
+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