diff --git a/flake.nix b/flake.nix index 59ed070..45f412c 100644 --- a/flake.nix +++ b/flake.nix @@ -15,70 +15,88 @@ ... }: utils.lib.eachDefaultSystem ( - system: let - pkgs = import nixpkgs {inherit system;}; + system: + if system == "x86_64-linux" + then let + hostPkgs = import nixpkgs {inherit system;}; + muslPkgs = import nixpkgs { + system = "x86_64-linux-musl"; + overlays = [ + (self: super: { + mimalloc = super.mimalloc.overrideAttrs (oldAttrs: { + cmakeFlags = + (oldAttrs.cmakeFlags or []) + ++ [(self.lib.cmakeBool "MI_LIBC_MUSL" true)]; - llvmPackages = pkgs.llvmPackages_20; + postPatch = '' + sed -i '\||s|^|// |' src/prim/unix/prim.c + ''; + }); + }) + ]; + }; - stdenv = with pkgs; - ( - if hostPlatform.isLinux - then stdenvAdapters.useMoldLinker - else lib.id - ) - llvmPackages.stdenv; + llvmPackages = muslPkgs.llvmPackages_20; - sources = import ./_sources/generated.nix { - inherit (pkgs) fetchFromGitHub fetchgit fetchurl dockerTools; - }; + stdenv = + muslPkgs.stdenvAdapters.useMoldLinker + llvmPackages.libcxxStdenv; - dbus-cxx = stdenv.mkDerivation { - inherit (sources.dbus-cxx) pname version src; - nativeBuildInputs = with pkgs; [cmake pkg-config]; + glaze = (muslPkgs.glaze.override {inherit stdenv;}).overrideAttrs (oldAttrs: { + cmakeFlags = + (oldAttrs.cmakeFlags or []) + ++ [ + "-Dglaze_DEVELOPER_MODE=OFF" + "-Dglaze_BUILD_EXAMPLES=OFF" + ]; - buildInputs = with pkgs.pkgsStatic; [libsigcxx30]; + doCheck = false; - prePatch = '' - substituteInPlace CMakeLists.txt --replace "add_library( dbus-cxx SHARED" "add_library( dbus-cxx STATIC" - ''; - }; + enableAvx2 = stdenv.hostPlatform.isx86; + }); - deps = with pkgs; - [ - (glaze.override {enableAvx2 = hostPlatform.isx86;}) - ] - ++ (with pkgsStatic; [ - curl - ftxui - (tomlplusplus.overrideAttrs { - doCheck = false; - }) - ]) - ++ darwinPkgs - ++ linuxPkgs; + mkOverridden = buildSystem: pkg: ((pkg.override {inherit stdenv;}).overrideAttrs (oldAttrs: { + "${buildSystem}Flags" = + (oldAttrs."${buildSystem}Flags" or []) + ++ ( + if buildSystem == "meson" + then ["-Ddefault_library=static"] + else if buildSystem == "cmake" + then [ + "-D${hostPkgs.lib.toUpper pkg.pname}_BUILD_EXAMPLES=OFF" + "-D${hostPkgs.lib.toUpper pkg.pname}_BUILD_TESTS=OFF" + "-DBUILD_SHARED_LIBS=OFF" + ] + else throw "Invalid build system: ${buildSystem}" + ); + })); - darwinPkgs = nixpkgs.lib.optionals stdenv.isDarwin (with pkgs.pkgsStatic; [libiconv]); - - linuxPkgs = nixpkgs.lib.optionals stdenv.isLinux (with pkgs; - [ - valgrind - ] - ++ (with pkgsStatic; [ - dbus-cxx - libsigcxx30 - sqlitecpp - xorg.libxcb + deps = with hostPkgs.pkgsStatic; [ + curlMinimal + dbus + glaze + llvmPackages.libcxx + openssl + sqlite wayland - ])); - in - with pkgs; { + xorg.libXau + xorg.libXdmcp + xorg.libxcb + + (mkOverridden "cmake" ftxui) + (mkOverridden "cmake" sqlitecpp) + (mkOverridden "meson" libsigcxx30) + (mkOverridden "meson" tomlplusplus) + ]; + in { packages = rec { draconisplusplus = stdenv.mkDerivation { name = "draconis++"; version = "0.1.0"; src = self; + NIX_ENFORCE_NO_NATIVE = 0; - nativeBuildInputs = [ + nativeBuildInputs = with muslPkgs; [ cmake meson ninja @@ -90,58 +108,160 @@ configurePhase = '' meson setup build ''; - buildPhase = '' meson compile -C build ''; - installPhase = '' mkdir -p $out/bin mv build/draconis++ $out/bin/draconis++ ''; - }; + meta.staticExecutable = true; + }; default = draconisplusplus; }; - formatter = treefmt-nix.lib.mkWrapper pkgs { + devShell = muslPkgs.mkShell.override {inherit stdenv;} { + packages = + (with hostPkgs; [bear cmake]) + ++ (with muslPkgs; [ + llvmPackages_20.clang-tools + meson + ninja + pkg-config + (hostPkgs.writeScriptBin "build" "meson compile -C build") + (hostPkgs.writeScriptBin "clean" "meson setup build --wipe") + (hostPkgs.writeScriptBin "run" "meson compile -C build && build/draconis++") + ]) + ++ deps; + + NIX_ENFORCE_NO_NATIVE = 0; + }; + + formatter = treefmt-nix.lib.mkWrapper hostPkgs { projectRootFile = "flake.nix"; programs = { alejandra.enable = true; deadnix.enable = true; - clang-format = { enable = true; - package = pkgs.llvmPackages.clang-tools; + package = hostPkgs.llvmPackages.clang-tools; }; }; }; - - devShell = mkShell.override {inherit stdenv;} { - packages = - [ - alejandra - bear - llvmPackages.clang-tools - cmake - include-what-you-use - lldb - hyperfine - meson - ninja - nvfetcher - pkg-config - unzip - - (writeScriptBin "build" "meson compile -C build") - (writeScriptBin "clean" "meson setup build --wipe") - (writeScriptBin "run" "meson compile -C build && build/draconis++") - ] - ++ deps; - - LD_LIBRARY_PATH = "${lib.makeLibraryPath deps}"; - NIX_ENFORCE_NO_NATIVE = 0; - }; } + else let + pkgs = import nixpkgs {inherit system;}; + + llvmPackages = pkgs.llvmPackages_20; + + stdenv = with pkgs; + ( + if hostPlatform.isLinux + then stdenvAdapters.useMoldLinker + else lib.id + ) + llvmPackages.stdenv; + + deps = with pkgs; + [ + (glaze.override {enableAvx2 = hostPlatform.isx86;}) + ] + ++ (with pkgsStatic; [ + curl + ftxui + (tomlplusplus.overrideAttrs { + doCheck = false; + }) + ]) + ++ darwinPkgs + ++ linuxPkgs; + + darwinPkgs = nixpkgs.lib.optionals stdenv.isDarwin (with pkgs.pkgsStatic; [libiconv]); + + linuxPkgs = nixpkgs.lib.optionals stdenv.isLinux (with pkgs; + [ + valgrind + ] + ++ (with pkgsStatic; [ + dbus + libsigcxx30 + sqlitecpp + xorg.libxcb + wayland + ])); + in + with pkgs; { + packages = rec { + draconisplusplus = stdenv.mkDerivation { + name = "draconis++"; + version = "0.1.0"; + src = self; + + nativeBuildInputs = [ + cmake + meson + ninja + pkg-config + ]; + + buildInputs = deps; + + configurePhase = '' + meson setup build + ''; + + buildPhase = '' + meson compile -C build + ''; + + installPhase = '' + mkdir -p $out/bin + mv build/draconis++ $out/bin/draconis++ + ''; + }; + + default = draconisplusplus; + }; + + formatter = treefmt-nix.lib.mkWrapper pkgs { + projectRootFile = "flake.nix"; + programs = { + alejandra.enable = true; + deadnix.enable = true; + + clang-format = { + enable = true; + package = pkgs.llvmPackages.clang-tools; + }; + }; + }; + + devShell = mkShell.override {inherit stdenv;} { + packages = + [ + alejandra + bear + llvmPackages.clang-tools + cmake + include-what-you-use + lldb + hyperfine + meson + ninja + nvfetcher + pkg-config + unzip + + (writeScriptBin "build" "meson compile -C build") + (writeScriptBin "clean" "meson setup build --wipe") + (writeScriptBin "run" "meson compile -C build && build/draconis++") + ] + ++ deps; + + LD_LIBRARY_PATH = "${lib.makeLibraryPath deps}"; + NIX_ENFORCE_NO_NATIVE = 0; + }; + } ); } diff --git a/meson.build b/meson.build index 7406f84..df37e9a 100644 --- a/meson.build +++ b/meson.build @@ -30,6 +30,7 @@ common_warning_flags = [ '-Wno-missing-prototypes', '-Wno-padded', '-Wno-pre-c++20-compat-pedantic', + '-Wno-unused-command-line-argument', '-Wunused-function', ] @@ -49,10 +50,7 @@ common_cpp_flags = { '/external:anglebrackets', '/std:c++latest', ], - 'unix_extra' : [ - '-march=native', - '-nostdlib++', - ], + 'unix_extra' : '-march=native', 'windows_extra' : '-DCURL_STATICLIB', } @@ -80,7 +78,6 @@ else if host_system == 'windows' common_cpp_args += common_cpp_flags['windows_extra'] endif - common_cpp_args = cpp.get_supported_arguments(common_cpp_args) endif add_project_arguments(common_cpp_args, language : 'cpp') @@ -91,7 +88,7 @@ add_project_arguments(common_cpp_args, language : 'cpp') base_sources = files('src/core/system_data.cpp', 'src/config/config.cpp', 'src/config/weather.cpp', 'src/main.cpp') platform_sources = { - 'linux' : ['src/os/linux.cpp', 'src/os/linux/issetugid_stub.cpp', 'src/os/linux/pkg_count.cpp'], + 'linux' : ['src/os/linux.cpp', 'src/os/linux/pkg_count.cpp'], 'darwin' : ['src/os/macos.cpp', 'src/os/macos/bridge.mm'], 'windows' : ['src/os/windows.cpp'], } @@ -125,21 +122,13 @@ elif host_system == 'windows' cpp.find_library('windowsapp'), ] elif host_system == 'linux' - dbus_cxx_dep = dependency('dbus-cxx', include_type : 'system', required : false) - - if not dbus_cxx_dep.found() - cmake = import('cmake') - dbus_cxx_proj = cmake.subproject('dbus_cxx') - dbus_cxx_dep = dbus_cxx_proj.dependency('dbus_cxx', include_type : 'system') - endif platform_deps += [ dependency('SQLiteCpp'), dependency('xcb'), dependency('xau'), dependency('xdmcp'), dependency('wayland-client'), - dependency('sigc++-3.0', include_type : 'system'), - dbus_cxx_dep, + dependency('dbus-1', include_type: 'system'), ] endif @@ -183,7 +172,7 @@ objc_args = [] if host_system == 'darwin' objc_args += ['-fobjc-arc'] elif cpp.get_id() == 'clang' - link_args += ['-static-libgcc', '-static-libstdc++'] + link_args += ['-static'] endif # ------------------- # diff --git a/src/config/weather.cpp b/src/config/weather.cpp index f93a4f9..027a8d8 100644 --- a/src/config/weather.cpp +++ b/src/config/weather.cpp @@ -6,11 +6,11 @@ #include // std::filesystem::{path, remove, rename} #include // std::format #include // std::{ifstream, ofstream} +#include // glz::read_beve +#include // glz::write_beve #include // glz::{error_ctx, error_code} -#include // glz::check_partial_read -#include // glz::read #include // glz::format_error -#include // glz::write_json +#include // glz::write_json #include // std::istreambuf_iterator #include // std::error_code #include // std::move @@ -18,17 +18,18 @@ #include "src/core/util/defs.hpp" #include "src/core/util/logging.hpp" +#include "src/core/util/types.hpp" #include "config.hpp" namespace fs = std::filesystem; -using namespace weather; - -using util::types::i32, util::types::Err, util::types::Exception; +using weather::Output; namespace { - using glz::opts, glz::error_ctx, glz::error_code, glz::write_json, glz::read, glz::format_error; + using glz::opts, glz::error_ctx, glz::error_code, glz::read, glz::read_beve, glz::write_beve, glz::format_error; + using util::types::usize, util::types::Err, util::types::Exception; + using weather::Coords; constexpr opts glaze_opts = { .error_on_unknown_keys = false }; @@ -39,7 +40,7 @@ namespace { if (errc) return Err("Failed to get temp directory: " + errc.message()); - cachePath /= "weather_cache.json"; + cachePath /= "weather_cache.beve"; return cachePath; } @@ -58,10 +59,19 @@ namespace { try { const String content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - Output result; + ifs.close(); - if (const error_ctx errc = read(result, content); errc.ec != error_code::none) - return Err(std::format("JSON parse error: {}", format_error(errc, content))); + if (content.empty()) + return Err(std::format("BEVE cache file is empty: {}", cachePath->string())); + + Output result; + + if (const error_ctx glazeErr = read_beve(result, content); glazeErr.ec != error_code::none) + return Err( + std::format( + "BEVE parse error reading cache (code {}): {}", static_cast(glazeErr.ec), cachePath->string() + ) + ); debug_log("Successfully read from cache file."); return result; @@ -69,6 +79,8 @@ namespace { } fn WriteCacheToFile(const Output& data) -> Result { + using util::types::isize; + Result cachePath = GetCachePath(); if (!cachePath) @@ -79,19 +91,22 @@ namespace { tempPath += ".tmp"; try { + String binaryBuffer; + + if (const error_ctx glazeErr = write_beve(data, binaryBuffer); glazeErr) + return Err(std::format("BEVE serialization error writing cache (code {})", static_cast(glazeErr.ec))); + { std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc); if (!ofs.is_open()) return Err("Failed to open temp file: " + tempPath.string()); - String jsonStr; - - if (const error_ctx errc = write_json(data, jsonStr); errc.ec != error_code::none) - return Err("JSON serialization error: " + format_error(errc, jsonStr)); - - ofs << jsonStr; - if (!ofs) - return Err("Failed to write to temp file"); + ofs.write(binaryBuffer.data(), static_cast(binaryBuffer.size())); + if (!ofs) { + std::error_code removeEc; + fs::remove(tempPath, removeEc); + return Err("Failed to write to temp BEVE cache file"); + } } std::error_code errc; @@ -105,7 +120,19 @@ namespace { debug_log("Successfully wrote to cache file."); return {}; - } catch (const Exception& e) { return Err(std::format("File operation error: {}", e.what())); } + } catch (const std::ios_base::failure& e) { + std::error_code removeEc; + fs::remove(tempPath, removeEc); + return Err(std::format("Filesystem error writing BEVE cache file {}: {}", tempPath.string(), e.what())); + } catch (const Exception& e) { + std::error_code removeEc; + fs::remove(tempPath, removeEc); + return Err(std::format("File operation error during BEVE cache write: {}", e.what())); + } catch (...) { + std::error_code removeEc; + fs::remove(tempPath, removeEc); + return Err(std::format("Unknown error writing BEVE cache file: {}", tempPath.string())); + } } fn WriteCallback(void* contents, const usize size, const usize nmemb, String* str) -> usize { @@ -136,7 +163,7 @@ namespace { Output output; - if (const error_ctx errc = glz::read(output, responseBuffer); errc.ec != error_code::none) + if (const error_ctx errc = read(output, responseBuffer); errc.ec != error_code::none) return Err("API response parse error: " + format_error(errc, responseBuffer)); return std::move(output); @@ -145,6 +172,7 @@ namespace { fn Weather::getWeatherInfo() const -> Output { using namespace std::chrono; + using util::types::i32; if (Result data = ReadCacheFromFile()) { const Output& dataVal = *data; @@ -173,8 +201,10 @@ fn Weather::getWeatherInfo() const -> Output { }; if (std::holds_alternative(location)) { - const auto& city = std::get(location); - char* escaped = curl_easy_escape(nullptr, city.c_str(), static_cast(city.length())); + const auto& city = std::get(location); + + char* escaped = curl_easy_escape(nullptr, city.c_str(), static_cast(city.length())); + debug_log("Requesting city: {}", escaped); const String apiUrl = @@ -184,12 +214,22 @@ fn Weather::getWeatherInfo() const -> Output { return handleApiResult(MakeApiRequest(apiUrl)); } - const auto& [lat, lon] = std::get(location); - debug_log("Requesting coordinates: lat={:.3f}, lon={:.3f}", lat, lon); + if (std::holds_alternative(location)) { + const auto& [lat, lon] = std::get(location); + debug_log("Requesting coordinates: lat={:.3f}, lon={:.3f}", lat, lon); + const String apiUrl = std::format( + "https://api.openweathermap.org/data/2.5/weather?lat={:.3f}&lon={:.3f}&appid={}&units={}", + lat, + lon, + api_key, + units + ); + return handleApiResult(MakeApiRequest(apiUrl)); + } +#ifdef __GLIBCXX__ + printf("Invalid location type in configuration. Expected String or Coords.\n"); +#endif - const String apiUrl = std::format( - "https://api.openweathermap.org/data/2.5/weather?lat={:.3f}&lon={:.3f}&appid={}&units={}", lat, lon, api_key, units - ); - - return handleApiResult(MakeApiRequest(apiUrl)); + error_log("Invalid location type in configuration. Expected String or Coords."); + return Output {}; } diff --git a/src/core/system_data.cpp b/src/core/system_data.cpp index 3bde682..9c1e3e0 100644 --- a/src/core/system_data.cpp +++ b/src/core/system_data.cpp @@ -15,12 +15,7 @@ namespace { const year_month_day ymd = year_month_day { floor(system_clock::now()) }; - try { - return std::format(std::locale(""), "{:%B %d}", ymd); - } catch (const std::runtime_error& e) { - warn_log("Could not retrieve or use system locale ({}). Falling back to default C locale.", e.what()); - return std::format(std::locale::classic(), "{:%B %d}", ymd); - } + return std::format("{:%B %d}", ymd); } fn log_timing(const std::string& name, const std::chrono::steady_clock::duration& duration) -> void { diff --git a/src/core/util/error.hpp b/src/core/util/error.hpp index 0c5cb48..e733158 100644 --- a/src/core/util/error.hpp +++ b/src/core/util/error.hpp @@ -1,13 +1,12 @@ #pragma once +#include #include // std::source_location #include // std::error_code #ifdef _WIN32 #include // error values #include // winrt::hresult_error -#elifdef __linux__ - #include // DBus::Error #endif #include "src/core/util/types.hpp" @@ -126,33 +125,6 @@ namespace util::error { return DraconisError { code, fullMsg, loc }; } - - #ifdef __linux__ - static auto fromDBus(const DBus::Error& err, const std::source_location& loc = std::source_location::current()) - -> DraconisError { - String name = err.name(); - DraconisErrorCode codeHint = DraconisErrorCode::PlatformSpecific; - String message; - - using namespace std::string_view_literals; - - if (name == "org.freedesktop.DBus.Error.ServiceUnknown"sv || - name == "org.freedesktop.DBus.Error.NameHasNoOwner"sv) { - codeHint = DraconisErrorCode::NotFound; - message = std::format("DBus service/name not found: {}", err.message()); - } else if (name == "org.freedesktop.DBus.Error.NoReply"sv || name == "org.freedesktop.DBus.Error.Timeout"sv) { - codeHint = DraconisErrorCode::Timeout; - message = std::format("DBus timeout/no reply: {}", err.message()); - } else if (name == "org.freedesktop.DBus.Error.AccessDenied"sv) { - codeHint = DraconisErrorCode::PermissionDenied; - message = std::format("DBus access denied: {}", err.message()); - } else { - message = std::format("DBus error: {} - {}", name, err.message()); - } - - return DraconisError { codeHint, message, loc }; - } - #endif #endif }; } // namespace util::error diff --git a/src/main.cpp b/src/main.cpp index da779c0..8141211 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,13 +1,10 @@ #include // std::lround #include // std::format -#include // ftxui::{hbox, vbox, text, separator, filler} -#include // ftxui::{Element, Render} +#include // ftxui::{Element, hbox, vbox, text, separator, filler, etc.} +#include // ftxui::{Render} #include // ftxui::Color #include // ftxui::{Screen, Dimension::Full} -#include // std::optional (operator!=) -#include // std::ranges::{to, views} -#include // std::string (String) -#include // std::string_view (StringView) +#include // std::ranges::{iota, to, transform} #include "src/config/weather.hpp" @@ -274,7 +271,7 @@ fn main() -> i32 { else error_at(packageCount.error()); - Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }), text("") }); + Element document = vbox({ hbox({ SystemInfoBox(config, data), filler() }) }); Screen screen = Screen::Create(Dimension::Full(), Dimension::Fit(document)); Render(screen, document); diff --git a/src/os/linux.cpp b/src/os/linux.cpp index 3f94ced..7b82fc0 100644 --- a/src/os/linux.cpp +++ b/src/os/linux.cpp @@ -2,22 +2,12 @@ // clang-format off #include // std::strlen -#include // DBus::CallMessage -#include // DBus::Connection -#include // DBus::Dispatcher -#include // DBus::{DataType, BusType} -#include // DBus::Error -#include // DBus::MessageAppendIterator -#include // DBus::Signature -#include // DBus::StandaloneDispatcher -#include // DBus::Variant +#include #include // std::{unexpected, expected} #include // std::{format, format_to_n} #include // std::ifstream #include // PATH_MAX #include // std::numeric_limits -#include // std::map (Map) -#include // std::shared_ptr (SharedPointer) #include // std::{getline, string (String)} #include // std::string_view (StringView) #include // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED @@ -25,10 +15,9 @@ #include // sysinfo #include // utsname, uname #include // readlink -#include // std::move -#include "src/core/util/logging.hpp" #include "src/core/util/helpers.hpp" +#include "src/core/util/logging.hpp" #include "src/wrappers/wayland.hpp" #include "src/wrappers/xcb.hpp" @@ -191,207 +180,328 @@ namespace { return String(compositorNameView); } - fn GetMprisPlayers(const SharedPointer& connection) -> Result { - using namespace std::string_view_literals; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wold-style-cast" + fn GetMprisPlayers(DBusConnection* connection) -> Vec { + Vec mprisPlayers; + DBusError err; + dbus_error_init(&err); - try { - const SharedPointer call = - DBus::CallMessage::create("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"); + // Create a method call to org.freedesktop.DBus.ListNames + DBusMessage* msg = dbus_message_new_method_call( + "org.freedesktop.DBus", // target service + "/org/freedesktop/DBus", // object path + "org.freedesktop.DBus", // interface name + "ListNames" // method name + ); - const SharedPointer reply = connection->send_with_reply_blocking(call, 5); - - if (!reply || !reply->is_valid()) - return Err(DraconisError(DraconisErrorCode::Timeout, "Failed to get reply from ListNames")); - - Vec allNamesStd; - DBus::MessageIterator reader(*reply); - reader >> allNamesStd; - - for (const String& name : allNamesStd) - if (StringView(name).contains("org.mpris.MediaPlayer2"sv)) - return name; - - return Err(DraconisError(DraconisErrorCode::NotFound, "No MPRIS players found")); - } catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) { - return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); + if (!msg) { + debug_log("Failed to create message for ListNames."); + return mprisPlayers; } + + // Send the message and block until we get a reply. + DBusMessage* reply = dbus_connection_send_with_reply_and_block(connection, msg, -1, &err); + dbus_message_unref(msg); + + if (dbus_error_is_set(&err)) { + debug_log("DBus error in ListNames: {}", err.message); + dbus_error_free(&err); + return mprisPlayers; + } + + if (!reply) { + debug_log("No reply received for ListNames."); + return mprisPlayers; + } + + // The expected reply signature is "as" (an array of strings) + DBusMessageIter iter; + + if (!dbus_message_iter_init(reply, &iter)) { + debug_log("Reply has no arguments."); + dbus_message_unref(reply); + return mprisPlayers; + } + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) { + debug_log("Reply argument is not an array."); + dbus_message_unref(reply); + return mprisPlayers; + } + + // Iterate over the array of strings + DBusMessageIter subIter; + dbus_message_iter_recurse(&iter, &subIter); + + while (dbus_message_iter_get_arg_type(&subIter) != DBUS_TYPE_INVALID) { + if (dbus_message_iter_get_arg_type(&subIter) == DBUS_TYPE_STRING) { + const char* name = nullptr; + dbus_message_iter_get_basic(&subIter, static_cast(&name)); + if (name && std::string_view(name).contains("org.mpris.MediaPlayer2")) + mprisPlayers.emplace_back(name); + } + dbus_message_iter_next(&subIter); + } + + dbus_message_unref(reply); + return mprisPlayers; } + #pragma clang diagnostic pop - fn GetMediaPlayerMetadata(const SharedPointer& connection, const String& playerBusName) - -> Result { - try { - const SharedPointer metadataCall = - DBus::CallMessage::create(playerBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); - - *metadataCall << "org.mpris.MediaPlayer2.Player" << "Metadata"; - - const SharedPointer metadataReply = connection->send_with_reply_blocking(metadataCall, 1000); - - if (!metadataReply || !metadataReply->is_valid()) { - return Err( - DraconisError(DraconisErrorCode::Timeout, "DBus Get Metadata call timed out or received invalid reply") - ); - } - - DBus::MessageIterator iter(*metadataReply); - DBus::Variant metadataVariant; - iter >> metadataVariant; // Can throw - - // MPRIS metadata is variant containing a dict a{sv} - if (metadataVariant.type() != DBus::DataType::DICT_ENTRY && metadataVariant.type() != DBus::DataType::ARRAY) { - return Err(DraconisError( - DraconisErrorCode::ParseError, - std::format( - "Inner metadata variant is not the expected type, expected dict/a{{sv}} but got '{}'", - metadataVariant.signature().str() - ) - )); - } - - Map metadata = metadataVariant.to_map(); // Can throw - - Option title = None; - Option artist = None; - - if (const auto titleIter = metadata.find("xesam:title"); - titleIter != metadata.end() && titleIter->second.type() == DBus::DataType::STRING) - title = titleIter->second.to_string(); - - if (const auto artistIter = metadata.find("xesam:artist"); artistIter != metadata.end()) { - if (artistIter->second.type() == DBus::DataType::ARRAY) { - if (Vec artists = artistIter->second.to_vector(); !artists.empty()) - artist = artists[0]; - } else if (artistIter->second.type() == DBus::DataType::STRING) { - artist = artistIter->second.to_string(); - } - } - - return MediaInfo(std::move(title), std::move(artist)); - } catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) { - return Err(DraconisError( - DraconisErrorCode::InternalError, std::format("Standard exception processing metadata: {}", e.what()) - )); - } + fn GetActivePlayer(const Vec& mprisPlayers) -> Option { + if (!mprisPlayers.empty()) + return mprisPlayers.front(); + return None; } } // namespace -fn os::GetOSVersion() -> Result { - constexpr CStr path = "/etc/os-release"; +namespace os { + fn GetOSVersion() -> Result { + constexpr CStr path = "/etc/os-release"; - std::ifstream file(path); + std::ifstream file(path); - if (!file) - return Err(DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open {}", path))); + if (!file) + return Err(DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open {}", path))); - String line; - constexpr StringView prefix = "PRETTY_NAME="; + String line; + constexpr StringView prefix = "PRETTY_NAME="; - while (std::getline(file, line)) { - if (StringView(line).starts_with(prefix)) { - String value = line.substr(prefix.size()); + while (std::getline(file, line)) { + if (StringView(line).starts_with(prefix)) { + String value = line.substr(prefix.size()); - if ((value.length() >= 2 && value.front() == '"' && value.back() == '"') || - (value.length() >= 2 && value.front() == '\'' && value.back() == '\'')) - value = value.substr(1, value.length() - 2); + if ((value.length() >= 2 && value.front() == '"' && value.back() == '"') || + (value.length() >= 2 && value.front() == '\'' && value.back() == '\'')) + value = value.substr(1, value.length() - 2); - if (value.empty()) - return Err(DraconisError( - DraconisErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path) - )); + if (value.empty()) + return Err(DraconisError( + DraconisErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path) + )); - return value; + return value; + } } + + return Err(DraconisError(DraconisErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path))); } - return Err(DraconisError(DraconisErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path))); -} + fn GetMemInfo() -> Result { + struct sysinfo info; -fn os::GetMemInfo() -> Result { - struct sysinfo info; + if (sysinfo(&info) != 0) + return Err(DraconisError::withErrno("sysinfo call failed")); - if (sysinfo(&info) != 0) - return Err(DraconisError::fromDBus("sysinfo call failed")); + const u64 totalRam = info.totalram; + const u64 memUnit = info.mem_unit; - const u64 totalRam = info.totalram; - const u64 memUnit = info.mem_unit; + if (memUnit == 0) + return Err(DraconisError(DraconisErrorCode::InternalError, "sysinfo returned mem_unit of zero")); - if (memUnit == 0) - return Err(DraconisError(DraconisErrorCode::InternalError, "sysinfo returned mem_unit of zero")); + if (totalRam > std::numeric_limits::max() / memUnit) + return Err(DraconisError(DraconisErrorCode::InternalError, "Potential overflow calculating total RAM")); - if (totalRam > std::numeric_limits::max() / memUnit) - return Err(DraconisError(DraconisErrorCode::InternalError, "Potential overflow calculating total RAM")); + return info.totalram * info.mem_unit; + } - return info.totalram * info.mem_unit; -} + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wold-style-cast" + fn GetNowPlaying() -> Result { + DBusError err; + dbus_error_init(&err); -fn os::GetNowPlaying() -> Result { - // Dispatcher must outlive the try-block because 'connection' depends on it later. - // ReSharper disable once CppTooWideScope, CppJoinDeclarationAndAssignment - SharedPointer dispatcher; - SharedPointer connection; - - try { - dispatcher = DBus::StandaloneDispatcher::create(); - - if (!dispatcher) - return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to create DBus dispatcher")); - - connection = dispatcher->create_connection(DBus::BusType::SESSION); + // Connect to the session bus + DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &err); if (!connection) - return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "Failed to connect to DBus session bus")); - } catch (const DBus::Error& e) { return Err(DraconisError::fromDBus(e)); } catch (const Exception& e) { - return Err(DraconisError(DraconisErrorCode::InternalError, e.what())); + if (dbus_error_is_set(&err)) { + error_log("DBus connection error: {}", err.message); + + DraconisError error = DraconisError(DraconisErrorCode::ApiUnavailable, err.message); + dbus_error_free(&err); + + return Err(error); + } + + Vec mprisPlayers = GetMprisPlayers(connection); + + if (mprisPlayers.empty()) { + dbus_connection_unref(connection); + return Err(DraconisError(DraconisErrorCode::NotFound, "No MPRIS players found")); + } + + Option activePlayer = GetActivePlayer(mprisPlayers); + + if (!activePlayer.has_value()) { + dbus_connection_unref(connection); + return Err(DraconisError(DraconisErrorCode::NotFound, "No active MPRIS player found")); + } + + // Prepare a call to the Properties.Get method to fetch "Metadata" + DBusMessage* msg = dbus_message_new_method_call( + activePlayer->c_str(), // target service (active player) + "/org/mpris/MediaPlayer2", // object path + "org.freedesktop.DBus.Properties", // interface + "Get" // method name + ); + + if (!msg) { + dbus_connection_unref(connection); + return Err(DraconisError(DraconisErrorCode::InternalError, "Failed to create DBus message")); + } + + const char* interfaceName = "org.mpris.MediaPlayer2.Player"; + const char* propertyName = "Metadata"; + + if (!dbus_message_append_args( + msg, DBUS_TYPE_STRING, &interfaceName, DBUS_TYPE_STRING, &propertyName, DBUS_TYPE_INVALID + )) { + dbus_message_unref(msg); + dbus_connection_unref(connection); + return Err(DraconisError(DraconisErrorCode::InternalError, "Failed to append arguments to DBus message")); + } + + // Call the method and block until reply is received. + DBusMessage* reply = dbus_connection_send_with_reply_and_block(connection, msg, -1, &err); + dbus_message_unref(msg); + + if (dbus_error_is_set(&err)) { + error_log("DBus error in Properties.Get: {}", err.message); + + DraconisError error = DraconisError(DraconisErrorCode::ApiUnavailable, err.message); + dbus_error_free(&err); + dbus_connection_unref(connection); + + return Err(error); + } + + if (!reply) { + dbus_connection_unref(connection); + return Err(DraconisError(DraconisErrorCode::ApiUnavailable, "No reply received for Properties.Get")); + } + + // The reply should contain a variant holding a dictionary ("a{sv}") + DBusMessageIter iter; + + if (!dbus_message_iter_init(reply, &iter)) { + dbus_message_unref(reply); + dbus_connection_unref(connection); + return Err(DraconisError(DraconisErrorCode::InternalError, "Reply has no arguments")); + } + + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) { + dbus_message_unref(reply); + dbus_connection_unref(connection); + return Err(DraconisError(DraconisErrorCode::InternalError, "Reply argument is not a variant")); + } + + // Recurse into the variant to get the dictionary + DBusMessageIter variantIter; + dbus_message_iter_recurse(&iter, &variantIter); + + if (dbus_message_iter_get_arg_type(&variantIter) != DBUS_TYPE_ARRAY) { + dbus_message_unref(reply); + dbus_connection_unref(connection); + return Err(DraconisError(DraconisErrorCode::InternalError, "Variant argument is not an array")); + } + + String title; + String artist; + DBusMessageIter arrayIter; + dbus_message_iter_recurse(&variantIter, &arrayIter); + + // Iterate over each dictionary entry (each entry is of type dict entry) + while (dbus_message_iter_get_arg_type(&arrayIter) != DBUS_TYPE_INVALID) { + if (dbus_message_iter_get_arg_type(&arrayIter) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter dictEntry; + dbus_message_iter_recurse(&arrayIter, &dictEntry); + + // Get the key (a string) + const char* key = nullptr; + + if (dbus_message_iter_get_arg_type(&dictEntry) == DBUS_TYPE_STRING) + dbus_message_iter_get_basic(&dictEntry, static_cast(&key)); + + // Move to the value (a variant) + dbus_message_iter_next(&dictEntry); + + if (dbus_message_iter_get_arg_type(&dictEntry) == DBUS_TYPE_VARIANT) { + DBusMessageIter valueIter; + dbus_message_iter_recurse(&dictEntry, &valueIter); + + if (key && std::string_view(key) == "xesam:title") { + if (dbus_message_iter_get_arg_type(&valueIter) == DBUS_TYPE_STRING) { + const char* val = nullptr; + dbus_message_iter_get_basic(&valueIter, static_cast(&val)); + + if (val) + title = val; + } + } else if (key && std::string_view(key) == "xesam:artist") { + // Expect an array of strings + if (dbus_message_iter_get_arg_type(&valueIter) == DBUS_TYPE_ARRAY) { + DBusMessageIter subIter; + dbus_message_iter_recurse(&valueIter, &subIter); + + if (dbus_message_iter_get_arg_type(&subIter) == DBUS_TYPE_STRING) { + const char* val = nullptr; + dbus_message_iter_get_basic(&subIter, static_cast(&val)); + + if (val) + artist = val; + } + } + } + } + } + + dbus_message_iter_next(&arrayIter); + } + + dbus_message_unref(reply); + dbus_connection_unref(connection); + + return MediaInfo(artist, title); + } + #pragma clang diagnostic pop + + fn GetWindowManager() -> Option { + if (Result waylandResult = GetWaylandCompositor()) + return *waylandResult; + else + debug_log("Could not detect Wayland compositor: {}", waylandResult.error().message); + + if (Result x11Result = GetX11WindowManager()) + return *x11Result; + else + debug_log("Could not detect X11 window manager: {}", x11Result.error().message); + + return None; } - Result playerBusName = GetMprisPlayers(connection); + fn GetDesktopEnvironment() -> Option { + return util::helpers::GetEnv("XDG_CURRENT_DESKTOP") + .transform([](const String& xdgDesktop) -> String { + if (const usize colon = xdgDesktop.find(':'); colon != String::npos) + return xdgDesktop.substr(0, colon); - if (!playerBusName) - return Err(playerBusName.error()); + return xdgDesktop; + }) + .or_else([](const DraconisError&) -> Result { + return util::helpers::GetEnv("DESKTOP_SESSION"); + }) + .transform([](const String& finalValue) -> Option { + debug_log("Found desktop environment: {}", finalValue); + return finalValue; + }) + .value_or(None); + } - Result metadataResult = GetMediaPlayerMetadata(connection, *playerBusName); - - if (!metadataResult) - return Err(metadataResult.error()); - - return std::move(*metadataResult); -} - -fn os::GetWindowManager() -> Option { - if (Result waylandResult = GetWaylandCompositor()) - return *waylandResult; - else - debug_log("Could not detect Wayland compositor: {}", waylandResult.error().message); - - if (Result x11Result = GetX11WindowManager()) - return *x11Result; - else - debug_log("Could not detect X11 window manager: {}", x11Result.error().message); - - return None; -} - -fn os::GetDesktopEnvironment() -> Option { - return util::helpers::GetEnv("XDG_CURRENT_DESKTOP") - .transform([](const String& xdgDesktop) -> String { - if (const usize colon = xdgDesktop.find(':'); colon != String::npos) - return xdgDesktop.substr(0, colon); - - return xdgDesktop; - }) - .or_else([](const DraconisError&) -> Result { - return util::helpers::GetEnv("DESKTOP_SESSION"); - }) - .transform([](const String& finalValue) -> Option { - debug_log("Found desktop environment: {}", finalValue); - return finalValue; - }) - .value_or(None); -} - -fn os::GetShell() -> Option { - if (const Result shellPath = util::helpers::GetEnv("SHELL")) { - // clang-format off + fn GetShell() -> Option { + if (const Result shellPath = util::helpers::GetEnv("SHELL")) { + // clang-format off constexpr Array, 5> shellMap {{ { "bash", "Bash" }, { "zsh", "Zsh" }, @@ -399,80 +509,81 @@ fn os::GetShell() -> Option { { "nu", "Nushell" }, { "sh", "SH" }, // sh last because other shells contain "sh" }}; - // clang-format on + // clang-format on - for (const auto& [exe, name] : shellMap) - if (shellPath->contains(exe)) - return String(name); + for (const auto& [exe, name] : shellMap) + if (shellPath->contains(exe)) + return String(name); - return *shellPath; // fallback to the raw shell path + return *shellPath; // fallback to the raw shell path + } + + return None; } - return None; -} + fn GetHost() -> Result { + constexpr CStr primaryPath = "/sys/class/dmi/id/product_family"; + constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name"; -fn os::GetHost() -> Result { - constexpr CStr primaryPath = "/sys/class/dmi/id/product_family"; - constexpr CStr fallbackPath = "/sys/class/dmi/id/product_name"; + fn readFirstLine = [&](const String& path) -> Result { + std::ifstream file(path); + String line; - fn readFirstLine = [&](const String& path) -> Result { - std::ifstream file(path); - String line; - - if (!file) - return Err( - DraconisError(DraconisErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path)) - ); - - if (!std::getline(file, line)) - return Err( - DraconisError(DraconisErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)) - ); - - return line; - }; - - return readFirstLine(primaryPath).or_else([&](const DraconisError& primaryError) -> Result { - return readFirstLine(fallbackPath) - .or_else([&](const DraconisError& fallbackError) -> Result { + if (!file) return Err(DraconisError( - DraconisErrorCode::InternalError, - std::format( - "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", - primaryPath, - primaryError.message, - fallbackPath, - fallbackError.message - ) + DraconisErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path) )); - }); - }); -} -fn os::GetKernelVersion() -> Result { - utsname uts; + if (!std::getline(file, line)) + return Err( + DraconisError(DraconisErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path)) + ); - if (uname(&uts) == -1) - return Err(DraconisError::withErrno("uname call failed")); + return line; + }; - if (std::strlen(uts.release) == 0) - return Err(DraconisError(DraconisErrorCode::ParseError, "uname returned null kernel release")); + return readFirstLine(primaryPath).or_else([&](const DraconisError& primaryError) -> Result { + return readFirstLine(fallbackPath) + .or_else([&](const DraconisError& fallbackError) -> Result { + return Err(DraconisError( + DraconisErrorCode::InternalError, + std::format( + "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", + primaryPath, + primaryError.message, + fallbackPath, + fallbackError.message + ) + )); + }); + }); + } - return uts.release; -} + fn GetKernelVersion() -> Result { + utsname uts; -fn os::GetDiskUsage() -> Result { - struct statvfs stat; + if (uname(&uts) == -1) + return Err(DraconisError::withErrno("uname call failed")); - if (statvfs("/", &stat) == -1) - return Err(DraconisError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)"))); + if (std::strlen(uts.release) == 0) + return Err(DraconisError(DraconisErrorCode::ParseError, "uname returned null kernel release")); - return DiskSpace { - .used_bytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize), - .total_bytes = stat.f_blocks * stat.f_frsize, - }; -} + return uts.release; + } -fn os::GetPackageCount() -> Result { return linux::GetTotalPackageCount(); } + fn GetDiskUsage() -> Result { + struct statvfs stat; + + if (statvfs("/", &stat) == -1) + return Err(DraconisError::withErrno(std::format("Failed to get filesystem stats for '/' (statvfs call failed)"))); + + return DiskSpace { + .used_bytes = (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize), + .total_bytes = stat.f_blocks * stat.f_frsize, + }; + } + + fn GetPackageCount() -> Result { return linux::GetTotalPackageCount(); } +} // namespace os #endif // __linux__ diff --git a/src/os/linux/issetugid_stub.cpp b/src/os/linux/issetugid_stub.cpp deleted file mode 100644 index c79e71c..0000000 --- a/src/os/linux/issetugid_stub.cpp +++ /dev/null @@ -1,4 +0,0 @@ -#include "src/core/util/defs.hpp" -#include "src/core/util/types.hpp" - -extern "C" fn issetugid() -> util::types::usize { return 0; } // NOLINT diff --git a/src/os/linux/pkg_count.cpp b/src/os/linux/pkg_count.cpp index 9feff02..e1340d0 100644 --- a/src/os/linux/pkg_count.cpp +++ b/src/os/linux/pkg_count.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include "src/core/util/logging.hpp" #include "src/core/util/types.hpp"