#ifdef __linux__ // clang-format off #include // needs to be at top for Success/None // clang-format on #include #include #include #include #include #include #include #include #include #include #include "os.h" #include "src/os/linux/display_guards.h" #include "src/util/macros.h" #include "src/util/types.h" using namespace std::string_view_literals; namespace { fn MakeOsErrorFromDBus(const DBus::Error& err) -> OsError { String name = err.name(); if (name == "org.freedesktop.DBus.Error.ServiceUnknown" || name == "org.freedesktop.DBus.Error.NameHasNoOwner") return OsError { OsErrorCode::NotFound, std::format("DBus service/name not found: {}", err.message()) }; if (name == "org.freedesktop.DBus.Error.NoReply" || name == "org.freedesktop.DBus.Error.Timeout") return OsError { OsErrorCode::Timeout, std::format("DBus timeout/no reply: {}", err.message()) }; if (name == "org.freedesktop.DBus.Error.AccessDenied") return OsError { OsErrorCode::PermissionDenied, std::format("DBus access denied: {}", err.message()) }; return OsError { OsErrorCode::PlatformSpecific, std::format("DBus error: {} - {}", name, err.message()) }; } fn MakeOsErrorFromErrno(const String& context = "") -> OsError { const i32 errNo = errno; const String msg = std::system_category().message(errNo); const String fullMsg = context.empty() ? msg : std::format("{}: {}", context, msg); switch (errNo) { case EACCES: case EPERM: return OsError { OsErrorCode::PermissionDenied, fullMsg }; case ENOENT: return OsError { OsErrorCode::NotFound, fullMsg }; case ETIMEDOUT: return OsError { OsErrorCode::Timeout, fullMsg }; case ENOTSUP: return OsError { OsErrorCode::NotSupported, fullMsg }; case EIO: return OsError { OsErrorCode::IoError, fullMsg }; case ECONNREFUSED: case ENETDOWN: case ENETUNREACH: return OsError { OsErrorCode::NetworkError, fullMsg }; default: return OsError { OsErrorCode::PlatformSpecific, fullMsg }; } } fn GetX11WindowManager() -> Result { using os::linux::XcbReplyGuard; using os::linux::XorgDisplayGuard; const XorgDisplayGuard conn; if (!conn) if (const i32 err = xcb_connection_has_error(conn.get()); !conn || err != 0) return Err( OsError { OsErrorCode::ApiUnavailable, [&] -> String { switch (err) { case 0: return "Connection object invalid, but no specific XCB error code"; case XCB_CONN_ERROR: return "Stream/Socket/Pipe Error"; case XCB_CONN_CLOSED_EXT_NOTSUPPORTED: return "Closed: Extension Not Supported"; case XCB_CONN_CLOSED_MEM_INSUFFICIENT: return "Closed: Insufficient Memory"; case XCB_CONN_CLOSED_REQ_LEN_EXCEED: return "Closed: Request Length Exceeded"; case XCB_CONN_CLOSED_PARSE_ERR: return "Closed: Display String Parse Error"; case XCB_CONN_CLOSED_INVALID_SCREEN: return "Closed: Invalid Screen"; case XCB_CONN_CLOSED_FDPASSING_FAILED: return "Closed: FD Passing Failed"; default: return std::format("Unknown Error Code ({})", err); } }(), } ); fn internAtom = [&conn](const StringView name) -> Result { const XcbReplyGuard reply(xcb_intern_atom_reply( conn.get(), xcb_intern_atom(conn.get(), 0, static_cast(name.size()), name.data()), nullptr )); if (!reply) return Err( OsError { OsErrorCode::PlatformSpecific, std::format("Failed to get X11 atom reply for '{}'", name) } ); return reply->atom; }; const Result supportingWmCheckAtom = internAtom("_NET_SUPPORTING_WM_CHECK"); const Result wmNameAtom = internAtom("_NET_WM_NAME"); const Result utf8StringAtom = internAtom("UTF8_STRING"); if (!supportingWmCheckAtom || !wmNameAtom || !utf8StringAtom) { if (!supportingWmCheckAtom) ERROR_LOG("Failed to get _NET_SUPPORTING_WM_CHECK atom"); if (!wmNameAtom) ERROR_LOG("Failed to get _NET_WM_NAME atom"); if (!utf8StringAtom) ERROR_LOG("Failed to get UTF8_STRING atom"); return Err(OsError { OsErrorCode::PlatformSpecific, "Failed to get X11 atoms" }); } const XcbReplyGuard wmWindowReply(xcb_get_property_reply( conn.get(), xcb_get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, XCB_ATOM_WINDOW, 0, 1), nullptr )); if (!wmWindowReply || wmWindowReply->type != XCB_ATOM_WINDOW || wmWindowReply->format != 32 || xcb_get_property_value_length(wmWindowReply.get()) == 0) return Err(OsError { OsErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property" }); const xcb_window_t wmRootWindow = *static_cast(xcb_get_property_value(wmWindowReply.get())); const XcbReplyGuard wmNameReply(xcb_get_property_reply( conn.get(), xcb_get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr )); if (!wmNameReply || wmNameReply->type != *utf8StringAtom || xcb_get_property_value_length(wmNameReply.get()) == 0) return Err(OsError { OsErrorCode::NotFound, "Failed to get _NET_WM_NAME property" }); const char* nameData = static_cast(xcb_get_property_value(wmNameReply.get())); const usize length = xcb_get_property_value_length(wmNameReply.get()); return String(nameData, length); } fn GetWaylandCompositor() -> Result { using os::linux::WaylandDisplayGuard; const WaylandDisplayGuard display; if (!display) return Err(OsError { OsErrorCode::NotFound, "Failed to connect to display (is Wayland running?)" }); const i32 fileDescriptor = display.fd(); if (fileDescriptor < 0) return Err(OsError { OsErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor" }); ucred cred; socklen_t len = sizeof(cred); if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) return Err(MakeOsErrorFromErrno("Failed to get socket credentials (SO_PEERCRED)")); Array exeLinkPathBuf; auto [out, size] = std::format_to_n(exeLinkPathBuf.data(), exeLinkPathBuf.size() - 1, "/proc/{}/exe", cred.pid); if (out >= exeLinkPathBuf.data() + exeLinkPathBuf.size() - 1) return Err(OsError { OsErrorCode::InternalError, "Failed to format /proc path (PID too large?)" }); *out = '\0'; const char* exeLinkPath = exeLinkPathBuf.data(); Array exeRealPathBuf; const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1); if (bytesRead == -1) return Err(MakeOsErrorFromErrno(std::format("Failed to read link '{}'", exeLinkPath))); exeRealPathBuf.at(bytesRead) = '\0'; StringView compositorNameView; const StringView pathView(exeRealPathBuf.data(), bytesRead); StringView filenameView; if (const usize lastCharPos = pathView.find_last_not_of('/'); lastCharPos != StringView::npos) { const StringView relevantPart = pathView.substr(0, lastCharPos + 1); if (const usize separatorPos = relevantPart.find_last_of('/'); separatorPos == StringView::npos) filenameView = relevantPart; else filenameView = relevantPart.substr(separatorPos + 1); } if (!filenameView.empty()) compositorNameView = filenameView; if (compositorNameView.empty() || compositorNameView == "." || compositorNameView == "/") return Err(OsError { OsErrorCode::NotFound, "Failed to get compositor name from path" }); if (constexpr StringView wrappedSuffix = "-wrapped"; compositorNameView.length() > 1 + wrappedSuffix.length() && compositorNameView[0] == '.' && compositorNameView.ends_with(wrappedSuffix)) { const StringView cleanedView = compositorNameView.substr(1, compositorNameView.length() - 1 - wrappedSuffix.length()); if (cleanedView.empty()) return Err(OsError { OsErrorCode::NotFound, "Compositor name invalid after heuristic" }); return String(cleanedView); } return String(compositorNameView); } fn GetMprisPlayers(const SharedPointer& connection) -> Result { try { const SharedPointer call = DBus::CallMessage::create("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"); const SharedPointer reply = connection->send_with_reply_blocking(call, 5); if (!reply || !reply->is_valid()) return Err(OsError { OsErrorCode::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(OsError { OsErrorCode::NotFound, "No MPRIS players found" }); } catch (const DBus::Error& e) { return Err(MakeOsErrorFromDBus(e)); } catch (const Exception& e) { return Err(OsError { OsErrorCode::InternalError, e.what() }); } } 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(OsError { OsErrorCode::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( OsError { OsErrorCode::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; Option album = None; Option appName = None; // Try to get app name too if (auto titleIter = metadata.find("xesam:title"); titleIter != metadata.end() && titleIter->second.type() == DBus::DataType::STRING) title = titleIter->second.to_string(); if (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(); } } if (auto albumIter = metadata.find("xesam:album"); albumIter != metadata.end() && albumIter->second.type() == DBus::DataType::STRING) album = albumIter->second.to_string(); try { const SharedPointer identityCall = DBus::CallMessage::create(playerBusName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get"); *identityCall << "org.mpris.MediaPlayer2" << "Identity"; if (const SharedPointer identityReply = connection->send_with_reply_blocking(identityCall, 500); identityReply && identityReply->is_valid()) { DBus::MessageIterator identityIter(*identityReply); DBus::Variant identityVariant; identityIter >> identityVariant; if (identityVariant.type() == DBus::DataType::STRING) appName = identityVariant.to_string(); } } catch (const DBus::Error& e) { DEBUG_LOG("Failed to get player Identity property for {}: {}", playerBusName, e.what()); // Non-fatal } return MediaInfo(std::move(title), std::move(artist), std::move(album), std::move(appName)); } catch (const DBus::Error& e) { return Err(MakeOsErrorFromDBus(e)); } catch (const Exception& e) { return Err( OsError { OsErrorCode::InternalError, std::format("Standard exception processing metadata: {}", e.what()) } ); } } } fn os::GetOSVersion() -> Result { constexpr CStr path = "/etc/os-release"; std::ifstream file(path); if (!file) return Err(OsError { OsErrorCode::NotFound, std::format("Failed to open {}", path) }); String line; constexpr StringView prefix = "PRETTY_NAME="; while (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.empty()) return Err( OsError { OsErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path) } ); return value; } } return Err(OsError { OsErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path) }); } fn os::GetMemInfo() -> Result { struct sysinfo info; if (sysinfo(&info) != 0) return Err(MakeOsErrorFromErrno("sysinfo call failed")); const u64 totalRam = info.totalram; const u64 memUnit = info.mem_unit; if (memUnit == 0) return Err(OsError { OsErrorCode::InternalError, "sysinfo returned mem_unit of zero" }); if (totalRam > std::numeric_limits::max() / memUnit) return Err(OsError { OsErrorCode::InternalError, "Potential overflow calculating total RAM" }); return info.totalram * info.mem_unit; } 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(OsError { OsErrorCode::ApiUnavailable, "Failed to create DBus dispatcher" }); connection = dispatcher->create_connection(DBus::BusType::SESSION); if (!connection) return Err(OsError { OsErrorCode::ApiUnavailable, "Failed to connect to DBus session bus" }); } catch (const DBus::Error& e) { return Err(MakeOsErrorFromDBus(e)); } catch (const Exception& e) { return Err(OsError { OsErrorCode::InternalError, e.what() }); } Result playerBusName = GetMprisPlayers(connection); if (!playerBusName) return Err(playerBusName.error()); 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 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 EnvError&) -> Result { return 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 = GetEnv("SHELL")) { // clang-format off constexpr Array, 5> shellMap {{ { "bash", "Bash" }, { "zsh", "Zsh" }, { "fish", "Fish" }, { "nu", "Nushell" }, { "sh", "SH" }, // sh last because other shells contain "sh" }}; // clang-format on for (const auto& [exe, name] : shellMap) if (shellPath->contains(exe)) return String(name); return *shellPath; // fallback to the raw shell path } return None; } 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; if (!file) return Err( OsError { OsErrorCode::NotFound, std::format("Failed to open DMI product identifier file '{}'", path) } ); if (!getline(file, line)) return Err(OsError { OsErrorCode::ParseError, std::format("DMI product identifier file ('{}') is empty", path) }); return line; }; return readFirstLine(primaryPath).or_else([&](const OsError& primaryError) -> Result { return readFirstLine(fallbackPath).or_else([&](const OsError& fallbackError) -> Result { return Err( OsError { OsErrorCode::InternalError, std::format( "Failed to get host identifier. Primary ('{}'): {}. Fallback ('{}'): {}", primaryPath, primaryError.message, fallbackPath, fallbackError.message ), } ); }); }); } fn os::GetKernelVersion() -> Result { utsname uts; if (uname(&uts) == -1) return Err(MakeOsErrorFromErrno("uname call failed")); if (strlen(uts.release) == 0) return Err(OsError { OsErrorCode::ParseError, "uname returned null kernel release" }); return uts.release; } fn os::GetDiskUsage() -> Result { struct statvfs stat; if (statvfs("/", &stat) == -1) return Err(MakeOsErrorFromErrno(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, }; } #endif