#ifdef __linux__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "os.h" #include "src/util/macros.h" // Minimal global using declarations needed for function signatures using std::expected; using std::optional; namespace fs = std::filesystem; using namespace std::literals::string_view_literals; namespace { // Local using declarations for the anonymous namespace using std::array; using std::bit_cast; using std::getenv; using std::ifstream; using std::istreambuf_iterator; using std::less; using std::lock_guard; using std::mutex; using std::nullopt; using std::ofstream; using std::pair; using std::string_view; using std::to_string; using std::unexpected; using std::vector; using std::ranges::is_sorted; using std::ranges::lower_bound; using std::ranges::replace; using std::ranges::subrange; using std::ranges::transform; fn GetX11WindowManager() -> string { Display* display = XOpenDisplay(nullptr); // If XOpenDisplay fails, likely in a TTY if (!display) return ""; Atom supportingWmCheck = XInternAtom(display, "_NET_SUPPORTING_WM_CHECK", False); Atom wmName = XInternAtom(display, "_NET_WM_NAME", False); Atom utf8String = XInternAtom(display, "UTF8_STRING", False); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wold-style-cast" #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" Window root = DefaultRootWindow(display); // NOLINT #pragma clang diagnostic pop Window wmWindow = 0; Atom actualType = 0; int actualFormat = 0; unsigned long nitems = 0, bytesAfter = 0; unsigned char* data = nullptr; if (XGetWindowProperty( display, root, supportingWmCheck, 0, 1, False, // XA_WINDOW static_cast(33), &actualType, &actualFormat, &nitems, &bytesAfter, &data ) == Success && data) { wmWindow = *bit_cast(data); XFree(data); data = nullptr; if (XGetWindowProperty( display, wmWindow, wmName, 0, 1024, False, utf8String, &actualType, &actualFormat, &nitems, &bytesAfter, &data ) == Success && data) { string name(bit_cast(data)); XFree(data); XCloseDisplay(display); return name; } } XCloseDisplay(display); return "Unknown (X11)"; // Changed to empty string } fn TrimHyprlandWrapper(const string& input) -> string { if (input.contains("hyprland")) return "Hyprland"; return input; } fn ReadProcessCmdline(int pid) -> string { string path = "/proc/" + to_string(pid) + "/cmdline"; ifstream cmdlineFile(path); string cmdline; if (getline(cmdlineFile, cmdline)) { // Replace null bytes with spaces replace(cmdline, '\0', ' '); return cmdline; } return ""; } fn DetectHyprlandSpecific() -> string { // Check environment variables first const char* xdgCurrentDesktop = getenv("XDG_CURRENT_DESKTOP"); if (xdgCurrentDesktop && strcasestr(xdgCurrentDesktop, "hyprland")) return "Hyprland"; // Check for Hyprland's specific environment variable if (getenv("HYPRLAND_INSTANCE_SIGNATURE")) return "Hyprland"; // Check for Hyprland socket if (fs::exists("/run/user/" + to_string(getuid()) + "/hypr")) return "Hyprland"; return ""; } fn GetWaylandCompositor() -> string { // First try Hyprland-specific detection string hypr = DetectHyprlandSpecific(); if (!hypr.empty()) return hypr; // Then try the standard Wayland detection wl_display* display = wl_display_connect(nullptr); if (!display) return ""; int fileDescriptor = wl_display_get_fd(display); struct ucred cred; socklen_t len = sizeof(cred); if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) { wl_display_disconnect(display); return ""; } // Read both comm and cmdline string compositorName; // 1. Check comm (might be wrapped) string commPath = "/proc/" + to_string(cred.pid) + "/comm"; ifstream commFile(commPath); if (commFile >> compositorName) { subrange removedRange = std::ranges::remove(compositorName, '\n'); compositorName.erase(removedRange.begin(), compositorName.end()); } // 2. Check cmdline for actual binary reference string cmdline = ReadProcessCmdline(cred.pid); if (cmdline.contains("hyprland")) { wl_display_disconnect(display); return "Hyprland"; } // 3. Check exe symlink string exePath = "/proc/" + to_string(cred.pid) + "/exe"; array buf; ssize_t lenBuf = readlink(exePath.c_str(), buf.data(), buf.size() - 1); if (lenBuf != -1) { buf.at(static_cast(lenBuf)) = '\0'; string exe(buf.data()); if (exe.contains("hyprland")) { wl_display_disconnect(display); return "Hyprland"; } } wl_display_disconnect(display); // Final cleanup of wrapper names return TrimHyprlandWrapper(compositorName); } fn DetectFromEnvVars() -> optional { // Use RAII to guard against concurrent env modifications static mutex EnvMutex; lock_guard lock(EnvMutex); // XDG_CURRENT_DESKTOP if (const char* xdgCurrentDesktop = getenv("XDG_CURRENT_DESKTOP")) { const string_view sview(xdgCurrentDesktop); const size_t colon = sview.find(':'); return string(sview.substr(0, colon)); // Direct construct from view } // DESKTOP_SESSION if (const char* desktopSession = getenv("DESKTOP_SESSION")) return string(string_view(desktopSession)); // Avoid intermediate view storage return nullopt; } fn DetectFromSessionFiles() -> optional { static constexpr array, 12> DE_PATTERNS = { // clang-format off pair { "Budgie"sv, "budgie"sv }, pair { "Cinnamon"sv, "cinnamon"sv }, pair { "LXQt"sv, "lxqt"sv }, pair { "MATE"sv, "mate"sv }, pair { "Unity"sv, "unity"sv }, pair { "gnome"sv, "GNOME"sv }, pair { "gnome-wayland"sv, "GNOME"sv }, pair { "gnome-xorg"sv, "GNOME"sv }, pair { "kde"sv, "KDE"sv }, pair { "plasma"sv, "KDE"sv }, pair { "plasmax11"sv, "KDE"sv }, pair { "xfce"sv, "XFCE"sv }, // clang-format on }; static_assert(is_sorted(DE_PATTERNS, {}, &pair::first)); // Precomputed session paths static constexpr array SESSION_PATHS = { "/usr/share/xsessions", "/usr/share/wayland-sessions" }; // Single memory reserve for lowercase conversions string lowercaseStem; lowercaseStem.reserve(32); for (const auto& path : SESSION_PATHS) { if (!fs::exists(path)) continue; for (const auto& entry : fs::directory_iterator(path)) { if (!entry.is_regular_file()) continue; // Reuse buffer lowercaseStem = entry.path().stem().string(); transform(lowercaseStem, lowercaseStem.begin(), ::tolower); // Modern ranges version const pair* const patternIter = lower_bound( DE_PATTERNS, lowercaseStem, less {}, &pair::first // Projection ); if (patternIter != DE_PATTERNS.end() && patternIter->first == lowercaseStem) return string(patternIter->second); } } return nullopt; } fn DetectFromProcesses() -> optional { const array processChecks = { // clang-format off pair { "plasmashell"sv, "KDE"sv }, pair { "gnome-shell"sv, "GNOME"sv }, pair { "xfce4-session"sv, "XFCE"sv }, pair { "mate-session"sv, "MATE"sv }, pair { "cinnamon-sessio"sv, "Cinnamon"sv }, pair { "budgie-wm"sv, "Budgie"sv }, pair { "lxqt-session"sv, "LXQt"sv }, // clang-format on }; ifstream cmdline("/proc/self/environ"); string envVars((istreambuf_iterator(cmdline)), istreambuf_iterator()); for (const auto& [process, deName] : processChecks) if (envVars.contains(process)) return string(deName); return nullopt; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wold-style-cast" fn GetMprisPlayers(DBusConnection* connection) -> vector { vector mprisPlayers; DBusError err; dbus_error_init(&err); // 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 ); 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 GetActivePlayer(const vector& mprisPlayers) -> optional { if (!mprisPlayers.empty()) return mprisPlayers.front(); return nullopt; } } fn GetOSVersion() -> expected { constexpr const char* path = "/etc/os-release"; ifstream file(path); if (!file.is_open()) return unexpected("Failed to open " + string(path)); string line; const string prefix = "PRETTY_NAME="; while (getline(file, line)) if (line.starts_with(prefix)) { string prettyName = line.substr(prefix.size()); if (!prettyName.empty() && prettyName.front() == '"' && prettyName.back() == '"') return prettyName.substr(1, prettyName.size() - 2); return prettyName; } return unexpected("PRETTY_NAME line not found in " + string(path)); } fn GetMemInfo() -> expected { using std::from_chars, std::errc; constexpr const char* path = "/proc/meminfo"; ifstream input(path); if (!input.is_open()) return unexpected("Failed to open " + string(path)); string line; while (getline(input, line)) { if (line.starts_with("MemTotal")) { const size_t colonPos = line.find(':'); if (colonPos == string::npos) return unexpected("Invalid MemTotal line: no colon found"); string_view view(line); view.remove_prefix(colonPos + 1); // Trim leading whitespace const size_t firstNonSpace = view.find_first_not_of(' '); if (firstNonSpace == string_view::npos) return unexpected("No number found after colon in MemTotal line"); view.remove_prefix(firstNonSpace); // Find the end of the numeric part const size_t end = view.find_first_not_of("0123456789"); if (end != string_view::npos) view = view.substr(0, end); // Get pointers via iterators const char* startPtr = &*view.begin(); const char* endPtr = &*view.end(); u64 value = 0; const auto result = from_chars(startPtr, endPtr, value); if (result.ec != errc() || result.ptr != endPtr) return unexpected("Failed to parse number in MemTotal line"); return value * 1024; } } return unexpected("MemTotal line not found in " + string(path)); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wold-style-cast" fn GetNowPlaying() -> expected { DBusError err; dbus_error_init(&err); // Connect to the session bus DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &err); if (!connection) if (dbus_error_is_set(&err)) { ERROR_LOG("DBus connection error: {}", err.message); NowPlayingError error = LinuxError(err.message); dbus_error_free(&err); return unexpected(error); } vector mprisPlayers = GetMprisPlayers(connection); if (mprisPlayers.empty()) { dbus_connection_unref(connection); return unexpected(NowPlayingError { NowPlayingCode::NoPlayers }); } optional activePlayer = GetActivePlayer(mprisPlayers); if (!activePlayer.has_value()) { dbus_connection_unref(connection); return unexpected(NowPlayingError { NowPlayingCode::NoActivePlayer }); } // 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 unexpected(NowPlayingError { /* error creating 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 unexpected(NowPlayingError { /* error appending arguments */ }); } // 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); NowPlayingError error = LinuxError(err.message); dbus_error_free(&err); dbus_connection_unref(connection); return unexpected(error); } if (!reply) { dbus_connection_unref(connection); return unexpected(NowPlayingError { /* no reply error */ }); } // 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 unexpected(NowPlayingError { /* no arguments in reply */ }); } if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) { dbus_message_unref(reply); dbus_connection_unref(connection); return unexpected(NowPlayingError { /* unexpected argument type */ }); } // 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 unexpected(NowPlayingError { /* expected array type */ }); } 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); string result; if (!artist.empty() && !title.empty()) result = artist + " - " + title; else if (!title.empty()) result = title; else if (!artist.empty()) result = artist; else result = ""; return result; } #pragma clang diagnostic pop fn GetWindowManager() -> string { // Check environment variables first const char* xdgSessionType = getenv("XDG_SESSION_TYPE"); const char* waylandDisplay = getenv("WAYLAND_DISPLAY"); // Prefer Wayland detection if Wayland session if ((waylandDisplay != nullptr) || (xdgSessionType && string_view(xdgSessionType).contains("wayland"))) { string compositor = GetWaylandCompositor(); if (!compositor.empty()) return compositor; // Fallback environment check const char* xdgCurrentDesktop = getenv("XDG_CURRENT_DESKTOP"); if (xdgCurrentDesktop) { string desktop(xdgCurrentDesktop); transform(compositor, compositor.begin(), ::tolower); if (desktop.contains("hyprland")) return "hyprland"; } } // X11 detection string x11wm = GetX11WindowManager(); if (!x11wm.empty()) return x11wm; return "Unknown"; } fn GetDesktopEnvironment() -> optional { // Try environment variables first if (auto desktopEnvironment = DetectFromEnvVars(); desktopEnvironment.has_value()) return desktopEnvironment; // Try session files next if (auto desktopEnvironment = DetectFromSessionFiles(); desktopEnvironment.has_value()) return desktopEnvironment; // Fallback to process detection return DetectFromProcesses(); } fn GetShell() -> string { const string_view shell = getenv("SHELL"); if (shell.ends_with("bash")) return "Bash"; if (shell.ends_with("zsh")) return "Zsh"; if (shell.ends_with("fish")) return "Fish"; if (shell.ends_with("nu")) return "Nushell"; if (shell.ends_with("sh")) return "SH"; return !shell.empty() ? string(shell) : ""; } fn GetHost() -> string { constexpr const char* path = "/sys/class/dmi/id/product_family"; ifstream file(path); if (!file.is_open()) { ERROR_LOG("Failed to open {}", path); return ""; } string productFamily; if (!getline(file, productFamily)) { ERROR_LOG("Failed to read from {}", path); return ""; } return productFamily; } fn GetKernelVersion() -> string { struct utsname uts; if (uname(&uts) == -1) { ERROR_LOG("uname() failed: {}", strerror(errno)); return ""; } return static_cast(uts.release); } fn GetDiskUsage() -> pair { struct statvfs stat; if (statvfs("/", &stat) == -1) { ERROR_LOG("statvfs() failed: {}", strerror(errno)); return { 0, 0 }; } return { (stat.f_blocks * stat.f_frsize) - (stat.f_bfree * stat.f_frsize), stat.f_blocks * stat.f_frsize }; } #endif