From 1a2fba7fb8fa7b77a3a5417a4f0f266be482da68 Mon Sep 17 00:00:00 2001 From: pupbrained Date: Mon, 28 Apr 2025 04:14:13 -0400 Subject: [PATCH] guhg --- flake.nix | 2 - meson.build | 2 +- src/config/config.cpp | 46 ++-- src/config/{config.h => config.hpp} | 34 ++- src/config/weather.cpp | 109 +++++--- src/config/weather.h | 73 ----- src/config/weather.hpp | 78 ++++++ src/core/system_data.cpp | 36 +-- src/core/system_data.h | 88 ------ src/core/system_data.hpp | 91 ++++++ src/core/util/defs.hpp | 12 + src/core/util/error.hpp | 159 +++++++++++ src/core/util/helpers.hpp | 43 +++ src/core/util/logging.hpp | 286 +++++++++++++++++++ src/core/util/types.hpp | 145 ++++++++++ src/main.cpp | 75 +++-- src/os/linux.cpp | 325 +++++++++++----------- src/os/linux/display_guards.cpp | 70 ----- src/os/linux/display_guards.h | 110 -------- src/os/linux/issetugid_stub.cpp | 5 +- src/os/linux/pkg_count.cpp | 218 ++++++++++++++- src/os/linux/pkg_count.h | 36 --- src/os/linux/pkg_count.hpp | 43 +++ src/os/macos/{bridge.h => bridge.hpp} | 0 src/os/{os.h => os.hpp} | 35 +-- src/util/macros.h | 347 ----------------------- src/util/types.h | 383 -------------------------- src/wrappers/wayland.hpp | 58 ++++ src/wrappers/xcb.hpp | 168 +++++++++++ 29 files changed, 1676 insertions(+), 1401 deletions(-) rename src/config/{config.h => config.hpp} (84%) delete mode 100644 src/config/weather.h create mode 100644 src/config/weather.hpp delete mode 100644 src/core/system_data.h create mode 100644 src/core/system_data.hpp create mode 100644 src/core/util/defs.hpp create mode 100644 src/core/util/error.hpp create mode 100644 src/core/util/helpers.hpp create mode 100644 src/core/util/logging.hpp create mode 100644 src/core/util/types.hpp delete mode 100644 src/os/linux/display_guards.cpp delete mode 100644 src/os/linux/display_guards.h delete mode 100644 src/os/linux/pkg_count.h create mode 100644 src/os/linux/pkg_count.hpp rename src/os/macos/{bridge.h => bridge.hpp} (100%) rename src/os/{os.h => os.hpp} (79%) delete mode 100644 src/util/macros.h delete mode 100644 src/util/types.h create mode 100644 src/wrappers/wayland.hpp create mode 100644 src/wrappers/xcb.hpp diff --git a/flake.nix b/flake.nix index e545c26..59ed070 100644 --- a/flake.nix +++ b/flake.nix @@ -141,8 +141,6 @@ LD_LIBRARY_PATH = "${lib.makeLibraryPath deps}"; NIX_ENFORCE_NO_NATIVE = 0; - - name = "C++"; }; } ); diff --git a/meson.build b/meson.build index bfa9a03..a8c3f57 100644 --- a/meson.build +++ b/meson.build @@ -91,7 +91,7 @@ add_project_arguments(common_cpp_args, language : 'cpp') base_sources = files('src/core/system_data.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', 'src/main.cpp') platform_sources = { - 'linux' : ['src/os/linux.cpp', 'src/os/linux/issetugid_stub.cpp', 'src/os/linux/display_guards.cpp'], + 'linux' : ['src/os/linux.cpp', 'src/os/linux/issetugid_stub.cpp', 'src/os/linux/pkg_count.cpp'], 'darwin' : ['src/os/macos.cpp', 'src/os/macos/bridge.mm'], 'windows' : ['src/os/windows.cpp'], } diff --git a/src/config/config.cpp b/src/config/config.cpp index ff069dc..07226bc 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -1,13 +1,19 @@ -#include -#include +#include "config.hpp" -#include "config.h" +#include // std::filesystem::{path, operator/, exists, create_directories} +#include // std::{ifstream, ofstream, operator<<} +#include // std::runtime_error +#include // std::error_code +#include // toml::{parse_file, parse_result} +#include // std::pair (Pair) -#include "src/util/macros.h" +#include "src/core/util/logging.hpp" namespace fs = std::filesystem; namespace { + using util::types::Vec, util::types::CStr, util::types::Exception; + fn GetConfigPath() -> fs::path { Vec possiblePaths; @@ -25,10 +31,10 @@ namespace { possiblePaths.push_back(fs::path(".") / "config.toml"); #else - if (Result result = GetEnv("XDG_CONFIG_HOME")) + if (Result result = util::helpers::GetEnv("XDG_CONFIG_HOME")) possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml"); - if (Result result = GetEnv("HOME")) { + if (Result result = util::helpers::GetEnv("HOME")) { possiblePaths.emplace_back(fs::path(*result) / ".config" / "draconis++" / "config.toml"); possiblePaths.emplace_back(fs::path(*result) / ".draconis++" / "config.toml"); } @@ -46,7 +52,7 @@ namespace { if (std::error_code errc; !exists(defaultDir, errc) && !errc) { create_directories(defaultDir, errc); if (errc) - WARN_LOG("Warning: Failed to create config directory: {}", errc.message()); + warn_log("Warning: Failed to create config directory: {}", errc.message()); } return possiblePaths[0]; @@ -60,7 +66,7 @@ namespace { std::error_code errc; create_directories(configPath.parent_path(), errc); if (errc) { - ERROR_LOG("Failed to create config directory: {}", errc.message()); + error_log("Failed to create config directory: {}", errc.message()); return false; } @@ -76,8 +82,8 @@ namespace { const passwd* pwd = getpwuid(getuid()); CStr pwdName = pwd ? pwd->pw_name : nullptr; - const Result envUser = GetEnv("USER"); - const Result envLogname = GetEnv("LOGNAME"); + const Result envUser = util::helpers::GetEnv("USER"); + const Result envLogname = util::helpers::GetEnv("LOGNAME"); String defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User"; #endif @@ -97,7 +103,7 @@ namespace { std::ofstream file(configPath); if (!file) { - ERROR_LOG("Failed to open config file for writing: {}", configPath.string()); + error_log("Failed to open config file for writing: {}", configPath.string()); return false; } @@ -124,34 +130,34 @@ namespace { file << "# lat = 51.5074\n"; file << "# lon = -0.1278\n"; - INFO_LOG("Created default config file at {}", configPath.string()); + info_log("Created default config file at {}", configPath.string()); return true; - } catch (const std::exception& e) { - ERROR_LOG("Failed to create default config file: {}", e.what()); + } catch (const Exception& e) { + error_log("Failed to create default config file: {}", e.what()); return false; } } -} +} // namespace fn Config::getInstance() -> Config { try { const fs::path configPath = GetConfigPath(); if (!exists(configPath)) { - INFO_LOG("Config file not found, creating defaults at {}", configPath.string()); + info_log("Config file not found, creating defaults at {}", configPath.string()); if (!CreateDefaultConfig(configPath)) { - WARN_LOG("Failed to create default config, using in-memory defaults"); + warn_log("Failed to create default config, using in-memory defaults"); return {}; } } const toml::parse_result config = toml::parse_file(configPath.string()); - DEBUG_LOG("Config loaded from {}", configPath.string()); + debug_log("Config loaded from {}", configPath.string()); return fromToml(config); - } catch (const std::exception& e) { - DEBUG_LOG("Config loading failed: {}, using defaults", e.what()); + } catch (const Exception& e) { + debug_log("Config loading failed: {}, using defaults", e.what()); return {}; } } diff --git a/src/config/config.h b/src/config/config.hpp similarity index 84% rename from src/config/config.h rename to src/config/config.hpp index 97fed3c..42eb8a4 100644 --- a/src/config/config.h +++ b/src/config/config.hpp @@ -1,19 +1,31 @@ #pragma once #ifdef _WIN32 -#include // GetUserNameA + #include // GetUserNameA #else -#include // getpwuid -#include // getuid + #include // getpwuid, passwd + #include // getuid #endif -#include +#include // std::runtime_error +#include // std::string (String) +#include // toml::node +#include // toml::node_view +#include // toml::table +#include // std::variant -#include "src/util/macros.h" -#include "weather.h" +#include "src/core/util/defs.hpp" +#include "src/core/util/error.hpp" +#include "src/core/util/helpers.hpp" +#include "src/core/util/types.hpp" + +#include "weather.hpp" + +using util::error::DraconisError; +using util::types::String, util::types::Array, util::types::Option, util::types::Result; /// Alias for the location type used in Weather config, can be a city name (String) or coordinates (Coords). -using Location = std::variant; +using Location = std::variant; /** * @struct General @@ -43,11 +55,11 @@ struct General { return pwd->pw_name; // Try to get the username using environment variables - if (Result envUser = GetEnv("USER")) + if (Result envUser = util::helpers::GetEnv("USER")) return *envUser; // Finally, try to get the username using LOGNAME - if (Result envLogname = GetEnv("LOGNAME")) + if (Result envLogname = util::helpers::GetEnv("LOGNAME")) return *envLogname; // If all else fails, return a default name @@ -116,7 +128,7 @@ struct Weather { if (location.is_string()) weather.location = *location.value(); else if (location.is_table()) - weather.location = Coords { + weather.location = weather::Coords { .lat = *location.as_table()->get("lat")->value(), .lon = *location.as_table()->get("lon")->value(), }; @@ -135,7 +147,7 @@ struct Weather { * API key, and units. It returns a WeatherOutput object containing the * retrieved weather data. */ - [[nodiscard]] fn getWeatherInfo() const -> WeatherOutput; + [[nodiscard]] fn getWeatherInfo() const -> weather::Output; }; /** diff --git a/src/config/weather.cpp b/src/config/weather.cpp index 452e546..a4a00bd 100644 --- a/src/config/weather.cpp +++ b/src/config/weather.cpp @@ -1,18 +1,37 @@ -#include -#include -#include -#include +#include "weather.hpp" -#include "weather.h" +#include // std::chrono::{duration, operator-} +#include // curl_easy_init, curl_easy_setopt, curl_easy_perform, curl_easy_cleanup +#include // std::{expected (Result), unexpected (Err)} +#include // std::filesystem::{path, remove, rename} +#include // std::format +#include // std::{ifstream, ofstream} +#include // glz::{error_ctx, error_code} +#include // glz::check_partial_read +#include // glz::read +#include // glz::format_error +#include // glz::write_json +#include // glz::atoi +#include // std::istreambuf_iterator +#include // std::error_code +#include // std::move +#include // std::{get, holds_alternative} -#include "config.h" -#include "src/util/macros.h" +#include "src/core/util/defs.hpp" +#include "src/core/util/logging.hpp" + +#include "config.hpp" namespace fs = std::filesystem; -using namespace std::string_literals; + +using namespace weather; + +using util::types::i32, util::types::Err, util::types::Exception; namespace { - constexpr glz::opts glaze_opts = { .error_on_unknown_keys = false }; + using glz::opts, glz::error_ctx, glz::error_code, glz::write_json, glz::read, glz::format_error; + + constexpr opts glaze_opts = { .error_on_unknown_keys = false }; fn GetCachePath() -> Result { std::error_code errc; @@ -25,7 +44,7 @@ namespace { return cachePath; } - fn ReadCacheFromFile() -> Result { + fn ReadCacheFromFile() -> Result { Result cachePath = GetCachePath(); if (!cachePath) @@ -36,27 +55,27 @@ namespace { if (!ifs.is_open()) return Err("Cache file not found: " + cachePath->string()); - DEBUG_LOG("Reading from cache file..."); + debug_log("Reading from cache file..."); try { - const String content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - WeatherOutput result; + const String content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + Output result; - if (const glz::error_ctx errc = glz::read(result, content); errc.ec != glz::error_code::none) - return Err("JSON parse error: " + glz::format_error(errc, content)); + if (const error_ctx errc = read(result, content); errc.ec != error_code::none) + return Err(std::format("JSON parse error: {}", format_error(errc, content))); - DEBUG_LOG("Successfully read from cache file."); + debug_log("Successfully read from cache file."); return result; - } catch (const std::exception& e) { return Err("Error reading cache: "s + e.what()); } + } catch (const Exception& e) { return Err(std::format("Error reading cache: {}", e.what())); } } - fn WriteCacheToFile(const WeatherOutput& data) -> Result { + fn WriteCacheToFile(const Output& data) -> Result { Result cachePath = GetCachePath(); if (!cachePath) return Err(cachePath.error()); - DEBUG_LOG("Writing to cache file..."); + debug_log("Writing to cache file..."); fs::path tempPath = *cachePath; tempPath += ".tmp"; @@ -68,8 +87,8 @@ namespace { String jsonStr; - if (const glz::error_ctx errc = glz::write_json(data, jsonStr); errc.ec != glz::error_code::none) - return Err("JSON serialization error: " + glz::format_error(errc, jsonStr)); + if (const error_ctx errc = write_json(data, jsonStr); errc.ec != error_code::none) + return Err("JSON serialization error: " + format_error(errc, jsonStr)); ofs << jsonStr; if (!ofs) @@ -80,24 +99,24 @@ namespace { fs::rename(tempPath, *cachePath, errc); if (errc) { if (!fs::remove(tempPath, errc)) - DEBUG_LOG("Failed to remove temp file: {}", errc.message()); + debug_log("Failed to remove temp file: {}", errc.message()); - return Err("Failed to replace cache file: " + errc.message()); + return Err(std::format("Failed to replace cache file: {}", errc.message())); } - DEBUG_LOG("Successfully wrote to cache file."); + debug_log("Successfully wrote to cache file."); return {}; - } catch (const std::exception& e) { return Err("File operation error: "s + e.what()); } + } catch (const Exception& e) { return Err(std::format("File operation error: {}", e.what())); } } - fn WriteCallback(void* contents, const size_t size, const size_t nmemb, String* str) -> size_t { - const size_t totalSize = size * nmemb; + fn WriteCallback(void* contents, const usize size, const usize nmemb, String* str) -> usize { + const usize totalSize = size * nmemb; str->append(static_cast(contents), totalSize); return totalSize; } - fn MakeApiRequest(const String& url) -> Result { - DEBUG_LOG("Making API request to URL: {}", url); + fn MakeApiRequest(const String& url) -> Result { + debug_log("Making API request to URL: {}", url); CURL* curl = curl_easy_init(); String responseBuffer; @@ -116,40 +135,40 @@ namespace { if (res != CURLE_OK) return Err(std::format("cURL error: {}", curl_easy_strerror(res))); - WeatherOutput output; + Output output; - if (const glz::error_ctx errc = glz::read(output, responseBuffer); errc.ec != glz::error_code::none) - return Err("API response parse error: " + glz::format_error(errc, responseBuffer)); + if (const error_ctx errc = glz::read(output, responseBuffer); errc.ec != error_code::none) + return Err("API response parse error: " + format_error(errc, responseBuffer)); return std::move(output); } -} +} // namespace -fn Weather::getWeatherInfo() const -> WeatherOutput { +fn Weather::getWeatherInfo() const -> Output { using namespace std::chrono; - if (Result data = ReadCacheFromFile()) { - const WeatherOutput& dataVal = *data; + if (Result data = ReadCacheFromFile()) { + const Output& dataVal = *data; if (const duration cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt)); cacheAge < 10min) { - DEBUG_LOG("Using valid cache"); + debug_log("Using valid cache"); return dataVal; } - DEBUG_LOG("Cache expired"); + debug_log("Cache expired"); } else { - DEBUG_LOG("Cache error: {}", data.error()); + debug_log("Cache error: {}", data.error()); } - fn handleApiResult = [](const Result& result) -> WeatherOutput { + fn handleApiResult = [](const Result& result) -> Output { if (!result) { - ERROR_LOG("API request failed: {}", result.error()); - return WeatherOutput {}; + error_log("API request failed: {}", result.error()); + return Output {}; } if (Result writeResult = WriteCacheToFile(*result); !writeResult) - ERROR_LOG("Failed to write cache: {}", writeResult.error()); + error_log("Failed to write cache: {}", writeResult.error()); return *result; }; @@ -157,7 +176,7 @@ fn Weather::getWeatherInfo() const -> WeatherOutput { if (std::holds_alternative(location)) { const auto& city = std::get(location); char* escaped = curl_easy_escape(nullptr, city.c_str(), static_cast(city.length())); - DEBUG_LOG("Requesting city: {}", escaped); + debug_log("Requesting city: {}", escaped); const String apiUrl = std::format("https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units={}", escaped, api_key, units); @@ -167,7 +186,7 @@ fn Weather::getWeatherInfo() const -> WeatherOutput { } const auto& [lat, lon] = std::get(location); - DEBUG_LOG("Requesting coordinates: lat={:.3f}, lon={:.3f}", lat, lon); + debug_log("Requesting coordinates: lat={:.3f}, lon={:.3f}", lat, lon); const String apiUrl = std::format( "https://api.openweathermap.org/data/2.5/weather?lat={:.3f}&lon={:.3f}&appid={}&units={}", lat, lon, api_key, units diff --git a/src/config/weather.h b/src/config/weather.h deleted file mode 100644 index 0276fe1..0000000 --- a/src/config/weather.h +++ /dev/null @@ -1,73 +0,0 @@ -#pragma once - -#include - -#include "../util/types.h" - -// NOLINTBEGIN(readability-identifier-naming) - Needs to specifically use `glaze` -/** - * @struct Condition - * @brief Represents weather conditions. - */ -struct Condition { - String description; ///< Weather condition description (e.g., "clear sky", "light rain"). - - /** - * @brief Glaze serialization and deserialization for Condition. - */ - struct [[maybe_unused]] glaze { - using T = Condition; - - static constexpr glz::detail::Object value = glz::object("description", &T::description); - }; -}; - -/** - * @struct Main - * @brief Represents the main weather data. - */ -struct Main { - f64 temp; ///< Temperature in degrees (C/F, depending on config). - - /** - * @brief Glaze serialization and deserialization for Main. - */ - struct [[maybe_unused]] glaze { - using T = Main; - - static constexpr glz::detail::Object value = glz::object("temp", &T::temp); - }; -}; - -/** - * @struct Coords - * @brief Represents geographical coordinates. - */ -struct Coords { - double lat; ///< Latitude coordinate. - double lon; ///< Longitude coordinate. -}; - -/** - * @struct WeatherOutput - * @brief Represents the output of the weather API. - * - * Contains main weather data, location name, and weather conditions. - */ -struct WeatherOutput { - Main main; ///< Main weather data (temperature, etc.). - String name; ///< Location name (e.g., city name). - Vec weather; ///< List of weather conditions (e.g., clear, rain). - usize dt; ///< Timestamp of the weather data (in seconds since epoch). - - /** - * @brief Glaze serialization and deserialization for WeatherOutput. - */ - struct [[maybe_unused]] glaze { - using T = WeatherOutput; - - static constexpr glz::detail::Object value = - glz::object("main", &T::main, "name", &T::name, "weather", &T::weather, "dt", &T::dt); - }; -}; -// NOLINTEND(readability-identifier-naming) diff --git a/src/config/weather.hpp b/src/config/weather.hpp new file mode 100644 index 0000000..935d1e2 --- /dev/null +++ b/src/config/weather.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include // object +#include // Object + +#include "src/core/util/types.hpp" + +namespace weather { + using glz::detail::Object, glz::object; + using util::types::String, util::types::Vec, util::types::f64, util::types::usize; + + // NOLINTBEGIN(readability-identifier-naming) - Needs to specifically use `glaze` + /** + * @struct Condition + * @brief Represents weather conditions. + */ + struct Condition { + String description; ///< Weather condition description (e.g., "clear sky", "light rain"). + + /** + * @brief Glaze serialization and deserialization for Condition. + */ + struct [[maybe_unused]] glaze { + using T = Condition; + + static constexpr Object value = object("description", &T::description); + }; + }; + + /** + * @struct Main + * @brief Represents the main weather data. + */ + struct Main { + f64 temp; ///< Temperature in degrees (C/F, depending on config). + + /** + * @brief Glaze serialization and deserialization for Main. + */ + struct [[maybe_unused]] glaze { + using T = Main; + + static constexpr Object value = object("temp", &T::temp); + }; + }; + + /** + * @struct Coords + * @brief Represents geographical coordinates. + */ + struct Coords { + double lat; ///< Latitude coordinate. + double lon; ///< Longitude coordinate. + }; + + /** + * @struct Output + * @brief Represents the output of the weather API. + * + * Contains main weather data, location name, and weather conditions. + */ + struct Output { + Main main; ///< Main weather data (temperature, etc.). + String name; ///< Location name (e.g., city name). + Vec weather; ///< List of weather conditions (e.g., clear, rain). + usize dt; ///< Timestamp of the weather data (in seconds since epoch). + + /** + * @brief Glaze serialization and deserialization for WeatherOutput. + */ + struct [[maybe_unused]] glaze { + using T = Output; + + static constexpr Object value = object("main", &T::main, "name", &T::name, "weather", &T::weather, "dt", &T::dt); + }; + }; + // NOLINTEND(readability-identifier-naming) +} // namespace weather diff --git a/src/core/system_data.cpp b/src/core/system_data.cpp index 87766a5..f61b69e 100644 --- a/src/core/system_data.cpp +++ b/src/core/system_data.cpp @@ -1,15 +1,17 @@ -#include "system_data.h" +#include "system_data.hpp" -#include // for year_month_day, floor, days... -#include // for exception -#include // for future, async, launch -#include // for locale -#include // for runtime_error -#include // for tuple, get, make_tuple -#include // for move +#include // std::chrono::{year_month_day, floor, days, system_clock} +#include // std::exception (Exception) +#include // std::{future, async, launch} +#include // std::locale +#include // std::runtime_error +#include // std::{tuple, get, make_tuple} +#include // std::move -#include "src/config/config.h" // for Config, Weather, NowPlaying -#include "src/os/os.h" // for GetDesktopEnvironment, GetHost... +#include "src/config/config.hpp" +#include "src/os/os.hpp" + +#include "util/logging.hpp" namespace { fn GetDate() -> String { @@ -20,13 +22,13 @@ namespace { try { return std::format(std::locale(""), "{:%B %d}", ymd); } catch (const std::runtime_error& e) { - WARN_LOG("Could not retrieve or use system locale ({}). Falling back to default C locale.", e.what()); + warn_log("Could not retrieve or use system locale ({}). Falling back to default C locale.", e.what()); return std::format(std::locale::classic(), "{:%B %d}", ymd); } } } // namespace -SystemData SystemData::fetchSystemData(const Config& config) { +fn SystemData::fetchSystemData(const Config& config) -> SystemData { SystemData data { .date = GetDate(), .host = os::GetHost(), @@ -42,13 +44,13 @@ SystemData SystemData::fetchSystemData(const Config& config) { }; auto diskShellFuture = std::async(std::launch::async, [] { - Result diskResult = os::GetDiskUsage(); - Option shellOption = os::GetShell(); + Result diskResult = os::GetDiskUsage(); + Option shellOption = os::GetShell(); return std::make_tuple(std::move(diskResult), std::move(shellOption)); }); - std::future weatherFuture; - std::future> nowPlayingFuture; + std::future weatherFuture; + std::future> nowPlayingFuture; if (config.weather.enabled) weatherFuture = std::async(std::launch::async, [&config] { return config.weather.getWeatherInfo(); }); @@ -65,7 +67,7 @@ SystemData SystemData::fetchSystemData(const Config& config) { try { data.weather_info = weatherFuture.get(); } catch (const std::exception& e) { - ERROR_LOG("Failed to get weather info: {}", e.what()); + error_log("Failed to get weather info: {}", e.what()); data.weather_info = None; } diff --git a/src/core/system_data.h b/src/core/system_data.h deleted file mode 100644 index 5ce2c0e..0000000 --- a/src/core/system_data.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include // for formatter, format_to -#include // for basic_string -#include // for formatter - -#include "src/config/weather.h" // for WeatherOutput -#include "src/util/macros.h" // for fn -#include "src/util/types.h" // for OsError, DiskSpace, Result, String - -struct Config; - -/** - * @struct BytesToGiB - * @brief Helper struct to format a byte value to GiB (Gibibytes). - * - * Encapsulates a byte value and provides a custom formatter - * to convert it to GiB for display purposes. - */ -struct BytesToGiB { - u64 value; ///< The byte value to be converted. -}; - -/// @brief Conversion factor from bytes to GiB -constexpr u64 GIB = 1'073'741'824; - -/** - * @brief Custom formatter for BytesToGiB. - * - * Allows formatting BytesToGiB values using std::format. - * Outputs the value in GiB with two decimal places. - * - * @code{.cpp} - * #include - * #include "system_data.h" // Assuming BytesToGiB is defined here - * - * i32 main() { - * BytesToGiB data_size{2'147'483'648}; // 2 GiB - * String formatted = std::format("Size: {}", data_size); - * std::println("{}", formatted); // formatted will be "Size: 2.00GiB" - * return 0; - * } - * @endcode - */ -template <> -struct std::formatter : std::formatter { - /** - * @brief Formats the BytesToGiB value. - * @param BTG The BytesToGiB instance to format. - * @param ctx The formatting context. - * @return An iterator to the end of the formatted output. - */ - fn format(const BytesToGiB& BTG, auto& ctx) const { - return std::format_to(ctx.out(), "{:.2f}GiB", static_cast(BTG.value) / GIB); - } -}; - -/** - * @struct SystemData - * @brief Holds various pieces of system information collected from the OS. - * - * This structure aggregates information about the system, - * in order to display it at all at once during runtime. - */ -struct SystemData { - using NowPlayingResult = Option>; - - // clang-format off - String date; ///< Current date (e.g., "April 26th"). Always expected to succeed. - Result host; ///< Host/product family (e.g., "MacBookPro18,3") or OS error. - Result kernel_version; ///< OS kernel version (e.g., "23.4.0") or OS error. - Result os_version; ///< OS pretty name (e.g., "macOS Sonoma 14.4.1") or OS error. - Result mem_info; ///< Total physical RAM in bytes or OS error. - Option desktop_environment; ///< Detected desktop environment (e.g., "Aqua", "Plasma"). None if not detected/applicable. - Option window_manager; ///< Detected window manager (e.g., "Quartz Compositor", "KWin"). None if not detected/applicable. - Result disk_usage; ///< Used/Total disk space for root filesystem or OS error. - Option shell; ///< Name of the current user shell (e.g., "zsh"). None if not detected. - NowPlayingResult now_playing; ///< Optional: Result of fetching media info (MediaInfo on success, NowPlayingError on failure). None if disabled. - Option weather_info; ///< Optional: Weather information. None if disabled or error during fetch. - // clang-format on - - /** - * @brief Fetches all system data asynchronously. - * @param config The application configuration. - * @return A populated SystemData object. - */ - static fn fetchSystemData(const Config& config) -> SystemData; -}; diff --git a/src/core/system_data.hpp b/src/core/system_data.hpp new file mode 100644 index 0000000..eb72abb --- /dev/null +++ b/src/core/system_data.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include // std::{formatter, format_to} + +#include "src/config/weather.hpp" // weather::Output + +#include "util/defs.hpp" +#include "util/error.hpp" +#include "util/types.hpp" + +struct Config; + +using util::types::u64, util::types::f64, util::types::String, util::types::Option, util::types::Result, + util::types::MediaInfo, util::types::DiskSpace; + +/** + * @struct BytesToGiB + * @brief Helper struct to format a byte value to GiB (Gibibytes). + * + * Encapsulates a byte value and provides a custom formatter + * to convert it to GiB for display purposes. + */ +struct BytesToGiB { + u64 value; ///< The byte value to be converted. +}; + +/// @brief Conversion factor from bytes to GiB +constexpr u64 GIB = 1'073'741'824; + +/** + * @brief Custom formatter for BytesToGiB. + * + * Allows formatting BytesToGiB values using std::format. + * Outputs the value in GiB with two decimal places. + * + * @code{.cpp} + * #include + * #include "system_data.h" // Assuming BytesToGiB is defined here + * + * i32 main() { + * BytesToGiB data_size{2'147'483'648}; // 2 GiB + * String formatted = std::format("Size: {}", data_size); + * std::println("{}", formatted); // formatted will be "Size: 2.00GiB" + * return 0; + * } + * @endcode + */ +template <> +struct std::formatter : std::formatter { + /** + * @brief Formats the BytesToGiB value. + * @param BTG The BytesToGiB instance to format. + * @param ctx The formatting context. + * @return An iterator to the end of the formatted output. + */ + fn format(const BytesToGiB& BTG, auto& ctx) const { + return std::format_to(ctx.out(), "{:.2f}GiB", static_cast(BTG.value) / GIB); + } +}; + +/** + * @struct SystemData + * @brief Holds various pieces of system information collected from the OS. + * + * This structure aggregates information about the system, + * in order to display it at all at once during runtime. + */ +struct SystemData { + using NowPlayingResult = Option>; + + // clang-format off + String date; ///< Current date (e.g., "April 26th"). Always expected to succeed. + Result host; ///< Host/product family (e.g., "MacBookPro18,3") or OS util::erroror. + Result kernel_version; ///< OS kernel version (e.g., "23.4.0") or OS error. + Result os_version; ///< OS pretty name (e.g., "macOS Sonoma 14.4.1") or OS error. + Result mem_info; ///< Total physical RAM in bytes or OS error. + Option desktop_environment; ///< Detected desktop environment (e.g., "Aqua", "Plasma"). None if not detected/applicable. + Option window_manager; ///< Detected window manager (e.g., "Quartz Compositor", "KWin"). None if not detected/applicable. + Result disk_usage; ///< Used/Total disk space for root filesystem or OS error. + Option shell; ///< Name of the current user shell (e.g., "zsh"). None if not detected. + NowPlayingResult now_playing; ///< Optional: Result of fetching media info (MediaInfo on success, NowPlayingError on failure). None if disabled. + Option weather_info; ///< Optional: Weather information. None if disabled or util::erroror during fetch. + // clang-format on + + /** + * @brief Fetches all system data asynchronously. + * @param config The application configuration. + * @return A populated SystemData object. + */ + static fn fetchSystemData(const Config& config) -> SystemData; +}; diff --git a/src/core/util/defs.hpp b/src/core/util/defs.hpp new file mode 100644 index 0000000..fa8e7d1 --- /dev/null +++ b/src/core/util/defs.hpp @@ -0,0 +1,12 @@ +#pragma once + +// Fixes conflict in Windows with +#ifdef _WIN32 + #undef ERROR +#endif // _WIN32 + +/// Macro alias for trailing return type functions. +#define fn auto + +/// Macro alias for std::nullopt, represents an empty optional value. +#define None std::nullopt diff --git a/src/core/util/error.hpp b/src/core/util/error.hpp new file mode 100644 index 0000000..b0ed098 --- /dev/null +++ b/src/core/util/error.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include // std::format +#include // std::source_location +#include // std::string_view (StringView) +#include // std::error_code + +#ifdef _WIN32 + #include // winrt::hresult_error +#elifdef __linux__ + #include // DBus::Error +#endif + +#include "src/core/util/types.hpp" + +namespace util::error { + using types::u8, types::i32, types::String, types::StringView, types::Exception; + + /** + * @enum DraconisErrorCode + * @brief Error codes for general OS-level operations. + */ + enum class DraconisErrorCode : u8 { + IoError, ///< General I/O error (filesystem, pipes, etc.). + PermissionDenied, ///< Insufficient permissions to perform the operation. + NotFound, ///< A required resource (file, registry key, device, API endpoint) was not found. + ParseError, ///< Failed to parse data obtained from the OS (e.g., file content, API output). + ApiUnavailable, ///< A required OS service/API is unavailable or failed unexpectedly at runtime. + NotSupported, ///< The requested operation is not supported on this platform, version, or configuration. + Timeout, ///< An operation timed out (e.g., waiting for IPC reply). + BufferTooSmall, ///< Optional: Keep if using fixed C-style buffers, otherwise remove. + InternalError, ///< An error occurred within the application's OS abstraction code logic. + NetworkError, ///< A network-related error occurred (e.g., DNS resolution, connection failure). + PlatformSpecific, ///< An unmapped error specific to the underlying OS platform occurred (check message). + Other, ///< A generic or unclassified error originating from the OS or an external library. + }; + + /** + * @struct DraconisError + * @brief Holds structured information about an OS-level error. + * + * Used as the error type in Result for many os:: functions. + */ + struct DraconisError { + // ReSharper disable CppDFANotInitializedField + String message; ///< A descriptive error message, potentially including platform details. + DraconisErrorCode code; ///< The general category of the error. + std::source_location location; ///< The source location where the error occurred (file, line, function). + // ReSharper restore CppDFANotInitializedField + + DraconisError( + const DraconisErrorCode errc, + String msg, + const std::source_location& loc = std::source_location::current() + ) + : message(std::move(msg)), code(errc), location(loc) {} + + explicit DraconisError(const Exception& exc, const std::source_location& loc = std::source_location::current()) + : message(exc.what()), code(DraconisErrorCode::InternalError), location(loc) {} + + explicit DraconisError( + const std::error_code& errc, + const std::source_location& loc = std::source_location::current() + ) + : message(errc.message()), location(loc) { + using enum DraconisErrorCode; + using enum std::errc; + + switch (static_cast(errc.value())) { + case permission_denied: code = PermissionDenied; break; + case no_such_file_or_directory: code = NotFound; break; + case timed_out: code = Timeout; break; + case io_error: code = IoError; break; + case network_unreachable: + case network_down: + case connection_refused: code = NetworkError; break; + case not_supported: code = NotSupported; break; + default: code = errc.category() == std::generic_category() ? InternalError : PlatformSpecific; break; + } + } +#ifdef _WIN32 + explicit OsError(const winrt::hresult_error& e) : message(winrt::to_string(e.message())) { + switch (e.code()) { + case E_ACCESSDENIED: code = OsErrorCode::PermissionDenied; break; + case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND): + case HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND): + case HRESULT_FROM_WIN32(ERROR_SERVICE_NOT_FOUND): code = OsErrorCode::NotFound; break; + case HRESULT_FROM_WIN32(ERROR_TIMEOUT): + case HRESULT_FROM_WIN32(ERROR_SEM_TIMEOUT): code = OsErrorCode::Timeout; break; + case HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED): code = OsErrorCode::NotSupported; break; + default: code = OsErrorCode::PlatformSpecific; break; + } + } +#else + DraconisError(const DraconisErrorCode code_hint, const int errno_val) + : message(std::system_category().message(errno_val)), code(code_hint) { + using enum DraconisErrorCode; + + switch (errno_val) { + case EACCES: code = PermissionDenied; break; + case ENOENT: code = NotFound; break; + case ETIMEDOUT: code = Timeout; break; + case ENOTSUP: code = NotSupported; break; + default: code = PlatformSpecific; break; + } + } + + static auto withErrno(const String& context, const std::source_location& loc = std::source_location::current()) + -> DraconisError { + const i32 errNo = errno; + const String msg = std::system_category().message(errNo); + const String fullMsg = std::format("{}: {}", context, msg); + + DraconisErrorCode code = DraconisErrorCode::PlatformSpecific; + switch (errNo) { + case EACCES: + case EPERM: code = DraconisErrorCode::PermissionDenied; break; + case ENOENT: code = DraconisErrorCode::NotFound; break; + case ETIMEDOUT: code = DraconisErrorCode::Timeout; break; + case ENOTSUP: code = DraconisErrorCode::NotSupported; break; + case EIO: code = DraconisErrorCode::IoError; break; + case ECONNREFUSED: + case ENETDOWN: + case ENETUNREACH: code = DraconisErrorCode::NetworkError; break; + default: code = DraconisErrorCode::PlatformSpecific; break; + } + + return DraconisError { code, fullMsg, loc }; + } + + #ifdef __linux__ + static auto fromDBus(const DBus::Error& err, const std::source_location& loc = std::source_location::current()) + -> DraconisError { + String name = err.name(); + DraconisErrorCode codeHint = DraconisErrorCode::PlatformSpecific; + String message; + + using namespace std::string_view_literals; + + if (name == "org.freedesktop.DBus.Error.ServiceUnknown"sv || + name == "org.freedesktop.DBus.Error.NameHasNoOwner"sv) { + codeHint = DraconisErrorCode::NotFound; + message = std::format("DBus service/name not found: {}", err.message()); + } else if (name == "org.freedesktop.DBus.Error.NoReply"sv || name == "org.freedesktop.DBus.Error.Timeout"sv) { + codeHint = DraconisErrorCode::Timeout; + message = std::format("DBus timeout/no reply: {}", err.message()); + } else if (name == "org.freedesktop.DBus.Error.AccessDenied"sv) { + codeHint = DraconisErrorCode::PermissionDenied; + message = std::format("DBus access denied: {}", err.message()); + } else { + message = std::format("DBus error: {} - {}", name, err.message()); + } + + return DraconisError { codeHint, message, loc }; + } + #endif +#endif + }; +} // namespace util::error diff --git a/src/core/util/helpers.hpp b/src/core/util/helpers.hpp new file mode 100644 index 0000000..0a14942 --- /dev/null +++ b/src/core/util/helpers.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "defs.hpp" +#include "error.hpp" +#include "types.hpp" + +namespace util::helpers { + using types::Result, types::String, types::CStr, types::UniquePointer, types::Err; + + /** + * @brief Safely retrieves an environment variable. + * @param name The name of the environment variable to retrieve. + * @return A Result containing the value of the environment variable as a String, + * or an EnvError if an error occurred. + */ + [[nodiscard]] inline fn GetEnv(CStr name) -> Result { +#ifdef _WIN32 + char* rawPtr = nullptr; + usize bufferSize = 0; + + // Use _dupenv_s to safely retrieve environment variables on Windows + const i32 err = _dupenv_s(&rawPtr, &bufferSize, name); + + const UniquePointer ptrManager(rawPtr, free); + + if (err != 0) + return Err(DraconisError(DraconisErrorCode::PermissionDenied, "Failed to retrieve environment variable")); + + if (!ptrManager) + return Err(DraconisError(DraconisErrorCode::NotFound, "Environment variable not found")); + + return ptrManager.get(); +#else + // Use std::getenv to retrieve environment variables on POSIX systems + const CStr value = std::getenv(name); + + if (!value) + return Err(error::DraconisError(error::DraconisErrorCode::NotFound, "Environment variable not found")); + + return value; +#endif + } +} // namespace util::helpers diff --git a/src/core/util/logging.hpp b/src/core/util/logging.hpp new file mode 100644 index 0000000..aaef273 --- /dev/null +++ b/src/core/util/logging.hpp @@ -0,0 +1,286 @@ +#pragma once + +#include // std::chrono::{days, floor, seconds, system_clock} +#include // std::filesystem::path +#include // std::format +#include // std::print +#include // std::source_location +#include // std::{forward, make_pair} + +#include "src/core/util/defs.hpp" +#include "src/core/util/error.hpp" +#include "src/core/util/types.hpp" + +namespace util::logging { + using types::u8, types::i32, types::String, types::StringView, types::Option; + + /** + * @namespace term + * @brief Provides terminal-related utilities, including color and style formatting. + */ + namespace term { + /** + * @enum Emphasis + * @brief Represents text emphasis styles. + * + * Enum values can be combined using bitwise OR to apply multiple styles at once. + */ + enum class Emphasis : u8 { + Bold, ///< Bold text. + Italic ///< Italic text. + }; + + /** + * @enum Color + * @brief Represents ANSI color codes for terminal output. + * + * Color codes can be used to format terminal output. + */ + enum class Color : u8 { + Black = 30, ///< Black color. + Red = 31, ///< Red color. + Green = 32, ///< Green color. + Yellow = 33, ///< Yellow color. + Blue = 34, ///< Blue color. + Magenta = 35, ///< Magenta color. + Cyan = 36, ///< Cyan color. + White = 37, ///< White color. + BrightBlack = 90, ///< Bright black (gray) color. + BrightRed = 91, ///< Bright red color. + BrightGreen = 92, ///< Bright green color. + BrightYellow = 93, ///< Bright yellow color. + BrightBlue = 94, ///< Bright blue color. + BrightMagenta = 95, ///< Bright magenta color. + BrightCyan = 96, ///< Bright cyan color. + BrightWhite = 97, ///< Bright white color. + }; + + /** + * @brief Combines two emphasis styles using bitwise OR. + * @param emphA The first emphasis style. + * @param emphB The second emphasis style. + * @return The combined emphasis style. + */ + constexpr fn operator|(Emphasis emphA, Emphasis emphB)->Emphasis { + return static_cast(static_cast(emphA) | static_cast(emphB)); + } + + /** + * @brief Checks if two emphasis styles are equal using bitwise AND. + * @param emphA The first emphasis style. + * @param emphB The second emphasis style. + * @return The result of the bitwise AND operation. + */ + constexpr fn operator&(Emphasis emphA, Emphasis emphB)->u8 { + return static_cast(emphA) & static_cast(emphB); + } + + /** + * @struct Style + * @brief Represents a combination of text styles. + * + * Emphasis and color are both optional, allowing for flexible styling. + */ + struct Style { + Option emph; ///< Optional emphasis style. + Option fg_col; ///< Optional foreground color style. + + /** + * @brief Generates the ANSI escape code for the combined styles. + * @return The ANSI escape code for the combined styles. + */ + [[nodiscard]] fn ansiCode() const -> String { + String result; + + if (emph) { + if ((*emph & Emphasis::Bold) != 0) + result += "\033[1m"; + if ((*emph & Emphasis::Italic) != 0) + result += "\033[3m"; + } + + if (fg_col) + result += std::format("\033[{}m", static_cast(*fg_col)); + + return result; + } + }; + + /** + * @brief Combines an emphasis style and a foreground color into a Style. + * @param emph The emphasis style to apply. + * @param fgColor The foreground color to apply. + * @return The combined style. + */ + // ReSharper disable CppDFAConstantParameter + constexpr fn operator|(const Emphasis emph, const Color fgColor)->Style { + return { .emph = emph, .fg_col = fgColor }; + } + // ReSharper restore CppDFAConstantParameter + + /** + * @brief Combines a foreground color and an emphasis style into a Style. + * @param fgColor The foreground color to apply. + * @param emph The emphasis style to apply. + * @return The combined style. + */ + constexpr fn operator|(const Color fgColor, const Emphasis emph)->Style { + return { .emph = emph, .fg_col = fgColor }; + } + + /** + * @brief Prints formatted text with the specified style. + * @tparam Args Parameter pack for format arguments. + * @param style The Style object containing emphasis and/or color. + * @param fmt The format string. + * @param args The arguments for the format string. + */ + template + fn Print(const Style& style, std::format_string fmt, Args&&... args) -> void { + if (const String styleCode = style.ansiCode(); styleCode.empty()) + std::print(fmt, std::forward(args)...); + else + std::print("{}{}{}", styleCode, std::format(fmt, std::forward(args)...), "\033[0m"); + } + + /** + * @brief Prints formatted text with the specified foreground color. + * @tparam Args Parameter pack for format arguments. + * @param fgColor The foreground color to apply. + * @param fmt The format string. + * @param args The arguments for the format string. + */ + template + fn Print(const Color& fgColor, std::format_string fmt, Args&&... args) -> void { + Print({ .emph = None, .fg_col = fgColor }, fmt, std::forward(args)...); + } + + /** + * @brief Prints formatted text with the specified emphasis style. + * @tparam Args Parameter pack for format arguments. + * @param emph The emphasis style to apply. + * @param fmt The format string. + * @param args The arguments for the format string. + */ + template + fn Print(const Emphasis emph, std::format_string fmt, Args&&... args) -> void { + Print({ .emph = emph, .fg_col = None }, fmt, std::forward(args)...); + } + + /** + * @brief Prints formatted text with no specific style (default terminal style). + * @tparam Args Parameter pack for format arguments. + * @param fmt The format string. + * @param args The arguments for the format string. + */ + template + fn Print(std::format_string fmt, Args&&... args) -> void { + // Directly use std::print for unstyled output + std::print(fmt, std::forward(args)...); + } + } // namespace term + + /** + * @enum LogLevel + * @brief Represents different log levels. + */ + enum class LogLevel : u8 { DEBUG, INFO, WARN, ERROR }; + + /** + * @brief Logs a message with the specified log level, source location, and format string. + * @tparam Args Parameter pack for format arguments. + * @param level The log level (DEBUG, INFO, WARN, ERROR). + * @param loc The source location of the log message. + * @param fmt The format string. + * @param args The arguments for the format string. + */ + template + fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_string fmt, Args&&... args) { + using namespace std::chrono; + using namespace term; + +#ifdef _MSC_VER + using enum term::Color; +#else + using enum Color; +#endif // _MSC_VER + + const auto [color, levelStr] = [&] { + switch (level) { + case LogLevel::DEBUG: return std::make_pair(Cyan, "DEBUG"); + case LogLevel::INFO: return std::make_pair(Green, "INFO "); + case LogLevel::WARN: return std::make_pair(Yellow, "WARN "); + case LogLevel::ERROR: return std::make_pair(Red, "ERROR"); + default: std::unreachable(); + } + }(); + + Print(BrightWhite, "[{:%X}] ", std::chrono::floor(system_clock::now())); + Print(Emphasis::Bold | color, "{} ", levelStr); + Print(fmt, std::forward(args)...); + +#ifndef NDEBUG + Print(BrightWhite, "\n{:>14} ", "╰──"); + Print( + Emphasis::Italic | BrightWhite, + "{}:{}", + std::filesystem::path(loc.file_name()).lexically_normal().string(), + loc.line() + ); +#endif // !NDEBUG + + Print("\n"); + } + + template + fn LogAppError(const LogLevel level, const ErrorType& error_obj) { + using DecayedErrorType = std::decay_t; + + std::source_location log_location; + String error_message_part; + + if constexpr (std::is_same_v) { + log_location = error_obj.location; + error_message_part = error_obj.message; + } else { + log_location = std::source_location::current(); + if constexpr (std::is_base_of_v) + error_message_part = error_obj.what(); + else if constexpr (requires { error_obj.message; }) + error_message_part = error_obj.message; + else + error_message_part = "Unknown error type logged"; + } + + LogImpl(level, log_location, "{}", error_message_part); + } + +#ifndef NDEBUG + #define debug_log(fmt, ...) \ + ::util::logging::LogImpl( \ + ::util::logging::LogLevel::DEBUG, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \ + ) + #define debug_at(error_obj) ::util::logging::LogAppError(::util::logging::LogLevel::DEBUG, error_obj); +#else + #define debug_log(...) ((void)0) + #define debug_at(...) ((void)0) +#endif + +#define info_log(fmt, ...) \ + ::util::logging::LogImpl( \ + ::util::logging::LogLevel::INFO, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \ + ) +#define info_at(error_obj) ::util::logging::LogAppError(::util::logging::LogLevel::INFO, error_obj); + +#define warn_log(fmt, ...) \ + ::util::logging::LogImpl( \ + ::util::logging::LogLevel::WARN, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \ + ) +#define warn_at(error_obj) ::util::logging::LogAppError(::util::logging::LogLevel::WARN, error_obj); + +#define error_log(fmt, ...) \ + ::util::logging::LogImpl( \ + ::util::logging::LogLevel::ERROR, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \ + ) +#define error_at(error_obj) ::util::logging::LogAppError(::util::logging::LogLevel::ERROR, error_obj); +} // namespace util::logging diff --git a/src/core/util/types.hpp b/src/core/util/types.hpp new file mode 100644 index 0000000..38f03a2 --- /dev/null +++ b/src/core/util/types.hpp @@ -0,0 +1,145 @@ +#pragma once + +#include // std::array (Array) +#include // std::expected (Result) +#include // std::map (Map) +#include // std::shared_ptr and std::unique_ptr (SharedPointer, UniquePointer) +#include // std::optional (Option) +#include // std::string (String, StringView) +#include // std::string_view (StringView) +#include // std::pair (Pair) +#include // std::vector (Vec) + +namespace util::types { + using u8 = std::uint8_t; ///< 8-bit unsigned integer. + using u16 = std::uint16_t; ///< 16-bit unsigned integer. + using u32 = std::uint32_t; ///< 32-bit unsigned integer. + using u64 = std::uint64_t; ///< 64-bit unsigned integer. + + using i8 = std::int8_t; ///< 8-bit signed integer. + using i16 = std::int16_t; ///< 16-bit signed integer. + using i32 = std::int32_t; ///< 32-bit signed integer. + using i64 = std::int64_t; ///< 64-bit signed integer. + + using f32 = float; ///< 32-bit floating-point number. + using f64 = double; ///< 64-bit floating-point number. + + using usize = std::size_t; ///< Unsigned size type (result of sizeof). + using isize = std::ptrdiff_t; ///< Signed size type (result of pointer subtraction). + + using String = std::string; ///< Owning, mutable string. + using StringView = std::string_view; ///< Non-owning view of a string. + using CStr = const char*; ///< Pointer to a null-terminated C-style string. + + using Exception = std::exception; ///< Standard exception type. + + /** + * @typedef Result + * @brief Alias for std::expected. Represents a value that can either be + * a success value of type Tp or an error value of type Er. + * @tparam Tp The type of the success value. + * @tparam Er The type of the error value. + */ + template + using Result = std::expected; + + /** + * @typedef Err + * @brief Alias for std::unexpected. Used to construct a Result in an error state. + * @tparam Er The type of the error value. + */ + template + using Err = std::unexpected; + + /** + * @typedef Option + * @brief Alias for std::optional. Represents a value that may or may not be present. + * @tparam Tp The type of the potential value. + */ + template + using Option = std::optional; + + /** + * @typedef Array + * @brief Alias for std::array. Represents a fixed-size array. + * @tparam Tp The element type. + * @tparam sz The size of the array. + */ + template + using Array = std::array; + + /** + * @typedef Vec + * @brief Alias for std::vector. Represents a dynamic-size array (vector). + * @tparam Tp The element type. + */ + template + using Vec = std::vector; + + /** + * @typedef Pair + * @brief Alias for std::pair. Represents a pair of values. + * @tparam T1 The type of the first element. + * @tparam T2 The type of the second element. + */ + template + using Pair = std::pair; + + /** + * @typedef Map + * @brief Alias for std::map. Represents an ordered map (dictionary). + * @tparam Key The key type. + * @tparam Val The value type. + */ + template + using Map = std::map; + + /** + * @typedef SharedPointer + * @brief Alias for std::shared_ptr. Manages shared ownership of a dynamically allocated object. + * @tparam Tp The type of the managed object. + */ + template + using SharedPointer = std::shared_ptr; + + /** + * @typedef UniquePointer + * @brief Alias for std::unique_ptr. Manages unique ownership of a dynamically allocated object. + * @tparam Tp The type of the managed object. + * @tparam Dp The deleter type (defaults to std::default_delete). + */ + template > + using UniquePointer = std::unique_ptr; + + /** + * @struct DiskSpace + * @brief Represents disk usage information. + * + * Used as the success type for os::GetDiskUsage. + */ + struct DiskSpace { + u64 used_bytes; ///< Currently used disk space in bytes. + u64 total_bytes; ///< Total disk space in bytes. + }; + + /** + * @struct MediaInfo + * @brief Holds structured metadata about currently playing media. + * + * Used as the success type for os::GetNowPlaying. + * Using Option<> for fields that might not always be available. + */ + struct MediaInfo { + Option title; ///< Track title. + Option artist; ///< Track artist(s). + Option album; ///< Album name. + Option app_name; ///< Name of the media player application (e.g., "Spotify", "Firefox"). + + MediaInfo() = default; + + MediaInfo(Option title, Option artist) : title(std::move(title)), artist(std::move(artist)) {} + + MediaInfo(Option title, Option artist, Option album, Option app) + : title(std::move(title)), artist(std::move(artist)), album(std::move(album)), app_name(std::move(app)) {} + }; +} // namespace util::types diff --git a/src/main.cpp b/src/main.cpp index 6397459..115b184 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,20 +1,28 @@ -#include -#include -#include -#include -#include -#include -#include +#include // std::lround +#include // std::format +#include // ftxui::{hbox, vbox, text, separator, filler} +#include // ftxui::{Element, Render} +#include // ftxui::Color +#include // ftxui::{Screen, Dimension::Full} +#include // std::optional (operator!=) +#include // std::ranges::{to, views} +#include // std::string (String) +#include // std::string_view (StringView) -#include "config/config.h" -#include "core/system_data.h" -#include "os/os.h" +#include "src/config/weather.hpp" + +#include "config/config.hpp" +#include "core/system_data.hpp" +#include "core/util/logging.hpp" +#include "os/os.hpp" namespace ui { using ftxui::Color; + using util::types::StringView, util::types::i32; - static constexpr inline bool SHOW_ICONS = true; - static constexpr i32 MAX_PARAGRAPH_LENGTH = 30; + static constexpr inline StringView ICON_TYPE = "EMOJI"; + + static constexpr i32 MAX_PARAGRAPH_LENGTH = 30; // Color themes struct Theme { @@ -47,7 +55,7 @@ namespace ui { StringView window_manager; }; - static constexpr Icons EMPTY_ICONS = { + static constexpr Icons NONE = { .user = "", .palette = "", .calendar = "", @@ -63,7 +71,7 @@ namespace ui { .window_manager = "", }; - static constexpr Icons NERD_ICONS = { + static constexpr Icons NERD = { .user = "  ", .palette = "  ", .calendar = "  ", @@ -78,9 +86,26 @@ namespace ui { .desktop = " 󰇄 ", .window_manager = "  ", }; + + static constexpr Icons EMOJI = { + .user = " 👤 ", + .palette = " 🎨 ", + .calendar = " 📅 ", + .host = " 💻 ", + .kernel = " 🫀 ", + .os = " 🤖 ", + .memory = " 🧠 ", + .weather = " 🌈 ", + .music = " 🎵 ", + .disk = " 💾 ", + .shell = " 💲 ", + .desktop = " 🖥 ️", + .window_manager = " 🪟 ", + }; } // namespace ui namespace { + using namespace util::logging; using namespace ftxui; fn CreateColorCircles() -> Element { @@ -97,7 +122,9 @@ namespace { const Weather weather = config.weather; const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, deIcon, wmIcon] = - ui::SHOW_ICONS ? ui::NERD_ICONS : ui::EMPTY_ICONS; + ui::ICON_TYPE == "NERD" ? ui::NERD + : ui::ICON_TYPE == "EMOJI" ? ui::EMOJI + : ui::NONE; Elements content; @@ -129,7 +156,7 @@ namespace { // Weather row if (weather.enabled && data.weather_info.has_value()) { - const WeatherOutput& weatherInfo = data.weather_info.value(); + const weather::Output& weatherInfo = data.weather_info.value(); if (weather.show_town_name) content.push_back(hbox( @@ -172,22 +199,22 @@ namespace { if (data.host) content.push_back(createRow(hostIcon, "Host", *data.host)); else - ERROR_LOG_LOC(data.host.error()); + error_at(data.host.error()); if (data.kernel_version) content.push_back(createRow(kernelIcon, "Kernel", *data.kernel_version)); else - ERROR_LOG_LOC(data.kernel_version.error()); + error_at(data.kernel_version.error()); if (data.os_version) content.push_back(createRow(String(osIcon), "OS", *data.os_version)); else - ERROR_LOG_LOC(data.os_version.error()); + error_at(data.os_version.error()); if (data.mem_info) content.push_back(createRow(memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.mem_info }))); else - ERROR_LOG_LOC(data.mem_info.error()); + error_at(data.mem_info.error()); if (data.disk_usage) content.push_back(createRow( @@ -196,7 +223,7 @@ namespace { std::format("{}/{}", BytesToGiB { data.disk_usage->used_bytes }, BytesToGiB { data.disk_usage->total_bytes }) )); else - ERROR_LOG_LOC(data.disk_usage.error()); + error_at(data.disk_usage.error()); if (data.shell) content.push_back(createRow(shellIcon, "Shell", *data.shell)); @@ -210,7 +237,7 @@ namespace { content.push_back(createRow(wmIcon, "WM", *data.window_manager)); if (config.now_playing.enabled && data.now_playing) { - if (const Result& nowPlayingResult = *data.now_playing) { + if (const Result& nowPlayingResult = *data.now_playing) { const MediaInfo& info = *nowPlayingResult; const String title = info.title.value_or("Unknown Title"); @@ -229,7 +256,7 @@ namespace { } )); } else - DEBUG_LOG_LOC(nowPlayingResult.error()); + debug_at(nowPlayingResult.error()); } return vbox(content) | borderRounded | color(Color::White); @@ -240,6 +267,8 @@ fn main() -> i32 { const Config& config = Config::getInstance(); const SystemData data = SystemData::fetchSystemData(config); + debug_log("{}", *os::GetPackageCount()); + Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }), text("") }); Screen screen = Screen::Create(Dimension::Full(), Dimension::Fit(document)); diff --git a/src/os/linux.cpp b/src/os/linux.cpp index a4e5a38..d611a47 100644 --- a/src/os/linux.cpp +++ b/src/os/linux.cpp @@ -1,125 +1,147 @@ #ifdef __linux__ // clang-format off -#include // needs to be at top for Success/None -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include // std::strlen +#include // DBus::CallMessage +#include // DBus::Connection +#include // DBus::Dispatcher +#include // DBus::{DataType, BusType} +#include // DBus::Error +#include // DBus::MessageAppendIterator +#include // DBus::Signature +#include // DBus::StandaloneDispatcher +#include // DBus::Variant +#include // std::{unexpected, expected} +#include // std::{format, format_to_n} +#include // std::ifstream +#include // PATH_MAX +#include // std::numeric_limits +#include // std::map (Map) +#include // std::shared_ptr (SharedPointer) +#include // std::{getline, string (String)} +#include // std::string_view (StringView) +#include // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED +#include // statvfs +#include // sysinfo +#include // utsname, uname +#include // readlink +#include // std::move -#include "os.h" -#include "src/os/linux/display_guards.h" -#include "src/util/macros.h" -#include "src/util/types.h" +#include "src/core/util/logging.hpp" +#include "src/core/util/helpers.hpp" + +#include "src/wrappers/wayland.hpp" +#include "src/wrappers/xcb.hpp" + +#include "os.hpp" +#include "linux/pkg_count.hpp" // clang-format on -using namespace std::string_view_literals; +using namespace util::types; +using util::error::DraconisError, util::error::DraconisErrorCode; namespace { - fn GetX11WindowManager() -> Result { - using os::linux::XcbReplyGuard; - using os::linux::XorgDisplayGuard; + fn GetX11WindowManager() -> Result { + using namespace xcb; - const XorgDisplayGuard conn; + const DisplayGuard conn; if (!conn) - if (const i32 err = xcb_connection_has_error(conn.get()); !conn || err != 0) - return Err(OsError(OsErrorCode::ApiUnavailable, [&] -> String { - switch (err) { - case 0: return "Connection object invalid, but no specific XCB error code"; - case XCB_CONN_ERROR: return "Stream/Socket/Pipe Error"; - case XCB_CONN_CLOSED_EXT_NOTSUPPORTED: return "Closed: Extension Not Supported"; - case XCB_CONN_CLOSED_MEM_INSUFFICIENT: return "Closed: Insufficient Memory"; - case XCB_CONN_CLOSED_REQ_LEN_EXCEED: return "Closed: Request Length Exceeded"; - case XCB_CONN_CLOSED_PARSE_ERR: return "Closed: Display String Parse Error"; - case XCB_CONN_CLOSED_INVALID_SCREEN: return "Closed: Invalid Screen"; - case XCB_CONN_CLOSED_FDPASSING_FAILED: return "Closed: FD Passing Failed"; - default: return std::format("Unknown Error Code ({})", err); + if (const i32 err = connection_has_error(conn.get())) + return Err(DraconisError(DraconisErrorCode::ApiUnavailable, [&] -> String { + if (const Option connErr = getConnError(err)) { + switch (*connErr) { + case Generic: return "Stream/Socket/Pipe Error"; + case ExtNotSupported: return "Extension Not Supported"; + case MemInsufficient: return "Insufficient Memory"; + case ReqLenExceed: return "Request Length Exceeded"; + case ParseErr: return "Display String Parse Error"; + case InvalidScreen: return "Invalid Screen"; + case FdPassingFailed: return "FD Passing Failed"; + default: return std::format("Unknown Error Code ({})", err); + } } + + return std::format("Unknown Error Code ({})", err); }())); - fn internAtom = [&conn](const StringView name) -> Result { - const XcbReplyGuard reply(xcb_intern_atom_reply( - conn.get(), xcb_intern_atom(conn.get(), 0, static_cast(name.size()), name.data()), nullptr - )); + fn internAtom = [&conn](const StringView name) -> Result { + const ReplyGuard reply( + intern_atom_reply(conn.get(), intern_atom(conn.get(), 0, static_cast(name.size()), name.data()), nullptr) + ); if (!reply) - return Err(OsError(OsErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name))); + return Err( + DraconisError(DraconisErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name)) + ); return reply->atom; }; - const Result supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK"); - const Result wmNameAtom = internAtom("_NET_WM_NAME"); - const Result utf8StringAtom = internAtom("UTF8_STRING"); + const Result supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK"); + const Result wmNameAtom = internAtom("_NET_WM_NAME"); + const Result utf8StringAtom = internAtom("UTF8_STRING"); if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) { if (!supportingWmCheckAtom) - ERROR_LOG("Failed to get _NET_SUPPORTING_WM_CHECK atom"); + error_log("Failed to get _NET_SUPPORTING_WM_CHECK atom"); if (!wmNameAtom) - ERROR_LOG("Failed to get _NET_WM_NAME atom"); + error_log("Failed to get _NET_WM_NAME atom"); if (!utf8StringAtom) - ERROR_LOG("Failed to get UTF8_STRING atom"); + error_log("Failed to get UTF8_STRING atom"); - return Err(OsError(OsErrorCode::PlatformSpecific, "Failed to get X11 atoms")); + return Err(DraconisError(DraconisErrorCode::PlatformSpecific, "Failed to get X11 atoms")); } - const XcbReplyGuard wmWindowReply(xcb_get_property_reply( + const ReplyGuard wmWindowReply(get_property_reply( conn.get(), - xcb_get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, XCB_ATOM_WINDOW, 0, 1), + get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1), nullptr )); - if (!wmWindowReply || wmWindowReply->type != XCB_ATOM_WINDOW || wmWindowReply->format != 32 || - xcb_get_property_value_length(wmWindowReply.get()) == 0) - return Err(OsError(OsErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property")); + if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 || + get_property_value_length(wmWindowReply.get()) == 0) + return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property")); - const xcb_window_t wmRootWindow = *static_cast(xcb_get_property_value(wmWindowReply.get())); + const window_t wmRootWindow = *static_cast(get_property_value(wmWindowReply.get())); - const XcbReplyGuard wmNameReply(xcb_get_property_reply( - conn.get(), xcb_get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr + const ReplyGuard wmNameReply(get_property_reply( + conn.get(), get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr )); - if (!wmNameReply || wmNameReply->type != *utf8StringAtom || xcb_get_property_value_length(wmNameReply.get()) == 0) - return Err(OsError(OsErrorCode::NotFound, "Failed to get _NET_WM_NAME property")); + if (!wmNameReply || wmNameReply->type != *utf8StringAtom || get_property_value_length(wmNameReply.get()) == 0) + return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get _NET_WM_NAME property")); - const char* nameData = static_cast(xcb_get_property_value(wmNameReply.get())); - const usize length = xcb_get_property_value_length(wmNameReply.get()); + const char* nameData = static_cast(get_property_value(wmNameReply.get())); + const usize length = get_property_value_length(wmNameReply.get()); return String(nameData, length); } - fn GetWaylandCompositor() -> Result { - using os::linux::WaylandDisplayGuard; - - const WaylandDisplayGuard display; + fn GetWaylandCompositor() -> Result { + const wl::DisplayGuard display; if (!display) - return Err(OsError(OsErrorCode::NotFound, "Failed to connect to display (is Wayland running?)")); + return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to connect to display (is Wayland running?)")); const i32 fileDescriptor = display.fd(); if (fileDescriptor < 0) - return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor")); + return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor")); ucred cred; socklen_t len = sizeof(cred); if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) - return Err(OsError::withErrno("Failed to get socket credentials (SO_PEERCRED)")); + return Err(DraconisError::withErrno("Failed to get socket credentials (SO_PEERCRED)")); Array exeLinkPathBuf; auto [out, size] = std::format_to_n(exeLinkPathBuf.data(), exeLinkPathBuf.size() - 1, "/proc/{}/exe", cred.pid); if (out >= exeLinkPathBuf.data() + exeLinkPathBuf.size() - 1) - return Err(OsError(OsErrorCode::InternalError, "Failed to format /proc path (PID too large?)")); + return Err(DraconisError(DraconisErrorCode::InternalError, "Failed to format /proc path (PID too large?)")); *out = '\0'; @@ -130,7 +152,7 @@ namespace { const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1); if (bytesRead == -1) - return Err(OsError::withErrno(std::format("Failed to read link '{}'", exeLinkPath))); + return Err(DraconisError::withErrno(std::format("Failed to read link '{}'", exeLinkPath))); exeRealPathBuf.at(bytesRead) = '\0'; @@ -153,7 +175,7 @@ namespace { compositorNameView = filenameView; if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/") - return Err(OsError(OsErrorCode::NotFound, "Failed to get compositor name from path")); + return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get compositor name from path")); if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() && compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) { @@ -161,7 +183,7 @@ namespace { compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length()); if (cleanedView.empty()) - return Err(OsError(OsErrorCode::NotFound, "Compositor name invalid after heuristic")); + return Err(DraconisError(DraconisErrorCode::NotFound, "Compositor name invalid after heuristic")); return String(cleanedView); } @@ -169,7 +191,9 @@ namespace { return String(compositorNameView); } - fn GetMprisPlayers(const SharedPointer& connection) -> Result { + fn GetMprisPlayers(const SharedPointer& connection) -> Result { + using namespace std::string_view_literals; + try { const SharedPointer call = DBus::CallMessage::create("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"); @@ -177,7 +201,7 @@ namespace { const SharedPointer reply = connection->send_with_reply_blocking(call, 5); if (!reply || !reply->is_valid()) - return Err(OsError(OsErrorCode::Timeout, "Failed to get reply from ListNames")); + return Err(DraconisError(DraconisErrorCode::Timeout, "Failed to get reply from ListNames")); Vec allNamesStd; DBus::MessageIterator reader(*reply); @@ -187,14 +211,14 @@ namespace { if (StringView(name).contains("org.mpris.MediaPlayer2"sv)) return name; - return Err(OsError(OsErrorCode::NotFound, "No MPRIS players found")); - } catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) { - return Err(OsError(OsErrorCode::InternalError, e.what())); + return Err(DraconisError(DraconisErrorCode::NotFound, "No MPRIS players found")); + } catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) { + return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); } } fn GetMediaPlayerMetadata(const SharedPointer& connection, const String& playerBusName) - -> Result { + -> Result { try { const SharedPointer metadataCall = DBus::CallMessage::create(playerBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); @@ -204,7 +228,9 @@ namespace { const SharedPointer metadataReply = connection->send_with_reply_blocking(metadataCall, 1000); if (!metadataReply || !metadataReply->is_valid()) { - return Err(OsError(OsErrorCode::Timeout, "DBus Get Metadata call timed out or received invalid reply")); + return Err( + DraconisError(DraconisErrorCode::Timeout, "DBus Get Metadata call timed out or received invalid reply") + ); } DBus::MessageIterator iter(*metadataReply); @@ -213,8 +239,8 @@ namespace { // MPRIS metadata is variant containing a dict a{sv} if (metadataVariant.type() != DBus::DataType::DICT_ENTRY && metadataVariant.type() != DBus::DataType::ARRAY) { - return Err(OsError( - OsErrorCode::ParseError, + return Err(DraconisError( + DraconisErrorCode::ParseError, std::format( "Inner metadata variant is not the expected type, expected dict/a{{sv}} but got '{}'", metadataVariant.signature().str() @@ -224,16 +250,14 @@ namespace { Map metadata = metadataVariant.to_map(); // Can throw - Option title = None; - Option artist = None; - Option album = None; - Option appName = None; // Try to get app name too + Option title = None; + Option artist = None; - if (auto titleIter = metadata.find("xesam:title"); + if (const auto titleIter = metadata.find("xesam:title"); titleIter != metadata.end() && titleIter->second.type() == DBus::DataType::STRING) title = titleIter->second.to_string(); - if (auto artistIter = metadata.find("xesam:artist"); artistIter != metadata.end()) { + if (const auto artistIter = metadata.find("xesam:artist"); artistIter != metadata.end()) { if (artistIter->second.type() == DBus::DataType::ARRAY) { if (Vec artists = artistIter->second.to_vector(); !artists.empty()) artist = artists[0]; @@ -242,47 +266,27 @@ namespace { } } - if (auto albumIter = metadata.find("xesam:album"); - albumIter != metadata.end() && albumIter->second.type() == DBus::DataType::STRING) - album = albumIter->second.to_string(); - - try { - const SharedPointer identityCall = - DBus::CallMessage::create(playerBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); - *identityCall << "org.mpris.MediaPlayer2" << "Identity"; - if (const SharedPointer identityReply = connection->send_with_reply_blocking(identityCall, 500); - identityReply && identityReply->is_valid()) { - DBus::MessageIterator identityIter(*identityReply); - DBus::Variant identityVariant; - identityIter >> identityVariant; - if (identityVariant.type() == DBus::DataType::STRING) - appName = identityVariant.to_string(); - } - } catch (const DBus::Error& e) { - DEBUG_LOG("Failed to get player Identity property for {}: {}", playerBusName, e.what()); // Non-fatal - } - - return MediaInfo(std::move(title), std::move(artist), std::move(album), std::move(appName)); - } catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) { - return Err( - OsError(OsErrorCode::InternalError, std::format("Standard exception processing metadata: {}", e.what())) - ); + return MediaInfo(std::move(title), std::move(artist)); + } catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) { + return Err(DraconisError( + DraconisErrorCode::InternalError, std::format("Standard exception processing metadata: {}", e.what()) + )); } } } // namespace -fn os::GetOSVersion() -> Result { +fn os::GetOSVersion() -> Result { constexpr CStr path = "/etc/os-release"; std::ifstream file(path); if (!file) - return Err(OsError(OsErrorCode::NotFound, std::format("Failed to open {}", path))); + return Err(DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open {}", path))); String line; constexpr StringView prefix = "PRETTY_NAME="; - while (getline(file, line)) { + while (std::getline(file, line)) { if (StringView(line).starts_with(prefix)) { String value = line.substr(prefix.size()); @@ -291,36 +295,36 @@ fn os::GetOSVersion() -> Result { value = value.substr(1, value.length() - 2); if (value.empty()) - return Err( - OsError(OsErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path)) - ); + return Err(DraconisError( + DraconisErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path) + )); return value; } } - return Err(OsError(OsErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path))); + return Err(DraconisError(DraconisErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path))); } -fn os::GetMemInfo() -> Result { +fn os::GetMemInfo() -> Result { struct sysinfo info; if (sysinfo(&info) != 0) - return Err(OsError::fromDBus("sysinfo call failed")); + return Err(DraconisError::fromDBus("sysinfo call failed")); const u64 totalRam = info.totalram; const u64 memUnit = info.mem_unit; if (memUnit == 0) - return Err(OsError(OsErrorCode::InternalError, "sysinfo returned mem_unit of zero")); + return Err(DraconisError(DraconisErrorCode::InternalError, "sysinfo returned mem_unit of zero")); if (totalRam > std::numeric_limits::max() / memUnit) - return Err(OsError(OsErrorCode::InternalError, "Potential overflow calculating total RAM")); + return Err(DraconisError(DraconisErrorCode::InternalError, "Potential overflow calculating total RAM")); return info.totalram * info.mem_unit; } -fn os::GetNowPlaying() -> Result { +fn os::GetNowPlaying() -> Result { // Dispatcher must outlive the try-block because 'connection' depends on it later. // ReSharper disable once CppTooWideScope, CppJoinDeclarationAndAssignment SharedPointer dispatcher; @@ -330,22 +334,22 @@ fn os::GetNowPlaying() -> Result { dispatcher = DBus::StandaloneDispatcher::create(); if (!dispatcher) - return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to create DBus dispatcher")); + return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to create DBus dispatcher")); connection = dispatcher->create_connection(DBus::BusType::SESSION); if (!connection) - return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to connect to DBus session bus")); - } catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) { - return Err(OsError(OsErrorCode::InternalError, e.what())); + return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to connect to DBus session bus")); + } catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) { + return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); } - Result playerBusName = GetMprisPlayers(connection); + Result playerBusName = GetMprisPlayers(connection); if (!playerBusName) return Err(playerBusName.error()); - Result metadataResult = GetMediaPlayerMetadata(connection, *playerBusName); + Result metadataResult = GetMediaPlayerMetadata(connection, *playerBusName); if (!metadataResult) return Err(metadataResult.error()); @@ -354,37 +358,39 @@ fn os::GetNowPlaying() -> Result { } fn os::GetWindowManager() -> Option { - if (Result waylandResult = GetWaylandCompositor()) + if (Result waylandResult = GetWaylandCompositor()) return *waylandResult; else - DEBUG_LOG("Could not detect Wayland compositor: {}", waylandResult.error().message); + debug_log("Could not detect Wayland compositor: {}", waylandResult.error().message); - if (Result x11Result = GetX11WindowManager()) + if (Result x11Result = GetX11WindowManager()) return *x11Result; else - DEBUG_LOG("Could not detect X11 window manager: {}", x11Result.error().message); + debug_log("Could not detect X11 window manager: {}", x11Result.error().message); return None; } fn os::GetDesktopEnvironment() -> Option { - return GetEnv("XDG_CURRENT_DESKTOP") + return util::helpers::GetEnv("XDG_CURRENT_DESKTOP") .transform([](const String& xdgDesktop) -> String { if (const usize colon = xdgDesktop.find(':'); colon != String::npos) return xdgDesktop.substr(0, colon); return xdgDesktop; }) - .or_else([](const EnvError&) -> Result { return GetEnv("DESKTOP_SESSION"); }) + .or_else([](const DraconisError&) -> Result { + return util::helpers::GetEnv("DESKTOP_SESSION"); + }) .transform([](const String& finalValue) -> Option { - DEBUG_LOG("Found desktop environment: {}", finalValue); + debug_log("Found desktop environment: {}", finalValue); return finalValue; }) .value_or(None); } fn os::GetShell() -> Option { - if (const Result shellPath = GetEnv("SHELL")) { + if (const Result shellPath = util::helpers::GetEnv("SHELL")) { // clang-format off constexpr Array, 5> shellMap {{ { "bash", "Bash" }, @@ -405,56 +411,61 @@ fn os::GetShell() -> Option { return None; } -fn os::GetHost() -> Result { +fn os::GetHost() -> Result { constexpr CStr primaryPath = "/sys/class/dmi/id/product_family"; constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name"; - fn readFirstLine = [&](const String& path) -> Result { + fn readFirstLine = [&](const String& path) -> Result { std::ifstream file(path); String line; if (!file) - return Err(OsError(OsErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path))); + return Err( + DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path)) + ); - if (!getline(file, line)) - return Err(OsError(OsErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path))); + if (!std::getline(file, line)) + return Err( + DraconisError(DraconisErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)) + ); return line; }; - return readFirstLine(primaryPath).or_else([&](const OsError& primaryError) -> Result { - return readFirstLine(fallbackPath).or_else([&](const OsError& fallbackError) -> Result { - return Err(OsError( - OsErrorCode::InternalError, - std::format( - "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", - primaryPath, - primaryError.message, - fallbackPath, - fallbackError.message - ) - )); - }); + return readFirstLine(primaryPath).or_else([&](const DraconisError& primaryError) -> Result { + return readFirstLine(fallbackPath) + .or_else([&](const DraconisError& fallbackError) -> Result { + return Err(DraconisError( + DraconisErrorCode::InternalError, + std::format( + "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", + primaryPath, + primaryError.message, + fallbackPath, + fallbackError.message + ) + )); + }); }); } -fn os::GetKernelVersion() -> Result { +fn os::GetKernelVersion() -> Result { utsname uts; if (uname(&uts) == -1) - return Err(OsError::withErrno("uname call failed")); + return Err(DraconisError::withErrno("uname call failed")); - if (strlen(uts.release) == 0) - return Err(OsError(OsErrorCode::ParseError, "uname returned null kernel release")); + if (std::strlen(uts.release) == 0) + return Err(DraconisError(DraconisErrorCode::ParseError, "uname returned null kernel release")); return uts.release; } -fn os::GetDiskUsage() -> Result { +fn os::GetDiskUsage() -> Result { struct statvfs stat; if (statvfs("/", &stat) == -1) - return Err(OsError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)"))); + return Err(DraconisError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)"))); return DiskSpace { .used_bytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize), @@ -462,4 +473,6 @@ fn os::GetDiskUsage() -> Result { }; } +fn os::GetPackageCount() -> Result { return linux::GetNixPackageCount(); } + #endif // __linux__ diff --git a/src/os/linux/display_guards.cpp b/src/os/linux/display_guards.cpp deleted file mode 100644 index c417e7e..0000000 --- a/src/os/linux/display_guards.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#ifdef __linux__ - -#include - -#include "display_guards.h" - -#include "src/util/macros.h" - -namespace os::linux { - XorgDisplayGuard::XorgDisplayGuard(const CStr name) : m_Connection(xcb_connect(name, nullptr)) {} - - XorgDisplayGuard::~XorgDisplayGuard() { - if (m_Connection) - xcb_disconnect(m_Connection); - } - - XorgDisplayGuard::XorgDisplayGuard(XorgDisplayGuard&& other) noexcept - : m_Connection(std::exchange(other.m_Connection, nullptr)) {} - - fn XorgDisplayGuard::operator=(XorgDisplayGuard&& other) noexcept -> XorgDisplayGuard& { - if (this != &other) { - if (m_Connection) - xcb_disconnect(m_Connection); - m_Connection = std::exchange(other.m_Connection, nullptr); - } - return *this; - } - - XorgDisplayGuard::operator bool() const { return m_Connection && !xcb_connection_has_error(m_Connection); } - - fn XorgDisplayGuard::get() const -> xcb_connection_t* { return m_Connection; } - - fn XorgDisplayGuard::setup() const -> const xcb_setup_t* { - return m_Connection ? xcb_get_setup(m_Connection) : nullptr; - } - - fn XorgDisplayGuard::rootScreen() const -> xcb_screen_t* { - const xcb_setup_t* setup = this->setup(); - return setup ? xcb_setup_roots_iterator(setup).data : nullptr; - } - - WaylandDisplayGuard::WaylandDisplayGuard() : m_Display(wl_display_connect(nullptr)) {} - - WaylandDisplayGuard::~WaylandDisplayGuard() { - if (m_Display) - wl_display_disconnect(m_Display); - } - - WaylandDisplayGuard::WaylandDisplayGuard(WaylandDisplayGuard&& other) noexcept - : m_Display(std::exchange(other.m_Display, nullptr)) {} - - fn WaylandDisplayGuard::operator=(WaylandDisplayGuard&& other) noexcept -> WaylandDisplayGuard& { - if (this != &other) { - if (m_Display) - wl_display_disconnect(m_Display); - - m_Display = std::exchange(other.m_Display, nullptr); - } - - return *this; - } - - WaylandDisplayGuard::operator bool() const { return m_Display != nullptr; } - - fn WaylandDisplayGuard::get() const -> wl_display* { return m_Display; } - - fn WaylandDisplayGuard::fd() const -> i32 { return m_Display ? wl_display_get_fd(m_Display) : -1; } -} - -#endif diff --git a/src/os/linux/display_guards.h b/src/os/linux/display_guards.h deleted file mode 100644 index 8733e2f..0000000 --- a/src/os/linux/display_guards.h +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once - -#ifdef __linux__ - -#include -#include - -#include "src/util/macros.h" - -namespace os::linux { - /** - * RAII wrapper for X11 Display connections - * Automatically handles resource acquisition and cleanup - */ - class XorgDisplayGuard { - xcb_connection_t* m_Connection = nullptr; - - public: - /** - * Opens an XCB connection - * @param name Display name (nullptr for default) - */ - explicit XorgDisplayGuard(CStr name = nullptr); - ~XorgDisplayGuard(); - - // Non-copyable - XorgDisplayGuard(const XorgDisplayGuard&) = delete; - fn operator=(const XorgDisplayGuard&)->XorgDisplayGuard& = delete; - - // Movable - XorgDisplayGuard(XorgDisplayGuard&& other) noexcept; - fn operator=(XorgDisplayGuard&& other) noexcept -> XorgDisplayGuard&; - - [[nodiscard]] explicit operator bool() const; - - [[nodiscard]] fn get() const -> xcb_connection_t*; - [[nodiscard]] fn setup() const -> const xcb_setup_t*; - [[nodiscard]] fn rootScreen() const -> xcb_screen_t*; - }; - - /** - * RAII wrapper for XCB replies - * Handles automatic cleanup of various XCB reply objects - */ - template - class XcbReplyGuard { - T* m_Reply = nullptr; - - public: - XcbReplyGuard() = default; - explicit XcbReplyGuard(T* reply) : m_Reply(reply) {} - - ~XcbReplyGuard() { - if (m_Reply) - free(m_Reply); - } - - // Non-copyable - XcbReplyGuard(const XcbReplyGuard&) = delete; - fn operator=(const XcbReplyGuard&)->XcbReplyGuard& = delete; - - // Movable - XcbReplyGuard(XcbReplyGuard&& other) noexcept : m_Reply(std::exchange(other.m_Reply, nullptr)) {} - fn operator=(XcbReplyGuard&& other) noexcept -> XcbReplyGuard& { - if (this != &other) { - if (m_Reply) - free(m_Reply); - - m_Reply = std::exchange(other.m_Reply, nullptr); - } - return *this; - } - - [[nodiscard]] explicit operator bool() const { return m_Reply != nullptr; } - - [[nodiscard]] fn get() const -> T* { return m_Reply; } - [[nodiscard]] fn operator->() const->T* { return m_Reply; } - [[nodiscard]] fn operator*() const->T& { return *m_Reply; } - }; - - /** - * RAII wrapper for Wayland display connections - * Automatically handles resource acquisition and cleanup - */ - class WaylandDisplayGuard { - wl_display* m_Display; - - public: - /** - * Opens a Wayland display connection - */ - WaylandDisplayGuard(); - ~WaylandDisplayGuard(); - - // Non-copyable - WaylandDisplayGuard(const WaylandDisplayGuard&) = delete; - fn operator=(const WaylandDisplayGuard&)->WaylandDisplayGuard& = delete; - - // Movable - WaylandDisplayGuard(WaylandDisplayGuard&& other) noexcept; - fn operator=(WaylandDisplayGuard&& other) noexcept -> WaylandDisplayGuard&; - - [[nodiscard]] explicit operator bool() const; - - [[nodiscard]] fn get() const -> wl_display*; - [[nodiscard]] fn fd() const -> i32; - }; -} - -#endif diff --git a/src/os/linux/issetugid_stub.cpp b/src/os/linux/issetugid_stub.cpp index 4a77e3e..c79e71c 100644 --- a/src/os/linux/issetugid_stub.cpp +++ b/src/os/linux/issetugid_stub.cpp @@ -1,3 +1,4 @@ -#include "src/util/macros.h" +#include "src/core/util/defs.hpp" +#include "src/core/util/types.hpp" -extern "C" fn issetugid() -> usize { return 0; } // NOLINT +extern "C" fn issetugid() -> util::types::usize { return 0; } // NOLINT diff --git a/src/os/linux/pkg_count.cpp b/src/os/linux/pkg_count.cpp index 7413648..cf50a2f 100644 --- a/src/os/linux/pkg_count.cpp +++ b/src/os/linux/pkg_count.cpp @@ -1 +1,217 @@ -#include "src/os/linux/pkg_count.h" +#include "src/os/linux/pkg_count.hpp" + +#include +#include +#include +#include +#include +#include + +#include "src/core/util/logging.hpp" +#include "src/core/util/types.hpp" + +using util::error::DraconisError, util::error::DraconisErrorCode; +using util::types::u64, util::types::i64, util::types::Result, util::types::Err, util::types::String, + util::types::Exception; + +namespace { + namespace fs = std::filesystem; + using namespace std::chrono; + + struct NixPkgCacheData { + u64 count {}; + system_clock::time_point timestamp; + + // NOLINTBEGIN(readability-identifier-naming) - Needs to specifically use `glaze` + struct [[maybe_unused]] glaze { + using T = NixPkgCacheData; + static constexpr auto value = glz::object("count", &T::count, "timestamp", [](auto& self) -> auto& { + thread_local auto epoch_seconds = duration_cast(self.timestamp.time_since_epoch()).count(); + return epoch_seconds; + }); + }; + // NOLINTEND(readability-identifier-naming) + }; + + fn GetPkgCountCachePath() -> Result { + std::error_code errc; + const fs::path cacheDir = fs::temp_directory_path(errc); + if (errc) { + return Err(DraconisError(DraconisErrorCode::IoError, "Failed to get temp directory: " + errc.message())); + } + return cacheDir / "nix_pkg_count_cache.json"; + } + + fn ReadPkgCountCache() -> Result { + auto cachePathResult = GetPkgCountCachePath(); + if (!cachePathResult) { + return Err(cachePathResult.error()); + } + const fs::path& cachePath = *cachePathResult; + + if (!fs::exists(cachePath)) { + return Err(DraconisError(DraconisErrorCode::NotFound, "Cache file not found: " + cachePath.string())); + } + + std::ifstream ifs(cachePath, std::ios::binary); + if (!ifs.is_open()) { + return Err( + DraconisError(DraconisErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string()) + ); + } + + debug_log("Reading Nix package count from cache file: {}", cachePath.string()); + + try { + const String content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + NixPkgCacheData result; + glz::context ctx {}; + + if (auto glazeResult = glz::read(result, content, ctx); + glazeResult.ec != glz::error_code::none) { + return Err(DraconisError( + DraconisErrorCode::ParseError, + std::format("JSON parse error reading cache: {}", glz::format_error(glazeResult, content)) + )); + } + + if (size_t tsPos = content.find("\"timestamp\""); tsPos != String::npos) { + size_t colonPos = content.find(':', tsPos); + if (size_t valueStart = content.find_first_of("0123456789", colonPos); valueStart != String::npos) { + long long timestampSeconds = 0; + char* endPtr = nullptr; + timestampSeconds = std::strtoll(content.c_str() + valueStart, &endPtr, 10); + result.timestamp = system_clock::time_point(seconds(timestampSeconds)); + } else { + return Err(DraconisError(DraconisErrorCode::ParseError, "Could not parse timestamp value from cache JSON.")); + } + } else { + return Err(DraconisError(DraconisErrorCode::ParseError, "Timestamp field not found in cache JSON.")); + } + + debug_log("Successfully read package count from cache file."); + return result; + } catch (const Exception& e) { + return Err( + DraconisError(DraconisErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what())) + ); + } + } + + fn WritePkgCountCache(const NixPkgCacheData& data) -> Result { + auto cachePathResult = GetPkgCountCachePath(); + if (!cachePathResult) { + return Err(cachePathResult.error()); + } + const fs::path& cachePath = *cachePathResult; + fs::path tempPath = cachePath; + tempPath += ".tmp"; + + debug_log("Writing Nix package count to cache file: {}", cachePath.string()); + + try { + { + std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc); + if (!ofs.is_open()) { + return Err(DraconisError(DraconisErrorCode::IoError, "Failed to open temp cache file: " + tempPath.string())); + } + + String jsonStr; + + NixPkgCacheData mutableData = data; + + if (auto glazeErr = glz::write_json(mutableData, jsonStr); glazeErr.ec != glz::error_code::none) { + return Err(DraconisError( + DraconisErrorCode::ParseError, + std::format("JSON serialization error writing cache: {}", glz::format_error(glazeErr, jsonStr)) + )); + } + + ofs << jsonStr; + if (!ofs) { + return Err(DraconisError(DraconisErrorCode::IoError, "Failed to write to temp cache file")); + } + } + + std::error_code errc; + fs::rename(tempPath, cachePath, errc); + if (errc) { + fs::remove(tempPath); + return Err(DraconisError( + DraconisErrorCode::IoError, + std::format("Failed to replace cache file '{}': {}", cachePath.string(), errc.message()) + )); + } + + debug_log("Successfully wrote package count to cache file."); + return {}; + } catch (const Exception& e) { + fs::remove(tempPath); + return Err(DraconisError( + DraconisErrorCode::InternalError, std::format("File operation error writing package count cache: {}", e.what()) + )); + } + } + +} // namespace + +fn os::linux::GetNixPackageCount() -> Result { + const fs::path nixDbPath = "/nix/var/nix/db/db.sqlite"; + + if (Result cachedDataResult = ReadPkgCountCache()) { + const auto& [count, timestamp] = *cachedDataResult; + + std::error_code errc; + std::filesystem::file_time_type dbModTime = fs::last_write_time(nixDbPath, errc); + + if (errc) { + warn_log("Could not get modification time for '{}': {}. Invalidating cache.", nixDbPath.string(), errc.message()); + } else { + if (timestamp.time_since_epoch() >= dbModTime.time_since_epoch()) { + debug_log( + "Using valid Nix package count cache (DB file unchanged since {}). Count: {}", + std::format("{:%F %T %Z}", floor(timestamp)), + count + ); + return count; + } + debug_log("Nix package count cache stale (DB file modified)."); + } + } else { + if (cachedDataResult.error().code != DraconisErrorCode::NotFound) + debug_at(cachedDataResult.error()); + debug_log("Nix package count cache not found or unreadable."); + } + + debug_log("Fetching fresh Nix package count from database: {}", nixDbPath.string()); + u64 count = 0; + + try { + const SQLite::Database database("/nix/var/nix/db/db.sqlite", SQLite::OPEN_READONLY); + + if (SQLite::Statement query(database, "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL"); + query.executeStep()) { + const i64 countInt64 = query.getColumn(0).getInt64(); + if (countInt64 < 0) + return Err(DraconisError(DraconisErrorCode::ParseError, "Negative count returned by Nix DB COUNT(*) query.")); + count = static_cast(countInt64); + } else { + return Err(DraconisError(DraconisErrorCode::ParseError, "No rows returned by Nix DB COUNT(*) query.")); + } + } catch (const SQLite::Exception& e) { + return Err(DraconisError( + DraconisErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing Nix DB: {}", e.what()) + )); + } catch (const Exception& e) { return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); } catch (...) { + return Err(DraconisError(DraconisErrorCode::Other, "Unknown error occurred accessing Nix DB")); + } + + const NixPkgCacheData dataToCache = { .count = count, .timestamp = system_clock::now() }; + if (Result writeResult = WritePkgCountCache(dataToCache); !writeResult) { + warn_at(writeResult.error()); + warn_log("Failed to write Nix package count to cache."); + } + + debug_log("Fetched fresh Nix package count: {}", count); + return count; +} diff --git a/src/os/linux/pkg_count.h b/src/os/linux/pkg_count.h deleted file mode 100644 index b2eaa96..0000000 --- a/src/os/linux/pkg_count.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "src/util/macros.h" - -// Get package count from dpkg (Debian/Ubuntu) -fn GetDpkgPackageCount() -> Option; - -// Get package count from RPM (Red Hat/Fedora/CentOS) -fn GetRpmPackageCount() -> Option; - -// Get package count from pacman (Arch Linux) -fn GetPacmanPackageCount() -> Option; - -// Get package count from Portage (Gentoo) -fn GetPortagePackageCount() -> Option; - -// Get package count from zypper (openSUSE) -fn GetZypperPackageCount() -> Option; - -// Get package count from apk (Alpine) -fn GetApkPackageCount() -> Option; - -// Get package count from nix -fn GetNixPackageCount() -> Option; - -// Get package count from flatpak -fn GetFlatpakPackageCount() -> Option; - -// Get package count from snap -fn GetSnapPackageCount() -> Option; - -// Get package count from AppImage -fn GetAppimagePackageCount() -> Option; - -// Get total package count from all available package managers -fn GetTotalPackageCount() -> Option; diff --git a/src/os/linux/pkg_count.hpp b/src/os/linux/pkg_count.hpp new file mode 100644 index 0000000..f96d0a6 --- /dev/null +++ b/src/os/linux/pkg_count.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "src/core/util/defs.hpp" +#include "src/core/util/error.hpp" +#include "src/core/util/types.hpp" + +namespace os::linux { + using util::error::DraconisError; + using util::types::Result, util::types::u64; + + // Get package count from dpkg (Debian/Ubuntu) + fn GetDpkgPackageCount() -> Result; + + // Get package count from RPM (Red Hat/Fedora/CentOS) + fn GetRpmPackageCount() -> Result; + + // Get package count from pacman (Arch Linux) + fn GetPacmanPackageCount() -> Result; + + // Get package count from Portage (Gentoo) + fn GetPortagePackageCount() -> Result; + + // Get package count from zypper (openSUSE) + fn GetZypperPackageCount() -> Result; + + // Get package count from apk (Alpine) + fn GetApkPackageCount() -> Result; + + // Get package count from nix + fn GetNixPackageCount() -> Result; + + // Get package count from flatpak + fn GetFlatpakPackageCount() -> Result; + + // Get package count from snap + fn GetSnapPackageCount() -> Result; + + // Get package count from AppImage + fn GetAppimagePackageCount() -> Result; + + // Get total package count from all available package managers + fn GetTotalPackageCount() -> Result; +} // namespace os::linux \ No newline at end of file diff --git a/src/os/macos/bridge.h b/src/os/macos/bridge.hpp similarity index 100% rename from src/os/macos/bridge.h rename to src/os/macos/bridge.hpp diff --git a/src/os/os.h b/src/os/os.hpp similarity index 79% rename from src/os/os.h rename to src/os/os.hpp index e5717fc..971bec0 100644 --- a/src/os/os.h +++ b/src/os/os.hpp @@ -1,7 +1,8 @@ #pragma once -#include "../util/macros.h" -#include "../util/types.h" +#include "src/core/util/defs.hpp" +#include "src/core/util/error.hpp" +#include "src/core/util/types.hpp" /** * @namespace os @@ -13,33 +14,35 @@ * (found in linux.cpp, windows.cpp, macos.cpp). */ namespace os { + using util::error::DraconisError; + using util::types::u64, util::types::String, util::types::Option, util::types::Result, util::types::MediaInfo, + util::types::DiskSpace; + /** * @brief Get the total amount of physical RAM installed in the system. * @return A Result containing the total RAM in bytes (u64) on success, * or an OsError on failure. */ - fn GetMemInfo() -> Result; + fn GetMemInfo() -> Result; /** * @brief Gets structured metadata about the currently playing media. * @return A Result containing the media information (MediaInfo struct) on success, * or a NowPlayingError (indicating player state or system error) on failure. */ - fn GetNowPlaying() -> Result; + fn GetNowPlaying() -> Result; /** * @brief Gets the "pretty" name of the operating system. * @details Examples: "Ubuntu 24.04.2 LTS", "Windows 11 Pro 24H2", "macOS 15 Sequoia". - * @return A Result containing the OS version String on success, - * or an OsError on failure. + * @return A Result containing the OS version String on success, or an OsError on failure. */ - fn GetOSVersion() -> Result; + fn GetOSVersion() -> Result; /** * @brief Attempts to retrieve the desktop environment name. - * @details This is most relevant on Linux. May check environment variables (XDG_CURRENT_DESKTOP), - * session files, or running processes. On Windows/macOS, it might return a - * UI theme identifier (e.g., "Fluent", "Aqua") or None. + * @details This is most relevant on Linux. May check environment variables (XDG_CURRENT_DESKTOP), session files, + * or running processes. On Windows/macOS, it might return a UI theme identifier (e.g., "Fluent", "Aqua") or None. * @return An Option containing the detected DE name String, or None if detection fails or is not applicable. */ fn GetDesktopEnvironment() -> Option; @@ -56,8 +59,8 @@ namespace os { * @brief Attempts to detect the current user shell name. * @details Checks the SHELL environment variable on Linux/macOS. On Windows, inspects the process tree * to identify known shells like PowerShell, Cmd, or MSYS2 shells (Bash, Zsh). - * @return An Option containing the detected shell name (e.g., "Bash", "Zsh", "PowerShell", "Fish"), or None if - * detection fails. + * @return An Option containing the detected shell name (e.g., "Bash", "Zsh", "PowerShell", "Fish"), + * or None if detection fails. */ fn GetShell() -> Option; @@ -68,7 +71,7 @@ namespace os { * @return A Result containing the host/product identifier String on success, * or an OsError on failure (e.g., permission reading DMI/registry, API error). */ - fn GetHost() -> Result; + fn GetHost() -> Result; /** * @brief Gets the operating system's kernel version string. @@ -77,7 +80,7 @@ namespace os { * @return A Result containing the kernel version String on success, * or an OsError on failure. */ - fn GetKernelVersion() -> Result; + fn GetKernelVersion() -> Result; /** * @brief Gets the number of installed packages (Platform-specific). @@ -86,7 +89,7 @@ namespace os { * or an OsError on failure (e.g., permission errors, command not found) * or if not supported (OsErrorCode::NotSupported). */ - fn GetPackageCount() -> Result; // Note: Returns OsError{OsErrorCode::NotSupported} on Win/Mac likely + fn GetPackageCount() -> Result; /** * @brief Gets the disk usage for the primary/root filesystem. @@ -94,5 +97,5 @@ namespace os { * @return A Result containing the DiskSpace struct (used/total bytes) on success, * or an OsError on failure (e.g., filesystem not found, permission error). */ - fn GetDiskUsage() -> Result; + fn GetDiskUsage() -> Result; } // namespace os diff --git a/src/util/macros.h b/src/util/macros.h deleted file mode 100644 index c5ad961..0000000 --- a/src/util/macros.h +++ /dev/null @@ -1,347 +0,0 @@ -#pragma once - -// Fixes conflict in Windows with -#ifdef _WIN32 - #undef ERROR -#endif // _WIN32 - -#include -#include -#include -#include -#include -#include - -#include "types.h" - -/// Macro alias for trailing return type functions. -#define fn auto - -/// Macro alias for std::nullopt, represents an empty optional value. -#define None std::nullopt - -/** - * @namespace term - * @brief Provides terminal-related utilities, including color and style formatting. - */ -namespace term { - /** - * @enum Emphasis - * @brief Represents text emphasis styles. - * - * Enum values can be combined using bitwise OR to apply multiple styles at once. - */ - enum class Emphasis : u8 { - Bold, ///< Bold text. - Italic ///< Italic text. - }; - - /** - * @enum Color - * @brief Represents ANSI color codes for terminal output. - * - * Color codes can be used to format terminal output. - */ - enum class Color : u8 { - Black = 30, ///< Black color. - Red = 31, ///< Red color. - Green = 32, ///< Green color. - Yellow = 33, ///< Yellow color. - Blue = 34, ///< Blue color. - Magenta = 35, ///< Magenta color. - Cyan = 36, ///< Cyan color. - White = 37, ///< White color. - BrightBlack = 90, ///< Bright black (gray) color. - BrightRed = 91, ///< Bright red color. - BrightGreen = 92, ///< Bright green color. - BrightYellow = 93, ///< Bright yellow color. - BrightBlue = 94, ///< Bright blue color. - BrightMagenta = 95, ///< Bright magenta color. - BrightCyan = 96, ///< Bright cyan color. - BrightWhite = 97, ///< Bright white color. - }; - - /** - * @brief Combines two emphasis styles using bitwise OR. - * @param emphA The first emphasis style. - * @param emphB The second emphasis style. - * @return The combined emphasis style. - */ - constexpr fn operator|(Emphasis emphA, Emphasis emphB)->Emphasis { - return static_cast(static_cast(emphA) | static_cast(emphB)); - } - - /** - * @brief Checks if two emphasis styles are equal using bitwise AND. - * @param emphA The first emphasis style. - * @param emphB The second emphasis style. - * @return The result of the bitwise AND operation. - */ - constexpr fn operator&(Emphasis emphA, Emphasis emphB)->u8 { return static_cast(emphA) & static_cast(emphB); } - - /** - * @struct Style - * @brief Represents a combination of text styles. - * - * Emphasis and color are both optional, allowing for flexible styling. - */ - struct Style { - Option emph; ///< Optional emphasis style. - Option fg_col; ///< Optional foreground color style. - - /** - * @brief Generates the ANSI escape code for the combined styles. - * @return The ANSI escape code for the combined styles. - */ - [[nodiscard]] fn ansiCode() const -> String { - String result; - - if (emph) { - if ((*emph & Emphasis::Bold) != 0) - result += "\033[1m"; - if ((*emph & Emphasis::Italic) != 0) - result += "\033[3m"; - } - - if (fg_col) - result += std::format("\033[{}m", static_cast(*fg_col)); - - return result; - } - }; - - /** - * @brief Combines an emphasis style and a foreground color into a Style. - * @param emph The emphasis style to apply. - * @param fgColor The foreground color to apply. - * @return The combined style. - */ - // ReSharper disable CppDFAConstantParameter - constexpr fn operator|(const Emphasis emph, const Color fgColor)->Style { - return { .emph = emph, .fg_col = fgColor }; - } - // ReSharper restore CppDFAConstantParameter - - /** - * @brief Combines a foreground color and an emphasis style into a Style. - * @param fgColor The foreground color to apply. - * @param emph The emphasis style to apply. - * @return The combined style. - */ - constexpr fn operator|(const Color fgColor, const Emphasis emph)->Style { - return { .emph = emph, .fg_col = fgColor }; - } - - /** - * @brief Prints formatted text with the specified style. - * @tparam Args Parameter pack for format arguments. - * @param style The Style object containing emphasis and/or color. - * @param fmt The format string. - * @param args The arguments for the format string. - */ - template - fn Print(const Style& style, std::format_string fmt, Args&&... args) -> void { - if (const String styleCode = style.ansiCode(); styleCode.empty()) - std::print(fmt, std::forward(args)...); - else - std::print("{}{}{}", styleCode, std::format(fmt, std::forward(args)...), "\033[0m"); - } - - /** - * @brief Prints formatted text with the specified foreground color. - * @tparam Args Parameter pack for format arguments. - * @param fgColor The foreground color to apply. - * @param fmt The format string. - * @param args The arguments for the format string. - */ - template - fn Print(const Color& fgColor, std::format_string fmt, Args&&... args) -> void { - Print({ .emph = None, .fg_col = fgColor }, fmt, std::forward(args)...); - } - - /** - * @brief Prints formatted text with the specified emphasis style. - * @tparam Args Parameter pack for format arguments. - * @param emph The emphasis style to apply. - * @param fmt The format string. - * @param args The arguments for the format string. - */ - template - fn Print(const Emphasis emph, std::format_string fmt, Args&&... args) -> void { - Print({ .emph = emph, .fg_col = None }, fmt, std::forward(args)...); - } - - /** - * @brief Prints formatted text with no specific style (default terminal style). - * @tparam Args Parameter pack for format arguments. - * @param fmt The format string. - * @param args The arguments for the format string. - */ - template - fn Print(std::format_string fmt, Args&&... args) -> void { - // Directly use std::print for unstyled output - std::print(fmt, std::forward(args)...); - } -} // namespace term - -/** - * @enum LogLevel - * @brief Represents different log levels. - */ -enum class LogLevel : u8 { DEBUG, INFO, WARN, ERROR }; - -/** - * @brief Logs a message with the specified log level, source location, and format string. - * @tparam Args Parameter pack for format arguments. - * @param level The log level (DEBUG, INFO, WARN, ERROR). - * @param loc The source location of the log message. - * @param fmt The format string. - * @param args The arguments for the format string. - */ -template -fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_string fmt, Args&&... args) { - using namespace std::chrono; - using namespace term; - -#ifdef _MSC_VER - using enum term::Color; -#else - using enum Color; -#endif // _MSC_VER - - const auto [color, levelStr] = [&] { - switch (level) { - case LogLevel::DEBUG: return std::make_pair(Cyan, "DEBUG"); - case LogLevel::INFO: return std::make_pair(Green, "INFO "); - case LogLevel::WARN: return std::make_pair(Yellow, "WARN "); - case LogLevel::ERROR: return std::make_pair(Red, "ERROR"); - default: std::unreachable(); - } - }(); - - Print(BrightWhite, "[{:%X}] ", std::chrono::floor(system_clock::now())); - Print(Emphasis::Bold | color, "{} ", levelStr); - Print(fmt, std::forward(args)...); - -#ifndef NDEBUG - Print(BrightWhite, "\n{:>14} ", "╰──"); - Print( - Emphasis::Italic | BrightWhite, - "{}:{}", - std::filesystem::path(loc.file_name()).lexically_normal().string(), - loc.line() - ); -#endif // !NDEBUG - - Print("\n"); -} - -namespace detail { - template - fn LogAppError(const LogLevel level, const ErrorType& error_obj) { - using DecayedErrorType = std::decay_t; - - std::source_location log_location = std::source_location::current(); - String error_message_part; - LogLevel final_log_level = level; - - if constexpr (std::is_same_v) { - log_location = error_obj.location; - error_message_part = error_obj.message; - - } else if constexpr (std::is_same_v) { - if (std::holds_alternative(error_obj)) { - const OsError& osErr = std::get(error_obj); - log_location = osErr.location; - error_message_part = osErr.message; - } else if (std::holds_alternative(error_obj)) { - const NowPlayingCode npCode = std::get(error_obj); - log_location = std::source_location::current(); - final_log_level = LogLevel::DEBUG; - switch (npCode) { - case NowPlayingCode::NoPlayers: error_message_part = "No media players found"; break; - case NowPlayingCode::NoActivePlayer: error_message_part = "No active media player found"; break; - default: error_message_part = "Unknown NowPlayingCode"; break; - } - } - } else { - log_location = std::source_location::current(); - if constexpr (std::is_base_of_v) - error_message_part = error_obj.what(); - else if constexpr (requires { error_obj.message; }) - error_message_part = error_obj.message; - else - error_message_part = "Unknown error type logged"; - } - - LogImpl(final_log_level, log_location, "{}", error_message_part); - } -} // namespace detail - -// Suppress unused macro warnings in Clang -#ifdef __clang__ - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wunused-macros" -#endif // __clang__ - -#ifdef NDEBUG - #define DEBUG_LOG(...) static_cast(0) - #define DEBUG_LOG_LOC(...) static_cast(0) -#else - /** - * @def DEBUG_LOG - * @brief Logs a message at the DEBUG level. - * @details Only active in non-release builds (when NDEBUG is not defined). - * Includes timestamp, level, message, and source location. - * @param ... Format string and arguments for the log message. - */ - #define DEBUG_LOG(...) LogImpl(LogLevel::DEBUG, std::source_location::current(), __VA_ARGS__) - /** - * @def DEBUG_LOG_LOC(error_obj) - * @brief Logs an application-specific error at the DEBUG level, using its stored location if available. - * @details Only active in non-release builds (when NDEBUG is not defined). - * @param error_obj The error object (e.g., OsError, NowPlayingError). - */ - #define DEBUG_LOG_LOC(error_obj) \ - do { \ - [&](const auto& err) { detail::LogAppError(LogLevel::DEBUG, err); }(error_obj); \ - } while (0) -#endif // NDEBUG - -/** - * @def INFO_LOG(...) - * @brief Logs a message at the INFO level. - * @details Includes timestamp, level, message, and source location (in debug builds). - * @param ... Format string and arguments for the log message. - */ -#define INFO_LOG(...) LogImpl(LogLevel::INFO, std::source_location::current(), __VA_ARGS__) - -/** - * @def WARN_LOG(...) - * @brief Logs a message at the WARN level. - * @details Includes timestamp, level, message, and source location (in debug builds). - * @param ... Format string and arguments for the log message. - */ -#define WARN_LOG(...) LogImpl(LogLevel::WARN, std::source_location::current(), __VA_ARGS__) - -/** - * @def ERROR_LOG(...) - * @brief Logs a message at the ERROR level. - * @details Includes timestamp, level, message, and source location (in debug builds). - * @param ... Format string and arguments for the log message. - */ -#define ERROR_LOG(...) LogImpl(LogLevel::ERROR, std::source_location::current(), __VA_ARGS__) - -/** - * @def ERROR_LOG_LOC(error_obj) - * @brief Logs an application-specific error at the ERROR level, using its stored location if available. - * @param error_obj The error object (e.g., OsError, NowPlayingError). - */ -#define ERROR_LOG_LOC(error_obj) \ - do { \ - [&](const auto& err) { detail::LogAppError(LogLevel::ERROR, err); }(error_obj); \ - } while (0) - -#ifdef __clang__ - #pragma clang diagnostic pop -#endif // __clang__ diff --git a/src/util/types.h b/src/util/types.h deleted file mode 100644 index 40b4da9..0000000 --- a/src/util/types.h +++ /dev/null @@ -1,383 +0,0 @@ -#pragma once - -#include // std::array alias (Array) -#include // std::getenv, std::free -#include // std::expected alias (Result) -#include // std::format -#include // std::map alias (Map) -#include // std::shared_ptr and std::unique_ptr aliases (SharedPointer, UniquePointer) -#include // std::optional alias (Option) -#include // std::source_location -#include // std::string and std::string_view aliases (String, StringView) -#include // std::error_code and std::system_error -#include // std::pair alias (Pair) -#include // std::variant alias (NowPlayingError) -#include // std::vector alias (Vec) - -#ifdef _WIN32 - #include // winrt::hresult_error -#elifdef __linux__ - #include // DBus::Error -#endif - -//----------------------------------------------------------------// -// Integer Type Aliases // -// Provides concise names for standard fixed-width integer types. // -//----------------------------------------------------------------// - -using u8 = std::uint8_t; ///< 8-bit unsigned integer. -using u16 = std::uint16_t; ///< 16-bit unsigned integer. -using u32 = std::uint32_t; ///< 32-bit unsigned integer. -using u64 = std::uint64_t; ///< 64-bit unsigned integer. - -using i8 = std::int8_t; ///< 8-bit signed integer. -using i16 = std::int16_t; ///< 16-bit signed integer. -using i32 = std::int32_t; ///< 32-bit signed integer. -using i64 = std::int64_t; ///< 64-bit signed integer. - -//-----------------------------------------------------------// -// Floating-Point Type Aliases // -// Provides concise names for standard floating-point types. // -//-----------------------------------------------------------// - -using f32 = float; ///< 32-bit floating-point number. -using f64 = double; ///< 64-bit floating-point number. - -//-------------------------------------------------// -// Size Type Aliases // -// Provides concise names for standard size types. // -//-------------------------------------------------// - -using usize = std::size_t; ///< Unsigned size type (result of sizeof). -using isize = std::ptrdiff_t; ///< Signed size type (result of pointer subtraction). - -//---------------------------------------------------// -// String Type Aliases // -// Provides concise names for standard string types. // -//---------------------------------------------------// - -using String = std::string; ///< Owning, mutable string. -using StringView = std::string_view; ///< Non-owning view of a string. -using CStr = const char*; ///< Pointer to a null-terminated C-style string. - -//----------------------------------------------------// -// Standard Library Type Aliases // -// Provides concise names for standard library types. // -//----------------------------------------------------// - -using Exception = std::exception; ///< Standard exception type. - -/** - * @typedef Result - * @brief Alias for std::expected. Represents a value that can either be - * a success value of type Tp or an error value of type Er. - * @tparam Tp The type of the success value. - * @tparam Er The type of the error value. - */ -template -using Result = std::expected; - -/** - * @typedef Err - * @brief Alias for std::unexpected. Used to construct a Result in an error state. - * @tparam Er The type of the error value. - */ -template -using Err = std::unexpected; - -/** - * @typedef Option - * @brief Alias for std::optional. Represents a value that may or may not be present. - * @tparam Tp The type of the potential value. - */ -template -using Option = std::optional; - -/** - * @typedef Array - * @brief Alias for std::array. Represents a fixed-size array. - * @tparam Tp The element type. - * @tparam sz The size of the array. - */ -template -using Array = std::array; - -/** - * @typedef Vec - * @brief Alias for std::vector. Represents a dynamic-size array (vector). - * @tparam Tp The element type. - */ -template -using Vec = std::vector; - -/** - * @typedef Pair - * @brief Alias for std::pair. Represents a pair of values. - * @tparam T1 The type of the first element. - * @tparam T2 The type of the second element. - */ -template -using Pair = std::pair; - -/** - * @typedef Map - * @brief Alias for std::map. Represents an ordered map (dictionary). - * @tparam Key The key type. - * @tparam Val The value type. - */ -template -using Map = std::map; - -/** - * @typedef SharedPointer - * @brief Alias for std::shared_ptr. Manages shared ownership of a dynamically allocated object. - * @tparam Tp The type of the managed object. - */ -template -using SharedPointer = std::shared_ptr; - -/** - * @typedef UniquePointer - * @brief Alias for std::unique_ptr. Manages unique ownership of a dynamically allocated object. - * @tparam Tp The type of the managed object. - * @tparam Dp The deleter type (defaults to std::default_delete). - */ -template > -using UniquePointer = std::unique_ptr; - -//--------------------------------------------------------// -// Application-Specific Type Aliases // -// Provides concise names for application-specific types. // -//--------------------------------------------------------// - -/** - * @enum NowPlayingCode - * @brief Error codes specific to the Now Playing feature. - */ -enum class NowPlayingCode : u8 { - NoPlayers, ///< No media players were found (e.g., no MPRIS services on Linux). - NoActivePlayer, ///< Players were found, but none are currently active or playing. -}; - -/** - * @enum OsErrorCode - * @brief Error codes for general OS-level operations. - */ -enum class OsErrorCode : u8 { - IoError, ///< General I/O error (filesystem, pipes, etc.). - PermissionDenied, ///< Insufficient permissions to perform the operation. - NotFound, ///< A required resource (file, registry key, device, API endpoint) was not found. - ParseError, ///< Failed to parse data obtained from the OS (e.g., file content, API output). - ApiUnavailable, ///< A required OS service/API is unavailable or failed unexpectedly at runtime. - NotSupported, ///< The requested operation is not supported on this platform, version, or configuration. - Timeout, ///< An operation timed out (e.g., waiting for IPC reply). - BufferTooSmall, ///< Optional: Keep if using fixed C-style buffers, otherwise remove. - InternalError, ///< An error occurred within the application's OS abstraction code logic. - NetworkError, ///< A network-related error occurred (e.g., DNS resolution, connection failure). - PlatformSpecific, ///< An unmapped error specific to the underlying OS platform occurred (check message). - Other, ///< A generic or unclassified error originating from the OS or an external library. -}; - -/** - * @struct OsError - * @brief Holds structured information about an OS-level error. - * - * Used as the error type in Result for many os:: functions. - */ -struct OsError { - // ReSharper disable CppDFANotInitializedField - String message; ///< A descriptive error message, potentially including platform details. - OsErrorCode code; ///< The general category of the error. - std::source_location location; ///< The source location where the error occurred (file, line, function). - // ReSharper restore CppDFANotInitializedField - - OsError(const OsErrorCode errc, String msg, const std::source_location& loc = std::source_location::current()) - : message(std::move(msg)), code(errc), location(loc) {} - - explicit OsError(const Exception& exc, const std::source_location& loc = std::source_location::current()) - : message(exc.what()), code(OsErrorCode::InternalError), location(loc) {} - - explicit OsError(const std::error_code& errc, const std::source_location& loc = std::source_location::current()) - : message(errc.message()), location(loc) { - using enum OsErrorCode; - using enum std::errc; - - switch (static_cast(errc.value())) { - case permission_denied: code = PermissionDenied; break; - case no_such_file_or_directory: code = NotFound; break; - case timed_out: code = Timeout; break; - case io_error: code = IoError; break; - case network_unreachable: - case network_down: - case connection_refused: code = NetworkError; break; - case not_supported: code = NotSupported; break; - default: code = errc.category() == std::generic_category() ? InternalError : PlatformSpecific; break; - } - } -#ifdef _WIN32 - explicit OsError(const winrt::hresult_error& e) : message(winrt::to_string(e.message())) { - switch (e.code()) { - case E_ACCESSDENIED: code = OsErrorCode::PermissionDenied; break; - case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND): - case HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND): - case HRESULT_FROM_WIN32(ERROR_SERVICE_NOT_FOUND): code = OsErrorCode::NotFound; break; - case HRESULT_FROM_WIN32(ERROR_TIMEOUT): - case HRESULT_FROM_WIN32(ERROR_SEM_TIMEOUT): code = OsErrorCode::Timeout; break; - case HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED): code = OsErrorCode::NotSupported; break; - default: code = OsErrorCode::PlatformSpecific; break; - } - } -#else - OsError(const OsErrorCode code_hint, const int errno_val) - : message(std::system_category().message(errno_val)), code(code_hint) { - using enum OsErrorCode; - - switch (errno_val) { - case EACCES: code = PermissionDenied; break; - case ENOENT: code = NotFound; break; - case ETIMEDOUT: code = Timeout; break; - case ENOTSUP: code = NotSupported; break; - default: code = PlatformSpecific; break; - } - } - - static auto withErrno(const String& context, const std::source_location& loc = std::source_location::current()) - -> OsError { - const i32 errNo = errno; - const String msg = std::system_category().message(errNo); - const String fullMsg = std::format("{}: {}", context, msg); - - OsErrorCode code; - switch (errNo) { - case EACCES: - case EPERM: code = OsErrorCode::PermissionDenied; break; - case ENOENT: code = OsErrorCode::NotFound; break; - case ETIMEDOUT: code = OsErrorCode::Timeout; break; - case ENOTSUP: code = OsErrorCode::NotSupported; break; - case EIO: code = OsErrorCode::IoError; break; - case ECONNREFUSED: - case ENETDOWN: - case ENETUNREACH: code = OsErrorCode::NetworkError; break; - default: code = OsErrorCode::PlatformSpecific; break; - } - - return OsError { code, fullMsg, loc }; - } - - #ifdef __linux__ - static auto fromDBus(const DBus::Error& err, const std::source_location& loc = std::source_location::current()) - -> OsError { - String name = err.name(); - OsErrorCode codeHint = OsErrorCode::PlatformSpecific; - String message; - - using namespace std::string_view_literals; - - if (name == "org.freedesktop.DBus.Error.ServiceUnknown"sv || - name == "org.freedesktop.DBus.Error.NameHasNoOwner"sv) { - codeHint = OsErrorCode::NotFound; - message = std::format("DBus service/name not found: {}", err.message()); - } else if (name == "org.freedesktop.DBus.Error.NoReply"sv || name == "org.freedesktop.DBus.Error.Timeout"sv) { - codeHint = OsErrorCode::Timeout; - message = std::format("DBus timeout/no reply: {}", err.message()); - } else if (name == "org.freedesktop.DBus.Error.AccessDenied"sv) { - codeHint = OsErrorCode::PermissionDenied; - message = std::format("DBus access denied: {}", err.message()); - } else { - message = std::format("DBus error: {} - {}", name, err.message()); - } - - return OsError { codeHint, message, loc }; - } - #endif -#endif -}; - -/** - * @struct DiskSpace - * @brief Represents disk usage information. - * - * Used as the success type for os::GetDiskUsage. - */ -struct DiskSpace { - u64 used_bytes; ///< Currently used disk space in bytes. - u64 total_bytes; ///< Total disk space in bytes. -}; - -/** - * @struct MediaInfo - * @brief Holds structured metadata about currently playing media. - * - * Used as the success type for os::GetNowPlaying. - * Using Option<> for fields that might not always be available. - */ -struct MediaInfo { - Option title; ///< Track title. - Option artist; ///< Track artist(s). - Option album; ///< Album name. - Option app_name; ///< Name of the media player application (e.g., "Spotify", "Firefox"). - - MediaInfo() = default; - - MediaInfo(Option title, Option artist) : title(std::move(title)), artist(std::move(artist)) {} - - MediaInfo(Option title, Option artist, Option album, Option app) - : title(std::move(title)), artist(std::move(artist)), album(std::move(album)), app_name(std::move(app)) {} -}; - -//--------------------------------------------------------// -// Potentially Update Existing Application-Specific Types // -//--------------------------------------------------------// - -/** - * @typedef NowPlayingError (Updated Recommendation) - * @brief Represents the possible errors returned by os::GetNowPlaying. - * - * It's a variant that can hold either a specific NowPlayingCode - * (indicating player state like 'no active player') or a general OsError - * (indicating an underlying system/API failure). - */ -using NowPlayingError = std::variant; - -/** - * @enum EnvError - * @brief Error codes for environment variable retrieval. - */ -enum class EnvError : u8 { - NotFound, ///< Environment variable not found. - AccessError, ///< Access error when trying to retrieve the variable. -}; - -/** - * @brief Safely retrieves an environment variable. - * @param name The name of the environment variable to retrieve. - * @return A Result containing the value of the environment variable as a String, - * or an EnvError if an error occurred. - */ -[[nodiscard]] inline auto GetEnv(CStr name) -> Result { -#ifdef _WIN32 - char* rawPtr = nullptr; - usize bufferSize = 0; - - // Use _dupenv_s to safely retrieve environment variables on Windows - const i32 err = _dupenv_s(&rawPtr, &bufferSize, name); - - const UniquePointer ptrManager(rawPtr, free); - - if (err != 0) - return Err(EnvError::AccessError); // Error retrieving environment variable - - if (!ptrManager) - return Err(EnvError::NotFound); // Environment variable not found - - return ptrManager.get(); -#else - // Use std::getenv to retrieve environment variables on POSIX systems - const CStr value = std::getenv(name); - - if (!value) - return Err(EnvError::NotFound); // Environment variable not found - - return value; -#endif -} diff --git a/src/wrappers/wayland.hpp b/src/wrappers/wayland.hpp new file mode 100644 index 0000000..d3b1f46 --- /dev/null +++ b/src/wrappers/wayland.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include "src/core/util/defs.hpp" +#include "src/core/util/types.hpp" + +struct wl_display; + +namespace wl { + using display = wl_display; + + // NOLINTBEGIN(readability-identifier-naming) + inline fn connect(const char* name) -> display* { return wl_display_connect(name); } + inline fn disconnect(display* display) -> void { wl_display_disconnect(display); } + inline fn get_fd(display* display) -> int { return wl_display_get_fd(display); } + // NOLINTEND(readability-identifier-naming) + + /** + * RAII wrapper for Wayland display connections + * Automatically handles resource acquisition and cleanup + */ + class DisplayGuard { + display* m_Display; + + public: + /** + * Opens a Wayland display connection + */ + DisplayGuard() : m_Display(connect(nullptr)) {} + ~DisplayGuard() { + if (m_Display) + disconnect(m_Display); + } + + // Non-copyable + DisplayGuard(const DisplayGuard&) = delete; + fn operator=(const DisplayGuard&)->DisplayGuard& = delete; + + // Movable + DisplayGuard(DisplayGuard&& other) noexcept : m_Display(std::exchange(other.m_Display, nullptr)) {} + fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& { + if (this != &other) { + if (m_Display) + disconnect(m_Display); + + m_Display = std::exchange(other.m_Display, nullptr); + } + + return *this; + } + + [[nodiscard]] explicit operator bool() const { return m_Display != nullptr; } + + [[nodiscard]] fn get() const -> display* { return m_Display; } + [[nodiscard]] fn fd() const -> util::types::i32 { return get_fd(m_Display); } + }; +} // namespace wl diff --git a/src/wrappers/xcb.hpp b/src/wrappers/xcb.hpp new file mode 100644 index 0000000..86f81ae --- /dev/null +++ b/src/wrappers/xcb.hpp @@ -0,0 +1,168 @@ +#pragma once + +#include + +#include "src/core/util/defs.hpp" +#include "src/core/util/types.hpp" + +namespace xcb { + using util::types::u8, util::types::i32, util::types::CStr; + + using connection_t = xcb_connection_t; + using setup_t = xcb_setup_t; + using screen_t = xcb_screen_t; + using window_t = xcb_window_t; + using atom_t = xcb_atom_t; + + using generic_error_t = xcb_generic_error_t; + using intern_atom_cookie_t = xcb_intern_atom_cookie_t; + using intern_atom_reply_t = xcb_intern_atom_reply_t; + using get_property_cookie_t = xcb_get_property_cookie_t; + using get_property_reply_t = xcb_get_property_reply_t; + + constexpr atom_t ATOM_WINDOW = XCB_ATOM_WINDOW; + + enum ConnError : u8 { + Generic = XCB_CONN_ERROR, + ExtNotSupported = XCB_CONN_CLOSED_EXT_NOTSUPPORTED, + MemInsufficient = XCB_CONN_CLOSED_MEM_INSUFFICIENT, + ReqLenExceed = XCB_CONN_CLOSED_REQ_LEN_EXCEED, + ParseErr = XCB_CONN_CLOSED_PARSE_ERR, + InvalidScreen = XCB_CONN_CLOSED_INVALID_SCREEN, + FdPassingFailed = XCB_CONN_CLOSED_FDPASSING_FAILED + }; + + // NOLINTBEGIN(readability-identifier-naming) + inline fn getConnError(const util::types::i32 code) -> util::types::Option { + switch (code) { + case XCB_CONN_ERROR: return Generic; + case XCB_CONN_CLOSED_EXT_NOTSUPPORTED: return ExtNotSupported; + case XCB_CONN_CLOSED_MEM_INSUFFICIENT: return MemInsufficient; + case XCB_CONN_CLOSED_REQ_LEN_EXCEED: return ReqLenExceed; + case XCB_CONN_CLOSED_PARSE_ERR: return ParseErr; + case XCB_CONN_CLOSED_INVALID_SCREEN: return InvalidScreen; + case XCB_CONN_CLOSED_FDPASSING_FAILED: return FdPassingFailed; + default: return None; + } + } + + inline fn connect(const char* displayname, int* screenp) -> connection_t* { + return xcb_connect(displayname, screenp); + } + inline fn disconnect(connection_t* conn) -> void { xcb_disconnect(conn); } + inline fn connection_has_error(connection_t* conn) -> int { return xcb_connection_has_error(conn); } + inline fn intern_atom(connection_t* conn, const uint8_t only_if_exists, const uint16_t name_len, const char* name) + -> intern_atom_cookie_t { + return xcb_intern_atom(conn, only_if_exists, name_len, name); + } + inline fn intern_atom_reply(connection_t* conn, const intern_atom_cookie_t cookie, generic_error_t** err) + -> intern_atom_reply_t* { + return xcb_intern_atom_reply(conn, cookie, err); + } + inline fn get_property( + connection_t* conn, + const uint8_t _delete, + const window_t window, + const atom_t property, + const atom_t type, + const uint32_t long_offset, + const uint32_t long_length + ) -> get_property_cookie_t { + return xcb_get_property(conn, _delete, window, property, type, long_offset, long_length); + } + inline fn get_property_reply(connection_t* conn, const get_property_cookie_t cookie, generic_error_t** err) + -> get_property_reply_t* { + return xcb_get_property_reply(conn, cookie, err); + } + inline fn get_property_value_length(const get_property_reply_t* reply) -> int { + return xcb_get_property_value_length(reply); + } + inline fn get_property_value(const get_property_reply_t* reply) -> void* { return xcb_get_property_value(reply); } + // NOLINTEND(readability-identifier-naming) + + /** + * RAII wrapper for X11 Display connections + * Automatically handles resource acquisition and cleanup + */ + class DisplayGuard { + connection_t* m_Connection = nullptr; + + public: + /** + * Opens an XCB connection + * @param name Display name (nullptr for default) + */ + explicit DisplayGuard(const util::types::CStr name = nullptr) : m_Connection(connect(name, nullptr)) {} + ~DisplayGuard() { + if (m_Connection) + disconnect(m_Connection); + } + + // Non-copyable + DisplayGuard(const DisplayGuard&) = delete; + fn operator=(const DisplayGuard&)->DisplayGuard& = delete; + + // Movable + DisplayGuard(DisplayGuard&& other) noexcept : m_Connection(std::exchange(other.m_Connection, nullptr)) {} + fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& { + if (this != &other) { + if (m_Connection) + disconnect(m_Connection); + + m_Connection = std::exchange(other.m_Connection, nullptr); + } + return *this; + } + + [[nodiscard]] explicit operator bool() const { return m_Connection && !connection_has_error(m_Connection); } + + [[nodiscard]] fn get() const -> connection_t* { return m_Connection; } + + [[nodiscard]] fn setup() const -> const setup_t* { return m_Connection ? xcb_get_setup(m_Connection) : nullptr; } + + [[nodiscard]] fn rootScreen() const -> screen_t* { + const setup_t* setup = this->setup(); + return setup ? xcb_setup_roots_iterator(setup).data : nullptr; + } + }; + + /** + * RAII wrapper for XCB replies + * Handles automatic cleanup of various XCB reply objects + */ + template + class ReplyGuard { + T* m_Reply = nullptr; + + public: + ReplyGuard() = default; + explicit ReplyGuard(T* reply) : m_Reply(reply) {} + + ~ReplyGuard() { + if (m_Reply) + free(m_Reply); + } + + // Non-copyable + ReplyGuard(const ReplyGuard&) = delete; + fn operator=(const ReplyGuard&)->ReplyGuard& = delete; + + // Movable + ReplyGuard(ReplyGuard&& other) noexcept : m_Reply(std::exchange(other.m_Reply, nullptr)) {} + fn operator=(ReplyGuard&& other) noexcept -> ReplyGuard& { + if (this != &other) { + if (m_Reply) + free(m_Reply); + + m_Reply = std::exchange(other.m_Reply, nullptr); + } + return *this; + } + + [[nodiscard]] explicit operator bool() const { return m_Reply != nullptr; } + + [[nodiscard]] fn get() const -> T* { return m_Reply; } + [[nodiscard]] fn operator->() const->T* { return m_Reply; } + [[nodiscard]] fn operator*() const->T& { return *m_Reply; } + }; +} // namespace xcb