i should really work on this more

This commit is contained in:
Mars 2025-04-03 22:37:51 -04:00
parent c2536c361e
commit a8e175a5f9
9 changed files with 230 additions and 78 deletions

View file

@ -1,6 +1,7 @@
#include <cstdlib>
#include <filesystem>
#include <fmt/core.h>
#include <iostream>
#include <stdexcept>
#include "config.h"
@ -11,34 +12,165 @@ namespace fs = std::filesystem;
namespace {
fn GetConfigPath() -> fs::path {
std::vector<fs::path> 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<char, decltype(&free)> 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<char, 256> 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 {};
}
}

View file

@ -34,7 +34,7 @@ struct General {
static fn fromToml(const toml::table& tbl) -> General {
General gen;
if (std::optional<string> name = tbl["name"].value<string>())
if (const std::optional<string> name = tbl["name"].value<string>())
gen.name = *name;
return gen;
@ -65,7 +65,7 @@ struct Weather {
weather.api_key = tbl["api_key"].value<string>().value_or("");
weather.units = tbl["units"].value<string>().value_or("metric");
if (auto location = tbl["location"]) {
if (const toml::node_view<const toml::node> location = tbl["location"]) {
if (location.is_string()) {
weather.location = location.value<string>().value();
} else if (location.is_table()) {

View file

@ -13,13 +13,10 @@
namespace fs = std::filesystem;
using namespace std::string_literals;
template <typename T>
using Result = std::expected<T, string>;
namespace {
constexpr glz::opts glaze_opts = { .error_on_unknown_keys = false };
fn GetCachePath() -> Result<fs::path> {
fn GetCachePath() -> std::expected<fs::path, string> {
std::error_code errc;
fs::path cachePath = fs::temp_directory_path(errc);
@ -30,8 +27,8 @@ namespace {
return cachePath;
}
fn ReadCacheFromFile() -> Result<WeatherOutput> {
Result<fs::path> cachePath = GetCachePath();
fn ReadCacheFromFile() -> std::expected<WeatherOutput, string> {
std::expected<fs::path, string> 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<char>(ifs)), std::istreambuf_iterator<char>());
WeatherOutput result;
glz::error_ctx errc = glz::read<glaze_opts>(result, content);
const string content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
WeatherOutput result;
if (errc.ec != glz::error_code::none)
if (const glz::error_ctx errc = glz::read<glaze_opts>(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<void> {
Result<fs::path> cachePath = GetCachePath();
fn WriteCacheToFile(const WeatherOutput& data) -> std::expected<void, string> {
std::expected<fs::path, string> 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<WeatherOutput> {
fn MakeApiRequest(const string& url) -> std::expected<WeatherOutput, string> {
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<glaze_opts>(output, responseBuffer);
WeatherOutput output;
if (errc.ec != glz::error_code::none)
if (const glz::error_ctx errc = glz::read<glaze_opts>(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<WeatherOutput> data = ReadCacheFromFile()) {
const WeatherOutput& dataVal = *data;
const duration<double> cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt));
if (std::expected<WeatherOutput, string> data = ReadCacheFromFile()) {
const WeatherOutput& dataVal = *data;
if (cacheAge < 10min) {
if (const duration<double> 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<WeatherOutput>& result) -> WeatherOutput {
fn handleApiResult = [](const std::expected<WeatherOutput, string>& result) -> WeatherOutput {
if (!result) {
ERROR_LOG("API request failed: {}", result.error());
return WeatherOutput {};
}
if (Result<void> writeResult = WriteCacheToFile(*result); !writeResult)
if (std::expected<void, string> writeResult = WriteCacheToFile(*result); !writeResult)
ERROR_LOG("Failed to write cache: {}", writeResult.error());
return *result;

View file

@ -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)

View file

@ -2,7 +2,6 @@
#include <expected>
#include <fmt/chrono.h>
#include <fmt/color.h>
#include <fmt/core.h>
#include <fmt/format.h>
#include <ftxui/dom/elements.hpp>
#include <ftxui/screen/screen.hpp>
@ -29,7 +28,7 @@ struct fmt::formatter<BytesToGiB> : fmt::formatter<double> {
template <typename FmtCtx>
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<double>(BTG.value) / GIB);
return fmt::format_to(ctx.out(), "{:.2f}GiB", static_cast<f64>(BTG.value) / GIB);
}
};
@ -265,9 +264,8 @@ namespace {
// Now Playing row
if (nowPlayingEnabled && data.now_playing.has_value()) {
const std::expected<string, NowPlayingError>& nowPlayingResult = *data.now_playing;
if (nowPlayingResult.has_value()) {
if (const std::expected<string, NowPlayingError>& nowPlayingResult = *data.now_playing;
nowPlayingResult.has_value()) {
const std::string& npText = *nowPlayingResult;
content.push_back(separator() | color(borderColor));

View file

@ -1,3 +1,4 @@
#include <ranges>
#ifdef _WIN32
// clang-format off
@ -14,6 +15,8 @@
#include <guiddef.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Media.Control.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.System.Diagnostics.h>
#include <winrt/base.h>
#include <winrt/impl/Windows.Media.Control.2.h>
@ -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<const char*>(pe32.szExeFile)));
processes.emplace_back(pe32.th32ProcessID, string(reinterpret_cast<const char*>(pe32.szExeFile)));
// Add remaining processes
while (Process32Next(h_snapshot, &pe32))
processes.emplace_back(pe32.th32ProcessID, string(static_cast<const char*>(pe32.szExeFile)));
processes.emplace_back(pe32.th32ProcessID, string(reinterpret_cast<const char*>(pe32.szExeFile)));
}
return processes;
@ -94,7 +97,7 @@ namespace {
}
fn GetProcessInfo() -> std::vector<std::pair<DWORD, string>> {
ProcessSnapshot snapshot;
const ProcessSnapshot snapshot;
return snapshot.isValid() ? snapshot.getProcesses() : std::vector<std::pair<DWORD, string>> {};
}
@ -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<const char*>(pe32.szExeFile) };
return reinterpret_cast<const char*>(pe32.szExeFile);
while (Process32Next(snapshot.h_snapshot, &pe32))
if (pe32.th32ProcessID == pid)
return { static_cast<const char*>(pe32.szExeFile) };
return reinterpret_cast<const char*>(pe32.szExeFile);
return "";
}
}
fn GetMemInfo() -> expected<u64, string> {
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<string, NowPlayingError> {
@ -185,11 +188,11 @@ fn GetNowPlaying() -> expected<string, NowPlayingError> {
fn GetOSVersion() -> expected<string, string> {
// 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<RtlGetVersionPtr>(GetProcAddress(ntdllHandle, "RtlGetVersion")))
status = rtlGetVersion(std::bit_cast<PRTL_OSVERSIONINFOW>(&osvi));
@ -290,7 +293,7 @@ fn GetWindowManager() -> string {
std::vector<string> 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<string, string> wmProcesses = {
@ -382,7 +385,6 @@ fn GetShell() -> string {
size_t shellLen = 0;
_dupenv_s(&shell, &shellLen, "SHELL");
const std::unique_ptr<char, decltype(&free)> 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<u64, u64> {
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

View file

@ -1,7 +1,6 @@
#pragma once
// probably stupid but it fixes the issue with windows.h defining ERROR
#include <utility>
#undef ERROR
#include <filesystem>

View file

@ -5,6 +5,8 @@
#include <string>
#ifdef _WIN32
#include <expected>
// ReSharper disable once CppUnusedIncludeDirective
#include <guiddef.h>
#include <variant>
#include <winrt/base.h>
@ -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<std::string, EnvError> {
#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
}

@ -1 +1 @@
Subproject commit cfa3c5838cc04e2f179faddf8e4757f90fa5fbe7
Subproject commit bad0345d6358a649d5f72e90ada2be75d04b75cd