From a8e175a5f9a20274b568a02a0625a5225ec71aaf Mon Sep 17 00:00:00 2001 From: Mars Date: Thu, 3 Apr 2025 22:37:51 -0400 Subject: [PATCH] i should really work on this more --- src/config/config.cpp | 164 +++++++++++++++++++++++++++++++++++++---- src/config/config.h | 4 +- src/config/weather.cpp | 44 +++++------ src/config/weather.h | 2 - src/main.cpp | 8 +- src/os/windows.cpp | 48 ++++++------ src/util/macros.h | 1 - src/util/types.h | 35 ++++++++- subprojects/glaze | 2 +- 9 files changed, 230 insertions(+), 78 deletions(-) diff --git a/src/config/config.cpp b/src/config/config.cpp index c689944..9c3a3b2 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include "config.h" @@ -11,34 +12,165 @@ namespace fs = std::filesystem; namespace { fn GetConfigPath() -> fs::path { + std::vector possiblePaths; + #ifdef _WIN32 - char* rawPtr = nullptr; - size_t bufferSize = 0; - if (_dupenv_s(&rawPtr, &bufferSize, "LOCALAPPDATA") != 0 || !rawPtr) - throw std::runtime_error("LOCALAPPDATA env var not found"); - std::unique_ptr localAppData(rawPtr, free); - return fs::path(localAppData.get()) / "draconis++" / "config.toml"; + // Windows possible paths in order of preference + if (auto result = GetEnv("LOCALAPPDATA"); result) + possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml"); + + if (auto result = GetEnv("USERPROFILE"); result) { + // Support for .config style on Windows (some users prefer this) + possiblePaths.push_back(fs::path(*result) / ".config" / "draconis++" / "config.toml"); + // Traditional Windows location alternative + possiblePaths.push_back(fs::path(*result) / "AppData" / "Local" / "draconis++" / "config.toml"); + } + + if (auto result = GetEnv("APPDATA"); result) + possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml"); + + // Portable app option - config in same directory as executable + possiblePaths.push_back(fs::path(".") / "config.toml"); #else - const char* home = std::getenv("HOME"); - if (!home) - throw std::runtime_error("HOME env var not found"); - return fs::path(home) / ".config" / "draconis++" / "config.toml"; + // Unix/Linux paths in order of preference + if (auto result = getEnv("XDG_CONFIG_HOME"); result) + possiblePaths.push_back(fs::path(*result) / "draconis++" / "config.toml"); + + if (auto result = getEnv("HOME"); result) { + possiblePaths.push_back(fs::path(*result) / ".config" / "draconis++" / "config.toml"); + possiblePaths.push_back(fs::path(*result) / ".draconis++" / "config.toml"); + } + + // System-wide config + possiblePaths.push_back(fs::path("/etc/draconis++/config.toml")); #endif + + // Check if any of these configs already exist + for (const auto& path : possiblePaths) + if (std::error_code errc; exists(path, errc) && !errc) + return path; + + // If no config exists yet, return the default (first in priority) + if (!possiblePaths.empty()) { + // Create directory structure for the default path + const fs::path defaultDir = possiblePaths[0].parent_path(); + + if (std::error_code errc; !exists(defaultDir, errc) && !errc) { + create_directories(defaultDir, errc); + if (errc) + WARN_LOG("Warning: Failed to create config directory: {}", errc.message()); + } + + return possiblePaths[0]; + } + + // Ultimate fallback if somehow we have no paths + throw std::runtime_error("Could not determine a valid config path"); + } + + fn CreateDefaultConfig(const fs::path& configPath) -> bool { + try { + // Ensure the directory exists + std::error_code errc; + create_directories(configPath.parent_path(), errc); + if (errc) { + ERROR_LOG("Failed to create config directory: {}", errc.message()); + return false; + } + + // Create a default TOML document + toml::table root; + + // Get default username for General section + std::string defaultName; +#ifdef _WIN32 + std::array username; + DWORD size = sizeof(username); + defaultName = GetUserNameA(username.data(), &size) ? username.data() : "User"; +#else + if (struct passwd* pwd = getpwuid(getuid()); pwd) + defaultName = pwd->pw_name; + else if (const char* envUser = getenv("USER")) + defaultName = envUser; + else + defaultName = "User"; +#endif + + // General section + toml::table* general = root.insert("general", toml::table {}).first->second.as_table(); + general->insert("name", defaultName); + + // Now Playing section + toml::table* nowPlaying = root.insert("now_playing", toml::table {}).first->second.as_table(); + nowPlaying->insert("enabled", false); + + // Weather section + 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"); + + // Write to file (using a stringstream for comments + TOML) + 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"; + + file << "# General settings\n"; + file << "[general]\n"; + file << "name = \"" << defaultName << "\" # Your display name\n\n"; + + 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"; + + INFO_LOG("Created default config file at {}", configPath.string()); + return true; + } catch (const std::exception& e) { + ERROR_LOG("Failed to create default config file: {}", e.what()); + return false; + } } } fn Config::getInstance() -> Config { try { const fs::path configPath = GetConfigPath(); - if (!fs::exists(configPath)) { - WARN_LOG("Config file not found, using defaults"); - return Config {}; + + // Check if the config file exists + if (!exists(configPath)) { + INFO_LOG("Config file not found, creating defaults at {}", configPath.string()); + + // Create default config + if (!CreateDefaultConfig(configPath)) { + WARN_LOG("Failed to create default config, using in-memory defaults"); + return {}; + } } - auto config = toml::parse_file(configPath.string()); - return Config::fromToml(config); + // Now we should have a config file to read + const toml::parse_result config = toml::parse_file(configPath.string()); + return fromToml(config); } catch (const std::exception& e) { DEBUG_LOG("Config loading failed: {}, using defaults", e.what()); - return Config {}; + return {}; } } diff --git a/src/config/config.h b/src/config/config.h index 209bd99..5b44c7d 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -34,7 +34,7 @@ struct General { static fn fromToml(const toml::table& tbl) -> General { General gen; - if (std::optional name = tbl["name"].value()) + if (const std::optional name = tbl["name"].value()) gen.name = *name; return gen; @@ -65,7 +65,7 @@ struct Weather { weather.api_key = tbl["api_key"].value().value_or(""); weather.units = tbl["units"].value().value_or("metric"); - if (auto location = tbl["location"]) { + if (const toml::node_view location = tbl["location"]) { if (location.is_string()) { weather.location = location.value().value(); } else if (location.is_table()) { diff --git a/src/config/weather.cpp b/src/config/weather.cpp index 9e04446..3ac3b94 100644 --- a/src/config/weather.cpp +++ b/src/config/weather.cpp @@ -13,13 +13,10 @@ namespace fs = std::filesystem; using namespace std::string_literals; -template -using Result = std::expected; - namespace { constexpr glz::opts glaze_opts = { .error_on_unknown_keys = false }; - fn GetCachePath() -> Result { + fn GetCachePath() -> std::expected { std::error_code errc; fs::path cachePath = fs::temp_directory_path(errc); @@ -30,8 +27,8 @@ namespace { return cachePath; } - fn ReadCacheFromFile() -> Result { - Result cachePath = GetCachePath(); + fn ReadCacheFromFile() -> std::expected { + std::expected cachePath = GetCachePath(); if (!cachePath) return std::unexpected(cachePath.error()); @@ -44,11 +41,10 @@ namespace { DEBUG_LOG("Reading from cache file..."); try { - const string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - WeatherOutput result; - glz::error_ctx errc = glz::read(result, content); + const string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + WeatherOutput result; - if (errc.ec != glz::error_code::none) + if (const glz::error_ctx errc = glz::read(result, content); errc.ec != glz::error_code::none) return std::unexpected("JSON parse error: " + glz::format_error(errc, content)); DEBUG_LOG("Successfully read from cache file."); @@ -56,8 +52,8 @@ namespace { } catch (const std::exception& e) { return std::unexpected("Error reading cache: "s + e.what()); } } - fn WriteCacheToFile(const WeatherOutput& data) -> Result { - Result cachePath = GetCachePath(); + fn WriteCacheToFile(const WeatherOutput& data) -> std::expected { + std::expected cachePath = GetCachePath(); if (!cachePath) return std::unexpected(cachePath.error()); @@ -72,10 +68,9 @@ namespace { if (!ofs.is_open()) return std::unexpected("Failed to open temp file: " + tempPath.string()); - string jsonStr; - glz::error_ctx errc = glz::write_json(data, jsonStr); + string jsonStr; - if (errc.ec != glz::error_code::none) + if (const glz::error_ctx errc = glz::write_json(data, jsonStr); errc.ec != glz::error_code::none) return std::unexpected("JSON serialization error: " + glz::format_error(errc, jsonStr)); ofs << jsonStr; @@ -101,7 +96,7 @@ namespace { return totalSize; } - fn MakeApiRequest(const string& url) -> const Result { + fn MakeApiRequest(const string& url) -> std::expected { DEBUG_LOG("Making API request to URL: {}", url); CURL* curl = curl_easy_init(); string responseBuffer; @@ -121,10 +116,9 @@ namespace { if (res != CURLE_OK) return std::unexpected(fmt::format("cURL error: {}", curl_easy_strerror(res))); - WeatherOutput output; - glz::error_ctx errc = glz::read(output, responseBuffer); + WeatherOutput output; - if (errc.ec != glz::error_code::none) + if (const glz::error_ctx errc = glz::read(output, responseBuffer); errc.ec != glz::error_code::none) return std::unexpected("API response parse error: " + glz::format_error(errc, responseBuffer)); return std::move(output); @@ -134,11 +128,11 @@ namespace { fn Weather::getWeatherInfo() const -> WeatherOutput { using namespace std::chrono; - if (Result data = ReadCacheFromFile()) { - const WeatherOutput& dataVal = *data; - const duration cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt)); + if (std::expected data = ReadCacheFromFile()) { + const WeatherOutput& dataVal = *data; - if (cacheAge < 10min) { + if (const duration cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt)); + cacheAge < 10min) { DEBUG_LOG("Using valid cache"); return dataVal; } @@ -148,13 +142,13 @@ fn Weather::getWeatherInfo() const -> WeatherOutput { DEBUG_LOG("Cache error: {}", data.error()); } - fn handleApiResult = [](const Result& result) -> WeatherOutput { + fn handleApiResult = [](const std::expected& result) -> WeatherOutput { if (!result) { ERROR_LOG("API request failed: {}", result.error()); return WeatherOutput {}; } - if (Result writeResult = WriteCacheToFile(*result); !writeResult) + if (std::expected writeResult = WriteCacheToFile(*result); !writeResult) ERROR_LOG("Failed to write cache: {}", writeResult.error()); return *result; diff --git a/src/config/weather.h b/src/config/weather.h index ee1e07e..459ecbe 100644 --- a/src/config/weather.h +++ b/src/config/weather.h @@ -5,7 +5,6 @@ #include "../util/types.h" // NOLINTBEGIN(readability-identifier-naming) - struct Condition { string description; @@ -40,5 +39,4 @@ struct WeatherOutput { static constexpr auto value = glz::object("main", &T::main, "name", &T::name, "weather", &T::weather, "dt", &T::dt); }; }; - // NOLINTEND(readability-identifier-naming) diff --git a/src/main.cpp b/src/main.cpp index 3c8be8e..aaa2c0f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -29,7 +28,7 @@ struct fmt::formatter : fmt::formatter { template constexpr fn format(const BytesToGiB& BTG, FmtCtx& ctx) const -> typename FmtCtx::iterator { // Format as double with GiB suffix, no space - return fmt::format_to(ctx.out(), "{:.2f}GiB", static_cast(BTG.value) / GIB); + return fmt::format_to(ctx.out(), "{:.2f}GiB", static_cast(BTG.value) / GIB); } }; @@ -265,9 +264,8 @@ namespace { // Now Playing row if (nowPlayingEnabled && data.now_playing.has_value()) { - const std::expected& nowPlayingResult = *data.now_playing; - - if (nowPlayingResult.has_value()) { + if (const std::expected& nowPlayingResult = *data.now_playing; + nowPlayingResult.has_value()) { const std::string& npText = *nowPlayingResult; content.push_back(separator() | color(borderColor)); diff --git a/src/os/windows.cpp b/src/os/windows.cpp index cf9e811..2865f2d 100644 --- a/src/os/windows.cpp +++ b/src/os/windows.cpp @@ -1,3 +1,4 @@ +#include #ifdef _WIN32 // clang-format off @@ -14,6 +15,8 @@ #include #include #include +#include +#include #include #include @@ -22,7 +25,7 @@ using std::string_view; using RtlGetVersionPtr = NTSTATUS(WINAPI*)(PRTL_OSVERSIONINFOW); -// NOLINTBEGIN(*-pro-type-cstyle-cast,*-no-int-to-ptr) +// NOLINTBEGIN(*-pro-type-cstyle-cast,*-no-int-to-ptr,*-pro-type-reinterpret-cast) namespace { class ProcessSnapshot { public: @@ -55,11 +58,11 @@ namespace { // Get first process if (Process32First(h_snapshot, &pe32)) { // Add first process to vector - processes.emplace_back(pe32.th32ProcessID, string(static_cast(pe32.szExeFile))); + processes.emplace_back(pe32.th32ProcessID, string(reinterpret_cast(pe32.szExeFile))); // Add remaining processes while (Process32Next(h_snapshot, &pe32)) - processes.emplace_back(pe32.th32ProcessID, string(static_cast(pe32.szExeFile))); + processes.emplace_back(pe32.th32ProcessID, string(reinterpret_cast(pe32.szExeFile))); } return processes; @@ -94,7 +97,7 @@ namespace { } fn GetProcessInfo() -> std::vector> { - ProcessSnapshot snapshot; + const ProcessSnapshot snapshot; return snapshot.isValid() ? snapshot.getProcesses() : std::vector> {}; } @@ -104,8 +107,8 @@ namespace { }); } - fn GetParentProcessId(DWORD pid) -> DWORD { - ProcessSnapshot snapshot; + fn GetParentProcessId(const DWORD pid) -> DWORD { + const ProcessSnapshot snapshot; if (!snapshot.isValid()) return 0; @@ -126,7 +129,7 @@ namespace { } fn GetProcessName(const DWORD pid) -> string { - ProcessSnapshot snapshot; + const ProcessSnapshot snapshot; if (!snapshot.isValid()) return ""; @@ -137,24 +140,24 @@ namespace { return ""; if (pe32.th32ProcessID == pid) - return { static_cast(pe32.szExeFile) }; + return reinterpret_cast(pe32.szExeFile); while (Process32Next(snapshot.h_snapshot, &pe32)) if (pe32.th32ProcessID == pid) - return { static_cast(pe32.szExeFile) }; + return reinterpret_cast(pe32.szExeFile); return ""; } } fn GetMemInfo() -> expected { - MEMORYSTATUSEX memInfo; - memInfo.dwLength = sizeof(MEMORYSTATUSEX); - - if (!GlobalMemoryStatusEx(&memInfo)) - return std::unexpected("Failed to get memory status"); - - return memInfo.ullTotalPhys; + try { + using namespace winrt::Windows::System::Diagnostics; + const SystemDiagnosticInfo diag = SystemDiagnosticInfo::GetForCurrentSystem(); + return diag.MemoryUsage().GetReport().TotalPhysicalSizeInBytes(); + } catch (const winrt::hresult_error& e) { + return std::unexpected("Failed to get memory info: " + to_string(e.message())); + } } fn GetNowPlaying() -> expected { @@ -185,11 +188,11 @@ fn GetNowPlaying() -> expected { fn GetOSVersion() -> expected { // First try using the native Windows API - OSVERSIONINFOEXW osvi = { sizeof(OSVERSIONINFOEXW), 0, 0, 0, 0, { 0 }, 0, 0, 0, 0, 0 }; - NTSTATUS status = 0; + constexpr OSVERSIONINFOEXW osvi = { sizeof(OSVERSIONINFOEXW), 0, 0, 0, 0, { 0 }, 0, 0, 0, 0, 0 }; + NTSTATUS status = 0; // Get RtlGetVersion function from ntdll.dll (not affected by application manifest) - if (HMODULE ntdllHandle = GetModuleHandleW(L"ntdll.dll")) + if (const HMODULE ntdllHandle = GetModuleHandleW(L"ntdll.dll")) if (const auto rtlGetVersion = std::bit_cast(GetProcAddress(ntdllHandle, "RtlGetVersion"))) status = rtlGetVersion(std::bit_cast(&osvi)); @@ -290,7 +293,7 @@ fn GetWindowManager() -> string { std::vector processNames; processNames.reserve(processInfo.size()); - for (const auto& [pid, name] : processInfo) processNames.push_back(name); + for (const auto& name : processInfo | std::views::values) processNames.push_back(name); // Check for third-party WMs using a map for cleaner code const std::unordered_map wmProcesses = { @@ -382,7 +385,6 @@ fn GetShell() -> string { size_t shellLen = 0; _dupenv_s(&shell, &shellLen, "SHELL"); const std::unique_ptr shellGuard(shell, free); - string shellExe; // If SHELL is empty, try LOGINSHELL if (!shell || strlen(shell) == 0) { @@ -394,6 +396,7 @@ fn GetShell() -> string { } if (shell) { + string shellExe; const string shellPath = shell; const size_t lastSlash = shellPath.find_last_of("\\/"); shellExe = (lastSlash != string::npos) ? shellPath.substr(lastSlash + 1) : shellPath; @@ -463,5 +466,6 @@ fn GetDiskUsage() -> std::pair { return { 0, 0 }; } -// NOLINTEND(*-pro-type-cstyle-cast,*-no-int-to-ptr) +// NOLINTEND(*-pro-type-cstyle-cast,*-no-int-to-ptr,*-pro-type-reinterpret-cast) + #endif diff --git a/src/util/macros.h b/src/util/macros.h index 526ebb1..96a6b35 100644 --- a/src/util/macros.h +++ b/src/util/macros.h @@ -1,7 +1,6 @@ #pragma once // probably stupid but it fixes the issue with windows.h defining ERROR -#include #undef ERROR #include diff --git a/src/util/types.h b/src/util/types.h index 18131b2..e9d5314 100644 --- a/src/util/types.h +++ b/src/util/types.h @@ -5,6 +5,8 @@ #include #ifdef _WIN32 +#include +// ReSharper disable once CppUnusedIncludeDirective #include #include #include @@ -149,13 +151,13 @@ enum class NowPlayingCode : u8 { * @brief Represents a Linux-specific error. */ using LinuxError = std::string; -#elifdef __APPLE__ +#elif defined(__APPLE__) /** * @typedef MacError * @brief Represents a macOS-specific error. */ using MacError = std::string; -#elifdef _WIN32 +#elif defined(_WIN32) /** * @typedef WindowsError * @brief Represents a Windows-specific error. @@ -168,9 +170,34 @@ using NowPlayingError = std::variant< NowPlayingCode, #ifdef __linux__ LinuxError -#elifdef __APPLE__ +#elif defined(__APPLE__) MacError -#elifdef _WIN32 +#elif defined(_WIN32) WindowsError #endif >; + +enum class EnvError : u8 { NotFound, AccessError }; + +inline auto GetEnv(const std::string& name) -> std::expected { +#ifdef _WIN32 + char* rawPtr = nullptr; + size_t bufferSize = 0; + + if (_dupenv_s(&rawPtr, &bufferSize, name.c_str()) != 0) + return std::unexpected(EnvError::AccessError); + + if (!rawPtr) + return std::unexpected(EnvError::NotFound); + + const std::string result(rawPtr); + free(rawPtr); + return result; +#else + const char* value = std::getenv(name.c_str()); + if (!value) + return std::unexpected(EnvError::NotFound); + + return std::string(value); +#endif +} diff --git a/subprojects/glaze b/subprojects/glaze index cfa3c58..bad0345 160000 --- a/subprojects/glaze +++ b/subprojects/glaze @@ -1 +1 @@ -Subproject commit cfa3c5838cc04e2f179faddf8e4757f90fa5fbe7 +Subproject commit bad0345d6358a649d5f72e90ada2be75d04b75cd