diff --git a/flake.lock b/flake.lock index 23bd37a91..29f517b77 100644 --- a/flake.lock +++ b/flake.lock @@ -141,6 +141,32 @@ "type": "github" } }, + "hyprland-qtutils": { + "inputs": { + "hyprutils": [ + "hyprutils" + ], + "nixpkgs": [ + "nixpkgs" + ], + "systems": [ + "systems" + ] + }, + "locked": { + "lastModified": 1733472316, + "narHash": "sha256-PvXiFLIExJEJj+goLbIuXLTN5CSDSAUsAfiYSdbbWg0=", + "owner": "hyprwm", + "repo": "hyprland-qtutils", + "rev": "969427419276c7ee170301ef1ebe0f68eb6eb2e2", + "type": "github" + }, + "original": { + "owner": "hyprwm", + "repo": "hyprland-qtutils", + "type": "github" + } + }, "hyprlang": { "inputs": { "hyprutils": [ @@ -215,11 +241,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1732758367, - "narHash": "sha256-RzaI1RO0UXqLjydtz3GAXSTzHkpb/lLD1JD8a0W4Wpo=", + "lastModified": 1733392399, + "narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fa42b5a5f401aab8a32bd33c9a4de0738180dc59", + "rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661", "type": "github" }, "original": { @@ -255,11 +281,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1732021966, - "narHash": "sha256-mnTbjpdqF0luOkou8ZFi2asa1N3AA2CchR/RqCNmsGE=", + "lastModified": 1733318908, + "narHash": "sha256-SVQVsbafSM1dJ4fpgyBqLZ+Lft+jcQuMtEL3lQWx2Sk=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "3308484d1a443fc5bc92012435d79e80458fe43c", + "rev": "6f4e2a2112050951a314d2733a994fbab94864c6", "type": "github" }, "original": { @@ -274,6 +300,7 @@ "hyprcursor": "hyprcursor", "hyprgraphics": "hyprgraphics", "hyprland-protocols": "hyprland-protocols", + "hyprland-qtutils": "hyprland-qtutils", "hyprlang": "hyprlang", "hyprutils": "hyprutils", "hyprwayland-scanner": "hyprwayland-scanner", @@ -320,11 +347,11 @@ ] }, "locked": { - "lastModified": 1731703417, - "narHash": "sha256-rheDc/7C+yI+QspYr9J2z9kQ5P9F4ATapI7qyFAe1XA=", + "lastModified": 1733157064, + "narHash": "sha256-NetqJHAN4bbZDQADvpep+wXk2AbMZ2bN6tINz8Kpz6M=", "owner": "hyprwm", "repo": "xdg-desktop-portal-hyprland", - "rev": "8070f36deec723de71e7557441acb17e478204d3", + "rev": "fd85ef39369f95eed67fdf3f025e86916edeea2f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index f26f1c311..821a5f90f 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,13 @@ inputs.systems.follows = "systems"; }; + hyprland-qtutils = { + url = "github:hyprwm/hyprland-qtutils"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.systems.follows = "systems"; + inputs.hyprutils.follows = "hyprutils"; + }; + hyprlang = { url = "github:hyprwm/hyprlang"; inputs.nixpkgs.follows = "nixpkgs"; @@ -123,13 +130,11 @@ inherit (pkgsFor.${system}) # hyprland-packages - hyprland hyprland-debug hyprland-legacy-renderer hyprland-unwrapped # hyprland-extras - xdg-desktop-portal-hyprland ; hyprland-cross = (pkgsCrossFor.${system} "aarch64-linux").hyprland; diff --git a/nix/default.nix b/nix/default.nix index 3a84ccc8e..aeb4a4bbd 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -14,6 +14,7 @@ hyprcursor, hyprgraphics, hyprland-protocols, + hyprland-qtutils, hyprlang, hyprutils, hyprwayland-scanner, @@ -168,6 +169,7 @@ in wrapProgram $out/bin/Hyprland \ --suffix PATH : ${makeBinPath [ binutils + hyprland-qtutils pciutils pkgconf ]} diff --git a/nix/overlays.nix b/nix/overlays.nix index c2103f311..b632d0b4d 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -24,6 +24,7 @@ in { inputs.hyprcursor.overlays.default inputs.hyprgraphics.overlays.default inputs.hyprland-protocols.overlays.default + inputs.hyprland-qtutils.overlays.default inputs.hyprlang.overlays.default inputs.hyprutils.overlays.default inputs.hyprwayland-scanner.overlays.default diff --git a/src/Compositor.cpp b/src/Compositor.cpp index 1261d0039..74a2c8d14 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -6,6 +6,7 @@ #include "managers/TokenManager.hpp" #include "managers/PointerManager.hpp" #include "managers/SeatManager.hpp" +#include "managers/VersionKeeperManager.hpp" #include "managers/eventLoop/EventLoopManager.hpp" #include #include @@ -645,6 +646,9 @@ void CCompositor::initManagers(eManagersInitStage stage) { Debug::log(LOG, "Creating the CursorManager!"); g_pCursorManager = std::make_unique(); + Debug::log(LOG, "Creating the VersionKeeper!"); + g_pVersionKeeperMgr = std::make_unique(); + Debug::log(LOG, "Starting XWayland"); g_pXWayland = std::make_unique(g_pCompositor->m_bEnableXwayland); } break; @@ -2610,7 +2614,8 @@ WORKSPACEID CCompositor::getNewSpecialID() { } void CCompositor::performUserChecks() { - static auto PNOCHECKXDG = CConfigValue("misc:disable_xdg_env_checks"); + static auto PNOCHECKXDG = CConfigValue("misc:disable_xdg_env_checks"); + static auto PNOCHECKQTUTILS = CConfigValue("misc:disable_hyprland_qtutils_check"); if (!*PNOCHECKXDG) { const auto CURRENT_DESKTOP_ENV = getenv("XDG_CURRENT_DESKTOP"); @@ -2622,6 +2627,13 @@ void CCompositor::performUserChecks() { } } + if (!*PNOCHECKQTUTILS) { + if (!executableExistsInPath("hyprland-dialog")) { + g_pHyprNotificationOverlay->addNotification( + "Your system does not have hyprland-qtutils installed. This is a runtime dependency for some dialogs. Consider installing it.", CHyprColor{}, 15000, ICON_WARNING); + } + } + if (g_pHyprOpenGL->failedAssetsNo > 0) { g_pHyprNotificationOverlay->addNotification(std::format("Hyprland failed to load {} essential asset{}, blame your distro's packager for doing a bad job at packaging!", g_pHyprOpenGL->failedAssetsNo, g_pHyprOpenGL->failedAssetsNo > 1 ? "s" : ""), diff --git a/src/config/ConfigDescriptions.hpp b/src/config/ConfigDescriptions.hpp index b95eeab1a..a1d4858c3 100644 --- a/src/config/ConfigDescriptions.hpp +++ b/src/config/ConfigDescriptions.hpp @@ -1133,6 +1133,12 @@ inline static const std::vector CONFIG_OPTIONS = { .type = CONFIG_OPTION_BOOL, .data = SConfigOptionDescription::SBoolData{false}, }, + SConfigOptionDescription{ + .value = "misc:disable_hyprland_qtutils_check", + .description = "disable the warning if hyprland-qtutils is missing", + .type = CONFIG_OPTION_BOOL, + .data = SConfigOptionDescription::SBoolData{false}, + }, SConfigOptionDescription{ .value = "misc:lockdead_screen_delay", .description = "the delay in ms after the lockdead screen appears if the lock screen did not appear after a lock event occurred.", diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 820844eb6..50fbe403d 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -383,6 +383,7 @@ CConfigManager::CConfigManager() { m_pConfig->addConfigValue("misc:middle_click_paste", Hyprlang::INT{1}); m_pConfig->addConfigValue("misc:render_unfocused_fps", Hyprlang::INT{15}); m_pConfig->addConfigValue("misc:disable_xdg_env_checks", Hyprlang::INT{0}); + m_pConfig->addConfigValue("misc:disable_hyprland_qtutils_check", Hyprlang::INT{0}); m_pConfig->addConfigValue("misc:lockdead_screen_delay", Hyprlang::INT{1000}); m_pConfig->addConfigValue("group:insert_after_current", Hyprlang::INT{1}); @@ -606,6 +607,8 @@ CConfigManager::CConfigManager() { m_pConfig->addConfigValue("render:direct_scanout", Hyprlang::INT{0}); m_pConfig->addConfigValue("render:expand_undersized_textures", Hyprlang::INT{1}); + m_pConfig->addConfigValue("ecosystem:no_update_news", Hyprlang::INT{0}); + // devices m_pConfig->addSpecialCategory("device", {"name"}); m_pConfig->addSpecialConfigValue("device", "sensitivity", {0.F}); diff --git a/src/helpers/MiscFunctions.cpp b/src/helpers/MiscFunctions.cpp index dc181d4f9..2da90eb73 100644 --- a/src/helpers/MiscFunctions.cpp +++ b/src/helpers/MiscFunctions.cpp @@ -930,4 +930,34 @@ float stringToPercentage(const std::string& VALUE, const float REL) { return (std::stof(VALUE.substr(0, VALUE.length() - 1)) * REL) / 100.f; else return std::stof(VALUE); -}; +} + +bool executableExistsInPath(const std::string& exe) { + if (!getenv("PATH")) + return false; + + static CVarList paths(getenv("PATH"), 0, ':', true); + + for (auto& p : paths) { + std::string path = p + std::string{"/"} + exe; + std::error_code ec; + if (!std::filesystem::exists(path, ec) || ec) + continue; + + if (!std::filesystem::is_regular_file(path, ec) || ec) + continue; + + auto stat = std::filesystem::status(path, ec); + if (ec) + continue; + + auto perms = stat.permissions(); + + if (std::filesystem::perms::none == (perms & std::filesystem::perms::others_exec)) + return false; + + return true; + } + + return false; +} diff --git a/src/helpers/MiscFunctions.hpp b/src/helpers/MiscFunctions.hpp index bb686df40..018efbedb 100644 --- a/src/helpers/MiscFunctions.hpp +++ b/src/helpers/MiscFunctions.hpp @@ -42,6 +42,7 @@ bool envEnabled(const std::string& env); int allocateSHMFile(size_t len); bool allocateSHMFilePair(size_t size, int* rw_fd_ptr, int* ro_fd_ptr); float stringToPercentage(const std::string& VALUE, const float REL); +bool executableExistsInPath(const std::string& exe); template [[deprecated("use std::format instead")]] std::string getFormat(std::format_string fmt, Args&&... args) { diff --git a/src/managers/VersionKeeperManager.cpp b/src/managers/VersionKeeperManager.cpp new file mode 100644 index 000000000..89f7823de --- /dev/null +++ b/src/managers/VersionKeeperManager.cpp @@ -0,0 +1,153 @@ +#include "VersionKeeperManager.hpp" +#include "../debug/Log.hpp" +#include "../macros.hpp" +#include "../version.h" +#include "../helpers/MiscFunctions.hpp" +#include "../helpers/varlist/VarList.hpp" +#include "eventLoop/EventLoopManager.hpp" +#include "../config/ConfigValue.hpp" + +#include +#include +#include +#include + +using namespace Hyprutils::String; +using namespace Hyprutils::OS; + +constexpr const char* VERSION_FILE_NAME = "lastVersion"; + +CVersionKeeperManager::CVersionKeeperManager() { + static auto PNONOTIFY = CConfigValue("ecosystem:no_update_news"); + + const auto DATAROOT = getDataHome(); + + if (!DATAROOT) + return; + + const auto LASTVER = getDataLastVersion(*DATAROOT); + + if (!LASTVER) + return; + + if (!isVersionOlderThanRunning(*LASTVER)) { + Debug::log(LOG, "CVersionKeeperManager: Read version {} matches or is older than running.", *LASTVER); + return; + } + + writeVersionToVersionFile(*DATAROOT); + + if (*PNONOTIFY) { + Debug::log(LOG, "CVersionKeeperManager: updated, but update news is disabled in the config :("); + return; + } + + if (!executableExistsInPath("hyprland-update-screen")) { + Debug::log(ERR, "CVersionKeeperManager: hyprland-update-screen doesn't seem to exist, skipping notif about update..."); + return; + } + + g_pEventLoopManager->doLater([]() { + CProcess proc("hyprland-update-screen", {"--new-version", HYPRLAND_VERSION}); + proc.runAsync(); + }); +} + +std::optional CVersionKeeperManager::getDataHome() { + const auto DATA_HOME = getenv("XDG_DATA_HOME"); + + std::string dataRoot; + + if (!DATA_HOME) { + const auto HOME = getenv("HOME"); + + if (!HOME) { + Debug::log(ERR, "CVersionKeeperManager: can't get data home: no $HOME or $XDG_DATA_HOME"); + return std::nullopt; + } + + dataRoot = HOME + std::string{"/.local/share/"}; + } else + dataRoot = DATA_HOME + std::string{"/"}; + + std::error_code ec; + if (!std::filesystem::exists(dataRoot, ec) || ec) { + Debug::log(ERR, "CVersionKeeperManager: can't get data home: inaccessible / missing"); + return std::nullopt; + } + + dataRoot += "hyprland/"; + + if (!std::filesystem::exists(dataRoot, ec) || ec) { + Debug::log(LOG, "CVersionKeeperManager: no hyprland data home, creating."); + std::filesystem::create_directory(dataRoot, ec); + if (ec) { + Debug::log(ERR, "CVersionKeeperManager: can't create new data home for hyprland"); + return std::nullopt; + } + std::filesystem::permissions(dataRoot, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::owner_exec, ec); + if (ec) + Debug::log(WARN, "CVersionKeeperManager: couldn't set perms on hyprland data store. Proceeding anyways."); + } + + if (!std::filesystem::exists(dataRoot, ec) || ec) { + Debug::log(ERR, "CVersionKeeperManager: no hyprland data home, failed to create."); + return std::nullopt; + } + + return dataRoot; +} + +std::optional CVersionKeeperManager::getDataLastVersion(const std::string& dataRoot) { + std::error_code ec; + std::string lastVerFile = dataRoot + "/" + VERSION_FILE_NAME; + + if (!std::filesystem::exists(lastVerFile, ec) || ec) { + Debug::log(LOG, "CVersionKeeperManager: no hyprland last version file, creating."); + writeVersionToVersionFile(dataRoot); + + return HYPRLAND_VERSION; + } + + std::ifstream file(lastVerFile); + if (!file.good()) { + Debug::log(ERR, "CVersionKeeperManager: couldn't open an ifstream for reading the version file."); + return std::nullopt; + } + + return trim(std::string((std::istreambuf_iterator(file)), (std::istreambuf_iterator()))); +} + +void CVersionKeeperManager::writeVersionToVersionFile(const std::string& dataRoot) { + std::string lastVerFile = dataRoot + "/" + VERSION_FILE_NAME; + std::ofstream of(lastVerFile, std::ios::trunc); + if (!of.good()) { + Debug::log(ERR, "CVersionKeeperManager: couldn't open an ofstream for writing the version file."); + return; + } + + of << HYPRLAND_VERSION; + of.close(); +} + +bool CVersionKeeperManager::isVersionOlderThanRunning(const std::string& ver) { + const CVarList verStrings(ver, 0, '.', true); + + const int V1 = configStringToInt(verStrings[0]).value_or(0); + const int V2 = configStringToInt(verStrings[1]).value_or(0); + const int V3 = configStringToInt(verStrings[2]).value_or(0); + + static const CVarList runningStrings(HYPRLAND_VERSION, 0, '.', true); + + static const int R1 = configStringToInt(runningStrings[0]).value_or(0); + static const int R2 = configStringToInt(runningStrings[1]).value_or(0); + static const int R3 = configStringToInt(runningStrings[2]).value_or(0); + + if (R1 > V1) + return true; + if (R2 > V2) + return true; + if (R3 > V3) + return true; + return false; +} diff --git a/src/managers/VersionKeeperManager.hpp b/src/managers/VersionKeeperManager.hpp new file mode 100644 index 000000000..f0dc05ce8 --- /dev/null +++ b/src/managers/VersionKeeperManager.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +class CVersionKeeperManager { + public: + CVersionKeeperManager(); + + private: + std::optional getDataHome(); + std::optional getDataLastVersion(const std::string& dataRoot); + void writeVersionToVersionFile(const std::string& dataRoot); + bool isVersionOlderThanRunning(const std::string& ver); +}; + +inline std::unique_ptr g_pVersionKeeperMgr; \ No newline at end of file