suckle
This commit is contained in:
parent
6d9390f02c
commit
33881d007e
7 changed files with 505 additions and 554 deletions
|
@ -73,7 +73,7 @@ endif
|
||||||
if cpp.get_id() in ['msvc', 'clang-cl']
|
if cpp.get_id() in ['msvc', 'clang-cl']
|
||||||
common_cpp_args = common_cpp_flags['msvc']
|
common_cpp_args = common_cpp_flags['msvc']
|
||||||
if cpp.get_id() == 'clang-cl'
|
if cpp.get_id() == 'clang-cl'
|
||||||
common_cpp_args += common_warning_flags + common_cpp_flags['common']
|
common_cpp_args += common_warning_flags + common_cpp_flags['common'] + ['-fcolor-diagnostics', '-fdiagnostics-format=clang']
|
||||||
endif
|
endif
|
||||||
else
|
else
|
||||||
common_cpp_args = common_warning_flags + common_cpp_flags['common'] + common_cpp_flags['unix_extra']
|
common_cpp_args = common_warning_flags + common_cpp_flags['common'] + common_cpp_flags['unix_extra']
|
||||||
|
|
|
@ -7,12 +7,36 @@
|
||||||
#include <toml++/impl/parser.hpp> // toml::{parse_file, parse_result}
|
#include <toml++/impl/parser.hpp> // toml::{parse_file, parse_result}
|
||||||
#include <utility> // std::pair (Pair)
|
#include <utility> // std::pair (Pair)
|
||||||
|
|
||||||
|
#include "src/core/util/helpers.hpp"
|
||||||
#include "src/core/util/logging.hpp"
|
#include "src/core/util/logging.hpp"
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using util::types::Vec, util::types::CStr, util::types::Exception;
|
using util::types::Vec, util::types::CStr, util::types::Exception;
|
||||||
|
constexpr const char* defaultConfigTemplate = R"cfg(# Draconis++ Configuration File
|
||||||
|
|
||||||
|
# General settings
|
||||||
|
[general]
|
||||||
|
name = "{}" # Your display name
|
||||||
|
|
||||||
|
# Now Playing integration
|
||||||
|
[now_playing]
|
||||||
|
enabled = false # Set to true to enable media integration
|
||||||
|
|
||||||
|
# Weather settings
|
||||||
|
[weather]
|
||||||
|
enabled = false # Set to true to enable weather display
|
||||||
|
show_town_name = false # Show location name in weather display
|
||||||
|
api_key = "" # Your weather API key
|
||||||
|
units = "metric" # Use "metric" for °C or "imperial" for °F
|
||||||
|
location = "London" # Your city name
|
||||||
|
|
||||||
|
# Alternatively, you can specify coordinates instead of a city name:
|
||||||
|
# [weather.location]
|
||||||
|
# lat = 51.5074
|
||||||
|
# lon = -0.1278
|
||||||
|
)cfg";
|
||||||
|
|
||||||
fn GetConfigPath() -> fs::path {
|
fn GetConfigPath() -> fs::path {
|
||||||
using util::helpers::GetEnv;
|
using util::helpers::GetEnv;
|
||||||
|
@ -30,8 +54,6 @@ namespace {
|
||||||
|
|
||||||
if (auto result = GetEnv("APPDATA"))
|
if (auto result = GetEnv("APPDATA"))
|
||||||
possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml");
|
possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml");
|
||||||
|
|
||||||
possiblePaths.push_back(fs::path(".") / "config.toml");
|
|
||||||
#else
|
#else
|
||||||
if (Result<String, DraconisError> result = GetEnv("XDG_CONFIG_HOME"))
|
if (Result<String, DraconisError> result = GetEnv("XDG_CONFIG_HOME"))
|
||||||
possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml");
|
possiblePaths.emplace_back(fs::path(*result) / "draconis++" / "config.toml");
|
||||||
|
@ -40,18 +62,18 @@ namespace {
|
||||||
possiblePaths.emplace_back(fs::path(*result) / ".config" / "draconis++" / "config.toml");
|
possiblePaths.emplace_back(fs::path(*result) / ".config" / "draconis++" / "config.toml");
|
||||||
possiblePaths.emplace_back(fs::path(*result) / ".draconis++" / "config.toml");
|
possiblePaths.emplace_back(fs::path(*result) / ".draconis++" / "config.toml");
|
||||||
}
|
}
|
||||||
|
|
||||||
possiblePaths.emplace_back("/etc/draconis++/config.toml");
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
possiblePaths.push_back(fs::path(".") / "config.toml");
|
||||||
|
|
||||||
for (const fs::path& path : possiblePaths)
|
for (const fs::path& path : possiblePaths)
|
||||||
if (std::error_code errc; exists(path, errc) && !errc)
|
if (std::error_code errc; fs::exists(path, errc) && !errc)
|
||||||
return path;
|
return path;
|
||||||
|
|
||||||
if (!possiblePaths.empty()) {
|
if (!possiblePaths.empty()) {
|
||||||
const fs::path defaultDir = possiblePaths[0].parent_path();
|
const fs::path defaultDir = possiblePaths[0].parent_path();
|
||||||
|
|
||||||
if (std::error_code errc; !exists(defaultDir, errc) && !errc) {
|
if (std::error_code errc; !fs::exists(defaultDir, errc) || !errc) {
|
||||||
create_directories(defaultDir, errc);
|
create_directories(defaultDir, errc);
|
||||||
if (errc)
|
if (errc)
|
||||||
warn_log("Warning: Failed to create config directory: {}", errc.message());
|
warn_log("Warning: Failed to create config directory: {}", errc.message());
|
||||||
|
@ -60,26 +82,33 @@ namespace {
|
||||||
return possiblePaths[0];
|
return possiblePaths[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
throw std::runtime_error("Could not determine a valid config path");
|
warn_log("Could not determine a preferred config path. Falling back to './config.toml'");
|
||||||
|
return fs::path(".") / "config.toml";
|
||||||
}
|
}
|
||||||
|
|
||||||
fn CreateDefaultConfig(const fs::path& configPath) -> bool {
|
fn CreateDefaultConfig(const fs::path& configPath) -> bool {
|
||||||
try {
|
try {
|
||||||
std::error_code errc;
|
std::error_code errc;
|
||||||
create_directories(configPath.parent_path(), errc);
|
create_directories(configPath.parent_path(), errc);
|
||||||
|
|
||||||
if (errc) {
|
if (errc) {
|
||||||
error_log("Failed to create config directory: {}", errc.message());
|
error_log("Failed to create config directory: {}", errc.message());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
toml::table root;
|
String defaultName;
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
Array<char, 256> username;
|
Array<char, 256> username;
|
||||||
|
|
||||||
DWORD size = sizeof(username);
|
DWORD size = sizeof(username);
|
||||||
|
|
||||||
String defaultName = GetUserNameA(username.data(), &size) ? username.data() : "User";
|
if (GetUserNameA(username.data(), &size)) {
|
||||||
|
defaultName = username.data();
|
||||||
|
} else {
|
||||||
|
debug_log("Failed to get username: {}", GetLastError());
|
||||||
|
defaultName = "User";
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
const passwd* pwd = getpwuid(getuid());
|
const passwd* pwd = getpwuid(getuid());
|
||||||
CStr pwdName = pwd ? pwd->pw_name : nullptr;
|
CStr pwdName = pwd ? pwd->pw_name : nullptr;
|
||||||
|
@ -87,79 +116,91 @@ namespace {
|
||||||
const Result<String, DraconisError> envUser = util::helpers::GetEnv("USER");
|
const Result<String, DraconisError> envUser = util::helpers::GetEnv("USER");
|
||||||
const Result<String, DraconisError> envLogname = util::helpers::GetEnv("LOGNAME");
|
const Result<String, DraconisError> envLogname = util::helpers::GetEnv("LOGNAME");
|
||||||
|
|
||||||
String defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User";
|
defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
toml::table* general = root.insert("general", toml::table {}).first->second.as_table();
|
|
||||||
general->insert("name", defaultName);
|
|
||||||
|
|
||||||
toml::table* nowPlaying = root.insert("now_playing", toml::table {}).first->second.as_table();
|
|
||||||
nowPlaying->insert("enabled", false);
|
|
||||||
|
|
||||||
toml::table* weather = root.insert("weather", toml::table {}).first->second.as_table();
|
|
||||||
weather->insert("enabled", false);
|
|
||||||
weather->insert("show_town_name", false);
|
|
||||||
weather->insert("api_key", "");
|
|
||||||
weather->insert("units", "metric");
|
|
||||||
weather->insert("location", "London");
|
|
||||||
|
|
||||||
std::ofstream file(configPath);
|
std::ofstream file(configPath);
|
||||||
if (!file) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
file << "# Draconis++ Configuration File\n\n";
|
try {
|
||||||
|
const std::string formattedConfig = std::format(defaultConfigTemplate, defaultName);
|
||||||
|
file << formattedConfig;
|
||||||
|
} catch (const std::format_error& fmt_err) {
|
||||||
|
error_log("Failed to format default config string: {}. Using fallback name 'User'.", fmt_err.what());
|
||||||
|
|
||||||
file << "# General settings\n";
|
try {
|
||||||
file << "[general]\n";
|
const std::string fallbackConfig = std::format(defaultConfigTemplate, "User");
|
||||||
file << "name = \"" << defaultName << "\" # Your display name\n\n";
|
file << fallbackConfig;
|
||||||
|
} catch (...) {
|
||||||
|
error_log("Failed to format default config even with fallback name.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
file << "# Now Playing integration\n";
|
if (!file) {
|
||||||
file << "[now_playing]\n";
|
error_log("Failed to write to config file: {}", configPath.string());
|
||||||
file << "enabled = false # Set to true to enable media integration\n\n";
|
return false;
|
||||||
|
}
|
||||||
file << "# Weather settings\n";
|
|
||||||
file << "[weather]\n";
|
|
||||||
file << "enabled = false # Set to true to enable weather display\n";
|
|
||||||
file << "show_town_name = false # Show location name in weather display\n";
|
|
||||||
file << "api_key = \"\" # Your weather API key\n";
|
|
||||||
file << "units = \"metric\" # Use \"metric\" for °C or \"imperial\" for °F\n";
|
|
||||||
file << "location = \"London\" # Your city name\n\n";
|
|
||||||
|
|
||||||
file << "# Alternatively, you can specify coordinates instead of a city name:\n";
|
|
||||||
file << "# [weather.location]\n";
|
|
||||||
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;
|
return true;
|
||||||
|
} catch (const fs::filesystem_error& fs_err) {
|
||||||
|
error_log("Filesystem error during default config creation: {}", fs_err.what());
|
||||||
|
return false;
|
||||||
} catch (const Exception& e) {
|
} catch (const Exception& e) {
|
||||||
error_log("Failed to create default config file: {}", e.what());
|
error_log("Failed to create default config file: {}", e.what());
|
||||||
return false;
|
return false;
|
||||||
|
} catch (...) {
|
||||||
|
error_log("An unexpected error occurred during default config creation.");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
Config::Config(const toml::table& tbl) {
|
||||||
|
const toml::node_view genTbl = tbl["general"];
|
||||||
|
const toml::node_view npTbl = tbl["now_playing"];
|
||||||
|
const toml::node_view wthTbl = tbl["weather"];
|
||||||
|
|
||||||
|
this->general = genTbl.is_table() ? General::fromToml(*genTbl.as_table()) : General {};
|
||||||
|
this->now_playing = npTbl.is_table() ? NowPlaying::fromToml(*npTbl.as_table()) : NowPlaying {};
|
||||||
|
this->weather = wthTbl.is_table() ? Weather::fromToml(*wthTbl.as_table()) : Weather {};
|
||||||
|
}
|
||||||
|
|
||||||
fn Config::getInstance() -> Config {
|
fn Config::getInstance() -> Config {
|
||||||
try {
|
try {
|
||||||
const fs::path configPath = GetConfigPath();
|
const fs::path configPath = GetConfigPath();
|
||||||
|
|
||||||
if (!exists(configPath)) {
|
std::error_code ec;
|
||||||
info_log("Config file not found, creating defaults at {}", configPath.string());
|
|
||||||
|
const bool exists = fs::exists(configPath, ec);
|
||||||
|
|
||||||
|
if (ec)
|
||||||
|
warn_log(
|
||||||
|
"Failed to check if config file exists at {}: {}. Assuming it doesn't.", configPath.string(), ec.message()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
info_log("Config file not found at {}, creating defaults.", configPath.string());
|
||||||
|
|
||||||
if (!CreateDefaultConfig(configPath)) {
|
if (!CreateDefaultConfig(configPath)) {
|
||||||
warn_log("Failed to create default config, using in-memory defaults");
|
warn_log("Failed to create default config file at {}. Using in-memory defaults.", configPath.string());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toml::parse_result config = toml::parse_file(configPath.string());
|
const toml::table config = toml::parse_file(configPath.string());
|
||||||
|
|
||||||
debug_log("Config loaded from {}", configPath.string());
|
debug_log("Config loaded from {}", configPath.string());
|
||||||
return fromToml(config);
|
return Config(config);
|
||||||
} catch (const Exception& e) {
|
} catch (const Exception& e) {
|
||||||
debug_log("Config loading failed: {}, using defaults", e.what());
|
debug_log("Config loading failed: {}, using defaults", e.what());
|
||||||
return {};
|
return {};
|
||||||
|
} catch (...) {
|
||||||
|
error_log("An unexpected error occurred during config loading. Using in-memory defaults.");
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
#include "src/core/util/defs.hpp"
|
#include "src/core/util/defs.hpp"
|
||||||
#include "src/core/util/error.hpp"
|
#include "src/core/util/error.hpp"
|
||||||
#include "src/core/util/helpers.hpp"
|
#include "src/core/util/logging.hpp"
|
||||||
#include "src/core/util/types.hpp"
|
#include "src/core/util/types.hpp"
|
||||||
|
|
||||||
#include "weather.hpp"
|
#include "weather.hpp"
|
||||||
|
@ -132,8 +132,10 @@ struct Weather {
|
||||||
.lat = *location.as_table()->get("lat")->value<double>(),
|
.lat = *location.as_table()->get("lat")->value<double>(),
|
||||||
.lon = *location.as_table()->get("lon")->value<double>(),
|
.lon = *location.as_table()->get("lon")->value<double>(),
|
||||||
};
|
};
|
||||||
else
|
else {
|
||||||
throw std::runtime_error("Invalid location type");
|
error_log("Invalid location format in config.");
|
||||||
|
weather.enabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return weather;
|
return weather;
|
||||||
|
@ -159,22 +161,9 @@ struct Config {
|
||||||
Weather weather; ///< Weather configuration settings.`
|
Weather weather; ///< Weather configuration settings.`
|
||||||
NowPlaying now_playing; ///< Now Playing configuration settings.
|
NowPlaying now_playing; ///< Now Playing configuration settings.
|
||||||
|
|
||||||
/**
|
Config() = default;
|
||||||
* @brief Parses a TOML table to create a Config instance.
|
|
||||||
* @param tbl The TOML table to parse, containing [general], [now_playing], and [weather].
|
|
||||||
* @return A Config instance with the parsed values, or defaults otherwise.
|
|
||||||
*/
|
|
||||||
static fn fromToml(const toml::table& tbl) -> Config {
|
|
||||||
const toml::node_view genTbl = tbl["general"];
|
|
||||||
const toml::node_view npTbl = tbl["now_playing"];
|
|
||||||
const toml::node_view wthTbl = tbl["weather"];
|
|
||||||
|
|
||||||
return {
|
explicit Config(const toml::table& tbl);
|
||||||
.general = genTbl.is_table() ? General::fromToml(*genTbl.as_table()) : General {},
|
|
||||||
.weather = wthTbl.is_table() ? Weather::fromToml(*wthTbl.as_table()) : Weather {},
|
|
||||||
.now_playing = npTbl.is_table() ? NowPlaying::fromToml(*npTbl.as_table()) : NowPlaying {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Retrieves the path to the configuration file.
|
* @brief Retrieves the path to the configuration file.
|
||||||
|
|
|
@ -26,52 +26,59 @@ namespace {
|
||||||
return std::format(std::locale::classic(), "{:%B %d}", ymd);
|
return std::format(std::locale::classic(), "{:%B %d}", ymd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_timing(const std::string& name, const std::chrono::steady_clock::duration& duration) -> void {
|
||||||
|
const auto millis = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(duration);
|
||||||
|
debug_log("{} took: {} ms", name, millis.count());
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Func>
|
||||||
|
fn time_execution(const std::string& name, Func&& func) {
|
||||||
|
const auto start = std::chrono::steady_clock::now();
|
||||||
|
if constexpr (std::is_void_v<decltype(func())>) {
|
||||||
|
func();
|
||||||
|
|
||||||
|
const auto end = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
log_timing(name, end - start);
|
||||||
|
} else {
|
||||||
|
auto result = func();
|
||||||
|
|
||||||
|
const auto end = std::chrono::steady_clock::now();
|
||||||
|
log_timing(name, end - start);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
fn SystemData::fetchSystemData(const Config& config) -> SystemData {
|
fn SystemData::fetchSystemData(const Config& config) -> SystemData {
|
||||||
using util::types::None;
|
using util::types::None, util::types::Exception;
|
||||||
|
using namespace os;
|
||||||
|
|
||||||
SystemData data {
|
SystemData data {
|
||||||
.date = GetDate(),
|
.date = time_execution("GetDate", GetDate),
|
||||||
.host = os::GetHost(),
|
.host = time_execution("GetHost", GetHost),
|
||||||
.kernel_version = os::GetKernelVersion(),
|
.kernel_version = time_execution("GetKernelVersion", GetKernelVersion),
|
||||||
.os_version = os::GetOSVersion(),
|
.os_version = time_execution("GetOSVersion", GetOSVersion),
|
||||||
.mem_info = os::GetMemInfo(),
|
.mem_info = time_execution("GetMemInfo", GetMemInfo),
|
||||||
.desktop_environment = os::GetDesktopEnvironment(),
|
.desktop_environment = time_execution("GetDesktopEnvironment", GetDesktopEnvironment),
|
||||||
.window_manager = os::GetWindowManager(),
|
.window_manager = time_execution("GetWindowManager", GetWindowManager),
|
||||||
.disk_usage = {},
|
.disk_usage = time_execution("GetDiskUsage", GetDiskUsage),
|
||||||
.shell = None,
|
.shell = time_execution("GetShell", GetShell),
|
||||||
.now_playing = None,
|
|
||||||
.weather_info = None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
auto diskShellFuture = std::async(std::launch::async, [] {
|
if (const Result<MediaInfo, DraconisError>& nowPlayingResult = time_execution("GetNowPlaying", os::GetNowPlaying)) {
|
||||||
Result<DiskSpace, DraconisError> diskResult = os::GetDiskUsage();
|
data.now_playing = nowPlayingResult;
|
||||||
Option<String> shellOption = os::GetShell();
|
} else {
|
||||||
return std::make_tuple(std::move(diskResult), std::move(shellOption));
|
data.now_playing = None;
|
||||||
});
|
}
|
||||||
|
|
||||||
std::future<weather::Output> weatherFuture;
|
const auto start = std::chrono::steady_clock::now();
|
||||||
std::future<Result<MediaInfo, DraconisError>> nowPlayingFuture;
|
data.weather_info = config.weather.getWeatherInfo();
|
||||||
|
const auto end = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
if (config.weather.enabled)
|
log_timing("config.weather.getWeatherInfo", end - start);
|
||||||
weatherFuture = std::async(std::launch::async, [&config] { return config.weather.getWeatherInfo(); });
|
|
||||||
|
|
||||||
if (config.now_playing.enabled)
|
|
||||||
nowPlayingFuture = std::async(std::launch::async, os::GetNowPlaying);
|
|
||||||
|
|
||||||
auto [diskResult, shellOption] = diskShellFuture.get();
|
|
||||||
|
|
||||||
data.disk_usage = std::move(diskResult);
|
|
||||||
data.shell = std::move(shellOption);
|
|
||||||
|
|
||||||
if (weatherFuture.valid())
|
|
||||||
try {
|
|
||||||
data.weather_info = weatherFuture.get();
|
|
||||||
} catch (const std::exception& e) { data.weather_info = None; }
|
|
||||||
|
|
||||||
if (nowPlayingFuture.valid())
|
|
||||||
data.now_playing = nowPlayingFuture.get();
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,184 +1,63 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <chrono> // std::chrono::{days, floor, seconds, system_clock}
|
#include <chrono> // std::chrono::{days, floor, seconds, system_clock}
|
||||||
#include <filesystem> // std::filesystem::path
|
#include <filesystem> // std::filesystem::path
|
||||||
#include <format> // std::format
|
#include <format> // std::format
|
||||||
#include <print> // std::print
|
#include <ftxui/screen/color.hpp> // ftxui::Color
|
||||||
#include <source_location> // std::source_location
|
#include <mutex> // std::mutex
|
||||||
#include <utility> // std::{forward, make_pair}
|
#include <print> // std::print
|
||||||
|
#include <source_location> // std::source_location
|
||||||
|
#include <utility> // std::forward
|
||||||
|
|
||||||
#include "src/core/util/defs.hpp"
|
#include "src/core/util/defs.hpp"
|
||||||
#include "src/core/util/error.hpp"
|
#include "src/core/util/error.hpp"
|
||||||
#include "src/core/util/types.hpp"
|
#include "src/core/util/types.hpp"
|
||||||
|
|
||||||
namespace util::logging {
|
namespace util::logging {
|
||||||
using types::u8, types::i32, types::String, types::StringView, types::Option, types::None;
|
using types::usize, types::u8, types::i32, types::i64, types::CStr, types::String, types::StringView, types::Array,
|
||||||
|
types::Option, types::None;
|
||||||
|
|
||||||
/**
|
// Store all compile-time constants in a struct
|
||||||
* @namespace term
|
struct LogLevelConst {
|
||||||
* @brief Provides terminal-related utilities, including color and style formatting.
|
// ANSI color codes
|
||||||
*/
|
// clang-format off
|
||||||
namespace term {
|
static constexpr Array<const char*, 16> COLOR_CODE_LITERALS = {
|
||||||
/**
|
"\033[38;5;0m", "\033[38;5;1m", "\033[38;5;2m", "\033[38;5;3m",
|
||||||
* @enum Emphasis
|
"\033[38;5;4m", "\033[38;5;5m", "\033[38;5;6m", "\033[38;5;7m",
|
||||||
* @brief Represents text emphasis styles.
|
"\033[38;5;8m", "\033[38;5;9m", "\033[38;5;10m", "\033[38;5;11m",
|
||||||
*
|
"\033[38;5;12m", "\033[38;5;13m", "\033[38;5;14m", "\033[38;5;15m",
|
||||||
* Enum values can be combined using bitwise OR to apply multiple styles at once.
|
|
||||||
*/
|
|
||||||
enum class Emphasis : u8 {
|
|
||||||
Bold, ///< Bold text.
|
|
||||||
Italic ///< Italic text.
|
|
||||||
};
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
/**
|
// ANSI formatting constants
|
||||||
* @enum Color
|
static constexpr const char* RESET_CODE = "\033[0m";
|
||||||
* @brief Represents ANSI color codes for terminal output.
|
static constexpr const char* BOLD_START = "\033[1m";
|
||||||
*
|
static constexpr const char* BOLD_END = "\033[22m";
|
||||||
* Color codes can be used to format terminal output.
|
static constexpr const char* ITALIC_START = "\033[3m";
|
||||||
*/
|
static constexpr const char* ITALIC_END = "\033[23m";
|
||||||
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.
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// Log level text constants
|
||||||
* @brief Combines two emphasis styles using bitwise OR.
|
static constexpr StringView DEBUG_STR = "DEBUG";
|
||||||
* @param emphA The first emphasis style.
|
static constexpr StringView INFO_STR = "INFO ";
|
||||||
* @param emphB The second emphasis style.
|
static constexpr StringView WARN_STR = "WARN ";
|
||||||
* @return The combined emphasis style.
|
static constexpr StringView ERROR_STR = "ERROR";
|
||||||
*/
|
|
||||||
constexpr fn operator|(Emphasis emphA, Emphasis emphB)->Emphasis {
|
|
||||||
return static_cast<Emphasis>(static_cast<u8>(emphA) | static_cast<u8>(emphB));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Log level color constants
|
||||||
* @brief Checks if two emphasis styles are equal using bitwise AND.
|
static constexpr ftxui::Color::Palette16 DEBUG_COLOR = ftxui::Color::Palette16::Cyan;
|
||||||
* @param emphA The first emphasis style.
|
static constexpr ftxui::Color::Palette16 INFO_COLOR = ftxui::Color::Palette16::Green;
|
||||||
* @param emphB The second emphasis style.
|
static constexpr ftxui::Color::Palette16 WARN_COLOR = ftxui::Color::Palette16::Yellow;
|
||||||
* @return The result of the bitwise AND operation.
|
static constexpr ftxui::Color::Palette16 ERROR_COLOR = ftxui::Color::Palette16::Red;
|
||||||
*/
|
static constexpr ftxui::Color::Palette16 DEBUG_INFO_COLOR = ftxui::Color::Palette16::GrayLight;
|
||||||
constexpr fn operator&(Emphasis emphA, Emphasis emphB)->u8 {
|
|
||||||
return static_cast<u8>(emphA) & static_cast<u8>(emphB);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
static constexpr CStr TIMESTAMP_FORMAT = "{:%X}";
|
||||||
* @struct Style
|
static constexpr CStr LOG_FORMAT = "{} {} {}";
|
||||||
* @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.
|
|
||||||
|
|
||||||
/**
|
#ifndef NDEBUG
|
||||||
* @brief Generates the ANSI escape code for the combined styles.
|
static constexpr CStr DEBUG_INFO_FORMAT = "{}{}{}\n";
|
||||||
* @return The ANSI escape code for the combined styles.
|
static constexpr CStr FILE_LINE_FORMAT = "{}:{}";
|
||||||
*/
|
static constexpr CStr DEBUG_LINE_PREFIX = " ╰── ";
|
||||||
[[nodiscard]] fn ansiCode() const -> String {
|
#endif
|
||||||
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
|
* @enum LogLevel
|
||||||
|
@ -186,6 +65,73 @@ namespace util::logging {
|
||||||
*/
|
*/
|
||||||
enum class LogLevel : u8 { Debug, Info, Warn, Error };
|
enum class LogLevel : u8 { Debug, Info, Warn, Error };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Directly applies ANSI color codes to text
|
||||||
|
* @param text The text to colorize
|
||||||
|
* @param color The FTXUI color
|
||||||
|
* @return Styled string with ANSI codes
|
||||||
|
*/
|
||||||
|
inline fn Colorize(const String& text, const ftxui::Color::Palette16& color) -> String {
|
||||||
|
return String(LogLevelConst::COLOR_CODE_LITERALS[static_cast<int>(color)]) + text + LogLevelConst::RESET_CODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Make text bold with ANSI codes
|
||||||
|
* @param text The text to make bold
|
||||||
|
* @return Bold text
|
||||||
|
*/
|
||||||
|
inline fn Bold(const StringView text) -> String {
|
||||||
|
return String(LogLevelConst::BOLD_START) + String(text) + String(LogLevelConst::BOLD_END);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Make text italic with ANSI codes
|
||||||
|
* @param text The text to make italic
|
||||||
|
* @return Italic text
|
||||||
|
*/
|
||||||
|
inline fn Italic(const StringView text) -> String {
|
||||||
|
return String(LogLevelConst::ITALIC_START) + String(text) + String(LogLevelConst::ITALIC_END);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize at runtime using the constexpr static values
|
||||||
|
// This can't be constexpr itself due to the string operations
|
||||||
|
inline const Array<String, 4> LEVEL_INFO = {
|
||||||
|
Bold(Colorize(LogLevelConst::DEBUG_STR.data(), LogLevelConst::DEBUG_COLOR)),
|
||||||
|
Bold(Colorize(LogLevelConst::INFO_STR.data(), LogLevelConst::INFO_COLOR)),
|
||||||
|
Bold(Colorize(LogLevelConst::WARN_STR.data(), LogLevelConst::WARN_COLOR)),
|
||||||
|
Bold(Colorize(LogLevelConst::ERROR_STR.data(), LogLevelConst::ERROR_COLOR)),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns FTXUI color representation for a log level
|
||||||
|
* @param level The log level
|
||||||
|
* @return FTXUI color code
|
||||||
|
*/
|
||||||
|
constexpr fn GetLevelColor(const LogLevel level) -> ftxui::Color::Palette16 {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel::Debug: return LogLevelConst::DEBUG_COLOR;
|
||||||
|
case LogLevel::Info: return LogLevelConst::INFO_COLOR;
|
||||||
|
case LogLevel::Warn: return LogLevelConst::WARN_COLOR;
|
||||||
|
case LogLevel::Error: return LogLevelConst::ERROR_COLOR;
|
||||||
|
default: std::unreachable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns string representation of a log level
|
||||||
|
* @param level The log level
|
||||||
|
* @return String representation
|
||||||
|
*/
|
||||||
|
constexpr fn GetLevelString(const LogLevel level) -> String {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel::Debug: return LogLevelConst::DEBUG_STR.data();
|
||||||
|
case LogLevel::Info: return LogLevelConst::INFO_STR.data();
|
||||||
|
case LogLevel::Warn: return LogLevelConst::WARN_STR.data();
|
||||||
|
case LogLevel::Error: return LogLevelConst::ERROR_STR.data();
|
||||||
|
default: std::unreachable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Logs a message with the specified log level, source location, and format string.
|
* @brief Logs a message with the specified log level, source location, and format string.
|
||||||
* @tparam Args Parameter pack for format arguments.
|
* @tparam Args Parameter pack for format arguments.
|
||||||
|
@ -197,37 +143,45 @@ namespace util::logging {
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_string<Args...> fmt, Args&&... args) {
|
fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_string<Args...> fmt, Args&&... args) {
|
||||||
using namespace std::chrono;
|
using namespace std::chrono;
|
||||||
using namespace term;
|
using std::filesystem::path;
|
||||||
|
|
||||||
const auto [color, levelStr] = [&] {
|
using Buffer = Array<char, 512>;
|
||||||
switch (level) {
|
|
||||||
case LogLevel::Debug: return std::make_pair(Color::Cyan, "DEBUG");
|
|
||||||
case LogLevel::Info: return std::make_pair(Color::Green, "INFO ");
|
|
||||||
case LogLevel::Warn: return std::make_pair(Color::Yellow, "WARN ");
|
|
||||||
case LogLevel::Error: return std::make_pair(Color::Red, "ERROR");
|
|
||||||
default: std::unreachable();
|
|
||||||
}
|
|
||||||
}();
|
|
||||||
|
|
||||||
Print(Color::BrightWhite, "[{:%X}] ", std::chrono::floor<seconds>(system_clock::now()));
|
// Dynamic parts (runtime)
|
||||||
Print(Emphasis::Bold | color, "{} ", levelStr);
|
const time_point<system_clock, duration<i64>> now = floor<seconds>(system_clock::now());
|
||||||
Print(fmt, std::forward<Args>(args)...);
|
|
||||||
|
const String timestamp = std::format(LogLevelConst::TIMESTAMP_FORMAT, now);
|
||||||
|
const String message = std::format(fmt, std::forward<Args>(args)...);
|
||||||
|
|
||||||
|
Buffer buffer {};
|
||||||
|
|
||||||
|
Buffer::iterator iter = std::format_to(
|
||||||
|
buffer.begin(),
|
||||||
|
LogLevelConst::LOG_FORMAT,
|
||||||
|
Colorize("[" + timestamp + "]", LogLevelConst::DEBUG_INFO_COLOR),
|
||||||
|
LEVEL_INFO[static_cast<usize>(level)],
|
||||||
|
message
|
||||||
|
);
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
Print(Color::BrightWhite, "\n{:>14} ", "╰──");
|
iter = std::format_to(
|
||||||
Print(
|
iter,
|
||||||
Emphasis::Italic | Color::BrightWhite,
|
"\n{}",
|
||||||
"{}:{}",
|
Italic(Colorize(
|
||||||
std::filesystem::path(loc.file_name()).lexically_normal().string(),
|
LogLevelConst::DEBUG_LINE_PREFIX +
|
||||||
loc.line()
|
std::format("{}:{}", path(loc.file_name()).lexically_normal().string(), std::to_string(loc.line())),
|
||||||
|
LogLevelConst::DEBUG_INFO_COLOR
|
||||||
|
))
|
||||||
);
|
);
|
||||||
#endif // !NDEBUG
|
#endif
|
||||||
|
|
||||||
Print("\n");
|
const usize length = std::distance(buffer.begin(), iter);
|
||||||
|
|
||||||
|
std::println("{}", std::string_view(buffer.data(), length));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename ErrorType>
|
template <typename ErrorType>
|
||||||
fn LogAppError(const LogLevel level, const ErrorType& error_obj) {
|
fn LogError(const LogLevel level, const ErrorType& error_obj) {
|
||||||
using DecayedErrorType = std::decay_t<ErrorType>;
|
using DecayedErrorType = std::decay_t<ErrorType>;
|
||||||
|
|
||||||
std::source_location logLocation;
|
std::source_location logLocation;
|
||||||
|
@ -254,7 +208,7 @@ namespace util::logging {
|
||||||
::util::logging::LogImpl( \
|
::util::logging::LogImpl( \
|
||||||
::util::logging::LogLevel::Debug, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
|
::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);
|
#define debug_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Debug, error_obj);
|
||||||
#else
|
#else
|
||||||
#define debug_log(...) ((void)0)
|
#define debug_log(...) ((void)0)
|
||||||
#define debug_at(...) ((void)0)
|
#define debug_at(...) ((void)0)
|
||||||
|
@ -264,17 +218,17 @@ namespace util::logging {
|
||||||
::util::logging::LogImpl( \
|
::util::logging::LogImpl( \
|
||||||
::util::logging::LogLevel::Info, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
|
::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 info_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Info, error_obj);
|
||||||
|
|
||||||
#define warn_log(fmt, ...) \
|
#define warn_log(fmt, ...) \
|
||||||
::util::logging::LogImpl( \
|
::util::logging::LogImpl( \
|
||||||
::util::logging::LogLevel::Warn, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
|
::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 warn_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Warn, error_obj);
|
||||||
|
|
||||||
#define error_log(fmt, ...) \
|
#define error_log(fmt, ...) \
|
||||||
::util::logging::LogImpl( \
|
::util::logging::LogImpl( \
|
||||||
::util::logging::LogLevel::Error, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \
|
::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);
|
#define error_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Error, error_obj);
|
||||||
} // namespace util::logging
|
} // namespace util::logging
|
||||||
|
|
20
src/main.cpp
20
src/main.cpp
|
@ -20,8 +20,6 @@ namespace ui {
|
||||||
using ftxui::Color;
|
using ftxui::Color;
|
||||||
using util::types::StringView, util::types::i32;
|
using util::types::StringView, util::types::i32;
|
||||||
|
|
||||||
static constexpr inline StringView ICON_TYPE = "EMOJI";
|
|
||||||
|
|
||||||
static constexpr i32 MAX_PARAGRAPH_LENGTH = 30;
|
static constexpr i32 MAX_PARAGRAPH_LENGTH = 30;
|
||||||
|
|
||||||
// Color themes
|
// Color themes
|
||||||
|
@ -55,7 +53,7 @@ namespace ui {
|
||||||
StringView window_manager;
|
StringView window_manager;
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr Icons NONE = {
|
[[maybe_unused]] static constexpr Icons NONE = {
|
||||||
.user = "",
|
.user = "",
|
||||||
.palette = "",
|
.palette = "",
|
||||||
.calendar = "",
|
.calendar = "",
|
||||||
|
@ -71,7 +69,7 @@ namespace ui {
|
||||||
.window_manager = "",
|
.window_manager = "",
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr Icons NERD = {
|
[[maybe_unused]] static constexpr Icons NERD = {
|
||||||
.user = " ",
|
.user = " ",
|
||||||
.palette = " ",
|
.palette = " ",
|
||||||
.calendar = " ",
|
.calendar = " ",
|
||||||
|
@ -87,7 +85,7 @@ namespace ui {
|
||||||
.window_manager = " ",
|
.window_manager = " ",
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr Icons EMOJI = {
|
[[maybe_unused]] static constexpr Icons EMOJI = {
|
||||||
.user = " 👤 ",
|
.user = " 👤 ",
|
||||||
.palette = " 🎨 ",
|
.palette = " 🎨 ",
|
||||||
.calendar = " 📅 ",
|
.calendar = " 📅 ",
|
||||||
|
@ -99,9 +97,11 @@ namespace ui {
|
||||||
.music = " 🎵 ",
|
.music = " 🎵 ",
|
||||||
.disk = " 💾 ",
|
.disk = " 💾 ",
|
||||||
.shell = " 💲 ",
|
.shell = " 💲 ",
|
||||||
.desktop = " 🖥 ️",
|
.desktop = " 🖥️ ",
|
||||||
.window_manager = " 🪟 ",
|
.window_manager = " 🪟 ",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static constexpr inline Icons ICON_TYPE = NERD;
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
@ -122,9 +122,7 @@ namespace {
|
||||||
const Weather weather = config.weather;
|
const Weather weather = config.weather;
|
||||||
|
|
||||||
const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, deIcon, wmIcon] =
|
const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, deIcon, wmIcon] =
|
||||||
ui::ICON_TYPE == "NERD" ? ui::NERD
|
ui::ICON_TYPE;
|
||||||
: ui::ICON_TYPE == "EMOJI" ? ui::EMOJI
|
|
||||||
: ui::NONE;
|
|
||||||
|
|
||||||
Elements content;
|
Elements content;
|
||||||
|
|
||||||
|
@ -264,6 +262,10 @@ namespace {
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
fn main() -> i32 {
|
fn main() -> i32 {
|
||||||
|
#ifdef _WIN32
|
||||||
|
winrt::init_apartment();
|
||||||
|
#endif
|
||||||
|
|
||||||
const Config& config = Config::getInstance();
|
const Config& config = Config::getInstance();
|
||||||
const SystemData data = SystemData::fetchSystemData(config);
|
const SystemData data = SystemData::fetchSystemData(config);
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,14 @@
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <wincrypt.h>
|
#include <wincrypt.h>
|
||||||
#include <dwmapi.h>
|
#include <dwmapi.h>
|
||||||
|
#include <tlhelp32.h>
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include <winrt/Windows.Foundation.Collections.h>
|
#include <winrt/Windows.Foundation.Collections.h>
|
||||||
#include <winrt/Windows.Media.Control.h>
|
#include <winrt/Windows.Media.Control.h>
|
||||||
#include <winrt/Windows.Storage.h>
|
#include <winrt/Windows.Storage.h>
|
||||||
#include <winrt/Windows.System.Diagnostics.h>
|
|
||||||
#include <winrt/Windows.System.Profile.h>
|
#include <winrt/Windows.System.Profile.h>
|
||||||
|
#include <winrt/Windows.Management.Deployment.h>
|
||||||
#include <winrt/base.h>
|
#include <winrt/base.h>
|
||||||
#include <winrt/impl/Windows.Media.Control.2.h>
|
#include <winrt/impl/Windows.Media.Control.2.h>
|
||||||
|
|
||||||
|
@ -24,33 +24,15 @@
|
||||||
#include "os.hpp"
|
#include "os.hpp"
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
|
using RtlGetVersionPtr = NTSTATUS(WINAPI*)(PRTL_OSVERSIONINFOW);
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using util::error::DraconisError, util::error::DraconisErrorCode;
|
using util::error::DraconisError, util::error::DraconisErrorCode;
|
||||||
using namespace util::types;
|
using namespace util::types;
|
||||||
|
|
||||||
struct OSVersion {
|
struct ProcessData {
|
||||||
u16 major;
|
DWORD parentPid = 0;
|
||||||
u16 minor;
|
String baseExeNameLower;
|
||||||
u16 build;
|
|
||||||
u16 revision;
|
|
||||||
|
|
||||||
static fn parseDeviceFamilyVersion(const winrt::hstring& versionString) -> OSVersion {
|
|
||||||
try {
|
|
||||||
const u64 versionUl = std::stoull(winrt::to_string(versionString));
|
|
||||||
return {
|
|
||||||
.major = static_cast<u16>((versionUl >> 48) & 0xFFFF),
|
|
||||||
.minor = static_cast<u16>((versionUl >> 32) & 0xFFFF),
|
|
||||||
.build = static_cast<u16>((versionUl >> 16) & 0xFFFF),
|
|
||||||
.revision = static_cast<u16>(versionUl & 0xFFFF),
|
|
||||||
};
|
|
||||||
} catch (const std::invalid_argument& e) {
|
|
||||||
error_log("Invalid argument: {}", e.what());
|
|
||||||
} catch (const std::out_of_range& e) {
|
|
||||||
error_log("Value out of range: {}", e.what());
|
|
||||||
} catch (const winrt::hresult_error& e) { error_log("Windows error: {}", winrt::to_string(e.message())); }
|
|
||||||
|
|
||||||
return { .major = 0, .minor = 0, .build = 0, .revision = 0 };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
|
@ -91,74 +73,76 @@ namespace {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn GetProcessInfo() -> Result<Vec<Pair<DWORD, String>>, DraconisError> {
|
|
||||||
try {
|
|
||||||
using namespace winrt::Windows::System::Diagnostics;
|
|
||||||
using namespace winrt::Windows::Foundation::Collections;
|
|
||||||
|
|
||||||
const IVectorView<ProcessDiagnosticInfo> processInfos = ProcessDiagnosticInfo::GetForProcesses();
|
|
||||||
|
|
||||||
Vec<Pair<DWORD, String>> processes;
|
|
||||||
processes.reserve(processInfos.Size());
|
|
||||||
|
|
||||||
for (const auto& processInfo : processInfos)
|
|
||||||
processes.emplace_back(processInfo.ProcessId(), winrt::to_string(processInfo.ExecutableFileName()));
|
|
||||||
return processes;
|
|
||||||
} catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); } catch (const std::exception& e) {
|
|
||||||
return Err(DraconisError(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn IsProcessRunning(const Vec<String>& processNames, const String& name) -> bool {
|
|
||||||
return std::ranges::any_of(processNames, [&name](const String& proc) -> bool {
|
|
||||||
return _stricmp(proc.c_str(), name.c_str()) == 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
template <usize sz>
|
template <usize sz>
|
||||||
fn FindShellInProcessTree(const DWORD startPid, const Array<Pair<StringView, StringView>, sz>& shellMap)
|
fn FindShellInProcessTree(const DWORD startPid, const Array<Pair<StringView, StringView>, sz>& shellMap)
|
||||||
-> Option<String> {
|
-> Option<String> {
|
||||||
if (startPid == 0)
|
if (startPid == 0)
|
||||||
return None;
|
return None;
|
||||||
|
|
||||||
try {
|
std::unordered_map<DWORD, ProcessData> processMap;
|
||||||
using namespace winrt::Windows::System::Diagnostics;
|
|
||||||
|
|
||||||
ProcessDiagnosticInfo currentProcessInfo = nullptr;
|
const HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
|
|
||||||
try {
|
if (hSnap == INVALID_HANDLE_VALUE) {
|
||||||
currentProcessInfo = ProcessDiagnosticInfo::TryGetForProcessId(startPid);
|
error_log("FindShellInProcessTree: Failed snapshot, error {}", GetLastError());
|
||||||
} catch (const winrt::hresult_error& e) {
|
return None;
|
||||||
error_log("Failed to get process info for PID {}: {}", startPid, winrt::to_string(e.message()));
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (currentProcessInfo) {
|
|
||||||
String processName = winrt::to_string(currentProcessInfo.ExecutableFileName());
|
|
||||||
|
|
||||||
if (!processName.empty()) {
|
|
||||||
std::ranges::transform(processName, processName.begin(), [](const u8 character) {
|
|
||||||
return static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (processName.length() > 4 && processName.ends_with(".exe"))
|
|
||||||
processName.resize(processName.length() - 4);
|
|
||||||
|
|
||||||
auto iter =
|
|
||||||
std::ranges::find_if(shellMap, [&](const auto& pair) { return StringView { processName } == pair.first; });
|
|
||||||
|
|
||||||
if (iter != std::ranges::end(shellMap))
|
|
||||||
return String { iter->second };
|
|
||||||
}
|
|
||||||
|
|
||||||
currentProcessInfo = currentProcessInfo.Parent();
|
|
||||||
}
|
|
||||||
} catch (const winrt::hresult_error& e) {
|
|
||||||
error_log("WinRT error during process tree walk (start PID {}): {}", startPid, winrt::to_string(e.message()));
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
error_log("Standard exception during process tree walk (start PID {}): {}", startPid, e.what());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PROCESSENTRY32 pe32;
|
||||||
|
pe32.dwSize = sizeof(PROCESSENTRY32);
|
||||||
|
|
||||||
|
if (Process32First(hSnap, &pe32)) {
|
||||||
|
do {
|
||||||
|
String fullName = pe32.szExeFile;
|
||||||
|
String baseName;
|
||||||
|
|
||||||
|
const size_t lastSlash = fullName.find_last_of("\\/");
|
||||||
|
|
||||||
|
baseName = (lastSlash == String::npos) ? fullName : fullName.substr(lastSlash + 1);
|
||||||
|
|
||||||
|
std::transform(baseName.begin(), baseName.end(), baseName.begin(), [](const u8 character) {
|
||||||
|
return std::tolower(character);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (baseName.length() > 4 && baseName.ends_with(".exe"))
|
||||||
|
baseName.resize(baseName.length() - 4);
|
||||||
|
|
||||||
|
processMap[pe32.th32ProcessID] = ProcessData { pe32.th32ParentProcessID, std::move(baseName) };
|
||||||
|
} while (Process32Next(hSnap, &pe32));
|
||||||
|
} else {
|
||||||
|
error_log("FindShellInProcessTree: Process32First failed, error {}", GetLastError());
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(hSnap);
|
||||||
|
|
||||||
|
DWORD currentPid = startPid;
|
||||||
|
|
||||||
|
i32 depth = 0;
|
||||||
|
|
||||||
|
constexpr int maxDepth = 32;
|
||||||
|
|
||||||
|
while (currentPid != 0 && depth < maxDepth) {
|
||||||
|
auto procIt = processMap.find(currentPid);
|
||||||
|
|
||||||
|
if (procIt == processMap.end())
|
||||||
|
break;
|
||||||
|
|
||||||
|
const String& processName = procIt->second.baseExeNameLower;
|
||||||
|
|
||||||
|
auto mapIter =
|
||||||
|
std::ranges::find_if(shellMap, [&](const auto& pair) { return StringView { processName } == pair.first; });
|
||||||
|
|
||||||
|
if (mapIter != std::ranges::end(shellMap))
|
||||||
|
return String { mapIter->second };
|
||||||
|
|
||||||
|
currentPid = procIt->second.parentPid;
|
||||||
|
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth >= maxDepth)
|
||||||
|
error_log("FindShellInProcessTree: Reached max depth limit ({}) walking parent PIDs from {}", maxDepth, startPid);
|
||||||
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,233 +164,207 @@ namespace {
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
fn os::GetMemInfo() -> Result<u64, DraconisError> {
|
namespace os {
|
||||||
try {
|
fn GetMemInfo() -> Result<u64, DraconisError> {
|
||||||
return winrt::Windows::System::Diagnostics::SystemDiagnosticInfo::GetForCurrentSystem()
|
MEMORYSTATUSEX memInfo;
|
||||||
.MemoryUsage()
|
memInfo.dwLength = sizeof(MEMORYSTATUSEX);
|
||||||
.GetReport()
|
|
||||||
.TotalPhysicalSizeInBytes();
|
|
||||||
} catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn os::GetNowPlaying() -> Result<MediaInfo, DraconisError> {
|
if (GlobalMemoryStatusEx(&memInfo))
|
||||||
using namespace winrt::Windows::Media::Control;
|
return memInfo.ullTotalPhys;
|
||||||
using namespace winrt::Windows::Foundation;
|
|
||||||
|
|
||||||
using Session = GlobalSystemMediaTransportControlsSession;
|
DWORD lastError = GetLastError();
|
||||||
using SessionManager = GlobalSystemMediaTransportControlsSessionManager;
|
return Err(DraconisError(
|
||||||
using MediaProperties = GlobalSystemMediaTransportControlsSessionMediaProperties;
|
DraconisErrorCode::PlatformSpecific, std::format("GlobalMemoryStatusEx failed with error code {}", lastError)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
fn GetNowPlaying() -> Result<MediaInfo, DraconisError> {
|
||||||
const IAsyncOperation<SessionManager> sessionManagerOp = SessionManager::RequestAsync();
|
using namespace winrt::Windows::Media::Control;
|
||||||
const SessionManager sessionManager = sessionManagerOp.get();
|
using namespace winrt::Windows::Foundation;
|
||||||
|
|
||||||
if (const Session currentSession = sessionManager.GetCurrentSession()) {
|
using Session = GlobalSystemMediaTransportControlsSession;
|
||||||
const MediaProperties mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
|
using SessionManager = GlobalSystemMediaTransportControlsSessionManager;
|
||||||
|
using MediaProperties = GlobalSystemMediaTransportControlsSessionMediaProperties;
|
||||||
|
|
||||||
return MediaInfo(
|
try {
|
||||||
winrt::to_string(mediaProperties.Title()), winrt::to_string(mediaProperties.Artist()), None, None
|
const IAsyncOperation<SessionManager> sessionManagerOp = SessionManager::RequestAsync();
|
||||||
);
|
const SessionManager sessionManager = sessionManagerOp.get();
|
||||||
}
|
|
||||||
|
|
||||||
return Err(DraconisError(DraconisErrorCode::NotFound, "No media session found"));
|
if (const Session currentSession = sessionManager.GetCurrentSession()) {
|
||||||
} catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); }
|
const MediaProperties mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
|
||||||
}
|
|
||||||
|
|
||||||
fn os::GetOSVersion() -> Result<String, DraconisError> {
|
return MediaInfo(
|
||||||
try {
|
winrt::to_string(mediaProperties.Title()), winrt::to_string(mediaProperties.Artist()), None, None
|
||||||
const String regSubKey = R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)";
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String productName = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "ProductName");
|
return Err(DraconisError(DraconisErrorCode::NotFound, "No media session found"));
|
||||||
const String displayVersion = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "DisplayVersion");
|
} catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); }
|
||||||
|
}
|
||||||
|
|
||||||
if (productName.empty())
|
fn GetOSVersion() -> Result<String, DraconisError> {
|
||||||
return Err(DraconisError(DraconisErrorCode::NotFound, "ProductName not found in registry"));
|
try {
|
||||||
|
const String regSubKey = R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)";
|
||||||
|
|
||||||
if (const Option<u64> buildNumberOpt = GetBuildNumber()) {
|
String productName = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "ProductName");
|
||||||
if (const u64 buildNumber = *buildNumberOpt; buildNumber >= 22000) {
|
const String displayVersion = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "DisplayVersion");
|
||||||
if (const size_t pos = productName.find("Windows 10"); pos != String::npos) {
|
|
||||||
const bool startBoundary = (pos == 0 || !isalnum(static_cast<unsigned char>(productName[pos - 1])));
|
|
||||||
const bool endBoundary =
|
|
||||||
(pos + 10 == productName.length() || !isalnum(static_cast<unsigned char>(productName[pos + 10])));
|
|
||||||
|
|
||||||
if (startBoundary && endBoundary) {
|
if (productName.empty())
|
||||||
productName.replace(pos, 10, "Windows 11");
|
return Err(DraconisError(DraconisErrorCode::NotFound, "ProductName not found in registry"));
|
||||||
|
|
||||||
|
if (const Option<u64> buildNumberOpt = GetBuildNumber()) {
|
||||||
|
if (const u64 buildNumber = *buildNumberOpt; buildNumber >= 22000) {
|
||||||
|
if (const size_t pos = productName.find("Windows 10"); pos != String::npos) {
|
||||||
|
const bool startBoundary = (pos == 0 || !isalnum(static_cast<unsigned char>(productName[pos - 1])));
|
||||||
|
const bool endBoundary =
|
||||||
|
(pos + 10 == productName.length() || !isalnum(static_cast<unsigned char>(productName[pos + 10])));
|
||||||
|
|
||||||
|
if (startBoundary && endBoundary) {
|
||||||
|
productName.replace(pos, 10, "Windows 11");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
debug_log("Warning: Could not get build number via WinRT; Win11 detection might be inaccurate.");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
debug_log("Warning: Could not get build number via WinRT; Win11 detection might be inaccurate.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayVersion.empty() ? productName : productName + " " + displayVersion;
|
return displayVersion.empty() ? productName : productName + " " + displayVersion;
|
||||||
} catch (const std::exception& e) { return Err(DraconisError(e)); }
|
} catch (const std::exception& e) { return Err(DraconisError(e)); }
|
||||||
}
|
|
||||||
|
|
||||||
fn os::GetHost() -> Result<String, DraconisError> {
|
|
||||||
return GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SYSTEM\HardwareConfig\Current)", "SystemFamily");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn os::GetKernelVersion() -> Result<String, DraconisError> {
|
|
||||||
try {
|
|
||||||
using namespace winrt::Windows::System::Profile;
|
|
||||||
|
|
||||||
const AnalyticsVersionInfo versionInfo = AnalyticsInfo::VersionInfo();
|
|
||||||
|
|
||||||
if (const winrt::hstring familyVersion = versionInfo.DeviceFamilyVersion(); !familyVersion.empty())
|
|
||||||
if (auto [major, minor, build, revision] = OSVersion::parseDeviceFamilyVersion(familyVersion); build > 0)
|
|
||||||
return std::format("{}.{}.{}.{}", major, minor, build, revision);
|
|
||||||
} catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); } catch (const Exception& e) {
|
|
||||||
return Err(DraconisError(e));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(DraconisError(DraconisErrorCode::NotFound, "Could not determine kernel version"));
|
fn GetHost() -> Result<String, DraconisError> {
|
||||||
}
|
return GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SYSTEM\HardwareConfig\Current)", "SystemFamily");
|
||||||
|
}
|
||||||
|
|
||||||
fn os::GetWindowManager() -> Option<String> {
|
fn GetKernelVersion() -> Result<String, DraconisError> {
|
||||||
if (const Result<Vec<Pair<DWORD, String>>, DraconisError> processInfoResult = GetProcessInfo()) {
|
if (const HMODULE ntdllHandle = GetModuleHandleW(L"ntdll.dll")) {
|
||||||
const Vec<Pair<DWORD, String>>& processInfo = *processInfoResult;
|
if (const auto rtlGetVersion = std::bit_cast<RtlGetVersionPtr>(GetProcAddress(ntdllHandle, "RtlGetVersion"))) {
|
||||||
|
RTL_OSVERSIONINFOW osInfo = {};
|
||||||
|
osInfo.dwOSVersionInfoSize = sizeof(osInfo);
|
||||||
|
|
||||||
Vec<String> processNames;
|
if (rtlGetVersion(&osInfo) == 0)
|
||||||
processNames.reserve(processInfo.size());
|
return std::format(
|
||||||
|
"{}.{}.{}.{}", osInfo.dwMajorVersion, osInfo.dwMinorVersion, osInfo.dwBuildNumber, osInfo.dwPlatformId
|
||||||
for (const String& val : processInfo | std::views::values) {
|
);
|
||||||
if (!val.empty()) {
|
|
||||||
const usize lastSlash = val.find_last_of("/\\");
|
|
||||||
processNames.push_back(lastSlash == String::npos ? val : val.substr(lastSlash + 1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::unordered_map<String, String> wmProcesses = {
|
return Err(DraconisError(DraconisErrorCode::NotFound, "Could not determine kernel version using RtlGetVersion"));
|
||||||
{ "glazewm.exe", "GlazeWM" },
|
|
||||||
{ "fancywm.exe", "FancyWM" },
|
|
||||||
{ "komorebi.exe", "Komorebi" },
|
|
||||||
{ "komorebic.exe", "Komorebi" },
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& [processExe, wmName] : wmProcesses)
|
|
||||||
if (IsProcessRunning(processNames, processExe))
|
|
||||||
return wmName;
|
|
||||||
} else {
|
|
||||||
error_log("Failed to get process info for WM detection: {}", processInfoResult.error().message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOL compositionEnabled = FALSE;
|
fn GetWindowManager() -> Option<String> {
|
||||||
|
BOOL compositionEnabled = FALSE;
|
||||||
if (SUCCEEDED(DwmIsCompositionEnabled(&compositionEnabled)))
|
if (SUCCEEDED(DwmIsCompositionEnabled(&compositionEnabled))) {
|
||||||
return compositionEnabled ? "DWM" : "Windows Manager (Basic)";
|
return compositionEnabled ? "DWM" : "Windows Manager (Basic)";
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn os::GetDesktopEnvironment() -> Option<String> {
|
|
||||||
const String buildStr =
|
|
||||||
GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "CurrentBuildNumber");
|
|
||||||
|
|
||||||
if (buildStr.empty()) {
|
|
||||||
debug_log("Failed to get CurrentBuildNumber from registry");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const i32 build = stoi(buildStr);
|
|
||||||
|
|
||||||
// Windows 11+ (Fluent)
|
|
||||||
if (build >= 22000)
|
|
||||||
return "Fluent (Windows 11)";
|
|
||||||
|
|
||||||
// Windows 10 Fluent Era
|
|
||||||
if (build >= 15063)
|
|
||||||
return "Fluent (Windows 10)";
|
|
||||||
|
|
||||||
// Windows 8.1/10 Metro Era
|
|
||||||
if (build >= 9200) { // Windows 8+
|
|
||||||
// Distinguish between Windows 8 and 10
|
|
||||||
const String productName =
|
|
||||||
GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "ProductName");
|
|
||||||
|
|
||||||
if (productName.find("Windows 10") != String::npos)
|
|
||||||
return "Metro (Windows 10)";
|
|
||||||
|
|
||||||
if (build >= 9600)
|
|
||||||
return "Metro (Windows 8.1)";
|
|
||||||
|
|
||||||
return "Metro (Windows 8)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows 7 Aero
|
error_log("GetWindowManager: DwmIsCompositionEnabled failed");
|
||||||
if (build >= 7600)
|
|
||||||
return "Aero (Windows 7)";
|
|
||||||
|
|
||||||
// Pre-Win7
|
|
||||||
return "Classic";
|
|
||||||
} catch (...) {
|
|
||||||
debug_log("Failed to parse CurrentBuildNumber");
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn os::GetShell() -> Option<String> {
|
fn GetDesktopEnvironment() -> Option<String> {
|
||||||
using util::helpers::GetEnv;
|
const String buildStr =
|
||||||
|
GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "CurrentBuildNumber");
|
||||||
|
|
||||||
try {
|
if (buildStr.empty()) {
|
||||||
const DWORD currentPid =
|
debug_log("Failed to get CurrentBuildNumber from registry");
|
||||||
winrt::Windows::System::Diagnostics::ProcessDiagnosticInfo::GetForCurrentProcess().ProcessId();
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const i32 build = stoi(buildStr);
|
||||||
|
|
||||||
|
// Windows 11+ (Fluent)
|
||||||
|
if (build >= 22000)
|
||||||
|
return "Fluent (Windows 11)";
|
||||||
|
|
||||||
|
// Windows 10 Fluent Era
|
||||||
|
if (build >= 15063)
|
||||||
|
return "Fluent (Windows 10)";
|
||||||
|
|
||||||
|
// Windows 8.1/10 Metro Era
|
||||||
|
if (build >= 9200) { // Windows 8+
|
||||||
|
// Distinguish between Windows 8 and 10
|
||||||
|
const String productName =
|
||||||
|
GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "ProductName");
|
||||||
|
|
||||||
|
if (productName.find("Windows 10") != String::npos)
|
||||||
|
return "Metro (Windows 10)";
|
||||||
|
|
||||||
|
if (build >= 9600)
|
||||||
|
return "Metro (Windows 8.1)";
|
||||||
|
|
||||||
|
return "Metro (Windows 8)";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows 7 Aero
|
||||||
|
if (build >= 7600)
|
||||||
|
return "Aero (Windows 7)";
|
||||||
|
|
||||||
|
// Pre-Win7
|
||||||
|
return "Classic";
|
||||||
|
} catch (...) {
|
||||||
|
debug_log("Failed to parse CurrentBuildNumber");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn GetShell() -> Option<String> {
|
||||||
|
using util::helpers::GetEnv;
|
||||||
|
|
||||||
if (const Result<String, DraconisError> msystemResult = GetEnv("MSYSTEM");
|
if (const Result<String, DraconisError> msystemResult = GetEnv("MSYSTEM");
|
||||||
msystemResult && !msystemResult->empty()) {
|
msystemResult && !msystemResult->empty()) {
|
||||||
String shellPath;
|
String shellPath;
|
||||||
|
|
||||||
if (const Result<String, DraconisError> shellResult = GetEnv("SHELL"); shellResult && !shellResult->empty())
|
if (const Result<String, DraconisError> shellResult = GetEnv("SHELL"); shellResult && !shellResult->empty()) {
|
||||||
shellPath = *shellResult;
|
shellPath = *shellResult;
|
||||||
else if (const Result<String, DraconisError> loginShellResult = GetEnv("LOGINSHELL");
|
} else if (const Result<String, DraconisError> loginShellResult = GetEnv("LOGINSHELL");
|
||||||
loginShellResult && !loginShellResult->empty())
|
loginShellResult && !loginShellResult->empty()) {
|
||||||
shellPath = *loginShellResult;
|
shellPath = *loginShellResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (!shellPath.empty()) {
|
if (!shellPath.empty()) {
|
||||||
const usize lastSlash = shellPath.find_last_of("\\/");
|
const usize lastSlash = shellPath.find_last_of("\\/");
|
||||||
String shellExe = (lastSlash != String::npos) ? shellPath.substr(lastSlash + 1) : shellPath;
|
String shellExe = (lastSlash != String::npos) ? shellPath.substr(lastSlash + 1) : shellPath;
|
||||||
|
std::ranges::transform(shellExe, shellExe.begin(), [](const u8 character) { return std::tolower(character); });
|
||||||
std::ranges::transform(shellExe, shellExe.begin(), [](const u8 character) {
|
|
||||||
return static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shellExe.ends_with(".exe"))
|
if (shellExe.ends_with(".exe"))
|
||||||
shellExe.resize(shellExe.length() - 4);
|
shellExe.resize(shellExe.length() - 4);
|
||||||
|
|
||||||
const auto iter =
|
const auto iter =
|
||||||
std::ranges::find_if(msysShellMap, [&](const auto& pair) { return StringView { shellExe } == pair.first; });
|
std::ranges::find_if(msysShellMap, [&](const auto& pair) { return StringView { shellExe } == pair.first; });
|
||||||
|
|
||||||
if (iter != std::ranges::end(msysShellMap))
|
if (iter != std::ranges::end(msysShellMap))
|
||||||
return String { iter->second };
|
return String { iter->second };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DWORD currentPid = GetCurrentProcessId();
|
||||||
if (const Option<String> msysShell = FindShellInProcessTree(currentPid, msysShellMap))
|
if (const Option<String> msysShell = FindShellInProcessTree(currentPid, msysShellMap))
|
||||||
return *msysShell;
|
return msysShell;
|
||||||
|
|
||||||
return "MSYS2 Environment";
|
return "MSYS2 Environment";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DWORD currentPid = GetCurrentProcessId();
|
||||||
if (const Option<String> windowsShell = FindShellInProcessTree(currentPid, windowsShellMap))
|
if (const Option<String> windowsShell = FindShellInProcessTree(currentPid, windowsShellMap))
|
||||||
return *windowsShell;
|
return windowsShell;
|
||||||
} catch (const winrt::hresult_error& e) {
|
|
||||||
error_log("WinRT error during shell detection: {}", winrt::to_string(e.message()));
|
|
||||||
} catch (const std::exception& e) { error_log("Standard exception during shell detection: {}", e.what()); }
|
|
||||||
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn os::GetDiskUsage() -> Result<DiskSpace, DraconisError> {
|
fn GetDiskUsage() -> Result<DiskSpace, DraconisError> {
|
||||||
ULARGE_INTEGER freeBytes, totalBytes;
|
ULARGE_INTEGER freeBytes, totalBytes;
|
||||||
|
|
||||||
if (GetDiskFreeSpaceExW(L"C:\\", nullptr, &totalBytes, &freeBytes))
|
if (GetDiskFreeSpaceExW(L"C:\\", nullptr, &totalBytes, &freeBytes))
|
||||||
return DiskSpace { .used_bytes = totalBytes.QuadPart - freeBytes.QuadPart, .total_bytes = totalBytes.QuadPart };
|
return DiskSpace { .used_bytes = totalBytes.QuadPart - freeBytes.QuadPart, .total_bytes = totalBytes.QuadPart };
|
||||||
|
|
||||||
return Err(DraconisError(util::error::DraconisErrorCode::NotFound, "Failed to get disk usage"));
|
return Err(DraconisError(util::error::DraconisErrorCode::NotFound, "Failed to get disk usage"));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn os::GetPackageCount() -> Result<u64, DraconisError> {
|
fn GetPackageCount() -> Result<u64, DraconisError> {
|
||||||
return Err(DraconisError(DraconisErrorCode::NotFound, "GetPackageCount not implemented"));
|
try {
|
||||||
}
|
return std::ranges::distance(winrt::Windows::Management::Deployment::PackageManager().FindPackagesForUser(L""));
|
||||||
|
} catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); }
|
||||||
|
}
|
||||||
|
} // namespace os
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue