This commit is contained in:
Mars 2025-05-01 02:06:05 -04:00
parent 3ad961a571
commit 3b16fee5f4
Signed by: pupbrained
GPG key ID: 0FF5B8826803F895
28 changed files with 1242 additions and 1047 deletions

View file

@ -29,24 +29,24 @@ Checks: >
-readability-isolate-declaration, -readability-isolate-declaration,
-readability-magic-numbers -readability-magic-numbers
CheckOptions: CheckOptions:
cppcoreguidelines-avoid-do-while.IgnoreMacros: "true" cppcoreguidelines-avoid-do-while.IgnoreMacros: true
readability-else-after-return.WarnOnUnfixable: false readability-else-after-return.WarnOnUnfixable: false
readability-identifier-naming.ClassCase: CamelCase readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.EnumCase: CamelCase readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.LocalConstantCase: camelBack readability-identifier-naming.LocalConstantCase: camelBack
readability-identifier-naming.LocalVariableCase: camelBack readability-identifier-naming.LocalVariableCase: camelBack
readability-identifier-naming.GlobalFunctionCase: CamelCase readability-identifier-naming.GlobalFunctionCase: CamelCase
readability-identifier-naming.MemberCase: lower_case readability-identifier-naming.MemberCase: camelBack
readability-identifier-naming.MethodCase: camelBack readability-identifier-naming.MethodCase: camelBack
readability-identifier-naming.MethodIgnoredRegexp: ((to|from)_class) readability-identifier-naming.MethodIgnoredRegexp: ((to|from)_class)
readability-identifier-naming.ParameterPackCase: lower_case readability-identifier-naming.ParameterPackCase: lower_case
readability-identifier-naming.PrivateMemberCase: CamelCase readability-identifier-naming.PrivateMemberCase: camelBack
readability-identifier-naming.PrivateMemberPrefix: 'm_' readability-identifier-naming.PrivateMemberPrefix: 'm_'
readability-identifier-naming.PrivateMethodCase: CamelCase readability-identifier-naming.PrivateMethodCase: camelBack
readability-identifier-naming.PrivateMethodPrefix: '' readability-identifier-naming.PrivateMethodPrefix: ''
readability-identifier-naming.ProtectedMemberPrefix: 'm_' readability-identifier-naming.ProtectedMemberPrefix: 'm_'
readability-identifier-naming.ProtectedMethodPrefix: '' readability-identifier-naming.ProtectedMethodPrefix: ''
readability-identifier-naming.PublicMemberCase: lower_case readability-identifier-naming.PublicMemberCase: camelBack
readability-identifier-naming.StaticConstantCase: UPPER_CASE readability-identifier-naming.StaticConstantCase: UPPER_CASE
readability-identifier-naming.StaticVariableCase: CamelCase readability-identifier-naming.StaticVariableCase: CamelCase
readability-identifier-naming.StructCase: CamelCase readability-identifier-naming.StructCase: CamelCase

View file

@ -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"
}
}

View file

@ -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
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1745377448, "lastModified": 1745998881,
"narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=", "narHash": "sha256-vonyYAKJSlsX4n9GCsS0pHxR6yCrfqBIuGvANlkwG6U=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c", "rev": "423d2df5b04b4ee7688c3d71396e872afa236a89",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,11 +18,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1735554305, "lastModified": 1745377448,
"narHash": "sha256-zExSA1i/b+1NMRhGGLtNfFGXgLtgo+dcuzHzaWA6w3Q=", "narHash": "sha256-jhZDfXVKdD7TSEGgzFJQvEEZ2K65UMiqW5YJ2aIqxMA=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "0e82ab234249d8eee3e8c91437802b32c74bb3fd", "rev": "507b63021ada5fee621b6ca371c4fca9ca46f52c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -59,11 +59,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1744961264, "lastModified": 1745929750,
"narHash": "sha256-aRmUh0AMwcbdjJHnytg1e5h5ECcaWtIFQa6d9gI85AI=", "narHash": "sha256-k5ELLpTwRP/OElcLpNaFWLNf8GRDq4/eHBmFy06gGko=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "8d404a69efe76146368885110f29a2ca3700bee6", "rev": "82bf32e541b30080d94e46af13d46da0708609ea",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -18,7 +18,7 @@
system: system:
if system == "x86_64-linux" if system == "x86_64-linux"
then let then let
hostPkgs = import nixpkgs {inherit system;}; pkgs = import nixpkgs {inherit system;};
muslPkgs = import nixpkgs { muslPkgs = import nixpkgs {
system = "x86_64-linux-musl"; system = "x86_64-linux-musl";
overlays = [ overlays = [
@ -63,15 +63,15 @@
then ["-Ddefault_library=static"] then ["-Ddefault_library=static"]
else if buildSystem == "cmake" else if buildSystem == "cmake"
then [ then [
"-D${hostPkgs.lib.toUpper pkg.pname}_BUILD_EXAMPLES=OFF" "-D${pkgs.lib.toUpper pkg.pname}_BUILD_EXAMPLES=OFF"
"-D${hostPkgs.lib.toUpper pkg.pname}_BUILD_TESTS=OFF" "-D${pkgs.lib.toUpper pkg.pname}_BUILD_TESTS=OFF"
"-DBUILD_SHARED_LIBS=OFF" "-DBUILD_SHARED_LIBS=OFF"
] ]
else throw "Invalid build system: ${buildSystem}" else throw "Invalid build system: ${buildSystem}"
); );
})); }));
deps = with hostPkgs.pkgsStatic; [ deps = with pkgs.pkgsStatic; [
curlMinimal curlMinimal
dbus dbus
glaze glaze
@ -85,7 +85,6 @@
(mkOverridden "cmake" ftxui) (mkOverridden "cmake" ftxui)
(mkOverridden "cmake" sqlitecpp) (mkOverridden "cmake" sqlitecpp)
(mkOverridden "meson" libsigcxx30)
(mkOverridden "meson" tomlplusplus) (mkOverridden "meson" tomlplusplus)
]; ];
in { in {
@ -94,7 +93,6 @@
name = "draconis++"; name = "draconis++";
version = "0.1.0"; version = "0.1.0";
src = self; src = self;
NIX_ENFORCE_NO_NATIVE = 0;
nativeBuildInputs = with muslPkgs; [ nativeBuildInputs = with muslPkgs; [
cmake cmake
@ -123,29 +121,29 @@
devShell = muslPkgs.mkShell.override {inherit stdenv;} { devShell = muslPkgs.mkShell.override {inherit stdenv;} {
packages = packages =
(with hostPkgs; [bear cmake]) (with pkgs; [bear cmake])
++ (with muslPkgs; [ ++ (with muslPkgs; [
llvmPackages_20.clang-tools llvmPackages_20.clang-tools
meson meson
ninja ninja
pkg-config pkg-config
(hostPkgs.writeScriptBin "build" "meson compile -C build") (pkgs.writeScriptBin "build" "meson compile -C build")
(hostPkgs.writeScriptBin "clean" "meson setup build --wipe") (pkgs.writeScriptBin "clean" "meson setup build --wipe")
(hostPkgs.writeScriptBin "run" "meson compile -C build && build/draconis++") (pkgs.writeScriptBin "run" "meson compile -C build && build/draconis++")
]) ])
++ deps; ++ deps;
NIX_ENFORCE_NO_NATIVE = 0; NIX_ENFORCE_NO_NATIVE = 0;
}; };
formatter = treefmt-nix.lib.mkWrapper hostPkgs { formatter = treefmt-nix.lib.mkWrapper pkgs {
projectRootFile = "flake.nix"; projectRootFile = "flake.nix";
programs = { programs = {
alejandra.enable = true; alejandra.enable = true;
deadnix.enable = true; deadnix.enable = true;
clang-format = { clang-format = {
enable = true; enable = true;
package = hostPkgs.llvmPackages.clang-tools; package = pkgs.llvmPackages.clang-tools;
}; };
}; };
}; };
@ -185,7 +183,6 @@
] ]
++ (with pkgsStatic; [ ++ (with pkgsStatic; [
dbus dbus
libsigcxx30
sqlitecpp sqlitecpp
xorg.libxcb xorg.libxcb
wayland wayland

View file

@ -7,9 +7,10 @@ project(
version : '0.1.0', version : '0.1.0',
default_options : [ default_options : [
'default_library=static', 'default_library=static',
'buildtype=debugoptimized', 'buildtype=release',
'b_vscrt=mt', 'b_vscrt=mt',
'b_lto=true', 'b_lto=true',
'b_ndebug=if-release',
'warning_level=3', 'warning_level=3',
], ],
) )
@ -85,7 +86,7 @@ add_project_arguments(common_cpp_args, language : 'cpp')
# ------- # # ------- #
# Files # # 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 = { platform_sources = {
'linux' : ['src/os/linux.cpp', 'src/os/linux/pkg_count.cpp'], 'linux' : ['src/os/linux.cpp', 'src/os/linux/pkg_count.cpp'],
@ -128,7 +129,7 @@ elif host_system == 'linux'
dependency('xau'), dependency('xau'),
dependency('xdmcp'), dependency('xdmcp'),
dependency('wayland-client'), dependency('wayland-client'),
dependency('dbus-1', include_type: 'system'), dependency('dbus-1', include_type : 'system'),
] ]
endif endif

View file

@ -1,3 +0,0 @@
[dbus-cxx]
src.github = "dbus-cxx/dbus-cxx"
fetch.github = "dbus-cxx/dbus-cxx"

View file

@ -1,12 +1,19 @@
#include "config.hpp" #include "config.hpp"
#include <filesystem> // std::filesystem::{path, operator/, exists, create_directories} #include <filesystem> // std::filesystem::{path, operator/, exists, create_directories}
#include <fstream> // std::{ifstream, ofstream, operator<<} #include <format> // std::{format, format_error}
#include <system_error> // std::error_code #include <fstream> // std::{ifstream, ofstream, operator<<}
#include <toml++/impl/parser.hpp> // toml::{parse_file, parse_result} #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/helpers.hpp"
#include "src/core/util/logging.hpp" #include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp"
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -42,21 +49,21 @@ location = "London" # Your city name
Vec<fs::path> possiblePaths; Vec<fs::path> possiblePaths;
#ifdef _WIN32 #ifdef _WIN32
if (auto result = GetEnv("LOCALAPPDATA")) if (Result<String, DracError> result = GetEnv("LOCALAPPDATA"))
possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml"); 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) / ".config" / "draconis++" / "config.toml");
possiblePaths.push_back(fs::path(*result) / "AppData" / "Local" / "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"); possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml");
#else #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"); 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) / ".config" / "draconis++" / "config.toml");
possiblePaths.emplace_back(fs::path(*result) / ".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()); const passwd* pwd = getpwuid(getuid());
CStr pwdName = pwd ? pwd->pw_name : nullptr; CStr pwdName = pwd ? pwd->pw_name : nullptr;
const Result<String, DraconisError> envUser = util::helpers::GetEnv("USER"); const Result<String, DracError> envUser = util::helpers::GetEnv("USER");
const Result<String, DraconisError> envLogname = util::helpers::GetEnv("LOGNAME"); const Result<String, DracError> envLogname = util::helpers::GetEnv("LOGNAME");
defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User"; defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User";
#endif #endif
@ -124,13 +131,13 @@ location = "London" # Your city name
} }
try { try {
const std::string formattedConfig = std::format(defaultConfigTemplate, defaultName); const String formattedConfig = std::format(defaultConfigTemplate, defaultName);
file << formattedConfig; file << formattedConfig;
} catch (const std::format_error& fmtErr) { } catch (const std::format_error& fmtErr) {
error_log("Failed to format default config string: {}. Using fallback name 'User'.", fmtErr.what()); error_log("Failed to format default config string: {}. Using fallback name 'User'.", fmtErr.what());
try { try {
const std::string fallbackConfig = std::format(defaultConfigTemplate, "User"); const String fallbackConfig = std::format(defaultConfigTemplate, "User");
file << fallbackConfig; file << fallbackConfig;
} catch (...) { } catch (...) {
error_log("Failed to format default config even with fallback name."); error_log("Failed to format default config even with fallback name.");
@ -163,9 +170,9 @@ Config::Config(const toml::table& tbl) {
const toml::node_view npTbl = tbl["now_playing"]; const toml::node_view npTbl = tbl["now_playing"];
const toml::node_view wthTbl = tbl["weather"]; const toml::node_view wthTbl = tbl["weather"];
this->general = genTbl.is_table() ? General::fromToml(*genTbl.as_table()) : General {}; 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 {}; this->weather = wthTbl.is_table() ? Weather::fromToml(*wthTbl.as_table()) : Weather {};
} }
fn Config::getInstance() -> Config { fn Config::getInstance() -> Config {

View file

@ -22,7 +22,7 @@
#include "weather.hpp" #include "weather.hpp"
using util::error::DraconisError; using util::error::DracError;
using util::types::String, util::types::Array, util::types::Option, util::types::Result; 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). /// 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; return pwd->pw_name;
// Try to get the username using environment variables // Try to get the username using environment variables
if (Result<String, DraconisError> envUser = GetEnv("USER")) if (Result<String, DracError> envUser = GetEnv("USER"))
return *envUser; return *envUser;
// Finally, try to get the username using LOGNAME // Finally, try to get the username using LOGNAME
if (Result<String, DraconisError> envLogname = GetEnv("LOGNAME")) if (Result<String, DracError> envLogname = GetEnv("LOGNAME"))
return *envLogname; return *envLogname;
// If all else fails, return a default name // If all else fails, return a default name
@ -102,11 +102,11 @@ struct NowPlaying {
*/ */
struct Weather { struct Weather {
Location location; ///< Location for weather data, can be a city name or coordinates. 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". String units; ///< Units for temperature, either "metric" or "imperial".
bool enabled = false; ///< Flag to enable or disable the Weather feature. 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. * @brief Parses a TOML table to create a Weather instance.
@ -123,9 +123,9 @@ struct Weather {
if (!weather.enabled) if (!weather.enabled)
return weather; return weather;
weather.api_key = *apiKey; weather.apiKey = *apiKey;
weather.show_town_name = tbl["show_town_name"].value_or(false); weather.showTownName = tbl["show_town_name"].value_or(false);
weather.units = tbl["units"].value_or("metric"); weather.units = tbl["units"].value_or("metric");
if (const toml::node_view<const toml::node> location = tbl["location"]) { if (const toml::node_view<const toml::node> location = tbl["location"]) {
if (location.is_string()) if (location.is_string())
@ -152,7 +152,7 @@ struct Weather {
* API key, and units. It returns a WeatherOutput object containing the * API key, and units. It returns a WeatherOutput object containing the
* retrieved weather data. * retrieved weather data.
*/ */
[[nodiscard]] fn getWeatherInfo() const -> weather::Output; [[nodiscard]] fn getWeatherInfo() const -> Result<weather::Output, DracError>;
}; };
/** /**
@ -160,9 +160,9 @@ struct Weather {
* @brief Holds the application configuration settings. * @brief Holds the application configuration settings.
*/ */
struct Config { struct Config {
General general; ///< General configuration settings. General general; ///< General configuration settings.
Weather weather; ///< Weather configuration settings.` Weather weather; ///< Weather configuration settings.`
NowPlaying now_playing; ///< Now Playing configuration settings. NowPlaying nowPlaying; ///< Now Playing configuration settings.
Config() = default; Config() = default;

View file

@ -1,7 +1,8 @@
#include "weather.hpp" #include "weather.hpp"
#include <chrono> // std::chrono::{duration, operator-} #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 <expected> // std::{expected (Result), unexpected (Err)}
#include <filesystem> // std::filesystem::{path, remove, rename} #include <filesystem> // std::filesystem::{path, remove, rename}
#include <format> // std::format #include <format> // std::format
@ -9,14 +10,17 @@
#include <glaze/beve/read.hpp> // glz::read_beve #include <glaze/beve/read.hpp> // glz::read_beve
#include <glaze/beve/write.hpp> // glz::write_beve #include <glaze/beve/write.hpp> // glz::write_beve
#include <glaze/core/context.hpp> // glz::{error_ctx, error_code} #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/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 <iterator> // std::istreambuf_iterator #include <ios> // std::ios::{binary, trunc}
#include <system_error> // std::error_code #include <iterator> // std::istreambuf_iterator
#include <utility> // std::move #include <system_error> // std::error_code
#include <variant> // std::{get, holds_alternative} #include <utility> // std::move
#include <variant> // std::{get, holds_alternative}
#include "src/core/util/defs.hpp" #include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/logging.hpp" #include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp" #include "src/core/util/types.hpp"
@ -28,6 +32,7 @@ using weather::Output;
namespace { namespace {
using glz::opts, glz::error_ctx, glz::error_code, glz::read, glz::read_beve, glz::write_beve, glz::format_error; 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 util::types::usize, util::types::Err, util::types::Exception;
using weather::Coords; using weather::Coords;
@ -67,11 +72,9 @@ namespace {
Output result; Output result;
if (const error_ctx glazeErr = read_beve(result, content); glazeErr.ec != error_code::none) if (const error_ctx glazeErr = read_beve(result, content); glazeErr.ec != error_code::none)
return Err( return Err(std::format(
std::format( "BEVE parse error reading cache (code {}): {}", static_cast<int>(glazeErr.ec), cachePath->string()
"BEVE parse error reading cache (code {}): {}", static_cast<int>(glazeErr.ec), cachePath->string() ));
)
);
debug_log("Successfully read from cache file."); debug_log("Successfully read from cache file.");
return result; return result;
@ -170,7 +173,7 @@ namespace {
} }
} // namespace } // namespace
fn Weather::getWeatherInfo() const -> Output { fn Weather::getWeatherInfo() const -> Result<Output, DracError> {
using namespace std::chrono; using namespace std::chrono;
using util::types::i32; using util::types::i32;
@ -178,7 +181,7 @@ fn Weather::getWeatherInfo() const -> Output {
const Output& dataVal = *data; const Output& dataVal = *data;
if (const duration<double> cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt)); 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"); debug_log("Using valid cache");
return dataVal; return dataVal;
} }
@ -188,11 +191,9 @@ fn Weather::getWeatherInfo() const -> Output {
debug_log("Cache error: {}", data.error()); debug_log("Cache error: {}", data.error());
} }
fn handleApiResult = [](const Result<Output, String>& result) -> Output { fn handleApiResult = [](const Result<Output, String>& result) -> Result<Output, DracError> {
if (!result) { if (!result)
error_log("API request failed: {}", result.error()); return Err(DracError(DracErrorCode::ApiUnavailable, result.error()));
return Output {};
}
if (Result<void, String> writeResult = WriteCacheToFile(*result); !writeResult) if (Result<void, String> writeResult = WriteCacheToFile(*result); !writeResult)
error_log("Failed to write cache: {}", writeResult.error()); error_log("Failed to write cache: {}", writeResult.error());
@ -208,28 +209,23 @@ fn Weather::getWeatherInfo() const -> Output {
debug_log("Requesting city: {}", escaped); debug_log("Requesting city: {}", escaped);
const String apiUrl = 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); curl_free(escaped);
return handleApiResult(MakeApiRequest(apiUrl)); return handleApiResult(MakeApiRequest(apiUrl));
} }
if (std::holds_alternative<Coords>(location)) { if (std::holds_alternative<Coords>(location)) {
const auto& [lat, lon] = std::get<Coords>(location); const auto& [lat, lon] = std::get<Coords>(location);
debug_log("Requesting coordinates: lat={:.3f}, lon={:.3f}", lat, lon); debug_log("Requesting coordinates: lat={:.3f}, lon={:.3f}", lat, lon);
const String apiUrl = std::format( const String apiUrl = std::format(
"https://api.openweathermap.org/data/2.5/weather?lat={:.3f}&lon={:.3f}&appid={}&units={}", "https://api.openweathermap.org/data/2.5/weather?lat={:.3f}&lon={:.3f}&appid={}&units={}", lat, lon, apiKey, units
lat,
lon,
api_key,
units
); );
return handleApiResult(MakeApiRequest(apiUrl)); 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 Err(DracError(DracErrorCode::ParseError, "Invalid location type in configuration."));
return Output {};
} }

View file

@ -1,77 +1,55 @@
#include "system_data.hpp" #include "system_data.hpp"
#include <chrono> // std::chrono::{year_month_day, floor, days, system_clock} #include <chrono> // std::chrono::{year_month_day, days, floor, system_clock}
#include <locale> // std::locale #include <format> // std::format
#include <stdexcept> // std::runtime_error #include <future> // std::async
#include "src/config/config.hpp" #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 "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 { Future<Result<String, DracError>> hostFut = std::async(async, GetHost);
fn GetDate() -> String { Future<Result<String, DracError>> kernelFut = std::async(async, GetKernelVersion);
using namespace std::chrono; 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()) }; this->date = std::format("{:%B %d}", year_month_day { floor<days>(system_clock::now()) });
this->host = hostFut.get();
return std::format("{:%B %d}", ymd); 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
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;
}

View file

@ -2,10 +2,10 @@
#include <format> // std::{formatter, format_to} #include <format> // std::{formatter, format_to}
#include "src/config/config.hpp" // Config
#include "src/config/weather.hpp" // weather::Output #include "src/config/weather.hpp" // weather::Output
#include "util/defs.hpp" #include "util/defs.hpp"
#include "util/error.hpp"
#include "util/types.hpp" #include "util/types.hpp"
struct Config; struct Config;
@ -58,34 +58,32 @@ struct std::formatter<BytesToGiB> : std::formatter<double> {
} }
}; };
/** namespace os {
* @struct SystemData
* @brief Holds various pieces of system information collected from the OS.
*
* This structure aggregates information about the system,
* in order to display it at all at once during runtime.
*/
struct SystemData {
using NowPlayingResult = Option<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
/** /**
* @brief Fetches all system data asynchronously. * @struct SystemData
* @param config The application configuration. * @brief Holds various pieces of system information collected from the OS.
* @return A populated SystemData object. *
* This structure aggregates information about the system,
* in order to display it at all at once during runtime.
*/ */
static fn fetchSystemData(const Config& config) -> SystemData; struct SystemData {
}; String date; ///< Current date (e.g., "April 26"). Always expected to succeed.
Result<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 Constructs a SystemData object and initializes its members.
* @param config The configuration object containing settings for the system data.
*/
explicit SystemData(const Config& config);
};
} // namespace os

View file

@ -15,53 +15,47 @@ namespace util::error {
using types::u8, types::i32, types::String, types::StringView, types::Exception; using types::u8, types::i32, types::String, types::StringView, types::Exception;
/** /**
* @enum DraconisErrorCode * @enum DracErrorCode
* @brief Error codes for general OS-level operations. * @brief Error codes for general OS-level operations.
*/ */
enum class DraconisErrorCode : u8 { enum class DracErrorCode : u8 {
IoError, ///< General I/O error (filesystem, pipes, etc.).
PermissionDenied, ///< Insufficient permissions to perform the operation.
NotFound, ///< A required resource (file, registry key, device, API endpoint) was not found.
ParseError, ///< Failed to parse data obtained from the OS (e.g., file content, API output).
ApiUnavailable, ///< A required OS service/API is unavailable or failed unexpectedly at runtime. 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. 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). 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. 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. * @brief Holds structured information about an OS-level error.
* *
* Used as the error type in Result for many os:: functions. * Used as the error type in Result for many os:: functions.
*/ */
struct DraconisError { struct DracError {
// ReSharper disable CppDFANotInitializedField // ReSharper disable CppDFANotInitializedField
String message; ///< A descriptive error message, potentially including platform details. 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). std::source_location location; ///< The source location where the error occurred (file, line, function).
// ReSharper restore CppDFANotInitializedField // ReSharper restore CppDFANotInitializedField
DraconisError( DracError(const DracErrorCode errc, String msg, const std::source_location& loc = std::source_location::current())
const DraconisErrorCode errc,
String msg,
const std::source_location& loc = std::source_location::current()
)
: message(std::move(msg)), code(errc), location(loc) {} : message(std::move(msg)), code(errc), location(loc) {}
explicit DraconisError(const Exception& exc, const std::source_location& loc = std::source_location::current()) explicit DracError(const Exception& exc, const std::source_location& loc = std::source_location::current())
: message(exc.what()), code(DraconisErrorCode::InternalError), location(loc) {} : message(exc.what()), code(DracErrorCode::InternalError), location(loc) {}
explicit DraconisError( explicit DracError(const std::error_code& errc, const std::source_location& loc = std::source_location::current())
const std::error_code& errc,
const std::source_location& loc = std::source_location::current()
)
: message(errc.message()), location(loc) { : message(errc.message()), location(loc) {
using enum DraconisErrorCode; using enum DracErrorCode;
using enum std::errc; using enum std::errc;
switch (static_cast<std::errc>(errc.value())) { switch (static_cast<std::errc>(errc.value())) {
@ -90,9 +84,9 @@ namespace util::error {
} }
} }
#else #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) { : message(std::system_category().message(errno_val)), code(code_hint) {
using enum DraconisErrorCode; using enum DracErrorCode;
switch (errno_val) { switch (errno_val) {
case EACCES: code = PermissionDenied; break; 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()) static auto withErrno(const String& context, const std::source_location& loc = std::source_location::current())
-> DraconisError { -> DracError {
const i32 errNo = errno; const i32 errNo = errno;
const String msg = std::system_category().message(errNo); const String msg = std::system_category().message(errNo);
const String fullMsg = std::format("{}: {}", context, msg); const String fullMsg = std::format("{}: {}", context, msg);
DraconisErrorCode code = DraconisErrorCode::PlatformSpecific; const DracErrorCode code = [&errNo] {
switch (errNo) { switch (errNo) {
case EACCES: case EACCES:
case EPERM: code = DraconisErrorCode::PermissionDenied; break; case EPERM: return DracErrorCode::PermissionDenied;
case ENOENT: code = DraconisErrorCode::NotFound; break; case ENOENT: return DracErrorCode::NotFound;
case ETIMEDOUT: code = DraconisErrorCode::Timeout; break; case ETIMEDOUT: return DracErrorCode::Timeout;
case ENOTSUP: code = DraconisErrorCode::NotSupported; break; case ENOTSUP: return DracErrorCode::NotSupported;
case EIO: code = DraconisErrorCode::IoError; break; case EIO: return DracErrorCode::IoError;
case ECONNREFUSED: case ECONNREFUSED:
case ENETDOWN: case ENETDOWN:
case ENETUNREACH: code = DraconisErrorCode::NetworkError; break; case ENETUNREACH: return DracErrorCode::NetworkError;
default: code = DraconisErrorCode::PlatformSpecific; break; default: return DracErrorCode::PlatformSpecific;
} }
}();
return DraconisError { code, fullMsg, loc }; return DracError { code, fullMsg, loc };
} }
#endif #endif
}; };

View file

@ -5,7 +5,7 @@
#include "types.hpp" #include "types.hpp"
namespace util::helpers { namespace util::helpers {
using error::DraconisError, error::DraconisErrorCode; using error::DracError, error::DracErrorCode;
using types::Result, types::String, types::CStr, types::Err; 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, * @return A Result containing the value of the environment variable as a String,
* or an EnvError if an error occurred. * 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 #ifdef _WIN32
using types::i32, types::usize, types::UniquePointer; using types::i32, types::usize, types::UniquePointer;
@ -38,7 +38,7 @@ namespace util::helpers {
const CStr value = std::getenv(name); const CStr value = std::getenv(name);
if (!value) if (!value)
return Err(DraconisError(DraconisErrorCode::NotFound, "Environment variable not found")); return Err(DracError(DracErrorCode::NotFound, "Environment variable not found"));
return value; return value;
#endif #endif

View file

@ -148,7 +148,14 @@ namespace util::logging {
* @param args The arguments for the format string. * @param args The arguments for the format string.
*/ */
template <typename... Args> 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 namespace std::chrono;
using std::filesystem::path; using std::filesystem::path;
@ -186,14 +193,21 @@ namespace util::logging {
fn LogError(const LogLevel level, const ErrorType& error_obj) { fn LogError(const LogLevel level, const ErrorType& error_obj) {
using DecayedErrorType = std::decay_t<ErrorType>; using DecayedErrorType = std::decay_t<ErrorType>;
#ifndef NDEBUG
std::source_location logLocation; std::source_location logLocation;
String errorMessagePart; #endif
if constexpr (std::is_same_v<DecayedErrorType, error::DraconisError>) { String errorMessagePart;
logLocation = error_obj.location;
if constexpr (std::is_same_v<DecayedErrorType, error::DracError>) {
#ifndef NDEBUG
logLocation = error_obj.location;
#endif
errorMessagePart = error_obj.message; errorMessagePart = error_obj.message;
} else { } else {
#ifndef NDEBUG
logLocation = std::source_location::current(); logLocation = std::source_location::current();
#endif
if constexpr (std::is_base_of_v<std::exception, DecayedErrorType>) if constexpr (std::is_base_of_v<std::exception, DecayedErrorType>)
errorMessagePart = error_obj.what(); errorMessagePart = error_obj.what();
else if constexpr (requires { error_obj.message; }) else if constexpr (requires { error_obj.message; })
@ -202,7 +216,11 @@ namespace util::logging {
errorMessagePart = "Unknown error type logged"; errorMessagePart = "Unknown error type logged";
} }
#ifndef NDEBUG
LogImpl(level, logLocation, "{}", errorMessagePart); LogImpl(level, logLocation, "{}", errorMessagePart);
#else
LogImpl(level, "{}", errorMessagePart);
#endif
} }
#ifndef NDEBUG #ifndef NDEBUG
@ -216,21 +234,28 @@ namespace util::logging {
#define debug_at(...) ((void)0) #define debug_at(...) ((void)0)
#endif #endif
#define info_log(fmt, ...) \ #define info_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Info, error_obj);
::util::logging::LogImpl( \ #define warn_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Warn, error_obj);
::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); #define error_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Error, error_obj);
#ifdef NDEBUG
#define info_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Info, fmt __VA_OPT__(, ) __VA_ARGS__)
#define warn_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Warn, fmt __VA_OPT__(, ) __VA_ARGS__)
#define error_log(fmt, ...) ::util::logging::LogImpl(::util::logging::LogLevel::Error, fmt __VA_OPT__(, ) __VA_ARGS__)
#else
#define info_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::Info, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define warn_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::Warn, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define error_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::Error, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#endif
} // namespace util::logging } // namespace util::logging

View file

@ -2,6 +2,7 @@
#include <array> // std::array (Array) #include <array> // std::array (Array)
#include <expected> // std::expected (Result) #include <expected> // std::expected (Result)
#include <future> // std::future (Future)
#include <map> // std::map (Map) #include <map> // std::map (Map)
#include <memory> // std::shared_ptr and std::unique_ptr (SharedPointer, UniquePointer) #include <memory> // std::shared_ptr and std::unique_ptr (SharedPointer, UniquePointer)
#include <optional> // std::optional (Option) #include <optional> // std::optional (Option)
@ -113,6 +114,14 @@ namespace util::types {
template <typename Tp, typename Dp = std::default_delete<Tp>> template <typename Tp, typename Dp = std::default_delete<Tp>>
using UniquePointer = std::unique_ptr<Tp, Dp>; 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 * @struct DiskSpace
* @brief Represents disk usage information. * @brief Represents disk usage information.
@ -132,16 +141,11 @@ namespace util::types {
* Using Option<> for fields that might not always be available. * Using Option<> for fields that might not always be available.
*/ */
struct MediaInfo { struct MediaInfo {
Option<String> title; ///< Track title. Option<String> title; ///< Track title.
Option<String> artist; ///< Track artist(s). 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() = default;
MediaInfo(Option<String> title, Option<String> artist) : title(std::move(title)), artist(std::move(artist)) {} 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 } // namespace util::types

View file

@ -6,12 +6,12 @@
#include <ftxui/screen/screen.hpp> // ftxui::{Screen, Dimension::Full} #include <ftxui/screen/screen.hpp> // ftxui::{Screen, Dimension::Full}
#include <ranges> // std::ranges::{iota, to, transform} #include <ranges> // std::ranges::{iota, to, transform}
#include "src/config/config.hpp"
#include "src/config/weather.hpp" #include "src/config/weather.hpp"
#include "src/core/system_data.hpp"
#include "config/config.hpp" #include "src/core/util/defs.hpp"
#include "core/system_data.hpp" #include "src/core/util/logging.hpp"
#include "core/util/logging.hpp" #include "src/core/util/types.hpp"
#include "os/os.hpp"
namespace ui { namespace ui {
using ftxui::Color; using ftxui::Color;
@ -46,56 +46,60 @@ namespace ui {
StringView music; StringView music;
StringView disk; StringView disk;
StringView shell; StringView shell;
StringView package;
StringView desktop; StringView desktop;
StringView window_manager; StringView windowManager;
}; };
[[maybe_unused]] static constexpr Icons NONE = { [[maybe_unused]] static constexpr Icons NONE = {
.user = "", .user = "",
.palette = "", .palette = "",
.calendar = "", .calendar = "",
.host = "", .host = "",
.kernel = "", .kernel = "",
.os = "", .os = "",
.memory = "", .memory = "",
.weather = "", .weather = "",
.music = "", .music = "",
.disk = "", .disk = "",
.shell = "", .shell = "",
.desktop = "", .package = "",
.window_manager = "", .desktop = "",
.windowManager = "",
}; };
[[maybe_unused]] static constexpr Icons NERD = { [[maybe_unused]] static constexpr Icons NERD = {
.user = "", .user = "",
.palette = "", .palette = "",
.calendar = "", .calendar = "",
.host = " 󰌢 ", .host = " 󰌢 ",
.kernel = "", .kernel = "",
.os = "", .os = "",
.memory = "", .memory = "",
.weather = "", .weather = "",
.music = "", .music = "",
.disk = " 󰋊 ", .disk = " 󰋊 ",
.shell = "", .shell = "",
.desktop = " 󰇄 ", .package = " 󰏖 ",
.window_manager = "  ", .desktop = " 󰇄 ",
.windowManager = "  ",
}; };
[[maybe_unused]] static constexpr Icons EMOJI = { [[maybe_unused]] static constexpr Icons EMOJI = {
.user = " 👤 ", .user = " 👤 ",
.palette = " 🎨 ", .palette = " 🎨 ",
.calendar = " 📅 ", .calendar = " 📅 ",
.host = " 💻 ", .host = " 💻 ",
.kernel = " 🫀 ", .kernel = " 🫀 ",
.os = " 🤖 ", .os = " 🤖 ",
.memory = " 🧠 ", .memory = " 🧠 ",
.weather = " 🌈 ", .weather = " 🌈 ",
.music = " 🎵 ", .music = " 🎵 ",
.disk = " 💾 ", .disk = " 💾 ",
.shell = " 💲 ", .shell = " 💲 ",
.desktop = " 🖥️ ", .package = " 📦 ",
.window_manager = " 🪟 ", .desktop = " 🖥️ ",
.windowManager = " 🪟 ",
}; };
static constexpr inline Icons ICON_TYPE = NERD; 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 String& name = config.general.name;
const Weather weather = config.weather; 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; ui::ICON_TYPE;
Elements content; Elements content;
content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan)); content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan));
content.push_back(separator() | color(ui::DEFAULT_THEME.border)); 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),
text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), CreateColorCircles(),
CreateColorCircles(), }));
}
));
content.push_back(separator() | color(ui::DEFAULT_THEME.border)); content.push_back(separator() | color(ui::DEFAULT_THEME.border));
// Helper function for aligned rows // Helper function for aligned rows
fn createRow = [&](const StringView& icon, const StringView& label, const StringView& value) { // NEW 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(icon)) | color(ui::DEFAULT_THEME.icon), text(String(label)) | color(ui::DEFAULT_THEME.label),
text(String(label)) | color(ui::DEFAULT_THEME.label), filler(),
filler(), text(String(value)) | color(ui::DEFAULT_THEME.value),
text(String(value)) | color(ui::DEFAULT_THEME.value), text(" "),
text(" "), });
}
);
}; };
// System info rows // System info rows
content.push_back(createRow(calendarIcon, "Date", data.date)); content.push_back(createRow(calendarIcon, "Date", data.date));
// Weather row // Weather row
if (weather.enabled && data.weather_info.has_value()) { if (weather.enabled && data.weather) {
const weather::Output& weatherInfo = data.weather_info.value(); const weather::Output& weatherInfo = *data.weather;
if (weather.show_town_name) if (weather.showTownName)
content.push_back(hbox( content.push_back(hbox({
{ text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon),
text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon), text("Weather") | color(ui::DEFAULT_THEME.label),
text("Weather") | color(ui::DEFAULT_THEME.label), filler(),
filler(),
hbox( hbox({
{ text(std::format("{}°F ", std::lround(weatherInfo.main.temp))),
text(std::format("{}°F ", std::lround(weatherInfo.main.temp))), text("in "),
text("in "), text(weatherInfo.name),
text(weatherInfo.name), text(" "),
text(" "), }) |
} color(ui::DEFAULT_THEME.value),
) | }));
color(ui::DEFAULT_THEME.value),
}
));
else else
content.push_back(hbox( content.push_back(hbox({
{ text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon),
text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon), text("Weather") | color(ui::DEFAULT_THEME.label),
text("Weather") | color(ui::DEFAULT_THEME.label), filler(),
filler(),
hbox( hbox({
{ text(std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description)),
text(std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description)), text(" "),
text(" "), }) |
} color(ui::DEFAULT_THEME.value),
) | }));
color(ui::DEFAULT_THEME.value), } else if (weather.enabled)
} error_at(data.weather.error());
));
}
content.push_back(separator() | color(ui::DEFAULT_THEME.border)); content.push_back(separator() | color(ui::DEFAULT_THEME.border));
@ -196,62 +189,64 @@ namespace {
else else
error_at(data.host.error()); error_at(data.host.error());
if (data.kernel_version) if (data.kernelVersion)
content.push_back(createRow(kernelIcon, "Kernel", *data.kernel_version)); content.push_back(createRow(kernelIcon, "Kernel", *data.kernelVersion));
else else
error_at(data.kernel_version.error()); error_at(data.kernelVersion.error());
if (data.os_version) if (data.osVersion)
content.push_back(createRow(String(osIcon), "OS", *data.os_version)); content.push_back(createRow(String(osIcon), "OS", *data.osVersion));
else else
error_at(data.os_version.error()); error_at(data.osVersion.error());
if (data.mem_info) if (data.memInfo)
content.push_back(createRow(memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.mem_info }))); content.push_back(createRow(memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.memInfo })));
else else
error_at(data.mem_info.error()); error_at(data.memInfo.error());
if (data.disk_usage) if (data.diskUsage)
content.push_back(createRow( content.push_back(createRow(
diskIcon, diskIcon,
"Disk", "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 else
error_at(data.disk_usage.error()); error_at(data.diskUsage.error());
if (data.shell) if (data.shell)
content.push_back(createRow(shellIcon, "Shell", *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)); content.push_back(separator() | color(ui::DEFAULT_THEME.border));
if (data.desktop_environment && *data.desktop_environment != data.window_manager) if (data.desktopEnv && *data.desktopEnv != data.windowMgr)
content.push_back(createRow(deIcon, "DE", *data.desktop_environment)); content.push_back(createRow(deIcon, "DE", *data.desktopEnv));
if (data.window_manager) if (data.windowMgr)
content.push_back(createRow(wmIcon, "WM", *data.window_manager)); content.push_back(createRow(wmIcon, "WM", *data.windowMgr));
else
error_at(data.windowMgr.error());
if (config.now_playing.enabled && data.now_playing) { if (config.nowPlaying.enabled && data.nowPlaying) {
if (const Result<MediaInfo, DraconisError>& nowPlayingResult = *data.now_playing) { const String title = data.nowPlaying->title.value_or("Unknown Title");
const MediaInfo& info = *nowPlayingResult; const String artist = data.nowPlaying->artist.value_or("Unknown Artist");
const String npText = artist + " - " + title;
const String title = info.title.value_or("Unknown Title"); content.push_back(separator() | color(ui::DEFAULT_THEME.border));
const String artist = info.artist.value_or("Unknown Artist"); content.push_back(hbox({
const String npText = artist + " - " + title; text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon),
text("Playing") | color(ui::DEFAULT_THEME.label),
content.push_back(separator() | color(ui::DEFAULT_THEME.border)); text(" "),
content.push_back(hbox( filler(),
{ paragraph(npText) | color(Color::Magenta) | size(WIDTH, LESS_THAN, ui::MAX_PARAGRAPH_LENGTH),
text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon), text(" "),
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); return vbox(content) | borderRounded | color(Color::White);
@ -259,17 +254,14 @@ namespace {
} // namespace } // namespace
fn main() -> i32 { fn main() -> i32 {
using os::SystemData;
#ifdef _WIN32 #ifdef _WIN32
winrt::init_apartment(); winrt::init_apartment();
#endif #endif
const Config& config = Config::getInstance(); const Config& config = Config::getInstance();
const SystemData data = SystemData::fetchSystemData(config); const SystemData data = SystemData(config);
if (const Result<u64, DraconisError>& packageCount = os::GetPackageCount())
debug_log("{}", *packageCount);
else
error_at(packageCount.error());
Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }) }); Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }) });

View file

@ -1,22 +1,28 @@
#ifdef __linux__ #ifdef __linux__
// clang-format off // clang-format off
#include <cstring> // std::strlen #include <cstring> // std::strlen
#include <dbus/dbus.h> // DBus::{DBusConnection, DBusMessage, DBusMessageIter, etc.} #include <dbus/dbus-shared.h> // DBUS_BUS_SESSION
#include <expected> // std::{unexpected, expected} #include <dbus/dbus-protocol.h> // DBUS_TYPE_*
#include <format> // std::{format, format_to_n} #include <expected> // std::{unexpected, expected}
#include <fstream> // std::ifstream #include <format> // std::{format, format_to_n}
#include <limits> // std::numeric_limits #include <fstream> // std::ifstream
#include <string> // std::{getline, string (String)} #include <climits> // PATH_MAX
#include <string_view> // std::string_view (StringView) #include <limits> // std::numeric_limits
#include <sys/socket.h> // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED #include <string> // std::{getline, string (String)}
#include <sys/statvfs.h> // statvfs #include <string_view> // std::string_view (StringView)
#include <sys/sysinfo.h> // sysinfo #include <sys/socket.h> // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED
#include <sys/utsname.h> // utsname, uname #include <sys/statvfs.h> // statvfs
#include <unistd.h> // readlink #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/helpers.hpp"
#include "src/core/util/logging.hpp" #include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp"
#include "src/wrappers/dbus.hpp" #include "src/wrappers/dbus.hpp"
#include "src/wrappers/wayland.hpp" #include "src/wrappers/wayland.hpp"
#include "src/wrappers/xcb.hpp" #include "src/wrappers/xcb.hpp"
@ -26,17 +32,18 @@
// clang-format on // clang-format on
using namespace util::types; using namespace util::types;
using util::error::DraconisError, util::error::DraconisErrorCode; using util::error::DracError, util::error::DracErrorCode;
using util::helpers::GetEnv;
namespace { namespace {
fn GetX11WindowManager() -> Result<String, DraconisError> { fn GetX11WindowManager() -> Result<String, DracError> {
using namespace xcb; using namespace xcb;
const DisplayGuard conn; const DisplayGuard conn;
if (!conn) if (!conn)
if (const i32 err = connection_has_error(conn.get())) 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)) { if (const Option<ConnError> connErr = getConnError(err)) {
switch (*connErr) { switch (*connErr) {
case Generic: return "Stream/Socket/Pipe Error"; case Generic: return "Stream/Socket/Pipe Error";
@ -53,22 +60,22 @@ namespace {
return std::format("Unknown Error Code ({})", err); 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( 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) intern_atom_reply(conn.get(), intern_atom(conn.get(), 0, static_cast<u16>(name.size()), name.data()), nullptr)
); );
if (!reply) if (!reply)
return Err( 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; return reply->atom;
}; };
const Result<atom_t, DraconisError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK"); const Result<atom_t, DracError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
const Result<atom_t, DraconisError> wmNameAtom = internAtom("_NET_WM_NAME"); const Result<atom_t, DracError> wmNameAtom = internAtom("_NET_WM_NAME");
const Result<atom_t, DraconisError> utf8StringAtom = internAtom("UTF8_STRING"); const Result<atom_t, DracError> utf8StringAtom = internAtom("UTF8_STRING");
if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) { if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) {
if (!supportingWmCheckAtom) if (!supportingWmCheckAtom)
@ -80,7 +87,7 @@ namespace {
if (!utf8StringAtom) if (!utf8StringAtom)
error_log("Failed to get UTF8_STRING atom"); 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( const ReplyGuard<get_property_reply_t> wmWindowReply(get_property_reply(
@ -91,7 +98,7 @@ namespace {
if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 || if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 ||
get_property_value_length(wmWindowReply.get()) == 0) 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())); 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) 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 char* nameData = static_cast<const char*>(get_property_value(wmNameReply.get()));
const usize length = get_property_value_length(wmNameReply.get()); const usize length = get_property_value_length(wmNameReply.get());
@ -108,39 +115,39 @@ namespace {
return String(nameData, length); return String(nameData, length);
} }
fn GetWaylandCompositor() -> Result<String, DraconisError> { fn GetWaylandCompositor() -> Result<String, DracError> {
const wl::DisplayGuard display; const wl::DisplayGuard display;
if (!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(); const i32 fileDescriptor = display.fd();
if (fileDescriptor < 0) 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; ucred cred;
socklen_t len = sizeof(cred); socklen_t len = sizeof(cred);
if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) 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; Array<char, 128> exeLinkPathBuf;
auto [out, size] = std::format_to_n(exeLinkPathBuf.data(), exeLinkPathBuf.size() - 1, "/proc/{}/exe", cred.pid); auto [out, size] = std::format_to_n(exeLinkPathBuf.data(), exeLinkPathBuf.size() - 1, "/proc/{}/exe", cred.pid);
if (out >= exeLinkPathBuf.data() + exeLinkPathBuf.size() - 1) 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'; *out = '\0';
const char* exeLinkPath = exeLinkPathBuf.data(); 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); const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1);
if (bytesRead == -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'; exeRealPathBuf.at(bytesRead) = '\0';
@ -163,7 +170,7 @@ namespace {
compositorNameView = filenameView; compositorNameView = filenameView;
if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/") 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() && if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() &&
compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) { compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) {
@ -171,7 +178,7 @@ namespace {
compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length()); compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length());
if (cleanedView.empty()) 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); return String(cleanedView);
} }
@ -181,13 +188,13 @@ namespace {
} // namespace } // namespace
namespace os { namespace os {
fn GetOSVersion() -> Result<String, DraconisError> { fn GetOSVersion() -> Result<String, DracError> {
constexpr CStr path = "/etc/os-release"; constexpr CStr path = "/etc/os-release";
std::ifstream file(path); std::ifstream file(path);
if (!file) 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; String line;
constexpr StringView prefix = "PRETTY_NAME="; constexpr StringView prefix = "PRETTY_NAME=";
@ -201,206 +208,197 @@ namespace os {
value = value.substr(1, value.length() - 2); value = value.substr(1, value.length() - 2);
if (value.empty()) if (value.empty())
return Err(DraconisError( return Err(
DraconisErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path) DracError(DracErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path))
)); );
return value; 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; struct sysinfo info;
if (sysinfo(&info) != 0) 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 totalRam = info.totalram;
const u64 memUnit = info.mem_unit; const u64 memUnit = info.mem_unit;
if (memUnit == 0) 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) 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; return info.totalram * info.mem_unit;
} }
fn GetNowPlaying() -> Result<MediaInfo, DraconisError> { fn GetNowPlaying() -> Result<MediaInfo, DracError> {
Result<dbus::ConnectionGuard, DraconisError> connectionResult = dbus::BusGet(DBUS_BUS_SESSION); using namespace dbus;
Result<Connection, DracError> connectionResult = Connection::busGet(DBUS_BUS_SESSION);
if (!connectionResult) if (!connectionResult)
return Err(connectionResult.error()); return Err(connectionResult.error());
dbus::ConnectionGuard& connection = *connectionResult; const Connection& connection = *connectionResult;
Option<String> activePlayer = None; Option<String> activePlayer = None;
{ {
Result<dbus::MessageGuard, DraconisError> listNamesResult = dbus::MessageNewMethodCall( Result<Message, DracError> listNamesResult =
"org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames" Message::newMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
);
if (!listNamesResult) if (!listNamesResult)
return Err(listNamesResult.error()); return Err(listNamesResult.error());
dbus::MessageGuard& listNames = *listNamesResult; Result<Message, DracError> listNamesReplyResult = connection.sendWithReplyAndBlock(*listNamesResult, 100);
Result<dbus::MessageGuard, DraconisError> listNamesReplyResult =
dbus::ConnectionSendWithReplyAndBlock(connection, listNames, 100);
if (!listNamesReplyResult) if (!listNamesReplyResult)
return Err(listNamesReplyResult.error()); 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) { while (subIter.getArgType() != DBUS_TYPE_INVALID) {
dbus::MessageIter subIter; if (Option<String> name = subIter.getString())
dbus::MessageIterRecurse(&iter, &subIter); if (name->starts_with("org.mpris.MediaPlayer2.")) {
activePlayer = std::move(*name);
while (dbus::MessageIterGetArgType(&subIter) != DBUS_TYPE_INVALID) { break;
if (Option<String> name = dbus::MessageIterGetString(&subIter)) }
if (name->find("org.mpris.MediaPlayer2") != String::npos) { if (!subIter.next())
activePlayer = std::move(*name); break;
break;
}
dbus::MessageIterNext(&subIter);
}
} else {
return Err(DraconisError(DraconisErrorCode::ParseError, "Invalid DBus ListNames reply format"));
} }
} }
if (!activePlayer) 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" activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"
); );
if (!msgResult) if (!msgResult)
return Err(msgResult.error()); return Err(msgResult.error());
dbus::MessageGuard& msg = *msgResult; Message& msg = *msgResult;
if (!dbus::MessageAppendArgs( if (!msg.appendArgs("org.mpris.MediaPlayer2.Player", "Metadata"))
msg, DBUS_TYPE_STRING, "org.mpris.MediaPlayer2.Player", DBUS_TYPE_STRING, "Metadata", DBUS_TYPE_INVALID return Err(DracError(DracErrorCode::InternalError, "Failed to append arguments to Properties.Get message"));
))
return Err(DraconisError(DraconisErrorCode::InternalError, "Failed to append arguments to DBus message"));
Result<dbus::MessageGuard, DraconisError> replyResult = Result<Message, DracError> replyResult = connection.sendWithReplyAndBlock(msg, 100);
dbus::ConnectionSendWithReplyAndBlock(connection, msg, 100);
if (!replyResult) if (!replyResult)
return Err(replyResult.error()); return Err(replyResult.error());
dbus::MessageGuard& reply = *replyResult;
Option<String> title = None; Option<String> title = None;
Option<String> artist = None; Option<String> artist = None;
dbus::MessageIter propIter; MessageIter propIter = replyResult->iterInit();
if (!dbus::MessageIterInit(reply, &propIter)) if (!propIter.isValid())
return Err(DraconisError(DraconisErrorCode::ParseError, "Properties.Get reply has no arguments")); return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply has no arguments or invalid iterator"));
if (dbus::MessageIterGetArgType(&propIter) != DBUS_TYPE_VARIANT) if (propIter.getArgType() != DBUS_TYPE_VARIANT)
return Err(DraconisError(DraconisErrorCode::ParseError, "Properties.Get reply argument is not a variant")); return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply argument is not a variant"));
dbus::MessageIter variantIter; MessageIter variantIter = propIter.recurse();
dbus::MessageIterRecurse(&propIter, &variantIter); if (!variantIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Could not recurse into variant"));
if (dbus::MessageIterGetArgType(&variantIter) != DBUS_TYPE_ARRAY || if (variantIter.getArgType() != DBUS_TYPE_ARRAY || variantIter.getElementType() != DBUS_TYPE_DICT_ENTRY)
dbus_message_iter_get_element_type(&variantIter) != DBUS_TYPE_DICT_ENTRY) return Err(DracError(DracErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})"));
return Err(
DraconisError(DraconisErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})")
);
dbus::MessageIter dictIter; MessageIter dictIter = variantIter.recurse();
dbus::MessageIterRecurse(&variantIter, &dictIter); if (!dictIter.isValid())
return Err(DracError(DracErrorCode::ParseError, "Could not recurse into metadata dictionary array"));
while (dbus::MessageIterGetArgType(&dictIter) == DBUS_TYPE_DICT_ENTRY) { while (dictIter.getArgType() == DBUS_TYPE_DICT_ENTRY) {
dbus::MessageIter entryIter; MessageIter entryIter = dictIter.recurse();
dbus::MessageIterRecurse(&dictIter, &entryIter); if (!entryIter.isValid()) {
debug_log("Warning: Could not recurse into dict entry, skipping.");
Option<String> key = dbus::MessageIterGetString(&entryIter); if (!dictIter.next())
break;
continue;
}
Option<String> key = entryIter.getString();
if (!key) { if (!key) {
dbus::MessageIterNext(&dictIter); debug_log("Warning: Could not get key string from dict entry, skipping.");
if (!dictIter.next())
break;
continue; continue;
} }
if (!dbus::MessageIterNext(&entryIter) || dbus::MessageIterGetArgType(&entryIter) != DBUS_TYPE_VARIANT) { if (!entryIter.next() || entryIter.getArgType() != DBUS_TYPE_VARIANT) {
dbus::MessageIterNext(&dictIter); if (!dictIter.next())
break;
continue; continue;
} }
dbus::MessageIter valueVariantIter; MessageIter valueVariantIter = entryIter.recurse();
dbus::MessageIterRecurse(&entryIter, &valueVariantIter); if (!valueVariantIter.isValid()) {
if (!dictIter.next())
break;
continue;
}
if (*key == "xesam:title") if (*key == "xesam:title") {
title = dbus::MessageIterGetString(&valueVariantIter); title = valueVariantIter.getString();
else if (*key == "xesam:artist") } else if (*key == "xesam:artist") {
if (dbus::MessageIterGetArgType(&valueVariantIter) == DBUS_TYPE_ARRAY && if (valueVariantIter.getArgType() == DBUS_TYPE_ARRAY && valueVariantIter.getElementType() == DBUS_TYPE_STRING) {
dbus_message_iter_get_element_type(&valueVariantIter) == DBUS_TYPE_STRING) { if (MessageIter artistArrayIter = valueVariantIter.recurse(); artistArrayIter.isValid())
dbus::MessageIter artistArrayIter; artist = artistArrayIter.getString();
dbus::MessageIterRecurse(&valueVariantIter, &artistArrayIter); } else {
artist = dbus::MessageIterGetString(&artistArrayIter); debug_log("Warning: Artist value was not an array of strings as expected.");
} }
}
dbus::MessageIterNext(&dictIter); if (!dictIter.next())
break;
} }
return MediaInfo(std::move(title), std::move(artist)); return MediaInfo(std::move(title), std::move(artist));
} }
fn GetWindowManager() -> Option<String> { fn GetWindowManager() -> Result<String, DracError> {
if (Result<String, DraconisError> waylandResult = GetWaylandCompositor()) if (Result<String, DracError> waylandResult = GetWaylandCompositor())
return *waylandResult; 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; 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> { fn GetDesktopEnvironment() -> Result<String, DracError> {
return util::helpers::GetEnv("XDG_CURRENT_DESKTOP") return GetEnv("XDG_CURRENT_DESKTOP")
.transform([](const String& xdgDesktop) -> String { .transform([](String xdgDesktop) -> String {
if (const usize colon = xdgDesktop.find(':'); colon != String::npos) if (const usize colon = xdgDesktop.find(':'); colon != String::npos)
return xdgDesktop.substr(0, colon); xdgDesktop.resize(colon);
return xdgDesktop; return xdgDesktop;
}) })
.or_else([](const DraconisError&) -> Result<String, DraconisError> { .or_else([](const DracError&) -> Result<String, DracError> { return GetEnv("DESKTOP_SESSION"); });
return util::helpers::GetEnv("DESKTOP_SESSION");
})
.transform([](const String& finalValue) -> Option<String> {
debug_log("Found desktop environment: {}", finalValue);
return finalValue;
})
.value_or(None);
} }
fn GetShell() -> Option<String> { fn GetShell() -> Result<String, DracError> {
if (const Result<String, DraconisError> shellPath = util::helpers::GetEnv("SHELL")) { if (const Result<String, DracError> shellPath = GetEnv("SHELL")) {
// clang-format off // clang-format off
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{ constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
{ "bash", "Bash" }, { "bash", "Bash" },
{ "zsh", "Zsh" }, { "zsh", "Zsh" },
{ "fish", "Fish" }, { "fish", "Fish" },
{ "nu", "Nushell" }, { "nu", "Nushell" },
{ "sh", "SH" }, // sh last because other shells contain "sh" { "sh", "SH" }, // sh last because other shells contain "sh"
}}; }};
// clang-format on // clang-format on
for (const auto& [exe, name] : shellMap) for (const auto& [exe, name] : shellMap)
@ -410,64 +408,63 @@ namespace os {
return *shellPath; // fallback to the raw shell path 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 primaryPath = "/sys/class/dmi/id/product_family";
constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name"; 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); std::ifstream file(path);
String line; String line;
if (!file) if (!file)
return Err(DraconisError( return Err(
DraconisErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path) DracError(DracErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path))
)); );
if (!std::getline(file, line)) if (!std::getline(file, line))
return Err( 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 line;
}; };
return readFirstLine(primaryPath).or_else([&](const DraconisError& primaryError) -> Result<String, DraconisError> { return readFirstLine(primaryPath).or_else([&](const DracError& primaryError) -> Result<String, DracError> {
return readFirstLine(fallbackPath) return readFirstLine(fallbackPath).or_else([&](const DracError& fallbackError) -> Result<String, DracError> {
.or_else([&](const DraconisError& fallbackError) -> Result<String, DraconisError> { return Err(DracError(
return Err(DraconisError( DracErrorCode::InternalError,
DraconisErrorCode::InternalError, std::format(
std::format( "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}",
"Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", primaryPath,
primaryPath, primaryError.message,
primaryError.message, fallbackPath,
fallbackPath, fallbackError.message
fallbackError.message )
) ));
)); });
});
}); });
} }
fn GetKernelVersion() -> Result<String, DraconisError> { fn GetKernelVersion() -> Result<String, DracError> {
utsname uts; utsname uts;
if (uname(&uts) == -1) if (uname(&uts) == -1)
return Err(DraconisError::withErrno("uname call failed")); return Err(DracError::withErrno("uname call failed"));
if (std::strlen(uts.release) == 0) 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; return uts.release;
} }
fn GetDiskUsage() -> Result<DiskSpace, DraconisError> { fn GetDiskUsage() -> Result<DiskSpace, DracError> {
struct statvfs stat; struct statvfs stat;
if (statvfs("/", &stat) == -1) 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 { return DiskSpace {
.used_bytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize), .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 } // namespace os
#endif // __linux__ #endif // __linux__

View file

@ -3,53 +3,53 @@
// clang-format off // clang-format off
#include "src/os/linux/pkg_count.hpp" #include "src/os/linux/pkg_count.hpp"
#include <SQLiteCpp/SQLiteCpp.h> #include <SQLiteCpp/Database.h> // SQLite::{Database, OPEN_READONLY}
#include <fstream> #include <SQLiteCpp/Exception.h> // SQLite::Exception
#include <glaze/beve/read.hpp> #include <SQLiteCpp/Statement.h> // SQLite::Statement
#include <glaze/beve/write.hpp> #include <chrono> // std::chrono::{duration_cast, seconds, system_clock}
#include <glaze/core/common.hpp> #include <filesystem> // std::filesystem::{current_path, directory_entry, directory_iterator, etc.}
#include <glaze/core/read.hpp> #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/logging.hpp"
#include "src/core/util/types.hpp" #include "src/core/util/types.hpp"
// clang-format on // 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, 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 {
namespace fs = std::filesystem; namespace fs = std::filesystem;
using namespace std::chrono; using namespace std::chrono;
using os::linux::PkgCountCacheData, os::linux::PackageManagerInfo;
struct PkgCountCacheData { constexpr StringView ALLOWED_PMID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
u64 count {};
i64 timestamp_epoch_seconds {};
// NOLINTBEGIN(readability-identifier-naming) fn GetPkgCountCachePath(const String& pmId) -> Result<fs::path, DracError> {
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> {
std::error_code errc; std::error_code errc;
const fs::path cacheDir = fs::temp_directory_path(errc); const fs::path cacheDir = fs::temp_directory_path(errc);
if (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() || if (pmId.empty() || pmId.find_first_not_of(ALLOWED_PMID_CHARS) != String::npos)
pm_id.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") != String::npos) return Err(DracError(DracErrorCode::ParseError, "Invalid package manager ID for cache path: " + pmId));
return Err(DraconisError(DraconisErrorCode::ParseError, "Invalid package manager ID for cache path: " + pm_id));
return cacheDir / (pm_id + "_pkg_count_cache.beve"); return cacheDir / (pmId + "_pkg_count_cache.beve");
} }
fn ReadPkgCountCache(const String& pm_id) -> Result<PkgCountCacheData, DraconisError> { fn ReadPkgCountCache(const String& pmId) -> Result<PkgCountCacheData, DracError> {
Result<fs::path, DraconisError> cachePathResult = GetPkgCountCachePath(pm_id); Result<fs::path, DracError> cachePathResult = GetPkgCountCachePath(pmId);
if (!cachePathResult) if (!cachePathResult)
return Err(cachePathResult.error()); return Err(cachePathResult.error());
@ -57,57 +57,45 @@ namespace {
const fs::path& cachePath = *cachePathResult; const fs::path& cachePath = *cachePathResult;
if (!fs::exists(cachePath)) 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); std::ifstream ifs(cachePath, std::ios::binary);
if (!ifs.is_open()) if (!ifs.is_open())
return Err( return Err(DracError(DracErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string()));
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());
try { 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>()); 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()) { if (content.empty())
return Err(DraconisError(DraconisErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string())); return Err(DracError(DracErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string()));
}
PkgCountCacheData result; PkgCountCacheData result;
const glz::context ctx {}; const glz::context ctx {};
if (auto glazeResult = glz::read_beve(result, content); glazeResult.ec != glz::error_code::none) if (glz::error_ctx glazeResult = glz::read_beve(result, content); glazeResult.ec != glz::error_code::none)
return Err(DraconisError( return Err(DracError(
DraconisErrorCode::ParseError, DracErrorCode::ParseError,
std::format( std::format(
"BEVE parse error reading cache (code {}): {}", static_cast<int>(glazeResult.ec), cachePath.string() "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; return result;
} catch (const std::ios_base::failure& e) { } catch (const std::ios_base::failure& e) {
return Err(DraconisError( return Err(DracError(
DraconisErrorCode::IoError, DracErrorCode::IoError, std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what())
std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what())
)); ));
} catch (const Exception& e) { } catch (const Exception& e) {
return Err( return Err(DracError(DracErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what()))
DraconisError(DraconisErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what()))
); );
} }
} }
// Modified to take pm_id and PkgCountCacheData fn WritePkgCountCache(const String& pmId, const PkgCountCacheData& data) -> Result<void, DracError> {
fn WritePkgCountCache(const String& pm_id, const PkgCountCacheData& data) -> Result<void, DraconisError> {
using util::types::isize; using util::types::isize;
Result<fs::path, DraconisError> cachePathResult = GetPkgCountCachePath(pm_id); Result<fs::path, DracError> cachePathResult = GetPkgCountCachePath(pmId);
if (!cachePathResult) if (!cachePathResult)
return Err(cachePathResult.error()); return Err(cachePathResult.error());
@ -116,80 +104,65 @@ namespace {
fs::path tempPath = cachePath; fs::path tempPath = cachePath;
tempPath += ".tmp"; tempPath += ".tmp";
debug_log("Writing {} package count to BEVE cache file: {}", pm_id, cachePath.string());
try { try {
String binaryBuffer; String binaryBuffer;
PkgCountCacheData mutableData = data; PkgCountCacheData mutableData = data;
if (auto glazeErr = glz::write_beve(mutableData, binaryBuffer); glazeErr) { if (glz::error_ctx glazeErr = glz::write_beve(mutableData, binaryBuffer); glazeErr)
return Err(DraconisError( return Err(DracError(
DraconisErrorCode::ParseError, DracErrorCode::ParseError,
std::format("BEVE serialization error writing cache (code {})", static_cast<int>(glazeErr.ec)) std::format("BEVE serialization error writing cache (code {})", static_cast<int>(glazeErr.ec))
)); ));
}
{ {
std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc); std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) { if (!ofs.is_open())
return Err(DraconisError(DraconisErrorCode::IoError, "Failed to open temp cache file: " + tempPath.string())); return Err(DracError(DracErrorCode::IoError, "Failed to open temp cache file: " + tempPath.string()));
}
ofs.write(binaryBuffer.data(), static_cast<isize>(binaryBuffer.size())); ofs.write(binaryBuffer.data(), static_cast<isize>(binaryBuffer.size()));
if (!ofs) { if (!ofs) {
std::error_code removeEc; std::error_code removeEc;
fs::remove(tempPath, removeEc); fs::remove(tempPath, removeEc);
return Err( return Err(DracError(DracErrorCode::IoError, "Failed to write to temp cache file: " + tempPath.string()));
DraconisError(DraconisErrorCode::IoError, "Failed to write to temp cache file: " + tempPath.string())
);
} }
} }
// Atomically replace the old cache file with the new one
std::error_code errc; std::error_code errc;
fs::rename(tempPath, cachePath, errc); fs::rename(tempPath, cachePath, errc);
if (errc) { if (errc) {
fs::remove(tempPath, errc); // Clean up temp file on failure (ignore error) fs::remove(tempPath, errc);
return Err(DraconisError( return Err(DracError(
DraconisErrorCode::IoError, DracErrorCode::IoError,
std::format("Failed to replace cache file '{}': {}", cachePath.string(), errc.message()) std::format("Failed to replace cache file '{}': {}", cachePath.string(), errc.message())
)); ));
} }
debug_log("Successfully wrote {} package count to BEVE cache file.", pm_id);
return {}; return {};
} catch (const std::ios_base::failure& e) { } catch (const std::ios_base::failure& e) {
std::error_code removeEc; std::error_code removeEc;
fs::remove(tempPath, removeEc); fs::remove(tempPath, removeEc);
return Err(DraconisError( return Err(DracError(
DraconisErrorCode::IoError, DracErrorCode::IoError, std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what())
std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what())
)); ));
} catch (const Exception& e) { } catch (const Exception& e) {
std::error_code removeEc; std::error_code removeEc;
fs::remove(tempPath, removeEc); fs::remove(tempPath, removeEc);
return Err( return Err(DracError(DracErrorCode::InternalError, std::format("Error writing package count cache: {}", e.what()))
DraconisError(DraconisErrorCode::InternalError, std::format("Error writing package count cache: {}", e.what()))
); );
} catch (...) { } catch (...) {
std::error_code removeEc; std::error_code removeEc;
fs::remove(tempPath, removeEc); fs::remove(tempPath, removeEc);
return Err( return Err(DracError(DracErrorCode::Other, std::format("Unknown error writing cache file: {}", tempPath.string()))
DraconisError(DraconisErrorCode::Other, std::format("Unknown error writing cache file: {}", tempPath.string()))
); );
} }
} }
fn GetPackageCountInternal(const os::linux::PackageManagerInfo& pmInfo) -> Result<u64, DraconisError> { fn GetPackageCountInternalDb(const PackageManagerInfo& pmInfo) -> Result<u64, DracError> {
// Use info from the struct const auto& [pmId, dbPath, countQuery] = pmInfo;
const fs::path& dbPath = pmInfo.db_path;
const String& pmId = pmInfo.id;
const String& queryStr = pmInfo.count_query;
// Try reading from cache using pm_id if (Result<PkgCountCacheData, DracError> cachedDataResult = ReadPkgCountCache(pmId)) {
if (Result<PkgCountCacheData, DraconisError> cachedDataResult = ReadPkgCountCache(pmId)) {
const auto& [count, timestamp] = *cachedDataResult; const auto& [count, timestamp] = *cachedDataResult;
std::error_code errc; std::error_code errc;
const std::filesystem::file_time_type dbModTime = fs::last_write_time(dbPath, 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 "Could not get modification time for '{}': {}. Invalidating {} cache.", dbPath.string(), errc.message(), pmId
); );
} else { } 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()) { cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) {
// Use cacheTimePoint for logging as well
debug_log( debug_log(
"Using valid {} package count cache (DB file unchanged since {}).", "Using valid {} package count cache (DB file unchanged since {}).",
pmId, pmId,
@ -212,7 +184,7 @@ namespace {
debug_log("{} package count cache stale (DB file modified).", pmId); debug_log("{} package count cache stale (DB file modified).", pmId);
} }
} else { } else {
if (cachedDataResult.error().code != DraconisErrorCode::NotFound) if (cachedDataResult.error().code != DracErrorCode::NotFound)
debug_at(cachedDataResult.error()); debug_at(cachedDataResult.error());
debug_log("{} package count cache not found or unreadable.", pmId); debug_log("{} package count cache not found or unreadable.", pmId);
} }
@ -222,121 +194,202 @@ namespace {
try { try {
const SQLite::Database database(dbPath.string(), SQLite::OPEN_READONLY); 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(); const i64 countInt64 = query.getColumn(0).getInt64();
if (countInt64 < 0) if (countInt64 < 0)
return Err(DraconisError( return Err(
DraconisErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId) DracError(DracErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId))
)); );
count = static_cast<u64>(countInt64); count = static_cast<u64>(countInt64);
} else { } else {
return Err( return Err(DracError(DracErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId)));
DraconisError(DraconisErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId))
);
} }
} catch (const SQLite::Exception& e) { } catch (const SQLite::Exception& e) {
return Err(DraconisError( return Err(DracError(
DraconisErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what()) DracErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what())
)); ));
} catch (const Exception& e) { } catch (const Exception& e) { return Err(DracError(DracErrorCode::InternalError, e.what())); } catch (...) {
return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); return Err(DracError(DracErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId)));
} catch (...) {
return Err(DraconisError(DraconisErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId)));
} }
const i64 nowEpochSeconds = duration_cast<seconds>(system_clock::now().time_since_epoch()).count(); 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) { if (Result<void, DracError> writeResult = WritePkgCountCache(pmId, dataToCache); !writeResult)
warn_at(writeResult.error()); error_at(writeResult.error());
warn_log("Failed to write {} package count to cache.", pmId);
return 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()))
);
} }
debug_log("Fetched fresh {} package count: {}", pmId, count); if (!fs::is_directory(dirPath, errc)) {
if (errc)
return Err(DracError(
DracErrorCode::IoError, std::format("Filesystem error checking {} path type: {}", pmId, errc.message())
));
warn_log("Expected {} directory at '{}', but it's not a directory.", pmId, dirPath.string());
return Err(
DracError(DracErrorCode::IoError, std::format("{} path is not a directory: {}", pmId, dirPath.string()))
);
}
u64 count = 0;
try {
const fs::directory_iterator dirIter(dirPath, fs::directory_options::skip_permission_denied, errc);
if (errc)
return Err(
DracError(DracErrorCode::IoError, std::format("Failed to iterate {} directory: {}", pmId, errc.message()))
);
for (const fs::directory_entry& entry : dirIter) {
if (!file_extension_filter.empty()) {
if (std::error_code fileErrc; !entry.is_regular_file(fileErrc) || fileErrc) {
if (fileErrc)
warn_log(
"Error checking file status in {} directory for '{}': {}",
pmId,
entry.path().string(),
fileErrc.message()
);
continue;
}
if (entry.path().extension().string() == file_extension_filter)
count++;
} else {
count++;
}
}
} catch (const fs::filesystem_error& e) {
return Err(DracError(
DracErrorCode::IoError,
std::format("Filesystem error iterating {} directory '{}': {}", pmId, dirPath.string(), e.what())
));
} catch (...) {
return Err(DracError(
DracErrorCode::Other, std::format("Unknown error iterating {} directory '{}'", pmId, dirPath.string())
));
}
if (subtract_one && count > 0)
count--;
return count; return count;
} }
} // namespace } // namespace
namespace os::linux { 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."); debug_log("Attempting to get Moss package count.");
const PackageManagerInfo mossInfo = { const PackageManagerInfo mossInfo = {
.id = "moss", .id = "moss",
.db_path = "/.moss/db/install", .dbPath = "/.moss/db/install",
.count_query = "SELECT COUNT(*) FROM meta", .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) { if (errc) {
warn_log("Filesystem error checking for Moss DB at '{}': {}", mossInfo.db_path.string(), errc.message()); warn_log("Filesystem error checking for Moss DB at '{}': {}", mossInfo.dbPath.string(), errc.message());
return Err(DraconisError(DraconisErrorCode::IoError, "Filesystem error checking Moss DB: " + 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(DracError(DracErrorCode::ApiUnavailable, "Moss db not found: " + mossInfo.dbPath.string()));
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Moss db not found: " + mossInfo.db_path.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"));
}
return *countResult - 1;
} }
fn GetNixPackageCount() -> Result<u64, DraconisError> { fn GetNixPackageCount() -> Result<u64, DracError> {
debug_log("Attempting to get Nix package count."); debug_log("Attempting to get Nix package count.");
const PackageManagerInfo nixInfo = { const PackageManagerInfo nixInfo = {
.id = "nix", .id = "nix",
.db_path = "/nix/var/nix/db/db.sqlite", .dbPath = "/nix/var/nix/db/db.sqlite",
.count_query = "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL", .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) { if (errc) {
warn_log("Filesystem error checking for Nix DB at '{}': {}", nixInfo.db_path.string(), errc.message()); warn_log("Filesystem error checking for Nix DB at '{}': {}", nixInfo.dbPath.string(), errc.message());
return Err(DraconisError(DraconisErrorCode::IoError, "Filesystem error checking Nix DB: " + 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(DracError(DracErrorCode::ApiUnavailable, "Nix db not found: " + nixInfo.dbPath.string()));
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Nix db not found: " + nixInfo.db_path.string()));
} }
debug_log("Nix database found at '{}'. Proceeding with count.", nixInfo.db_path.string()); return GetPackageCountInternalDb(nixInfo);
return GetPackageCountInternal(nixInfo);
} }
fn GetTotalPackageCount() -> Result<u64, DraconisError> { fn GetPacmanPackageCount() -> Result<u64, DracError> {
debug_log("Attempting to get total package count from all package managers."); return GetPackageCountInternalDir(
"Pacman", fs::current_path().root_path() / "var" / "lib" / "pacman" / "local", "", true
);
}
const PackageManagerInfo mossInfo = { fn GetTotalPackageCount() -> Result<u64, DracError> {
.id = "moss", using util::types::Array, util::types::Future;
.db_path = "/.moss/db/install",
.count_query = "SELECT COUNT(*) FROM meta",
};
const PackageManagerInfo nixInfo = { Array<Future<Result<u64, DracError>>, 4> futures = {
.id = "nix", std::async(std::launch::async, GetDpkgPackageCount),
.db_path = "/nix/var/nix/db/db.sqlite", std::async(std::launch::async, GetMossPackageCount),
.count_query = "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL", std::async(std::launch::async, GetNixPackageCount),
std::async(std::launch::async, GetPacmanPackageCount),
}; };
u64 totalCount = 0; u64 totalCount = 0;
if (Result<u64, DraconisError> mossCountResult = GetMossPackageCount(); mossCountResult) { for (Future<Result<u64, DracError>>& fut : futures) try {
// `moss list installed` returns 1 less than the db count, if (Result<u64, DracError> result = fut.get()) {
// so we subtract 1 for consistency. totalCount += *result;
totalCount += (*mossCountResult - 1); } else {
} else { if (result.error().code != DracErrorCode::ApiUnavailable) {
debug_at(mossCountResult.error()); error_at(result.error());
} } else {
debug_at(result.error());
if (Result<u64, DraconisError> nixCountResult = GetNixPackageCount(); nixCountResult) { }
totalCount += *nixCountResult; }
} else { } catch (const Exception& e) {
debug_at(nixCountResult.error()); error_log("Caught exception while getting package count future: {}", e.what());
} } catch (...) { error_log("Caught unknown exception while getting package count future."); }
return totalCount; return totalCount;
} }

View file

@ -3,7 +3,9 @@
#ifdef __linux__ #ifdef __linux__
// clang-format off // 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/defs.hpp"
#include "src/core/util/error.hpp" #include "src/core/util/error.hpp"
@ -11,50 +13,65 @@
// clang-format on // clang-format on
namespace os::linux { namespace os::linux {
using util::error::DraconisError; using util::error::DracError;
using util::types::Result, util::types::u64; using util::types::Result, util::types::u64, util::types::i64, util::types::String;
namespace fs = std::filesystem;
struct PackageManagerInfo { struct PackageManagerInfo {
util::types::String id; String id;
std::filesystem::path db_path; fs::path dbPath;
util::types::String count_query; 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) // 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) // 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) // Get package count from pacman (Arch Linux)
fn GetPacmanPackageCount() -> Result<u64, DraconisError>; fn GetPacmanPackageCount() -> Result<u64, DracError>;
// Get package count from Portage (Gentoo) // Get package count from Portage (Gentoo)
fn GetPortagePackageCount() -> Result<u64, DraconisError>; fn GetPortagePackageCount() -> Result<u64, DracError>;
// Get package count from zypper (openSUSE) // Get package count from zypper (openSUSE)
fn GetZypperPackageCount() -> Result<u64, DraconisError>; fn GetZypperPackageCount() -> Result<u64, DracError>;
// Get package count from apk (Alpine) // Get package count from apk (Alpine)
fn GetApkPackageCount() -> Result<u64, DraconisError>; fn GetApkPackageCount() -> Result<u64, DracError>;
// Get package count from moss (AerynOS) // Get package count from moss (AerynOS)
fn GetMossPackageCount() -> Result<u64, DraconisError>; fn GetMossPackageCount() -> Result<u64, DracError>;
// Get package count from nix // Get package count from nix
fn GetNixPackageCount() -> Result<u64, DraconisError>; fn GetNixPackageCount() -> Result<u64, DracError>;
// Get package count from flatpak // Get package count from flatpak
fn GetFlatpakPackageCount() -> Result<u64, DraconisError>; fn GetFlatpakPackageCount() -> Result<u64, DracError>;
// Get package count from snap // Get package count from snap
fn GetSnapPackageCount() -> Result<u64, DraconisError>; fn GetSnapPackageCount() -> Result<u64, DracError>;
// Get package count from AppImage // Get package count from AppImage
fn GetAppimagePackageCount() -> Result<u64, DraconisError>; fn GetAppimagePackageCount() -> Result<u64, DracError>;
// Get total package count from all available package managers // Get total package count from all available package managers
fn GetTotalPackageCount() -> Result<u64, DraconisError>; fn GetTotalPackageCount() -> Result<u64, DracError>;
} // namespace os::linux } // namespace os::linux
#endif // __linux__ #endif // __linux__

View file

@ -14,88 +14,100 @@
* (found in linux.cpp, windows.cpp, macos.cpp). * (found in linux.cpp, windows.cpp, macos.cpp).
*/ */
namespace os { 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, using util::types::u64, util::types::String, util::types::Option, util::types::Result, util::types::MediaInfo,
util::types::DiskSpace; util::types::DiskSpace;
/** /**
* @brief Get the total amount of physical RAM installed in the system. * @brief Get the total amount of physical RAM installed in the system.
* @return A Result containing the total RAM in bytes (u64) on success, * @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. * @brief Gets structured metadata about the currently playing media.
* @return A Result containing the media information (MediaInfo struct) on success, * @return A Result containing the media information (MediaInfo struct) on success,
* or a NowPlayingError (indicating player state or system error) on failure. * 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. * @brief Gets the "pretty" name of the operating system.
* @details Examples: "Ubuntu 24.04.2 LTS", "Windows 11 Pro 24H2", "macOS 15 Sequoia". * @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. * @brief Attempts to retrieve the desktop environment name.
* @details This is most relevant on Linux. May check environment variables (XDG_CURRENT_DESKTOP), session files, * @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. * 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. * @brief Attempts to retrieve the window manager name.
* @details On Linux, checks Wayland compositor or X11 WM properties. On Windows, returns "DWM" or similar. * @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. * 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. * @brief Attempts to detect the current user shell name.
* @details Checks the SHELL environment variable on Linux/macOS. On Windows, inspects the process tree * @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). * 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"), * @return A Result containing the shell name String on success,
* or None if detection fails. * 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. * @brief Gets a system identifier, often the hardware model or product family.
* @details Examples: "MacBookPro18,3", "Latitude 5420", "ThinkPad T490". * @details Examples: "MacBookPro18,3", "Latitude 5420", "ThinkPad T490".
* Implementation varies: reads DMI info on Linux, registry on Windows, sysctl on macOS. * Implementation varies: reads DMI info on Linux, registry on Windows, sysctl on macOS.
* @return A Result containing the host/product identifier String on success, * @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. * @brief Gets the operating system's kernel version string.
* @details Examples: "5.15.0-76-generic", "10.0.22621", "23.1.0". * @details Examples: "5.15.0-76-generic", "10.0.22621", "23.1.0".
* Uses uname() on Linux/macOS, WinRT/registry on Windows. * Uses uname() on Linux/macOS, WinRT/registry on Windows.
* @return A Result containing the kernel version String on success, * @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). * @brief Gets the number of installed packages (Platform-specific).
* @details On Linux, sums counts from various package managers. On other platforms, behavior may vary. * @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, * @return A Result containing the package count (u64) on success,
* or an OsError on failure (e.g., permission errors, command not found) * or a DracError on failure (e.g., permission errors, command not found)
* or if not supported (OsErrorCode::NotSupported). * 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. * @brief Gets the disk usage for the primary/root filesystem.
* @details Uses statvfs on Linux/macOS, GetDiskFreeSpaceExW on Windows. * @details Uses statvfs on Linux/macOS, GetDiskFreeSpaceExW on Windows.
* @return A Result containing the DiskSpace struct (used/total bytes) on success, * @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 } // namespace os

40
src/os/shared.cpp Normal file
View 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

View file

@ -1,12 +1,13 @@
#pragma once #pragma once
#include <cstring>
#ifdef __linux__ #ifdef __linux__
// clang-format off // clang-format off
#include <dbus/dbus.h> // DBus Library #include <dbus/dbus.h> // DBus Library
#include <utility> // std::exchange #include <utility> // std::exchange, std::forward
#include <format> // std::format #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/defs.hpp"
#include "src/core/util/error.hpp" #include "src/core/util/error.hpp"
@ -14,269 +15,375 @@
// clang-format on // clang-format on
namespace dbus { 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, 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. * @brief RAII wrapper for DBusError. Automatically initializes and frees.
*/ */
class ErrorGuard { class Error {
DBusError m_Err {}; DBusError m_err {};
bool m_IsInitialized = false; bool m_isInitialized = false;
public: public:
ErrorGuard() : m_IsInitialized(true) { dbus_error_init(&m_Err); } Error() : m_isInitialized(true) { dbus_error_init(&m_err); }
~ErrorGuard() { ~Error() {
if (m_IsInitialized) if (m_isInitialized)
dbus_error_free(&m_Err); dbus_error_free(&m_err);
} }
ErrorGuard(const ErrorGuard&) = delete; Error(const Error&) = delete;
fn operator=(const ErrorGuard&)->ErrorGuard& = delete; fn operator=(const Error&)->Error& = delete;
ErrorGuard(ErrorGuard&& other) noexcept : m_Err(other.m_Err), m_IsInitialized(other.m_IsInitialized) { Error(Error&& other) noexcept : m_err(other.m_err), m_isInitialized(other.m_isInitialized) {
other.m_IsInitialized = false; other.m_isInitialized = false;
dbus_error_init(&other.m_Err); dbus_error_init(&other.m_err);
} }
fn operator=(ErrorGuard&& other) noexcept -> ErrorGuard& { fn operator=(Error&& other) noexcept -> Error& {
if (this != &other) { if (this != &other) {
if (m_IsInitialized) { if (m_isInitialized)
dbus_error_free(&m_Err); dbus_error_free(&m_err);
}
m_Err = other.m_Err;
m_IsInitialized = other.m_IsInitialized;
other.m_IsInitialized = false; m_err = other.m_err;
dbus_error_init(&other.m_Err); m_isInitialized = other.m_isInitialized;
other.m_isInitialized = false;
dbus_error_init(&other.m_err);
} }
return *this; 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()) if (isSet())
return { code, std::format("D-Bus Error: {} ({})", message(), name()) }; return { code, std::format("D-Bus Error: {} ({})", message(), name()) };
return { DraconisErrorCode::InternalError, "Attempted to convert non-set DBusErrorGuard" }; return { DracErrorCode::InternalError, "Attempted to convert non-set ErrorGuard" };
}
};
/**
* @brief RAII wrapper for DBusMessageIter. Encapsulates iterator operations.
* Note: This wrapper does *not* own the message, only the iterator state.
* It's designed to be used within the scope where the MessageGuard is valid.
*/
class MessageIter {
DBusMessageIter m_iter {};
bool m_isValid = false;
explicit MessageIter(const DBusMessageIter& iter, const bool isValid) : m_iter(iter), m_isValid(isValid) {}
friend class Message;
/**
* @brief Gets the value of a basic-typed argument.
* Unsafe: Caller must ensure 'value' points to memory suitable for the actual argument type.
* @param value Pointer to store the retrieved value.
*/
fn getBasic(void* value) -> void {
if (m_isValid)
dbus_message_iter_get_basic(&m_iter, value);
}
public:
MessageIter(const MessageIter&) = delete;
fn operator=(const MessageIter&)->MessageIter& = delete;
MessageIter(MessageIter&&) = delete;
fn operator=(MessageIter&&)->MessageIter& = delete;
~MessageIter() = default;
/**
* @brief Checks if the iterator is validly initialized.
*/
[[nodiscard]] fn isValid() const -> bool { return m_isValid; }
/**
* @brief Gets the D-Bus type code of the current argument.
* @return The D-Bus type code, or DBUS_TYPE_INVALID otherwise.
*/
[[nodiscard]] fn getArgType() -> int {
return m_isValid ? dbus_message_iter_get_arg_type(&m_iter) : DBUS_TYPE_INVALID;
}
/**
* @brief Gets the element type of the container pointed to by the iterator.
* Only valid if the iterator points to an ARRAY or VARIANT.
* @return The D-Bus type code of the elements, or DBUS_TYPE_INVALID otherwise.
*/
[[nodiscard]] fn getElementType() -> int {
return m_isValid ? dbus_message_iter_get_element_type(&m_iter) : DBUS_TYPE_INVALID;
}
/**
* @brief Advances the iterator to the next argument.
* @return True if successful (moved to a next element), false if at the end or iterator is invalid.
*/
fn next() -> bool { return m_isValid && dbus_message_iter_next(&m_iter); }
/**
* @brief Recurses into a container-type argument (e.g., array, struct, variant).
* @return A new MessageIterGuard for the sub-container. The returned iterator might be invalid
* if the current element is not a container or the main iterator is invalid.
*/
[[nodiscard]] fn recurse() -> MessageIter {
if (!m_isValid)
return MessageIter({}, false);
DBusMessageIter subIter;
dbus_message_iter_recurse(&m_iter, &subIter);
return MessageIter(subIter, true);
}
/**
* @brief Helper to safely get a string argument from the iterator.
* @return An Option containing the string value if the current arg is a valid string, or None otherwise.
*/
[[nodiscard]] fn getString() -> Option<String> {
if (m_isValid && getArgType() == DBUS_TYPE_STRING) {
const char* strPtr = nullptr;
// 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. * @brief RAII wrapper for DBusConnection. Automatically unrefs.
*/ */
class ConnectionGuard { class Connection {
DBusConnection* m_Conn = nullptr; DBusConnection* m_conn = nullptr;
public: public:
explicit ConnectionGuard(DBusConnection* conn = nullptr) : m_Conn(conn) {} explicit Connection(DBusConnection* conn = nullptr) : m_conn(conn) {}
~ConnectionGuard() { ~Connection() {
if (m_Conn) if (m_conn)
dbus_connection_unref(m_Conn); dbus_connection_unref(m_conn);
} }
ConnectionGuard(const ConnectionGuard&) = delete; Connection(const Connection&) = delete;
fn operator=(const ConnectionGuard&)->ConnectionGuard& = delete; fn operator=(const Connection&)->Connection& = delete;
ConnectionGuard(ConnectionGuard&& other) noexcept : m_Conn(std::exchange(other.m_Conn, nullptr)) {} Connection(Connection&& other) noexcept : m_conn(std::exchange(other.m_conn, nullptr)) {}
fn operator=(ConnectionGuard&& other) noexcept -> ConnectionGuard& {
fn operator=(Connection&& other) noexcept -> Connection& {
if (this != &other) { if (this != &other) {
if (m_Conn) if (m_conn)
dbus_connection_unref(m_Conn); dbus_connection_unref(m_conn);
m_Conn = std::exchange(other.m_Conn, nullptr);
}
m_conn = std::exchange(other.m_conn, nullptr);
}
return *this; return *this;
} }
[[nodiscard]] fn get() const -> DBusConnection* { return m_Conn; } /**
explicit operator bool() const { return m_Conn != nullptr; } * @brief Gets the underlying DBusConnection pointer. Use with caution.
* @return The raw DBusConnection pointer, or nullptr if not holding a connection.
*/
[[nodiscard]] fn get() const -> DBusConnection* { return m_conn; }
[[nodiscard]] fn release() -> DBusConnection* { return std::exchange(m_Conn, nullptr); } /**
}; * @brief Sends a message and waits for a reply, blocking execution.
* @param message The D-Bus message guard to send.
* @param timeout_milliseconds Timeout duration in milliseconds.
* @return Result containing the reply MessageGuard on success, or DraconisError on failure.
*/
[[nodiscard]] fn sendWithReplyAndBlock(const Message& message, const i32 timeout_milliseconds = 1000) const
-> Result<Message, DracError> {
if (!m_conn || !message.get())
return Err(
DracError(DracErrorCode::InvalidArgument, "Invalid connection or message provided to sendWithReplyAndBlock")
);
/** Error err;
* @brief RAII wrapper for DBusMessage. Automatically unrefs. DBusMessage* rawReply =
*/ dbus_connection_send_with_reply_and_block(m_conn, message.get(), timeout_milliseconds, err.get());
class MessageGuard {
DBusMessage* m_Msg = nullptr;
public: if (err.isSet()) {
explicit MessageGuard(DBusMessage* msg = nullptr) : m_Msg(msg) {} if (const char* errName = err.name()) {
if (strcmp(errName, DBUS_ERROR_TIMEOUT) == 0 || strcmp(errName, DBUS_ERROR_NO_REPLY) == 0)
return Err(err.toDraconisError(DracErrorCode::Timeout));
~MessageGuard() { if (strcmp(errName, DBUS_ERROR_SERVICE_UNKNOWN) == 0)
if (m_Msg) return Err(err.toDraconisError(DracErrorCode::NotFound));
dbus_message_unref(m_Msg);
}
MessageGuard(const MessageGuard&) = delete; if (strcmp(errName, DBUS_ERROR_ACCESS_DENIED) == 0)
fn operator=(const MessageGuard&)->MessageGuard& = delete; return Err(err.toDraconisError(DracErrorCode::PermissionDenied));
}
MessageGuard(MessageGuard&& other) noexcept : m_Msg(std::exchange(other.m_Msg, nullptr)) {} return Err(err.toDraconisError(DracErrorCode::PlatformSpecific));
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; if (!rawReply)
return Err(DracError(
DracErrorCode::ApiUnavailable,
"dbus_connection_send_with_reply_and_block returned null without setting error (likely timeout or "
"disconnected)"
));
return Message(rawReply);
} }
[[nodiscard]] fn get() const -> DBusMessage* { return m_Msg; } /**
explicit operator bool() const { return m_Msg != nullptr; } * @brief Connects to a D-Bus bus type (Session or System).
* @param bus_type The type of bus (DBUS_BUS_SESSION or DBUS_BUS_SYSTEM).
* @return Result containing a ConnectionGuard on success, or DraconisError on failure.
*/
static fn busGet(const DBusBusType bus_type) -> Result<Connection, DracError> {
Error err;
DBusConnection* rawConn = dbus_bus_get(bus_type, err.get());
[[nodiscard]] fn release() -> DBusMessage* { return std::exchange(m_Msg, nullptr); } if (err.isSet())
return Err(err.toDraconisError(DracErrorCode::ApiUnavailable));
if (!rawConn)
return Err(DracError(DracErrorCode::ApiUnavailable, "dbus_bus_get returned null without setting error"));
return Connection(rawConn);
}
}; };
/**
* @brief Connects to a D-Bus bus type.
* @param bus_type The type of bus (e.g., DBUS_BUS_SESSION, DBUS_BUS_SYSTEM).
* @return Result containing a DBusConnectionGuard on success, or DraconisError on failure.
*/
inline fn BusGet(const DBusBusType bus_type) -> Result<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); }
/**
* @brief Gets the value of a basic-typed argument.
* @param iter The iterator.
* @param value Pointer to store the retrieved value. Must match the argument type.
*/
inline fn MessageIterGetBasic(MessageIter* iter, void* value) -> void { dbus_message_iter_get_basic(iter, value); }
/**
* @brief Advances the iterator to the next argument.
* @param iter The iterator.
* @return True if successful, false if at the end.
*/
inline fn MessageIterNext(MessageIter* iter) -> bool { return dbus_message_iter_next(iter); }
/**
* @brief Helper to safely get a string argument from an iterator.
* @param iter The iterator positioned at a DBUS_TYPE_STRING argument.
* @return An Option containing the string value, or None if the type is wrong or value is null.
*/
inline fn MessageIterGetString(MessageIter* iter) -> Option<String> {
if (MessageIterGetArgType(iter) == DBUS_TYPE_STRING) {
const char* strPtr = nullptr;
MessageIterGetBasic(iter, static_cast<void*>(&strPtr));
if (strPtr)
return String(strPtr);
}
return None;
}
} // namespace dbus } // namespace dbus
#endif // __linux__ #endif // __linux__

View file

@ -25,16 +25,16 @@ namespace wl {
* Automatically handles resource acquisition and cleanup * Automatically handles resource acquisition and cleanup
*/ */
class DisplayGuard { class DisplayGuard {
display* m_Display; display* m_display;
public: public:
/** /**
* Opens a Wayland display connection * Opens a Wayland display connection
*/ */
DisplayGuard() : m_Display(connect(nullptr)) {} DisplayGuard() : m_display(connect(nullptr)) {}
~DisplayGuard() { ~DisplayGuard() {
if (m_Display) if (m_display)
disconnect(m_Display); disconnect(m_display);
} }
// Non-copyable // Non-copyable
@ -42,22 +42,22 @@ namespace wl {
fn operator=(const DisplayGuard&)->DisplayGuard& = delete; fn operator=(const DisplayGuard&)->DisplayGuard& = delete;
// Movable // 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& { fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& {
if (this != &other) { if (this != &other) {
if (m_Display) if (m_display)
disconnect(m_Display); disconnect(m_display);
m_Display = std::exchange(other.m_Display, nullptr); m_display = std::exchange(other.m_display, nullptr);
} }
return *this; 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 get() const -> display* { return m_display; }
[[nodiscard]] fn fd() const -> util::types::i32 { return get_fd(m_Display); } [[nodiscard]] fn fd() const -> util::types::i32 { return get_fd(m_display); }
}; };
} // namespace wl } // namespace wl

View file

@ -89,17 +89,17 @@ namespace xcb {
* Automatically handles resource acquisition and cleanup * Automatically handles resource acquisition and cleanup
*/ */
class DisplayGuard { class DisplayGuard {
connection_t* m_Connection = nullptr; connection_t* m_connection = nullptr;
public: public:
/** /**
* Opens an XCB connection * Opens an XCB connection
* @param name Display name (nullptr for default) * @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() { ~DisplayGuard() {
if (m_Connection) if (m_connection)
disconnect(m_Connection); disconnect(m_connection);
} }
// Non-copyable // Non-copyable
@ -107,22 +107,22 @@ namespace xcb {
fn operator=(const DisplayGuard&)->DisplayGuard& = delete; fn operator=(const DisplayGuard&)->DisplayGuard& = delete;
// Movable // 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& { fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& {
if (this != &other) { if (this != &other) {
if (m_Connection) if (m_connection)
disconnect(m_Connection); disconnect(m_connection);
m_Connection = std::exchange(other.m_Connection, nullptr); m_connection = std::exchange(other.m_connection, nullptr);
} }
return *this; 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* { [[nodiscard]] fn rootScreen() const -> screen_t* {
const setup_t* setup = this->setup(); const setup_t* setup = this->setup();
@ -136,15 +136,15 @@ namespace xcb {
*/ */
template <typename T> template <typename T>
class ReplyGuard { class ReplyGuard {
T* m_Reply = nullptr; T* m_reply = nullptr;
public: public:
ReplyGuard() = default; ReplyGuard() = default;
explicit ReplyGuard(T* reply) : m_Reply(reply) {} explicit ReplyGuard(T* reply) : m_reply(reply) {}
~ReplyGuard() { ~ReplyGuard() {
if (m_Reply) if (m_reply)
free(m_Reply); free(m_reply);
} }
// Non-copyable // Non-copyable
@ -152,22 +152,22 @@ namespace xcb {
fn operator=(const ReplyGuard&)->ReplyGuard& = delete; fn operator=(const ReplyGuard&)->ReplyGuard& = delete;
// Movable // 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& { fn operator=(ReplyGuard&& other) noexcept -> ReplyGuard& {
if (this != &other) { if (this != &other) {
if (m_Reply) if (m_reply)
free(m_Reply); free(m_reply);
m_Reply = std::exchange(other.m_Reply, nullptr); m_reply = std::exchange(other.m_reply, nullptr);
} }
return *this; 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 get() const -> T* { return m_reply; }
[[nodiscard]] fn operator->() const->T* { return m_Reply; } [[nodiscard]] fn operator->() const->T* { return m_reply; }
[[nodiscard]] fn operator*() const->T& { return *m_Reply; } [[nodiscard]] fn operator*() const->T& { return *m_reply; }
}; };
} // namespace xcb } // namespace xcb

@ -1 +0,0 @@
Subproject commit 0285006ac71ba6b16d82a69d3927a85e1c104e4e

@ -1 +0,0 @@
Subproject commit bcf97a05c81ee249cff8a064a222480cadc25429

5
subprojects/glaze.wrap Normal file
View 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