diff --git a/.clang-tidy b/.clang-tidy index 12b2afc..87e4d6b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -29,24 +29,24 @@ Checks: > -readability-isolate-declaration, -readability-magic-numbers CheckOptions: - cppcoreguidelines-avoid-do-while.IgnoreMacros: "true" + cppcoreguidelines-avoid-do-while.IgnoreMacros: true readability-else-after-return.WarnOnUnfixable: false readability-identifier-naming.ClassCase: CamelCase readability-identifier-naming.EnumCase: CamelCase readability-identifier-naming.LocalConstantCase: camelBack readability-identifier-naming.LocalVariableCase: camelBack readability-identifier-naming.GlobalFunctionCase: CamelCase - readability-identifier-naming.MemberCase: lower_case + readability-identifier-naming.MemberCase: camelBack readability-identifier-naming.MethodCase: camelBack readability-identifier-naming.MethodIgnoredRegexp: ((to|from)_class) readability-identifier-naming.ParameterPackCase: lower_case - readability-identifier-naming.PrivateMemberCase: CamelCase + readability-identifier-naming.PrivateMemberCase: camelBack readability-identifier-naming.PrivateMemberPrefix: 'm_' - readability-identifier-naming.PrivateMethodCase: CamelCase + readability-identifier-naming.PrivateMethodCase: camelBack readability-identifier-naming.PrivateMethodPrefix: '' readability-identifier-naming.ProtectedMemberPrefix: 'm_' readability-identifier-naming.ProtectedMethodPrefix: '' - readability-identifier-naming.PublicMemberCase: lower_case + readability-identifier-naming.PublicMemberCase: camelBack readability-identifier-naming.StaticConstantCase: UPPER_CASE readability-identifier-naming.StaticVariableCase: CamelCase readability-identifier-naming.StructCase: CamelCase diff --git a/_sources/generated.json b/_sources/generated.json deleted file mode 100644 index def3137..0000000 --- a/_sources/generated.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "dbus-cxx": { - "cargoLocks": null, - "date": null, - "extract": null, - "name": "dbus-cxx", - "passthru": null, - "pinned": false, - "src": { - "deepClone": false, - "fetchSubmodules": false, - "leaveDotGit": false, - "name": null, - "owner": "dbus-cxx", - "repo": "dbus-cxx", - "rev": "2.5.2", - "sha256": "sha256-if/9XIsf3an5Sij91UIIyNB3vlFAcKrm6YT5Mk7NhB0=", - "sparseCheckout": [], - "type": "github" - }, - "version": "2.5.2" - } -} \ No newline at end of file diff --git a/_sources/generated.nix b/_sources/generated.nix deleted file mode 100644 index 6cb27da..0000000 --- a/_sources/generated.nix +++ /dev/null @@ -1,15 +0,0 @@ -# This file was generated by nvfetcher, please do not modify it manually. -{ fetchgit, fetchurl, fetchFromGitHub, dockerTools }: -{ - dbus-cxx = { - pname = "dbus-cxx"; - version = "2.5.2"; - src = fetchFromGitHub { - owner = "dbus-cxx"; - repo = "dbus-cxx"; - rev = "2.5.2"; - fetchSubmodules = false; - sha256 = "sha256-if/9XIsf3an5Sij91UIIyNB3vlFAcKrm6YT5Mk7NhB0="; - }; - }; -} diff --git a/flake.lock b/flake.lock index eb8e8a7..8d188e4 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1745377448, - "narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=", + "lastModified": 1745998881, + "narHash": "sha256-vonyYAKJSlsX4n9GCsS0pHxR6yCrfqBIuGvANlkwG6U=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c", + "rev": "423d2df5b04b4ee7688c3d71396e872afa236a89", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1735554305, - "narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=", + "lastModified": 1745377448, + "narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd", + "rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c", "type": "github" }, "original": { @@ -59,11 +59,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1744961264, - "narHash": "sha256-aRmUh0AMwcbdjJHnytg1e5h5ECcaWtIFQa6d9gI85AI=", + "lastModified": 1745929750, + "narHash": "sha256-k5ELLpTwRP/OElcLpNaFWLNf8GRDq4/eHBmFy06gGko=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "8d404a69efe76146368885110f29a2ca3700bee6", + "rev": "82bf32e541b30080d94e46af13d46da0708609ea", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 45f412c..8250101 100644 --- a/flake.nix +++ b/flake.nix @@ -18,7 +18,7 @@ system: if system == "x86_64-linux" then let - hostPkgs = import nixpkgs {inherit system;}; + pkgs = import nixpkgs {inherit system;}; muslPkgs = import nixpkgs { system = "x86_64-linux-musl"; overlays = [ @@ -63,15 +63,15 @@ then ["-Ddefault_library=static"] else if buildSystem == "cmake" then [ - "-D${hostPkgs.lib.toUpper pkg.pname}_BUILD_EXAMPLES=OFF" - "-D${hostPkgs.lib.toUpper pkg.pname}_BUILD_TESTS=OFF" + "-D${pkgs.lib.toUpper pkg.pname}_BUILD_EXAMPLES=OFF" + "-D${pkgs.lib.toUpper pkg.pname}_BUILD_TESTS=OFF" "-DBUILD_SHARED_LIBS=OFF" ] else throw "Invalid build system: ${buildSystem}" ); })); - deps = with hostPkgs.pkgsStatic; [ + deps = with pkgs.pkgsStatic; [ curlMinimal dbus glaze @@ -85,7 +85,6 @@ (mkOverridden "cmake" ftxui) (mkOverridden "cmake" sqlitecpp) - (mkOverridden "meson" libsigcxx30) (mkOverridden "meson" tomlplusplus) ]; in { @@ -94,7 +93,6 @@ name = "draconis++"; version = "0.1.0"; src = self; - NIX_ENFORCE_NO_NATIVE = 0; nativeBuildInputs = with muslPkgs; [ cmake @@ -123,29 +121,29 @@ devShell = muslPkgs.mkShell.override {inherit stdenv;} { packages = - (with hostPkgs; [bear cmake]) + (with pkgs; [bear cmake]) ++ (with muslPkgs; [ llvmPackages_20.clang-tools meson ninja pkg-config - (hostPkgs.writeScriptBin "build" "meson compile -C build") - (hostPkgs.writeScriptBin "clean" "meson setup build --wipe") - (hostPkgs.writeScriptBin "run" "meson compile -C build && build/draconis++") + (pkgs.writeScriptBin "build" "meson compile -C build") + (pkgs.writeScriptBin "clean" "meson setup build --wipe") + (pkgs.writeScriptBin "run" "meson compile -C build && build/draconis++") ]) ++ deps; NIX_ENFORCE_NO_NATIVE = 0; }; - formatter = treefmt-nix.lib.mkWrapper hostPkgs { + formatter = treefmt-nix.lib.mkWrapper pkgs { projectRootFile = "flake.nix"; programs = { alejandra.enable = true; deadnix.enable = true; clang-format = { enable = true; - package = hostPkgs.llvmPackages.clang-tools; + package = pkgs.llvmPackages.clang-tools; }; }; }; @@ -185,7 +183,6 @@ ] ++ (with pkgsStatic; [ dbus - libsigcxx30 sqlitecpp xorg.libxcb wayland diff --git a/meson.build b/meson.build index df37e9a..efda771 100644 --- a/meson.build +++ b/meson.build @@ -7,9 +7,10 @@ project( version : '0.1.0', default_options : [ 'default_library=static', - 'buildtype=debugoptimized', + 'buildtype=release', 'b_vscrt=mt', 'b_lto=true', + 'b_ndebug=if-release', 'warning_level=3', ], ) @@ -85,7 +86,7 @@ add_project_arguments(common_cpp_args, language : 'cpp') # ------- # # Files # # ------- # -base_sources = files('src/core/system_data.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', 'src/main.cpp') +base_sources = files('src/core/system_data.cpp', 'src/os/shared.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', 'src/main.cpp') platform_sources = { 'linux' : ['src/os/linux.cpp', 'src/os/linux/pkg_count.cpp'], @@ -128,7 +129,7 @@ elif host_system == 'linux' dependency('xau'), dependency('xdmcp'), dependency('wayland-client'), - dependency('dbus-1', include_type: 'system'), + dependency('dbus-1', include_type : 'system'), ] endif diff --git a/nvfetcher.toml b/nvfetcher.toml deleted file mode 100644 index 40bb694..0000000 --- a/nvfetcher.toml +++ /dev/null @@ -1,3 +0,0 @@ -[dbus-cxx] -src.github = "dbus-cxx/dbus-cxx" -fetch.github = "dbus-cxx/dbus-cxx" diff --git a/src/config/config.cpp b/src/config/config.cpp index a37becd..d4c6ae0 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -1,12 +1,19 @@ #include "config.hpp" -#include // std::filesystem::{path, operator/, exists, create_directories} -#include // std::{ifstream, ofstream, operator<<} -#include // std::error_code -#include // toml::{parse_file, parse_result} +#include // std::filesystem::{path, operator/, exists, create_directories} +#include // std::{format, format_error} +#include // std::{ifstream, ofstream, operator<<} +#include // passwd, getpwuid +#include // std::error_code +#include // toml::node_view +#include // toml::{parse_file, parse_result} +#include // toml::table +#include // getuid +#include "src/core/util/defs.hpp" #include "src/core/util/helpers.hpp" #include "src/core/util/logging.hpp" +#include "src/core/util/types.hpp" namespace fs = std::filesystem; @@ -42,21 +49,21 @@ location = "London" # Your city name Vec possiblePaths; #ifdef _WIN32 - if (auto result = GetEnv("LOCALAPPDATA")) + if (Result result = GetEnv("LOCALAPPDATA")) possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml"); - if (auto result = GetEnv("USERPROFILE")) { + if (Result result = GetEnv("USERPROFILE")) { possiblePaths.push_back(fs::path(*result) / ".config" / "draconis++" / "config.toml"); possiblePaths.push_back(fs::path(*result) / "AppData" / "Local" / "draconis++" / "config.toml"); } - if (auto result = GetEnv("APPDATA")) + if (Result result = GetEnv("APPDATA")) possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml"); #else - if (Result result = GetEnv("XDG_CONFIG_HOME")) + if (Result result = GetEnv("XDG_CONFIG_HOME")) possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml"); - if (Result result = GetEnv("HOME")) { + if (Result result = GetEnv("HOME")) { possiblePaths.emplace_back(fs::path(*result) / ".config" / "draconis++" / "config.toml"); possiblePaths.emplace_back(fs::path(*result) / ".draconis++" / "config.toml"); } @@ -111,8 +118,8 @@ location = "London" # Your city name const passwd* pwd = getpwuid(getuid()); CStr pwdName = pwd ? pwd->pw_name : nullptr; - const Result envUser = util::helpers::GetEnv("USER"); - const Result envLogname = util::helpers::GetEnv("LOGNAME"); + const Result envUser = util::helpers::GetEnv("USER"); + const Result envLogname = util::helpers::GetEnv("LOGNAME"); defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User"; #endif @@ -124,13 +131,13 @@ location = "London" # Your city name } try { - const std::string formattedConfig = std::format(defaultConfigTemplate, defaultName); + const String formattedConfig = std::format(defaultConfigTemplate, defaultName); file << formattedConfig; } catch (const std::format_error& fmtErr) { error_log("Failed to format default config string: {}. Using fallback name 'User'.", fmtErr.what()); try { - const std::string fallbackConfig = std::format(defaultConfigTemplate, "User"); + const String fallbackConfig = std::format(defaultConfigTemplate, "User"); file << fallbackConfig; } catch (...) { error_log("Failed to format default config even with fallback name."); @@ -163,9 +170,9 @@ Config::Config(const toml::table& tbl) { const toml::node_view npTbl = tbl["now_playing"]; const toml::node_view wthTbl = tbl["weather"]; - this->general = genTbl.is_table() ? General::fromToml(*genTbl.as_table()) : General {}; - this->now_playing = npTbl.is_table() ? NowPlaying::fromToml(*npTbl.as_table()) : NowPlaying {}; - this->weather = wthTbl.is_table() ? Weather::fromToml(*wthTbl.as_table()) : Weather {}; + this->general = genTbl.is_table() ? General::fromToml(*genTbl.as_table()) : General {}; + this->nowPlaying = npTbl.is_table() ? NowPlaying::fromToml(*npTbl.as_table()) : NowPlaying {}; + this->weather = wthTbl.is_table() ? Weather::fromToml(*wthTbl.as_table()) : Weather {}; } fn Config::getInstance() -> Config { diff --git a/src/config/config.hpp b/src/config/config.hpp index 135473f..85d04ee 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -22,7 +22,7 @@ #include "weather.hpp" -using util::error::DraconisError; +using util::error::DracError; 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). @@ -58,11 +58,11 @@ struct General { return pwd->pw_name; // Try to get the username using environment variables - if (Result envUser = GetEnv("USER")) + if (Result envUser = GetEnv("USER")) return *envUser; // Finally, try to get the username using LOGNAME - if (Result envLogname = GetEnv("LOGNAME")) + if (Result envLogname = GetEnv("LOGNAME")) return *envLogname; // If all else fails, return a default name @@ -102,11 +102,11 @@ struct NowPlaying { */ struct Weather { Location location; ///< Location for weather data, can be a city name or coordinates. - String api_key; ///< API key for the weather service. + String apiKey; ///< API key for the weather service. String units; ///< Units for temperature, either "metric" or "imperial". - bool enabled = false; ///< Flag to enable or disable the Weather feature. - bool show_town_name = false; ///< Flag to show the town name in the output. + bool enabled = false; ///< Flag to enable or disable the Weather feature. + bool showTownName = false; ///< Flag to show the town name in the output. /** * @brief Parses a TOML table to create a Weather instance. @@ -123,9 +123,9 @@ struct Weather { if (!weather.enabled) return weather; - weather.api_key = *apiKey; - weather.show_town_name = tbl["show_town_name"].value_or(false); - weather.units = tbl["units"].value_or("metric"); + weather.apiKey = *apiKey; + weather.showTownName = tbl["show_town_name"].value_or(false); + weather.units = tbl["units"].value_or("metric"); if (const toml::node_view location = tbl["location"]) { if (location.is_string()) @@ -152,7 +152,7 @@ struct Weather { * API key, and units. It returns a WeatherOutput object containing the * retrieved weather data. */ - [[nodiscard]] fn getWeatherInfo() const -> weather::Output; + [[nodiscard]] fn getWeatherInfo() const -> Result; }; /** @@ -160,9 +160,9 @@ struct Weather { * @brief Holds the application configuration settings. */ struct Config { - General general; ///< General configuration settings. - Weather weather; ///< Weather configuration settings.` - NowPlaying now_playing; ///< Now Playing configuration settings. + General general; ///< General configuration settings. + Weather weather; ///< Weather configuration settings.` + NowPlaying nowPlaying; ///< Now Playing configuration settings. Config() = default; diff --git a/src/config/weather.cpp b/src/config/weather.cpp index 027a8d8..e2bf73a 100644 --- a/src/config/weather.cpp +++ b/src/config/weather.cpp @@ -1,7 +1,8 @@ #include "weather.hpp" #include // std::chrono::{duration, operator-} -#include // curl_easy_init, curl_easy_setopt, curl_easy_perform, curl_easy_cleanup +#include // curl_easy_setopt +#include // curl_easy_init, curl_easy_perform, curl_easy_cleanup #include // std::{expected (Result), unexpected (Err)} #include // std::filesystem::{path, remove, rename} #include // std::format @@ -9,14 +10,17 @@ #include // glz::read_beve #include // glz::write_beve #include // glz::{error_ctx, error_code} +#include // glz::opts #include // glz::format_error -#include // glz::write_json -#include // std::istreambuf_iterator -#include // std::error_code -#include // std::move -#include // std::{get, holds_alternative} +#include // NOLINT(misc-include-cleaner) - glaze/json/read.hpp is needed for glz::read +#include // std::ios::{binary, trunc} +#include // std::istreambuf_iterator +#include // std::error_code +#include // std::move +#include // std::{get, holds_alternative} #include "src/core/util/defs.hpp" +#include "src/core/util/error.hpp" #include "src/core/util/logging.hpp" #include "src/core/util/types.hpp" @@ -28,6 +32,7 @@ using weather::Output; namespace { using glz::opts, glz::error_ctx, glz::error_code, glz::read, glz::read_beve, glz::write_beve, glz::format_error; + using util::error::DracError, util::error::DracErrorCode; using util::types::usize, util::types::Err, util::types::Exception; using weather::Coords; @@ -67,11 +72,9 @@ namespace { Output result; if (const error_ctx glazeErr = read_beve(result, content); glazeErr.ec != error_code::none) - return Err( - std::format( - "BEVE parse error reading cache (code {}): {}", static_cast(glazeErr.ec), cachePath->string() - ) - ); + return Err(std::format( + "BEVE parse error reading cache (code {}): {}", static_cast(glazeErr.ec), cachePath->string() + )); debug_log("Successfully read from cache file."); return result; @@ -170,7 +173,7 @@ namespace { } } // namespace -fn Weather::getWeatherInfo() const -> Output { +fn Weather::getWeatherInfo() const -> Result { using namespace std::chrono; using util::types::i32; @@ -178,7 +181,7 @@ fn Weather::getWeatherInfo() const -> Output { const Output& dataVal = *data; if (const duration cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt)); - cacheAge < 10min) { + cacheAge < 60min) { // NOLINT(misc-include-cleaner) - inherited from debug_log("Using valid cache"); return dataVal; } @@ -188,11 +191,9 @@ fn Weather::getWeatherInfo() const -> Output { debug_log("Cache error: {}", data.error()); } - fn handleApiResult = [](const Result& result) -> Output { - if (!result) { - error_log("API request failed: {}", result.error()); - return Output {}; - } + fn handleApiResult = [](const Result& result) -> Result { + if (!result) + return Err(DracError(DracErrorCode::ApiUnavailable, result.error())); if (Result writeResult = WriteCacheToFile(*result); !writeResult) error_log("Failed to write cache: {}", writeResult.error()); @@ -208,28 +209,23 @@ fn Weather::getWeatherInfo() const -> Output { 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); + std::format("https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units={}", escaped, apiKey, units); curl_free(escaped); + return handleApiResult(MakeApiRequest(apiUrl)); } if (std::holds_alternative(location)) { const auto& [lat, lon] = std::get(location); 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 + "https://api.openweathermap.org/data/2.5/weather?lat={:.3f}&lon={:.3f}&appid={}&units={}", lat, lon, apiKey, units ); + return handleApiResult(MakeApiRequest(apiUrl)); } -#ifdef __GLIBCXX__ - printf("Invalid location type in configuration. Expected String or Coords.\n"); -#endif - error_log("Invalid location type in configuration. Expected String or Coords."); - return Output {}; + return Err(DracError(DracErrorCode::ParseError, "Invalid location type in configuration.")); } diff --git a/src/core/system_data.cpp b/src/core/system_data.cpp index 9c1e3e0..fc7e09d 100644 --- a/src/core/system_data.cpp +++ b/src/core/system_data.cpp @@ -1,77 +1,55 @@ #include "system_data.hpp" -#include // std::chrono::{year_month_day, floor, days, system_clock} -#include // std::locale -#include // std::runtime_error +#include // std::chrono::{year_month_day, days, floor, system_clock} +#include // std::format +#include // std::async #include "src/config/config.hpp" +#include "src/config/weather.hpp" +#include "src/core/util/error.hpp" +#include "src/core/util/types.hpp" #include "src/os/os.hpp" -#include "util/logging.hpp" +namespace os { + SystemData::SystemData(const Config& config) { + // NOLINTNEXTLINE(misc-include-cleaner) - std::chrono::{days, floor} are inherited from + using std::chrono::year_month_day, std::chrono::system_clock, std::chrono::floor, std::chrono::days; + using util::error::DracError, util::error::DracErrorCode; + using util::types::Result, util::types::Err, util::types::Option, util::types::None, util::types::Exception, + util::types::Future; + using weather::Output; + using enum std::launch; -namespace { - fn GetDate() -> String { - using namespace std::chrono; + Future> hostFut = std::async(async, GetHost); + Future> kernelFut = std::async(async, GetKernelVersion); + Future> osFut = std::async(async, GetOSVersion); + Future> memFut = std::async(async, GetMemInfo); + Future> deFut = std::async(async, GetDesktopEnvironment); + Future> wmFut = std::async(async, GetWindowManager); + Future> diskFut = std::async(async, GetDiskUsage); + Future> shellFut = std::async(async, GetShell); + Future> pkgFut = std::async(async, GetPackageCount); + Future> npFut = + std::async(config.nowPlaying.enabled ? async : deferred, GetNowPlaying); + Future> wthrFut = + std::async(config.weather.enabled ? async : deferred, [&config] -> Result { + return config.weather.getWeatherInfo(); + }); - const year_month_day ymd = year_month_day { floor(system_clock::now()) }; - - return std::format("{:%B %d}", ymd); + this->date = std::format("{:%B %d}", year_month_day { floor(system_clock::now()) }); + this->host = hostFut.get(); + this->kernelVersion = kernelFut.get(); + this->osVersion = osFut.get(); + this->memInfo = memFut.get(); + this->desktopEnv = deFut.get(); + this->windowMgr = wmFut.get(); + this->diskUsage = diskFut.get(); + this->shell = shellFut.get(); + this->packageCount = pkgFut.get(); + this->weather = + config.weather.enabled ? wthrFut.get() : Err(DracError(DracErrorCode::ApiUnavailable, "Weather API disabled")); + this->nowPlaying = config.nowPlaying.enabled + ? npFut.get() + : Err(DracError(DracErrorCode::ApiUnavailable, "Now Playing API disabled")); } - - fn log_timing(const std::string& name, const std::chrono::steady_clock::duration& duration) -> void { - const auto millis = std::chrono::duration_cast>(duration); - debug_log("{} took: {} ms", name, millis.count()); - }; - - template - fn time_execution(const std::string& name, Func&& func) { - const auto start = std::chrono::steady_clock::now(); - if constexpr (std::is_void_v) { - func(); - - const auto end = std::chrono::steady_clock::now(); - - log_timing(name, end - start); - } else { - auto result = func(); - - const auto end = std::chrono::steady_clock::now(); - log_timing(name, end - start); - - return result; - } - } -} // namespace - -fn SystemData::fetchSystemData(const Config& config) -> SystemData { - using util::types::None, util::types::Exception; - using namespace os; - - SystemData data { - .date = time_execution("GetDate", GetDate), - .host = time_execution("GetHost", GetHost), - .kernel_version = time_execution("GetKernelVersion", GetKernelVersion), - .os_version = time_execution("GetOSVersion", GetOSVersion), - .mem_info = time_execution("GetMemInfo", GetMemInfo), - .desktop_environment = time_execution("GetDesktopEnvironment", GetDesktopEnvironment), - .window_manager = time_execution("GetWindowManager", GetWindowManager), - .disk_usage = time_execution("GetDiskUsage", GetDiskUsage), - .shell = time_execution("GetShell", GetShell), - .now_playing = None, - .weather_info = None, - }; - - if (const Result& nowPlayingResult = time_execution("GetNowPlaying", os::GetNowPlaying)) { - data.now_playing = nowPlayingResult; - } else { - data.now_playing = None; - } - - const auto start = std::chrono::steady_clock::now(); - data.weather_info = config.weather.getWeatherInfo(); - const auto end = std::chrono::steady_clock::now(); - - log_timing("config.weather.getWeatherInfo", end - start); - - return data; -} +} // namespace os diff --git a/src/core/system_data.hpp b/src/core/system_data.hpp index 6dfab8b..b099011 100644 --- a/src/core/system_data.hpp +++ b/src/core/system_data.hpp @@ -2,10 +2,10 @@ #include // std::{formatter, format_to} +#include "src/config/config.hpp" // Config #include "src/config/weather.hpp" // weather::Output #include "util/defs.hpp" -#include "util/error.hpp" #include "util/types.hpp" struct Config; @@ -58,34 +58,32 @@ struct std::formatter : std::formatter { } }; -/** - * @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 - +namespace os { /** - * @brief Fetches all system data asynchronously. - * @param config The application configuration. - * @return A populated SystemData object. + * @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. */ - static fn fetchSystemData(const Config& config) -> SystemData; -}; + struct SystemData { + String date; ///< Current date (e.g., "April 26"). Always expected to succeed. + Result host; ///< Host/product family (e.g., "MacBook Air") or OS util::erroror. + Result kernelVersion; ///< OS kernel version (e.g., "6.14.4") or OS error. + Result osVersion; ///< OS pretty name (e.g., "Ubuntu 24.04.2 LTS") or OS error. + Result memInfo; ///< Total physical RAM in bytes or OS error. + Result desktopEnv; ///< Desktop environment (e.g., "KDE") or None if not detected. + Result windowMgr; ///< Window manager (e.g., "KWin") or None if not detected. + Result diskUsage; ///< Used/Total disk space for root filesystem or OS error. + Result shell; ///< Name of the current user shell (e.g., "zsh"). None if not detected. + Result packageCount; ///< Total number of packages installed or OS error. + Result nowPlaying; ///< Result of fetching media info. + Result weather; ///< Result of fetching weather info. + + /** + * @brief Constructs a SystemData object and initializes its members. + * @param config The configuration object containing settings for the system data. + */ + explicit SystemData(const Config& config); + }; +} // namespace os diff --git a/src/core/util/error.hpp b/src/core/util/error.hpp index e733158..8625b3e 100644 --- a/src/core/util/error.hpp +++ b/src/core/util/error.hpp @@ -15,53 +15,47 @@ namespace util::error { using types::u8, types::i32, types::String, types::StringView, types::Exception; /** - * @enum DraconisErrorCode + * @enum DracErrorCode * @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). + enum class DracErrorCode : u8 { 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. + InvalidArgument, ///< An invalid argument was passed to a function or method. + IoError, ///< General I/O error (filesystem, pipes, etc.). 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). + NotFound, ///< A required resource (file, registry key, device, API endpoint) was not found. + NotSupported, ///< The requested operation is not supported on this platform, version, or configuration. Other, ///< A generic or unclassified error originating from the OS or an external library. + OutOfMemory, ///< The system ran out of memory or resources to complete the operation. + ParseError, ///< Failed to parse data obtained from the OS (e.g., file content, API output). + PermissionDenied, ///< Insufficient permissions to perform the operation. + PlatformSpecific, ///< An unmapped error specific to the underlying OS platform occurred (check message). + Timeout, ///< An operation timed out (e.g., waiting for IPC reply). }; /** - * @struct DraconisError + * @struct DracError * @brief Holds structured information about an OS-level error. * * Used as the error type in Result for many os:: functions. */ - struct DraconisError { + struct DracError { // ReSharper disable CppDFANotInitializedField String message; ///< A descriptive error message, potentially including platform details. - DraconisErrorCode code; ///< The general category of the error. + DracErrorCode 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() - ) + DracError(const DracErrorCode 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 DracError(const Exception& exc, const std::source_location& loc = std::source_location::current()) + : message(exc.what()), code(DracErrorCode::InternalError), location(loc) {} - explicit DraconisError( - const std::error_code& errc, - const std::source_location& loc = std::source_location::current() - ) + explicit DracError(const std::error_code& errc, const std::source_location& loc = std::source_location::current()) : message(errc.message()), location(loc) { - using enum DraconisErrorCode; + using enum DracErrorCode; using enum std::errc; switch (static_cast(errc.value())) { @@ -90,9 +84,9 @@ namespace util::error { } } #else - DraconisError(const DraconisErrorCode code_hint, const int errno_val) + DracError(const DracErrorCode code_hint, const int errno_val) : message(std::system_category().message(errno_val)), code(code_hint) { - using enum DraconisErrorCode; + using enum DracErrorCode; switch (errno_val) { case EACCES: code = PermissionDenied; break; @@ -104,26 +98,27 @@ namespace util::error { } static auto withErrno(const String& context, const std::source_location& loc = std::source_location::current()) - -> DraconisError { + -> DracError { 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; - } + const DracErrorCode code = [&errNo] { + switch (errNo) { + case EACCES: + case EPERM: return DracErrorCode::PermissionDenied; + case ENOENT: return DracErrorCode::NotFound; + case ETIMEDOUT: return DracErrorCode::Timeout; + case ENOTSUP: return DracErrorCode::NotSupported; + case EIO: return DracErrorCode::IoError; + case ECONNREFUSED: + case ENETDOWN: + case ENETUNREACH: return DracErrorCode::NetworkError; + default: return DracErrorCode::PlatformSpecific; + } + }(); - return DraconisError { code, fullMsg, loc }; + return DracError { code, fullMsg, loc }; } #endif }; diff --git a/src/core/util/helpers.hpp b/src/core/util/helpers.hpp index 000791e..3e8b1fc 100644 --- a/src/core/util/helpers.hpp +++ b/src/core/util/helpers.hpp @@ -5,7 +5,7 @@ #include "types.hpp" namespace util::helpers { - using error::DraconisError, error::DraconisErrorCode; + using error::DracError, error::DracErrorCode; using types::Result, types::String, types::CStr, types::Err; /** @@ -14,7 +14,7 @@ namespace util::helpers { * @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 { + [[nodiscard]] inline fn GetEnv(CStr name) -> Result { #ifdef _WIN32 using types::i32, types::usize, types::UniquePointer; @@ -38,7 +38,7 @@ namespace util::helpers { const CStr value = std::getenv(name); if (!value) - return Err(DraconisError(DraconisErrorCode::NotFound, "Environment variable not found")); + return Err(DracError(DracErrorCode::NotFound, "Environment variable not found")); return value; #endif diff --git a/src/core/util/logging.hpp b/src/core/util/logging.hpp index 1518dd5..cb8e93a 100644 --- a/src/core/util/logging.hpp +++ b/src/core/util/logging.hpp @@ -148,7 +148,14 @@ namespace util::logging { * @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) { + fn LogImpl( + const LogLevel level, +#ifndef NDEBUG + const std::source_location& loc, +#endif + std::format_string fmt, + Args&&... args + ) { using namespace std::chrono; using std::filesystem::path; @@ -186,14 +193,21 @@ namespace util::logging { fn LogError(const LogLevel level, const ErrorType& error_obj) { using DecayedErrorType = std::decay_t; +#ifndef NDEBUG std::source_location logLocation; - String errorMessagePart; +#endif - if constexpr (std::is_same_v) { - logLocation = error_obj.location; + String errorMessagePart; + + if constexpr (std::is_same_v) { +#ifndef NDEBUG + logLocation = error_obj.location; +#endif errorMessagePart = error_obj.message; } else { +#ifndef NDEBUG logLocation = std::source_location::current(); +#endif if constexpr (std::is_base_of_v) errorMessagePart = error_obj.what(); else if constexpr (requires { error_obj.message; }) @@ -202,7 +216,11 @@ namespace util::logging { errorMessagePart = "Unknown error type logged"; } +#ifndef NDEBUG LogImpl(level, logLocation, "{}", errorMessagePart); +#else + LogImpl(level, "{}", errorMessagePart); +#endif } #ifndef NDEBUG @@ -216,21 +234,28 @@ namespace util::logging { #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::LogError(::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::LogError(::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 info_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Info, error_obj); +#define warn_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Warn, error_obj); #define error_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Error, error_obj); + +#ifdef NDEBUG + #define info_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Info, fmt __VA_OPT__(, ) __VA_ARGS__) + #define warn_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Warn, fmt __VA_OPT__(, ) __VA_ARGS__) + #define error_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Error, fmt __VA_OPT__(, ) __VA_ARGS__) +#else + #define info_log(fmt, ...) \ + ::util::logging::LogImpl( \ + ::util::logging::LogLevel::Info, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \ + ) + + #define warn_log(fmt, ...) \ + ::util::logging::LogImpl( \ + ::util::logging::LogLevel::Warn, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \ + ) + + #define error_log(fmt, ...) \ + ::util::logging::LogImpl( \ + ::util::logging::LogLevel::Error, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \ + ) +#endif } // namespace util::logging diff --git a/src/core/util/types.hpp b/src/core/util/types.hpp index 988d8af..beb6092 100644 --- a/src/core/util/types.hpp +++ b/src/core/util/types.hpp @@ -2,6 +2,7 @@ #include // std::array (Array) #include // std::expected (Result) +#include // std::future (Future) #include // std::map (Map) #include // std::shared_ptr and std::unique_ptr (SharedPointer, UniquePointer) #include // std::optional (Option) @@ -113,6 +114,14 @@ namespace util::types { template > using UniquePointer = std::unique_ptr; + /** + * @typedef Future + * @brief Alias for std::future. Represents a value that will be available in the future. + * @tparam Tp The type of the value. + */ + template + using Future = std::future; + /** * @struct DiskSpace * @brief Represents disk usage information. @@ -132,16 +141,11 @@ namespace util::types { * 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"). + Option title; ///< Track title. + Option artist; ///< Track artist(s). 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 8141211..c3d3a1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,12 +6,12 @@ #include // ftxui::{Screen, Dimension::Full} #include // std::ranges::{iota, to, transform} +#include "src/config/config.hpp" #include "src/config/weather.hpp" - -#include "config/config.hpp" -#include "core/system_data.hpp" -#include "core/util/logging.hpp" -#include "os/os.hpp" +#include "src/core/system_data.hpp" +#include "src/core/util/defs.hpp" +#include "src/core/util/logging.hpp" +#include "src/core/util/types.hpp" namespace ui { using ftxui::Color; @@ -46,56 +46,60 @@ namespace ui { StringView music; StringView disk; StringView shell; + StringView package; StringView desktop; - StringView window_manager; + StringView windowManager; }; [[maybe_unused]] static constexpr Icons NONE = { - .user = "", - .palette = "", - .calendar = "", - .host = "", - .kernel = "", - .os = "", - .memory = "", - .weather = "", - .music = "", - .disk = "", - .shell = "", - .desktop = "", - .window_manager = "", + .user = "", + .palette = "", + .calendar = "", + .host = "", + .kernel = "", + .os = "", + .memory = "", + .weather = "", + .music = "", + .disk = "", + .shell = "", + .package = "", + .desktop = "", + .windowManager = "", }; [[maybe_unused]] static constexpr Icons NERD = { - .user = "  ", - .palette = "  ", - .calendar = "  ", - .host = " 󰌢 ", - .kernel = "  ", - .os = "  ", - .memory = "  ", - .weather = "  ", - .music = "  ", - .disk = " 󰋊 ", - .shell = "  ", - .desktop = " 󰇄 ", - .window_manager = "  ", + .user = "  ", + .palette = "  ", + .calendar = "  ", + .host = " 󰌢 ", + .kernel = "  ", + .os = "  ", + .memory = "  ", + .weather = "  ", + .music = "  ", + .disk = " 󰋊 ", + .shell = "  ", + .package = " 󰏖 ", + .desktop = " 󰇄 ", + .windowManager = "  ", }; [[maybe_unused]] static constexpr Icons EMOJI = { - .user = " 👤 ", - .palette = " 🎨 ", - .calendar = " 📅 ", - .host = " 💻 ", - .kernel = " 🫀 ", - .os = " 🤖 ", - .memory = " 🧠 ", - .weather = " 🌈 ", - .music = " 🎵 ", - .disk = " 💾 ", - .shell = " 💲 ", - .desktop = " 🖥️ ", - .window_manager = " 🪟 ", + .user = " 👤 ", + .palette = " 🎨 ", + .calendar = " 📅 ", + .host = " 💻 ", + .kernel = " 🫀 ", + .os = " 🤖 ", + .memory = " 🧠 ", + .weather = " 🌈 ", + .music = " 🎵 ", + .disk = " 💾 ", + .shell = " 💲 ", + .package = " 📦 ", + .desktop = " 🖥️ ", + .windowManager = " 🪟 ", }; static constexpr inline Icons ICON_TYPE = NERD; @@ -114,80 +118,69 @@ namespace { ); } - fn SystemInfoBox(const Config& config, const SystemData& data) -> Element { + fn SystemInfoBox(const Config& config, const os::SystemData& data) -> Element { const String& name = config.general.name; const Weather weather = config.weather; - const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, deIcon, wmIcon] = + const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, packageIcon, deIcon, wmIcon] = ui::ICON_TYPE; Elements content; content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan)); content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - content.push_back(hbox( - { - text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), - CreateColorCircles(), - } - )); + content.push_back(hbox({ + text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), + CreateColorCircles(), + })); content.push_back(separator() | color(ui::DEFAULT_THEME.border)); // Helper function for aligned rows fn createRow = [&](const StringView& icon, const StringView& label, const StringView& value) { // NEW - return hbox( - { - text(String(icon)) | color(ui::DEFAULT_THEME.icon), - text(String(label)) | color(ui::DEFAULT_THEME.label), - filler(), - text(String(value)) | color(ui::DEFAULT_THEME.value), - text(" "), - } - ); + return hbox({ + text(String(icon)) | color(ui::DEFAULT_THEME.icon), + text(String(label)) | color(ui::DEFAULT_THEME.label), + filler(), + text(String(value)) | color(ui::DEFAULT_THEME.value), + text(" "), + }); }; // System info rows content.push_back(createRow(calendarIcon, "Date", data.date)); // Weather row - if (weather.enabled && data.weather_info.has_value()) { - const weather::Output& weatherInfo = data.weather_info.value(); + if (weather.enabled && data.weather) { + const weather::Output& weatherInfo = *data.weather; - if (weather.show_town_name) - content.push_back(hbox( - { - text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon), - text("Weather") | color(ui::DEFAULT_THEME.label), - filler(), + if (weather.showTownName) + content.push_back(hbox({ + text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon), + text("Weather") | color(ui::DEFAULT_THEME.label), + filler(), - hbox( - { - text(std::format("{}°F ", std::lround(weatherInfo.main.temp))), - text("in "), - text(weatherInfo.name), - text(" "), - } - ) | - color(ui::DEFAULT_THEME.value), - } - )); + hbox({ + text(std::format("{}°F ", std::lround(weatherInfo.main.temp))), + text("in "), + text(weatherInfo.name), + text(" "), + }) | + color(ui::DEFAULT_THEME.value), + })); else - content.push_back(hbox( - { - text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon), - text("Weather") | color(ui::DEFAULT_THEME.label), - filler(), + content.push_back(hbox({ + text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon), + text("Weather") | color(ui::DEFAULT_THEME.label), + filler(), - hbox( - { - text(std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description)), - text(" "), - } - ) | - color(ui::DEFAULT_THEME.value), - } - )); - } + hbox({ + text(std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description)), + text(" "), + }) | + color(ui::DEFAULT_THEME.value), + })); + } else if (weather.enabled) + error_at(data.weather.error()); content.push_back(separator() | color(ui::DEFAULT_THEME.border)); @@ -196,62 +189,64 @@ namespace { else error_at(data.host.error()); - if (data.kernel_version) - content.push_back(createRow(kernelIcon, "Kernel", *data.kernel_version)); + if (data.kernelVersion) + content.push_back(createRow(kernelIcon, "Kernel", *data.kernelVersion)); else - error_at(data.kernel_version.error()); + error_at(data.kernelVersion.error()); - if (data.os_version) - content.push_back(createRow(String(osIcon), "OS", *data.os_version)); + if (data.osVersion) + content.push_back(createRow(String(osIcon), "OS", *data.osVersion)); else - error_at(data.os_version.error()); + error_at(data.osVersion.error()); - if (data.mem_info) - content.push_back(createRow(memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.mem_info }))); + if (data.memInfo) + content.push_back(createRow(memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.memInfo }))); else - error_at(data.mem_info.error()); + error_at(data.memInfo.error()); - if (data.disk_usage) + if (data.diskUsage) content.push_back(createRow( diskIcon, "Disk", - std::format("{}/{}", BytesToGiB { data.disk_usage->used_bytes }, BytesToGiB { data.disk_usage->total_bytes }) + std::format("{}/{}", BytesToGiB { data.diskUsage->used_bytes }, BytesToGiB { data.diskUsage->total_bytes }) )); else - error_at(data.disk_usage.error()); + error_at(data.diskUsage.error()); if (data.shell) content.push_back(createRow(shellIcon, "Shell", *data.shell)); + else + error_at(data.shell.error()); + + if (data.packageCount) + content.push_back(createRow(packageIcon, "Packages", std::format("{}", *data.packageCount))); + else + error_at(data.packageCount.error()); content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - if (data.desktop_environment && *data.desktop_environment != data.window_manager) - content.push_back(createRow(deIcon, "DE", *data.desktop_environment)); + if (data.desktopEnv && *data.desktopEnv != data.windowMgr) + content.push_back(createRow(deIcon, "DE", *data.desktopEnv)); - if (data.window_manager) - content.push_back(createRow(wmIcon, "WM", *data.window_manager)); + if (data.windowMgr) + content.push_back(createRow(wmIcon, "WM", *data.windowMgr)); + else + error_at(data.windowMgr.error()); - if (config.now_playing.enabled && data.now_playing) { - if (const Result& nowPlayingResult = *data.now_playing) { - const MediaInfo& info = *nowPlayingResult; + if (config.nowPlaying.enabled && data.nowPlaying) { + const String title = data.nowPlaying->title.value_or("Unknown Title"); + const String artist = data.nowPlaying->artist.value_or("Unknown Artist"); + const String npText = artist + " - " + title; - const String title = info.title.value_or("Unknown Title"); - const String artist = info.artist.value_or("Unknown Artist"); - const String npText = artist + " - " + title; - - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - content.push_back(hbox( - { - text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon), - text("Playing") | color(ui::DEFAULT_THEME.label), - text(" "), - filler(), - paragraph(npText) | color(Color::Magenta) | size(WIDTH, LESS_THAN, ui::MAX_PARAGRAPH_LENGTH), - text(" "), - } - )); - } else - debug_at(nowPlayingResult.error()); + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + content.push_back(hbox({ + text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon), + text("Playing") | color(ui::DEFAULT_THEME.label), + text(" "), + filler(), + paragraph(npText) | color(Color::Magenta) | size(WIDTH, LESS_THAN, ui::MAX_PARAGRAPH_LENGTH), + text(" "), + })); } return vbox(content) | borderRounded | color(Color::White); @@ -259,17 +254,14 @@ namespace { } // namespace fn main() -> i32 { + using os::SystemData; + #ifdef _WIN32 winrt::init_apartment(); #endif const Config& config = Config::getInstance(); - const SystemData data = SystemData::fetchSystemData(config); - - if (const Result& packageCount = os::GetPackageCount()) - debug_log("{}", *packageCount); - else - error_at(packageCount.error()); + const SystemData data = SystemData(config); Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }) }); diff --git a/src/os/linux.cpp b/src/os/linux.cpp index 612fd2d..e023482 100644 --- a/src/os/linux.cpp +++ b/src/os/linux.cpp @@ -1,22 +1,28 @@ #ifdef __linux__ // clang-format off -#include // std::strlen -#include // DBus::{DBusConnection, DBusMessage, DBusMessageIter, etc.} -#include // std::{unexpected, expected} -#include // std::{format, format_to_n} -#include // std::ifstream -#include // std::numeric_limits -#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::strlen +#include // DBUS_BUS_SESSION +#include // DBUS_TYPE_* +#include // std::{unexpected, expected} +#include // std::{format, format_to_n} +#include // std::ifstream +#include // PATH_MAX +#include // std::numeric_limits +#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 "src/core/util/defs.hpp" +#include "src/core/util/error.hpp" #include "src/core/util/helpers.hpp" #include "src/core/util/logging.hpp" +#include "src/core/util/types.hpp" #include "src/wrappers/dbus.hpp" #include "src/wrappers/wayland.hpp" #include "src/wrappers/xcb.hpp" @@ -26,17 +32,18 @@ // clang-format on using namespace util::types; -using util::error::DraconisError, util::error::DraconisErrorCode; +using util::error::DracError, util::error::DracErrorCode; +using util::helpers::GetEnv; namespace { - fn GetX11WindowManager() -> Result { + fn GetX11WindowManager() -> Result { using namespace xcb; const DisplayGuard conn; if (!conn) if (const i32 err = connection_has_error(conn.get())) - return Err(DraconisError(DraconisErrorCode::ApiUnavailable, [&] -> String { + return Err(DracError(DracErrorCode::ApiUnavailable, [&] -> String { if (const Option connErr = getConnError(err)) { switch (*connErr) { case Generic: return "Stream/Socket/Pipe Error"; @@ -53,22 +60,22 @@ namespace { return std::format("Unknown Error Code ({})", err); }())); - fn internAtom = [&conn](const StringView name) -> Result { + 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( - DraconisError(DraconisErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name)) + DracError(DracErrorCode::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) @@ -80,7 +87,7 @@ namespace { if (!utf8StringAtom) error_log("Failed to get UTF8_STRING atom"); - return Err(DraconisError(DraconisErrorCode::PlatformSpecific, "Failed to get X11 atoms")); + return Err(DracError(DracErrorCode::PlatformSpecific, "Failed to get X11 atoms")); } const ReplyGuard wmWindowReply(get_property_reply( @@ -91,7 +98,7 @@ namespace { 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")); + return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property")); const window_t wmRootWindow = *static_cast(get_property_value(wmWindowReply.get())); @@ -100,7 +107,7 @@ namespace { )); if (!wmNameReply || wmNameReply->type != *utf8StringAtom || get_property_value_length(wmNameReply.get()) == 0) - return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get _NET_WM_NAME property")); + return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_WM_NAME property")); const char* nameData = static_cast(get_property_value(wmNameReply.get())); const usize length = get_property_value_length(wmNameReply.get()); @@ -108,39 +115,39 @@ namespace { return String(nameData, length); } - fn GetWaylandCompositor() -> Result { + fn GetWaylandCompositor() -> Result { const wl::DisplayGuard display; if (!display) - return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to connect to display (is Wayland running?)")); + return Err(DracError(DracErrorCode::NotFound, "Failed to connect to display (is Wayland running?)")); const i32 fileDescriptor = display.fd(); if (fileDescriptor < 0) - return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor")); + return Err(DracError(DracErrorCode::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(DraconisError::withErrno("Failed to get socket credentials (SO_PEERCRED)")); + return Err(DracError::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(DraconisError(DraconisErrorCode::InternalError, "Failed to format /proc path (PID too large?)")); + return Err(DracError(DracErrorCode::InternalError, "Failed to format /proc path (PID too large?)")); *out = '\0'; const char* exeLinkPath = exeLinkPathBuf.data(); - Array exeRealPathBuf; + Array exeRealPathBuf; // NOLINT(misc-include-cleaner) - PATH_MAX is in const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1); if (bytesRead == -1) - return Err(DraconisError::withErrno(std::format("Failed to read link '{}'", exeLinkPath))); + return Err(DracError::withErrno(std::format("Failed to read link '{}'", exeLinkPath))); exeRealPathBuf.at(bytesRead) = '\0'; @@ -163,7 +170,7 @@ namespace { compositorNameView = filenameView; if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/") - return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get compositor name from path")); + return Err(DracError(DracErrorCode::NotFound, "Failed to get compositor name from path")); if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() && compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) { @@ -171,7 +178,7 @@ namespace { compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length()); if (cleanedView.empty()) - return Err(DraconisError(DraconisErrorCode::NotFound, "Compositor name invalid after heuristic")); + return Err(DracError(DracErrorCode::NotFound, "Compositor name invalid after heuristic")); return String(cleanedView); } @@ -181,13 +188,13 @@ namespace { } // namespace namespace os { - fn GetOSVersion() -> Result { + fn GetOSVersion() -> Result { constexpr CStr path = "/etc/os-release"; std::ifstream file(path); if (!file) - return Err(DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open {}", path))); + return Err(DracError(DracErrorCode::NotFound, std::format("Failed to open {}", path))); String line; constexpr StringView prefix = "PRETTY_NAME="; @@ -201,206 +208,197 @@ namespace os { value = value.substr(1, value.length() - 2); if (value.empty()) - return Err(DraconisError( - DraconisErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path) - )); + return Err( + DracError(DracErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path)) + ); return value; } } - return Err(DraconisError(DraconisErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path))); + return Err(DracError(DracErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path))); } - fn GetMemInfo() -> Result { + fn GetMemInfo() -> Result { struct sysinfo info; if (sysinfo(&info) != 0) - return Err(DraconisError::withErrno("sysinfo call failed")); + return Err(DracError::withErrno("sysinfo call failed")); const u64 totalRam = info.totalram; const u64 memUnit = info.mem_unit; if (memUnit == 0) - return Err(DraconisError(DraconisErrorCode::InternalError, "sysinfo returned mem_unit of zero")); + return Err(DracError(DracErrorCode::InternalError, "sysinfo returned mem_unit of zero")); if (totalRam > std::numeric_limits::max() / memUnit) - return Err(DraconisError(DraconisErrorCode::InternalError, "Potential overflow calculating total RAM")); + return Err(DracError(DracErrorCode::InternalError, "Potential overflow calculating total RAM")); return info.totalram * info.mem_unit; } - fn GetNowPlaying() -> Result { - Result connectionResult = dbus::BusGet(DBUS_BUS_SESSION); + fn GetNowPlaying() -> Result { + using namespace dbus; + Result connectionResult = Connection::busGet(DBUS_BUS_SESSION); if (!connectionResult) return Err(connectionResult.error()); - dbus::ConnectionGuard& connection = *connectionResult; + const Connection& connection = *connectionResult; Option activePlayer = None; { - Result listNamesResult = dbus::MessageNewMethodCall( - "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames" - ); + Result listNamesResult = + Message::newMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"); if (!listNamesResult) return Err(listNamesResult.error()); - dbus::MessageGuard& listNames = *listNamesResult; - - Result listNamesReplyResult = - dbus::ConnectionSendWithReplyAndBlock(connection, listNames, 100); - + Result listNamesReplyResult = connection.sendWithReplyAndBlock(*listNamesResult, 100); if (!listNamesReplyResult) return Err(listNamesReplyResult.error()); - dbus::MessageGuard& listNamesReply = *listNamesReplyResult; + MessageIter iter = listNamesReplyResult->iterInit(); + if (!iter.isValid() || iter.getArgType() != DBUS_TYPE_ARRAY) + return Err(DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Expected array")); - dbus::MessageIter iter; + MessageIter subIter = iter.recurse(); + if (!subIter.isValid()) + return Err( + DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Could not recurse into array") + ); - if (dbus::MessageIterInit(listNamesReply, &iter) && dbus::MessageIterGetArgType(&iter) == DBUS_TYPE_ARRAY) { - dbus::MessageIter subIter; - dbus::MessageIterRecurse(&iter, &subIter); - - while (dbus::MessageIterGetArgType(&subIter) != DBUS_TYPE_INVALID) { - if (Option name = dbus::MessageIterGetString(&subIter)) - if (name->find("org.mpris.MediaPlayer2") != String::npos) { - activePlayer = std::move(*name); - break; - } - - dbus::MessageIterNext(&subIter); - } - } else { - return Err(DraconisError(DraconisErrorCode::ParseError, "Invalid DBus ListNames reply format")); + while (subIter.getArgType() != DBUS_TYPE_INVALID) { + if (Option name = subIter.getString()) + if (name->starts_with("org.mpris.MediaPlayer2.")) { + activePlayer = std::move(*name); + break; + } + if (!subIter.next()) + break; } } if (!activePlayer) - return Err(DraconisError(DraconisErrorCode::NotFound, "No active MPRIS players found")); + return Err(DracError(DracErrorCode::NotFound, "No active MPRIS players found")); - Result msgResult = dbus::MessageNewMethodCall( + Result msgResult = Message::newMethodCall( activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get" ); if (!msgResult) return Err(msgResult.error()); - dbus::MessageGuard& msg = *msgResult; + Message& msg = *msgResult; - if (!dbus::MessageAppendArgs( - msg, DBUS_TYPE_STRING, "org.mpris.MediaPlayer2.Player", DBUS_TYPE_STRING, "Metadata", DBUS_TYPE_INVALID - )) - return Err(DraconisError(DraconisErrorCode::InternalError, "Failed to append arguments to DBus message")); + if (!msg.appendArgs("org.mpris.MediaPlayer2.Player", "Metadata")) + return Err(DracError(DracErrorCode::InternalError, "Failed to append arguments to Properties.Get message")); - Result replyResult = - dbus::ConnectionSendWithReplyAndBlock(connection, msg, 100); + Result replyResult = connection.sendWithReplyAndBlock(msg, 100); if (!replyResult) return Err(replyResult.error()); - dbus::MessageGuard& reply = *replyResult; - Option title = None; Option artist = None; - dbus::MessageIter propIter; - if (!dbus::MessageIterInit(reply, &propIter)) - return Err(DraconisError(DraconisErrorCode::ParseError, "Properties.Get reply has no arguments")); + MessageIter propIter = replyResult->iterInit(); + if (!propIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply has no arguments or invalid iterator")); - if (dbus::MessageIterGetArgType(&propIter) != DBUS_TYPE_VARIANT) - return Err(DraconisError(DraconisErrorCode::ParseError, "Properties.Get reply argument is not a variant")); + if (propIter.getArgType() != DBUS_TYPE_VARIANT) + return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply argument is not a variant")); - dbus::MessageIter variantIter; - dbus::MessageIterRecurse(&propIter, &variantIter); + MessageIter variantIter = propIter.recurse(); + if (!variantIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Could not recurse into variant")); - if (dbus::MessageIterGetArgType(&variantIter) != DBUS_TYPE_ARRAY || - dbus_message_iter_get_element_type(&variantIter) != DBUS_TYPE_DICT_ENTRY) - return Err( - DraconisError(DraconisErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})") - ); + if (variantIter.getArgType() != DBUS_TYPE_ARRAY || variantIter.getElementType() != DBUS_TYPE_DICT_ENTRY) + return Err(DracError(DracErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})")); - dbus::MessageIter dictIter; - dbus::MessageIterRecurse(&variantIter, &dictIter); + MessageIter dictIter = variantIter.recurse(); + if (!dictIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Could not recurse into metadata dictionary array")); - while (dbus::MessageIterGetArgType(&dictIter) == DBUS_TYPE_DICT_ENTRY) { - dbus::MessageIter entryIter; - dbus::MessageIterRecurse(&dictIter, &entryIter); - - Option key = dbus::MessageIterGetString(&entryIter); + while (dictIter.getArgType() == DBUS_TYPE_DICT_ENTRY) { + MessageIter entryIter = dictIter.recurse(); + if (!entryIter.isValid()) { + debug_log("Warning: Could not recurse into dict entry, skipping."); + if (!dictIter.next()) + break; + continue; + } + Option key = entryIter.getString(); if (!key) { - dbus::MessageIterNext(&dictIter); + debug_log("Warning: Could not get key string from dict entry, skipping."); + if (!dictIter.next()) + break; continue; } - if (!dbus::MessageIterNext(&entryIter) || dbus::MessageIterGetArgType(&entryIter) != DBUS_TYPE_VARIANT) { - dbus::MessageIterNext(&dictIter); + if (!entryIter.next() || entryIter.getArgType() != DBUS_TYPE_VARIANT) { + if (!dictIter.next()) + break; continue; } - dbus::MessageIter valueVariantIter; - dbus::MessageIterRecurse(&entryIter, &valueVariantIter); + MessageIter valueVariantIter = entryIter.recurse(); + if (!valueVariantIter.isValid()) { + if (!dictIter.next()) + break; + continue; + } - if (*key == "xesam:title") - title = dbus::MessageIterGetString(&valueVariantIter); - else if (*key == "xesam:artist") - if (dbus::MessageIterGetArgType(&valueVariantIter) == DBUS_TYPE_ARRAY && - dbus_message_iter_get_element_type(&valueVariantIter) == DBUS_TYPE_STRING) { - dbus::MessageIter artistArrayIter; - dbus::MessageIterRecurse(&valueVariantIter, &artistArrayIter); - artist = dbus::MessageIterGetString(&artistArrayIter); + if (*key == "xesam:title") { + title = valueVariantIter.getString(); + } else if (*key == "xesam:artist") { + if (valueVariantIter.getArgType() == DBUS_TYPE_ARRAY && valueVariantIter.getElementType() == DBUS_TYPE_STRING) { + if (MessageIter artistArrayIter = valueVariantIter.recurse(); artistArrayIter.isValid()) + artist = artistArrayIter.getString(); + } else { + debug_log("Warning: Artist value was not an array of strings as expected."); } + } - dbus::MessageIterNext(&dictIter); + if (!dictIter.next()) + break; } return MediaInfo(std::move(title), std::move(artist)); } - fn GetWindowManager() -> Option { - if (Result waylandResult = GetWaylandCompositor()) + fn GetWindowManager() -> Result { + if (Result waylandResult = GetWaylandCompositor()) return *waylandResult; - else - 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); - return None; + return Err(DracError(DracErrorCode::NotFound, "Could not detect window manager (Wayland/X11) or both failed")); } - fn GetDesktopEnvironment() -> Option { - return util::helpers::GetEnv("XDG_CURRENT_DESKTOP") - .transform([](const String& xdgDesktop) -> String { + fn GetDesktopEnvironment() -> Result { + return GetEnv("XDG_CURRENT_DESKTOP") + .transform([](String xdgDesktop) -> String { if (const usize colon = xdgDesktop.find(':'); colon != String::npos) - return xdgDesktop.substr(0, colon); + xdgDesktop.resize(colon); return xdgDesktop; }) - .or_else([](const DraconisError&) -> Result { - return util::helpers::GetEnv("DESKTOP_SESSION"); - }) - .transform([](const String& finalValue) -> Option { - debug_log("Found desktop environment: {}", finalValue); - return finalValue; - }) - .value_or(None); + .or_else([](const DracError&) -> Result { return GetEnv("DESKTOP_SESSION"); }); } - fn GetShell() -> Option { - if (const Result shellPath = util::helpers::GetEnv("SHELL")) { + fn GetShell() -> Result { + if (const Result shellPath = GetEnv("SHELL")) { // clang-format off - constexpr Array, 5> shellMap {{ - { "bash", "Bash" }, - { "zsh", "Zsh" }, - { "fish", "Fish" }, - { "nu", "Nushell" }, - { "sh", "SH" }, // sh last because other shells contain "sh" - }}; + constexpr Array, 5> shellMap {{ + { "bash", "Bash" }, + { "zsh", "Zsh" }, + { "fish", "Fish" }, + { "nu", "Nushell" }, + { "sh", "SH" }, // sh last because other shells contain "sh" + }}; // clang-format on for (const auto& [exe, name] : shellMap) @@ -410,64 +408,63 @@ namespace os { return *shellPath; // fallback to the raw shell path } - return None; + return Err(DracError(DracErrorCode::NotFound, "Could not find SHELL environment variable")); } - fn GetHost() -> Result { + fn 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(DraconisError( - DraconisErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path) - )); + return Err( + DracError(DracErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path)) + ); if (!std::getline(file, line)) return Err( - DraconisError(DraconisErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)) + DracError(DracErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)) ); return line; }; - 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 - ) - )); - }); + return readFirstLine(primaryPath).or_else([&](const DracError& primaryError) -> Result { + return readFirstLine(fallbackPath).or_else([&](const DracError& fallbackError) -> Result { + return Err(DracError( + DracErrorCode::InternalError, + std::format( + "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", + primaryPath, + primaryError.message, + fallbackPath, + fallbackError.message + ) + )); + }); }); } - fn GetKernelVersion() -> Result { + fn GetKernelVersion() -> Result { utsname uts; if (uname(&uts) == -1) - return Err(DraconisError::withErrno("uname call failed")); + return Err(DracError::withErrno("uname call failed")); if (std::strlen(uts.release) == 0) - return Err(DraconisError(DraconisErrorCode::ParseError, "uname returned null kernel release")); + return Err(DracError(DracErrorCode::ParseError, "uname returned null kernel release")); return uts.release; } - fn GetDiskUsage() -> Result { + fn GetDiskUsage() -> Result { struct statvfs stat; if (statvfs("/", &stat) == -1) - return Err(DraconisError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)"))); + return Err(DracError::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), @@ -475,7 +472,21 @@ namespace os { }; } - fn GetPackageCount() -> Result { return linux::GetTotalPackageCount(); } + fn GetPackageCount() -> Result { + u64 count = 0; + + if (Result linuxCount = linux::GetTotalPackageCount()) + count += *linuxCount; + else + return Err(linuxCount.error()); + + if (Result sharedCount = shared::GetPackageCount()) + count += *sharedCount; + else + return Err(sharedCount.error()); + + return count; + } } // namespace os #endif // __linux__ diff --git a/src/os/linux/pkg_count.cpp b/src/os/linux/pkg_count.cpp index e500b34..b4df757 100644 --- a/src/os/linux/pkg_count.cpp +++ b/src/os/linux/pkg_count.cpp @@ -3,53 +3,53 @@ // clang-format off #include "src/os/linux/pkg_count.hpp" -#include -#include -#include -#include -#include -#include +#include // SQLite::{Database, OPEN_READONLY} +#include // SQLite::Exception +#include // SQLite::Statement +#include // std::chrono::{duration_cast, seconds, system_clock} +#include // std::filesystem::{current_path, directory_entry, directory_iterator, etc.} +#include // std::format +#include // std::{ifstream, ofstream} +#include // std::{async, launch} +#include // std::ios::{binary, trunc}, std::ios_base +#include // std::istreambuf_iterator +#include // glz::read_beve +#include // glz::write_beve +#include // glz::{context, error_code, error_ctx} +#include // std::error_code +#include "src/core/util/defs.hpp" +#include "src/core/util/error.hpp" #include "src/core/util/logging.hpp" #include "src/core/util/types.hpp" // clang-format on -using util::error::DraconisError, util::error::DraconisErrorCode; +using util::error::DracError, util::error::DracErrorCode; using util::types::u64, util::types::i64, util::types::Result, util::types::Err, util::types::String, - util::types::Exception; + util::types::StringView, util::types::Exception; namespace { namespace fs = std::filesystem; using namespace std::chrono; + using os::linux::PkgCountCacheData, os::linux::PackageManagerInfo; - struct PkgCountCacheData { - u64 count {}; - i64 timestamp_epoch_seconds {}; + constexpr StringView ALLOWED_PMID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"; - // NOLINTBEGIN(readability-identifier-naming) - struct [[maybe_unused]] glaze { - using T = PkgCountCacheData; - static constexpr auto value = glz::object("count", &T::count, "timestamp", &T::timestamp_epoch_seconds); - }; - // NOLINTEND(readability-identifier-naming) - }; - - fn GetPkgCountCachePath(const String& pm_id) -> Result { + fn GetPkgCountCachePath(const String& pmId) -> 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 Err(DracError(DracErrorCode::IoError, "Failed to get temp directory: " + errc.message())); - if (pm_id.empty() || - pm_id.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") != String::npos) - return Err(DraconisError(DraconisErrorCode::ParseError, "Invalid package manager ID for cache path: " + pm_id)); + if (pmId.empty() || pmId.find_first_not_of(ALLOWED_PMID_CHARS) != String::npos) + return Err(DracError(DracErrorCode::ParseError, "Invalid package manager ID for cache path: " + pmId)); - return cacheDir / (pm_id + "_pkg_count_cache.beve"); + return cacheDir / (pmId + "_pkg_count_cache.beve"); } - fn ReadPkgCountCache(const String& pm_id) -> Result { - Result cachePathResult = GetPkgCountCachePath(pm_id); + fn ReadPkgCountCache(const String& pmId) -> Result { + Result cachePathResult = GetPkgCountCachePath(pmId); if (!cachePathResult) return Err(cachePathResult.error()); @@ -57,57 +57,45 @@ namespace { const fs::path& cachePath = *cachePathResult; if (!fs::exists(cachePath)) - return Err(DraconisError(DraconisErrorCode::NotFound, "Cache file not found: " + cachePath.string())); + return Err(DracError(DracErrorCode::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()) - ); - - // Update log message - debug_log("Reading {} package count from cache file: {}", pm_id, cachePath.string()); + return Err(DracError(DracErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string())); try { - // Read the entire binary content - // Using std::string buffer is fine, it can hold arbitrary binary data const String content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - ifs.close(); // Close the file stream after reading + ifs.close(); - if (content.empty()) { - return Err(DraconisError(DraconisErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string())); - } + if (content.empty()) + return Err(DracError(DracErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string())); PkgCountCacheData result; const glz::context ctx {}; - if (auto glazeResult = glz::read_beve(result, content); glazeResult.ec != glz::error_code::none) - return Err(DraconisError( - DraconisErrorCode::ParseError, + if (glz::error_ctx glazeResult = glz::read_beve(result, content); glazeResult.ec != glz::error_code::none) + return Err(DracError( + DracErrorCode::ParseError, std::format( "BEVE parse error reading cache (code {}): {}", static_cast(glazeResult.ec), cachePath.string() ) )); - debug_log("Successfully read {} package count from BEVE cache file.", pm_id); return result; } catch (const std::ios_base::failure& e) { - return Err(DraconisError( - DraconisErrorCode::IoError, - std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what()) + return Err(DracError( + DracErrorCode::IoError, std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what()) )); } catch (const Exception& e) { - return Err( - DraconisError(DraconisErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what())) + return Err(DracError(DracErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what())) ); } } - // Modified to take pm_id and PkgCountCacheData - fn WritePkgCountCache(const String& pm_id, const PkgCountCacheData& data) -> Result { + fn WritePkgCountCache(const String& pmId, const PkgCountCacheData& data) -> Result { using util::types::isize; - Result cachePathResult = GetPkgCountCachePath(pm_id); + Result cachePathResult = GetPkgCountCachePath(pmId); if (!cachePathResult) return Err(cachePathResult.error()); @@ -116,80 +104,65 @@ namespace { fs::path tempPath = cachePath; tempPath += ".tmp"; - debug_log("Writing {} package count to BEVE cache file: {}", pm_id, cachePath.string()); - try { String binaryBuffer; PkgCountCacheData mutableData = data; - if (auto glazeErr = glz::write_beve(mutableData, binaryBuffer); glazeErr) { - return Err(DraconisError( - DraconisErrorCode::ParseError, + if (glz::error_ctx glazeErr = glz::write_beve(mutableData, binaryBuffer); glazeErr) + return Err(DracError( + DracErrorCode::ParseError, std::format("BEVE serialization error writing cache (code {})", static_cast(glazeErr.ec)) )); - } { 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())); - } + if (!ofs.is_open()) + return Err(DracError(DracErrorCode::IoError, "Failed to open temp cache file: " + tempPath.string())); ofs.write(binaryBuffer.data(), static_cast(binaryBuffer.size())); if (!ofs) { std::error_code removeEc; fs::remove(tempPath, removeEc); - return Err( - DraconisError(DraconisErrorCode::IoError, "Failed to write to temp cache file: " + tempPath.string()) - ); + return Err(DracError(DracErrorCode::IoError, "Failed to write to temp cache file: " + tempPath.string())); } } - // Atomically replace the old cache file with the new one std::error_code errc; fs::rename(tempPath, cachePath, errc); if (errc) { - fs::remove(tempPath, errc); // Clean up temp file on failure (ignore error) - return Err(DraconisError( - DraconisErrorCode::IoError, + fs::remove(tempPath, errc); + return Err(DracError( + DracErrorCode::IoError, std::format("Failed to replace cache file '{}': {}", cachePath.string(), errc.message()) )); } - debug_log("Successfully wrote {} package count to BEVE cache file.", pm_id); return {}; } catch (const std::ios_base::failure& e) { std::error_code removeEc; fs::remove(tempPath, removeEc); - return Err(DraconisError( - DraconisErrorCode::IoError, - std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what()) + return Err(DracError( + DracErrorCode::IoError, std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what()) )); } catch (const Exception& e) { std::error_code removeEc; fs::remove(tempPath, removeEc); - return Err( - DraconisError(DraconisErrorCode::InternalError, std::format("Error writing package count cache: {}", e.what())) + return Err(DracError(DracErrorCode::InternalError, std::format("Error writing package count cache: {}", e.what())) ); } catch (...) { std::error_code removeEc; fs::remove(tempPath, removeEc); - return Err( - DraconisError(DraconisErrorCode::Other, std::format("Unknown error writing cache file: {}", tempPath.string())) + return Err(DracError(DracErrorCode::Other, std::format("Unknown error writing cache file: {}", tempPath.string())) ); } } - fn GetPackageCountInternal(const os::linux::PackageManagerInfo& pmInfo) -> Result { - // Use info from the struct - const fs::path& dbPath = pmInfo.db_path; - const String& pmId = pmInfo.id; - const String& queryStr = pmInfo.count_query; + fn GetPackageCountInternalDb(const PackageManagerInfo& pmInfo) -> Result { + const auto& [pmId, dbPath, countQuery] = pmInfo; - // Try reading from cache using pm_id - if (Result cachedDataResult = ReadPkgCountCache(pmId)) { + if (Result cachedDataResult = ReadPkgCountCache(pmId)) { const auto& [count, timestamp] = *cachedDataResult; std::error_code errc; const std::filesystem::file_time_type dbModTime = fs::last_write_time(dbPath, errc); @@ -199,9 +172,8 @@ namespace { "Could not get modification time for '{}': {}. Invalidating {} cache.", dbPath.string(), errc.message(), pmId ); } else { - if (const auto cacheTimePoint = system_clock::time_point(seconds(timestamp)); + if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp)); cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) { - // Use cacheTimePoint for logging as well debug_log( "Using valid {} package count cache (DB file unchanged since {}).", pmId, @@ -212,7 +184,7 @@ namespace { debug_log("{} package count cache stale (DB file modified).", pmId); } } else { - if (cachedDataResult.error().code != DraconisErrorCode::NotFound) + if (cachedDataResult.error().code != DracErrorCode::NotFound) debug_at(cachedDataResult.error()); debug_log("{} package count cache not found or unreadable.", pmId); } @@ -222,121 +194,202 @@ namespace { try { const SQLite::Database database(dbPath.string(), SQLite::OPEN_READONLY); - if (SQLite::Statement query(database, queryStr); query.executeStep()) { + if (SQLite::Statement query(database, countQuery); query.executeStep()) { const i64 countInt64 = query.getColumn(0).getInt64(); if (countInt64 < 0) - return Err(DraconisError( - DraconisErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId) - )); + return Err( + DracError(DracErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId)) + ); count = static_cast(countInt64); } else { - return Err( - DraconisError(DraconisErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId)) - ); + return Err(DracError(DracErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId))); } } catch (const SQLite::Exception& e) { - return Err(DraconisError( - DraconisErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what()) + return Err(DracError( + DracErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what()) )); - } catch (const Exception& e) { - return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); - } catch (...) { - return Err(DraconisError(DraconisErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId))); + } catch (const Exception& e) { return Err(DracError(DracErrorCode::InternalError, e.what())); } catch (...) { + return Err(DracError(DracErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId))); } const i64 nowEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); - const PkgCountCacheData dataToCache = { .count = count, .timestamp_epoch_seconds = nowEpochSeconds }; + const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds }; - if (Result writeResult = WritePkgCountCache(pmId, dataToCache); !writeResult) { - warn_at(writeResult.error()); - warn_log("Failed to write {} package count to cache.", pmId); + if (Result writeResult = WritePkgCountCache(pmId, dataToCache); !writeResult) + error_at(writeResult.error()); + + return count; + } + + fn GetPackageCountInternalDir( + const String& pmId, + const fs::path& dirPath, + const String& file_extension_filter = "", + const bool subtract_one = false + ) -> Result { + debug_log("Attempting to get {} package count.", pmId); + + std::error_code errc; + if (!fs::exists(dirPath, errc)) { + if (errc) + return Err(DracError( + DracErrorCode::IoError, std::format("Filesystem error checking {} directory: {}", pmId, errc.message()) + )); + + return Err( + DracError(DracErrorCode::ApiUnavailable, std::format("{} directory not found: {}", pmId, dirPath.string())) + ); } - debug_log("Fetched fresh {} package count: {}", pmId, count); + if (!fs::is_directory(dirPath, errc)) { + if (errc) + return Err(DracError( + DracErrorCode::IoError, std::format("Filesystem error checking {} path type: {}", pmId, errc.message()) + )); + + warn_log("Expected {} directory at '{}', but it's not a directory.", pmId, dirPath.string()); + return Err( + DracError(DracErrorCode::IoError, std::format("{} path is not a directory: {}", pmId, dirPath.string())) + ); + } + + u64 count = 0; + + try { + const fs::directory_iterator dirIter(dirPath, fs::directory_options::skip_permission_denied, errc); + + if (errc) + return Err( + DracError(DracErrorCode::IoError, std::format("Failed to iterate {} directory: {}", pmId, errc.message())) + ); + + for (const fs::directory_entry& entry : dirIter) { + if (!file_extension_filter.empty()) { + if (std::error_code fileErrc; !entry.is_regular_file(fileErrc) || fileErrc) { + if (fileErrc) + warn_log( + "Error checking file status in {} directory for '{}': {}", + pmId, + entry.path().string(), + fileErrc.message() + ); + + continue; + } + + if (entry.path().extension().string() == file_extension_filter) + count++; + } else { + count++; + } + } + } catch (const fs::filesystem_error& e) { + return Err(DracError( + DracErrorCode::IoError, + std::format("Filesystem error iterating {} directory '{}': {}", pmId, dirPath.string(), e.what()) + )); + } catch (...) { + return Err(DracError( + DracErrorCode::Other, std::format("Unknown error iterating {} directory '{}'", pmId, dirPath.string()) + )); + } + + if (subtract_one && count > 0) + count--; + return count; } } // namespace namespace os::linux { - fn GetMossPackageCount() -> Result { + fn GetDpkgPackageCount() -> Result { + return GetPackageCountInternalDir( + "Dpkg", fs::current_path().root_path() / "var" / "lib" / "dpkg" / "info", String(".list") + ); + } + + fn GetMossPackageCount() -> Result { debug_log("Attempting to get Moss package count."); const PackageManagerInfo mossInfo = { - .id = "moss", - .db_path = "/.moss/db/install", - .count_query = "SELECT COUNT(*) FROM meta", + .id = "moss", + .dbPath = "/.moss/db/install", + .countQuery = "SELECT COUNT(*) FROM meta", }; - if (std::error_code errc; !fs::exists(mossInfo.db_path, errc)) { + if (std::error_code errc; !fs::exists(mossInfo.dbPath, errc)) { if (errc) { - warn_log("Filesystem error checking for Moss DB at '{}': {}", mossInfo.db_path.string(), errc.message()); - return Err(DraconisError(DraconisErrorCode::IoError, "Filesystem error checking Moss DB: " + errc.message())); + warn_log("Filesystem error checking for Moss DB at '{}': {}", mossInfo.dbPath.string(), errc.message()); + return Err(DracError(DracErrorCode::IoError, "Filesystem error checking Moss DB: " + errc.message())); } - debug_log("Moss database not found at '{}'. Assuming 0 Moss packages.", mossInfo.db_path.string()); - - return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Moss db not found: " + mossInfo.db_path.string())); + return Err(DracError(DracErrorCode::ApiUnavailable, "Moss db not found: " + mossInfo.dbPath.string())); } - debug_log("Moss database found at '{}'. Proceeding with count.", mossInfo.db_path.string()); + Result countResult = GetPackageCountInternalDb(mossInfo); - return GetPackageCountInternal(mossInfo); + if (!countResult) { + if (countResult.error().code != DracErrorCode::ParseError) + debug_at(countResult.error()); + + return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get package count from Moss DB")); + } + + return *countResult - 1; } - fn GetNixPackageCount() -> Result { + fn GetNixPackageCount() -> Result { debug_log("Attempting to get Nix package count."); + const PackageManagerInfo nixInfo = { - .id = "nix", - .db_path = "/nix/var/nix/db/db.sqlite", - .count_query = "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL", + .id = "nix", + .dbPath = "/nix/var/nix/db/db.sqlite", + .countQuery = "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL", }; - if (std::error_code errc; !fs::exists(nixInfo.db_path, errc)) { + if (std::error_code errc; !fs::exists(nixInfo.dbPath, errc)) { if (errc) { - warn_log("Filesystem error checking for Nix DB at '{}': {}", nixInfo.db_path.string(), errc.message()); - return Err(DraconisError(DraconisErrorCode::IoError, "Filesystem error checking Nix DB: " + errc.message())); + warn_log("Filesystem error checking for Nix DB at '{}': {}", nixInfo.dbPath.string(), errc.message()); + return Err(DracError(DracErrorCode::IoError, "Filesystem error checking Nix DB: " + errc.message())); } - debug_log("Nix database not found at '{}'. Assuming 0 Nix packages.", nixInfo.db_path.string()); - - return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Nix db not found: " + nixInfo.db_path.string())); + return Err(DracError(DracErrorCode::ApiUnavailable, "Nix db not found: " + nixInfo.dbPath.string())); } - debug_log("Nix database found at '{}'. Proceeding with count.", nixInfo.db_path.string()); - - return GetPackageCountInternal(nixInfo); + return GetPackageCountInternalDb(nixInfo); } - fn GetTotalPackageCount() -> Result { - debug_log("Attempting to get total package count from all package managers."); + fn GetPacmanPackageCount() -> Result { + return GetPackageCountInternalDir( + "Pacman", fs::current_path().root_path() / "var" / "lib" / "pacman" / "local", "", true + ); + } - const PackageManagerInfo mossInfo = { - .id = "moss", - .db_path = "/.moss/db/install", - .count_query = "SELECT COUNT(*) FROM meta", - }; + fn GetTotalPackageCount() -> Result { + using util::types::Array, util::types::Future; - const PackageManagerInfo nixInfo = { - .id = "nix", - .db_path = "/nix/var/nix/db/db.sqlite", - .count_query = "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL", + Array>, 4> futures = { + std::async(std::launch::async, GetDpkgPackageCount), + std::async(std::launch::async, GetMossPackageCount), + std::async(std::launch::async, GetNixPackageCount), + std::async(std::launch::async, GetPacmanPackageCount), }; u64 totalCount = 0; - if (Result mossCountResult = GetMossPackageCount(); mossCountResult) { - // `moss list installed` returns 1 less than the db count, - // so we subtract 1 for consistency. - totalCount += (*mossCountResult - 1); - } else { - debug_at(mossCountResult.error()); - } - - if (Result nixCountResult = GetNixPackageCount(); nixCountResult) { - totalCount += *nixCountResult; - } else { - debug_at(nixCountResult.error()); - } + for (Future>& fut : futures) try { + if (Result result = fut.get()) { + totalCount += *result; + } else { + if (result.error().code != DracErrorCode::ApiUnavailable) { + error_at(result.error()); + } else { + debug_at(result.error()); + } + } + } catch (const Exception& e) { + error_log("Caught exception while getting package count future: {}", e.what()); + } catch (...) { error_log("Caught unknown exception while getting package count future."); } return totalCount; } diff --git a/src/os/linux/pkg_count.hpp b/src/os/linux/pkg_count.hpp index 8e11879..39d3124 100644 --- a/src/os/linux/pkg_count.hpp +++ b/src/os/linux/pkg_count.hpp @@ -3,7 +3,9 @@ #ifdef __linux__ // clang-format off -#include +#include // std::filesystem::path +#include // glz::object +#include // glz::detail::Object #include "src/core/util/defs.hpp" #include "src/core/util/error.hpp" @@ -11,50 +13,65 @@ // clang-format on namespace os::linux { - using util::error::DraconisError; - using util::types::Result, util::types::u64; + using util::error::DracError; + using util::types::Result, util::types::u64, util::types::i64, util::types::String; + namespace fs = std::filesystem; struct PackageManagerInfo { - util::types::String id; - std::filesystem::path db_path; - util::types::String count_query; + String id; + fs::path dbPath; + String countQuery; + }; + + struct PkgCountCacheData { + u64 count {}; + i64 timestampEpochSeconds {}; + + // NOLINTBEGIN(readability-identifier-naming) + struct [[maybe_unused]] glaze { + using T = PkgCountCacheData; + + static constexpr glz::detail::Object value = + glz::object("count", &T::count, "timestamp", &T::timestampEpochSeconds); + }; + // NOLINTEND(readability-identifier-naming) }; // Get package count from dpkg (Debian/Ubuntu) - fn GetDpkgPackageCount() -> Result; + fn GetDpkgPackageCount() -> Result; // Get package count from RPM (Red Hat/Fedora/CentOS) - fn GetRpmPackageCount() -> Result; + fn GetRpmPackageCount() -> Result; // Get package count from pacman (Arch Linux) - fn GetPacmanPackageCount() -> Result; + fn GetPacmanPackageCount() -> Result; // Get package count from Portage (Gentoo) - fn GetPortagePackageCount() -> Result; + fn GetPortagePackageCount() -> Result; // Get package count from zypper (openSUSE) - fn GetZypperPackageCount() -> Result; + fn GetZypperPackageCount() -> Result; // Get package count from apk (Alpine) - fn GetApkPackageCount() -> Result; + fn GetApkPackageCount() -> Result; // Get package count from moss (AerynOS) - fn GetMossPackageCount() -> Result; + fn GetMossPackageCount() -> Result; // Get package count from nix - fn GetNixPackageCount() -> Result; + fn GetNixPackageCount() -> Result; // Get package count from flatpak - fn GetFlatpakPackageCount() -> Result; + fn GetFlatpakPackageCount() -> Result; // Get package count from snap - fn GetSnapPackageCount() -> Result; + fn GetSnapPackageCount() -> Result; // Get package count from AppImage - fn GetAppimagePackageCount() -> Result; + fn GetAppimagePackageCount() -> Result; // Get total package count from all available package managers - fn GetTotalPackageCount() -> Result; + fn GetTotalPackageCount() -> Result; } // namespace os::linux #endif // __linux__ diff --git a/src/os/os.hpp b/src/os/os.hpp index 971bec0..7bc81d1 100644 --- a/src/os/os.hpp +++ b/src/os/os.hpp @@ -14,88 +14,100 @@ * (found in linux.cpp, windows.cpp, macos.cpp). */ namespace os { - using util::error::DraconisError; + using util::error::DracError; 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. + * or a DracError 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 a DracError 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. - * @return An Option containing the detected DE name String, or None if detection fails or is not applicable. + * @return A Result containing the DE name String on success, + * or a DracError on failure (e.g., permission error, API error). */ - fn GetDesktopEnvironment() -> Option; + fn GetDesktopEnvironment() -> Result; /** * @brief Attempts to retrieve the window manager name. * @details On Linux, checks Wayland compositor or X11 WM properties. On Windows, returns "DWM" or similar. * On macOS, might return "Quartz Compositor" or a specific tiling WM name if active. - * @return An Option containing the detected WM name String, or None if detection fails. + * @return A Result containing the detected WM name String on success, + * or a DracError on failure (e.g., permission error, API error). */ - fn GetWindowManager() -> Option; + fn GetWindowManager() -> Result; /** * @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 A Result containing the shell name String on success, + * or a DracError on failure (e.g., permission error, API error). */ - fn GetShell() -> Option; + fn GetShell() -> Result; /** * @brief Gets a system identifier, often the hardware model or product family. * @details Examples: "MacBookPro18,3", "Latitude 5420", "ThinkPad T490". * Implementation varies: reads DMI info on Linux, registry on Windows, sysctl on macOS. * @return A Result containing the host/product identifier String on success, - * or an OsError on failure (e.g., permission reading DMI/registry, API error). + * or a DracError 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. * @details Examples: "5.15.0-76-generic", "10.0.22621", "23.1.0". * Uses uname() on Linux/macOS, WinRT/registry on Windows. * @return A Result containing the kernel version String on success, - * or an OsError on failure. + * or a DracError on failure. */ - fn GetKernelVersion() -> Result; + fn GetKernelVersion() -> Result; /** * @brief Gets the number of installed packages (Platform-specific). * @details On Linux, sums counts from various package managers. On other platforms, behavior may vary. * @return A Result containing the package count (u64) on success, - * or an OsError on failure (e.g., permission errors, command not found) - * or if not supported (OsErrorCode::NotSupported). + * or a DracError on failure (e.g., permission errors, command not found) + * or if not supported (DracErrorCode::NotSupported). */ - fn GetPackageCount() -> Result; + fn GetPackageCount() -> Result; /** * @brief Gets the disk usage for the primary/root filesystem. * @details Uses statvfs on Linux/macOS, GetDiskFreeSpaceExW on Windows. * @return A Result containing the DiskSpace struct (used/total bytes) on success, - * or an OsError on failure (e.g., filesystem not found, permission error). + * or a DracError on failure (e.g., filesystem not found, permission error). */ - fn GetDiskUsage() -> Result; + fn GetDiskUsage() -> Result; + + namespace shared { + /** + * @brief Gets the number of installed packages from OS-agnostic package managers. + * @details Currently only supports Cargo package manager. + * @return A Result containing the package count (u64) on success, + * or a DracError on failure (e.g., permission errors, command not found). + */ + fn GetPackageCount() -> Result; + } // namespace shared } // namespace os diff --git a/src/os/shared.cpp b/src/os/shared.cpp new file mode 100644 index 0000000..52b2c4e --- /dev/null +++ b/src/os/shared.cpp @@ -0,0 +1,40 @@ +#include + +#include "src/core/util/defs.hpp" +#include "src/core/util/error.hpp" +#include "src/core/util/helpers.hpp" +#include "src/core/util/logging.hpp" +#include "src/core/util/types.hpp" + +#include "os.hpp" + +namespace os::shared { + using util::error::DracError, util::error::DracErrorCode; + using util::types::u64, util::types::String, util::types::Result, util::types::Err; + + namespace fs = std::filesystem; + + fn GetPackageCount() -> Result { + using util::helpers::GetEnv; + + fs::path cargoPath {}; + + if (Result cargoHome = GetEnv("CARGO_HOME")) + cargoPath = fs::path(*cargoHome) / "bin"; + else if (Result homeDir = GetEnv("HOME")) + cargoPath = fs::path(*homeDir) / ".cargo" / "bin"; + + if (cargoPath.empty() || !fs::exists(cargoPath)) + return Err(DracError(DracErrorCode::NotFound, "Could not find cargo directory")); + + u64 count = 0; + + for (const fs::directory_entry& entry : fs::directory_iterator(cargoPath)) + if (entry.is_regular_file()) + ++count; + + debug_log("Found {} packages in cargo directory: {}", count, cargoPath.string()); + + return count; + } +} // namespace os::shared diff --git a/src/wrappers/dbus.hpp b/src/wrappers/dbus.hpp index f04c076..5be8e68 100644 --- a/src/wrappers/dbus.hpp +++ b/src/wrappers/dbus.hpp @@ -1,12 +1,13 @@ #pragma once +#include #ifdef __linux__ // clang-format off #include // DBus Library -#include // std::exchange +#include // std::exchange, std::forward #include // std::format -#include // va_list, va_start, va_end +#include // std::is_convertible_v #include "src/core/util/defs.hpp" #include "src/core/util/error.hpp" @@ -14,269 +15,375 @@ // clang-format on namespace dbus { - using util::error::DraconisError, util::error::DraconisErrorCode; + using util::error::DracError, util::error::DracErrorCode; using util::types::Option, util::types::Result, util::types::Err, util::types::String, util::types::i32, - util::types::None; + util::types::u32, util::types::None; /** * @brief RAII wrapper for DBusError. Automatically initializes and frees. */ - class ErrorGuard { - DBusError m_Err {}; - bool m_IsInitialized = false; + class Error { + DBusError m_err {}; + bool m_isInitialized = false; public: - ErrorGuard() : m_IsInitialized(true) { dbus_error_init(&m_Err); } + Error() : m_isInitialized(true) { dbus_error_init(&m_err); } - ~ErrorGuard() { - if (m_IsInitialized) - dbus_error_free(&m_Err); + ~Error() { + if (m_isInitialized) + dbus_error_free(&m_err); } - ErrorGuard(const ErrorGuard&) = delete; - fn operator=(const ErrorGuard&)->ErrorGuard& = delete; + Error(const Error&) = delete; + fn operator=(const Error&)->Error& = delete; - ErrorGuard(ErrorGuard&& other) noexcept : m_Err(other.m_Err), m_IsInitialized(other.m_IsInitialized) { - other.m_IsInitialized = false; - dbus_error_init(&other.m_Err); + Error(Error&& other) noexcept : m_err(other.m_err), m_isInitialized(other.m_isInitialized) { + other.m_isInitialized = false; + dbus_error_init(&other.m_err); } - fn operator=(ErrorGuard&& other) noexcept -> ErrorGuard& { + fn operator=(Error&& other) noexcept -> Error& { if (this != &other) { - if (m_IsInitialized) { - dbus_error_free(&m_Err); - } - m_Err = other.m_Err; - m_IsInitialized = other.m_IsInitialized; + if (m_isInitialized) + dbus_error_free(&m_err); - other.m_IsInitialized = false; - dbus_error_init(&other.m_Err); + m_err = other.m_err; + m_isInitialized = other.m_isInitialized; + + other.m_isInitialized = false; + dbus_error_init(&other.m_err); } return *this; } - [[nodiscard]] fn isSet() const -> bool { return m_IsInitialized && dbus_error_is_set(&m_Err); } + /** + * @brief Checks if the D-Bus error is set. + * @return True if an error is set, false otherwise. + */ + [[nodiscard]] fn isSet() const -> bool { return m_isInitialized && dbus_error_is_set(&m_err); } - [[nodiscard]] fn message() const -> const char* { return isSet() ? m_Err.message : ""; } + /** + * @brief Gets the error message. + * @return The error message string, or "" if not set or not initialized. + */ + [[nodiscard]] fn message() const -> const char* { return isSet() ? m_err.message : ""; } - [[nodiscard]] fn name() const -> const char* { return isSet() ? m_Err.name : ""; } + /** + * @brief Gets the error name. + * @return The error name string (e.g., "org.freedesktop.DBus.Error.Failed"), or "" if not set or not initialized. + */ + [[nodiscard]] fn name() const -> const char* { return isSet() ? m_err.name : ""; } - [[nodiscard]] fn get() -> DBusError* { return &m_Err; } - [[nodiscard]] fn get() const -> const DBusError* { return &m_Err; } + /** + * @brief Gets a pointer to the underlying DBusError. Use with caution. + * @return Pointer to the DBusError struct. + */ + [[nodiscard]] fn get() -> DBusError* { return &m_err; } + /** + * @brief Gets a const pointer to the underlying DBusError. + * @return Const pointer to the DBusError struct. + */ + [[nodiscard]] fn get() const -> const DBusError* { return &m_err; } - [[nodiscard]] fn toDraconisError(const DraconisErrorCode code = DraconisErrorCode::PlatformSpecific) const - -> DraconisError { + /** + * @brief Converts the D-Bus error to a DraconisError. + * @param code The DraconisError code to use if the D-Bus error is set. + * @return A DraconisError representing the D-Bus error, or an internal error if called when no D-Bus error is set. + */ + [[nodiscard]] fn toDraconisError(const DracErrorCode code = DracErrorCode::PlatformSpecific) const -> DracError { if (isSet()) return { code, std::format("D-Bus Error: {} ({})", message(), name()) }; - return { DraconisErrorCode::InternalError, "Attempted to convert non-set DBusErrorGuard" }; + return { DracErrorCode::InternalError, "Attempted to convert non-set ErrorGuard" }; + } + }; + + /** + * @brief RAII wrapper for DBusMessageIter. Encapsulates iterator operations. + * Note: This wrapper does *not* own the message, only the iterator state. + * It's designed to be used within the scope where the MessageGuard is valid. + */ + class MessageIter { + DBusMessageIter m_iter {}; + bool m_isValid = false; + + explicit MessageIter(const DBusMessageIter& iter, const bool isValid) : m_iter(iter), m_isValid(isValid) {} + + friend class Message; + + /** + * @brief Gets the value of a basic-typed argument. + * Unsafe: Caller must ensure 'value' points to memory suitable for the actual argument type. + * @param value Pointer to store the retrieved value. + */ + fn getBasic(void* value) -> void { + if (m_isValid) + dbus_message_iter_get_basic(&m_iter, value); + } + + public: + MessageIter(const MessageIter&) = delete; + fn operator=(const MessageIter&)->MessageIter& = delete; + MessageIter(MessageIter&&) = delete; + fn operator=(MessageIter&&)->MessageIter& = delete; + ~MessageIter() = default; + + /** + * @brief Checks if the iterator is validly initialized. + */ + [[nodiscard]] fn isValid() const -> bool { return m_isValid; } + + /** + * @brief Gets the D-Bus type code of the current argument. + * @return The D-Bus type code, or DBUS_TYPE_INVALID otherwise. + */ + [[nodiscard]] fn getArgType() -> int { + return m_isValid ? dbus_message_iter_get_arg_type(&m_iter) : DBUS_TYPE_INVALID; + } + + /** + * @brief Gets the element type of the container pointed to by the iterator. + * Only valid if the iterator points to an ARRAY or VARIANT. + * @return The D-Bus type code of the elements, or DBUS_TYPE_INVALID otherwise. + */ + [[nodiscard]] fn getElementType() -> int { + return m_isValid ? dbus_message_iter_get_element_type(&m_iter) : DBUS_TYPE_INVALID; + } + + /** + * @brief Advances the iterator to the next argument. + * @return True if successful (moved to a next element), false if at the end or iterator is invalid. + */ + fn next() -> bool { return m_isValid && dbus_message_iter_next(&m_iter); } + + /** + * @brief Recurses into a container-type argument (e.g., array, struct, variant). + * @return A new MessageIterGuard for the sub-container. The returned iterator might be invalid + * if the current element is not a container or the main iterator is invalid. + */ + [[nodiscard]] fn recurse() -> MessageIter { + if (!m_isValid) + return MessageIter({}, false); + + DBusMessageIter subIter; + dbus_message_iter_recurse(&m_iter, &subIter); + + return MessageIter(subIter, true); + } + + /** + * @brief Helper to safely get a string argument from the iterator. + * @return An Option containing the string value if the current arg is a valid string, or None otherwise. + */ + [[nodiscard]] fn getString() -> Option { + if (m_isValid && getArgType() == DBUS_TYPE_STRING) { + const char* strPtr = nullptr; + + // ReSharper disable once CppRedundantCastExpression + getBasic(static_cast(&strPtr)); + + if (strPtr) + return String(strPtr); + } + + return None; + } + }; + + /** + * @brief RAII wrapper for DBusMessage. Automatically unrefs. + */ + class Message { + DBusMessage* m_msg = nullptr; + + public: + explicit Message(DBusMessage* msg = nullptr) : m_msg(msg) {} + + ~Message() { + if (m_msg) + dbus_message_unref(m_msg); + } + + Message(const Message&) = delete; + fn operator=(const Message&)->Message& = delete; + + Message(Message&& other) noexcept : m_msg(std::exchange(other.m_msg, nullptr)) {} + + fn operator=(Message&& other) noexcept -> Message& { + if (this != &other) { + if (m_msg) + dbus_message_unref(m_msg); + m_msg = std::exchange(other.m_msg, nullptr); + } + return *this; + } + + /** + * @brief Gets the underlying DBusMessage pointer. Use with caution. + * @return The raw DBusMessage pointer, or nullptr if not holding a message. + */ + [[nodiscard]] fn get() const -> DBusMessage* { return m_msg; } + + /** + * @brief Initializes a message iterator for reading arguments from this message. + * @return A MessageIterGuard. Check iter.isValid() before use. + */ + [[nodiscard]] fn iterInit() const -> MessageIter { + if (!m_msg) + return MessageIter({}, false); + + DBusMessageIter iter; + const bool isValid = dbus_message_iter_init(m_msg, &iter); + return MessageIter(iter, isValid); + } + + /** + * @brief Appends arguments of basic types to the message. + * @tparam Args Types of the arguments to append. + * @param args The arguments to append. + * @return True if all arguments were appended successfully, false otherwise (e.g., allocation error). + */ + template + [[nodiscard]] fn appendArgs(Args&&... args) -> bool { + if (!m_msg) + return false; + + DBusMessageIter iter; + dbus_message_iter_init_append(m_msg, &iter); + + bool success = true; + ((success = success && appendArgInternal(iter, std::forward(args))), ...); // NOLINT + return success; + } + + /** + * @brief Creates a new D-Bus method call message. + * @param destination Service name (e.g., "org.freedesktop.Notifications"). Can be null. + * @param path Object path (e.g., "/org/freedesktop/Notifications"). Must not be null. + * @param interface Interface name (e.g., "org.freedesktop.Notifications"). Can be null. + * @param method Method name (e.g., "Notify"). Must not be null. + * @return Result containing a MessageGuard on success, or DraconisError on failure. + */ + static fn newMethodCall(const char* destination, const char* path, const char* interface, const char* method) + -> Result { + DBusMessage* rawMsg = dbus_message_new_method_call(destination, path, interface, method); + + if (!rawMsg) + return Err(DracError(DracErrorCode::OutOfMemory, "dbus_message_new_method_call failed (allocation failed?)")); + + return Message(rawMsg); + } + + private: + template + fn appendArgInternal(DBusMessageIter& iter, T&& arg) -> bool { + using DecayedT = std::decay_t; + + if constexpr (std::is_convertible_v) { + const char* valuePtr = static_cast(arg); + return dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &valuePtr); + } else { + static_assert(!sizeof(T*), "Unsupported type passed to appendArgs"); + return false; + } } }; /** * @brief RAII wrapper for DBusConnection. Automatically unrefs. */ - class ConnectionGuard { - DBusConnection* m_Conn = nullptr; + class Connection { + DBusConnection* m_conn = nullptr; public: - explicit ConnectionGuard(DBusConnection* conn = nullptr) : m_Conn(conn) {} + explicit Connection(DBusConnection* conn = nullptr) : m_conn(conn) {} - ~ConnectionGuard() { - if (m_Conn) - dbus_connection_unref(m_Conn); + ~Connection() { + if (m_conn) + dbus_connection_unref(m_conn); } - ConnectionGuard(const ConnectionGuard&) = delete; - fn operator=(const ConnectionGuard&)->ConnectionGuard& = delete; + Connection(const Connection&) = delete; + fn operator=(const Connection&)->Connection& = delete; - ConnectionGuard(ConnectionGuard&& other) noexcept : m_Conn(std::exchange(other.m_Conn, nullptr)) {} - fn operator=(ConnectionGuard&& other) noexcept -> ConnectionGuard& { + Connection(Connection&& other) noexcept : m_conn(std::exchange(other.m_conn, nullptr)) {} + + fn operator=(Connection&& other) noexcept -> Connection& { if (this != &other) { - if (m_Conn) - dbus_connection_unref(m_Conn); - m_Conn = std::exchange(other.m_Conn, nullptr); - } + if (m_conn) + dbus_connection_unref(m_conn); + m_conn = std::exchange(other.m_conn, nullptr); + } return *this; } - [[nodiscard]] fn get() const -> DBusConnection* { return m_Conn; } - explicit operator bool() const { return m_Conn != nullptr; } + /** + * @brief Gets the underlying DBusConnection pointer. Use with caution. + * @return The raw DBusConnection pointer, or nullptr if not holding a connection. + */ + [[nodiscard]] fn get() const -> DBusConnection* { return m_conn; } - [[nodiscard]] fn release() -> DBusConnection* { return std::exchange(m_Conn, nullptr); } - }; + /** + * @brief Sends a message and waits for a reply, blocking execution. + * @param message The D-Bus message guard to send. + * @param timeout_milliseconds Timeout duration in milliseconds. + * @return Result containing the reply MessageGuard on success, or DraconisError on failure. + */ + [[nodiscard]] fn sendWithReplyAndBlock(const Message& message, const i32 timeout_milliseconds = 1000) const + -> Result { + if (!m_conn || !message.get()) + return Err( + DracError(DracErrorCode::InvalidArgument, "Invalid connection or message provided to sendWithReplyAndBlock") + ); - /** - * @brief RAII wrapper for DBusMessage. Automatically unrefs. - */ - class MessageGuard { - DBusMessage* m_Msg = nullptr; + Error err; + DBusMessage* rawReply = + dbus_connection_send_with_reply_and_block(m_conn, message.get(), timeout_milliseconds, err.get()); - public: - explicit MessageGuard(DBusMessage* msg = nullptr) : m_Msg(msg) {} + if (err.isSet()) { + if (const char* errName = err.name()) { + if (strcmp(errName, DBUS_ERROR_TIMEOUT) == 0 || strcmp(errName, DBUS_ERROR_NO_REPLY) == 0) + return Err(err.toDraconisError(DracErrorCode::Timeout)); - ~MessageGuard() { - if (m_Msg) - dbus_message_unref(m_Msg); - } + if (strcmp(errName, DBUS_ERROR_SERVICE_UNKNOWN) == 0) + return Err(err.toDraconisError(DracErrorCode::NotFound)); - MessageGuard(const MessageGuard&) = delete; - fn operator=(const MessageGuard&)->MessageGuard& = delete; + if (strcmp(errName, DBUS_ERROR_ACCESS_DENIED) == 0) + return Err(err.toDraconisError(DracErrorCode::PermissionDenied)); + } - MessageGuard(MessageGuard&& other) noexcept : m_Msg(std::exchange(other.m_Msg, nullptr)) {} - fn operator=(MessageGuard&& other) noexcept -> MessageGuard& { - if (this != &other) { - if (m_Msg) - dbus_message_unref(m_Msg); - m_Msg = std::exchange(other.m_Msg, nullptr); + return Err(err.toDraconisError(DracErrorCode::PlatformSpecific)); } - return *this; + if (!rawReply) + return Err(DracError( + DracErrorCode::ApiUnavailable, + "dbus_connection_send_with_reply_and_block returned null without setting error (likely timeout or " + "disconnected)" + )); + + return Message(rawReply); } - [[nodiscard]] fn get() const -> DBusMessage* { return m_Msg; } - explicit operator bool() const { return m_Msg != nullptr; } + /** + * @brief Connects to a D-Bus bus type (Session or System). + * @param bus_type The type of bus (DBUS_BUS_SESSION or DBUS_BUS_SYSTEM). + * @return Result containing a ConnectionGuard on success, or DraconisError on failure. + */ + static fn busGet(const DBusBusType bus_type) -> Result { + Error err; + DBusConnection* rawConn = dbus_bus_get(bus_type, err.get()); - [[nodiscard]] fn release() -> DBusMessage* { return std::exchange(m_Msg, nullptr); } + if (err.isSet()) + return Err(err.toDraconisError(DracErrorCode::ApiUnavailable)); + + if (!rawConn) + return Err(DracError(DracErrorCode::ApiUnavailable, "dbus_bus_get returned null without setting error")); + + return Connection(rawConn); + } }; - - /** - * @brief Connects to a D-Bus bus type. - * @param bus_type The type of bus (e.g., DBUS_BUS_SESSION, DBUS_BUS_SYSTEM). - * @return Result containing a DBusConnectionGuard on success, or DraconisError on failure. - */ - inline fn BusGet(const DBusBusType bus_type) -> Result { - ErrorGuard err; - DBusConnection* rawConn = dbus_bus_get(bus_type, err.get()); - - if (err.isSet()) - return Err(err.toDraconisError(DraconisErrorCode::ApiUnavailable)); - - if (!rawConn) - return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "dbus_bus_get returned null without setting error")); - - return ConnectionGuard(rawConn); - } - - /** - * @brief Creates a new D-Bus method call message. - * @param destination Service name (e.g., "org.freedesktop.Notifications"). - * @param path Object path (e.g., "/org/freedesktop/Notifications"). - * @param interface Interface name (e.g., "org.freedesktop.Notifications"). - * @param method Method name (e.g., "Notify"). - * @return Result containing a DBusMessageGuard on success, or DraconisError on failure. - */ - inline fn MessageNewMethodCall(const char* destination, const char* path, const char* interface, const char* method) - -> Result { - DBusMessage* rawMsg = dbus_message_new_method_call(destination, path, interface, method); - if (!rawMsg) - return Err(DraconisError(DraconisErrorCode::InternalError, "dbus_message_new_method_call failed (allocation?)")); - - return MessageGuard(rawMsg); - } - - /** - * @brief Sends a message and waits for a reply. - * @param connection The D-Bus connection guard. - * @param message The D-Bus message guard to send. - * @param timeout_milliseconds Timeout duration. - * @return Result containing the reply DBusMessageGuard on success, or DraconisError on failure. - */ - inline fn ConnectionSendWithReplyAndBlock( - const ConnectionGuard& connection, - const MessageGuard& message, - const i32 timeout_milliseconds = 100 - ) -> Result { - ErrorGuard err; - DBusMessage* rawReply = - dbus_connection_send_with_reply_and_block(connection.get(), message.get(), timeout_milliseconds, err.get()); - - if (err.isSet()) - return Err(err.toDraconisError(DraconisErrorCode::ApiUnavailable)); - - if (!rawReply) - return Err(DraconisError( - DraconisErrorCode::ApiUnavailable, - "dbus_connection_send_with_reply_and_block returned null without setting error" - )); - - return MessageGuard(rawReply); - } - - /** - * @brief Appends arguments to a D-Bus message using varargs. - * @param message The message guard. - * @param first_arg_type The D-Bus type code of the first argument. - * @param ... Subsequent arguments (type code, value pointer, type code, value pointer...). - * End with DBUS_TYPE_INVALID. - * @return True on success, false on failure (e.g., allocation error). - */ - inline fn MessageAppendArgs(const MessageGuard& message, const int first_arg_type, ...) -> bool { - va_list args; - va_start(args, first_arg_type); - const bool result = dbus_message_append_args_valist(message.get(), first_arg_type, args); - va_end(args); - return result; - } - - using MessageIter = DBusMessageIter; - - /** - * @brief Initializes a message iterator. - * @param message The message guard. - * @param iter Pointer to the iterator to initialize. - * @return True if iterator is valid, false otherwise. - */ - inline fn MessageIterInit(const MessageGuard& message, MessageIter* iter) -> bool { - return dbus_message_iter_init(message.get(), iter); - } - - /** - * @brief Recurses into a container-type argument. - * @param iter The current iterator. - * @param sub_iter Pointer to the sub-iterator to initialize. - */ - inline fn MessageIterRecurse(MessageIter* iter, MessageIter* sub_iter) -> void { - dbus_message_iter_recurse(iter, sub_iter); - } - - /** - * @brief Gets the D-Bus type code of the current argument. - * @param iter The iterator. - * @return The type code (e.g., DBUS_TYPE_STRING, DBUS_TYPE_INVALID). - */ - inline fn MessageIterGetArgType(MessageIter* iter) -> int { return dbus_message_iter_get_arg_type(iter); } - - /** - * @brief Gets the value of a basic-typed argument. - * @param iter The iterator. - * @param value Pointer to store the retrieved value. Must match the argument type. - */ - inline fn MessageIterGetBasic(MessageIter* iter, void* value) -> void { dbus_message_iter_get_basic(iter, value); } - - /** - * @brief Advances the iterator to the next argument. - * @param iter The iterator. - * @return True if successful, false if at the end. - */ - inline fn MessageIterNext(MessageIter* iter) -> bool { return dbus_message_iter_next(iter); } - - /** - * @brief Helper to safely get a string argument from an iterator. - * @param iter The iterator positioned at a DBUS_TYPE_STRING argument. - * @return An Option containing the string value, or None if the type is wrong or value is null. - */ - inline fn MessageIterGetString(MessageIter* iter) -> Option { - if (MessageIterGetArgType(iter) == DBUS_TYPE_STRING) { - const char* strPtr = nullptr; - MessageIterGetBasic(iter, static_cast(&strPtr)); - if (strPtr) - return String(strPtr); - } - - return None; - } } // namespace dbus #endif // __linux__ diff --git a/src/wrappers/wayland.hpp b/src/wrappers/wayland.hpp index 500f1e2..4b9cf75 100644 --- a/src/wrappers/wayland.hpp +++ b/src/wrappers/wayland.hpp @@ -25,16 +25,16 @@ namespace wl { * Automatically handles resource acquisition and cleanup */ class DisplayGuard { - display* m_Display; + display* m_display; public: /** * Opens a Wayland display connection */ - DisplayGuard() : m_Display(connect(nullptr)) {} + DisplayGuard() : m_display(connect(nullptr)) {} ~DisplayGuard() { - if (m_Display) - disconnect(m_Display); + if (m_display) + disconnect(m_display); } // Non-copyable @@ -42,22 +42,22 @@ namespace wl { fn operator=(const DisplayGuard&)->DisplayGuard& = delete; // Movable - DisplayGuard(DisplayGuard&& other) noexcept : m_Display(std::exchange(other.m_Display, nullptr)) {} + 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); + if (m_display) + disconnect(m_display); - m_Display = std::exchange(other.m_Display, nullptr); + m_display = std::exchange(other.m_display, nullptr); } return *this; } - [[nodiscard]] explicit operator bool() const { return m_Display != nullptr; } + [[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); } + [[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 index ae3ffcd..8eedffd 100644 --- a/src/wrappers/xcb.hpp +++ b/src/wrappers/xcb.hpp @@ -89,17 +89,17 @@ namespace xcb { * Automatically handles resource acquisition and cleanup */ class DisplayGuard { - connection_t* m_Connection = nullptr; + 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)) {} + explicit DisplayGuard(const util::types::CStr name = nullptr) : m_connection(connect(name, nullptr)) {} ~DisplayGuard() { - if (m_Connection) - disconnect(m_Connection); + if (m_connection) + disconnect(m_connection); } // Non-copyable @@ -107,22 +107,22 @@ namespace xcb { fn operator=(const DisplayGuard&)->DisplayGuard& = delete; // Movable - DisplayGuard(DisplayGuard&& other) noexcept : m_Connection(std::exchange(other.m_Connection, nullptr)) {} + 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); + if (m_connection) + disconnect(m_connection); - m_Connection = std::exchange(other.m_Connection, nullptr); + m_connection = std::exchange(other.m_connection, nullptr); } return *this; } - [[nodiscard]] explicit operator bool() const { return m_Connection && !connection_has_error(m_Connection); } + [[nodiscard]] explicit operator bool() const { return m_connection && !connection_has_error(m_connection); } - [[nodiscard]] fn get() const -> connection_t* { return 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 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(); @@ -136,15 +136,15 @@ namespace xcb { */ template class ReplyGuard { - T* m_Reply = nullptr; + T* m_reply = nullptr; public: ReplyGuard() = default; - explicit ReplyGuard(T* reply) : m_Reply(reply) {} + explicit ReplyGuard(T* reply) : m_reply(reply) {} ~ReplyGuard() { - if (m_Reply) - free(m_Reply); + if (m_reply) + free(m_reply); } // Non-copyable @@ -152,22 +152,22 @@ namespace xcb { fn operator=(const ReplyGuard&)->ReplyGuard& = delete; // Movable - ReplyGuard(ReplyGuard&& other) noexcept : m_Reply(std::exchange(other.m_Reply, nullptr)) {} + 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); + if (m_reply) + free(m_reply); - m_Reply = std::exchange(other.m_Reply, nullptr); + m_reply = std::exchange(other.m_reply, nullptr); } return *this; } - [[nodiscard]] explicit operator bool() const { return m_Reply != nullptr; } + [[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; } + [[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 diff --git a/subprojects/dbus_cxx b/subprojects/dbus_cxx deleted file mode 160000 index 0285006..0000000 --- a/subprojects/dbus_cxx +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0285006ac71ba6b16d82a69d3927a85e1c104e4e diff --git a/subprojects/glaze b/subprojects/glaze deleted file mode 160000 index bcf97a0..0000000 --- a/subprojects/glaze +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bcf97a05c81ee249cff8a064a222480cadc25429 diff --git a/subprojects/glaze.wrap b/subprojects/glaze.wrap new file mode 100644 index 0000000..9858a5f --- /dev/null +++ b/subprojects/glaze.wrap @@ -0,0 +1,5 @@ +[wrap-file] +source_url = https://github.com/stephenberry/glaze/archive/refs/tags/v5.1.1.tar.gz +source_filename = glaze-5.1.1.tar.gz +source_hash = 7fed59aae4c09b27761c6c94e1e450ed30ddc4d7303ddc70591ec268d90512f5 +directory = glaze