diff --git a/src/Compositor.cpp b/src/Compositor.cpp index 79574084..fcd15312 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -9,6 +9,7 @@ #include "managers/PointerManager.hpp" #include "managers/SeatManager.hpp" #include "managers/VersionKeeperManager.hpp" +#include "managers/DonationNagManager.hpp" #include "managers/eventLoop/EventLoopManager.hpp" #include #include @@ -25,6 +26,7 @@ #endif #include #include "helpers/varlist/VarList.hpp" +#include "helpers/fs/FsUtils.hpp" #include "protocols/FractionalScale.hpp" #include "protocols/PointerConstraints.hpp" #include "protocols/LayerShell.hpp" @@ -544,6 +546,8 @@ void CCompositor::cleanup() { g_pSeatManager.reset(); g_pHyprCtl.reset(); g_pEventLoopManager.reset(); + g_pVersionKeeperMgr.reset(); + g_pDonationNagManager.reset(); if (m_pAqBackend) m_pAqBackend.reset(); @@ -645,6 +649,9 @@ void CCompositor::initManagers(eManagersInitStage stage) { Debug::log(LOG, "Creating the VersionKeeper!"); g_pVersionKeeperMgr = std::make_unique(); + Debug::log(LOG, "Creating the DonationNag!"); + g_pDonationNagManager = std::make_unique(); + Debug::log(LOG, "Starting XWayland"); g_pXWayland = std::make_unique(g_pCompositor->m_bEnableXwayland); } break; @@ -2645,7 +2652,7 @@ void CCompositor::performUserChecks() { } if (!*PNOCHECKQTUTILS) { - if (!executableExistsInPath("hyprland-dialog")) { + if (!NFsUtils::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); } diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index b575a167..3f8862ee 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -621,6 +621,7 @@ CConfigManager::CConfigManager() { m_pConfig->addConfigValue("render:ctm_animation", Hyprlang::INT{2}); m_pConfig->addConfigValue("ecosystem:no_update_news", Hyprlang::INT{0}); + m_pConfig->addConfigValue("ecosystem:no_donation_nag", Hyprlang::INT{0}); m_pConfig->addConfigValue("experimental:wide_color_gamut", Hyprlang::INT{0}); m_pConfig->addConfigValue("experimental:hdr", Hyprlang::INT{0}); diff --git a/src/helpers/MiscFunctions.cpp b/src/helpers/MiscFunctions.cpp index e970b781..08a1106a 100644 --- a/src/helpers/MiscFunctions.cpp +++ b/src/helpers/MiscFunctions.cpp @@ -911,30 +911,3 @@ float stringToPercentage(const std::string& VALUE, const float REL) { 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(); - - return std::filesystem::perms::none != (perms & std::filesystem::perms::others_exec); - } - - return false; -} diff --git a/src/helpers/MiscFunctions.hpp b/src/helpers/MiscFunctions.hpp index b179b3d6..64802279 100644 --- a/src/helpers/MiscFunctions.hpp +++ b/src/helpers/MiscFunctions.hpp @@ -40,7 +40,6 @@ 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/helpers/fs/FsUtils.cpp b/src/helpers/fs/FsUtils.cpp new file mode 100644 index 00000000..0bc2e685 --- /dev/null +++ b/src/helpers/fs/FsUtils.cpp @@ -0,0 +1,107 @@ +#include "FsUtils.hpp" +#include "../../debug/Log.hpp" + +#include +#include + +#include +#include +using namespace Hyprutils::String; + +std::optional NFsUtils::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, "FsUtils::getDataHome: 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, "FsUtils::getDataHome: can't get data home: inaccessible / missing"); + return std::nullopt; + } + + dataRoot += "hyprland/"; + + if (!std::filesystem::exists(dataRoot, ec) || ec) { + Debug::log(LOG, "FsUtils::getDataHome: no hyprland data home, creating."); + std::filesystem::create_directory(dataRoot, ec); + if (ec) { + Debug::log(ERR, "FsUtils::getDataHome: 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, "FsUtils::getDataHome: couldn't set perms on hyprland data store. Proceeding anyways."); + } + + if (!std::filesystem::exists(dataRoot, ec) || ec) { + Debug::log(ERR, "FsUtils::getDataHome: no hyprland data home, failed to create."); + return std::nullopt; + } + + return dataRoot; +} + +std::optional NFsUtils::readFileAsString(const std::string& path) { + std::error_code ec; + + if (!std::filesystem::exists(path, ec) || ec) + return std::nullopt; + + std::ifstream file(path); + if (!file.good()) + return std::nullopt; + + return trim(std::string((std::istreambuf_iterator(file)), (std::istreambuf_iterator()))); +} + +bool NFsUtils::writeToFile(const std::string& path, const std::string& content) { + std::ofstream of(path, std::ios::trunc); + if (!of.good()) { + Debug::log(ERR, "CVersionKeeperManager: couldn't open an ofstream for writing the version file."); + return false; + } + + of << content; + of.close(); + + return true; +} + +bool NFsUtils::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(); + + return std::filesystem::perms::none != (perms & std::filesystem::perms::others_exec); + } + + return false; +} diff --git a/src/helpers/fs/FsUtils.hpp b/src/helpers/fs/FsUtils.hpp new file mode 100644 index 00000000..bc3b3bf1 --- /dev/null +++ b/src/helpers/fs/FsUtils.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +#include + +namespace NFsUtils { + // Returns the path to the hyprland directory in data home. + std::optional getDataHome(); + + std::optional readFileAsString(const std::string& path); + + // overwrites the file if exists + bool writeToFile(const std::string& path, const std::string& content); + + bool executableExistsInPath(const std::string& exe); +}; diff --git a/src/managers/DonationNagManager.cpp b/src/managers/DonationNagManager.cpp new file mode 100644 index 00000000..d7eab9ae --- /dev/null +++ b/src/managers/DonationNagManager.cpp @@ -0,0 +1,114 @@ +#include "DonationNagManager.hpp" +#include "../debug/Log.hpp" +#include "VersionKeeperManager.hpp" +#include "eventLoop/EventLoopManager.hpp" +#include "../config/ConfigValue.hpp" + +#include +#include + +#include "../helpers/fs/FsUtils.hpp" + +#include +using namespace Hyprutils::OS; + +constexpr const char* LAST_NAG_FILE_NAME = "lastNag"; +constexpr uint64_t DAY_IN_SECONDS = 3600ULL * 24; +constexpr uint64_t MONTH_IN_SECONDS = DAY_IN_SECONDS * 30; + +struct SNagDatePoint { + // Counted from 1, as in Jan 1st is 1, 1 + // No month-boundaries because I am lazy + uint8_t month = 0, dayStart = 0, dayEnd = 0; +}; + +// clang-format off +const std::vector NAG_DATE_POINTS = { + SNagDatePoint { + 7, 20, 31, + }, + SNagDatePoint { + 12, 1, 28 + }, +}; +// clang-format on + +CDonationNagManager::CDonationNagManager() { + static auto PNONAG = CConfigValue("ecosystem:no_donation_nag"); + + if (g_pVersionKeeperMgr->fired() || *PNONAG) + return; + + const auto DATAROOT = NFsUtils::getDataHome(); + + if (!DATAROOT) + return; + + const auto EPOCH = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + + const auto LASTNAGSTR = NFsUtils::readFileAsString(*DATAROOT + "/" + LAST_NAG_FILE_NAME); + + if (!LASTNAGSTR) { + const auto EPOCHSTR = std::format("{}", EPOCH); + NFsUtils::writeToFile(*DATAROOT + "/" + LAST_NAG_FILE_NAME, EPOCHSTR); + return; + } + + uint64_t LAST_EPOCH = 0; + + try { + LAST_EPOCH = std::stoull(*LASTNAGSTR); + } catch (std::exception& e) { + Debug::log(ERR, "DonationNag: Last epoch invalid? Failed to parse \"{}\". Setting to today.", *LASTNAGSTR); + const auto EPOCHSTR = std::format("{}", EPOCH); + NFsUtils::writeToFile(*DATAROOT + "/" + LAST_NAG_FILE_NAME, EPOCHSTR); + return; + } + + // don't nag if the last nag was less than a month ago. This is + // mostly for first-time nags, as other nags happen in specific time frames shorter than a month + if (EPOCH - LAST_EPOCH < MONTH_IN_SECONDS) { + Debug::log(LOG, "DonationNag: last nag was {} days ago, too early for a nag.", (int)std::round((EPOCH - LAST_EPOCH) / (double)MONTH_IN_SECONDS)); + return; + } + + if (!NFsUtils::executableExistsInPath("hyprland-donate-screen")) { + Debug::log(ERR, "DonationNag: executable doesn't exist, skipping."); + return; + } + + auto tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + auto local = *localtime(&tt); + + const auto MONTH = local.tm_mon + 1; + const auto DAY = local.tm_mday; + + for (const auto& nagPoint : NAG_DATE_POINTS) { + if (MONTH != nagPoint.month) + continue; + + if (DAY < nagPoint.dayStart || DAY > nagPoint.dayEnd) + continue; + + Debug::log(LOG, "DonationNag: hit nag month {} days {}-{}, it's {} today, nagging", MONTH, nagPoint.dayStart, nagPoint.dayEnd, DAY); + + m_bFired = true; + + const auto EPOCHSTR = std::format("{}", EPOCH); + NFsUtils::writeToFile(*DATAROOT + "/" + LAST_NAG_FILE_NAME, EPOCHSTR); + + g_pEventLoopManager->doLater([] { + CProcess proc("hyprland-donate-screen", {}); + proc.runAsync(); + }); + + break; + } + + if (!m_bFired) + Debug::log(LOG, "DonationNag: didn't hit any nagging periods"); +} + +bool CDonationNagManager::fired() { + return m_bFired; +} \ No newline at end of file diff --git a/src/managers/DonationNagManager.hpp b/src/managers/DonationNagManager.hpp new file mode 100644 index 00000000..e296d815 --- /dev/null +++ b/src/managers/DonationNagManager.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +class CDonationNagManager { + public: + CDonationNagManager(); + + // whether the donation nag was shown this boot. + bool fired(); + + private: + bool m_bFired = false; +}; + +inline std::unique_ptr g_pDonationNagManager; \ No newline at end of file diff --git a/src/managers/VersionKeeperManager.cpp b/src/managers/VersionKeeperManager.cpp index 1ef1040e..cc03a7b9 100644 --- a/src/managers/VersionKeeperManager.cpp +++ b/src/managers/VersionKeeperManager.cpp @@ -6,6 +6,7 @@ #include "../helpers/varlist/VarList.hpp" #include "eventLoop/EventLoopManager.hpp" #include "../config/ConfigValue.hpp" +#include "../helpers/fs/FsUtils.hpp" #include #include @@ -20,12 +21,12 @@ constexpr const char* VERSION_FILE_NAME = "lastVersion"; CVersionKeeperManager::CVersionKeeperManager() { static auto PNONOTIFY = CConfigValue("ecosystem:no_update_news"); - const auto DATAROOT = getDataHome(); + const auto DATAROOT = NFsUtils::getDataHome(); if (!DATAROOT) return; - const auto LASTVER = getDataLastVersion(*DATAROOT); + const auto LASTVER = NFsUtils::readFileAsString(*DATAROOT + "/" + VERSION_FILE_NAME); if (!LASTVER) return; @@ -35,101 +36,26 @@ CVersionKeeperManager::CVersionKeeperManager() { return; } - writeVersionToVersionFile(*DATAROOT); + NFsUtils::writeToFile(*DATAROOT + "/" + VERSION_FILE_NAME, HYPRLAND_VERSION); if (*PNONOTIFY) { Debug::log(LOG, "CVersionKeeperManager: updated, but update news is disabled in the config :("); return; } - if (!executableExistsInPath("hyprland-update-screen")) { + if (!NFsUtils::executableExistsInPath("hyprland-update-screen")) { Debug::log(ERR, "CVersionKeeperManager: hyprland-update-screen doesn't seem to exist, skipping notif about update..."); return; } + m_bFired = true; + 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 "0.0.0"; - } - - 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); @@ -151,3 +77,7 @@ bool CVersionKeeperManager::isVersionOlderThanRunning(const std::string& ver) { return true; return false; } + +bool CVersionKeeperManager::fired() { + return m_bFired; +} diff --git a/src/managers/VersionKeeperManager.hpp b/src/managers/VersionKeeperManager.hpp index f0dc05ce..eb404d88 100644 --- a/src/managers/VersionKeeperManager.hpp +++ b/src/managers/VersionKeeperManager.hpp @@ -1,17 +1,18 @@ #pragma once #include -#include class CVersionKeeperManager { public: CVersionKeeperManager(); + // whether the update screen was shown this boot. + bool fired(); + private: - std::optional getDataHome(); - std::optional getDataLastVersion(const std::string& dataRoot); - void writeVersionToVersionFile(const std::string& dataRoot); - bool isVersionOlderThanRunning(const std::string& ver); + bool isVersionOlderThanRunning(const std::string& ver); + + bool m_bFired = false; }; inline std::unique_ptr g_pVersionKeeperMgr; \ No newline at end of file