guhg
This commit is contained in:
parent
24b6a72614
commit
1a2fba7fb8
29 changed files with 1676 additions and 1401 deletions
|
@ -141,8 +141,6 @@
|
|||
|
||||
LD_LIBRARY_PATH = "${lib.makeLibraryPath deps}";
|
||||
NIX_ENFORCE_NO_NATIVE = 0;
|
||||
|
||||
name = "C++";
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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'],
|
||||
}
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
|
@ -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;
|
||||
const String content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
|
||||
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
|
||||
|
|
|
@ -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
78
src/config/weather.hpp
Normal 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
|
|
@ -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();
|
||||
Option<String> shellOption = os::GetShell();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
91
src/core/system_data.hpp
Normal 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
12
src/core/util/defs.hpp
Normal 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
159
src/core/util/error.hpp
Normal 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
43
src/core/util/helpers.hpp
Normal 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
286
src/core/util/logging.hpp
Normal 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
145
src/core/util/types.hpp
Normal 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
|
75
src/main.cpp
75
src/main.cpp
|
@ -1,20 +1,28 @@
|
|||
#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 bool SHOW_ICONS = true;
|
||||
static constexpr i32 MAX_PARAGRAPH_LENGTH = 30;
|
||||
static constexpr inline StringView ICON_TYPE = "EMOJI";
|
||||
|
||||
static constexpr i32 MAX_PARAGRAPH_LENGTH = 30;
|
||||
|
||||
// Color themes
|
||||
struct Theme {
|
||||
|
@ -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));
|
||||
|
|
325
src/os/linux.cpp
325
src/os/linux.cpp
|
@ -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";
|
||||
default: return std::format("Unknown Error Code ({})", err);
|
||||
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()
|
||||
|
@ -224,16 +250,14 @@ namespace {
|
|||
|
||||
Map<String, DBus::Variant> metadata = metadataVariant.to_map<String, DBus::Variant>(); // Can throw
|
||||
|
||||
Option<String> title = None;
|
||||
Option<String> artist = None;
|
||||
Option<String> album = None;
|
||||
Option<String> appName = None; // Try to get app name too
|
||||
Option<String> title = None;
|
||||
Option<String> artist = None;
|
||||
|
||||
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,56 +411,61 @@ 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,
|
||||
std::format(
|
||||
"Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}",
|
||||
primaryPath,
|
||||
primaryError.message,
|
||||
fallbackPath,
|
||||
fallbackError.message
|
||||
)
|
||||
));
|
||||
});
|
||||
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,
|
||||
primaryError.message,
|
||||
fallbackPath,
|
||||
fallbackError.message
|
||||
)
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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__
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
43
src/os/linux/pkg_count.hpp
Normal file
43
src/os/linux/pkg_count.hpp
Normal 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
|
|
@ -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
|
|
@ -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__
|
383
src/util/types.h
383
src/util/types.h
|
@ -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
58
src/wrappers/wayland.hpp
Normal 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
168
src/wrappers/xcb.hpp
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue