ough
This commit is contained in:
parent
3ad961a571
commit
3b16fee5f4
28 changed files with 1242 additions and 1047 deletions
10
.clang-tidy
10
.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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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=";
|
||||
};
|
||||
};
|
||||
}
|
18
flake.lock
generated
18
flake.lock
generated
|
@ -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": {
|
||||
|
|
23
flake.nix
23
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
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[dbus-cxx]
|
||||
src.github = "dbus-cxx/dbus-cxx"
|
||||
fetch.github = "dbus-cxx/dbus-cxx"
|
|
@ -1,12 +1,19 @@
|
|||
#include "config.hpp"
|
||||
|
||||
#include <filesystem> // std::filesystem::{path, operator/, exists, create_directories}
|
||||
#include <format> // std::{format, format_error}
|
||||
#include <fstream> // std::{ifstream, ofstream, operator<<}
|
||||
#include <pwd.h> // passwd, getpwuid
|
||||
#include <system_error> // std::error_code
|
||||
#include <toml++/impl/node_view.hpp> // toml::node_view
|
||||
#include <toml++/impl/parser.hpp> // toml::{parse_file, parse_result}
|
||||
#include <toml++/impl/table.hpp> // toml::table
|
||||
#include <unistd.h> // 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<fs::path> possiblePaths;
|
||||
|
||||
#ifdef _WIN32
|
||||
if (auto result = GetEnv("LOCALAPPDATA"))
|
||||
if (Result<String, DracError> result = GetEnv("LOCALAPPDATA"))
|
||||
possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml");
|
||||
|
||||
if (auto result = GetEnv("USERPROFILE")) {
|
||||
if (Result<String, DracError> 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<String, DracError> result = GetEnv("APPDATA"))
|
||||
possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml");
|
||||
#else
|
||||
if (Result<String, DraconisError> result = GetEnv("XDG_CONFIG_HOME"))
|
||||
if (Result<String, DracError> result = GetEnv("XDG_CONFIG_HOME"))
|
||||
possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml");
|
||||
|
||||
if (Result<String, DraconisError> result = GetEnv("HOME")) {
|
||||
if (Result<String, DracError> 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<String, DraconisError> envUser = util::helpers::GetEnv("USER");
|
||||
const Result<String, DraconisError> envLogname = util::helpers::GetEnv("LOGNAME");
|
||||
const Result<String, DracError> envUser = util::helpers::GetEnv("USER");
|
||||
const Result<String, DracError> 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.");
|
||||
|
@ -164,7 +171,7 @@ Config::Config(const toml::table& tbl) {
|
|||
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->nowPlaying = npTbl.is_table() ? NowPlaying::fromToml(*npTbl.as_table()) : NowPlaying {};
|
||||
this->weather = wthTbl.is_table() ? Weather::fromToml(*wthTbl.as_table()) : Weather {};
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String, DraconisError> envUser = GetEnv("USER"))
|
||||
if (Result<String, DracError> envUser = GetEnv("USER"))
|
||||
return *envUser;
|
||||
|
||||
// Finally, try to get the username using LOGNAME
|
||||
if (Result<String, DraconisError> envLogname = GetEnv("LOGNAME"))
|
||||
if (Result<String, DracError> 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 showTownName = false; ///< Flag to show the town name in the output.
|
||||
|
||||
/**
|
||||
* @brief Parses a TOML table to create a Weather instance.
|
||||
|
@ -123,8 +123,8 @@ struct Weather {
|
|||
if (!weather.enabled)
|
||||
return weather;
|
||||
|
||||
weather.api_key = *apiKey;
|
||||
weather.show_town_name = tbl["show_town_name"].value_or(false);
|
||||
weather.apiKey = *apiKey;
|
||||
weather.showTownName = tbl["show_town_name"].value_or(false);
|
||||
weather.units = tbl["units"].value_or("metric");
|
||||
|
||||
if (const toml::node_view<const toml::node> location = tbl["location"]) {
|
||||
|
@ -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<weather::Output, DracError>;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -162,7 +162,7 @@ struct Weather {
|
|||
struct Config {
|
||||
General general; ///< General configuration settings.
|
||||
Weather weather; ///< Weather configuration settings.`
|
||||
NowPlaying now_playing; ///< Now Playing configuration settings.
|
||||
NowPlaying nowPlaying; ///< Now Playing configuration settings.
|
||||
|
||||
Config() = default;
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#include "weather.hpp"
|
||||
|
||||
#include <chrono> // std::chrono::{duration, operator-}
|
||||
#include <curl/curl.h> // curl_easy_init, curl_easy_setopt, curl_easy_perform, curl_easy_cleanup
|
||||
#include <curl/curl.h> // curl_easy_setopt
|
||||
#include <curl/easy.h> // curl_easy_init, curl_easy_perform, curl_easy_cleanup
|
||||
#include <expected> // std::{expected (Result), unexpected (Err)}
|
||||
#include <filesystem> // std::filesystem::{path, remove, rename}
|
||||
#include <format> // std::format
|
||||
|
@ -9,14 +10,17 @@
|
|||
#include <glaze/beve/read.hpp> // glz::read_beve
|
||||
#include <glaze/beve/write.hpp> // glz::write_beve
|
||||
#include <glaze/core/context.hpp> // glz::{error_ctx, error_code}
|
||||
#include <glaze/core/opts.hpp> // glz::opts
|
||||
#include <glaze/core/reflect.hpp> // glz::format_error
|
||||
#include <glaze/json/read.hpp> // glz::write_json
|
||||
#include <glaze/json/read.hpp> // NOLINT(misc-include-cleaner) - glaze/json/read.hpp is needed for glz::read<glz::opts>
|
||||
#include <ios> // std::ios::{binary, trunc}
|
||||
#include <iterator> // std::istreambuf_iterator
|
||||
#include <system_error> // std::error_code
|
||||
#include <utility> // std::move
|
||||
#include <variant> // 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(
|
||||
return Err(std::format(
|
||||
"BEVE parse error reading cache (code {}): {}", static_cast<int>(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<Output, DracError> {
|
||||
using namespace std::chrono;
|
||||
using util::types::i32;
|
||||
|
||||
|
@ -178,7 +181,7 @@ fn Weather::getWeatherInfo() const -> Output {
|
|||
const Output& dataVal = *data;
|
||||
|
||||
if (const duration<double> cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt));
|
||||
cacheAge < 10min) {
|
||||
cacheAge < 60min) { // NOLINT(misc-include-cleaner) - inherited from <chrono>
|
||||
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<Output, String>& result) -> Output {
|
||||
if (!result) {
|
||||
error_log("API request failed: {}", result.error());
|
||||
return Output {};
|
||||
}
|
||||
fn handleApiResult = [](const Result<Output, String>& result) -> Result<Output, DracError> {
|
||||
if (!result)
|
||||
return Err(DracError(DracErrorCode::ApiUnavailable, result.error()));
|
||||
|
||||
if (Result<void, String> 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<Coords>(location)) {
|
||||
const auto& [lat, lon] = std::get<Coords>(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."));
|
||||
}
|
||||
|
|
|
@ -1,77 +1,55 @@
|
|||
#include "system_data.hpp"
|
||||
|
||||
#include <chrono> // std::chrono::{year_month_day, floor, days, system_clock}
|
||||
#include <locale> // std::locale
|
||||
#include <stdexcept> // std::runtime_error
|
||||
#include <chrono> // std::chrono::{year_month_day, days, floor, system_clock}
|
||||
#include <format> // std::format
|
||||
#include <future> // 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 <chrono>
|
||||
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<Result<String, DracError>> hostFut = std::async(async, GetHost);
|
||||
Future<Result<String, DracError>> kernelFut = std::async(async, GetKernelVersion);
|
||||
Future<Result<String, DracError>> osFut = std::async(async, GetOSVersion);
|
||||
Future<Result<u64, DracError>> memFut = std::async(async, GetMemInfo);
|
||||
Future<Result<String, DracError>> deFut = std::async(async, GetDesktopEnvironment);
|
||||
Future<Result<String, DracError>> wmFut = std::async(async, GetWindowManager);
|
||||
Future<Result<DiskSpace, DracError>> diskFut = std::async(async, GetDiskUsage);
|
||||
Future<Result<String, DracError>> shellFut = std::async(async, GetShell);
|
||||
Future<Result<u64, DracError>> pkgFut = std::async(async, GetPackageCount);
|
||||
Future<Result<MediaInfo, DracError>> npFut =
|
||||
std::async(config.nowPlaying.enabled ? async : deferred, GetNowPlaying);
|
||||
Future<Result<Output, DracError>> wthrFut =
|
||||
std::async(config.weather.enabled ? async : deferred, [&config] -> Result<Output, DracError> {
|
||||
return config.weather.getWeatherInfo();
|
||||
});
|
||||
|
||||
const year_month_day ymd = year_month_day { floor<days>(system_clock::now()) };
|
||||
|
||||
return std::format("{:%B %d}", ymd);
|
||||
}
|
||||
|
||||
fn log_timing(const std::string& name, const std::chrono::steady_clock::duration& duration) -> void {
|
||||
const auto millis = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(duration);
|
||||
debug_log("{} took: {} ms", name, millis.count());
|
||||
};
|
||||
|
||||
template <typename Func>
|
||||
fn time_execution(const std::string& name, Func&& func) {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
if constexpr (std::is_void_v<decltype(func())>) {
|
||||
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<MediaInfo, DraconisError>& 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;
|
||||
this->date = std::format("{:%B %d}", year_month_day { floor<days>(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"));
|
||||
}
|
||||
} // namespace os
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
#include <format> // 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,6 +58,7 @@ struct std::formatter<BytesToGiB> : std::formatter<double> {
|
|||
}
|
||||
};
|
||||
|
||||
namespace os {
|
||||
/**
|
||||
* @struct SystemData
|
||||
* @brief Holds various pieces of system information collected from the OS.
|
||||
|
@ -66,26 +67,23 @@ struct std::formatter<BytesToGiB> : std::formatter<double> {
|
|||
* in order to display it at all at once during runtime.
|
||||
*/
|
||||
struct SystemData {
|
||||
using NowPlayingResult = Option<Result<MediaInfo, util::error::DraconisError>>;
|
||||
|
||||
// clang-format off
|
||||
String date; ///< Current date (e.g., "April 26th"). Always expected to succeed.
|
||||
Result<String, util::error::DraconisError> host; ///< Host/product family (e.g., "MacBookPro18,3") or OS util::erroror.
|
||||
Result<String, util::error::DraconisError> kernel_version; ///< OS kernel version (e.g., "23.4.0") or OS error.
|
||||
Result<String, util::error::DraconisError> os_version; ///< OS pretty name (e.g., "macOS Sonoma 14.4.1") or OS error.
|
||||
Result<u64, util::error::DraconisError> mem_info; ///< Total physical RAM in bytes or OS error.
|
||||
Option<String> desktop_environment; ///< Detected desktop environment (e.g., "Aqua", "Plasma"). None if not detected/applicable.
|
||||
Option<String> window_manager; ///< Detected window manager (e.g., "Quartz Compositor", "KWin"). None if not detected/applicable.
|
||||
Result<DiskSpace, util::error::DraconisError> disk_usage; ///< Used/Total disk space for root filesystem or OS error.
|
||||
Option<String> 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::Output> weather_info; ///< Optional: Weather information. None if disabled or util::erroror during fetch.
|
||||
// clang-format on
|
||||
String date; ///< Current date (e.g., "April 26"). Always expected to succeed.
|
||||
Result<String, DracError> host; ///< Host/product family (e.g., "MacBook Air") or OS util::erroror.
|
||||
Result<String, DracError> kernelVersion; ///< OS kernel version (e.g., "6.14.4") or OS error.
|
||||
Result<String, DracError> osVersion; ///< OS pretty name (e.g., "Ubuntu 24.04.2 LTS") or OS error.
|
||||
Result<u64, DracError> memInfo; ///< Total physical RAM in bytes or OS error.
|
||||
Result<String, DracError> desktopEnv; ///< Desktop environment (e.g., "KDE") or None if not detected.
|
||||
Result<String, DracError> windowMgr; ///< Window manager (e.g., "KWin") or None if not detected.
|
||||
Result<DiskSpace, DracError> diskUsage; ///< Used/Total disk space for root filesystem or OS error.
|
||||
Result<String, DracError> shell; ///< Name of the current user shell (e.g., "zsh"). None if not detected.
|
||||
Result<u64, DracError> packageCount; ///< Total number of packages installed or OS error.
|
||||
Result<MediaInfo, DracError> nowPlaying; ///< Result of fetching media info.
|
||||
Result<weather::Output, DracError> weather; ///< Result of fetching weather info.
|
||||
|
||||
/**
|
||||
* @brief Fetches all system data asynchronously.
|
||||
* @param config The application configuration.
|
||||
* @return A populated SystemData object.
|
||||
* @brief Constructs a SystemData object and initializes its members.
|
||||
* @param config The configuration object containing settings for the system data.
|
||||
*/
|
||||
static fn fetchSystemData(const Config& config) -> SystemData;
|
||||
explicit SystemData(const Config& config);
|
||||
};
|
||||
} // namespace os
|
||||
|
|
|
@ -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<std::errc>(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;
|
||||
const DracErrorCode code = [&errNo] {
|
||||
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 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: code = DraconisErrorCode::NetworkError; break;
|
||||
default: code = DraconisErrorCode::PlatformSpecific; break;
|
||||
case ENETUNREACH: return DracErrorCode::NetworkError;
|
||||
default: return DracErrorCode::PlatformSpecific;
|
||||
}
|
||||
}();
|
||||
|
||||
return DraconisError { code, fullMsg, loc };
|
||||
return DracError { code, fullMsg, loc };
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
|
|
@ -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<String, DraconisError> {
|
||||
[[nodiscard]] inline fn GetEnv(CStr name) -> Result<String, DracError> {
|
||||
#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
|
||||
|
|
|
@ -148,7 +148,14 @@ namespace util::logging {
|
|||
* @param args The arguments for the format string.
|
||||
*/
|
||||
template <typename... Args>
|
||||
fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_string<Args...> fmt, Args&&... args) {
|
||||
fn LogImpl(
|
||||
const LogLevel level,
|
||||
#ifndef NDEBUG
|
||||
const std::source_location& loc,
|
||||
#endif
|
||||
std::format_string<Args...> 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<ErrorType>;
|
||||
|
||||
#ifndef NDEBUG
|
||||
std::source_location logLocation;
|
||||
#endif
|
||||
|
||||
String errorMessagePart;
|
||||
|
||||
if constexpr (std::is_same_v<DecayedErrorType, error::DraconisError>) {
|
||||
if constexpr (std::is_same_v<DecayedErrorType, error::DracError>) {
|
||||
#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<std::exception, DecayedErrorType>)
|
||||
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_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 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 error_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Error, error_obj);
|
||||
#endif
|
||||
} // namespace util::logging
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <array> // std::array (Array)
|
||||
#include <expected> // std::expected (Result)
|
||||
#include <future> // std::future (Future)
|
||||
#include <map> // std::map (Map)
|
||||
#include <memory> // std::shared_ptr and std::unique_ptr (SharedPointer, UniquePointer)
|
||||
#include <optional> // std::optional (Option)
|
||||
|
@ -113,6 +114,14 @@ namespace util::types {
|
|||
template <typename Tp, typename Dp = std::default_delete<Tp>>
|
||||
using UniquePointer = std::unique_ptr<Tp, Dp>;
|
||||
|
||||
/**
|
||||
* @typedef Future
|
||||
* @brief Alias for std::future<Tp>. Represents a value that will be available in the future.
|
||||
* @tparam Tp The type of the value.
|
||||
*/
|
||||
template <typename Tp>
|
||||
using Future = std::future<Tp>;
|
||||
|
||||
/**
|
||||
* @struct DiskSpace
|
||||
* @brief Represents disk usage information.
|
||||
|
@ -134,14 +143,9 @@ namespace util::types {
|
|||
struct MediaInfo {
|
||||
Option<String> title; ///< Track title.
|
||||
Option<String> artist; ///< Track artist(s).
|
||||
Option<String> album; ///< Album name.
|
||||
Option<String> app_name; ///< Name of the media player application (e.g., "Spotify", "Firefox").
|
||||
|
||||
MediaInfo() = default;
|
||||
|
||||
MediaInfo(Option<String> title, Option<String> artist) : title(std::move(title)), artist(std::move(artist)) {}
|
||||
|
||||
MediaInfo(Option<String> title, Option<String> artist, Option<String> album, Option<String> app)
|
||||
: title(std::move(title)), artist(std::move(artist)), album(std::move(album)), app_name(std::move(app)) {}
|
||||
};
|
||||
} // namespace util::types
|
||||
|
|
138
src/main.cpp
138
src/main.cpp
|
@ -6,12 +6,12 @@
|
|||
#include <ftxui/screen/screen.hpp> // ftxui::{Screen, Dimension::Full}
|
||||
#include <ranges> // 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,8 +46,9 @@ namespace ui {
|
|||
StringView music;
|
||||
StringView disk;
|
||||
StringView shell;
|
||||
StringView package;
|
||||
StringView desktop;
|
||||
StringView window_manager;
|
||||
StringView windowManager;
|
||||
};
|
||||
|
||||
[[maybe_unused]] static constexpr Icons NONE = {
|
||||
|
@ -62,8 +63,9 @@ namespace ui {
|
|||
.music = "",
|
||||
.disk = "",
|
||||
.shell = "",
|
||||
.package = "",
|
||||
.desktop = "",
|
||||
.window_manager = "",
|
||||
.windowManager = "",
|
||||
};
|
||||
|
||||
[[maybe_unused]] static constexpr Icons NERD = {
|
||||
|
@ -78,8 +80,9 @@ namespace ui {
|
|||
.music = " ",
|
||||
.disk = " ",
|
||||
.shell = " ",
|
||||
.package = " ",
|
||||
.desktop = " ",
|
||||
.window_manager = " ",
|
||||
.windowManager = " ",
|
||||
};
|
||||
|
||||
[[maybe_unused]] static constexpr Icons EMOJI = {
|
||||
|
@ -94,8 +97,9 @@ namespace ui {
|
|||
.music = " 🎵 ",
|
||||
.disk = " 💾 ",
|
||||
.shell = " 💲 ",
|
||||
.package = " 📦 ",
|
||||
.desktop = " 🖥️ ",
|
||||
.window_manager = " 🪟 ",
|
||||
.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(
|
||||
{
|
||||
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(
|
||||
{
|
||||
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(
|
||||
{
|
||||
if (weather.showTownName)
|
||||
content.push_back(hbox({
|
||||
text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon),
|
||||
text("Weather") | color(ui::DEFAULT_THEME.label),
|
||||
filler(),
|
||||
|
||||
hbox(
|
||||
{
|
||||
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(
|
||||
{
|
||||
content.push_back(hbox({
|
||||
text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon),
|
||||
text("Weather") | color(ui::DEFAULT_THEME.label),
|
||||
filler(),
|
||||
|
||||
hbox(
|
||||
{
|
||||
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<MediaInfo, DraconisError>& nowPlayingResult = *data.now_playing) {
|
||||
const MediaInfo& info = *nowPlayingResult;
|
||||
|
||||
const String title = info.title.value_or("Unknown Title");
|
||||
const String artist = info.artist.value_or("Unknown Artist");
|
||||
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;
|
||||
|
||||
content.push_back(separator() | color(ui::DEFAULT_THEME.border));
|
||||
content.push_back(hbox(
|
||||
{
|
||||
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());
|
||||
}));
|
||||
}
|
||||
|
||||
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<u64, DraconisError>& packageCount = os::GetPackageCount())
|
||||
debug_log("{}", *packageCount);
|
||||
else
|
||||
error_at(packageCount.error());
|
||||
const SystemData data = SystemData(config);
|
||||
|
||||
Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }) });
|
||||
|
||||
|
|
293
src/os/linux.cpp
293
src/os/linux.cpp
|
@ -2,10 +2,12 @@
|
|||
|
||||
// clang-format off
|
||||
#include <cstring> // std::strlen
|
||||
#include <dbus/dbus.h> // DBus::{DBusConnection, DBusMessage, DBusMessageIter, etc.}
|
||||
#include <dbus/dbus-shared.h> // DBUS_BUS_SESSION
|
||||
#include <dbus/dbus-protocol.h> // DBUS_TYPE_*
|
||||
#include <expected> // std::{unexpected, expected}
|
||||
#include <format> // std::{format, format_to_n}
|
||||
#include <fstream> // std::ifstream
|
||||
#include <climits> // PATH_MAX
|
||||
#include <limits> // std::numeric_limits
|
||||
#include <string> // std::{getline, string (String)}
|
||||
#include <string_view> // std::string_view (StringView)
|
||||
|
@ -14,9 +16,13 @@
|
|||
#include <sys/sysinfo.h> // sysinfo
|
||||
#include <sys/utsname.h> // utsname, uname
|
||||
#include <unistd.h> // readlink
|
||||
#include <utility> // 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<String, DraconisError> {
|
||||
fn GetX11WindowManager() -> Result<String, DracError> {
|
||||
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<ConnError> 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<atom_t, DraconisError> {
|
||||
fn internAtom = [&conn](const StringView name) -> Result<atom_t, DracError> {
|
||||
const ReplyGuard<intern_atom_reply_t> reply(
|
||||
intern_atom_reply(conn.get(), intern_atom(conn.get(), 0, static_cast<u16>(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<atom_t, DraconisError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
|
||||
const Result<atom_t, DraconisError> wmNameAtom = internAtom("_NET_WM_NAME");
|
||||
const Result<atom_t, DraconisError> utf8StringAtom = internAtom("UTF8_STRING");
|
||||
const Result<atom_t, DracError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
|
||||
const Result<atom_t, DracError> wmNameAtom = internAtom("_NET_WM_NAME");
|
||||
const Result<atom_t, DracError> 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<get_property_reply_t> 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<window_t*>(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<const char*>(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<String, DraconisError> {
|
||||
fn GetWaylandCompositor() -> Result<String, DracError> {
|
||||
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<char, 128> 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<char, PATH_MAX> exeRealPathBuf;
|
||||
Array<char, PATH_MAX> exeRealPathBuf; // NOLINT(misc-include-cleaner) - PATH_MAX is in <climits>
|
||||
|
||||
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<String, DraconisError> {
|
||||
fn GetOSVersion() -> Result<String, DracError> {
|
||||
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,198 +208,189 @@ 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<u64, DraconisError> {
|
||||
fn GetMemInfo() -> Result<u64, DracError> {
|
||||
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<u64>::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<MediaInfo, DraconisError> {
|
||||
Result<dbus::ConnectionGuard, DraconisError> connectionResult = dbus::BusGet(DBUS_BUS_SESSION);
|
||||
fn GetNowPlaying() -> Result<MediaInfo, DracError> {
|
||||
using namespace dbus;
|
||||
|
||||
Result<Connection, DracError> connectionResult = Connection::busGet(DBUS_BUS_SESSION);
|
||||
if (!connectionResult)
|
||||
return Err(connectionResult.error());
|
||||
|
||||
dbus::ConnectionGuard& connection = *connectionResult;
|
||||
const Connection& connection = *connectionResult;
|
||||
|
||||
Option<String> activePlayer = None;
|
||||
|
||||
{
|
||||
Result<dbus::MessageGuard, DraconisError> listNamesResult = dbus::MessageNewMethodCall(
|
||||
"org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"
|
||||
);
|
||||
Result<Message, DracError> listNamesResult =
|
||||
Message::newMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
|
||||
if (!listNamesResult)
|
||||
return Err(listNamesResult.error());
|
||||
|
||||
dbus::MessageGuard& listNames = *listNamesResult;
|
||||
|
||||
Result<dbus::MessageGuard, DraconisError> listNamesReplyResult =
|
||||
dbus::ConnectionSendWithReplyAndBlock(connection, listNames, 100);
|
||||
|
||||
Result<Message, DracError> 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<String> name = dbus::MessageIterGetString(&subIter))
|
||||
if (name->find("org.mpris.MediaPlayer2") != String::npos) {
|
||||
while (subIter.getArgType() != DBUS_TYPE_INVALID) {
|
||||
if (Option<String> name = subIter.getString())
|
||||
if (name->starts_with("org.mpris.MediaPlayer2.")) {
|
||||
activePlayer = std::move(*name);
|
||||
break;
|
||||
}
|
||||
|
||||
dbus::MessageIterNext(&subIter);
|
||||
}
|
||||
} else {
|
||||
return Err(DraconisError(DraconisErrorCode::ParseError, "Invalid DBus ListNames reply format"));
|
||||
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<dbus::MessageGuard, DraconisError> msgResult = dbus::MessageNewMethodCall(
|
||||
Result<Message, DracError> 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<dbus::MessageGuard, DraconisError> replyResult =
|
||||
dbus::ConnectionSendWithReplyAndBlock(connection, msg, 100);
|
||||
Result<Message, DracError> replyResult = connection.sendWithReplyAndBlock(msg, 100);
|
||||
|
||||
if (!replyResult)
|
||||
return Err(replyResult.error());
|
||||
|
||||
dbus::MessageGuard& reply = *replyResult;
|
||||
|
||||
Option<String> title = None;
|
||||
Option<String> 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<String> 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<String> 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);
|
||||
|
||||
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);
|
||||
MessageIter valueVariantIter = entryIter.recurse();
|
||||
if (!valueVariantIter.isValid()) {
|
||||
if (!dictIter.next())
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
dbus::MessageIterNext(&dictIter);
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!dictIter.next())
|
||||
break;
|
||||
}
|
||||
|
||||
return MediaInfo(std::move(title), std::move(artist));
|
||||
}
|
||||
|
||||
fn GetWindowManager() -> Option<String> {
|
||||
if (Result<String, DraconisError> waylandResult = GetWaylandCompositor())
|
||||
fn GetWindowManager() -> Result<String, DracError> {
|
||||
if (Result<String, DracError> waylandResult = GetWaylandCompositor())
|
||||
return *waylandResult;
|
||||
else
|
||||
debug_log("Could not detect Wayland compositor: {}", waylandResult.error().message);
|
||||
|
||||
if (Result<String, DraconisError> x11Result = GetX11WindowManager())
|
||||
if (Result<String, DracError> 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<String> {
|
||||
return util::helpers::GetEnv("XDG_CURRENT_DESKTOP")
|
||||
.transform([](const String& xdgDesktop) -> String {
|
||||
fn GetDesktopEnvironment() -> Result<String, DracError> {
|
||||
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<String, DraconisError> {
|
||||
return util::helpers::GetEnv("DESKTOP_SESSION");
|
||||
})
|
||||
.transform([](const String& finalValue) -> Option<String> {
|
||||
debug_log("Found desktop environment: {}", finalValue);
|
||||
return finalValue;
|
||||
})
|
||||
.value_or(None);
|
||||
.or_else([](const DracError&) -> Result<String, DracError> { return GetEnv("DESKTOP_SESSION"); });
|
||||
}
|
||||
|
||||
fn GetShell() -> Option<String> {
|
||||
if (const Result<String, DraconisError> shellPath = util::helpers::GetEnv("SHELL")) {
|
||||
fn GetShell() -> Result<String, DracError> {
|
||||
if (const Result<String, DracError> shellPath = GetEnv("SHELL")) {
|
||||
// clang-format off
|
||||
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
|
||||
{ "bash", "Bash" },
|
||||
|
@ -410,35 +408,34 @@ 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<String, DraconisError> {
|
||||
fn GetHost() -> Result<String, DracError> {
|
||||
constexpr CStr primaryPath = "/sys/class/dmi/id/product_family";
|
||||
constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name";
|
||||
|
||||
fn readFirstLine = [&](const String& path) -> Result<String, DraconisError> {
|
||||
fn readFirstLine = [&](const String& path) -> Result<String, DracError> {
|
||||
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<String, DraconisError> {
|
||||
return readFirstLine(fallbackPath)
|
||||
.or_else([&](const DraconisError& fallbackError) -> Result<String, DraconisError> {
|
||||
return Err(DraconisError(
|
||||
DraconisErrorCode::InternalError,
|
||||
return readFirstLine(primaryPath).or_else([&](const DracError& primaryError) -> Result<String, DracError> {
|
||||
return readFirstLine(fallbackPath).or_else([&](const DracError& fallbackError) -> Result<String, DracError> {
|
||||
return Err(DracError(
|
||||
DracErrorCode::InternalError,
|
||||
std::format(
|
||||
"Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}",
|
||||
primaryPath,
|
||||
|
@ -451,23 +448,23 @@ namespace os {
|
|||
});
|
||||
}
|
||||
|
||||
fn GetKernelVersion() -> Result<String, DraconisError> {
|
||||
fn GetKernelVersion() -> Result<String, DracError> {
|
||||
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<DiskSpace, DraconisError> {
|
||||
fn GetDiskUsage() -> Result<DiskSpace, DracError> {
|
||||
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<u64, DraconisError> { return linux::GetTotalPackageCount(); }
|
||||
fn GetPackageCount() -> Result<u64, DracError> {
|
||||
u64 count = 0;
|
||||
|
||||
if (Result<u64, DracError> linuxCount = linux::GetTotalPackageCount())
|
||||
count += *linuxCount;
|
||||
else
|
||||
return Err(linuxCount.error());
|
||||
|
||||
if (Result<u64, DracError> sharedCount = shared::GetPackageCount())
|
||||
count += *sharedCount;
|
||||
else
|
||||
return Err(sharedCount.error());
|
||||
|
||||
return count;
|
||||
}
|
||||
} // namespace os
|
||||
|
||||
#endif // __linux__
|
||||
|
|
|
@ -3,53 +3,53 @@
|
|||
// clang-format off
|
||||
#include "src/os/linux/pkg_count.hpp"
|
||||
|
||||
#include <SQLiteCpp/SQLiteCpp.h>
|
||||
#include <fstream>
|
||||
#include <glaze/beve/read.hpp>
|
||||
#include <glaze/beve/write.hpp>
|
||||
#include <glaze/core/common.hpp>
|
||||
#include <glaze/core/read.hpp>
|
||||
#include <SQLiteCpp/Database.h> // SQLite::{Database, OPEN_READONLY}
|
||||
#include <SQLiteCpp/Exception.h> // SQLite::Exception
|
||||
#include <SQLiteCpp/Statement.h> // SQLite::Statement
|
||||
#include <chrono> // std::chrono::{duration_cast, seconds, system_clock}
|
||||
#include <filesystem> // std::filesystem::{current_path, directory_entry, directory_iterator, etc.}
|
||||
#include <format> // std::format
|
||||
#include <fstream> // std::{ifstream, ofstream}
|
||||
#include <future> // std::{async, launch}
|
||||
#include <ios> // std::ios::{binary, trunc}, std::ios_base
|
||||
#include <iterator> // std::istreambuf_iterator
|
||||
#include <glaze/beve/read.hpp> // glz::read_beve
|
||||
#include <glaze/beve/write.hpp> // glz::write_beve
|
||||
#include <glaze/core/context.hpp> // glz::{context, error_code, error_ctx}
|
||||
#include <system_error> // 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<fs::path, DraconisError> {
|
||||
fn GetPkgCountCachePath(const String& pmId) -> Result<fs::path, DracError> {
|
||||
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<PkgCountCacheData, DraconisError> {
|
||||
Result<fs::path, DraconisError> cachePathResult = GetPkgCountCachePath(pm_id);
|
||||
fn ReadPkgCountCache(const String& pmId) -> Result<PkgCountCacheData, DracError> {
|
||||
Result<fs::path, DracError> 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<char>(ifs)), std::istreambuf_iterator<char>());
|
||||
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<int>(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<void, DraconisError> {
|
||||
fn WritePkgCountCache(const String& pmId, const PkgCountCacheData& data) -> Result<void, DracError> {
|
||||
using util::types::isize;
|
||||
|
||||
Result<fs::path, DraconisError> cachePathResult = GetPkgCountCachePath(pm_id);
|
||||
Result<fs::path, DracError> 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<int>(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<isize>(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<u64, DraconisError> {
|
||||
// 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<u64, DracError> {
|
||||
const auto& [pmId, dbPath, countQuery] = pmInfo;
|
||||
|
||||
// Try reading from cache using pm_id
|
||||
if (Result<PkgCountCacheData, DraconisError> cachedDataResult = ReadPkgCountCache(pmId)) {
|
||||
if (Result<PkgCountCacheData, DracError> 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<u64>(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<seconds>(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<void, DraconisError> writeResult = WritePkgCountCache(pmId, dataToCache); !writeResult) {
|
||||
warn_at(writeResult.error());
|
||||
warn_log("Failed to write {} package count to cache.", pmId);
|
||||
if (Result<void, DracError> writeResult = WritePkgCountCache(pmId, dataToCache); !writeResult)
|
||||
error_at(writeResult.error());
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
debug_log("Fetched fresh {} package count: {}", pmId, count);
|
||||
fn GetPackageCountInternalDir(
|
||||
const String& pmId,
|
||||
const fs::path& dirPath,
|
||||
const String& file_extension_filter = "",
|
||||
const bool subtract_one = false
|
||||
) -> Result<u64, DracError> {
|
||||
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()))
|
||||
);
|
||||
}
|
||||
|
||||
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<u64, DraconisError> {
|
||||
fn GetDpkgPackageCount() -> Result<u64, DracError> {
|
||||
return GetPackageCountInternalDir(
|
||||
"Dpkg", fs::current_path().root_path() / "var" / "lib" / "dpkg" / "info", String(".list")
|
||||
);
|
||||
}
|
||||
|
||||
fn GetMossPackageCount() -> Result<u64, DracError> {
|
||||
debug_log("Attempting to get Moss package count.");
|
||||
|
||||
const PackageManagerInfo mossInfo = {
|
||||
.id = "moss",
|
||||
.db_path = "/.moss/db/install",
|
||||
.count_query = "SELECT COUNT(*) FROM meta",
|
||||
.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<u64, DracError> 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"));
|
||||
}
|
||||
|
||||
fn GetNixPackageCount() -> Result<u64, DraconisError> {
|
||||
return *countResult - 1;
|
||||
}
|
||||
|
||||
fn GetNixPackageCount() -> Result<u64, DracError> {
|
||||
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",
|
||||
.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<u64, DraconisError> {
|
||||
debug_log("Attempting to get total package count from all package managers.");
|
||||
fn GetPacmanPackageCount() -> Result<u64, DracError> {
|
||||
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<u64, DracError> {
|
||||
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<Future<Result<u64, DracError>>, 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<u64, DraconisError> mossCountResult = GetMossPackageCount(); mossCountResult) {
|
||||
// `moss list installed` returns 1 less than the db count,
|
||||
// so we subtract 1 for consistency.
|
||||
totalCount += (*mossCountResult - 1);
|
||||
for (Future<Result<u64, DracError>>& fut : futures) try {
|
||||
if (Result<u64, DracError> result = fut.get()) {
|
||||
totalCount += *result;
|
||||
} else {
|
||||
debug_at(mossCountResult.error());
|
||||
}
|
||||
|
||||
if (Result<u64, DraconisError> nixCountResult = GetNixPackageCount(); nixCountResult) {
|
||||
totalCount += *nixCountResult;
|
||||
if (result.error().code != DracErrorCode::ApiUnavailable) {
|
||||
error_at(result.error());
|
||||
} else {
|
||||
debug_at(nixCountResult.error());
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
#ifdef __linux__
|
||||
|
||||
// clang-format off
|
||||
#include <filesystem>
|
||||
#include <filesystem> // std::filesystem::path
|
||||
#include <glaze/core/common.hpp> // glz::object
|
||||
#include <glaze/core/meta.hpp> // 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<u64, DraconisError>;
|
||||
fn GetDpkgPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from RPM (Red Hat/Fedora/CentOS)
|
||||
fn GetRpmPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetRpmPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from pacman (Arch Linux)
|
||||
fn GetPacmanPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetPacmanPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from Portage (Gentoo)
|
||||
fn GetPortagePackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetPortagePackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from zypper (openSUSE)
|
||||
fn GetZypperPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetZypperPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from apk (Alpine)
|
||||
fn GetApkPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetApkPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from moss (AerynOS)
|
||||
fn GetMossPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetMossPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from nix
|
||||
fn GetNixPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetNixPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from flatpak
|
||||
fn GetFlatpakPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetFlatpakPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from snap
|
||||
fn GetSnapPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetSnapPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get package count from AppImage
|
||||
fn GetAppimagePackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetAppimagePackageCount() -> Result<u64, DracError>;
|
||||
|
||||
// Get total package count from all available package managers
|
||||
fn GetTotalPackageCount() -> Result<u64, DraconisError>;
|
||||
fn GetTotalPackageCount() -> Result<u64, DracError>;
|
||||
} // namespace os::linux
|
||||
|
||||
#endif // __linux__
|
||||
|
|
|
@ -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<u64, DraconisError>;
|
||||
fn GetMemInfo() -> Result<u64, DracError>;
|
||||
|
||||
/**
|
||||
* @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<MediaInfo, DraconisError>;
|
||||
fn GetNowPlaying() -> Result<MediaInfo, DracError>;
|
||||
|
||||
/**
|
||||
* @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<String, DraconisError>;
|
||||
fn GetOSVersion() -> Result<String, DracError>;
|
||||
|
||||
/**
|
||||
* @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<String>;
|
||||
fn GetDesktopEnvironment() -> Result<String, DracError>;
|
||||
|
||||
/**
|
||||
* @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<String>;
|
||||
fn GetWindowManager() -> Result<String, DracError>;
|
||||
|
||||
/**
|
||||
* @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<String>;
|
||||
fn GetShell() -> Result<String, DracError>;
|
||||
|
||||
/**
|
||||
* @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<String, DraconisError>;
|
||||
fn GetHost() -> Result<String, DracError>;
|
||||
|
||||
/**
|
||||
* @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<String, DraconisError>;
|
||||
fn GetKernelVersion() -> Result<String, DracError>;
|
||||
|
||||
/**
|
||||
* @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<u64, DraconisError>;
|
||||
fn GetPackageCount() -> Result<u64, DracError>;
|
||||
|
||||
/**
|
||||
* @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<DiskSpace, DraconisError>;
|
||||
fn GetDiskUsage() -> Result<DiskSpace, DracError>;
|
||||
|
||||
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<u64, DracError>;
|
||||
} // namespace shared
|
||||
} // namespace os
|
||||
|
|
40
src/os/shared.cpp
Normal file
40
src/os/shared.cpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#include <filesystem>
|
||||
|
||||
#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<u64, DracError> {
|
||||
using util::helpers::GetEnv;
|
||||
|
||||
fs::path cargoPath {};
|
||||
|
||||
if (Result<String, DracError> cargoHome = GetEnv("CARGO_HOME"))
|
||||
cargoPath = fs::path(*cargoHome) / "bin";
|
||||
else if (Result<String, DracError> 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
|
|
@ -1,12 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#ifdef __linux__
|
||||
|
||||
// clang-format off
|
||||
#include <dbus/dbus.h> // DBus Library
|
||||
#include <utility> // std::exchange
|
||||
#include <utility> // std::exchange, std::forward
|
||||
#include <format> // std::format
|
||||
#include <cstdarg> // va_list, va_start, va_end
|
||||
#include <type_traits> // 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 DBusConnection. Automatically unrefs.
|
||||
* @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 ConnectionGuard {
|
||||
DBusConnection* m_Conn = nullptr;
|
||||
class MessageIter {
|
||||
DBusMessageIter m_iter {};
|
||||
bool m_isValid = false;
|
||||
|
||||
public:
|
||||
explicit ConnectionGuard(DBusConnection* conn = nullptr) : m_Conn(conn) {}
|
||||
explicit MessageIter(const DBusMessageIter& iter, const bool isValid) : m_iter(iter), m_isValid(isValid) {}
|
||||
|
||||
~ConnectionGuard() {
|
||||
if (m_Conn)
|
||||
dbus_connection_unref(m_Conn);
|
||||
}
|
||||
|
||||
ConnectionGuard(const ConnectionGuard&) = delete;
|
||||
fn operator=(const ConnectionGuard&)->ConnectionGuard& = delete;
|
||||
|
||||
ConnectionGuard(ConnectionGuard&& other) noexcept : m_Conn(std::exchange(other.m_Conn, nullptr)) {}
|
||||
fn operator=(ConnectionGuard&& other) noexcept -> ConnectionGuard& {
|
||||
if (this != &other) {
|
||||
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; }
|
||||
|
||||
[[nodiscard]] fn release() -> DBusConnection* { return std::exchange(m_Conn, nullptr); }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief RAII wrapper for DBusMessage. Automatically unrefs.
|
||||
*/
|
||||
class MessageGuard {
|
||||
DBusMessage* m_Msg = nullptr;
|
||||
|
||||
public:
|
||||
explicit MessageGuard(DBusMessage* msg = nullptr) : m_Msg(msg) {}
|
||||
|
||||
~MessageGuard() {
|
||||
if (m_Msg)
|
||||
dbus_message_unref(m_Msg);
|
||||
}
|
||||
|
||||
MessageGuard(const MessageGuard&) = delete;
|
||||
fn operator=(const MessageGuard&)->MessageGuard& = delete;
|
||||
|
||||
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 *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] fn get() const -> DBusMessage* { return m_Msg; }
|
||||
explicit operator bool() const { return m_Msg != nullptr; }
|
||||
|
||||
[[nodiscard]] fn release() -> DBusMessage* { return std::exchange(m_Msg, nullptr); }
|
||||
};
|
||||
|
||||
/**
|
||||
* @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<ConnectionGuard, DraconisError> {
|
||||
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<MessageGuard, DraconisError> {
|
||||
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<MessageGuard, DraconisError> {
|
||||
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); }
|
||||
friend class Message;
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* Unsafe: Caller must ensure 'value' points to memory suitable for the actual argument type.
|
||||
* @param value Pointer to store the retrieved value.
|
||||
*/
|
||||
inline fn MessageIterGetBasic(MessageIter* iter, void* value) -> void { dbus_message_iter_get_basic(iter, 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.
|
||||
* @param iter The iterator.
|
||||
* @return True if successful, false if at the end.
|
||||
* @return True if successful (moved to a next element), false if at the end or iterator is invalid.
|
||||
*/
|
||||
inline fn MessageIterNext(MessageIter* iter) -> bool { return dbus_message_iter_next(iter); }
|
||||
fn next() -> bool { return m_isValid && dbus_message_iter_next(&m_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.
|
||||
* @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.
|
||||
*/
|
||||
inline fn MessageIterGetString(MessageIter* iter) -> Option<String> {
|
||||
if (MessageIterGetArgType(iter) == DBUS_TYPE_STRING) {
|
||||
[[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<String> {
|
||||
if (m_isValid && getArgType() == DBUS_TYPE_STRING) {
|
||||
const char* strPtr = nullptr;
|
||||
MessageIterGetBasic(iter, static_cast<void*>(&strPtr));
|
||||
|
||||
// ReSharper disable once CppRedundantCastExpression
|
||||
getBasic(static_cast<void*>(&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 <typename... Args>
|
||||
[[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>(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<Message, DracError> {
|
||||
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 <typename T>
|
||||
fn appendArgInternal(DBusMessageIter& iter, T&& arg) -> bool {
|
||||
using DecayedT = std::decay_t<T>;
|
||||
|
||||
if constexpr (std::is_convertible_v<DecayedT, const char*>) {
|
||||
const char* valuePtr = static_cast<const char*>(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 Connection {
|
||||
DBusConnection* m_conn = nullptr;
|
||||
|
||||
public:
|
||||
explicit Connection(DBusConnection* conn = nullptr) : m_conn(conn) {}
|
||||
|
||||
~Connection() {
|
||||
if (m_conn)
|
||||
dbus_connection_unref(m_conn);
|
||||
}
|
||||
|
||||
Connection(const Connection&) = delete;
|
||||
fn operator=(const Connection&)->Connection& = delete;
|
||||
|
||||
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);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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; }
|
||||
|
||||
/**
|
||||
* @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<Message, DracError> {
|
||||
if (!m_conn || !message.get())
|
||||
return Err(
|
||||
DracError(DracErrorCode::InvalidArgument, "Invalid connection or message provided to sendWithReplyAndBlock")
|
||||
);
|
||||
|
||||
Error err;
|
||||
DBusMessage* rawReply =
|
||||
dbus_connection_send_with_reply_and_block(m_conn, message.get(), timeout_milliseconds, err.get());
|
||||
|
||||
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));
|
||||
|
||||
if (strcmp(errName, DBUS_ERROR_SERVICE_UNKNOWN) == 0)
|
||||
return Err(err.toDraconisError(DracErrorCode::NotFound));
|
||||
|
||||
if (strcmp(errName, DBUS_ERROR_ACCESS_DENIED) == 0)
|
||||
return Err(err.toDraconisError(DracErrorCode::PermissionDenied));
|
||||
}
|
||||
|
||||
return Err(err.toDraconisError(DracErrorCode::PlatformSpecific));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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<Connection, DracError> {
|
||||
Error err;
|
||||
DBusConnection* rawConn = dbus_bus_get(bus_type, err.get());
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
} // namespace dbus
|
||||
|
||||
#endif // __linux__
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 <typename T>
|
||||
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
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 0285006ac71ba6b16d82a69d3927a85e1c104e4e
|
|
@ -1 +0,0 @@
|
|||
Subproject commit bcf97a05c81ee249cff8a064a222480cadc25429
|
5
subprojects/glaze.wrap
Normal file
5
subprojects/glaze.wrap
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue