This commit is contained in:
Mars 2025-04-28 04:14:13 -04:00
parent 24b6a72614
commit 1a2fba7fb8
Signed by: pupbrained
GPG key ID: 0FF5B8826803F895
29 changed files with 1676 additions and 1401 deletions

View file

@ -141,8 +141,6 @@
LD_LIBRARY_PATH = "${lib.makeLibraryPath deps}";
NIX_ENFORCE_NO_NATIVE = 0;
name = "C++";
};
}
);

View file

@ -91,7 +91,7 @@ add_project_arguments(common_cpp_args, language : 'cpp')
base_sources = files('src/core/system_data.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', 'src/main.cpp')
platform_sources = {
'linux' : ['src/os/linux.cpp', 'src/os/linux/issetugid_stub.cpp', 'src/os/linux/display_guards.cpp'],
'linux' : ['src/os/linux.cpp', 'src/os/linux/issetugid_stub.cpp', 'src/os/linux/pkg_count.cpp'],
'darwin' : ['src/os/macos.cpp', 'src/os/macos/bridge.mm'],
'windows' : ['src/os/windows.cpp'],
}

View file

@ -1,13 +1,19 @@
#include <filesystem>
#include <stdexcept>
#include "config.hpp"
#include "config.h"
#include <filesystem> // std::filesystem::{path, operator/, exists, create_directories}
#include <fstream> // std::{ifstream, ofstream, operator<<}
#include <stdexcept> // std::runtime_error
#include <system_error> // std::error_code
#include <toml++/impl/parser.hpp> // toml::{parse_file, parse_result}
#include <utility> // std::pair (Pair)
#include "src/util/macros.h"
#include "src/core/util/logging.hpp"
namespace fs = std::filesystem;
namespace {
using util::types::Vec, util::types::CStr, util::types::Exception;
fn GetConfigPath() -> fs::path {
Vec<fs::path> possiblePaths;
@ -25,10 +31,10 @@ namespace {
possiblePaths.push_back(fs::path(".") / "config.toml");
#else
if (Result<String, EnvError> result = GetEnv("XDG_CONFIG_HOME"))
if (Result<String, DraconisError> result = util::helpers::GetEnv("XDG_CONFIG_HOME"))
possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml");
if (Result<String, EnvError> result = GetEnv("HOME")) {
if (Result<String, DraconisError> result = util::helpers::GetEnv("HOME")) {
possiblePaths.emplace_back(fs::path(*result) / ".config" / "draconis++" / "config.toml");
possiblePaths.emplace_back(fs::path(*result) / ".draconis++" / "config.toml");
}
@ -46,7 +52,7 @@ namespace {
if (std::error_code errc; !exists(defaultDir, errc) && !errc) {
create_directories(defaultDir, errc);
if (errc)
WARN_LOG("Warning: Failed to create config directory: {}", errc.message());
warn_log("Warning: Failed to create config directory: {}", errc.message());
}
return possiblePaths[0];
@ -60,7 +66,7 @@ namespace {
std::error_code errc;
create_directories(configPath.parent_path(), errc);
if (errc) {
ERROR_LOG("Failed to create config directory: {}", errc.message());
error_log("Failed to create config directory: {}", errc.message());
return false;
}
@ -76,8 +82,8 @@ namespace {
const passwd* pwd = getpwuid(getuid());
CStr pwdName = pwd ? pwd->pw_name : nullptr;
const Result<String, EnvError> envUser = GetEnv("USER");
const Result<String, EnvError> envLogname = GetEnv("LOGNAME");
const Result<String, DraconisError> envUser = util::helpers::GetEnv("USER");
const Result<String, DraconisError> envLogname = util::helpers::GetEnv("LOGNAME");
String defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User";
#endif
@ -97,7 +103,7 @@ namespace {
std::ofstream file(configPath);
if (!file) {
ERROR_LOG("Failed to open config file for writing: {}", configPath.string());
error_log("Failed to open config file for writing: {}", configPath.string());
return false;
}
@ -124,34 +130,34 @@ namespace {
file << "# lat = 51.5074\n";
file << "# lon = -0.1278\n";
INFO_LOG("Created default config file at {}", configPath.string());
info_log("Created default config file at {}", configPath.string());
return true;
} catch (const std::exception& e) {
ERROR_LOG("Failed to create default config file: {}", e.what());
} catch (const Exception& e) {
error_log("Failed to create default config file: {}", e.what());
return false;
}
}
}
} // namespace
fn Config::getInstance() -> Config {
try {
const fs::path configPath = GetConfigPath();
if (!exists(configPath)) {
INFO_LOG("Config file not found, creating defaults at {}", configPath.string());
info_log("Config file not found, creating defaults at {}", configPath.string());
if (!CreateDefaultConfig(configPath)) {
WARN_LOG("Failed to create default config, using in-memory defaults");
warn_log("Failed to create default config, using in-memory defaults");
return {};
}
}
const toml::parse_result config = toml::parse_file(configPath.string());
DEBUG_LOG("Config loaded from {}", configPath.string());
debug_log("Config loaded from {}", configPath.string());
return fromToml(config);
} catch (const std::exception& e) {
DEBUG_LOG("Config loading failed: {}, using defaults", e.what());
} catch (const Exception& e) {
debug_log("Config loading failed: {}, using defaults", e.what());
return {};
}
}

View file

@ -1,19 +1,31 @@
#pragma once
#ifdef _WIN32
#include <windows.h> // GetUserNameA
#include <windows.h> // GetUserNameA
#else
#include <pwd.h> // getpwuid
#include <unistd.h> // getuid
#include <pwd.h> // getpwuid, passwd
#include <unistd.h> // getuid
#endif
#include <toml++/toml.hpp>
#include <stdexcept> // std::runtime_error
#include <string> // std::string (String)
#include <toml++/impl/node.hpp> // toml::node
#include <toml++/impl/node_view.hpp> // toml::node_view
#include <toml++/impl/table.hpp> // toml::table
#include <variant> // std::variant
#include "src/util/macros.h"
#include "weather.h"
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/helpers.hpp"
#include "src/core/util/types.hpp"
#include "weather.hpp"
using util::error::DraconisError;
using util::types::String, util::types::Array, util::types::Option, util::types::Result;
/// Alias for the location type used in Weather config, can be a city name (String) or coordinates (Coords).
using Location = std::variant<String, Coords>;
using Location = std::variant<String, weather::Coords>;
/**
* @struct General
@ -43,11 +55,11 @@ struct General {
return pwd->pw_name;
// Try to get the username using environment variables
if (Result<String, EnvError> envUser = GetEnv("USER"))
if (Result<String, DraconisError> envUser = util::helpers::GetEnv("USER"))
return *envUser;
// Finally, try to get the username using LOGNAME
if (Result<String, EnvError> envLogname = GetEnv("LOGNAME"))
if (Result<String, DraconisError> envLogname = util::helpers::GetEnv("LOGNAME"))
return *envLogname;
// If all else fails, return a default name
@ -116,7 +128,7 @@ struct Weather {
if (location.is_string())
weather.location = *location.value<String>();
else if (location.is_table())
weather.location = Coords {
weather.location = weather::Coords {
.lat = *location.as_table()->get("lat")->value<double>(),
.lon = *location.as_table()->get("lon")->value<double>(),
};
@ -135,7 +147,7 @@ struct Weather {
* API key, and units. It returns a WeatherOutput object containing the
* retrieved weather data.
*/
[[nodiscard]] fn getWeatherInfo() const -> WeatherOutput;
[[nodiscard]] fn getWeatherInfo() const -> weather::Output;
};
/**

View file

@ -1,18 +1,37 @@
#include <chrono>
#include <curl/curl.h>
#include <filesystem>
#include <fstream>
#include "weather.hpp"
#include "weather.h"
#include <chrono> // std::chrono::{duration, operator-}
#include <curl/curl.h> // curl_easy_init, curl_easy_setopt, curl_easy_perform, curl_easy_cleanup
#include <expected> // std::{expected (Result), unexpected (Err)}
#include <filesystem> // std::filesystem::{path, remove, rename}
#include <format> // std::format
#include <fstream> // std::{ifstream, ofstream}
#include <glaze/core/context.hpp> // glz::{error_ctx, error_code}
#include <glaze/core/opts.hpp> // glz::check_partial_read
#include <glaze/core/read.hpp> // glz::read
#include <glaze/core/reflect.hpp> // glz::format_error
#include <glaze/json/write.hpp> // glz::write_json
#include <glaze/util/atoi.hpp> // glz::atoi
#include <iterator> // std::istreambuf_iterator
#include <system_error> // std::error_code
#include <utility> // std::move
#include <variant> // std::{get, holds_alternative}
#include "config.h"
#include "src/util/macros.h"
#include "src/core/util/defs.hpp"
#include "src/core/util/logging.hpp"
#include "config.hpp"
namespace fs = std::filesystem;
using namespace std::string_literals;
using namespace weather;
using util::types::i32, util::types::Err, util::types::Exception;
namespace {
constexpr glz::opts glaze_opts = { .error_on_unknown_keys = false };
using glz::opts, glz::error_ctx, glz::error_code, glz::write_json, glz::read, glz::format_error;
constexpr opts glaze_opts = { .error_on_unknown_keys = false };
fn GetCachePath() -> Result<fs::path, String> {
std::error_code errc;
@ -25,7 +44,7 @@ namespace {
return cachePath;
}
fn ReadCacheFromFile() -> Result<WeatherOutput, String> {
fn ReadCacheFromFile() -> Result<Output, String> {
Result<fs::path, String> cachePath = GetCachePath();
if (!cachePath)
@ -36,27 +55,27 @@ namespace {
if (!ifs.is_open())
return Err("Cache file not found: " + cachePath->string());
DEBUG_LOG("Reading from cache file...");
debug_log("Reading from cache file...");
try {
const String content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
WeatherOutput result;
Output result;
if (const glz::error_ctx errc = glz::read<glaze_opts>(result, content); errc.ec != glz::error_code::none)
return Err("JSON parse error: " + glz::format_error(errc, content));
if (const error_ctx errc = read<glaze_opts>(result, content); errc.ec != error_code::none)
return Err(std::format("JSON parse error: {}", format_error(errc, content)));
DEBUG_LOG("Successfully read from cache file.");
debug_log("Successfully read from cache file.");
return result;
} catch (const std::exception& e) { return Err("Error reading cache: "s + e.what()); }
} catch (const Exception& e) { return Err(std::format("Error reading cache: {}", e.what())); }
}
fn WriteCacheToFile(const WeatherOutput& data) -> Result<void, String> {
fn WriteCacheToFile(const Output& data) -> Result<void, String> {
Result<fs::path, String> cachePath = GetCachePath();
if (!cachePath)
return Err(cachePath.error());
DEBUG_LOG("Writing to cache file...");
debug_log("Writing to cache file...");
fs::path tempPath = *cachePath;
tempPath += ".tmp";
@ -68,8 +87,8 @@ namespace {
String jsonStr;
if (const glz::error_ctx errc = glz::write_json(data, jsonStr); errc.ec != glz::error_code::none)
return Err("JSON serialization error: " + glz::format_error(errc, jsonStr));
if (const error_ctx errc = write_json(data, jsonStr); errc.ec != error_code::none)
return Err("JSON serialization error: " + format_error(errc, jsonStr));
ofs << jsonStr;
if (!ofs)
@ -80,24 +99,24 @@ namespace {
fs::rename(tempPath, *cachePath, errc);
if (errc) {
if (!fs::remove(tempPath, errc))
DEBUG_LOG("Failed to remove temp file: {}", errc.message());
debug_log("Failed to remove temp file: {}", errc.message());
return Err("Failed to replace cache file: " + errc.message());
return Err(std::format("Failed to replace cache file: {}", errc.message()));
}
DEBUG_LOG("Successfully wrote to cache file.");
debug_log("Successfully wrote to cache file.");
return {};
} catch (const std::exception& e) { return Err("File operation error: "s + e.what()); }
} catch (const Exception& e) { return Err(std::format("File operation error: {}", e.what())); }
}
fn WriteCallback(void* contents, const size_t size, const size_t nmemb, String* str) -> size_t {
const size_t totalSize = size * nmemb;
fn WriteCallback(void* contents, const usize size, const usize nmemb, String* str) -> usize {
const usize totalSize = size * nmemb;
str->append(static_cast<char*>(contents), totalSize);
return totalSize;
}
fn MakeApiRequest(const String& url) -> Result<WeatherOutput, String> {
DEBUG_LOG("Making API request to URL: {}", url);
fn MakeApiRequest(const String& url) -> Result<Output, String> {
debug_log("Making API request to URL: {}", url);
CURL* curl = curl_easy_init();
String responseBuffer;
@ -116,40 +135,40 @@ namespace {
if (res != CURLE_OK)
return Err(std::format("cURL error: {}", curl_easy_strerror(res)));
WeatherOutput output;
Output output;
if (const glz::error_ctx errc = glz::read<glaze_opts>(output, responseBuffer); errc.ec != glz::error_code::none)
return Err("API response parse error: " + glz::format_error(errc, responseBuffer));
if (const error_ctx errc = glz::read<glaze_opts>(output, responseBuffer); errc.ec != error_code::none)
return Err("API response parse error: " + format_error(errc, responseBuffer));
return std::move(output);
}
}
} // namespace
fn Weather::getWeatherInfo() const -> WeatherOutput {
fn Weather::getWeatherInfo() const -> Output {
using namespace std::chrono;
if (Result<WeatherOutput, String> data = ReadCacheFromFile()) {
const WeatherOutput& dataVal = *data;
if (Result<Output, String> data = ReadCacheFromFile()) {
const Output& dataVal = *data;
if (const duration<double> cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt));
cacheAge < 10min) {
DEBUG_LOG("Using valid cache");
debug_log("Using valid cache");
return dataVal;
}
DEBUG_LOG("Cache expired");
debug_log("Cache expired");
} else {
DEBUG_LOG("Cache error: {}", data.error());
debug_log("Cache error: {}", data.error());
}
fn handleApiResult = [](const Result<WeatherOutput, String>& result) -> WeatherOutput {
fn handleApiResult = [](const Result<Output, String>& result) -> Output {
if (!result) {
ERROR_LOG("API request failed: {}", result.error());
return WeatherOutput {};
error_log("API request failed: {}", result.error());
return Output {};
}
if (Result<void, String> writeResult = WriteCacheToFile(*result); !writeResult)
ERROR_LOG("Failed to write cache: {}", writeResult.error());
error_log("Failed to write cache: {}", writeResult.error());
return *result;
};
@ -157,7 +176,7 @@ fn Weather::getWeatherInfo() const -> WeatherOutput {
if (std::holds_alternative<String>(location)) {
const auto& city = std::get<String>(location);
char* escaped = curl_easy_escape(nullptr, city.c_str(), static_cast<i32>(city.length()));
DEBUG_LOG("Requesting city: {}", escaped);
debug_log("Requesting city: {}", escaped);
const String apiUrl =
std::format("https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units={}", escaped, api_key, units);
@ -167,7 +186,7 @@ fn Weather::getWeatherInfo() const -> WeatherOutput {
}
const auto& [lat, lon] = std::get<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(
"https://api.openweathermap.org/data/2.5/weather?lat={:.3f}&lon={:.3f}&appid={}&units={}", lat, lon, api_key, units

View file

@ -1,73 +0,0 @@
#pragma once
#include <glaze/glaze.hpp>
#include "../util/types.h"
// NOLINTBEGIN(readability-identifier-naming) - Needs to specifically use `glaze`
/**
* @struct Condition
* @brief Represents weather conditions.
*/
struct Condition {
String description; ///< Weather condition description (e.g., "clear sky", "light rain").
/**
* @brief Glaze serialization and deserialization for Condition.
*/
struct [[maybe_unused]] glaze {
using T = Condition;
static constexpr glz::detail::Object value = glz::object("description", &T::description);
};
};
/**
* @struct Main
* @brief Represents the main weather data.
*/
struct Main {
f64 temp; ///< Temperature in degrees (C/F, depending on config).
/**
* @brief Glaze serialization and deserialization for Main.
*/
struct [[maybe_unused]] glaze {
using T = Main;
static constexpr glz::detail::Object value = glz::object("temp", &T::temp);
};
};
/**
* @struct Coords
* @brief Represents geographical coordinates.
*/
struct Coords {
double lat; ///< Latitude coordinate.
double lon; ///< Longitude coordinate.
};
/**
* @struct WeatherOutput
* @brief Represents the output of the weather API.
*
* Contains main weather data, location name, and weather conditions.
*/
struct WeatherOutput {
Main main; ///< Main weather data (temperature, etc.).
String name; ///< Location name (e.g., city name).
Vec<Condition> weather; ///< List of weather conditions (e.g., clear, rain).
usize dt; ///< Timestamp of the weather data (in seconds since epoch).
/**
* @brief Glaze serialization and deserialization for WeatherOutput.
*/
struct [[maybe_unused]] glaze {
using T = WeatherOutput;
static constexpr glz::detail::Object value =
glz::object("main", &T::main, "name", &T::name, "weather", &T::weather, "dt", &T::dt);
};
};
// NOLINTEND(readability-identifier-naming)

78
src/config/weather.hpp Normal file
View file

@ -0,0 +1,78 @@
#pragma once
#include <glaze/core/common.hpp> // object
#include <glaze/core/meta.hpp> // Object
#include "src/core/util/types.hpp"
namespace weather {
using glz::detail::Object, glz::object;
using util::types::String, util::types::Vec, util::types::f64, util::types::usize;
// NOLINTBEGIN(readability-identifier-naming) - Needs to specifically use `glaze`
/**
* @struct Condition
* @brief Represents weather conditions.
*/
struct Condition {
String description; ///< Weather condition description (e.g., "clear sky", "light rain").
/**
* @brief Glaze serialization and deserialization for Condition.
*/
struct [[maybe_unused]] glaze {
using T = Condition;
static constexpr Object value = object("description", &T::description);
};
};
/**
* @struct Main
* @brief Represents the main weather data.
*/
struct Main {
f64 temp; ///< Temperature in degrees (C/F, depending on config).
/**
* @brief Glaze serialization and deserialization for Main.
*/
struct [[maybe_unused]] glaze {
using T = Main;
static constexpr Object value = object("temp", &T::temp);
};
};
/**
* @struct Coords
* @brief Represents geographical coordinates.
*/
struct Coords {
double lat; ///< Latitude coordinate.
double lon; ///< Longitude coordinate.
};
/**
* @struct Output
* @brief Represents the output of the weather API.
*
* Contains main weather data, location name, and weather conditions.
*/
struct Output {
Main main; ///< Main weather data (temperature, etc.).
String name; ///< Location name (e.g., city name).
Vec<Condition> weather; ///< List of weather conditions (e.g., clear, rain).
usize dt; ///< Timestamp of the weather data (in seconds since epoch).
/**
* @brief Glaze serialization and deserialization for WeatherOutput.
*/
struct [[maybe_unused]] glaze {
using T = Output;
static constexpr Object value = object("main", &T::main, "name", &T::name, "weather", &T::weather, "dt", &T::dt);
};
};
// NOLINTEND(readability-identifier-naming)
} // namespace weather

View file

@ -1,15 +1,17 @@
#include "system_data.h"
#include "system_data.hpp"
#include <chrono> // for year_month_day, floor, days...
#include <exception> // for exception
#include <future> // for future, async, launch
#include <locale> // for locale
#include <stdexcept> // for runtime_error
#include <tuple> // for tuple, get, make_tuple
#include <utility> // for move
#include <chrono> // std::chrono::{year_month_day, floor, days, system_clock}
#include <exception> // std::exception (Exception)
#include <future> // std::{future, async, launch}
#include <locale> // std::locale
#include <stdexcept> // std::runtime_error
#include <tuple> // std::{tuple, get, make_tuple}
#include <utility> // std::move
#include "src/config/config.h" // for Config, Weather, NowPlaying
#include "src/os/os.h" // for GetDesktopEnvironment, GetHost...
#include "src/config/config.hpp"
#include "src/os/os.hpp"
#include "util/logging.hpp"
namespace {
fn GetDate() -> String {
@ -20,13 +22,13 @@ namespace {
try {
return std::format(std::locale(""), "{:%B %d}", ymd);
} catch (const std::runtime_error& e) {
WARN_LOG("Could not retrieve or use system locale ({}). Falling back to default C locale.", e.what());
warn_log("Could not retrieve or use system locale ({}). Falling back to default C locale.", e.what());
return std::format(std::locale::classic(), "{:%B %d}", ymd);
}
}
} // namespace
SystemData SystemData::fetchSystemData(const Config& config) {
fn SystemData::fetchSystemData(const Config& config) -> SystemData {
SystemData data {
.date = GetDate(),
.host = os::GetHost(),
@ -42,13 +44,13 @@ SystemData SystemData::fetchSystemData(const Config& config) {
};
auto diskShellFuture = std::async(std::launch::async, [] {
Result<DiskSpace, OsError> diskResult = os::GetDiskUsage();
Result<DiskSpace, DraconisError> diskResult = os::GetDiskUsage();
Option<String> shellOption = os::GetShell();
return std::make_tuple(std::move(diskResult), std::move(shellOption));
});
std::future<WeatherOutput> weatherFuture;
std::future<Result<MediaInfo, NowPlayingError>> nowPlayingFuture;
std::future<weather::Output> weatherFuture;
std::future<Result<MediaInfo, DraconisError>> nowPlayingFuture;
if (config.weather.enabled)
weatherFuture = std::async(std::launch::async, [&config] { return config.weather.getWeatherInfo(); });
@ -65,7 +67,7 @@ SystemData SystemData::fetchSystemData(const Config& config) {
try {
data.weather_info = weatherFuture.get();
} catch (const std::exception& e) {
ERROR_LOG("Failed to get weather info: {}", e.what());
error_log("Failed to get weather info: {}", e.what());
data.weather_info = None;
}

View file

@ -1,88 +0,0 @@
#pragma once
#include <format> // for formatter, format_to
#include <string> // for basic_string
#include <thread> // for formatter
#include "src/config/weather.h" // for WeatherOutput
#include "src/util/macros.h" // for fn
#include "src/util/types.h" // for OsError, DiskSpace, Result, String
struct Config;
/**
* @struct BytesToGiB
* @brief Helper struct to format a byte value to GiB (Gibibytes).
*
* Encapsulates a byte value and provides a custom formatter
* to convert it to GiB for display purposes.
*/
struct BytesToGiB {
u64 value; ///< The byte value to be converted.
};
/// @brief Conversion factor from bytes to GiB
constexpr u64 GIB = 1'073'741'824;
/**
* @brief Custom formatter for BytesToGiB.
*
* Allows formatting BytesToGiB values using std::format.
* Outputs the value in GiB with two decimal places.
*
* @code{.cpp}
* #include <format>
* #include "system_data.h" // Assuming BytesToGiB is defined here
*
* i32 main() {
* BytesToGiB data_size{2'147'483'648}; // 2 GiB
* String formatted = std::format("Size: {}", data_size);
* std::println("{}", formatted); // formatted will be "Size: 2.00GiB"
* return 0;
* }
* @endcode
*/
template <>
struct std::formatter<BytesToGiB> : std::formatter<double> {
/**
* @brief Formats the BytesToGiB value.
* @param BTG The BytesToGiB instance to format.
* @param ctx The formatting context.
* @return An iterator to the end of the formatted output.
*/
fn format(const BytesToGiB& BTG, auto& ctx) const {
return std::format_to(ctx.out(), "{:.2f}GiB", static_cast<f64>(BTG.value) / GIB);
}
};
/**
* @struct SystemData
* @brief Holds various pieces of system information collected from the OS.
*
* This structure aggregates information about the system,
* in order to display it at all at once during runtime.
*/
struct SystemData {
using NowPlayingResult = Option<Result<MediaInfo, NowPlayingError>>;
// clang-format off
String date; ///< Current date (e.g., "April 26th"). Always expected to succeed.
Result<String, OsError> host; ///< Host/product family (e.g., "MacBookPro18,3") or OS error.
Result<String, OsError> kernel_version; ///< OS kernel version (e.g., "23.4.0") or OS error.
Result<String, OsError> os_version; ///< OS pretty name (e.g., "macOS Sonoma 14.4.1") or OS error.
Result<u64, OsError> 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, OsError> 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<WeatherOutput> weather_info; ///< Optional: Weather information. None if disabled or error during fetch.
// clang-format on
/**
* @brief Fetches all system data asynchronously.
* @param config The application configuration.
* @return A populated SystemData object.
*/
static fn fetchSystemData(const Config& config) -> SystemData;
};

91
src/core/system_data.hpp Normal file
View file

@ -0,0 +1,91 @@
#pragma once
#include <format> // std::{formatter, format_to}
#include "src/config/weather.hpp" // weather::Output
#include "util/defs.hpp"
#include "util/error.hpp"
#include "util/types.hpp"
struct Config;
using util::types::u64, util::types::f64, util::types::String, util::types::Option, util::types::Result,
util::types::MediaInfo, util::types::DiskSpace;
/**
* @struct BytesToGiB
* @brief Helper struct to format a byte value to GiB (Gibibytes).
*
* Encapsulates a byte value and provides a custom formatter
* to convert it to GiB for display purposes.
*/
struct BytesToGiB {
u64 value; ///< The byte value to be converted.
};
/// @brief Conversion factor from bytes to GiB
constexpr u64 GIB = 1'073'741'824;
/**
* @brief Custom formatter for BytesToGiB.
*
* Allows formatting BytesToGiB values using std::format.
* Outputs the value in GiB with two decimal places.
*
* @code{.cpp}
* #include <format>
* #include "system_data.h" // Assuming BytesToGiB is defined here
*
* i32 main() {
* BytesToGiB data_size{2'147'483'648}; // 2 GiB
* String formatted = std::format("Size: {}", data_size);
* std::println("{}", formatted); // formatted will be "Size: 2.00GiB"
* return 0;
* }
* @endcode
*/
template <>
struct std::formatter<BytesToGiB> : std::formatter<double> {
/**
* @brief Formats the BytesToGiB value.
* @param BTG The BytesToGiB instance to format.
* @param ctx The formatting context.
* @return An iterator to the end of the formatted output.
*/
fn format(const BytesToGiB& BTG, auto& ctx) const {
return std::format_to(ctx.out(), "{:.2f}GiB", static_cast<f64>(BTG.value) / GIB);
}
};
/**
* @struct SystemData
* @brief Holds various pieces of system information collected from the OS.
*
* This structure aggregates information about the system,
* in order to display it at all at once during runtime.
*/
struct SystemData {
using NowPlayingResult = Option<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.
* @param config The application configuration.
* @return A populated SystemData object.
*/
static fn fetchSystemData(const Config& config) -> SystemData;
};

12
src/core/util/defs.hpp Normal file
View file

@ -0,0 +1,12 @@
#pragma once
// Fixes conflict in Windows with <windows.h>
#ifdef _WIN32
#undef ERROR
#endif // _WIN32
/// Macro alias for trailing return type functions.
#define fn auto
/// Macro alias for std::nullopt, represents an empty optional value.
#define None std::nullopt

159
src/core/util/error.hpp Normal file
View file

@ -0,0 +1,159 @@
#pragma once
#include <format> // std::format
#include <source_location> // std::source_location
#include <string_view> // std::string_view (StringView)
#include <system_error> // std::error_code
#ifdef _WIN32
#include <winrt/base.h> // winrt::hresult_error
#elifdef __linux__
#include <dbus-cxx/error.h> // DBus::Error
#endif
#include "src/core/util/types.hpp"
namespace util::error {
using types::u8, types::i32, types::String, types::StringView, types::Exception;
/**
* @enum DraconisErrorCode
* @brief Error codes for general OS-level operations.
*/
enum class DraconisErrorCode : u8 {
IoError, ///< General I/O error (filesystem, pipes, etc.).
PermissionDenied, ///< Insufficient permissions to perform the operation.
NotFound, ///< A required resource (file, registry key, device, API endpoint) was not found.
ParseError, ///< Failed to parse data obtained from the OS (e.g., file content, API output).
ApiUnavailable, ///< A required OS service/API is unavailable or failed unexpectedly at runtime.
NotSupported, ///< The requested operation is not supported on this platform, version, or configuration.
Timeout, ///< An operation timed out (e.g., waiting for IPC reply).
BufferTooSmall, ///< Optional: Keep if using fixed C-style buffers, otherwise remove.
InternalError, ///< An error occurred within the application's OS abstraction code logic.
NetworkError, ///< A network-related error occurred (e.g., DNS resolution, connection failure).
PlatformSpecific, ///< An unmapped error specific to the underlying OS platform occurred (check message).
Other, ///< A generic or unclassified error originating from the OS or an external library.
};
/**
* @struct DraconisError
* @brief Holds structured information about an OS-level error.
*
* Used as the error type in Result for many os:: functions.
*/
struct DraconisError {
// ReSharper disable CppDFANotInitializedField
String message; ///< A descriptive error message, potentially including platform details.
DraconisErrorCode code; ///< The general category of the error.
std::source_location location; ///< The source location where the error occurred (file, line, function).
// ReSharper restore CppDFANotInitializedField
DraconisError(
const DraconisErrorCode errc,
String msg,
const std::source_location& loc = std::source_location::current()
)
: message(std::move(msg)), code(errc), location(loc) {}
explicit DraconisError(const Exception& exc, const std::source_location& loc = std::source_location::current())
: message(exc.what()), code(DraconisErrorCode::InternalError), location(loc) {}
explicit DraconisError(
const std::error_code& errc,
const std::source_location& loc = std::source_location::current()
)
: message(errc.message()), location(loc) {
using enum DraconisErrorCode;
using enum std::errc;
switch (static_cast<std::errc>(errc.value())) {
case permission_denied: code = PermissionDenied; break;
case no_such_file_or_directory: code = NotFound; break;
case timed_out: code = Timeout; break;
case io_error: code = IoError; break;
case network_unreachable:
case network_down:
case connection_refused: code = NetworkError; break;
case not_supported: code = NotSupported; break;
default: code = errc.category() == std::generic_category() ? InternalError : PlatformSpecific; break;
}
}
#ifdef _WIN32
explicit OsError(const winrt::hresult_error& e) : message(winrt::to_string(e.message())) {
switch (e.code()) {
case E_ACCESSDENIED: code = OsErrorCode::PermissionDenied; break;
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
case HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND):
case HRESULT_FROM_WIN32(ERROR_SERVICE_NOT_FOUND): code = OsErrorCode::NotFound; break;
case HRESULT_FROM_WIN32(ERROR_TIMEOUT):
case HRESULT_FROM_WIN32(ERROR_SEM_TIMEOUT): code = OsErrorCode::Timeout; break;
case HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED): code = OsErrorCode::NotSupported; break;
default: code = OsErrorCode::PlatformSpecific; break;
}
}
#else
DraconisError(const DraconisErrorCode code_hint, const int errno_val)
: message(std::system_category().message(errno_val)), code(code_hint) {
using enum DraconisErrorCode;
switch (errno_val) {
case EACCES: code = PermissionDenied; break;
case ENOENT: code = NotFound; break;
case ETIMEDOUT: code = Timeout; break;
case ENOTSUP: code = NotSupported; break;
default: code = PlatformSpecific; break;
}
}
static auto withErrno(const String& context, const std::source_location& loc = std::source_location::current())
-> DraconisError {
const i32 errNo = errno;
const String msg = std::system_category().message(errNo);
const String fullMsg = std::format("{}: {}", context, msg);
DraconisErrorCode code = DraconisErrorCode::PlatformSpecific;
switch (errNo) {
case EACCES:
case EPERM: code = DraconisErrorCode::PermissionDenied; break;
case ENOENT: code = DraconisErrorCode::NotFound; break;
case ETIMEDOUT: code = DraconisErrorCode::Timeout; break;
case ENOTSUP: code = DraconisErrorCode::NotSupported; break;
case EIO: code = DraconisErrorCode::IoError; break;
case ECONNREFUSED:
case ENETDOWN:
case ENETUNREACH: code = DraconisErrorCode::NetworkError; break;
default: code = DraconisErrorCode::PlatformSpecific; break;
}
return DraconisError { code, fullMsg, loc };
}
#ifdef __linux__
static auto fromDBus(const DBus::Error& err, const std::source_location& loc = std::source_location::current())
-> DraconisError {
String name = err.name();
DraconisErrorCode codeHint = DraconisErrorCode::PlatformSpecific;
String message;
using namespace std::string_view_literals;
if (name == "org.freedesktop.DBus.Error.ServiceUnknown"sv ||
name == "org.freedesktop.DBus.Error.NameHasNoOwner"sv) {
codeHint = DraconisErrorCode::NotFound;
message = std::format("DBus service/name not found: {}", err.message());
} else if (name == "org.freedesktop.DBus.Error.NoReply"sv || name == "org.freedesktop.DBus.Error.Timeout"sv) {
codeHint = DraconisErrorCode::Timeout;
message = std::format("DBus timeout/no reply: {}", err.message());
} else if (name == "org.freedesktop.DBus.Error.AccessDenied"sv) {
codeHint = DraconisErrorCode::PermissionDenied;
message = std::format("DBus access denied: {}", err.message());
} else {
message = std::format("DBus error: {} - {}", name, err.message());
}
return DraconisError { codeHint, message, loc };
}
#endif
#endif
};
} // namespace util::error

43
src/core/util/helpers.hpp Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include "defs.hpp"
#include "error.hpp"
#include "types.hpp"
namespace util::helpers {
using types::Result, types::String, types::CStr, types::UniquePointer, types::Err;
/**
* @brief Safely retrieves an environment variable.
* @param name The name of the environment variable to retrieve.
* @return A Result containing the value of the environment variable as a String,
* or an EnvError if an error occurred.
*/
[[nodiscard]] inline fn GetEnv(CStr name) -> Result<String, error::DraconisError> {
#ifdef _WIN32
char* rawPtr = nullptr;
usize bufferSize = 0;
// Use _dupenv_s to safely retrieve environment variables on Windows
const i32 err = _dupenv_s(&rawPtr, &bufferSize, name);
const UniquePointer<char, decltype(&free)> ptrManager(rawPtr, free);
if (err != 0)
return Err(DraconisError(DraconisErrorCode::PermissionDenied, "Failed to retrieve environment variable"));
if (!ptrManager)
return Err(DraconisError(DraconisErrorCode::NotFound, "Environment variable not found"));
return ptrManager.get();
#else
// Use std::getenv to retrieve environment variables on POSIX systems
const CStr value = std::getenv(name);
if (!value)
return Err(error::DraconisError(error::DraconisErrorCode::NotFound, "Environment variable not found"));
return value;
#endif
}
} // namespace util::helpers

286
src/core/util/logging.hpp Normal file
View file

@ -0,0 +1,286 @@
#pragma once
#include <chrono> // std::chrono::{days, floor, seconds, system_clock}
#include <filesystem> // std::filesystem::path
#include <format> // std::format
#include <print> // std::print
#include <source_location> // std::source_location
#include <utility> // std::{forward, make_pair}
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/types.hpp"
namespace util::logging {
using types::u8, types::i32, types::String, types::StringView, types::Option;
/**
* @namespace term
* @brief Provides terminal-related utilities, including color and style formatting.
*/
namespace term {
/**
* @enum Emphasis
* @brief Represents text emphasis styles.
*
* Enum values can be combined using bitwise OR to apply multiple styles at once.
*/
enum class Emphasis : u8 {
Bold, ///< Bold text.
Italic ///< Italic text.
};
/**
* @enum Color
* @brief Represents ANSI color codes for terminal output.
*
* Color codes can be used to format terminal output.
*/
enum class Color : u8 {
Black = 30, ///< Black color.
Red = 31, ///< Red color.
Green = 32, ///< Green color.
Yellow = 33, ///< Yellow color.
Blue = 34, ///< Blue color.
Magenta = 35, ///< Magenta color.
Cyan = 36, ///< Cyan color.
White = 37, ///< White color.
BrightBlack = 90, ///< Bright black (gray) color.
BrightRed = 91, ///< Bright red color.
BrightGreen = 92, ///< Bright green color.
BrightYellow = 93, ///< Bright yellow color.
BrightBlue = 94, ///< Bright blue color.
BrightMagenta = 95, ///< Bright magenta color.
BrightCyan = 96, ///< Bright cyan color.
BrightWhite = 97, ///< Bright white color.
};
/**
* @brief Combines two emphasis styles using bitwise OR.
* @param emphA The first emphasis style.
* @param emphB The second emphasis style.
* @return The combined emphasis style.
*/
constexpr fn operator|(Emphasis emphA, Emphasis emphB)->Emphasis {
return static_cast<Emphasis>(static_cast<u8>(emphA) | static_cast<u8>(emphB));
}
/**
* @brief Checks if two emphasis styles are equal using bitwise AND.
* @param emphA The first emphasis style.
* @param emphB The second emphasis style.
* @return The result of the bitwise AND operation.
*/
constexpr fn operator&(Emphasis emphA, Emphasis emphB)->u8 {
return static_cast<u8>(emphA) & static_cast<u8>(emphB);
}
/**
* @struct Style
* @brief Represents a combination of text styles.
*
* Emphasis and color are both optional, allowing for flexible styling.
*/
struct Style {
Option<Emphasis> emph; ///< Optional emphasis style.
Option<Color> fg_col; ///< Optional foreground color style.
/**
* @brief Generates the ANSI escape code for the combined styles.
* @return The ANSI escape code for the combined styles.
*/
[[nodiscard]] fn ansiCode() const -> String {
String result;
if (emph) {
if ((*emph & Emphasis::Bold) != 0)
result += "\033[1m";
if ((*emph & Emphasis::Italic) != 0)
result += "\033[3m";
}
if (fg_col)
result += std::format("\033[{}m", static_cast<u8>(*fg_col));
return result;
}
};
/**
* @brief Combines an emphasis style and a foreground color into a Style.
* @param emph The emphasis style to apply.
* @param fgColor The foreground color to apply.
* @return The combined style.
*/
// ReSharper disable CppDFAConstantParameter
constexpr fn operator|(const Emphasis emph, const Color fgColor)->Style {
return { .emph = emph, .fg_col = fgColor };
}
// ReSharper restore CppDFAConstantParameter
/**
* @brief Combines a foreground color and an emphasis style into a Style.
* @param fgColor The foreground color to apply.
* @param emph The emphasis style to apply.
* @return The combined style.
*/
constexpr fn operator|(const Color fgColor, const Emphasis emph)->Style {
return { .emph = emph, .fg_col = fgColor };
}
/**
* @brief Prints formatted text with the specified style.
* @tparam Args Parameter pack for format arguments.
* @param style The Style object containing emphasis and/or color.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn Print(const Style& style, std::format_string<Args...> fmt, Args&&... args) -> void {
if (const String styleCode = style.ansiCode(); styleCode.empty())
std::print(fmt, std::forward<Args>(args)...);
else
std::print("{}{}{}", styleCode, std::format(fmt, std::forward<Args>(args)...), "\033[0m");
}
/**
* @brief Prints formatted text with the specified foreground color.
* @tparam Args Parameter pack for format arguments.
* @param fgColor The foreground color to apply.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn Print(const Color& fgColor, std::format_string<Args...> fmt, Args&&... args) -> void {
Print({ .emph = None, .fg_col = fgColor }, fmt, std::forward<Args>(args)...);
}
/**
* @brief Prints formatted text with the specified emphasis style.
* @tparam Args Parameter pack for format arguments.
* @param emph The emphasis style to apply.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn Print(const Emphasis emph, std::format_string<Args...> fmt, Args&&... args) -> void {
Print({ .emph = emph, .fg_col = None }, fmt, std::forward<Args>(args)...);
}
/**
* @brief Prints formatted text with no specific style (default terminal style).
* @tparam Args Parameter pack for format arguments.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn Print(std::format_string<Args...> fmt, Args&&... args) -> void {
// Directly use std::print for unstyled output
std::print(fmt, std::forward<Args>(args)...);
}
} // namespace term
/**
* @enum LogLevel
* @brief Represents different log levels.
*/
enum class LogLevel : u8 { DEBUG, INFO, WARN, ERROR };
/**
* @brief Logs a message with the specified log level, source location, and format string.
* @tparam Args Parameter pack for format arguments.
* @param level The log level (DEBUG, INFO, WARN, ERROR).
* @param loc The source location of the log message.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_string<Args...> fmt, Args&&... args) {
using namespace std::chrono;
using namespace term;
#ifdef _MSC_VER
using enum term::Color;
#else
using enum Color;
#endif // _MSC_VER
const auto [color, levelStr] = [&] {
switch (level) {
case LogLevel::DEBUG: return std::make_pair(Cyan, "DEBUG");
case LogLevel::INFO: return std::make_pair(Green, "INFO ");
case LogLevel::WARN: return std::make_pair(Yellow, "WARN ");
case LogLevel::ERROR: return std::make_pair(Red, "ERROR");
default: std::unreachable();
}
}();
Print(BrightWhite, "[{:%X}] ", std::chrono::floor<seconds>(system_clock::now()));
Print(Emphasis::Bold | color, "{} ", levelStr);
Print(fmt, std::forward<Args>(args)...);
#ifndef NDEBUG
Print(BrightWhite, "\n{:>14} ", "╰──");
Print(
Emphasis::Italic | BrightWhite,
"{}:{}",
std::filesystem::path(loc.file_name()).lexically_normal().string(),
loc.line()
);
#endif // !NDEBUG
Print("\n");
}
template <typename ErrorType>
fn LogAppError(const LogLevel level, const ErrorType& error_obj) {
using DecayedErrorType = std::decay_t<ErrorType>;
std::source_location log_location;
String error_message_part;
if constexpr (std::is_same_v<DecayedErrorType, error::DraconisError>) {
log_location = error_obj.location;
error_message_part = error_obj.message;
} else {
log_location = std::source_location::current();
if constexpr (std::is_base_of_v<std::exception, DecayedErrorType>)
error_message_part = error_obj.what();
else if constexpr (requires { error_obj.message; })
error_message_part = error_obj.message;
else
error_message_part = "Unknown error type logged";
}
LogImpl(level, log_location, "{}", error_message_part);
}
#ifndef NDEBUG
#define debug_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::DEBUG, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define debug_at(error_obj) ::util::logging::LogAppError(::util::logging::LogLevel::DEBUG, error_obj);
#else
#define debug_log(...) ((void)0)
#define debug_at(...) ((void)0)
#endif
#define info_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::INFO, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define info_at(error_obj) ::util::logging::LogAppError(::util::logging::LogLevel::INFO, error_obj);
#define warn_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::WARN, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define warn_at(error_obj) ::util::logging::LogAppError(::util::logging::LogLevel::WARN, error_obj);
#define error_log(fmt, ...) \
::util::logging::LogImpl( \
::util::logging::LogLevel::ERROR, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
)
#define error_at(error_obj) ::util::logging::LogAppError(::util::logging::LogLevel::ERROR, error_obj);
} // namespace util::logging

145
src/core/util/types.hpp Normal file
View file

@ -0,0 +1,145 @@
#pragma once
#include <array> // std::array (Array)
#include <expected> // std::expected (Result)
#include <map> // std::map (Map)
#include <memory> // std::shared_ptr and std::unique_ptr (SharedPointer, UniquePointer)
#include <optional> // std::optional (Option)
#include <string> // std::string (String, StringView)
#include <string_view> // std::string_view (StringView)
#include <utility> // std::pair (Pair)
#include <vector> // std::vector (Vec)
namespace util::types {
using u8 = std::uint8_t; ///< 8-bit unsigned integer.
using u16 = std::uint16_t; ///< 16-bit unsigned integer.
using u32 = std::uint32_t; ///< 32-bit unsigned integer.
using u64 = std::uint64_t; ///< 64-bit unsigned integer.
using i8 = std::int8_t; ///< 8-bit signed integer.
using i16 = std::int16_t; ///< 16-bit signed integer.
using i32 = std::int32_t; ///< 32-bit signed integer.
using i64 = std::int64_t; ///< 64-bit signed integer.
using f32 = float; ///< 32-bit floating-point number.
using f64 = double; ///< 64-bit floating-point number.
using usize = std::size_t; ///< Unsigned size type (result of sizeof).
using isize = std::ptrdiff_t; ///< Signed size type (result of pointer subtraction).
using String = std::string; ///< Owning, mutable string.
using StringView = std::string_view; ///< Non-owning view of a string.
using CStr = const char*; ///< Pointer to a null-terminated C-style string.
using Exception = std::exception; ///< Standard exception type.
/**
* @typedef Result
* @brief Alias for std::expected<Tp, Er>. Represents a value that can either be
* a success value of type Tp or an error value of type Er.
* @tparam Tp The type of the success value.
* @tparam Er The type of the error value.
*/
template <typename Tp, typename Er>
using Result = std::expected<Tp, Er>;
/**
* @typedef Err
* @brief Alias for std::unexpected<Er>. Used to construct a Result in an error state.
* @tparam Er The type of the error value.
*/
template <typename Er>
using Err = std::unexpected<Er>;
/**
* @typedef Option
* @brief Alias for std::optional<Tp>. Represents a value that may or may not be present.
* @tparam Tp The type of the potential value.
*/
template <typename Tp>
using Option = std::optional<Tp>;
/**
* @typedef Array
* @brief Alias for std::array<Tp, sz>. Represents a fixed-size array.
* @tparam Tp The element type.
* @tparam sz The size of the array.
*/
template <typename Tp, usize sz>
using Array = std::array<Tp, sz>;
/**
* @typedef Vec
* @brief Alias for std::vector<Tp>. Represents a dynamic-size array (vector).
* @tparam Tp The element type.
*/
template <typename Tp>
using Vec = std::vector<Tp>;
/**
* @typedef Pair
* @brief Alias for std::pair<T1, T2>. Represents a pair of values.
* @tparam T1 The type of the first element.
* @tparam T2 The type of the second element.
*/
template <typename T1, typename T2>
using Pair = std::pair<T1, T2>;
/**
* @typedef Map
* @brief Alias for std::map<Key, Val>. Represents an ordered map (dictionary).
* @tparam Key The key type.
* @tparam Val The value type.
*/
template <typename Key, typename Val>
using Map = std::map<Key, Val>;
/**
* @typedef SharedPointer
* @brief Alias for std::shared_ptr<Tp>. Manages shared ownership of a dynamically allocated object.
* @tparam Tp The type of the managed object.
*/
template <typename Tp>
using SharedPointer = std::shared_ptr<Tp>;
/**
* @typedef UniquePointer
* @brief Alias for std::unique_ptr<Tp, Dp>. Manages unique ownership of a dynamically allocated object.
* @tparam Tp The type of the managed object.
* @tparam Dp The deleter type (defaults to std::default_delete<Tp>).
*/
template <typename Tp, typename Dp = std::default_delete<Tp>>
using UniquePointer = std::unique_ptr<Tp, Dp>;
/**
* @struct DiskSpace
* @brief Represents disk usage information.
*
* Used as the success type for os::GetDiskUsage.
*/
struct DiskSpace {
u64 used_bytes; ///< Currently used disk space in bytes.
u64 total_bytes; ///< Total disk space in bytes.
};
/**
* @struct MediaInfo
* @brief Holds structured metadata about currently playing media.
*
* Used as the success type for os::GetNowPlaying.
* Using Option<> for fields that might not always be available.
*/
struct MediaInfo {
Option<String> title; ///< Track title.
Option<String> artist; ///< Track artist(s).
Option<String> album; ///< Album name.
Option<String> app_name; ///< Name of the media player application (e.g., "Spotify", "Firefox").
MediaInfo() = default;
MediaInfo(Option<String> title, Option<String> artist) : title(std::move(title)), artist(std::move(artist)) {}
MediaInfo(Option<String> title, Option<String> artist, Option<String> album, Option<String> app)
: title(std::move(title)), artist(std::move(artist)), album(std::move(album)), app_name(std::move(app)) {}
};
} // namespace util::types

View file

@ -1,19 +1,27 @@
#include <chrono>
#include <ftxui/dom/elements.hpp>
#include <ftxui/screen/color.hpp>
#include <ftxui/screen/screen.hpp>
#include <future>
#include <string>
#include <variant>
#include <cmath> // std::lround
#include <format> // std::format
#include <ftxui/dom/elements.hpp> // ftxui::{hbox, vbox, text, separator, filler}
#include <ftxui/dom/node.hpp> // ftxui::{Element, Render}
#include <ftxui/screen/color.hpp> // ftxui::Color
#include <ftxui/screen/screen.hpp> // ftxui::{Screen, Dimension::Full}
#include <optional> // std::optional (operator!=)
#include <ranges> // std::ranges::{to, views}
#include <string> // std::string (String)
#include <string_view> // std::string_view (StringView)
#include "config/config.h"
#include "core/system_data.h"
#include "os/os.h"
#include "src/config/weather.hpp"
#include "config/config.hpp"
#include "core/system_data.hpp"
#include "core/util/logging.hpp"
#include "os/os.hpp"
namespace ui {
using ftxui::Color;
using util::types::StringView, util::types::i32;
static constexpr inline StringView ICON_TYPE = "EMOJI";
static constexpr inline bool SHOW_ICONS = true;
static constexpr i32 MAX_PARAGRAPH_LENGTH = 30;
// Color themes
@ -47,7 +55,7 @@ namespace ui {
StringView window_manager;
};
static constexpr Icons EMPTY_ICONS = {
static constexpr Icons NONE = {
.user = "",
.palette = "",
.calendar = "",
@ -63,7 +71,7 @@ namespace ui {
.window_manager = "",
};
static constexpr Icons NERD_ICONS = {
static constexpr Icons NERD = {
.user = "",
.palette = "",
.calendar = "",
@ -78,9 +86,26 @@ namespace ui {
.desktop = " 󰇄 ",
.window_manager = "  ",
};
static constexpr Icons EMOJI = {
.user = " 👤 ",
.palette = " 🎨 ",
.calendar = " 📅 ",
.host = " 💻 ",
.kernel = " 🫀 ",
.os = " 🤖 ",
.memory = " 🧠 ",
.weather = " 🌈 ",
.music = " 🎵 ",
.disk = " 💾 ",
.shell = " 💲 ",
.desktop = " 🖥 ",
.window_manager = " 🪟 ",
};
} // namespace ui
namespace {
using namespace util::logging;
using namespace ftxui;
fn CreateColorCircles() -> Element {
@ -97,7 +122,9 @@ namespace {
const Weather weather = config.weather;
const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, deIcon, wmIcon] =
ui::SHOW_ICONS ? ui::NERD_ICONS : ui::EMPTY_ICONS;
ui::ICON_TYPE == "NERD" ? ui::NERD
: ui::ICON_TYPE == "EMOJI" ? ui::EMOJI
: ui::NONE;
Elements content;
@ -129,7 +156,7 @@ namespace {
// Weather row
if (weather.enabled && data.weather_info.has_value()) {
const WeatherOutput& weatherInfo = data.weather_info.value();
const weather::Output& weatherInfo = data.weather_info.value();
if (weather.show_town_name)
content.push_back(hbox(
@ -172,22 +199,22 @@ namespace {
if (data.host)
content.push_back(createRow(hostIcon, "Host", *data.host));
else
ERROR_LOG_LOC(data.host.error());
error_at(data.host.error());
if (data.kernel_version)
content.push_back(createRow(kernelIcon, "Kernel", *data.kernel_version));
else
ERROR_LOG_LOC(data.kernel_version.error());
error_at(data.kernel_version.error());
if (data.os_version)
content.push_back(createRow(String(osIcon), "OS", *data.os_version));
else
ERROR_LOG_LOC(data.os_version.error());
error_at(data.os_version.error());
if (data.mem_info)
content.push_back(createRow(memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.mem_info })));
else
ERROR_LOG_LOC(data.mem_info.error());
error_at(data.mem_info.error());
if (data.disk_usage)
content.push_back(createRow(
@ -196,7 +223,7 @@ namespace {
std::format("{}/{}", BytesToGiB { data.disk_usage->used_bytes }, BytesToGiB { data.disk_usage->total_bytes })
));
else
ERROR_LOG_LOC(data.disk_usage.error());
error_at(data.disk_usage.error());
if (data.shell)
content.push_back(createRow(shellIcon, "Shell", *data.shell));
@ -210,7 +237,7 @@ namespace {
content.push_back(createRow(wmIcon, "WM", *data.window_manager));
if (config.now_playing.enabled && data.now_playing) {
if (const Result<MediaInfo, NowPlayingError>& nowPlayingResult = *data.now_playing) {
if (const Result<MediaInfo, DraconisError>& nowPlayingResult = *data.now_playing) {
const MediaInfo& info = *nowPlayingResult;
const String title = info.title.value_or("Unknown Title");
@ -229,7 +256,7 @@ namespace {
}
));
} else
DEBUG_LOG_LOC(nowPlayingResult.error());
debug_at(nowPlayingResult.error());
}
return vbox(content) | borderRounded | color(Color::White);
@ -240,6 +267,8 @@ fn main() -> i32 {
const Config& config = Config::getInstance();
const SystemData data = SystemData::fetchSystemData(config);
debug_log("{}", *os::GetPackageCount());
Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }), text("") });
Screen screen = Screen::Create(Dimension::Full(), Dimension::Fit(document));

View file

@ -1,125 +1,147 @@
#ifdef __linux__
// clang-format off
#include <dbus-cxx.h> // needs to be at top for Success/None
#include <cstring>
#include <fstream>
#include <sys/socket.h>
#include <sys/statvfs.h>
#include <sys/sysinfo.h>
#include <sys/utsname.h>
#include <unistd.h>
#include <wayland-client.h>
#include <xcb/xcb.h>
#include <cstring> // std::strlen
#include <dbus-cxx/callmessage.h> // DBus::CallMessage
#include <dbus-cxx/connection.h> // DBus::Connection
#include <dbus-cxx/dispatcher.h> // DBus::Dispatcher
#include <dbus-cxx/enums.h> // DBus::{DataType, BusType}
#include <dbus-cxx/error.h> // DBus::Error
#include <dbus-cxx/messageappenditerator.h> // DBus::MessageAppendIterator
#include <dbus-cxx/signature.h> // DBus::Signature
#include <dbus-cxx/standalonedispatcher.h> // DBus::StandaloneDispatcher
#include <dbus-cxx/variant.h> // DBus::Variant
#include <expected> // std::{unexpected, expected}
#include <format> // std::{format, format_to_n}
#include <fstream> // std::ifstream
#include <climits> // PATH_MAX
#include <limits> // std::numeric_limits
#include <map> // std::map (Map)
#include <memory> // std::shared_ptr (SharedPointer)
#include <string> // std::{getline, string (String)}
#include <string_view> // std::string_view (StringView)
#include <sys/socket.h> // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED
#include <sys/statvfs.h> // statvfs
#include <sys/sysinfo.h> // sysinfo
#include <sys/utsname.h> // utsname, uname
#include <unistd.h> // readlink
#include <utility> // std::move
#include "os.h"
#include "src/os/linux/display_guards.h"
#include "src/util/macros.h"
#include "src/util/types.h"
#include "src/core/util/logging.hpp"
#include "src/core/util/helpers.hpp"
#include "src/wrappers/wayland.hpp"
#include "src/wrappers/xcb.hpp"
#include "os.hpp"
#include "linux/pkg_count.hpp"
// clang-format on
using namespace std::string_view_literals;
using namespace util::types;
using util::error::DraconisError, util::error::DraconisErrorCode;
namespace {
fn GetX11WindowManager() -> Result<String, OsError> {
using os::linux::XcbReplyGuard;
using os::linux::XorgDisplayGuard;
fn GetX11WindowManager() -> Result<String, DraconisError> {
using namespace xcb;
const XorgDisplayGuard conn;
const DisplayGuard conn;
if (!conn)
if (const i32 err = xcb_connection_has_error(conn.get()); !conn || err != 0)
return Err(OsError(OsErrorCode::ApiUnavailable, [&] -> String {
switch (err) {
case 0: return "Connection object invalid, but no specific XCB error code";
case XCB_CONN_ERROR: return "Stream/Socket/Pipe Error";
case XCB_CONN_CLOSED_EXT_NOTSUPPORTED: return "Closed: Extension Not Supported";
case XCB_CONN_CLOSED_MEM_INSUFFICIENT: return "Closed: Insufficient Memory";
case XCB_CONN_CLOSED_REQ_LEN_EXCEED: return "Closed: Request Length Exceeded";
case XCB_CONN_CLOSED_PARSE_ERR: return "Closed: Display String Parse Error";
case XCB_CONN_CLOSED_INVALID_SCREEN: return "Closed: Invalid Screen";
case XCB_CONN_CLOSED_FDPASSING_FAILED: return "Closed: FD Passing Failed";
if (const i32 err = connection_has_error(conn.get()))
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, [&] -> String {
if (const Option<ConnError> connErr = getConnError(err)) {
switch (*connErr) {
case Generic: return "Stream/Socket/Pipe Error";
case ExtNotSupported: return "Extension Not Supported";
case MemInsufficient: return "Insufficient Memory";
case ReqLenExceed: return "Request Length Exceeded";
case ParseErr: return "Display String Parse Error";
case InvalidScreen: return "Invalid Screen";
case FdPassingFailed: return "FD Passing Failed";
default: return std::format("Unknown Error Code ({})", err);
}
}
return std::format("Unknown Error Code ({})", err);
}()));
fn internAtom = [&conn](const StringView name) -> Result<xcb_atom_t, OsError> {
const XcbReplyGuard<xcb_intern_atom_reply_t> reply(xcb_intern_atom_reply(
conn.get(), xcb_intern_atom(conn.get(), 0, static_cast<uint16_t>(name.size()), name.data()), nullptr
));
fn internAtom = [&conn](const StringView name) -> Result<atom_t, DraconisError> {
const ReplyGuard<intern_atom_reply_t> reply(
intern_atom_reply(conn.get(), intern_atom(conn.get(), 0, static_cast<u16>(name.size()), name.data()), nullptr)
);
if (!reply)
return Err(OsError(OsErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name)));
return Err(
DraconisError(DraconisErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name))
);
return reply->atom;
};
const Result<xcb_atom_t, OsError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
const Result<xcb_atom_t, OsError> wmNameAtom = internAtom("_NET_WM_NAME");
const Result<xcb_atom_t, OsError> utf8StringAtom = internAtom("UTF8_STRING");
const Result<atom_t, DraconisError> supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK");
const Result<atom_t, DraconisError> wmNameAtom = internAtom("_NET_WM_NAME");
const Result<atom_t, DraconisError> utf8StringAtom = internAtom("UTF8_STRING");
if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) {
if (!supportingWmCheckAtom)
ERROR_LOG("Failed to get _NET_SUPPORTING_WM_CHECK atom");
error_log("Failed to get _NET_SUPPORTING_WM_CHECK atom");
if (!wmNameAtom)
ERROR_LOG("Failed to get _NET_WM_NAME atom");
error_log("Failed to get _NET_WM_NAME atom");
if (!utf8StringAtom)
ERROR_LOG("Failed to get UTF8_STRING atom");
error_log("Failed to get UTF8_STRING atom");
return Err(OsError(OsErrorCode::PlatformSpecific, "Failed to get X11 atoms"));
return Err(DraconisError(DraconisErrorCode::PlatformSpecific, "Failed to get X11 atoms"));
}
const XcbReplyGuard<xcb_get_property_reply_t> wmWindowReply(xcb_get_property_reply(
const ReplyGuard<get_property_reply_t> wmWindowReply(get_property_reply(
conn.get(),
xcb_get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, XCB_ATOM_WINDOW, 0, 1),
get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1),
nullptr
));
if (!wmWindowReply || wmWindowReply->type != XCB_ATOM_WINDOW || wmWindowReply->format != 32 ||
xcb_get_property_value_length(wmWindowReply.get()) == 0)
return Err(OsError(OsErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property"));
if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 ||
get_property_value_length(wmWindowReply.get()) == 0)
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property"));
const xcb_window_t wmRootWindow = *static_cast<xcb_window_t*>(xcb_get_property_value(wmWindowReply.get()));
const window_t wmRootWindow = *static_cast<window_t*>(get_property_value(wmWindowReply.get()));
const XcbReplyGuard<xcb_get_property_reply_t> wmNameReply(xcb_get_property_reply(
conn.get(), xcb_get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr
const ReplyGuard<get_property_reply_t> wmNameReply(get_property_reply(
conn.get(), get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr
));
if (!wmNameReply || wmNameReply->type != *utf8StringAtom || xcb_get_property_value_length(wmNameReply.get()) == 0)
return Err(OsError(OsErrorCode::NotFound, "Failed to get _NET_WM_NAME property"));
if (!wmNameReply || wmNameReply->type != *utf8StringAtom || get_property_value_length(wmNameReply.get()) == 0)
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get _NET_WM_NAME property"));
const char* nameData = static_cast<const char*>(xcb_get_property_value(wmNameReply.get()));
const usize length = xcb_get_property_value_length(wmNameReply.get());
const char* nameData = static_cast<const char*>(get_property_value(wmNameReply.get()));
const usize length = get_property_value_length(wmNameReply.get());
return String(nameData, length);
}
fn GetWaylandCompositor() -> Result<String, OsError> {
using os::linux::WaylandDisplayGuard;
const WaylandDisplayGuard display;
fn GetWaylandCompositor() -> Result<String, DraconisError> {
const wl::DisplayGuard display;
if (!display)
return Err(OsError(OsErrorCode::NotFound, "Failed to connect to display (is Wayland running?)"));
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to connect to display (is Wayland running?)"));
const i32 fileDescriptor = display.fd();
if (fileDescriptor < 0)
return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor"));
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor"));
ucred cred;
socklen_t len = sizeof(cred);
if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1)
return Err(OsError::withErrno("Failed to get socket credentials (SO_PEERCRED)"));
return Err(DraconisError::withErrno("Failed to get socket credentials (SO_PEERCRED)"));
Array<char, 128> exeLinkPathBuf;
auto [out, size] = std::format_to_n(exeLinkPathBuf.data(), exeLinkPathBuf.size() - 1, "/proc/{}/exe", cred.pid);
if (out >= exeLinkPathBuf.data() + exeLinkPathBuf.size() - 1)
return Err(OsError(OsErrorCode::InternalError, "Failed to format /proc path (PID too large?)"));
return Err(DraconisError(DraconisErrorCode::InternalError, "Failed to format /proc path (PID too large?)"));
*out = '\0';
@ -130,7 +152,7 @@ namespace {
const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1);
if (bytesRead == -1)
return Err(OsError::withErrno(std::format("Failed to read link '{}'", exeLinkPath)));
return Err(DraconisError::withErrno(std::format("Failed to read link '{}'", exeLinkPath)));
exeRealPathBuf.at(bytesRead) = '\0';
@ -153,7 +175,7 @@ namespace {
compositorNameView = filenameView;
if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/")
return Err(OsError(OsErrorCode::NotFound, "Failed to get compositor name from path"));
return Err(DraconisError(DraconisErrorCode::NotFound, "Failed to get compositor name from path"));
if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() &&
compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) {
@ -161,7 +183,7 @@ namespace {
compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length());
if (cleanedView.empty())
return Err(OsError(OsErrorCode::NotFound, "Compositor name invalid after heuristic"));
return Err(DraconisError(DraconisErrorCode::NotFound, "Compositor name invalid after heuristic"));
return String(cleanedView);
}
@ -169,7 +191,9 @@ namespace {
return String(compositorNameView);
}
fn GetMprisPlayers(const SharedPointer<DBus::Connection>& connection) -> Result<String, OsError> {
fn GetMprisPlayers(const SharedPointer<DBus::Connection>& connection) -> Result<String, DraconisError> {
using namespace std::string_view_literals;
try {
const SharedPointer<DBus::CallMessage> call =
DBus::CallMessage::create("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
@ -177,7 +201,7 @@ namespace {
const SharedPointer<DBus::Message> reply = connection->send_with_reply_blocking(call, 5);
if (!reply || !reply->is_valid())
return Err(OsError(OsErrorCode::Timeout, "Failed to get reply from ListNames"));
return Err(DraconisError(DraconisErrorCode::Timeout, "Failed to get reply from ListNames"));
Vec<String> allNamesStd;
DBus::MessageIterator reader(*reply);
@ -187,14 +211,14 @@ namespace {
if (StringView(name).contains("org.mpris.MediaPlayer2"sv))
return name;
return Err(OsError(OsErrorCode::NotFound, "No MPRIS players found"));
} catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) {
return Err(OsError(OsErrorCode::InternalError, e.what()));
return Err(DraconisError(DraconisErrorCode::NotFound, "No MPRIS players found"));
} catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) {
return Err(DraconisError(DraconisErrorCode::InternalError, e.what()));
}
}
fn GetMediaPlayerMetadata(const SharedPointer<DBus::Connection>& connection, const String& playerBusName)
-> Result<MediaInfo, OsError> {
-> Result<MediaInfo, DraconisError> {
try {
const SharedPointer<DBus::CallMessage> metadataCall =
DBus::CallMessage::create(playerBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
@ -204,7 +228,9 @@ namespace {
const SharedPointer<DBus::Message> metadataReply = connection->send_with_reply_blocking(metadataCall, 1000);
if (!metadataReply || !metadataReply->is_valid()) {
return Err(OsError(OsErrorCode::Timeout, "DBus Get Metadata call timed out or received invalid reply"));
return Err(
DraconisError(DraconisErrorCode::Timeout, "DBus Get Metadata call timed out or received invalid reply")
);
}
DBus::MessageIterator iter(*metadataReply);
@ -213,8 +239,8 @@ namespace {
// MPRIS metadata is variant containing a dict a{sv}
if (metadataVariant.type() != DBus::DataType::DICT_ENTRY && metadataVariant.type() != DBus::DataType::ARRAY) {
return Err(OsError(
OsErrorCode::ParseError,
return Err(DraconisError(
DraconisErrorCode::ParseError,
std::format(
"Inner metadata variant is not the expected type, expected dict/a{{sv}} but got '{}'",
metadataVariant.signature().str()
@ -226,14 +252,12 @@ namespace {
Option<String> title = None;
Option<String> artist = None;
Option<String> album = None;
Option<String> appName = None; // Try to get app name too
if (auto titleIter = metadata.find("xesam:title");
if (const auto titleIter = metadata.find("xesam:title");
titleIter != metadata.end() && titleIter->second.type() == DBus::DataType::STRING)
title = titleIter->second.to_string();
if (auto artistIter = metadata.find("xesam:artist"); artistIter != metadata.end()) {
if (const auto artistIter = metadata.find("xesam:artist"); artistIter != metadata.end()) {
if (artistIter->second.type() == DBus::DataType::ARRAY) {
if (Vec<String> artists = artistIter->second.to_vector<String>(); !artists.empty())
artist = artists[0];
@ -242,47 +266,27 @@ namespace {
}
}
if (auto albumIter = metadata.find("xesam:album");
albumIter != metadata.end() && albumIter->second.type() == DBus::DataType::STRING)
album = albumIter->second.to_string();
try {
const SharedPointer<DBus::CallMessage> identityCall =
DBus::CallMessage::create(playerBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
*identityCall << "org.mpris.MediaPlayer2" << "Identity";
if (const SharedPointer<DBus::Message> identityReply = connection->send_with_reply_blocking(identityCall, 500);
identityReply && identityReply->is_valid()) {
DBus::MessageIterator identityIter(*identityReply);
DBus::Variant identityVariant;
identityIter >> identityVariant;
if (identityVariant.type() == DBus::DataType::STRING)
appName = identityVariant.to_string();
}
} catch (const DBus::Error& e) {
DEBUG_LOG("Failed to get player Identity property for {}: {}", playerBusName, e.what()); // Non-fatal
}
return MediaInfo(std::move(title), std::move(artist), std::move(album), std::move(appName));
} catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) {
return Err(
OsError(OsErrorCode::InternalError, std::format("Standard exception processing metadata: {}", e.what()))
);
return MediaInfo(std::move(title), std::move(artist));
} catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) {
return Err(DraconisError(
DraconisErrorCode::InternalError, std::format("Standard exception processing metadata: {}", e.what())
));
}
}
} // namespace
fn os::GetOSVersion() -> Result<String, OsError> {
fn os::GetOSVersion() -> Result<String, DraconisError> {
constexpr CStr path = "/etc/os-release";
std::ifstream file(path);
if (!file)
return Err(OsError(OsErrorCode::NotFound, std::format("Failed to open {}", path)));
return Err(DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open {}", path)));
String line;
constexpr StringView prefix = "PRETTY_NAME=";
while (getline(file, line)) {
while (std::getline(file, line)) {
if (StringView(line).starts_with(prefix)) {
String value = line.substr(prefix.size());
@ -291,36 +295,36 @@ fn os::GetOSVersion() -> Result<String, OsError> {
value = value.substr(1, value.length() - 2);
if (value.empty())
return Err(
OsError(OsErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path))
);
return Err(DraconisError(
DraconisErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path)
));
return value;
}
}
return Err(OsError(OsErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path)));
return Err(DraconisError(DraconisErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path)));
}
fn os::GetMemInfo() -> Result<u64, OsError> {
fn os::GetMemInfo() -> Result<u64, DraconisError> {
struct sysinfo info;
if (sysinfo(&info) != 0)
return Err(OsError::fromDBus("sysinfo call failed"));
return Err(DraconisError::fromDBus("sysinfo call failed"));
const u64 totalRam = info.totalram;
const u64 memUnit = info.mem_unit;
if (memUnit == 0)
return Err(OsError(OsErrorCode::InternalError, "sysinfo returned mem_unit of zero"));
return Err(DraconisError(DraconisErrorCode::InternalError, "sysinfo returned mem_unit of zero"));
if (totalRam > std::numeric_limits<u64>::max() / memUnit)
return Err(OsError(OsErrorCode::InternalError, "Potential overflow calculating total RAM"));
return Err(DraconisError(DraconisErrorCode::InternalError, "Potential overflow calculating total RAM"));
return info.totalram * info.mem_unit;
}
fn os::GetNowPlaying() -> Result<MediaInfo, NowPlayingError> {
fn os::GetNowPlaying() -> Result<MediaInfo, DraconisError> {
// Dispatcher must outlive the try-block because 'connection' depends on it later.
// ReSharper disable once CppTooWideScope, CppJoinDeclarationAndAssignment
SharedPointer<DBus::Dispatcher> dispatcher;
@ -330,22 +334,22 @@ fn os::GetNowPlaying() -> Result<MediaInfo, NowPlayingError> {
dispatcher = DBus::StandaloneDispatcher::create();
if (!dispatcher)
return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to create DBus dispatcher"));
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to create DBus dispatcher"));
connection = dispatcher->create_connection(DBus::BusType::SESSION);
if (!connection)
return Err(OsError(OsErrorCode::ApiUnavailable, "Failed to connect to DBus session bus"));
} catch (const DBus::Error& e) { return Err(OsError::fromDBus(e)); } catch (const Exception& e) {
return Err(OsError(OsErrorCode::InternalError, e.what()));
return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to connect to DBus session bus"));
} catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) {
return Err(DraconisError(DraconisErrorCode::InternalError, e.what()));
}
Result<String, NowPlayingError> playerBusName = GetMprisPlayers(connection);
Result<String, DraconisError> playerBusName = GetMprisPlayers(connection);
if (!playerBusName)
return Err(playerBusName.error());
Result<MediaInfo, OsError> metadataResult = GetMediaPlayerMetadata(connection, *playerBusName);
Result<MediaInfo, DraconisError> metadataResult = GetMediaPlayerMetadata(connection, *playerBusName);
if (!metadataResult)
return Err(metadataResult.error());
@ -354,37 +358,39 @@ fn os::GetNowPlaying() -> Result<MediaInfo, NowPlayingError> {
}
fn os::GetWindowManager() -> Option<String> {
if (Result<String, OsError> waylandResult = GetWaylandCompositor())
if (Result<String, DraconisError> waylandResult = GetWaylandCompositor())
return *waylandResult;
else
DEBUG_LOG("Could not detect Wayland compositor: {}", waylandResult.error().message);
debug_log("Could not detect Wayland compositor: {}", waylandResult.error().message);
if (Result<String, OsError> x11Result = GetX11WindowManager())
if (Result<String, DraconisError> x11Result = GetX11WindowManager())
return *x11Result;
else
DEBUG_LOG("Could not detect X11 window manager: {}", x11Result.error().message);
debug_log("Could not detect X11 window manager: {}", x11Result.error().message);
return None;
}
fn os::GetDesktopEnvironment() -> Option<String> {
return GetEnv("XDG_CURRENT_DESKTOP")
return util::helpers::GetEnv("XDG_CURRENT_DESKTOP")
.transform([](const String& xdgDesktop) -> String {
if (const usize colon = xdgDesktop.find(':'); colon != String::npos)
return xdgDesktop.substr(0, colon);
return xdgDesktop;
})
.or_else([](const EnvError&) -> Result<String, EnvError> { return GetEnv("DESKTOP_SESSION"); })
.or_else([](const DraconisError&) -> Result<String, DraconisError> {
return util::helpers::GetEnv("DESKTOP_SESSION");
})
.transform([](const String& finalValue) -> Option<String> {
DEBUG_LOG("Found desktop environment: {}", finalValue);
debug_log("Found desktop environment: {}", finalValue);
return finalValue;
})
.value_or(None);
}
fn os::GetShell() -> Option<String> {
if (const Result<String, EnvError> shellPath = GetEnv("SHELL")) {
if (const Result<String, DraconisError> shellPath = util::helpers::GetEnv("SHELL")) {
// clang-format off
constexpr Array<Pair<StringView, StringView>, 5> shellMap {{
{ "bash", "Bash" },
@ -405,27 +411,32 @@ fn os::GetShell() -> Option<String> {
return None;
}
fn os::GetHost() -> Result<String, OsError> {
fn os::GetHost() -> Result<String, DraconisError> {
constexpr CStr primaryPath = "/sys/class/dmi/id/product_family";
constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name";
fn readFirstLine = [&](const String& path) -> Result<String, OsError> {
fn readFirstLine = [&](const String& path) -> Result<String, DraconisError> {
std::ifstream file(path);
String line;
if (!file)
return Err(OsError(OsErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path)));
return Err(
DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path))
);
if (!getline(file, line))
return Err(OsError(OsErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)));
if (!std::getline(file, line))
return Err(
DraconisError(DraconisErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path))
);
return line;
};
return readFirstLine(primaryPath).or_else([&](const OsError& primaryError) -> Result<String, OsError> {
return readFirstLine(fallbackPath).or_else([&](const OsError& fallbackError) -> Result<String, OsError> {
return Err(OsError(
OsErrorCode::InternalError,
return readFirstLine(primaryPath).or_else([&](const DraconisError& primaryError) -> Result<String, DraconisError> {
return readFirstLine(fallbackPath)
.or_else([&](const DraconisError& fallbackError) -> Result<String, DraconisError> {
return Err(DraconisError(
DraconisErrorCode::InternalError,
std::format(
"Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}",
primaryPath,
@ -438,23 +449,23 @@ fn os::GetHost() -> Result<String, OsError> {
});
}
fn os::GetKernelVersion() -> Result<String, OsError> {
fn os::GetKernelVersion() -> Result<String, DraconisError> {
utsname uts;
if (uname(&uts) == -1)
return Err(OsError::withErrno("uname call failed"));
return Err(DraconisError::withErrno("uname call failed"));
if (strlen(uts.release) == 0)
return Err(OsError(OsErrorCode::ParseError, "uname returned null kernel release"));
if (std::strlen(uts.release) == 0)
return Err(DraconisError(DraconisErrorCode::ParseError, "uname returned null kernel release"));
return uts.release;
}
fn os::GetDiskUsage() -> Result<DiskSpace, OsError> {
fn os::GetDiskUsage() -> Result<DiskSpace, DraconisError> {
struct statvfs stat;
if (statvfs("/", &stat) == -1)
return Err(OsError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)")));
return Err(DraconisError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)")));
return DiskSpace {
.used_bytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize),
@ -462,4 +473,6 @@ fn os::GetDiskUsage() -> Result<DiskSpace, OsError> {
};
}
fn os::GetPackageCount() -> Result<u64, DraconisError> { return linux::GetNixPackageCount(); }
#endif // __linux__

View file

@ -1,70 +0,0 @@
#ifdef __linux__
#include <utility>
#include "display_guards.h"
#include "src/util/macros.h"
namespace os::linux {
XorgDisplayGuard::XorgDisplayGuard(const CStr name) : m_Connection(xcb_connect(name, nullptr)) {}
XorgDisplayGuard::~XorgDisplayGuard() {
if (m_Connection)
xcb_disconnect(m_Connection);
}
XorgDisplayGuard::XorgDisplayGuard(XorgDisplayGuard&& other) noexcept
: m_Connection(std::exchange(other.m_Connection, nullptr)) {}
fn XorgDisplayGuard::operator=(XorgDisplayGuard&& other) noexcept -> XorgDisplayGuard& {
if (this != &other) {
if (m_Connection)
xcb_disconnect(m_Connection);
m_Connection = std::exchange(other.m_Connection, nullptr);
}
return *this;
}
XorgDisplayGuard::operator bool() const { return m_Connection && !xcb_connection_has_error(m_Connection); }
fn XorgDisplayGuard::get() const -> xcb_connection_t* { return m_Connection; }
fn XorgDisplayGuard::setup() const -> const xcb_setup_t* {
return m_Connection ? xcb_get_setup(m_Connection) : nullptr;
}
fn XorgDisplayGuard::rootScreen() const -> xcb_screen_t* {
const xcb_setup_t* setup = this->setup();
return setup ? xcb_setup_roots_iterator(setup).data : nullptr;
}
WaylandDisplayGuard::WaylandDisplayGuard() : m_Display(wl_display_connect(nullptr)) {}
WaylandDisplayGuard::~WaylandDisplayGuard() {
if (m_Display)
wl_display_disconnect(m_Display);
}
WaylandDisplayGuard::WaylandDisplayGuard(WaylandDisplayGuard&& other) noexcept
: m_Display(std::exchange(other.m_Display, nullptr)) {}
fn WaylandDisplayGuard::operator=(WaylandDisplayGuard&& other) noexcept -> WaylandDisplayGuard& {
if (this != &other) {
if (m_Display)
wl_display_disconnect(m_Display);
m_Display = std::exchange(other.m_Display, nullptr);
}
return *this;
}
WaylandDisplayGuard::operator bool() const { return m_Display != nullptr; }
fn WaylandDisplayGuard::get() const -> wl_display* { return m_Display; }
fn WaylandDisplayGuard::fd() const -> i32 { return m_Display ? wl_display_get_fd(m_Display) : -1; }
}
#endif

View file

@ -1,110 +0,0 @@
#pragma once
#ifdef __linux__
#include <wayland-client.h>
#include <xcb/xcb.h>
#include "src/util/macros.h"
namespace os::linux {
/**
* RAII wrapper for X11 Display connections
* Automatically handles resource acquisition and cleanup
*/
class XorgDisplayGuard {
xcb_connection_t* m_Connection = nullptr;
public:
/**
* Opens an XCB connection
* @param name Display name (nullptr for default)
*/
explicit XorgDisplayGuard(CStr name = nullptr);
~XorgDisplayGuard();
// Non-copyable
XorgDisplayGuard(const XorgDisplayGuard&) = delete;
fn operator=(const XorgDisplayGuard&)->XorgDisplayGuard& = delete;
// Movable
XorgDisplayGuard(XorgDisplayGuard&& other) noexcept;
fn operator=(XorgDisplayGuard&& other) noexcept -> XorgDisplayGuard&;
[[nodiscard]] explicit operator bool() const;
[[nodiscard]] fn get() const -> xcb_connection_t*;
[[nodiscard]] fn setup() const -> const xcb_setup_t*;
[[nodiscard]] fn rootScreen() const -> xcb_screen_t*;
};
/**
* RAII wrapper for XCB replies
* Handles automatic cleanup of various XCB reply objects
*/
template <typename T>
class XcbReplyGuard {
T* m_Reply = nullptr;
public:
XcbReplyGuard() = default;
explicit XcbReplyGuard(T* reply) : m_Reply(reply) {}
~XcbReplyGuard() {
if (m_Reply)
free(m_Reply);
}
// Non-copyable
XcbReplyGuard(const XcbReplyGuard&) = delete;
fn operator=(const XcbReplyGuard&)->XcbReplyGuard& = delete;
// Movable
XcbReplyGuard(XcbReplyGuard&& other) noexcept : m_Reply(std::exchange(other.m_Reply, nullptr)) {}
fn operator=(XcbReplyGuard&& other) noexcept -> XcbReplyGuard& {
if (this != &other) {
if (m_Reply)
free(m_Reply);
m_Reply = std::exchange(other.m_Reply, nullptr);
}
return *this;
}
[[nodiscard]] explicit operator bool() const { return m_Reply != nullptr; }
[[nodiscard]] fn get() const -> T* { return m_Reply; }
[[nodiscard]] fn operator->() const->T* { return m_Reply; }
[[nodiscard]] fn operator*() const->T& { return *m_Reply; }
};
/**
* RAII wrapper for Wayland display connections
* Automatically handles resource acquisition and cleanup
*/
class WaylandDisplayGuard {
wl_display* m_Display;
public:
/**
* Opens a Wayland display connection
*/
WaylandDisplayGuard();
~WaylandDisplayGuard();
// Non-copyable
WaylandDisplayGuard(const WaylandDisplayGuard&) = delete;
fn operator=(const WaylandDisplayGuard&)->WaylandDisplayGuard& = delete;
// Movable
WaylandDisplayGuard(WaylandDisplayGuard&& other) noexcept;
fn operator=(WaylandDisplayGuard&& other) noexcept -> WaylandDisplayGuard&;
[[nodiscard]] explicit operator bool() const;
[[nodiscard]] fn get() const -> wl_display*;
[[nodiscard]] fn fd() const -> i32;
};
}
#endif

View file

@ -1,3 +1,4 @@
#include "src/util/macros.h"
#include "src/core/util/defs.hpp"
#include "src/core/util/types.hpp"
extern "C" fn issetugid() -> usize { return 0; } // NOLINT
extern "C" fn issetugid() -> util::types::usize { return 0; } // NOLINT

View file

@ -1 +1,217 @@
#include "src/os/linux/pkg_count.h"
#include "src/os/linux/pkg_count.hpp"
#include <SQLiteCpp/SQLiteCpp.h>
#include <fstream>
#include <glaze/core/common.hpp>
#include <glaze/core/read.hpp>
#include <glaze/core/reflect.hpp>
#include <glaze/json/write.hpp>
#include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp"
using util::error::DraconisError, util::error::DraconisErrorCode;
using util::types::u64, util::types::i64, util::types::Result, util::types::Err, util::types::String,
util::types::Exception;
namespace {
namespace fs = std::filesystem;
using namespace std::chrono;
struct NixPkgCacheData {
u64 count {};
system_clock::time_point timestamp;
// NOLINTBEGIN(readability-identifier-naming) - Needs to specifically use `glaze`
struct [[maybe_unused]] glaze {
using T = NixPkgCacheData;
static constexpr auto value = glz::object("count", &T::count, "timestamp", [](auto& self) -> auto& {
thread_local auto epoch_seconds = duration_cast<seconds>(self.timestamp.time_since_epoch()).count();
return epoch_seconds;
});
};
// NOLINTEND(readability-identifier-naming)
};
fn GetPkgCountCachePath() -> Result<fs::path, DraconisError> {
std::error_code errc;
const fs::path cacheDir = fs::temp_directory_path(errc);
if (errc) {
return Err(DraconisError(DraconisErrorCode::IoError, "Failed to get temp directory: " + errc.message()));
}
return cacheDir / "nix_pkg_count_cache.json";
}
fn ReadPkgCountCache() -> Result<NixPkgCacheData, DraconisError> {
auto cachePathResult = GetPkgCountCachePath();
if (!cachePathResult) {
return Err(cachePathResult.error());
}
const fs::path& cachePath = *cachePathResult;
if (!fs::exists(cachePath)) {
return Err(DraconisError(DraconisErrorCode::NotFound, "Cache file not found: " + cachePath.string()));
}
std::ifstream ifs(cachePath, std::ios::binary);
if (!ifs.is_open()) {
return Err(
DraconisError(DraconisErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string())
);
}
debug_log("Reading Nix package count from cache file: {}", cachePath.string());
try {
const String content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
NixPkgCacheData result;
glz::context ctx {};
if (auto glazeResult = glz::read<glz::opts { .error_on_unknown_keys = false }>(result, content, ctx);
glazeResult.ec != glz::error_code::none) {
return Err(DraconisError(
DraconisErrorCode::ParseError,
std::format("JSON parse error reading cache: {}", glz::format_error(glazeResult, content))
));
}
if (size_t tsPos = content.find("\"timestamp\""); tsPos != String::npos) {
size_t colonPos = content.find(':', tsPos);
if (size_t valueStart = content.find_first_of("0123456789", colonPos); valueStart != String::npos) {
long long timestampSeconds = 0;
char* endPtr = nullptr;
timestampSeconds = std::strtoll(content.c_str() + valueStart, &endPtr, 10);
result.timestamp = system_clock::time_point(seconds(timestampSeconds));
} else {
return Err(DraconisError(DraconisErrorCode::ParseError, "Could not parse timestamp value from cache JSON."));
}
} else {
return Err(DraconisError(DraconisErrorCode::ParseError, "Timestamp field not found in cache JSON."));
}
debug_log("Successfully read package count from cache file.");
return result;
} catch (const Exception& e) {
return Err(
DraconisError(DraconisErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what()))
);
}
}
fn WritePkgCountCache(const NixPkgCacheData& data) -> Result<void, DraconisError> {
auto cachePathResult = GetPkgCountCachePath();
if (!cachePathResult) {
return Err(cachePathResult.error());
}
const fs::path& cachePath = *cachePathResult;
fs::path tempPath = cachePath;
tempPath += ".tmp";
debug_log("Writing Nix package count to cache file: {}", cachePath.string());
try {
{
std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) {
return Err(DraconisError(DraconisErrorCode::IoError, "Failed to open temp cache file: " + tempPath.string()));
}
String jsonStr;
NixPkgCacheData mutableData = data;
if (auto glazeErr = glz::write_json(mutableData, jsonStr); glazeErr.ec != glz::error_code::none) {
return Err(DraconisError(
DraconisErrorCode::ParseError,
std::format("JSON serialization error writing cache: {}", glz::format_error(glazeErr, jsonStr))
));
}
ofs << jsonStr;
if (!ofs) {
return Err(DraconisError(DraconisErrorCode::IoError, "Failed to write to temp cache file"));
}
}
std::error_code errc;
fs::rename(tempPath, cachePath, errc);
if (errc) {
fs::remove(tempPath);
return Err(DraconisError(
DraconisErrorCode::IoError,
std::format("Failed to replace cache file '{}': {}", cachePath.string(), errc.message())
));
}
debug_log("Successfully wrote package count to cache file.");
return {};
} catch (const Exception& e) {
fs::remove(tempPath);
return Err(DraconisError(
DraconisErrorCode::InternalError, std::format("File operation error writing package count cache: {}", e.what())
));
}
}
} // namespace
fn os::linux::GetNixPackageCount() -> Result<u64, DraconisError> {
const fs::path nixDbPath = "/nix/var/nix/db/db.sqlite";
if (Result<NixPkgCacheData, DraconisError> cachedDataResult = ReadPkgCountCache()) {
const auto& [count, timestamp] = *cachedDataResult;
std::error_code errc;
std::filesystem::file_time_type dbModTime = fs::last_write_time(nixDbPath, errc);
if (errc) {
warn_log("Could not get modification time for '{}': {}. Invalidating cache.", nixDbPath.string(), errc.message());
} else {
if (timestamp.time_since_epoch() >= dbModTime.time_since_epoch()) {
debug_log(
"Using valid Nix package count cache (DB file unchanged since {}). Count: {}",
std::format("{:%F %T %Z}", floor<seconds>(timestamp)),
count
);
return count;
}
debug_log("Nix package count cache stale (DB file modified).");
}
} else {
if (cachedDataResult.error().code != DraconisErrorCode::NotFound)
debug_at(cachedDataResult.error());
debug_log("Nix package count cache not found or unreadable.");
}
debug_log("Fetching fresh Nix package count from database: {}", nixDbPath.string());
u64 count = 0;
try {
const SQLite::Database database("/nix/var/nix/db/db.sqlite", SQLite::OPEN_READONLY);
if (SQLite::Statement query(database, "SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL");
query.executeStep()) {
const i64 countInt64 = query.getColumn(0).getInt64();
if (countInt64 < 0)
return Err(DraconisError(DraconisErrorCode::ParseError, "Negative count returned by Nix DB COUNT(*) query."));
count = static_cast<u64>(countInt64);
} else {
return Err(DraconisError(DraconisErrorCode::ParseError, "No rows returned by Nix DB COUNT(*) query."));
}
} catch (const SQLite::Exception& e) {
return Err(DraconisError(
DraconisErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing Nix DB: {}", e.what())
));
} catch (const Exception& e) { return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); } catch (...) {
return Err(DraconisError(DraconisErrorCode::Other, "Unknown error occurred accessing Nix DB"));
}
const NixPkgCacheData dataToCache = { .count = count, .timestamp = system_clock::now() };
if (Result<void, DraconisError> writeResult = WritePkgCountCache(dataToCache); !writeResult) {
warn_at(writeResult.error());
warn_log("Failed to write Nix package count to cache.");
}
debug_log("Fetched fresh Nix package count: {}", count);
return count;
}

View file

@ -1,36 +0,0 @@
#pragma once
#include "src/util/macros.h"
// Get package count from dpkg (Debian/Ubuntu)
fn GetDpkgPackageCount() -> Option<usize>;
// Get package count from RPM (Red Hat/Fedora/CentOS)
fn GetRpmPackageCount() -> Option<usize>;
// Get package count from pacman (Arch Linux)
fn GetPacmanPackageCount() -> Option<usize>;
// Get package count from Portage (Gentoo)
fn GetPortagePackageCount() -> Option<usize>;
// Get package count from zypper (openSUSE)
fn GetZypperPackageCount() -> Option<usize>;
// Get package count from apk (Alpine)
fn GetApkPackageCount() -> Option<usize>;
// Get package count from nix
fn GetNixPackageCount() -> Option<usize>;
// Get package count from flatpak
fn GetFlatpakPackageCount() -> Option<usize>;
// Get package count from snap
fn GetSnapPackageCount() -> Option<usize>;
// Get package count from AppImage
fn GetAppimagePackageCount() -> Option<usize>;
// Get total package count from all available package managers
fn GetTotalPackageCount() -> Option<usize>;

View file

@ -0,0 +1,43 @@
#pragma once
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/types.hpp"
namespace os::linux {
using util::error::DraconisError;
using util::types::Result, util::types::u64;
// Get package count from dpkg (Debian/Ubuntu)
fn GetDpkgPackageCount() -> Result<u64, DraconisError>;
// Get package count from RPM (Red Hat/Fedora/CentOS)
fn GetRpmPackageCount() -> Result<u64, DraconisError>;
// Get package count from pacman (Arch Linux)
fn GetPacmanPackageCount() -> Result<u64, DraconisError>;
// Get package count from Portage (Gentoo)
fn GetPortagePackageCount() -> Result<u64, DraconisError>;
// Get package count from zypper (openSUSE)
fn GetZypperPackageCount() -> Result<u64, DraconisError>;
// Get package count from apk (Alpine)
fn GetApkPackageCount() -> Result<u64, DraconisError>;
// Get package count from nix
fn GetNixPackageCount() -> Result<u64, DraconisError>;
// Get package count from flatpak
fn GetFlatpakPackageCount() -> Result<u64, DraconisError>;
// Get package count from snap
fn GetSnapPackageCount() -> Result<u64, DraconisError>;
// Get package count from AppImage
fn GetAppimagePackageCount() -> Result<u64, DraconisError>;
// Get total package count from all available package managers
fn GetTotalPackageCount() -> Result<u64, DraconisError>;
} // namespace os::linux

View file

@ -1,7 +1,8 @@
#pragma once
#include "../util/macros.h"
#include "../util/types.h"
#include "src/core/util/defs.hpp"
#include "src/core/util/error.hpp"
#include "src/core/util/types.hpp"
/**
* @namespace os
@ -13,33 +14,35 @@
* (found in linux.cpp, windows.cpp, macos.cpp).
*/
namespace os {
using util::error::DraconisError;
using util::types::u64, util::types::String, util::types::Option, util::types::Result, util::types::MediaInfo,
util::types::DiskSpace;
/**
* @brief Get the total amount of physical RAM installed in the system.
* @return A Result containing the total RAM in bytes (u64) on success,
* or an OsError on failure.
*/
fn GetMemInfo() -> Result<u64, OsError>;
fn GetMemInfo() -> Result<u64, DraconisError>;
/**
* @brief Gets structured metadata about the currently playing media.
* @return A Result containing the media information (MediaInfo struct) on success,
* or a NowPlayingError (indicating player state or system error) on failure.
*/
fn GetNowPlaying() -> Result<MediaInfo, NowPlayingError>;
fn GetNowPlaying() -> Result<MediaInfo, DraconisError>;
/**
* @brief Gets the "pretty" name of the operating system.
* @details Examples: "Ubuntu 24.04.2 LTS", "Windows 11 Pro 24H2", "macOS 15 Sequoia".
* @return A Result containing the OS version String on success,
* or an OsError on failure.
* @return A Result containing the OS version String on success, or an OsError on failure.
*/
fn GetOSVersion() -> Result<String, OsError>;
fn GetOSVersion() -> Result<String, DraconisError>;
/**
* @brief Attempts to retrieve the desktop environment name.
* @details This is most relevant on Linux. May check environment variables (XDG_CURRENT_DESKTOP),
* session files, or running processes. On Windows/macOS, it might return a
* UI theme identifier (e.g., "Fluent", "Aqua") or None.
* @details This is most relevant on Linux. May check environment variables (XDG_CURRENT_DESKTOP), session files,
* or running processes. On Windows/macOS, it might return a UI theme identifier (e.g., "Fluent", "Aqua") or None.
* @return An Option containing the detected DE name String, or None if detection fails or is not applicable.
*/
fn GetDesktopEnvironment() -> Option<String>;
@ -56,8 +59,8 @@ namespace os {
* @brief Attempts to detect the current user shell name.
* @details Checks the SHELL environment variable on Linux/macOS. On Windows, inspects the process tree
* to identify known shells like PowerShell, Cmd, or MSYS2 shells (Bash, Zsh).
* @return An Option containing the detected shell name (e.g., "Bash", "Zsh", "PowerShell", "Fish"), or None if
* detection fails.
* @return An Option containing the detected shell name (e.g., "Bash", "Zsh", "PowerShell", "Fish"),
* or None if detection fails.
*/
fn GetShell() -> Option<String>;
@ -68,7 +71,7 @@ namespace os {
* @return A Result containing the host/product identifier String on success,
* or an OsError on failure (e.g., permission reading DMI/registry, API error).
*/
fn GetHost() -> Result<String, OsError>;
fn GetHost() -> Result<String, DraconisError>;
/**
* @brief Gets the operating system's kernel version string.
@ -77,7 +80,7 @@ namespace os {
* @return A Result containing the kernel version String on success,
* or an OsError on failure.
*/
fn GetKernelVersion() -> Result<String, OsError>;
fn GetKernelVersion() -> Result<String, DraconisError>;
/**
* @brief Gets the number of installed packages (Platform-specific).
@ -86,7 +89,7 @@ namespace os {
* or an OsError on failure (e.g., permission errors, command not found)
* or if not supported (OsErrorCode::NotSupported).
*/
fn GetPackageCount() -> Result<u64, OsError>; // Note: Returns OsError{OsErrorCode::NotSupported} on Win/Mac likely
fn GetPackageCount() -> Result<u64, DraconisError>;
/**
* @brief Gets the disk usage for the primary/root filesystem.
@ -94,5 +97,5 @@ namespace os {
* @return A Result containing the DiskSpace struct (used/total bytes) on success,
* or an OsError on failure (e.g., filesystem not found, permission error).
*/
fn GetDiskUsage() -> Result<DiskSpace, OsError>;
fn GetDiskUsage() -> Result<DiskSpace, DraconisError>;
} // namespace os

View file

@ -1,347 +0,0 @@
#pragma once
// Fixes conflict in Windows with <windows.h>
#ifdef _WIN32
#undef ERROR
#endif // _WIN32
#include <chrono>
#include <filesystem>
#include <format>
#include <print>
#include <source_location>
#include <utility>
#include "types.h"
/// Macro alias for trailing return type functions.
#define fn auto
/// Macro alias for std::nullopt, represents an empty optional value.
#define None std::nullopt
/**
* @namespace term
* @brief Provides terminal-related utilities, including color and style formatting.
*/
namespace term {
/**
* @enum Emphasis
* @brief Represents text emphasis styles.
*
* Enum values can be combined using bitwise OR to apply multiple styles at once.
*/
enum class Emphasis : u8 {
Bold, ///< Bold text.
Italic ///< Italic text.
};
/**
* @enum Color
* @brief Represents ANSI color codes for terminal output.
*
* Color codes can be used to format terminal output.
*/
enum class Color : u8 {
Black = 30, ///< Black color.
Red = 31, ///< Red color.
Green = 32, ///< Green color.
Yellow = 33, ///< Yellow color.
Blue = 34, ///< Blue color.
Magenta = 35, ///< Magenta color.
Cyan = 36, ///< Cyan color.
White = 37, ///< White color.
BrightBlack = 90, ///< Bright black (gray) color.
BrightRed = 91, ///< Bright red color.
BrightGreen = 92, ///< Bright green color.
BrightYellow = 93, ///< Bright yellow color.
BrightBlue = 94, ///< Bright blue color.
BrightMagenta = 95, ///< Bright magenta color.
BrightCyan = 96, ///< Bright cyan color.
BrightWhite = 97, ///< Bright white color.
};
/**
* @brief Combines two emphasis styles using bitwise OR.
* @param emphA The first emphasis style.
* @param emphB The second emphasis style.
* @return The combined emphasis style.
*/
constexpr fn operator|(Emphasis emphA, Emphasis emphB)->Emphasis {
return static_cast<Emphasis>(static_cast<u8>(emphA) | static_cast<u8>(emphB));
}
/**
* @brief Checks if two emphasis styles are equal using bitwise AND.
* @param emphA The first emphasis style.
* @param emphB The second emphasis style.
* @return The result of the bitwise AND operation.
*/
constexpr fn operator&(Emphasis emphA, Emphasis emphB)->u8 { return static_cast<u8>(emphA) & static_cast<u8>(emphB); }
/**
* @struct Style
* @brief Represents a combination of text styles.
*
* Emphasis and color are both optional, allowing for flexible styling.
*/
struct Style {
Option<Emphasis> emph; ///< Optional emphasis style.
Option<Color> fg_col; ///< Optional foreground color style.
/**
* @brief Generates the ANSI escape code for the combined styles.
* @return The ANSI escape code for the combined styles.
*/
[[nodiscard]] fn ansiCode() const -> String {
String result;
if (emph) {
if ((*emph & Emphasis::Bold) != 0)
result += "\033[1m";
if ((*emph & Emphasis::Italic) != 0)
result += "\033[3m";
}
if (fg_col)
result += std::format("\033[{}m", static_cast<u8>(*fg_col));
return result;
}
};
/**
* @brief Combines an emphasis style and a foreground color into a Style.
* @param emph The emphasis style to apply.
* @param fgColor The foreground color to apply.
* @return The combined style.
*/
// ReSharper disable CppDFAConstantParameter
constexpr fn operator|(const Emphasis emph, const Color fgColor)->Style {
return { .emph = emph, .fg_col = fgColor };
}
// ReSharper restore CppDFAConstantParameter
/**
* @brief Combines a foreground color and an emphasis style into a Style.
* @param fgColor The foreground color to apply.
* @param emph The emphasis style to apply.
* @return The combined style.
*/
constexpr fn operator|(const Color fgColor, const Emphasis emph)->Style {
return { .emph = emph, .fg_col = fgColor };
}
/**
* @brief Prints formatted text with the specified style.
* @tparam Args Parameter pack for format arguments.
* @param style The Style object containing emphasis and/or color.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn Print(const Style& style, std::format_string<Args...> fmt, Args&&... args) -> void {
if (const String styleCode = style.ansiCode(); styleCode.empty())
std::print(fmt, std::forward<Args>(args)...);
else
std::print("{}{}{}", styleCode, std::format(fmt, std::forward<Args>(args)...), "\033[0m");
}
/**
* @brief Prints formatted text with the specified foreground color.
* @tparam Args Parameter pack for format arguments.
* @param fgColor The foreground color to apply.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn Print(const Color& fgColor, std::format_string<Args...> fmt, Args&&... args) -> void {
Print({ .emph = None, .fg_col = fgColor }, fmt, std::forward<Args>(args)...);
}
/**
* @brief Prints formatted text with the specified emphasis style.
* @tparam Args Parameter pack for format arguments.
* @param emph The emphasis style to apply.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn Print(const Emphasis emph, std::format_string<Args...> fmt, Args&&... args) -> void {
Print({ .emph = emph, .fg_col = None }, fmt, std::forward<Args>(args)...);
}
/**
* @brief Prints formatted text with no specific style (default terminal style).
* @tparam Args Parameter pack for format arguments.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn Print(std::format_string<Args...> fmt, Args&&... args) -> void {
// Directly use std::print for unstyled output
std::print(fmt, std::forward<Args>(args)...);
}
} // namespace term
/**
* @enum LogLevel
* @brief Represents different log levels.
*/
enum class LogLevel : u8 { DEBUG, INFO, WARN, ERROR };
/**
* @brief Logs a message with the specified log level, source location, and format string.
* @tparam Args Parameter pack for format arguments.
* @param level The log level (DEBUG, INFO, WARN, ERROR).
* @param loc The source location of the log message.
* @param fmt The format string.
* @param args The arguments for the format string.
*/
template <typename... Args>
fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_string<Args...> fmt, Args&&... args) {
using namespace std::chrono;
using namespace term;
#ifdef _MSC_VER
using enum term::Color;
#else
using enum Color;
#endif // _MSC_VER
const auto [color, levelStr] = [&] {
switch (level) {
case LogLevel::DEBUG: return std::make_pair(Cyan, "DEBUG");
case LogLevel::INFO: return std::make_pair(Green, "INFO ");
case LogLevel::WARN: return std::make_pair(Yellow, "WARN ");
case LogLevel::ERROR: return std::make_pair(Red, "ERROR");
default: std::unreachable();
}
}();
Print(BrightWhite, "[{:%X}] ", std::chrono::floor<seconds>(system_clock::now()));
Print(Emphasis::Bold | color, "{} ", levelStr);
Print(fmt, std::forward<Args>(args)...);
#ifndef NDEBUG
Print(BrightWhite, "\n{:>14} ", "╰──");
Print(
Emphasis::Italic | BrightWhite,
"{}:{}",
std::filesystem::path(loc.file_name()).lexically_normal().string(),
loc.line()
);
#endif // !NDEBUG
Print("\n");
}
namespace detail {
template <typename ErrorType>
fn LogAppError(const LogLevel level, const ErrorType& error_obj) {
using DecayedErrorType = std::decay_t<ErrorType>;
std::source_location log_location = std::source_location::current();
String error_message_part;
LogLevel final_log_level = level;
if constexpr (std::is_same_v<DecayedErrorType, OsError>) {
log_location = error_obj.location;
error_message_part = error_obj.message;
} else if constexpr (std::is_same_v<DecayedErrorType, NowPlayingError>) {
if (std::holds_alternative<OsError>(error_obj)) {
const OsError& osErr = std::get<OsError>(error_obj);
log_location = osErr.location;
error_message_part = osErr.message;
} else if (std::holds_alternative<NowPlayingCode>(error_obj)) {
const NowPlayingCode npCode = std::get<NowPlayingCode>(error_obj);
log_location = std::source_location::current();
final_log_level = LogLevel::DEBUG;
switch (npCode) {
case NowPlayingCode::NoPlayers: error_message_part = "No media players found"; break;
case NowPlayingCode::NoActivePlayer: error_message_part = "No active media player found"; break;
default: error_message_part = "Unknown NowPlayingCode"; break;
}
}
} else {
log_location = std::source_location::current();
if constexpr (std::is_base_of_v<std::exception, DecayedErrorType>)
error_message_part = error_obj.what();
else if constexpr (requires { error_obj.message; })
error_message_part = error_obj.message;
else
error_message_part = "Unknown error type logged";
}
LogImpl(final_log_level, log_location, "{}", error_message_part);
}
} // namespace detail
// Suppress unused macro warnings in Clang
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-macros"
#endif // __clang__
#ifdef NDEBUG
#define DEBUG_LOG(...) static_cast<void>(0)
#define DEBUG_LOG_LOC(...) static_cast<void>(0)
#else
/**
* @def DEBUG_LOG
* @brief Logs a message at the DEBUG level.
* @details Only active in non-release builds (when NDEBUG is not defined).
* Includes timestamp, level, message, and source location.
* @param ... Format string and arguments for the log message.
*/
#define DEBUG_LOG(...) LogImpl(LogLevel::DEBUG, std::source_location::current(), __VA_ARGS__)
/**
* @def DEBUG_LOG_LOC(error_obj)
* @brief Logs an application-specific error at the DEBUG level, using its stored location if available.
* @details Only active in non-release builds (when NDEBUG is not defined).
* @param error_obj The error object (e.g., OsError, NowPlayingError).
*/
#define DEBUG_LOG_LOC(error_obj) \
do { \
[&](const auto& err) { detail::LogAppError(LogLevel::DEBUG, err); }(error_obj); \
} while (0)
#endif // NDEBUG
/**
* @def INFO_LOG(...)
* @brief Logs a message at the INFO level.
* @details Includes timestamp, level, message, and source location (in debug builds).
* @param ... Format string and arguments for the log message.
*/
#define INFO_LOG(...) LogImpl(LogLevel::INFO, std::source_location::current(), __VA_ARGS__)
/**
* @def WARN_LOG(...)
* @brief Logs a message at the WARN level.
* @details Includes timestamp, level, message, and source location (in debug builds).
* @param ... Format string and arguments for the log message.
*/
#define WARN_LOG(...) LogImpl(LogLevel::WARN, std::source_location::current(), __VA_ARGS__)
/**
* @def ERROR_LOG(...)
* @brief Logs a message at the ERROR level.
* @details Includes timestamp, level, message, and source location (in debug builds).
* @param ... Format string and arguments for the log message.
*/
#define ERROR_LOG(...) LogImpl(LogLevel::ERROR, std::source_location::current(), __VA_ARGS__)
/**
* @def ERROR_LOG_LOC(error_obj)
* @brief Logs an application-specific error at the ERROR level, using its stored location if available.
* @param error_obj The error object (e.g., OsError, NowPlayingError).
*/
#define ERROR_LOG_LOC(error_obj) \
do { \
[&](const auto& err) { detail::LogAppError(LogLevel::ERROR, err); }(error_obj); \
} while (0)
#ifdef __clang__
#pragma clang diagnostic pop
#endif // __clang__

View file

@ -1,383 +0,0 @@
#pragma once
#include <array> // std::array alias (Array)
#include <cstdlib> // std::getenv, std::free
#include <expected> // std::expected alias (Result)
#include <format> // std::format
#include <map> // std::map alias (Map)
#include <memory> // std::shared_ptr and std::unique_ptr aliases (SharedPointer, UniquePointer)
#include <optional> // std::optional alias (Option)
#include <source_location> // std::source_location
#include <string> // std::string and std::string_view aliases (String, StringView)
#include <system_error> // std::error_code and std::system_error
#include <utility> // std::pair alias (Pair)
#include <variant> // std::variant alias (NowPlayingError)
#include <vector> // std::vector alias (Vec)
#ifdef _WIN32
#include <winrt/base.h> // winrt::hresult_error
#elifdef __linux__
#include <dbus-cxx.h> // DBus::Error
#endif
//----------------------------------------------------------------//
// Integer Type Aliases //
// Provides concise names for standard fixed-width integer types. //
//----------------------------------------------------------------//
using u8 = std::uint8_t; ///< 8-bit unsigned integer.
using u16 = std::uint16_t; ///< 16-bit unsigned integer.
using u32 = std::uint32_t; ///< 32-bit unsigned integer.
using u64 = std::uint64_t; ///< 64-bit unsigned integer.
using i8 = std::int8_t; ///< 8-bit signed integer.
using i16 = std::int16_t; ///< 16-bit signed integer.
using i32 = std::int32_t; ///< 32-bit signed integer.
using i64 = std::int64_t; ///< 64-bit signed integer.
//-----------------------------------------------------------//
// Floating-Point Type Aliases //
// Provides concise names for standard floating-point types. //
//-----------------------------------------------------------//
using f32 = float; ///< 32-bit floating-point number.
using f64 = double; ///< 64-bit floating-point number.
//-------------------------------------------------//
// Size Type Aliases //
// Provides concise names for standard size types. //
//-------------------------------------------------//
using usize = std::size_t; ///< Unsigned size type (result of sizeof).
using isize = std::ptrdiff_t; ///< Signed size type (result of pointer subtraction).
//---------------------------------------------------//
// String Type Aliases //
// Provides concise names for standard string types. //
//---------------------------------------------------//
using String = std::string; ///< Owning, mutable string.
using StringView = std::string_view; ///< Non-owning view of a string.
using CStr = const char*; ///< Pointer to a null-terminated C-style string.
//----------------------------------------------------//
// Standard Library Type Aliases //
// Provides concise names for standard library types. //
//----------------------------------------------------//
using Exception = std::exception; ///< Standard exception type.
/**
* @typedef Result
* @brief Alias for std::expected<Tp, Er>. Represents a value that can either be
* a success value of type Tp or an error value of type Er.
* @tparam Tp The type of the success value.
* @tparam Er The type of the error value.
*/
template <typename Tp, typename Er>
using Result = std::expected<Tp, Er>;
/**
* @typedef Err
* @brief Alias for std::unexpected<Er>. Used to construct a Result in an error state.
* @tparam Er The type of the error value.
*/
template <typename Er>
using Err = std::unexpected<Er>;
/**
* @typedef Option
* @brief Alias for std::optional<Tp>. Represents a value that may or may not be present.
* @tparam Tp The type of the potential value.
*/
template <typename Tp>
using Option = std::optional<Tp>;
/**
* @typedef Array
* @brief Alias for std::array<Tp, sz>. Represents a fixed-size array.
* @tparam Tp The element type.
* @tparam sz The size of the array.
*/
template <typename Tp, usize sz>
using Array = std::array<Tp, sz>;
/**
* @typedef Vec
* @brief Alias for std::vector<Tp>. Represents a dynamic-size array (vector).
* @tparam Tp The element type.
*/
template <typename Tp>
using Vec = std::vector<Tp>;
/**
* @typedef Pair
* @brief Alias for std::pair<T1, T2>. Represents a pair of values.
* @tparam T1 The type of the first element.
* @tparam T2 The type of the second element.
*/
template <typename T1, typename T2>
using Pair = std::pair<T1, T2>;
/**
* @typedef Map
* @brief Alias for std::map<Key, Val>. Represents an ordered map (dictionary).
* @tparam Key The key type.
* @tparam Val The value type.
*/
template <typename Key, typename Val>
using Map = std::map<Key, Val>;
/**
* @typedef SharedPointer
* @brief Alias for std::shared_ptr<Tp>. Manages shared ownership of a dynamically allocated object.
* @tparam Tp The type of the managed object.
*/
template <typename Tp>
using SharedPointer = std::shared_ptr<Tp>;
/**
* @typedef UniquePointer
* @brief Alias for std::unique_ptr<Tp, Dp>. Manages unique ownership of a dynamically allocated object.
* @tparam Tp The type of the managed object.
* @tparam Dp The deleter type (defaults to std::default_delete<Tp>).
*/
template <typename Tp, typename Dp = std::default_delete<Tp>>
using UniquePointer = std::unique_ptr<Tp, Dp>;
//--------------------------------------------------------//
// Application-Specific Type Aliases //
// Provides concise names for application-specific types. //
//--------------------------------------------------------//
/**
* @enum NowPlayingCode
* @brief Error codes specific to the Now Playing feature.
*/
enum class NowPlayingCode : u8 {
NoPlayers, ///< No media players were found (e.g., no MPRIS services on Linux).
NoActivePlayer, ///< Players were found, but none are currently active or playing.
};
/**
* @enum OsErrorCode
* @brief Error codes for general OS-level operations.
*/
enum class OsErrorCode : u8 {
IoError, ///< General I/O error (filesystem, pipes, etc.).
PermissionDenied, ///< Insufficient permissions to perform the operation.
NotFound, ///< A required resource (file, registry key, device, API endpoint) was not found.
ParseError, ///< Failed to parse data obtained from the OS (e.g., file content, API output).
ApiUnavailable, ///< A required OS service/API is unavailable or failed unexpectedly at runtime.
NotSupported, ///< The requested operation is not supported on this platform, version, or configuration.
Timeout, ///< An operation timed out (e.g., waiting for IPC reply).
BufferTooSmall, ///< Optional: Keep if using fixed C-style buffers, otherwise remove.
InternalError, ///< An error occurred within the application's OS abstraction code logic.
NetworkError, ///< A network-related error occurred (e.g., DNS resolution, connection failure).
PlatformSpecific, ///< An unmapped error specific to the underlying OS platform occurred (check message).
Other, ///< A generic or unclassified error originating from the OS or an external library.
};
/**
* @struct OsError
* @brief Holds structured information about an OS-level error.
*
* Used as the error type in Result for many os:: functions.
*/
struct OsError {
// ReSharper disable CppDFANotInitializedField
String message; ///< A descriptive error message, potentially including platform details.
OsErrorCode code; ///< The general category of the error.
std::source_location location; ///< The source location where the error occurred (file, line, function).
// ReSharper restore CppDFANotInitializedField
OsError(const OsErrorCode errc, String msg, const std::source_location& loc = std::source_location::current())
: message(std::move(msg)), code(errc), location(loc) {}
explicit OsError(const Exception& exc, const std::source_location& loc = std::source_location::current())
: message(exc.what()), code(OsErrorCode::InternalError), location(loc) {}
explicit OsError(const std::error_code& errc, const std::source_location& loc = std::source_location::current())
: message(errc.message()), location(loc) {
using enum OsErrorCode;
using enum std::errc;
switch (static_cast<std::errc>(errc.value())) {
case permission_denied: code = PermissionDenied; break;
case no_such_file_or_directory: code = NotFound; break;
case timed_out: code = Timeout; break;
case io_error: code = IoError; break;
case network_unreachable:
case network_down:
case connection_refused: code = NetworkError; break;
case not_supported: code = NotSupported; break;
default: code = errc.category() == std::generic_category() ? InternalError : PlatformSpecific; break;
}
}
#ifdef _WIN32
explicit OsError(const winrt::hresult_error& e) : message(winrt::to_string(e.message())) {
switch (e.code()) {
case E_ACCESSDENIED: code = OsErrorCode::PermissionDenied; break;
case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):
case HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND):
case HRESULT_FROM_WIN32(ERROR_SERVICE_NOT_FOUND): code = OsErrorCode::NotFound; break;
case HRESULT_FROM_WIN32(ERROR_TIMEOUT):
case HRESULT_FROM_WIN32(ERROR_SEM_TIMEOUT): code = OsErrorCode::Timeout; break;
case HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED): code = OsErrorCode::NotSupported; break;
default: code = OsErrorCode::PlatformSpecific; break;
}
}
#else
OsError(const OsErrorCode code_hint, const int errno_val)
: message(std::system_category().message(errno_val)), code(code_hint) {
using enum OsErrorCode;
switch (errno_val) {
case EACCES: code = PermissionDenied; break;
case ENOENT: code = NotFound; break;
case ETIMEDOUT: code = Timeout; break;
case ENOTSUP: code = NotSupported; break;
default: code = PlatformSpecific; break;
}
}
static auto withErrno(const String& context, const std::source_location& loc = std::source_location::current())
-> OsError {
const i32 errNo = errno;
const String msg = std::system_category().message(errNo);
const String fullMsg = std::format("{}: {}", context, msg);
OsErrorCode code;
switch (errNo) {
case EACCES:
case EPERM: code = OsErrorCode::PermissionDenied; break;
case ENOENT: code = OsErrorCode::NotFound; break;
case ETIMEDOUT: code = OsErrorCode::Timeout; break;
case ENOTSUP: code = OsErrorCode::NotSupported; break;
case EIO: code = OsErrorCode::IoError; break;
case ECONNREFUSED:
case ENETDOWN:
case ENETUNREACH: code = OsErrorCode::NetworkError; break;
default: code = OsErrorCode::PlatformSpecific; break;
}
return OsError { code, fullMsg, loc };
}
#ifdef __linux__
static auto fromDBus(const DBus::Error& err, const std::source_location& loc = std::source_location::current())
-> OsError {
String name = err.name();
OsErrorCode codeHint = OsErrorCode::PlatformSpecific;
String message;
using namespace std::string_view_literals;
if (name == "org.freedesktop.DBus.Error.ServiceUnknown"sv ||
name == "org.freedesktop.DBus.Error.NameHasNoOwner"sv) {
codeHint = OsErrorCode::NotFound;
message = std::format("DBus service/name not found: {}", err.message());
} else if (name == "org.freedesktop.DBus.Error.NoReply"sv || name == "org.freedesktop.DBus.Error.Timeout"sv) {
codeHint = OsErrorCode::Timeout;
message = std::format("DBus timeout/no reply: {}", err.message());
} else if (name == "org.freedesktop.DBus.Error.AccessDenied"sv) {
codeHint = OsErrorCode::PermissionDenied;
message = std::format("DBus access denied: {}", err.message());
} else {
message = std::format("DBus error: {} - {}", name, err.message());
}
return OsError { codeHint, message, loc };
}
#endif
#endif
};
/**
* @struct DiskSpace
* @brief Represents disk usage information.
*
* Used as the success type for os::GetDiskUsage.
*/
struct DiskSpace {
u64 used_bytes; ///< Currently used disk space in bytes.
u64 total_bytes; ///< Total disk space in bytes.
};
/**
* @struct MediaInfo
* @brief Holds structured metadata about currently playing media.
*
* Used as the success type for os::GetNowPlaying.
* Using Option<> for fields that might not always be available.
*/
struct MediaInfo {
Option<String> title; ///< Track title.
Option<String> artist; ///< Track artist(s).
Option<String> album; ///< Album name.
Option<String> app_name; ///< Name of the media player application (e.g., "Spotify", "Firefox").
MediaInfo() = default;
MediaInfo(Option<String> title, Option<String> artist) : title(std::move(title)), artist(std::move(artist)) {}
MediaInfo(Option<String> title, Option<String> artist, Option<String> album, Option<String> app)
: title(std::move(title)), artist(std::move(artist)), album(std::move(album)), app_name(std::move(app)) {}
};
//--------------------------------------------------------//
// Potentially Update Existing Application-Specific Types //
//--------------------------------------------------------//
/**
* @typedef NowPlayingError (Updated Recommendation)
* @brief Represents the possible errors returned by os::GetNowPlaying.
*
* It's a variant that can hold either a specific NowPlayingCode
* (indicating player state like 'no active player') or a general OsError
* (indicating an underlying system/API failure).
*/
using NowPlayingError = std::variant<NowPlayingCode, OsError>;
/**
* @enum EnvError
* @brief Error codes for environment variable retrieval.
*/
enum class EnvError : u8 {
NotFound, ///< Environment variable not found.
AccessError, ///< Access error when trying to retrieve the variable.
};
/**
* @brief Safely retrieves an environment variable.
* @param name The name of the environment variable to retrieve.
* @return A Result containing the value of the environment variable as a String,
* or an EnvError if an error occurred.
*/
[[nodiscard]] inline auto GetEnv(CStr name) -> Result<String, EnvError> {
#ifdef _WIN32
char* rawPtr = nullptr;
usize bufferSize = 0;
// Use _dupenv_s to safely retrieve environment variables on Windows
const i32 err = _dupenv_s(&rawPtr, &bufferSize, name);
const UniquePointer<char, decltype(&free)> ptrManager(rawPtr, free);
if (err != 0)
return Err(EnvError::AccessError); // Error retrieving environment variable
if (!ptrManager)
return Err(EnvError::NotFound); // Environment variable not found
return ptrManager.get();
#else
// Use std::getenv to retrieve environment variables on POSIX systems
const CStr value = std::getenv(name);
if (!value)
return Err(EnvError::NotFound); // Environment variable not found
return value;
#endif
}

58
src/wrappers/wayland.hpp Normal file
View file

@ -0,0 +1,58 @@
#pragma once
#include <wayland-client.h>
#include "src/core/util/defs.hpp"
#include "src/core/util/types.hpp"
struct wl_display;
namespace wl {
using display = wl_display;
// NOLINTBEGIN(readability-identifier-naming)
inline fn connect(const char* name) -> display* { return wl_display_connect(name); }
inline fn disconnect(display* display) -> void { wl_display_disconnect(display); }
inline fn get_fd(display* display) -> int { return wl_display_get_fd(display); }
// NOLINTEND(readability-identifier-naming)
/**
* RAII wrapper for Wayland display connections
* Automatically handles resource acquisition and cleanup
*/
class DisplayGuard {
display* m_Display;
public:
/**
* Opens a Wayland display connection
*/
DisplayGuard() : m_Display(connect(nullptr)) {}
~DisplayGuard() {
if (m_Display)
disconnect(m_Display);
}
// Non-copyable
DisplayGuard(const DisplayGuard&) = delete;
fn operator=(const DisplayGuard&)->DisplayGuard& = delete;
// Movable
DisplayGuard(DisplayGuard&& other) noexcept : m_Display(std::exchange(other.m_Display, nullptr)) {}
fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& {
if (this != &other) {
if (m_Display)
disconnect(m_Display);
m_Display = std::exchange(other.m_Display, nullptr);
}
return *this;
}
[[nodiscard]] explicit operator bool() const { return m_Display != nullptr; }
[[nodiscard]] fn get() const -> display* { return m_Display; }
[[nodiscard]] fn fd() const -> util::types::i32 { return get_fd(m_Display); }
};
} // namespace wl

168
src/wrappers/xcb.hpp Normal file
View file

@ -0,0 +1,168 @@
#pragma once
#include <xcb/xcb.h>
#include "src/core/util/defs.hpp"
#include "src/core/util/types.hpp"
namespace xcb {
using util::types::u8, util::types::i32, util::types::CStr;
using connection_t = xcb_connection_t;
using setup_t = xcb_setup_t;
using screen_t = xcb_screen_t;
using window_t = xcb_window_t;
using atom_t = xcb_atom_t;
using generic_error_t = xcb_generic_error_t;
using intern_atom_cookie_t = xcb_intern_atom_cookie_t;
using intern_atom_reply_t = xcb_intern_atom_reply_t;
using get_property_cookie_t = xcb_get_property_cookie_t;
using get_property_reply_t = xcb_get_property_reply_t;
constexpr atom_t ATOM_WINDOW = XCB_ATOM_WINDOW;
enum ConnError : u8 {
Generic = XCB_CONN_ERROR,
ExtNotSupported = XCB_CONN_CLOSED_EXT_NOTSUPPORTED,
MemInsufficient = XCB_CONN_CLOSED_MEM_INSUFFICIENT,
ReqLenExceed = XCB_CONN_CLOSED_REQ_LEN_EXCEED,
ParseErr = XCB_CONN_CLOSED_PARSE_ERR,
InvalidScreen = XCB_CONN_CLOSED_INVALID_SCREEN,
FdPassingFailed = XCB_CONN_CLOSED_FDPASSING_FAILED
};
// NOLINTBEGIN(readability-identifier-naming)
inline fn getConnError(const util::types::i32 code) -> util::types::Option<ConnError> {
switch (code) {
case XCB_CONN_ERROR: return Generic;
case XCB_CONN_CLOSED_EXT_NOTSUPPORTED: return ExtNotSupported;
case XCB_CONN_CLOSED_MEM_INSUFFICIENT: return MemInsufficient;
case XCB_CONN_CLOSED_REQ_LEN_EXCEED: return ReqLenExceed;
case XCB_CONN_CLOSED_PARSE_ERR: return ParseErr;
case XCB_CONN_CLOSED_INVALID_SCREEN: return InvalidScreen;
case XCB_CONN_CLOSED_FDPASSING_FAILED: return FdPassingFailed;
default: return None;
}
}
inline fn connect(const char* displayname, int* screenp) -> connection_t* {
return xcb_connect(displayname, screenp);
}
inline fn disconnect(connection_t* conn) -> void { xcb_disconnect(conn); }
inline fn connection_has_error(connection_t* conn) -> int { return xcb_connection_has_error(conn); }
inline fn intern_atom(connection_t* conn, const uint8_t only_if_exists, const uint16_t name_len, const char* name)
-> intern_atom_cookie_t {
return xcb_intern_atom(conn, only_if_exists, name_len, name);
}
inline fn intern_atom_reply(connection_t* conn, const intern_atom_cookie_t cookie, generic_error_t** err)
-> intern_atom_reply_t* {
return xcb_intern_atom_reply(conn, cookie, err);
}
inline fn get_property(
connection_t* conn,
const uint8_t _delete,
const window_t window,
const atom_t property,
const atom_t type,
const uint32_t long_offset,
const uint32_t long_length
) -> get_property_cookie_t {
return xcb_get_property(conn, _delete, window, property, type, long_offset, long_length);
}
inline fn get_property_reply(connection_t* conn, const get_property_cookie_t cookie, generic_error_t** err)
-> get_property_reply_t* {
return xcb_get_property_reply(conn, cookie, err);
}
inline fn get_property_value_length(const get_property_reply_t* reply) -> int {
return xcb_get_property_value_length(reply);
}
inline fn get_property_value(const get_property_reply_t* reply) -> void* { return xcb_get_property_value(reply); }
// NOLINTEND(readability-identifier-naming)
/**
* RAII wrapper for X11 Display connections
* Automatically handles resource acquisition and cleanup
*/
class DisplayGuard {
connection_t* m_Connection = nullptr;
public:
/**
* Opens an XCB connection
* @param name Display name (nullptr for default)
*/
explicit DisplayGuard(const util::types::CStr name = nullptr) : m_Connection(connect(name, nullptr)) {}
~DisplayGuard() {
if (m_Connection)
disconnect(m_Connection);
}
// Non-copyable
DisplayGuard(const DisplayGuard&) = delete;
fn operator=(const DisplayGuard&)->DisplayGuard& = delete;
// Movable
DisplayGuard(DisplayGuard&& other) noexcept : m_Connection(std::exchange(other.m_Connection, nullptr)) {}
fn operator=(DisplayGuard&& other) noexcept -> DisplayGuard& {
if (this != &other) {
if (m_Connection)
disconnect(m_Connection);
m_Connection = std::exchange(other.m_Connection, nullptr);
}
return *this;
}
[[nodiscard]] explicit operator bool() const { return m_Connection && !connection_has_error(m_Connection); }
[[nodiscard]] fn get() const -> connection_t* { return m_Connection; }
[[nodiscard]] fn setup() const -> const setup_t* { return m_Connection ? xcb_get_setup(m_Connection) : nullptr; }
[[nodiscard]] fn rootScreen() const -> screen_t* {
const setup_t* setup = this->setup();
return setup ? xcb_setup_roots_iterator(setup).data : nullptr;
}
};
/**
* RAII wrapper for XCB replies
* Handles automatic cleanup of various XCB reply objects
*/
template <typename T>
class ReplyGuard {
T* m_Reply = nullptr;
public:
ReplyGuard() = default;
explicit ReplyGuard(T* reply) : m_Reply(reply) {}
~ReplyGuard() {
if (m_Reply)
free(m_Reply);
}
// Non-copyable
ReplyGuard(const ReplyGuard&) = delete;
fn operator=(const ReplyGuard&)->ReplyGuard& = delete;
// Movable
ReplyGuard(ReplyGuard&& other) noexcept : m_Reply(std::exchange(other.m_Reply, nullptr)) {}
fn operator=(ReplyGuard&& other) noexcept -> ReplyGuard& {
if (this != &other) {
if (m_Reply)
free(m_Reply);
m_Reply = std::exchange(other.m_Reply, nullptr);
}
return *this;
}
[[nodiscard]] explicit operator bool() const { return m_Reply != nullptr; }
[[nodiscard]] fn get() const -> T* { return m_Reply; }
[[nodiscard]] fn operator->() const->T* { return m_Reply; }
[[nodiscard]] fn operator*() const->T& { return *m_Reply; }
};
} // namespace xcb