diff --git a/flake.lock b/flake.lock index 8d188e4..cb22d13 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1745998881, - "narHash": "sha256-vonyYAKJSlsX4n9GCsS0pHxR6yCrfqBIuGvANlkwG6U=", + "lastModified": 1746576598, + "narHash": "sha256-FshoQvr6Aor5SnORVvh/ZdJ1Sa2U4ZrIMwKBX5k2wu0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "423d2df5b04b4ee7688c3d71396e872afa236a89", + "rev": "b3582c75c7f21ce0b429898980eddbbf05c68e55", "type": "github" }, "original": { @@ -59,11 +59,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1745929750, - "narHash": "sha256-k5ELLpTwRP/OElcLpNaFWLNf8GRDq4/eHBmFy06gGko=", + "lastModified": 1746216483, + "narHash": "sha256-4h3s1L/kKqt3gMDcVfN8/4v2jqHrgLIe4qok4ApH5x4=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "82bf32e541b30080d94e46af13d46da0708609ea", + "rev": "29ec5026372e0dec56f890e50dbe4f45930320fd", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 82ec1fb..4c6cd37 100644 --- a/flake.nix +++ b/flake.nix @@ -84,6 +84,7 @@ xorg.libxcb (mkOverridden "cmake" ftxui) + (mkOverridden "cmake" pugixml) (mkOverridden "cmake" sqlitecpp) (mkOverridden "meson" tomlplusplus) ]; @@ -193,6 +194,7 @@ ] ++ (with pkgsStatic; [ dbus + pugixml xorg.libxcb wayland ])); diff --git a/include/argparse.hpp b/include/argparse.hpp index add2589..10c24e5 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -1454,12 +1454,11 @@ namespace argparse { /** * @brief Get a pointer to this argument if it has choices - * @return Result containing a pointer to this argument or an error - * @details Returns an error if no choices have been added + * @return Pointer to this argument or nullptr if no choices have been added */ - fn choices() -> Result { + fn choices() -> Argument* { if (!m_choices.has_value() || m_choices.value().empty()) - return Err(DracError(DracErrorCode::InvalidArgument, "Zero choices provided")); + return nullptr; return this; } @@ -1470,10 +1469,10 @@ namespace argparse { * @tparam U Types of the remaining choices * @param first The first choice value * @param rest The remaining choice values - * @return Result containing a pointer to this argument or an error + * @return Pointer to this argument or nullptr if no choices have been added */ template - fn choices(T&& first, U&&... rest) -> Result { + fn choices(T&& first, U&&... rest) -> Argument* { add_choice(std::forward(first)); if constexpr (sizeof...(rest) == 0) { return choices(); @@ -1492,7 +1491,7 @@ namespace argparse { const auto& choices = m_choices.value(); if (m_default_value.has_value()) { - if (choices.find(m_default_value_str.value_or("")) == choices.end()) { + if (!choices.contains(m_default_value_str.value_or(""))) { const String choices_as_csv = std::accumulate(choices.begin(), choices.end(), String(), [](const String& a, const String& b) { return a + (a.empty() ? "" : ", ") + b; @@ -1696,67 +1695,41 @@ namespace argparse { * - Validates default values against choices */ [[nodiscard]] fn validate() const -> Result<> { - if (m_num_args_range.get_min() > m_num_args_range.get_max()) { + if (m_num_args_range.get_min() > m_num_args_range.get_max()) return Err(DracError(DracErrorCode::InvalidArgument, std::format("Invalid nargs range for argument '{}': min ({}) > max ({}). This indicates a configuration error when defining the argument.", m_names.empty() ? "UnnamedArgument" : m_names[0], m_num_args_range.get_min(), m_num_args_range.get_max()))); - } if (m_is_optional) { - if (!m_is_used && !m_default_value.has_value() && !m_implicit_value.has_value() && m_is_required) + if (!m_is_used && !m_default_value.has_value() && m_is_required) return Err(DracError(DracErrorCode::InvalidArgument, std::format("Required argument '{}' was not provided", m_names[0]))); if (m_is_used && m_is_required && m_values.empty()) return Err(DracError(DracErrorCode::InvalidArgument, std::format("Required argument '{}' requires a value, but none was provided", m_names[0]))); - - if (m_is_used && m_num_args_range.get_min() > m_values.size()) - return Err(DracError(DracErrorCode::InvalidArgument, std::format("Too few arguments for optional argument '{}'. Expected at least {}, got {}.", m_names[0], m_num_args_range.get_min(), m_values.size()))); } else { if (!m_num_args_range.contains(m_values.size()) && !m_default_value.has_value()) { String expected_str; - if (m_num_args_range.is_exact()) expected_str = std::to_string(m_num_args_range.get_min()); else if (!m_num_args_range.is_right_bounded()) expected_str = std::format("at least {}", m_num_args_range.get_min()); else expected_str = std::format("{} to {}", m_num_args_range.get_min(), m_num_args_range.get_max()); - return Err(DracError(DracErrorCode::InvalidArgument, std::format("Incorrect number of arguments for positional argument '{}'. Expected {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), expected_str, m_values.size()))); } - if (m_num_args_range.get_min() > m_values.size()) - return Err(DracError(DracErrorCode::InvalidArgument, std::format("Too few arguments for positional argument '{}'. Expected at least {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), m_num_args_range.get_min(), m_values.size()))); - } - - if (m_num_args_range.get_max() < m_values.size()) { - if (m_is_optional) - return Err(DracError(DracErrorCode::InvalidArgument, std::format("Too many arguments for optional argument '{}'. Expected at most {}, got {}.", m_names[0], m_num_args_range.get_max(), m_values.size()))); - - return Err(DracError(DracErrorCode::InvalidArgument, std::format("Too many arguments for positional argument '{}'. Expected at most {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), m_num_args_range.get_max(), m_values.size()))); + if (m_num_args_range.get_max() < m_values.size()) + return Err(DracError(DracErrorCode::InvalidArgument, std::format("Too many arguments for positional argument '{}'. Expected at most {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), m_num_args_range.get_max(), m_values.size()))); } if (m_choices.has_value()) { const auto& choices = m_choices.value(); if (m_default_value.has_value()) - if (const String& default_val_str = m_default_value_str.value(); choices.find(default_val_str) == choices.end()) { + if (const String& default_val_str = m_default_value_str.value(); !choices.contains(default_val_str)) { const String choices_as_csv = std::accumulate( choices.begin(), choices.end(), String(), [](const String& option_a, const String& option_b) -> String { return option_a + (option_a.empty() ? "" : ", ") + option_b; } ); return Err(DracError(DracErrorCode::InvalidArgument, std::format("Default value '{}' is not in the allowed choices: {{{}}}", default_val_str, choices_as_csv))); } - - for (const auto& value : m_values) { - if (value.index() != typeid(String).hash_code()) - return Err(DracError(DracErrorCode::InvalidArgument, std::format("Invalid argument type for choice validation - expected string, got '{}'", typeid(value).name()))); - - if (const String& value_str = std::get(value); choices.find(value_str) == choices.end()) { - const String choices_as_csv = std::accumulate( - choices.begin(), choices.end(), String(), [](const String& option_a, const String& option_b) -> String { return std::format("{}{}{}", option_a, option_a.empty() ? "" : ", ", option_b); } - ); - - return Err(DracError(DracErrorCode::InvalidArgument, std::format("Invalid argument '{}' - allowed options: {{{}}}", details::repr(value), choices_as_csv))); - } - } } return {}; @@ -2144,7 +2117,7 @@ namespace argparse { }; fn consume_digits = [=](StringView sd) -> StringView { - const auto it = std::ranges::find_if_not(sd, is_digit); + const char* const it = std::ranges::find_if_not(sd, is_digit); return sd.substr(static_cast(it - std::begin(sd))); }; @@ -3447,13 +3420,22 @@ namespace argparse { const String hypothetical_arg = { '-', compound_arg[j] }; auto arg_map_it2 = m_argument_map.find(hypothetical_arg); if (arg_map_it2 != m_argument_map.end()) { - const auto argument_iter2 = arg_map_it2->second; - Result consume_result = argument_iter2->consume(it, end, arg_map_it2->first); - if (!consume_result) - return Err(consume_result.error()); - it = consume_result.value(); - } else - return Err(DracError(DracErrorCode::InvalidArgument, std::format("Unknown argument: {} in compound {}", hypothetical_arg, current_argument))); + auto argument = arg_map_it2->second; + if (argument->m_num_args_range.get_max() == 0) { + // Flag: do not consume the next argument as a value + Result consume_result_flag = argument->consume(it, it, arg_map_it2->first); + if (!consume_result_flag) + return Err(consume_result_flag.error()); + it = consume_result_flag.value(); + } else { + // Option expects a value: consume as before + Result consume_result = argument->consume(it, end, arg_map_it2->first); + if (!consume_result) + return Err(consume_result.error()); + } + } else { + return Err(DracError(DracErrorCode::InvalidArgument, std::format("Unknown argument: {}", current_argument))); + } } } else return Err(DracError(DracErrorCode::InvalidArgument, std::format("Unknown argument: {}", current_argument))); diff --git a/meson.build b/meson.build index 703ee49..50a2df7 100644 --- a/meson.build +++ b/meson.build @@ -4,8 +4,8 @@ project( 'draconis++', 'cpp', - version : '0.1.0', - default_options : [ + version: '0.1.0', + default_options: [ 'default_library=static', 'buildtype=debugoptimized', 'b_vscrt=mt', @@ -35,35 +35,34 @@ common_warning_flags = [ ] common_cpp_flags = { - 'common' : [ + 'common': [ '-fno-strict-enums', '-fvisibility=hidden', '-fvisibility-inlines-hidden', '-std=c++26', ], - 'msvc' : [ - '-DNOMINMAX', - '/MT', + 'msvc': [ + '-DNOMINMAX', '/MT', '/Zc:__cplusplus', '/Zc:preprocessor', '/external:W0', '/external:anglebrackets', '/std:c++latest', ], - 'unix_extra' : '-march=native', - 'windows_extra' : '-DCURL_STATICLIB', + 'unix_extra': '-march=native', + 'windows_extra': '-DCURL_STATICLIB', } # Configure Objective-C++ for macOS if host_system == 'darwin' - add_languages('objcpp', native : false) + add_languages('objcpp', native: false) objcpp = meson.get_compiler('objcpp') objcpp_flags = common_warning_flags + [ '-std=c++26', '-fvisibility=hidden', '-fvisibility-inlines-hidden', ] - add_project_arguments(objcpp.get_supported_arguments(objcpp_flags), language : 'objcpp') + add_project_arguments(objcpp.get_supported_arguments(objcpp_flags), language: 'objcpp') endif # Apply C++ compiler arguments @@ -81,29 +80,29 @@ else endif endif -add_project_arguments(common_cpp_args, language : 'cpp') +add_project_arguments(common_cpp_args, language: 'cpp') # ------- # # Files # # ------- # base_sources = files( - 'src/core/system_data.cpp', - 'src/core/package.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', - 'src/ui/ui.cpp', + 'src/core/package.cpp', + 'src/core/system_data.cpp', 'src/main.cpp', + 'src/ui/ui.cpp', ) platform_sources = { - 'darwin' : ['src/os/macos.cpp', 'src/os/macos/bridge.mm'], - 'dragonfly' : ['src/os/bsd.cpp'], - 'freebsd' : ['src/os/bsd.cpp'], - 'haiku' : ['src/os/haiku.cpp'], - 'linux' : ['src/os/linux.cpp'], - 'netbsd' : ['src/os/bsd.cpp'], - 'serenity' : ['src/os/serenity.cpp'], - 'windows' : ['src/os/windows.cpp'], + 'darwin': ['src/os/macos.cpp', 'src/os/macos/bridge.mm'], + 'dragonfly': ['src/os/bsd.cpp'], + 'freebsd': ['src/os/bsd.cpp'], + 'haiku': ['src/os/haiku.cpp'], + 'linux': ['src/os/linux.cpp'], + 'netbsd': ['src/os/bsd.cpp'], + 'serenity': ['src/os/serenity.cpp'], + 'windows': ['src/os/windows.cpp'], } sources = base_sources + files(platform_sources.get(host_system, [])) @@ -112,9 +111,9 @@ sources = base_sources + files(platform_sources.get(host_system, [])) # Dependencies Config # # --------------------- # common_deps = [ - dependency('libcurl', include_type : 'system', static : true), - dependency('tomlplusplus', include_type : 'system', static : true), - dependency('openssl', include_type : 'system', static : true, required : false), + dependency('libcurl', include_type: 'system', static: true), + dependency('tomlplusplus', include_type: 'system', static: true), + dependency('openssl', include_type: 'system', static: true, required: false), ] # Platform-specific dependencies @@ -125,8 +124,8 @@ if host_system == 'darwin' dependency('SQLiteCpp'), dependency( 'appleframeworks', - modules : ['foundation', 'mediaplayer', 'systemconfiguration'], - static : true, + modules: ['foundation', 'mediaplayer', 'systemconfiguration'], + static: true, ), dependency('iconv'), ] @@ -138,6 +137,7 @@ elif host_system == 'windows' elif host_system != 'serenity' platform_deps += [ dependency('SQLiteCpp'), + dependency('pugixml'), dependency('xcb'), dependency('xau'), dependency('xdmcp'), @@ -150,28 +150,28 @@ endif ftxui_components = ['ftxui::screen', 'ftxui::dom', 'ftxui::component'] ftxui_dep = dependency( 'ftxui', - modules : ftxui_components, - include_type : 'system', - static : true, - required : false, + modules: ftxui_components, + include_type: 'system', + static: true, + required: false, ) if not ftxui_dep.found() ftxui_dep = declare_dependency( - dependencies : [ - dependency('ftxui-dom', fallback : ['ftxui', 'dom_dep']), - dependency('ftxui-screen', fallback : ['ftxui', 'screen_dep']), - dependency('ftxui-component', fallback : ['ftxui', 'component_dep']), + dependencies: [ + dependency('ftxui-dom', fallback: ['ftxui', 'dom_dep']), + dependency('ftxui-screen', fallback: ['ftxui', 'screen_dep']), + dependency('ftxui-component', fallback: ['ftxui', 'component_dep']), ], ) endif -glaze_dep = dependency('glaze', include_type : 'system', required : false) +glaze_dep = dependency('glaze', include_type: 'system', required: false) if not glaze_dep.found() cmake = import('cmake') glaze_proj = cmake.subproject('glaze') - glaze_dep = glaze_proj.dependency('glaze_glaze', include_type : 'system') + glaze_dep = glaze_proj.dependency('glaze_glaze', include_type: 'system') endif # Combine all dependencies @@ -197,8 +197,8 @@ endif executable( 'draconis++', sources, - objc_args : objc_args, - link_args : link_args, - dependencies : deps, - install : true, -) + objc_args: objc_args, + link_args: link_args, + dependencies: deps, + install: true, +) \ No newline at end of file diff --git a/src/core/package.cpp b/src/core/package.cpp index a8072c2..b1fbf25 100644 --- a/src/core/package.cpp +++ b/src/core/package.cpp @@ -6,6 +6,10 @@ #include // SQLite::Statement #endif +#ifdef __linux__ + #include +#endif + #include // std::chrono #include // std::filesystem #include // std::format @@ -144,8 +148,9 @@ namespace { if (count == 0) return Err(DracError(DracErrorCode::NotFound, std::format("No packages found in {} directory", pmId))); - const i64 nowEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); - const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds }; + const i64 timestampEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); + + const PkgCountCacheData dataToCache(count, timestampEpochSeconds); if (Result writeResult = WriteCache(pmId, dataToCache); !writeResult) debug_at(writeResult.error()); @@ -251,15 +256,99 @@ namespace package { debug_log("Successfully fetched {} package count: {}.", pmId, count); - const i64 nowEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); - const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds }; + const i64 timestampEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); + + const PkgCountCacheData dataToCache(count, timestampEpochSeconds); if (Result writeResult = WriteCache(cacheKey, dataToCache); !writeResult) debug_at(writeResult.error()); return count; } -#endif // __serenity__ +#endif // __serenity__ || _WIN32 + +#ifdef __linux__ + fn GetCountFromPlist(const String& pmId, const std::filesystem::path& plistPath) -> Result { + using namespace pugi; + using util::types::StringView; + + const String cacheKey = "pkg_count_" + pmId; + std::error_code fsErrCode; + + // Cache check + if (Result cachedDataResult = ReadCache(cacheKey)) { + const auto& [cachedCount, timestamp] = *cachedDataResult; + if (fs::exists(plistPath, fsErrCode) && !fsErrCode) { + const fs::file_time_type plistModTime = fs::last_write_time(plistPath, fsErrCode); + if (!fsErrCode) { + if (const std::chrono::system_clock::time_point cacheTimePoint = std::chrono::system_clock::time_point(std::chrono::seconds(timestamp)); + cacheTimePoint.time_since_epoch() >= plistModTime.time_since_epoch()) { + debug_log("Using valid {} plist count cache (file '{}' unchanged since {}). Count: {}", pmId, plistPath.string(), std::format("{:%F %T %Z}", std::chrono::floor(cacheTimePoint)), cachedCount); + return cachedCount; + } + } else { + warn_log("Could not get modification time for '{}': {}. Invalidating {} cache.", plistPath.string(), fsErrCode.message(), pmId); + } + } + } else if (cachedDataResult.error().code != DracErrorCode::NotFound) { + debug_at(cachedDataResult.error()); + } else { + debug_log("{} plist count cache not found or unreadable", pmId); + } + + // Parse plist and count + xml_document doc; + xml_parse_result result = doc.load_file(plistPath.c_str()); + + if (!result) + return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("Failed to parse plist file '{}': {}", plistPath.string(), result.description()))); + + xml_node dict = doc.child("plist").child("dict"); + + if (!dict) + return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("No in plist file '{}'.", plistPath.string()))); + + u64 count = 0; + + for (xml_node node = dict.first_child(); node; node = node.next_sibling()) { + if (StringView(node.name()) != "key") + continue; + + const StringView keyName = node.child_value(); + + if (keyName == "_XBPS_ALTERNATIVES_") + continue; + + xml_node pkgDict = node.next_sibling("dict"); + + if (!pkgDict) + continue; + + bool isInstalled = false; + + for (xml_node pkgNode = pkgDict.first_child(); pkgNode; pkgNode = pkgNode.next_sibling()) + if (StringView(pkgNode.name()) == "key" && StringView(pkgNode.child_value()) == "state") { + xml_node stateValue = pkgNode.next_sibling("string"); + if (stateValue && StringView(stateValue.child_value()) == "installed") { + isInstalled = true; + break; + } + } + + if (isInstalled) + ++count; + } + + if (count == 0) + return Err(util::error::DracError(util::error::DracErrorCode::NotFound, std::format("No installed packages found in plist file '{}'.", plistPath.string()))); + + const i64 timestampEpochSeconds = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + const PkgCountCacheData dataToCache(count, timestampEpochSeconds); + if (Result writeResult = WriteCache(cacheKey, dataToCache); !writeResult) + debug_at(writeResult.error()); + return count; + } +#endif // __linux__ #if defined(__linux__) || defined(__APPLE__) fn GetNixCount() -> Result { @@ -302,13 +391,14 @@ namespace package { Vec>> futures; #ifdef __linux__ - futures.push_back(std::async(std::launch::async, GetDpkgCount)); - futures.push_back(std::async(std::launch::async, GetPacmanCount)); - // futures.push_back(std::async(std::launch::async, GetRpmCount)); - // futures.push_back(std::async(std::launch::async, GetPortageCount)); - // futures.push_back(std::async(std::launch::async, GetZypperCount)); // futures.push_back(std::async(std::launch::async, GetApkCount)); + futures.push_back(std::async(std::launch::async, GetDpkgCount)); futures.push_back(std::async(std::launch::async, GetMossCount)); + futures.push_back(std::async(std::launch::async, GetPacmanCount)); + // futures.push_back(std::async(std::launch::async, GetPortageCount)); + futures.push_back(std::async(std::launch::async, GetRpmCount)); + futures.push_back(std::async(std::launch::async, GetXbpsCount)); + // futures.push_back(std::async(std::launch::async, GetZypperCount)); #elifdef __APPLE__ futures.push_back(std::async(std::launch::async, GetHomebrewCount)); futures.push_back(std::async(std::launch::async, GetMacPortsCount)); diff --git a/src/core/package.hpp b/src/core/package.hpp index a750efb..e7ebd3e 100644 --- a/src/core/package.hpp +++ b/src/core/package.hpp @@ -11,11 +11,7 @@ namespace package { namespace fs = std::filesystem; using util::error::DracError; - using util::types::Future; - using util::types::i64; - using util::types::Result; - using util::types::String; - using util::types::u64; + using util::types::Future, util::types::i64, util::types::Result, util::types::String, util::types::u64; /** * @struct PkgCountCacheData @@ -25,6 +21,9 @@ namespace package { u64 count {}; i64 timestampEpochSeconds {}; + PkgCountCacheData() = default; + PkgCountCacheData(u64 count, i64 timestampEpochSeconds) : count(count), timestampEpochSeconds(timestampEpochSeconds) {} + // NOLINTBEGIN(readability-identifier-naming) struct [[maybe_unused]] glaze { using T = PkgCountCacheData; @@ -102,13 +101,22 @@ namespace package { fn GetCountFromDirectory(const String& pmId, const fs::path& dirPath) -> Result; #ifdef __linux__ - fn GetDpkgCount() -> Result; - fn GetPacmanCount() -> Result; - fn GetMossCount() -> Result; - fn GetRpmCount() -> Result; - fn GetZypperCount() -> Result; - fn GetPortageCount() -> Result; fn GetApkCount() -> Result; + fn GetDpkgCount() -> Result; + fn GetMossCount() -> Result; + fn GetPacmanCount() -> Result; + // fn GetPortageCount() -> Result; + fn GetRpmCount() -> Result; + fn GetXbpsCount() -> Result; + // fn GetZypperCount() -> Result; + + /** + * @brief Counts installed packages in a plist file (used by xbps and potentially others). + * @param pmId Identifier for the package manager (for logging/cache). + * @param plistPath Path to the plist file. + * @return Result containing the count (u64) or a DracError. + */ + fn GetCountFromPlist(const String& pmId, const std::filesystem::path& plistPath) -> Result; #elifdef __APPLE__ fn GetHomebrewCount() -> Result; fn GetMacPortsCount() -> Result; diff --git a/src/main.cpp b/src/main.cpp index ea0f3e7..e97f1ad 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,4 @@ -#include // std::format +#include #include // ftxui::{Element, hbox, vbox, text, separator, filler, etc.} #include // ftxui::{Render} #include // ftxui::{Screen, Dimension::Full} @@ -19,57 +19,84 @@ #include "include/argparse.hpp" -using util::types::i32; +using util::types::i32, util::types::Exception; fn main(const i32 argc, char* argv[]) -> i32 { - using namespace ftxui; - using argparse::Argument, argparse::ArgumentParser; - using os::SystemData; - + try { #ifdef _WIN32 - winrt::init_apartment(); + winrt::init_apartment(); #endif - ArgumentParser parser("draconis", "0.1.0"); + argparse::ArgumentParser parser("draconis", "0.1.0"); - Argument& logLevel = parser .add_argument("--log-level") .help("Set the log level") - .default_value("info"); + .default_value("info") + .choices("debug", "info", "warn", "error"); - if (Result result = logLevel.choices("trace", "debug", "info", "warn", "error", "fatal"); !result) { - error_log("Error setting choices: {}", result.error().message); - return 1; - } + parser + .add_argument("-V", "--verbose") + .help("Enable verbose logging. Overrides --log-level.") + .flag(); - parser - .add_argument("-V", "--verbose") - .help("Enable verbose logging. Alias for --log-level=debug") - .flag(); + if (Result result = parser.parse_args(argc, argv); !result) { + error_at(result.error()); + return EXIT_FAILURE; + } - if (Result<> result = parser.parse_args(argc, argv); !result) { - error_log("Error parsing arguments: {}", result.error().message); - return 1; - } + bool verbose = parser.get("-V").value_or(false) || parser.get("--verbose").value_or(false); + Result logLevelStr = verbose ? "debug" : parser.get("--log-level"); - if (parser.get("--verbose").value_or(false) || parser.get("-v").value_or(false)) - info_log("Verbose logging enabled"); + { + using matchit::match, matchit::is, matchit::_; + using util::logging::LogLevel; + using enum util::logging::LogLevel; - const Config& config = Config::getInstance(); - const SystemData data = SystemData(config); - - Element document = ui::CreateUI(config, data); - - Screen screen = Screen::Create(Dimension::Full(), Dimension::Fit(document)); - Render(screen, document); - screen.Print(); - -#ifdef __cpp_lib_print - std::println(); + LogLevel minLevel = match(logLevelStr)( + is | "debug" = Debug, + is | "info" = Info, + is | "warn" = Warn, + is | "error" = Error, +#ifndef NDEBUG + is | _ = Debug #else - std::cout << '\n'; + is | _ = Info #endif + ); - return 0; + SetRuntimeLogLevel(minLevel); + } + + const Config& config = Config::getInstance(); + + const os::SystemData data = os::SystemData(config); + + { + using ftxui::Element, ftxui::Screen, ftxui::Render; + using ftxui::Dimension::Full, ftxui::Dimension::Fit; + + Element document = ui::CreateUI(config, data); + + Screen screen = Screen::Create(Full(), Fit(document)); + Render(screen, document); + screen.Print(); + } + + // Running the program as part of the shell's startup will cut + // off the last line of output, so we need to add a newline here. +#ifdef __cpp_lib_print + std::println(); +#else + std::cout << '\n'; +#endif + } catch (const DracError& e) { + error_at(e); + return EXIT_FAILURE; + } catch (const Exception& e) { + error_at(e); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; } diff --git a/src/os/linux.cpp b/src/os/linux.cpp index dac91fa..c57f210 100644 --- a/src/os/linux.cpp +++ b/src/os/linux.cpp @@ -15,6 +15,7 @@ #include // glz::read_beve #include // glz::write_beve #include // std::numeric_limits +#include // pugi::xml_document #include // std::{getline, string (String)} #include // std::string_view (StringView) #include // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED @@ -34,6 +35,8 @@ #include "src/wrappers/wayland.hpp" #include "src/wrappers/xcb.hpp" +#include "include/matchit.hpp" + #include "os.hpp" // clang-format on @@ -44,37 +47,34 @@ using util::helpers::GetEnv; namespace { fn GetX11WindowManager() -> Result { using namespace xcb; + using namespace matchit; + using enum ConnError; const DisplayGuard conn; if (!conn) - if (const i32 err = connection_has_error(conn.get())) - return Err(DracError(DracErrorCode::ApiUnavailable, [&] -> String { - if (const Option connErr = getConnError(err)) { - switch (*connErr) { - case Generic: return "Stream/Socket/Pipe Error"; - case ExtNotSupported: return "Extension Not Supported"; - case MemInsufficient: return "Insufficient Memory"; - case ReqLenExceed: return "Request Length Exceeded"; - case ParseErr: return "Display String Parse Error"; - case InvalidScreen: return "Invalid Screen"; - case FdPassingFailed: return "FD Passing Failed"; - default: return std::format("Unknown Error Code ({})", err); - } - } - - return std::format("Unknown Error Code ({})", err); - }())); + if (const i32 err = ConnectionHasError(conn.get())) + return Err( + DracError( + DracErrorCode::ApiUnavailable, + match(err)( + is | Generic = "Stream/Socket/Pipe Error", + is | ExtNotSupported = "Extension Not Supported", + is | MemInsufficient = "Insufficient Memory", + is | ReqLenExceed = "Request Length Exceeded", + is | ParseErr = "Display String Parse Error", + is | InvalidScreen = "Invalid Screen", + is | FdPassingFailed = "FD Passing Failed", + is | _ = std::format("Unknown Error Code ({})", err) + ) + ) + ); 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) - ); + const ReplyGuard reply(InternAtomReply(conn.get(), InternAtom(conn.get(), 0, static_cast(name.size()), name.data()), nullptr)); if (!reply) - return Err( - DracError(DracErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name)) - ); + return Err(DracError(DracErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name))); return reply->atom; }; @@ -96,27 +96,27 @@ namespace { return Err(DracError(DracErrorCode::PlatformSpecific, "Failed to get X11 atoms")); } - const ReplyGuard wmWindowReply(get_property_reply( + const ReplyGuard wmWindowReply(GetPropertyReply( conn.get(), - get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1), + GetProperty(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1), nullptr )); if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 || - get_property_value_length(wmWindowReply.get()) == 0) + GetPropertyValueLength(wmWindowReply.get()) == 0) return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property")); - const window_t wmRootWindow = *static_cast(get_property_value(wmWindowReply.get())); + const window_t wmRootWindow = *static_cast(GetPropertyValue(wmWindowReply.get())); - const ReplyGuard wmNameReply(get_property_reply( - conn.get(), get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr + const ReplyGuard wmNameReply(GetPropertyReply( + conn.get(), GetProperty(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr )); - if (!wmNameReply || wmNameReply->type != *utf8StringAtom || get_property_value_length(wmNameReply.get()) == 0) + if (!wmNameReply || wmNameReply->type != *utf8StringAtom || GetPropertyValueLength(wmNameReply.get()) == 0) 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()); + const char* nameData = static_cast(GetPropertyValue(wmNameReply.get())); + const usize length = GetPropertyValueLength(wmNameReply.get()); return String(nameData, length); } @@ -255,20 +255,23 @@ namespace os { Option activePlayer = None; { - Result listNamesResult = - Message::newMethodCall("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()); Result listNamesReplyResult = connection.sendWithReplyAndBlock(*listNamesResult, 100); + if (!listNamesReplyResult) return Err(listNamesReplyResult.error()); MessageIter iter = listNamesReplyResult->iterInit(); + if (!iter.isValid() || iter.getArgType() != DBUS_TYPE_ARRAY) return Err(DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Expected array")); MessageIter subIter = iter.recurse(); + if (!subIter.isValid()) return Err( DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Could not recurse into array") @@ -288,9 +291,7 @@ namespace os { if (!activePlayer) return Err(DracError(DracErrorCode::NotFound, "No active MPRIS players found")); - Result msgResult = Message::newMethodCall( - activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get" - ); + Result msgResult = Message::newMethodCall(activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); if (!msgResult) return Err(msgResult.error()); @@ -309,6 +310,7 @@ namespace os { Option artist = None; MessageIter propIter = replyResult->iterInit(); + if (!propIter.isValid()) return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply has no arguments or invalid iterator")); @@ -316,6 +318,7 @@ namespace os { return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply argument is not a variant")); MessageIter variantIter = propIter.recurse(); + if (!variantIter.isValid()) return Err(DracError(DracErrorCode::ParseError, "Could not recurse into variant")); @@ -323,11 +326,13 @@ namespace os { return Err(DracError(DracErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})")); MessageIter dictIter = variantIter.recurse(); + if (!dictIter.isValid()) return Err(DracError(DracErrorCode::ParseError, "Could not recurse into metadata dictionary array")); 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()) @@ -336,6 +341,7 @@ namespace os { } Option key = entryIter.getString(); + if (!key) { debug_log("Warning: Could not get key string from dict entry, skipping."); if (!dictIter.next()) @@ -350,6 +356,7 @@ namespace os { } MessageIter valueVariantIter = entryIter.recurse(); + if (!valueVariantIter.isValid()) { if (!dictIter.next()) break; @@ -362,9 +369,8 @@ namespace os { if (valueVariantIter.getArgType() == DBUS_TYPE_ARRAY && valueVariantIter.getElementType() == DBUS_TYPE_STRING) { if (MessageIter artistArrayIter = valueVariantIter.recurse(); artistArrayIter.isValid()) artist = artistArrayIter.getString(); - } else { + } else debug_log("Warning: Artist value was not an array of strings as expected."); - } } if (!dictIter.next()) @@ -426,32 +432,38 @@ namespace os { String line; if (!file) - return Err( - DracError(DracErrorCode::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) || line.empty()) - return Err( - DracError(DracErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)) - ); + return Err(DracError(DracErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path))); return line; }; - 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 - ) - )); - }); - }); + Result primaryResult = readFirstLine(primaryPath); + + if (primaryResult) + return primaryResult; + + DracError primaryError = primaryResult.error(); + + Result fallbackResult = readFirstLine(fallbackPath); + + if (fallbackResult) + return fallbackResult; + + DracError fallbackError = fallbackResult.error(); + + return Err(DracError( + DracErrorCode::InternalError, + std::format( + "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", + primaryPath, + primaryError.message, + fallbackPath, + fallbackError.message + ) + )); } fn GetKernelVersion() -> Result { @@ -505,6 +517,38 @@ namespace package { fn GetPacmanCount() -> Result { return GetCountFromDirectory("Pacman", fs::current_path().root_path() / "var" / "lib" / "pacman" / "local", true); } + + fn GetRpmCount() -> Result { + const PackageManagerInfo rpmInfo = { + .id = "rpm", + .dbPath = "/var/lib/rpm/rpmdb.sqlite", + .countQuery = "SELECT COUNT(*) FROM Installtid", + }; + + return GetCountFromDb(rpmInfo); + } + + fn GetXbpsCount() -> Result { + const StringView xbpsDbPath = "/var/db/xbps"; + const String pmId = "xbps"; + + if (!fs::exists(xbpsDbPath)) + return Err(DracError(DracErrorCode::NotFound, std::format("Xbps database path '{}' does not exist", xbpsDbPath))); + + fs::path plistPath; + for (const fs::directory_entry& entry : fs::directory_iterator(xbpsDbPath)) { + const String filename = entry.path().filename().string(); + if (filename.starts_with("pkgdb-") && filename.ends_with(".plist")) { + plistPath = entry.path(); + break; + } + } + + if (plistPath.empty()) + return Err(DracError(DracErrorCode::NotFound, "No Xbps database found")); + + return GetCountFromPlist(pmId, plistPath); + } } // namespace package #endif // __linux__ diff --git a/src/util/logging.hpp b/src/util/logging.hpp index dbc648b..7763002 100644 --- a/src/util/logging.hpp +++ b/src/util/logging.hpp @@ -71,10 +71,21 @@ namespace util::logging { * @enum LogLevel * @brief Represents different log levels. */ - enum class LogLevel : u8 { Debug, - Info, - Warn, - Error }; + enum class LogLevel : u8 { + Debug, + Info, + Warn, + Error, + }; + + inline fn GetRuntimeLogLevel() -> LogLevel& { + static LogLevel RuntimeLogLevel = LogLevel::Info; + return RuntimeLogLevel; + } + + inline fn SetRuntimeLogLevel(const LogLevel level) { + GetRuntimeLogLevel() = level; + } /** * @brief Directly applies ANSI color codes to text @@ -177,6 +188,9 @@ namespace util::logging { using namespace std::chrono; using std::filesystem::path; + if (level < GetRuntimeLogLevel()) + return; + const LockGuard lock(GetLogMutex()); const auto nowTp = system_clock::now(); diff --git a/src/wrappers/xcb.hpp b/src/wrappers/xcb.hpp index 521c783..7a271ad 100644 --- a/src/wrappers/xcb.hpp +++ b/src/wrappers/xcb.hpp @@ -24,50 +24,110 @@ namespace xcb { using get_property_cookie_t = xcb_get_property_cookie_t; using get_property_reply_t = xcb_get_property_reply_t; - constexpr atom_t ATOM_WINDOW = XCB_ATOM_WINDOW; + constexpr atom_t ATOM_WINDOW = XCB_ATOM_WINDOW; ///< Window atom + /** + * @brief Enum representing different types of connection errors + * + * This enum defines the possible types of errors that can occur when + * establishing or maintaining an XCB connection. Each error type + * corresponds to a specific error code defined in the XCB library. + */ enum ConnError : u8 { - Generic = XCB_CONN_ERROR, - ExtNotSupported = XCB_CONN_CLOSED_EXT_NOTSUPPORTED, - MemInsufficient = XCB_CONN_CLOSED_MEM_INSUFFICIENT, - ReqLenExceed = XCB_CONN_CLOSED_REQ_LEN_EXCEED, - ParseErr = XCB_CONN_CLOSED_PARSE_ERR, - InvalidScreen = XCB_CONN_CLOSED_INVALID_SCREEN, - FdPassingFailed = XCB_CONN_CLOSED_FDPASSING_FAILED, + Generic = XCB_CONN_ERROR, ///< Generic connection error + ExtNotSupported = XCB_CONN_CLOSED_EXT_NOTSUPPORTED, ///< Extension not supported + MemInsufficient = XCB_CONN_CLOSED_MEM_INSUFFICIENT, ///< Memory insufficient + ReqLenExceed = XCB_CONN_CLOSED_REQ_LEN_EXCEED, ///< Request length exceed + ParseErr = XCB_CONN_CLOSED_PARSE_ERR, ///< Parse error + InvalidScreen = XCB_CONN_CLOSED_INVALID_SCREEN, ///< Invalid screen + FdPassingFailed = XCB_CONN_CLOSED_FDPASSING_FAILED, ///< FD passing failed }; - // NOLINTBEGIN(readability-identifier-naming) - inline fn getConnError(const util::types::i32 code) -> util::types::Option { - switch (code) { - case XCB_CONN_ERROR: return Generic; - case XCB_CONN_CLOSED_EXT_NOTSUPPORTED: return ExtNotSupported; - case XCB_CONN_CLOSED_MEM_INSUFFICIENT: return MemInsufficient; - case XCB_CONN_CLOSED_REQ_LEN_EXCEED: return ReqLenExceed; - case XCB_CONN_CLOSED_PARSE_ERR: return ParseErr; - case XCB_CONN_CLOSED_INVALID_SCREEN: return InvalidScreen; - case XCB_CONN_CLOSED_FDPASSING_FAILED: return FdPassingFailed; - default: return None; - } - } - - inline fn connect(const char* displayname, int* screenp) -> connection_t* { + /** + * @brief Connect to an XCB display + * + * This function establishes a connection to an XCB display. It takes a + * display name and a pointer to an integer that will store the screen + * number. + * + * @param displayname The name of the display to connect to + * @param screenp Pointer to an integer that will store the screen number + * @return A pointer to the connection object + */ + inline fn Connect(const char* displayname, int* screenp) -> connection_t* { return xcb_connect(displayname, screenp); } - inline fn disconnect(connection_t* conn) -> void { + + /** + * @brief Disconnect from an XCB display + * + * This function disconnects from an XCB display. It takes a pointer to + * the connection object. + * + * @param conn The connection object to disconnect from + */ + inline fn Disconnect(connection_t* conn) -> void { xcb_disconnect(conn); } - inline fn connection_has_error(connection_t* conn) -> int { + + /** + * @brief Check if a connection has an error + * + * This function checks if a connection has an error. It takes a pointer + * to the connection object. + * + * @param conn The connection object to check + * @return 1 if the connection has an error, 0 otherwise + */ + inline fn ConnectionHasError(connection_t* conn) -> int { return xcb_connection_has_error(conn); } - inline fn intern_atom(connection_t* conn, const uint8_t only_if_exists, const uint16_t name_len, const char* name) + + /** + * @brief Intern an atom + * + * This function interns an atom. It takes a connection object, a flag + * + * @param conn The connection object to intern the atom on + * @param only_if_exists The flag to check if the atom exists + * @param name_len The length of the atom name + * @param name The name of the atom + * @return The cookie for the atom + */ + inline fn InternAtom(connection_t* conn, const uint8_t only_if_exists, const uint16_t name_len, const char* name) -> intern_atom_cookie_t { return xcb_intern_atom(conn, only_if_exists, name_len, name); } - inline fn intern_atom_reply(connection_t* conn, const intern_atom_cookie_t cookie, generic_error_t** err) + + /** + * @brief Get the reply for an interned atom + * + * This function gets the reply for an interned atom. It takes a connection + * object, a cookie, and a pointer to a generic error. + * + * @param conn The connection object + * @param cookie The cookie for the atom + * @param err The pointer to the generic error + * @return The reply for the atom + */ + inline fn InternAtomReply(connection_t* conn, const intern_atom_cookie_t cookie, generic_error_t** err) -> intern_atom_reply_t* { return xcb_intern_atom_reply(conn, cookie, err); } - inline fn get_property( + + /** + * @brief Get a property + * + * This function gets a property. It takes a connection object, a flag, + * a window, a property, a type, a long offset, and a long length. + * + * @param conn The connection object + * @param _delete The flag + * @param window The window + * @param property The property + * @param type The type + */ + inline fn GetProperty( connection_t* conn, const uint8_t _delete, const window_t window, @@ -78,24 +138,49 @@ namespace xcb { ) -> get_property_cookie_t { return xcb_get_property(conn, _delete, window, property, type, long_offset, long_length); } - inline fn get_property_reply(connection_t* conn, const get_property_cookie_t cookie, generic_error_t** err) + + /** + * @brief Get the reply for a property + * + * This function gets the reply for a property. It takes a connection + * object, a cookie, and a pointer to a generic error. + * + * @param conn The connection object + * @param cookie The cookie for the property + * @param err The pointer to the generic error + * @return The reply for the property + */ + inline fn GetPropertyReply(connection_t* conn, const get_property_cookie_t cookie, generic_error_t** err) -> get_property_reply_t* { return xcb_get_property_reply(conn, cookie, err); } - inline fn get_property_value_length(const get_property_reply_t* reply) -> int { + + /** + * @brief Get the value length for a property + * + * @param reply The reply for the property + * @return The value length for the property + */ + inline fn GetPropertyValueLength(const get_property_reply_t* reply) -> int { return xcb_get_property_value_length(reply); } - inline fn get_property_value(const get_property_reply_t* reply) -> void* { + + /** + * @brief Get the value for a property + * + * @param reply The reply for the property + * @return The value for the property + */ + inline fn GetPropertyValue(const get_property_reply_t* reply) -> void* { return xcb_get_property_value(reply); } - // NOLINTEND(readability-identifier-naming) /** * RAII wrapper for X11 Display connections * Automatically handles resource acquisition and cleanup */ class DisplayGuard { - connection_t* m_connection = nullptr; + connection_t* m_connection = nullptr; ///< The connection to the display public: /** @@ -103,10 +188,10 @@ namespace xcb { * @param name Display name (nullptr for default) */ explicit DisplayGuard(const util::types::CStr name = nullptr) - : m_connection(connect(name, nullptr)) {} + : m_connection(Connect(name, nullptr)) {} ~DisplayGuard() { if (m_connection) - disconnect(m_connection); + Disconnect(m_connection); } // Non-copyable @@ -116,28 +201,50 @@ namespace xcb { // Movable DisplayGuard(DisplayGuard&& other) noexcept : m_connection(std::exchange(other.m_connection, nullptr)) {} + + /** + * Move assignment operator + * @param other The other display guard + * @return The moved display guard + */ fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& { if (this != &other) { if (m_connection) - disconnect(m_connection); + Disconnect(m_connection); m_connection = std::exchange(other.m_connection, nullptr); } return *this; } + /** + * @brief Check if the display guard is valid + * @return True if the display guard is valid, false otherwise + */ [[nodiscard]] explicit operator bool() const { - return m_connection && !connection_has_error(m_connection); + return m_connection && !ConnectionHasError(m_connection); } + /** + * @brief Get the connection to the display + * @return The connection to the display + */ [[nodiscard]] fn get() const -> connection_t* { return m_connection; } + /** + * @brief Get the setup for the display + * @return The setup for the display + */ [[nodiscard]] fn setup() const -> const setup_t* { return m_connection ? xcb_get_setup(m_connection) : nullptr; } + /** + * @brief Get the root screen for the display + * @return The root screen for the display + */ [[nodiscard]] fn rootScreen() const -> screen_t* { const setup_t* setup = this->setup(); return setup ? xcb_setup_roots_iterator(setup).data : nullptr; @@ -150,13 +257,24 @@ namespace xcb { */ template class ReplyGuard { - T* m_reply = nullptr; + T* m_reply = nullptr; ///< The reply to the XCB request public: + /** + * @brief Default constructor + */ ReplyGuard() = default; + + /** + * @brief Constructor with a reply + * @param reply The reply to the XCB request + */ explicit ReplyGuard(T* reply) : m_reply(reply) {} + /** + * @brief Destructor + */ ~ReplyGuard() { if (m_reply) free(m_reply); @@ -169,6 +287,12 @@ namespace xcb { // Movable ReplyGuard(ReplyGuard&& other) noexcept : m_reply(std::exchange(other.m_reply, nullptr)) {} + + /** + * @brief Move assignment operator + * @param other The other reply guard + * @return The moved reply guard + */ fn operator=(ReplyGuard&& other) noexcept -> ReplyGuard& { if (this != &other) { if (m_reply) @@ -179,16 +303,34 @@ namespace xcb { return *this; } + /** + * @brief Check if the reply guard is valid + * @return True if the reply guard is valid, false otherwise + */ [[nodiscard]] explicit operator bool() const { return m_reply != nullptr; } + /** + * @brief Get the reply + * @return The reply + */ [[nodiscard]] fn get() const -> T* { return m_reply; } + + /** + * @brief Get the reply + * @return The reply + */ [[nodiscard]] fn operator->() const->T* { return m_reply; } + + /** + * @brief Dereference the reply + * @return The reply + */ [[nodiscard]] fn operator*() const->T& { return *m_reply; }