From 9aa2e445372fb24489d22cd05b3989284053349c Mon Sep 17 00:00:00 2001 From: Mars Date: Sun, 11 May 2025 01:43:10 -0400 Subject: [PATCH] sucjkl --- meson.build | 100 ++++---- src/Config/Config.hpp | 14 +- src/Core/SystemData.cpp | 25 +- src/Services/Weather/MetNoService.cpp | 300 ++++++++++++++++++++++ src/Services/Weather/MetNoService.hpp | 16 ++ src/Services/Weather/OpenMeteoService.hpp | 4 +- 6 files changed, 397 insertions(+), 62 deletions(-) create mode 100644 src/Services/Weather/MetNoService.cpp create mode 100644 src/Services/Weather/MetNoService.hpp diff --git a/meson.build b/meson.build index 356fad0..142bf4b 100644 --- a/meson.build +++ b/meson.build @@ -4,8 +4,8 @@ project( 'draconis++', 'cpp', - version : '0.1.0', - default_options : [ + version: '0.1.0', + default_options: [ 'default_library=static', 'buildtype=debugoptimized', 'b_vscrt=mt', @@ -15,6 +15,11 @@ project( ], ) +add_project_arguments( + '-DDRACONISPLUSPLUS_VERSION="' + meson.project_version() + '"', + language: ['cpp', 'objcpp'], +) + cpp = meson.get_compiler('cpp') host_system = host_machine.system() @@ -35,13 +40,13 @@ common_warning_flags = [ ] common_cpp_flags = { - 'common' : [ + 'common': [ '-fno-strict-enums', '-fvisibility=hidden', '-fvisibility-inlines-hidden', '-std=c++26', ], - 'msvc' : [ + 'msvc': [ '-DNOMINMAX', '/MT', '/Zc:__cplusplus', '/Zc:preprocessor', @@ -49,20 +54,20 @@ common_cpp_flags = { '/external:anglebrackets', '/std:c++latest', ], - 'unix_extra' : '-march=native', - 'windows_extra' : '-DCURL_STATICLIB', + 'unix_extra': '-march=native', + 'windows_extra': '-DCURL_STATICLIB', } # Configure Objective-C++ for macOS if host_system == 'darwin' - add_languages('objcpp', native : false) + add_languages('objcpp', native: false) objcpp = meson.get_compiler('objcpp') objcpp_flags = common_warning_flags + [ '-std=c++26', '-fvisibility=hidden', '-fvisibility-inlines-hidden', ] - add_project_arguments(objcpp.get_supported_arguments(objcpp_flags), language : 'objcpp') + add_project_arguments(objcpp.get_supported_arguments(objcpp_flags), language: 'objcpp') endif # Apply C++ compiler arguments @@ -80,13 +85,13 @@ else endif endif -add_project_arguments(common_cpp_args, language : 'cpp') +add_project_arguments(common_cpp_args, language: 'cpp') # --------------------- # # Include Directories # # --------------------- # project_internal_includes = include_directories('src') -project_public_includes = include_directories('include', is_system : true) +project_public_includes = include_directories('include', is_system: true) # ------- # # Files # @@ -94,22 +99,23 @@ project_public_includes = include_directories('include', is_system : true) base_sources = files( 'src/Config/Config.cpp', 'src/Core/SystemData.cpp', + 'src/Services/PackageCounting.cpp', + 'src/Services/Weather/MetNoService.cpp', 'src/Services/Weather/OpenMeteoService.cpp', 'src/Services/Weather/OpenWeatherMapService.cpp', - 'src/Services/PackageCounting.cpp', 'src/UI/UI.cpp', 'src/main.cpp', ) platform_sources = { - 'darwin' : ['src/OS/macOS.cpp', 'src/OS/macOS/bridge.mm'], - 'dragonfly' : ['src/OS/bsd.cpp'], - 'freebsd' : ['src/OS/bsd.cpp'], - 'haiku' : ['src/OS/haiku.cpp'], - 'linux' : ['src/OS/linux.cpp'], - 'netbsd' : ['src/OS/bsd.cpp'], - 'serenity' : ['src/OS/serenity.cpp'], - 'windows' : ['src/OS/Windows.cpp'], + 'darwin': ['src/OS/macOS.cpp', 'src/OS/macOS/bridge.mm'], + 'dragonfly': ['src/OS/bsd.cpp'], + 'freebsd': ['src/OS/bsd.cpp'], + 'haiku': ['src/OS/haiku.cpp'], + 'linux': ['src/OS/linux.cpp'], + 'netbsd': ['src/OS/bsd.cpp'], + 'serenity': ['src/OS/serenity.cpp'], + 'windows': ['src/OS/Windows.cpp'], } sources = base_sources + files(platform_sources.get(host_system, [])) @@ -118,9 +124,9 @@ sources = base_sources + files(platform_sources.get(host_system, [])) # Dependencies Config # # --------------------- # common_deps = [ - dependency('libcurl', include_type : 'system', static : true), - dependency('tomlplusplus', include_type : 'system', static : true), - dependency('openssl', include_type : 'system', static : true, required : false), + dependency('libcurl', include_type: 'system', static: true), + dependency('tomlplusplus', include_type: 'system', static: true), + dependency('openssl', include_type: 'system', static: true, required: false), ] # Platform-specific dependencies @@ -131,8 +137,8 @@ if host_system == 'darwin' dependency('SQLiteCpp'), dependency( 'appleframeworks', - modules : ['foundation', 'mediaplayer', 'systemconfiguration'], - static : true, + modules: ['foundation', 'mediaplayer', 'systemconfiguration'], + static: true, ), dependency('iconv'), ] @@ -143,11 +149,11 @@ elif host_system == 'windows' ] elif host_system != 'serenity' and host_system != 'haiku' # Make dbus, x11, and wayland dependencies optional - dbus_dep = dependency('dbus-1', required : false) - xcb_dep = dependency('xcb', required : false) - xau_dep = dependency('xau', required : false) - xdmcp_dep = dependency('xdmcp', required : false) - wayland_dep = dependency('wayland-client', required : false) + dbus_dep = dependency('dbus-1', required: false) + xcb_dep = dependency('xcb', required: false) + xau_dep = dependency('xau', required: false) + xdmcp_dep = dependency('xdmcp', required: false) + wayland_dep = dependency('wayland-client', required: false) platform_deps += [ dependency('SQLiteCpp'), @@ -156,15 +162,15 @@ elif host_system != 'serenity' and host_system != 'haiku' if dbus_dep.found() platform_deps += dbus_dep - add_project_arguments('-DHAVE_DBUS', language : 'cpp') + add_project_arguments('-DHAVE_DBUS', language: 'cpp') endif if xcb_dep.found() and xau_dep.found() and xdmcp_dep.found() platform_deps += [xcb_dep, xau_dep, xdmcp_dep] - add_project_arguments('-DHAVE_XCB', language : 'cpp') + add_project_arguments('-DHAVE_XCB', language: 'cpp') endif if wayland_dep.found() platform_deps += wayland_dep - add_project_arguments('-DHAVE_WAYLAND', language : 'cpp') + add_project_arguments('-DHAVE_WAYLAND', language: 'cpp') endif endif @@ -172,28 +178,28 @@ endif ftxui_components = ['ftxui::screen', 'ftxui::dom', 'ftxui::component'] ftxui_dep = dependency( 'ftxui', - modules : ftxui_components, - include_type : 'system', - static : true, - required : false, + modules: ftxui_components, + include_type: 'system', + static: true, + required: false, ) if not ftxui_dep.found() ftxui_dep = declare_dependency( - dependencies : [ - dependency('ftxui-dom', fallback : ['ftxui', 'dom_dep']), - dependency('ftxui-screen', fallback : ['ftxui', 'screen_dep']), - dependency('ftxui-component', fallback : ['ftxui', 'component_dep']), + dependencies: [ + dependency('ftxui-dom', fallback: ['ftxui', 'dom_dep']), + dependency('ftxui-screen', fallback: ['ftxui', 'screen_dep']), + dependency('ftxui-component', fallback: ['ftxui', 'component_dep']), ], ) endif -glaze_dep = dependency('glaze', include_type : 'system', required : false) +glaze_dep = dependency('glaze', include_type: 'system', required: false) if not glaze_dep.found() cmake = import('cmake') glaze_proj = cmake.subproject('glaze') - glaze_dep = glaze_proj.dependency('glaze_glaze', include_type : 'system') + glaze_dep = glaze_proj.dependency('glaze_glaze', include_type: 'system') endif # Combine all dependencies @@ -219,9 +225,9 @@ endif executable( 'draconis++', sources, - include_directories : [project_internal_includes, project_public_includes], - objc_args : objc_args, - link_args : link_args, - dependencies : deps, - install : true, + include_directories: [project_internal_includes, project_public_includes], + objc_args: objc_args, + link_args: link_args, + dependencies: deps, + install: true, ) \ No newline at end of file diff --git a/src/Config/Config.hpp b/src/Config/Config.hpp index b4aa943..4c400f9 100644 --- a/src/Config/Config.hpp +++ b/src/Config/Config.hpp @@ -15,6 +15,7 @@ #include "Util/Env.hpp" #endif +#include "../Services/Weather/MetNoService.hpp" #include "../Services/Weather/OpenMeteoService.hpp" #include "../Services/Weather/OpenWeatherMapService.hpp" #include "Services/Weather.hpp" @@ -155,8 +156,19 @@ struct Weather { error_log("OpenMeteo requires coordinates for location."); weather.enabled = false; } - } else { + } else if (provider == "metno") { + if (std::holds_alternative(weather.location)) { + const auto& coords = std::get(weather.location); + weather.service = std::make_unique(coords.lat, coords.lon, weather.units); + } else { + error_log("MetNo requires coordinates for location."); + weather.enabled = false; + } + } else if (provider == "openweathermap") { weather.service = std::make_unique(weather.location, weather.apiKey, weather.units); + } else { + error_log("Unknown weather provider: {}", provider); + weather.enabled = false; } } diff --git a/src/Core/SystemData.cpp b/src/Core/SystemData.cpp index e9a7350..d6eee9d 100644 --- a/src/Core/SystemData.cpp +++ b/src/Core/SystemData.cpp @@ -68,22 +68,23 @@ namespace { namespace os { SystemData::SystemData(const Config& config) { - using enum std::launch; using package::GetTotalCount; using util::types::Future, util::types::Err; + using weather::WeatherReport; + using enum std::launch; using enum util::error::DracErrorCode; - Future> hostFut = std::async(async, GetHost); - Future> kernelFut = std::async(async, GetKernelVersion); - Future> osFut = std::async(async, GetOSVersion); - Future> memFut = std::async(async, GetMemInfo); - Future> deFut = std::async(async, GetDesktopEnvironment); - Future> wmFut = std::async(async, GetWindowManager); - Future> diskFut = std::async(async, GetDiskUsage); - Future> shellFut = std::async(async, GetShell); - Future> pkgFut = std::async(async, GetTotalCount); - Future> npFut = std::async(config.nowPlaying.enabled ? async : deferred, GetNowPlaying); - Future> wthrFut = std::async(config.weather.enabled ? async : deferred, [&config]() -> Result { + Future> hostFut = std::async(async, GetHost); + Future> kernelFut = std::async(async, GetKernelVersion); + Future> osFut = std::async(async, GetOSVersion); + Future> memFut = std::async(async, GetMemInfo); + Future> deFut = std::async(async, GetDesktopEnvironment); + Future> wmFut = std::async(async, GetWindowManager); + Future> diskFut = std::async(async, GetDiskUsage); + Future> shellFut = std::async(async, GetShell); + Future> pkgFut = std::async(async, GetTotalCount); + Future> npFut = std::async(config.nowPlaying.enabled ? async : deferred, GetNowPlaying); + Future> wthrFut = std::async(config.weather.enabled ? async : deferred, [&config]() -> Result { return config.weather.enabled && config.weather.service ? config.weather.service->getWeatherInfo() : Err(DracError(ApiUnavailable, "Weather API disabled")); diff --git a/src/Services/Weather/MetNoService.cpp b/src/Services/Weather/MetNoService.cpp new file mode 100644 index 0000000..6703b77 --- /dev/null +++ b/src/Services/Weather/MetNoService.cpp @@ -0,0 +1,300 @@ +#define NOMINMAX + +#include "MetNoService.hpp" + +#include // std::chrono::{system_clock, minutes, seconds} +#include // CURL, CURLcode, CURLOPT_*, CURLE_OK +#include // curl_easy_init, curl_easy_setopt, curl_easy_perform, curl_easy_strerror, curl_easy_cleanup +#include // std::format +#include // glz::read +#include // std::istringstream +#include // std::unordered_map + +#include "Util/Caching.hpp" +#include "Util/Error.hpp" +#include "Util/Types.hpp" + +using weather::MetNoService; +using weather::WeatherReport; + +namespace weather { + using util::types::f64, util::types::i32, util::types::String, util::types::usize, util::logging::Option; + + struct MetNoTimeseriesDetails { + f64 airTemperature; + }; + + struct MetNoTimeseriesNext1hSummary { + String symbolCode; + }; + + struct MetNoTimeseriesNext1h { + MetNoTimeseriesNext1hSummary summary; + }; + + struct MetNoTimeseriesInstant { + MetNoTimeseriesDetails details; + }; + + struct MetNoTimeseriesData { + MetNoTimeseriesInstant instant; + Option next1Hours; + }; + + struct MetNoTimeseries { + String time; + MetNoTimeseriesData data; + }; + + struct MetNoProperties { + Vec timeseries; + }; + + struct MetNoResponse { + MetNoProperties properties; + }; + + struct MetNoTimeseriesDetailsGlaze { + using T = MetNoTimeseriesDetails; + + static constexpr auto value = glz::object("air_temperature", &T::airTemperature); + }; + + struct MetNoTimeseriesNext1hSummaryGlaze { + using T = MetNoTimeseriesNext1hSummary; + + static constexpr auto value = glz::object("symbol_code", &T::symbolCode); + }; + + struct MetNoTimeseriesNext1hGlaze { + using T = MetNoTimeseriesNext1h; + + static constexpr auto value = glz::object("summary", &T::summary); + }; + + struct MetNoTimeseriesInstantGlaze { + using T = MetNoTimeseriesInstant; + static constexpr auto value = glz::object("details", &T::details); + }; + + struct MetNoTimeseriesDataGlaze { + using T = MetNoTimeseriesData; + + // clang-format off + static constexpr auto value = glz::object( + "instant", &T::instant, + "next_1_hours", &T::next1Hours + ); + // clang-format on + }; + + struct MetNoTimeseriesGlaze { + using T = MetNoTimeseries; + + // clang-format off + static constexpr auto value = glz::object( + "time", &T::time, + "data", &T::data + ); + // clang-format on + }; + + struct MetNoPropertiesGlaze { + using T = MetNoProperties; + + static constexpr auto value = glz::object("timeseries", &T::timeseries); + }; + + struct MetNoResponseGlaze { + using T = MetNoResponse; + + static constexpr auto value = glz::object("properties", &T::properties); + }; +} // namespace weather + +template <> +struct glz::meta : weather::MetNoTimeseriesDetailsGlaze {}; +template <> +struct glz::meta : weather::MetNoTimeseriesNext1hSummaryGlaze {}; +template <> +struct glz::meta : weather::MetNoTimeseriesNext1hGlaze {}; +template <> +struct glz::meta : weather::MetNoTimeseriesInstantGlaze {}; +template <> +struct glz::meta : weather::MetNoTimeseriesDataGlaze {}; +template <> +struct glz::meta : weather::MetNoTimeseriesGlaze {}; +template <> +struct glz::meta : weather::MetNoPropertiesGlaze {}; +template <> +struct glz::meta : weather::MetNoResponseGlaze {}; + +namespace { + using glz::opts; + using util::error::DracError, util::error::DracErrorCode; + using util::types::usize, util::types::Err, util::types::String; + + constexpr opts glazeOpts = { .error_on_unknown_keys = false }; + + fn SYMBOL_DESCRIPTIONS() -> const std::unordered_map& { + static const std::unordered_map MAP = { + { "clearsky_day", "clear sky" }, + { "clearsky_night", "clear sky" }, + { "clearsky_polartwilight", "clear sky" }, + { "cloudy", "cloudy" }, + { "fair_day", "fair" }, + { "fair_night", "fair" }, + { "fair_polartwilight", "fair" }, + { "fog", "fog" }, + { "heavyrain", "heavy rain" }, + { "heavyrainandthunder", "heavy rain and thunder" }, + { "heavyrainshowers_day", "heavy rain showers" }, + { "heavyrainshowers_night", "heavy rain showers" }, + { "heavyrainshowers_polartwilight", "heavy rain showers" }, + { "heavysleet", "heavy sleet" }, + { "heavysleetandthunder", "heavy sleet and thunder" }, + { "heavysleetshowers_day", "heavy sleet showers" }, + { "heavysleetshowers_night", "heavy sleet showers" }, + { "heavysleetshowers_polartwilight", "heavy sleet showers" }, + { "heavysnow", "heavy snow" }, + { "heavysnowandthunder", "heavy snow and thunder" }, + { "heavysnowshowers_day", "heavy snow showers" }, + { "heavysnowshowers_night", "heavy snow showers" }, + { "heavysnowshowers_polartwilight", "heavy snow showers" }, + { "lightrain", "light rain" }, + { "lightrainandthunder", "light rain and thunder" }, + { "lightrainshowers_day", "light rain showers" }, + { "lightrainshowers_night", "light rain showers" }, + { "lightrainshowers_polartwilight", "light rain showers" }, + { "lightsleet", "light sleet" }, + { "lightsleetandthunder", "light sleet and thunder" }, + { "lightsleetshowers_day", "light sleet showers" }, + { "lightsleetshowers_night", "light sleet showers" }, + { "lightsleetshowers_polartwilight", "light sleet showers" }, + { "lightsnow", "light snow" }, + { "lightsnowandthunder", "light snow and thunder" }, + { "lightsnowshowers_day", "light snow showers" }, + { "lightsnowshowers_night", "light snow showers" }, + { "lightsnowshowers_polartwilight", "light snow showers" }, + { "partlycloudy_day", "partly cloudy" }, + { "partlycloudy_night", "partly cloudy" }, + { "partlycloudy_polartwilight", "partly cloudy" }, + { "rain", "rain" }, + { "rainandthunder", "rain and thunder" }, + { "rainshowers_day", "rain showers" }, + { "rainshowers_night", "rain showers" }, + { "rainshowers_polartwilight", "rain showers" }, + { "sleet", "sleet" }, + { "sleetandthunder", "sleet and thunder" }, + { "sleetshowers_day", "sleet showers" }, + { "sleetshowers_night", "sleet showers" }, + { "sleetshowers_polartwilight", "sleet showers" }, + { "snow", "snow" }, + { "snowandthunder", "snow and thunder" }, + { "snowshowers_day", "snow showers" }, + { "snowshowers_night", "snow showers" }, + { "snowshowers_polartwilight", "snow showers" }, + { "unknown", "unknown" } + }; + + return MAP; + } + + fn WriteCallback(void* contents, usize size, usize nmemb, String* str) -> usize { + usize totalSize = size * nmemb; + str->append(static_cast(contents), totalSize); + return totalSize; + } + + fn parse_iso8601_to_epoch(const String& iso8601) -> usize { + std::tm time = {}; + std::istringstream stream(iso8601); + + stream >> std::get_time(&time, "%Y-%m-%dT%H:%M:%SZ"); + + if (stream.fail()) + return 0; + +#ifdef _WIN32 + return static_cast(_mkgmtime(&time)); +#else + return static_cast(timegm(&time)); +#endif + } +} // namespace + +MetNoService::MetNoService(f64 lat, f64 lon, String units) + : m_lat(lat), m_lon(lon), m_units(std::move(units)) {} + +fn MetNoService::getWeatherInfo() const -> util::types::Result { + using glz::error_ctx, glz::error_code, glz::read, glz::format_error; + using util::cache::ReadCache, util::cache::WriteCache; + using util::types::String, util::types::Result, util::types::None; + + if (Result data = ReadCache("weather")) { + using std::chrono::system_clock, std::chrono::minutes, std::chrono::seconds; + + const WeatherReport& dataVal = *data; + + if (const auto cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.timestamp)); cacheAge < minutes(60)) + return dataVal; + } + + String url = std::format("https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={:.4f}&lon={:.4f}", m_lat, m_lon); + + CURL* curl = curl_easy_init(); + + if (!curl) + return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to initialize cURL")); + + String responseBuffer; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBuffer); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "draconisplusplus/" DRACONISPLUSPLUS_VERSION " git.pupbrained.xyz/draconisplusplus"); + + CURLcode res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) + return Err(DracError(DracErrorCode::ApiUnavailable, std::format("cURL error: {}", curl_easy_strerror(res)))); + + weather::MetNoResponse apiResp {}; + + if (error_ctx errc = read(apiResp, responseBuffer); errc) + return Err(DracError(DracErrorCode::ParseError, "Failed to parse met.no JSON response")); + + if (apiResp.properties.timeseries.empty()) + return Err(DracError(DracErrorCode::ParseError, "No timeseries data in met.no response")); + + const MetNoTimeseries& first = apiResp.properties.timeseries.front(); + + f64 temp = first.data.instant.details.airTemperature; + + if (m_units == "imperial") + temp = temp * 9.0 / 5.0 + 32.0; + + String symbolCode = first.data.next1Hours ? first.data.next1Hours->summary.symbolCode : ""; + String description = symbolCode; + + if (!symbolCode.empty()) { + auto iter = SYMBOL_DESCRIPTIONS().find(symbolCode); + + if (iter != SYMBOL_DESCRIPTIONS().end()) + description = iter->second; + } + + WeatherReport out = { + .temperature = temp, + .name = None, + .description = description, + .timestamp = parse_iso8601_to_epoch(first.time), + }; + + if (Result<> writeResult = WriteCache("weather", out); !writeResult) + return Err(writeResult.error()); + + return out; +} \ No newline at end of file diff --git a/src/Services/Weather/MetNoService.hpp b/src/Services/Weather/MetNoService.hpp new file mode 100644 index 0000000..9a9ac5d --- /dev/null +++ b/src/Services/Weather/MetNoService.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "IWeatherService.hpp" + +namespace weather { + class MetNoService : public IWeatherService { + public: + MetNoService(f64 lat, f64 lon, String units = "metric"); + [[nodiscard]] fn getWeatherInfo() const -> Result override; + + private: + f64 m_lat; + f64 m_lon; + String m_units; + }; +} // namespace weather \ No newline at end of file diff --git a/src/Services/Weather/OpenMeteoService.hpp b/src/Services/Weather/OpenMeteoService.hpp index 58db75a..af7bd65 100644 --- a/src/Services/Weather/OpenMeteoService.hpp +++ b/src/Services/Weather/OpenMeteoService.hpp @@ -9,8 +9,8 @@ namespace weather { [[nodiscard]] fn getWeatherInfo() const -> Result override; private: - double m_lat; - double m_lon; + f64 m_lat; + f64 m_lon; String m_units; }; } // namespace weather \ No newline at end of file