diff --git a/.clang-format b/.clang-format index 7385b38..0d77533 100644 --- a/.clang-format +++ b/.clang-format @@ -4,6 +4,7 @@ AlignConsecutiveAssignments: true AlignConsecutiveShortCaseStatements: Enabled: true AlignConsecutiveDeclarations: true +AlignOperands: DontAlign AllowShortBlocksOnASingleLine: Always AllowShortCaseLabelsOnASingleLine: true AllowShortEnumsOnASingleLine: true diff --git a/.clang-tidy b/.clang-tidy index 98bf1c6..26d25bd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -31,6 +31,7 @@ Checks: > -readability-isolate-declaration, -readability-magic-numbers CheckOptions: + cppcoreguidelines-avoid-do-while.IgnoreMacros: "true" readability-identifier-naming.ClassCase: CamelCase readability-identifier-naming.EnumCase: CamelCase readability-identifier-naming.LocalConstantCase: camelBack diff --git a/src/core/system_data.h b/src/core/system_data.h index 0f011b8..2e1a5da 100644 --- a/src/core/system_data.h +++ b/src/core/system_data.h @@ -65,7 +65,7 @@ struct SystemData { Result os_version; ///< OS pretty name (e.g., "Ubuntu 22.04 LTS") or an error message. Result mem_info; ///< Total physical RAM in bytes or an error message. Option desktop_environment; ///< Detected desktop environment (e.g., "GNOME", "KDE", "Fluent (Windows 11)"). Might be None. - String window_manager; ///< Detected window manager (e.g., "Mutter", "KWin", "DWM"). + Option window_manager; ///< Detected window manager (e.g., "Mutter", "KWin", "DWM"). NowPlayingResult now_playing; ///< Currently playing media ("Artist - Title") or an error/None if disabled/unavailable. Option weather_info; ///< Weather information or None if disabled/unavailable. u64 disk_used; ///< Used disk space in bytes for the root filesystem. diff --git a/src/main.cpp b/src/main.cpp index 6d60c5f..34cde63 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -199,8 +199,8 @@ namespace { if (data.desktop_environment && *data.desktop_environment != data.window_manager) content.push_back(createRow(deIcon, "DE", *data.desktop_environment)); - if (!data.window_manager.empty()) - content.push_back(createRow(wmIcon, "WM", data.window_manager)); + if (data.window_manager) + content.push_back(createRow(wmIcon, "WM", *data.window_manager)); // Now Playing row if (nowPlayingEnabled && data.now_playing) { diff --git a/src/os/linux.cpp b/src/os/linux.cpp index b31efe2..ffcfbdb 100644 --- a/src/os/linux.cpp +++ b/src/os/linux.cpp @@ -19,28 +19,17 @@ #include "src/os/linux/display_guards.h" #include "src/util/macros.h" -namespace fs = std::filesystem; using namespace std::string_view_literals; namespace { - constexpr auto Trim(StringView sview) -> StringView { - using namespace std::ranges; - - constexpr auto isSpace = [](const char character) { return std::isspace(static_cast(character)); }; - - const borrowed_iterator_t start = find_if_not(sview, isSpace); - const borrowed_iterator_t> rstart = find_if_not(sview | views::reverse, isSpace); - - return sview.substr(start - sview.begin(), sview.size() - (rstart - sview.rbegin())); - } - - fn GetX11WindowManager() -> String { + fn GetX11WindowManager() -> Option { using os::linux::XcbReplyGuard; using os::linux::XorgDisplayGuard; const XorgDisplayGuard conn; + if (!conn) - return ""; + RETURN_ERR("Failed to open X11 display"); fn internAtom = [&conn](const StringView name) -> XcbReplyGuard { const auto cookie = xcb_intern_atom(conn.get(), 0, static_cast(name.size()), name.data()); @@ -52,153 +41,108 @@ namespace { const XcbReplyGuard utf8String = internAtom("UTF8_STRING"); if (!supportingWmCheck || !wmName || !utf8String) - return "Unknown (X11)"; + RETURN_ERR("Failed to get X11 atoms"); - const xcb_window_t root = conn.rootScreen()->root; - - fn getProperty = [&conn]( - const xcb_window_t window, - const xcb_atom_t property, - const xcb_atom_t type, - const uint32_t offset, - const uint32_t length - ) -> XcbReplyGuard { - const xcb_get_property_cookie_t cookie = xcb_get_property(conn.get(), 0, window, property, type, offset, length); - return XcbReplyGuard(xcb_get_property_reply(conn.get(), cookie, nullptr)); - }; - - const XcbReplyGuard wmWindowReply = - getProperty(root, supportingWmCheck->atom, XCB_ATOM_WINDOW, 0, 1); + const XcbReplyGuard wmWindowReply(xcb_get_property_reply( + conn.get(), + xcb_get_property(conn.get(), 0, conn.rootScreen()->root, supportingWmCheck->atom, 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 "Unknown (X11)"; + RETURN_ERR("Failed to get _NET_SUPPORTING_WM_CHECK property"); - const xcb_window_t wmWindow = *static_cast(xcb_get_property_value(wmWindowReply.get())); - - const XcbReplyGuard wmNameReply = - getProperty(wmWindow, wmName->atom, utf8String->atom, 0, 1024); + const XcbReplyGuard wmNameReply(xcb_get_property_reply( + conn.get(), + xcb_get_property( + conn.get(), + 0, + *static_cast(xcb_get_property_value(wmWindowReply.get())), + wmName->atom, + utf8String->atom, + 0, + 1024 + ), + nullptr + )); if (!wmNameReply || wmNameReply->type != utf8String->atom || xcb_get_property_value_length(wmNameReply.get()) == 0) - return "Unknown (X11)"; + RETURN_ERR("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 { nameData, length }; + return String(nameData, length); } - fn ReadProcessCmdline(const i32 pid) -> String { - std::ifstream cmdlineFile("/proc/" + std::to_string(pid) + "/cmdline"); - - if (String cmdline; getline(cmdlineFile, cmdline)) { - std::ranges::replace(cmdline, '\0', ' '); - return cmdline; - } - - return ""; - } - - fn DetectHyprlandSpecific() -> Option { - if (Result xdgCurrentDesktop = GetEnv("XDG_CURRENT_DESKTOP")) { - std::ranges::transform(*xdgCurrentDesktop, xdgCurrentDesktop->begin(), tolower); - - if (xdgCurrentDesktop->contains("hyprland"sv)) - return "Hyprland"; - } - - if (GetEnv("HYPRLAND_INSTANCE_SIGNATURE")) - return "Hyprland"; - - if (fs::exists(std::format("/run/user/{}/hypr", getuid()))) - return "Hyprland"; - - return None; - } - - fn GetWaylandCompositor() -> String { + fn GetWaylandCompositor() -> Option { using os::linux::WaylandDisplayGuard; - if (const Option hypr = DetectHyprlandSpecific()) - return *hypr; - const WaylandDisplayGuard display; if (!display) - return ""; + RETURN_ERR("Failed to open Wayland display"); const i32 fileDescriptor = display.fd(); + if (fileDescriptor < 0) + RETURN_ERR("Failed to get Wayland file descriptor"); - ucred cred; - u32 len = sizeof(cred); + ucred cred; + socklen_t len = sizeof(cred); if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) - return ""; + RETURN_ERR("Failed to get socket credentials: {}", std::error_code(errno, std::generic_category()).message()); + + 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("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("Failed to read link {}: {}", exeLinkPath, std::error_code(errno, std::generic_category()).message()); + + exeRealPathBuf.at(bytesRead) = '\0'; String compositorName; - const String commPath = std::format("/proc/{}/comm", cred.pid); - if (std::ifstream commFile(commPath); commFile >> compositorName) { - const std::ranges::subrange removedRange = std::ranges::remove(compositorName, '\n'); - compositorName.erase(removedRange.begin(), removedRange.end()); + try { + namespace fs = std::filesystem; + + const fs::path exePath(exeRealPathBuf.data()); + + compositorName = exePath.filename().string(); + } catch (const std::filesystem::filesystem_error& e) { + RETURN_ERR("Error getting compositor name from path '{}': {}", exeRealPathBuf.data(), e.what()); + } catch (...) { RETURN_ERR("Unknown error getting compositor name"); } + + if (compositorName.empty() || compositorName == "." || compositorName == "/") + RETURN_ERR("Empty or invalid compositor name {}", compositorName); + + const StringView compositorNameView = compositorName; + + 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("Compositor name invalid after heuristic: original='%s'\n", compositorName.c_str()); + + return String(cleanedView); } - if (const String cmdline = ReadProcessCmdline(cred.pid); cmdline.contains("hyprland"sv)) - return "Hyprland"; - - const String exePath = std::format("/proc/{}/exe", cred.pid); - - Array buf; - if (const isize lenBuf = readlink(exePath.c_str(), buf.data(), buf.size() - 1); lenBuf != -1) { - buf.at(static_cast(lenBuf)) = '\0'; - if (const String exe(buf.data()); exe.contains("hyprland"sv)) - return "Hyprland"; - } - - return compositorName.contains("hyprland"sv) ? "Hyprland" : compositorName; - } - - fn DetectFromEnvVars() -> Option { - if (Result xdgCurrentDesktop = GetEnv("XDG_CURRENT_DESKTOP")) { - if (const usize colon = xdgCurrentDesktop->find(':'); colon != String::npos) - return xdgCurrentDesktop->substr(0, colon); - - DEBUG_LOG("Found XDG_CURRENT_DESKTOP: {}", *xdgCurrentDesktop); - - return *xdgCurrentDesktop; - } - - if (Result desktopSession = GetEnv("DESKTOP_SESSION")) { - DEBUG_LOG("Found DESKTOP_SESSION: {}", *desktopSession); - return *desktopSession; - } - - return None; - } - - fn DetectFromProcesses() -> Option { - // clang-format off - const Array, 7> processChecks = {{ - { "plasmashell", "KDE" }, - { "gnome-shell", "GNOME" }, - { "xfce4-session", "XFCE" }, - { "mate-session", "MATE" }, - { "cinnamon-session", "Cinnamon" }, - { "budgie-wm", "Budgie" }, - { "lxqt-session", "LXQt" }, - }}; - // clang-format on - - std::ifstream cmdline("/proc/self/environ"); - const String envVars((std::istreambuf_iterator(cmdline)), std::istreambuf_iterator()); - - for (const auto& [process, deName] : processChecks) - if (envVars.contains(process)) { - DEBUG_LOG("Found from process check: {}", deName); - return String(deName); - } - - return None; + return compositorName; } fn GetMprisPlayers(const SharedPointer& connection) -> Result, NowPlayingError> { @@ -275,6 +219,7 @@ fn os::GetNowPlaying() -> Result { return Err("Failed to create DBus dispatcher"); const SharedPointer connection = dispatcher->create_connection(DBus::BusType::SESSION); + if (!connection) return Err("Failed to connect to session bus"); @@ -335,36 +280,31 @@ fn os::GetNowPlaying() -> Result { } } -fn os::GetWindowManager() -> String { - const Result waylandDisplay = GetEnv("WAYLAND_DISPLAY"); - - if (const Result xdgSessionType = GetEnv("XDG_SESSION_TYPE"); - waylandDisplay || (xdgSessionType && xdgSessionType->contains("wayland"sv))) { - String compositor = GetWaylandCompositor(); - - if (!compositor.empty()) { - DEBUG_LOG("Found compositor: {}", compositor); - return compositor; - } - - if (const Result xdgCurrentDesktop = GetEnv("XDG_CURRENT_DESKTOP")) { - std::ranges::transform(compositor, compositor.begin(), tolower); - if (xdgCurrentDesktop->contains("hyprland"sv)) - return "Hyprland"; - } - } - - if (String x11wm = GetX11WindowManager(); !x11wm.empty()) - return x11wm; - - return "Unknown"; +fn os::GetWindowManager() -> Option { + // clang-format off + return GetWaylandCompositor() + .or_else([] { return GetX11WindowManager(); }) + .and_then([](const String& windowManager) -> Option { + DEBUG_LOG("Found window manager: {}", windowManager); + return windowManager; + }); + // clang-format on } fn os::GetDesktopEnvironment() -> Option { - if (Option desktopEnvironment = DetectFromEnvVars()) - return desktopEnvironment; + 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 DetectFromProcesses(); + 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() -> String { @@ -406,7 +346,7 @@ fn os::GetHost() -> String { return ""; } - return String(Trim(productFamily)); + return productFamily; } fn os::GetKernelVersion() -> String { diff --git a/src/os/os.h b/src/os/os.h index 8611e97..242de24 100644 --- a/src/os/os.h +++ b/src/os/os.h @@ -48,9 +48,9 @@ namespace os { * @brief Attempts to retrieve the window manager. * @details On Linux, checks Wayland compositor or X11 WM properties. On Windows, returns "DWM" or similar. * On macOS, might return "Quartz Compositor" or a specific tiling WM name if active. - * @return A String containing the detected WM name. Might return "Unknown" or a default value if detection fails. + * @return A String containing the detected WM name, or None if detection fails or is not applicable. */ - fn GetWindowManager() -> String; + fn GetWindowManager() -> Option; /** * @brief Attempts to detect the current user shell. diff --git a/src/util/macros.h b/src/util/macros.h index e868601..f56ec8f 100644 --- a/src/util/macros.h +++ b/src/util/macros.h @@ -217,7 +217,7 @@ fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_st } }(); - Print(BrightWhite, "[{:%X}] ", zoned_time { current_zone(), std::chrono::floor(system_clock::now()) }); + Print(BrightWhite, "[{:%X}] ", std::chrono::floor(system_clock::now())); Print(Emphasis::Bold | color, "{} ", levelStr); Print(fmt, std::forward(args)...); @@ -277,6 +277,18 @@ fn LogImpl(const LogLevel level, const std::source_location& loc, std::format_st */ #define ERROR_LOG(...) LogImpl(LogLevel::ERROR, std::source_location::current(), __VA_ARGS__) +/** + * @def RETURN_ERR(...) + * @brief Logs an error message and returns a value. + * @details Logs the error message with the ERROR log level and returns the specified value. + * @param ... Format string and arguments for the error message. + */ +#define RETURN_ERR(...) \ + do { \ + ERROR_LOG(__VA_ARGS__); \ + return None; \ + } while (0) + #ifdef __clang__ #pragma clang diagnostic pop #endif