diff --git a/meson.build b/meson.build index a8c3f57..7406f84 100644 --- a/meson.build +++ b/meson.build @@ -73,7 +73,7 @@ endif if cpp.get_id() in ['msvc', 'clang-cl'] common_cpp_args = common_cpp_flags['msvc'] 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 else common_cpp_args = common_warning_flags + common_cpp_flags['common'] + common_cpp_flags['unix_extra'] diff --git a/src/config/config.cpp b/src/config/config.cpp index e2d8e51..0ecec8c 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -7,12 +7,36 @@ #include // toml::{parse_file, parse_result} #include // std::pair (Pair) +#include "src/core/util/helpers.hpp" #include "src/core/util/logging.hpp" namespace fs = std::filesystem; namespace { 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 { using util::helpers::GetEnv; @@ -30,8 +54,6 @@ namespace { if (auto result = GetEnv("APPDATA")) possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml"); - - possiblePaths.push_back(fs::path(".") / "config.toml"); #else if (Result result = GetEnv("XDG_CONFIG_HOME")) 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) / ".draconis++" / "config.toml"); } - - possiblePaths.emplace_back("/etc/draconis++/config.toml"); #endif + possiblePaths.push_back(fs::path(".") / "config.toml"); + 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; if (!possiblePaths.empty()) { 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); if (errc) warn_log("Warning: Failed to create config directory: {}", errc.message()); @@ -60,26 +82,33 @@ namespace { 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 { try { std::error_code errc; create_directories(configPath.parent_path(), errc); + if (errc) { error_log("Failed to create config directory: {}", errc.message()); return false; } - toml::table root; + String defaultName; #ifdef _WIN32 Array 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 const passwd* pwd = getpwuid(getuid()); CStr pwdName = pwd ? pwd->pw_name : nullptr; @@ -87,79 +116,91 @@ namespace { const Result envUser = util::helpers::GetEnv("USER"); const Result envLogname = util::helpers::GetEnv("LOGNAME"); - String defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User"; + defaultName = pwdName ? pwdName : envUser ? *envUser : envLogname ? *envLogname : "User"; #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); if (!file) { error_log("Failed to open config file for writing: {}", configPath.string()); 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"; - file << "[general]\n"; - file << "name = \"" << defaultName << "\" # Your display name\n\n"; + try { + const std::string fallbackConfig = std::format(defaultConfigTemplate, "User"); + file << fallbackConfig; + } catch (...) { + error_log("Failed to format default config even with fallback name."); + return false; + } + } - file << "# Now Playing integration\n"; - file << "[now_playing]\n"; - file << "enabled = false # Set to true to enable media integration\n\n"; - - 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"; + if (!file) { + error_log("Failed to write to config file: {}", configPath.string()); + return false; + } info_log("Created default config file at {}", configPath.string()); 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) { error_log("Failed to create default config file: {}", e.what()); return false; + } catch (...) { + error_log("An unexpected error occurred during default config creation."); + return false; } } } // 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 { try { const fs::path configPath = GetConfigPath(); - if (!exists(configPath)) { - info_log("Config file not found, creating defaults at {}", configPath.string()); + std::error_code ec; + + 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)) { - 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 {}; } } - 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()); - return fromToml(config); + return Config(config); } catch (const Exception& e) { debug_log("Config loading failed: {}, using defaults", e.what()); return {}; + } catch (...) { + error_log("An unexpected error occurred during config loading. Using in-memory defaults."); + return {}; } } diff --git a/src/config/config.hpp b/src/config/config.hpp index 42eb8a4..e5f136a 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -16,7 +16,7 @@ #include "src/core/util/defs.hpp" #include "src/core/util/error.hpp" -#include "src/core/util/helpers.hpp" +#include "src/core/util/logging.hpp" #include "src/core/util/types.hpp" #include "weather.hpp" @@ -132,8 +132,10 @@ struct Weather { .lat = *location.as_table()->get("lat")->value(), .lon = *location.as_table()->get("lon")->value(), }; - else - throw std::runtime_error("Invalid location type"); + else { + error_log("Invalid location format in config."); + weather.enabled = false; + } } return weather; @@ -159,22 +161,9 @@ struct Config { Weather weather; ///< Weather configuration settings.` NowPlaying now_playing; ///< Now Playing configuration settings. - /** - * @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"]; + Config() = default; - return { - .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 {}, - }; - } + explicit Config(const toml::table& tbl); /** * @brief Retrieves the path to the configuration file. diff --git a/src/core/system_data.cpp b/src/core/system_data.cpp index c63c4d8..25f5217 100644 --- a/src/core/system_data.cpp +++ b/src/core/system_data.cpp @@ -26,52 +26,59 @@ namespace { 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>(duration); + debug_log("{} took: {} ms", name, millis.count()); + }; + + template + fn time_execution(const std::string& name, Func&& func) { + const auto start = std::chrono::steady_clock::now(); + if constexpr (std::is_void_v) { + func(); + + const auto end = std::chrono::steady_clock::now(); + + log_timing(name, end - start); + } else { + auto result = func(); + + const auto end = std::chrono::steady_clock::now(); + log_timing(name, end - start); + + return result; + } + } } // namespace fn SystemData::fetchSystemData(const Config& config) -> SystemData { - using util::types::None; + using util::types::None, util::types::Exception; + using namespace os; SystemData data { - .date = GetDate(), - .host = os::GetHost(), - .kernel_version = os::GetKernelVersion(), - .os_version = os::GetOSVersion(), - .mem_info = os::GetMemInfo(), - .desktop_environment = os::GetDesktopEnvironment(), - .window_manager = os::GetWindowManager(), - .disk_usage = {}, - .shell = None, - .now_playing = None, - .weather_info = None, + .date = time_execution("GetDate", GetDate), + .host = time_execution("GetHost", GetHost), + .kernel_version = time_execution("GetKernelVersion", GetKernelVersion), + .os_version = time_execution("GetOSVersion", GetOSVersion), + .mem_info = time_execution("GetMemInfo", GetMemInfo), + .desktop_environment = time_execution("GetDesktopEnvironment", GetDesktopEnvironment), + .window_manager = time_execution("GetWindowManager", GetWindowManager), + .disk_usage = time_execution("GetDiskUsage", GetDiskUsage), + .shell = time_execution("GetShell", GetShell), }; - auto diskShellFuture = std::async(std::launch::async, [] { - Result diskResult = os::GetDiskUsage(); - Option shellOption = os::GetShell(); - return std::make_tuple(std::move(diskResult), std::move(shellOption)); - }); + if (const Result& nowPlayingResult = time_execution("GetNowPlaying", os::GetNowPlaying)) { + data.now_playing = nowPlayingResult; + } else { + data.now_playing = None; + } - std::future weatherFuture; - std::future> nowPlayingFuture; + const auto start = std::chrono::steady_clock::now(); + data.weather_info = config.weather.getWeatherInfo(); + const auto end = std::chrono::steady_clock::now(); - if (config.weather.enabled) - 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(); + log_timing("config.weather.getWeatherInfo", end - start); return data; } diff --git a/src/core/util/logging.hpp b/src/core/util/logging.hpp index 0eda5e6..f08b777 100644 --- a/src/core/util/logging.hpp +++ b/src/core/util/logging.hpp @@ -1,184 +1,63 @@ #pragma once -#include // std::chrono::{days, floor, seconds, system_clock} -#include // std::filesystem::path -#include // std::format -#include // std::print -#include // std::source_location -#include // std::{forward, make_pair} +#include // std::chrono::{days, floor, seconds, system_clock} +#include // std::filesystem::path +#include // std::format +#include // ftxui::Color +#include // std::mutex +#include // std::print +#include // std::source_location +#include // std::forward #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, types::None; + using types::usize, types::u8, types::i32, types::i64, types::CStr, types::String, types::StringView, types::Array, + types::Option, types::None; - /** - * @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. + // Store all compile-time constants in a struct + struct LogLevelConst { + // ANSI color codes + // clang-format off + static constexpr Array COLOR_CODE_LITERALS = { + "\033[38;5;0m", "\033[38;5;1m", "\033[38;5;2m", "\033[38;5;3m", + "\033[38;5;4m", "\033[38;5;5m", "\033[38;5;6m", "\033[38;5;7m", + "\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", }; + // clang-format on - /** - * @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. - }; + // ANSI formatting constants + static constexpr const char* RESET_CODE = "\033[0m"; + static constexpr const char* BOLD_START = "\033[1m"; + static constexpr const char* BOLD_END = "\033[22m"; + static constexpr const char* ITALIC_START = "\033[3m"; + static constexpr const char* ITALIC_END = "\033[23m"; - /** - * @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(static_cast(emphA) | static_cast(emphB)); - } + // Log level text constants + static constexpr StringView DEBUG_STR = "DEBUG"; + static constexpr StringView INFO_STR = "INFO "; + static constexpr StringView WARN_STR = "WARN "; + static constexpr StringView ERROR_STR = "ERROR"; - /** - * @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(emphA) & static_cast(emphB); - } + // Log level color constants + static constexpr ftxui::Color::Palette16 DEBUG_COLOR = ftxui::Color::Palette16::Cyan; + static constexpr ftxui::Color::Palette16 INFO_COLOR = ftxui::Color::Palette16::Green; + static constexpr ftxui::Color::Palette16 WARN_COLOR = ftxui::Color::Palette16::Yellow; + static constexpr ftxui::Color::Palette16 ERROR_COLOR = ftxui::Color::Palette16::Red; + static constexpr ftxui::Color::Palette16 DEBUG_INFO_COLOR = ftxui::Color::Palette16::GrayLight; - /** - * @struct Style - * @brief Represents a combination of text styles. - * - * Emphasis and color are both optional, allowing for flexible styling. - */ - struct Style { - Option emph; ///< Optional emphasis style. - Option fg_col; ///< Optional foreground color style. + static constexpr CStr TIMESTAMP_FORMAT = "{:%X}"; + static constexpr CStr LOG_FORMAT = "{} {} {}"; - /** - * @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(*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 - fn Print(const Style& style, std::format_string fmt, Args&&... args) -> void { - if (const String styleCode = style.ansiCode(); styleCode.empty()) - std::print(fmt, std::forward(args)...); - else - std::print("{}{}{}", styleCode, std::format(fmt, std::forward(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 - fn Print(const Color& fgColor, std::format_string fmt, Args&&... args) -> void { - Print({ .emph = None, .fg_col = fgColor }, fmt, std::forward(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 - fn Print(const Emphasis emph, std::format_string fmt, Args&&... args) -> void { - Print({ .emph = emph, .fg_col = None }, fmt, std::forward(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 - fn Print(std::format_string fmt, Args&&... args) -> void { - // Directly use std::print for unstyled output - std::print(fmt, std::forward(args)...); - } - } // namespace term +#ifndef NDEBUG + static constexpr CStr DEBUG_INFO_FORMAT = "{}{}{}\n"; + static constexpr CStr FILE_LINE_FORMAT = "{}:{}"; + static constexpr CStr DEBUG_LINE_PREFIX = " ╰── "; +#endif + }; /** * @enum LogLevel @@ -186,6 +65,73 @@ namespace util::logging { */ 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(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 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. * @tparam Args Parameter pack for format arguments. @@ -197,37 +143,45 @@ namespace util::logging { template fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_string fmt, Args&&... args) { using namespace std::chrono; - using namespace term; + using std::filesystem::path; - const auto [color, levelStr] = [&] { - 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(); - } - }(); + using Buffer = Array; - Print(Color::BrightWhite, "[{:%X}] ", std::chrono::floor(system_clock::now())); - Print(Emphasis::Bold | color, "{} ", levelStr); - Print(fmt, std::forward(args)...); + // Dynamic parts (runtime) + const time_point> now = floor(system_clock::now()); + + const String timestamp = std::format(LogLevelConst::TIMESTAMP_FORMAT, now); + const String message = std::format(fmt, std::forward(args)...); + + Buffer buffer {}; + + Buffer::iterator iter = std::format_to( + buffer.begin(), + LogLevelConst::LOG_FORMAT, + Colorize("[" + timestamp + "]", LogLevelConst::DEBUG_INFO_COLOR), + LEVEL_INFO[static_cast(level)], + message + ); #ifndef NDEBUG - Print(Color::BrightWhite, "\n{:>14} ", "╰──"); - Print( - Emphasis::Italic | Color::BrightWhite, - "{}:{}", - std::filesystem::path(loc.file_name()).lexically_normal().string(), - loc.line() + iter = std::format_to( + iter, + "\n{}", + Italic(Colorize( + LogLevelConst::DEBUG_LINE_PREFIX + + 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 - fn LogAppError(const LogLevel level, const ErrorType& error_obj) { + fn LogError(const LogLevel level, const ErrorType& error_obj) { using DecayedErrorType = std::decay_t; std::source_location logLocation; @@ -254,7 +208,7 @@ namespace util::logging { ::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); + #define debug_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Debug, error_obj); #else #define debug_log(...) ((void)0) #define debug_at(...) ((void)0) @@ -264,17 +218,17 @@ namespace util::logging { ::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 info_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Info, error_obj); #define warn_log(fmt, ...) \ ::util::logging::LogImpl( \ ::util::logging::LogLevel::Warn, std::source_location::current(), fmt __VA_OPT__(, ) __VA_ARGS__ \ ) -#define warn_at(error_obj) ::util::logging::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, ...) \ ::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); +#define error_at(error_obj) ::util::logging::LogError(::util::logging::LogLevel::Error, error_obj); } // namespace util::logging diff --git a/src/main.cpp b/src/main.cpp index 39aa8df..da779c0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,8 +20,6 @@ namespace ui { using ftxui::Color; using util::types::StringView, util::types::i32; - static constexpr inline StringView ICON_TYPE = "EMOJI"; - static constexpr i32 MAX_PARAGRAPH_LENGTH = 30; // Color themes @@ -55,7 +53,7 @@ namespace ui { StringView window_manager; }; - static constexpr Icons NONE = { + [[maybe_unused]] static constexpr Icons NONE = { .user = "", .palette = "", .calendar = "", @@ -71,7 +69,7 @@ namespace ui { .window_manager = "", }; - static constexpr Icons NERD = { + [[maybe_unused]] static constexpr Icons NERD = { .user = "  ", .palette = "  ", .calendar = "  ", @@ -87,7 +85,7 @@ namespace ui { .window_manager = "  ", }; - static constexpr Icons EMOJI = { + [[maybe_unused]] static constexpr Icons EMOJI = { .user = " 👤 ", .palette = " 🎨 ", .calendar = " 📅 ", @@ -99,9 +97,11 @@ namespace ui { .music = " 🎵 ", .disk = " 💾 ", .shell = " 💲 ", - .desktop = " 🖥 ️", + .desktop = " 🖥️ ", .window_manager = " 🪟 ", }; + + static constexpr inline Icons ICON_TYPE = NERD; } // namespace ui namespace { @@ -122,9 +122,7 @@ namespace { const Weather weather = config.weather; const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, deIcon, wmIcon] = - ui::ICON_TYPE == "NERD" ? ui::NERD - : ui::ICON_TYPE == "EMOJI" ? ui::EMOJI - : ui::NONE; + ui::ICON_TYPE; Elements content; @@ -264,6 +262,10 @@ namespace { } // namespace fn main() -> i32 { +#ifdef _WIN32 + winrt::init_apartment(); +#endif + const Config& config = Config::getInstance(); const SystemData data = SystemData::fetchSystemData(config); diff --git a/src/os/windows.cpp b/src/os/windows.cpp index 3748bc8..e26760c 100644 --- a/src/os/windows.cpp +++ b/src/os/windows.cpp @@ -5,14 +5,14 @@ #include #include #include +#include -#include #include #include #include #include -#include #include +#include #include #include @@ -24,33 +24,15 @@ #include "os.hpp" // clang-format on +using RtlGetVersionPtr = NTSTATUS(WINAPI*)(PRTL_OSVERSIONINFOW); + namespace { using util::error::DraconisError, util::error::DraconisErrorCode; using namespace util::types; - struct OSVersion { - u16 major; - u16 minor; - 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((versionUl >> 48) & 0xFFFF), - .minor = static_cast((versionUl >> 32) & 0xFFFF), - .build = static_cast((versionUl >> 16) & 0xFFFF), - .revision = static_cast(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 }; - } + struct ProcessData { + DWORD parentPid = 0; + String baseExeNameLower; }; // clang-format off @@ -91,74 +73,76 @@ namespace { return value; } - fn GetProcessInfo() -> Result>, DraconisError> { - try { - using namespace winrt::Windows::System::Diagnostics; - using namespace winrt::Windows::Foundation::Collections; - - const IVectorView processInfos = ProcessDiagnosticInfo::GetForProcesses(); - - Vec> 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& 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 fn FindShellInProcessTree(const DWORD startPid, const Array, sz>& shellMap) -> Option { if (startPid == 0) return None; - try { - using namespace winrt::Windows::System::Diagnostics; + std::unordered_map processMap; - ProcessDiagnosticInfo currentProcessInfo = nullptr; + const HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - try { - currentProcessInfo = ProcessDiagnosticInfo::TryGetForProcessId(startPid); - } catch (const winrt::hresult_error& e) { - 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(std::tolower(static_cast(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()); + if (hSnap == INVALID_HANDLE_VALUE) { + error_log("FindShellInProcessTree: Failed snapshot, error {}", GetLastError()); + return None; } + 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; } @@ -180,233 +164,207 @@ namespace { } } // namespace -fn os::GetMemInfo() -> Result { - try { - return winrt::Windows::System::Diagnostics::SystemDiagnosticInfo::GetForCurrentSystem() - .MemoryUsage() - .GetReport() - .TotalPhysicalSizeInBytes(); - } catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); } -} +namespace os { + fn GetMemInfo() -> Result { + MEMORYSTATUSEX memInfo; + memInfo.dwLength = sizeof(MEMORYSTATUSEX); -fn os::GetNowPlaying() -> Result { - using namespace winrt::Windows::Media::Control; - using namespace winrt::Windows::Foundation; + if (GlobalMemoryStatusEx(&memInfo)) + return memInfo.ullTotalPhys; - using Session = GlobalSystemMediaTransportControlsSession; - using SessionManager = GlobalSystemMediaTransportControlsSessionManager; - using MediaProperties = GlobalSystemMediaTransportControlsSessionMediaProperties; + DWORD lastError = GetLastError(); + return Err(DraconisError( + DraconisErrorCode::PlatformSpecific, std::format("GlobalMemoryStatusEx failed with error code {}", lastError) + )); + } - try { - const IAsyncOperation sessionManagerOp = SessionManager::RequestAsync(); - const SessionManager sessionManager = sessionManagerOp.get(); + fn GetNowPlaying() -> Result { + using namespace winrt::Windows::Media::Control; + using namespace winrt::Windows::Foundation; - if (const Session currentSession = sessionManager.GetCurrentSession()) { - const MediaProperties mediaProperties = currentSession.TryGetMediaPropertiesAsync().get(); + using Session = GlobalSystemMediaTransportControlsSession; + using SessionManager = GlobalSystemMediaTransportControlsSessionManager; + using MediaProperties = GlobalSystemMediaTransportControlsSessionMediaProperties; - return MediaInfo( - winrt::to_string(mediaProperties.Title()), winrt::to_string(mediaProperties.Artist()), None, None - ); - } + try { + const IAsyncOperation sessionManagerOp = SessionManager::RequestAsync(); + const SessionManager sessionManager = sessionManagerOp.get(); - return Err(DraconisError(DraconisErrorCode::NotFound, "No media session found")); - } catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); } -} + if (const Session currentSession = sessionManager.GetCurrentSession()) { + const MediaProperties mediaProperties = currentSession.TryGetMediaPropertiesAsync().get(); -fn os::GetOSVersion() -> Result { - try { - const String regSubKey = R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)"; + return MediaInfo( + winrt::to_string(mediaProperties.Title()), winrt::to_string(mediaProperties.Artist()), None, None + ); + } - String productName = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "ProductName"); - const String displayVersion = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "DisplayVersion"); + return Err(DraconisError(DraconisErrorCode::NotFound, "No media session found")); + } catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); } + } - if (productName.empty()) - return Err(DraconisError(DraconisErrorCode::NotFound, "ProductName not found in registry")); + fn GetOSVersion() -> Result { + try { + const String regSubKey = R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)"; - if (const Option 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(productName[pos - 1]))); - const bool endBoundary = - (pos + 10 == productName.length() || !isalnum(static_cast(productName[pos + 10]))); + String productName = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "ProductName"); + const String displayVersion = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "DisplayVersion"); - if (startBoundary && endBoundary) { - productName.replace(pos, 10, "Windows 11"); + if (productName.empty()) + return Err(DraconisError(DraconisErrorCode::NotFound, "ProductName not found in registry")); + + if (const Option 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(productName[pos - 1]))); + const bool endBoundary = + (pos + 10 == productName.length() || !isalnum(static_cast(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; - } catch (const std::exception& e) { return Err(DraconisError(e)); } -} - -fn os::GetHost() -> Result { - return GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SYSTEM\HardwareConfig\Current)", "SystemFamily"); -} - -fn os::GetKernelVersion() -> Result { - 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 displayVersion.empty() ? productName : productName + " " + displayVersion; + } catch (const std::exception& e) { return Err(DraconisError(e)); } } - return Err(DraconisError(DraconisErrorCode::NotFound, "Could not determine kernel version")); -} + fn GetHost() -> Result { + return GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SYSTEM\HardwareConfig\Current)", "SystemFamily"); + } -fn os::GetWindowManager() -> Option { - if (const Result>, DraconisError> processInfoResult = GetProcessInfo()) { - const Vec>& processInfo = *processInfoResult; + fn GetKernelVersion() -> Result { + if (const HMODULE ntdllHandle = GetModuleHandleW(L"ntdll.dll")) { + if (const auto rtlGetVersion = std::bit_cast(GetProcAddress(ntdllHandle, "RtlGetVersion"))) { + RTL_OSVERSIONINFOW osInfo = {}; + osInfo.dwOSVersionInfoSize = sizeof(osInfo); - Vec processNames; - processNames.reserve(processInfo.size()); - - 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)); + if (rtlGetVersion(&osInfo) == 0) + return std::format( + "{}.{}.{}.{}", osInfo.dwMajorVersion, osInfo.dwMinorVersion, osInfo.dwBuildNumber, osInfo.dwPlatformId + ); } } - const std::unordered_map wmProcesses = { - { "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); + return Err(DraconisError(DraconisErrorCode::NotFound, "Could not determine kernel version using RtlGetVersion")); } - BOOL compositionEnabled = FALSE; - - if (SUCCEEDED(DwmIsCompositionEnabled(&compositionEnabled))) - return compositionEnabled ? "DWM" : "Windows Manager (Basic)"; - - return None; -} - -fn os::GetDesktopEnvironment() -> Option { - 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)"; + fn GetWindowManager() -> Option { + BOOL compositionEnabled = FALSE; + if (SUCCEEDED(DwmIsCompositionEnabled(&compositionEnabled))) { + return compositionEnabled ? "DWM" : "Windows Manager (Basic)"; } - // Windows 7 Aero - if (build >= 7600) - return "Aero (Windows 7)"; + error_log("GetWindowManager: DwmIsCompositionEnabled failed"); - // Pre-Win7 - return "Classic"; - } catch (...) { - debug_log("Failed to parse CurrentBuildNumber"); return None; } -} -fn os::GetShell() -> Option { - using util::helpers::GetEnv; + fn GetDesktopEnvironment() -> Option { + const String buildStr = + GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "CurrentBuildNumber"); - try { - const DWORD currentPid = - winrt::Windows::System::Diagnostics::ProcessDiagnosticInfo::GetForCurrentProcess().ProcessId(); + 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 + if (build >= 7600) + return "Aero (Windows 7)"; + + // Pre-Win7 + return "Classic"; + } catch (...) { + debug_log("Failed to parse CurrentBuildNumber"); + return None; + } + } + + fn GetShell() -> Option { + using util::helpers::GetEnv; if (const Result msystemResult = GetEnv("MSYSTEM"); msystemResult && !msystemResult->empty()) { String shellPath; - if (const Result shellResult = GetEnv("SHELL"); shellResult && !shellResult->empty()) + if (const Result shellResult = GetEnv("SHELL"); shellResult && !shellResult->empty()) { shellPath = *shellResult; - else if (const Result loginShellResult = GetEnv("LOGINSHELL"); - loginShellResult && !loginShellResult->empty()) + } else if (const Result loginShellResult = GetEnv("LOGINSHELL"); + loginShellResult && !loginShellResult->empty()) { shellPath = *loginShellResult; + } if (!shellPath.empty()) { const usize lastSlash = shellPath.find_last_of("\\/"); String shellExe = (lastSlash != String::npos) ? shellPath.substr(lastSlash + 1) : shellPath; - - std::ranges::transform(shellExe, shellExe.begin(), [](const u8 character) { - return static_cast(std::tolower(static_cast(character))); - }); - + std::ranges::transform(shellExe, shellExe.begin(), [](const u8 character) { return std::tolower(character); }); if (shellExe.ends_with(".exe")) shellExe.resize(shellExe.length() - 4); const auto iter = std::ranges::find_if(msysShellMap, [&](const auto& pair) { return StringView { shellExe } == pair.first; }); - if (iter != std::ranges::end(msysShellMap)) return String { iter->second }; } + const DWORD currentPid = GetCurrentProcessId(); if (const Option msysShell = FindShellInProcessTree(currentPid, msysShellMap)) - return *msysShell; + return msysShell; return "MSYS2 Environment"; } + const DWORD currentPid = GetCurrentProcessId(); if (const Option windowsShell = FindShellInProcessTree(currentPid, windowsShellMap)) - 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 windowsShell; - return None; -} + return None; + } -fn os::GetDiskUsage() -> Result { - ULARGE_INTEGER freeBytes, totalBytes; + fn GetDiskUsage() -> Result { + ULARGE_INTEGER freeBytes, totalBytes; - if (GetDiskFreeSpaceExW(L"C:\\", nullptr, &totalBytes, &freeBytes)) - return DiskSpace { .used_bytes = totalBytes.QuadPart - freeBytes.QuadPart, .total_bytes = totalBytes.QuadPart }; + if (GetDiskFreeSpaceExW(L"C:\\", nullptr, &totalBytes, &freeBytes)) + 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 { - return Err(DraconisError(DraconisErrorCode::NotFound, "GetPackageCount not implemented")); -} + fn GetPackageCount() -> Result { + 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