This commit is contained in:
Mars 2025-05-11 01:43:10 -04:00
parent fc69565f4d
commit 9aa2e44537
6 changed files with 397 additions and 62 deletions

View file

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

View file

@ -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 if (provider == "metno") {
if (std::holds_alternative<weather::Coords>(weather.location)) {
const auto& coords = std::get<weather::Coords>(weather.location);
weather.service = std::make_unique<weather::MetNoService>(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::OpenWeatherMapService>(weather.location, weather.apiKey, weather.units);
} else {
error_log("Unknown weather provider: {}", provider);
weather.enabled = false;
}
}

View file

@ -68,9 +68,10 @@ 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<Result<String>> hostFut = std::async(async, GetHost);
@ -83,7 +84,7 @@ namespace os {
Future<Result<String>> shellFut = std::async(async, GetShell);
Future<Result<u64>> pkgFut = std::async(async, GetTotalCount);
Future<Result<MediaInfo>> npFut = std::async(config.nowPlaying.enabled ? async : deferred, GetNowPlaying);
Future<Result<weather::WeatherReport>> wthrFut = std::async(config.weather.enabled ? async : deferred, [&config]() -> Result<weather::WeatherReport> {
Future<Result<WeatherReport>> wthrFut = std::async(config.weather.enabled ? async : deferred, [&config]() -> Result<WeatherReport> {
return config.weather.enabled && config.weather.service
? config.weather.service->getWeatherInfo()
: Err(DracError(ApiUnavailable, "Weather API disabled"));

View file

@ -0,0 +1,300 @@
#define NOMINMAX
#include "MetNoService.hpp"
#include <chrono> // std::chrono::{system_clock, minutes, seconds}
#include <curl/curl.h> // CURL, CURLcode, CURLOPT_*, CURLE_OK
#include <curl/easy.h> // curl_easy_init, curl_easy_setopt, curl_easy_perform, curl_easy_strerror, curl_easy_cleanup
#include <format> // std::format
#include <glaze/json/read.hpp> // glz::read
#include <sstream> // std::istringstream
#include <unordered_map> // 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<MetNoTimeseriesNext1h> next1Hours;
};
struct MetNoTimeseries {
String time;
MetNoTimeseriesData data;
};
struct MetNoProperties {
Vec<MetNoTimeseries> 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::MetNoTimeseriesDetails> : weather::MetNoTimeseriesDetailsGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseriesNext1hSummary> : weather::MetNoTimeseriesNext1hSummaryGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseriesNext1h> : weather::MetNoTimeseriesNext1hGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseriesInstant> : weather::MetNoTimeseriesInstantGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseriesData> : weather::MetNoTimeseriesDataGlaze {};
template <>
struct glz::meta<weather::MetNoTimeseries> : weather::MetNoTimeseriesGlaze {};
template <>
struct glz::meta<weather::MetNoProperties> : weather::MetNoPropertiesGlaze {};
template <>
struct glz::meta<weather::MetNoResponse> : 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<String, String>& {
static const std::unordered_map<String, String> 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<char*>(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<util::types::usize>(_mkgmtime(&time));
#else
return static_cast<util::types::usize>(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<WeatherReport> {
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<WeatherReport> data = ReadCache<WeatherReport>("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<glazeOpts>(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;
}

View file

@ -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<WeatherReport> override;
private:
f64 m_lat;
f64 m_lon;
String m_units;
};
} // namespace weather

View file

@ -9,8 +9,8 @@ namespace weather {
[[nodiscard]] fn getWeatherInfo() const -> Result<WeatherReport> override;
private:
double m_lat;
double m_lon;
f64 m_lat;
f64 m_lon;
String m_units;
};
} // namespace weather