From f280792a4525ca1c683d2239f12edfea0be93288 Mon Sep 17 00:00:00 2001 From: Mars Date: Thu, 1 May 2025 23:48:58 -0400 Subject: [PATCH] freebsd support again yay --- .clangd | 2 +- meson.build | 7 +- src/main.cpp | 305 ++++++++++++++++----------- src/os/freebsd.cpp | 444 +++++++++++++++++++++++++++++++++++++++ src/util/logging.hpp | 19 +- src/wrappers/dbus.hpp | 3 - src/wrappers/wayland.hpp | 43 +++- src/wrappers/xcb.hpp | 4 - subprojects/glaze.wrap | 2 +- 9 files changed, 680 insertions(+), 149 deletions(-) create mode 100644 src/os/freebsd.cpp diff --git a/.clangd b/.clangd index 6d4fd28..94f5206 100644 --- a/.clangd +++ b/.clangd @@ -6,4 +6,4 @@ CompileFlags: Diagnostics: Suppress: > -Wmissing-template-arg-list-after-template-kw, - -Wctad-maybe-unsupported \ No newline at end of file + -Wctad-maybe-unsupported diff --git a/meson.build b/meson.build index a0da6b4..7b82397 100644 --- a/meson.build +++ b/meson.build @@ -89,6 +89,7 @@ base_sources = files('src/core/system_data.cpp', 'src/os/shared.cpp', 'src/confi platform_sources = { 'linux' : ['src/os/linux.cpp', 'src/os/linux/pkg_count.cpp'], + 'freebsd' : ['src/os/freebsd.cpp'], 'darwin' : ['src/os/macos.cpp', 'src/os/macos/bridge.mm'], 'windows' : ['src/os/windows.cpp'], } @@ -122,14 +123,14 @@ elif host_system == 'windows' cpp.find_library('dwmapi'), cpp.find_library('windowsapp'), ] -elif host_system == 'linux' +else platform_deps += [ dependency('SQLiteCpp'), dependency('xcb'), dependency('xau'), dependency('xdmcp'), dependency('wayland-client'), - dependency('dbus-1', include_type : 'system'), + dependency('dbus-1'), ] endif @@ -172,7 +173,7 @@ objc_args = [] if host_system == 'darwin' objc_args += ['-fobjc-arc'] -elif cpp.get_id() == 'clang' +elif host_system == 'linux' link_args += ['-static'] endif diff --git a/src/main.cpp b/src/main.cpp index b187418..ce2c9ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -110,158 +110,225 @@ namespace { using namespace util::logging; using namespace ftxui; + // Helper struct to hold row data before calculating max width + struct RowInfo { + StringView icon; + StringView label; + String value; // Store the final formatted value as String + }; + fn CreateColorCircles() -> Element { return hbox( - std::views::iota(0, 16) | std::views::transform([](i32 colorIndex) { + std::views::iota(0, 16) | std::views::transform([](ui::i32 colorIndex) { return hbox({ text("◯") | bold | color(static_cast(colorIndex)), text(" ") }); }) | std::ranges::to() ); } + fn find_max_label_len(const std::vector& rows) -> usize { + usize max_len = 0; + for (const auto& row : rows) { max_len = std::max(max_len, row.label.length()); } + return max_len; + }; + fn SystemInfoBox(const Config& config, const os::SystemData& data) -> Element { - const String& name = config.general.name; - const Weather weather = config.weather; + const String& name = config.general.name; + const Weather& weather = config.weather; const auto& [userIcon, paletteIcon, calendarIcon, hostIcon, kernelIcon, osIcon, memoryIcon, weatherIcon, musicIcon, diskIcon, shellIcon, packageIcon, deIcon, wmIcon] = ui::ICON_TYPE; - Elements content; + // --- Stage 1: Collect data for rows into logical sections --- + std::vector initial_rows; // Date, Weather + std::vector system_info_rows; // Host, Kernel, OS, RAM, Disk, Shell, Packages + std::vector env_info_rows; // DE, WM - content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan)); - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - content.push_back(hbox( - { - text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), - CreateColorCircles(), + // --- Section 1: Date and Weather --- + if (data.date) { + initial_rows.push_back({ calendarIcon, "Date", *data.date }); + } else { + debug_at(data.date.error()); + } + if (weather.enabled && data.weather) { + const weather::Output& weatherInfo = *data.weather; + String weatherValue = weather.showTownName + ? std::format("{}°F in {}", std::lround(weatherInfo.main.temp), weatherInfo.name) + : std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description); + initial_rows.push_back({ weatherIcon, "Weather", std::move(weatherValue) }); + } else if (weather.enabled) { + debug_at(data.weather.error()); + } + + // --- Section 2: Core System Info --- + if (data.host && !data.host->empty()) { + system_info_rows.push_back({ hostIcon, "Host", *data.host }); + } else { + debug_at(data.host.error()); + } + if (data.kernelVersion) { + system_info_rows.push_back({ kernelIcon, "Kernel", *data.kernelVersion }); + } else { + debug_at(data.kernelVersion.error()); + } + if (data.osVersion) { + system_info_rows.push_back({ osIcon, "OS", *data.osVersion }); + } else { + debug_at(data.osVersion.error()); + } + if (data.memInfo) { + system_info_rows.push_back({ memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.memInfo }) }); + } else { + debug_at(data.memInfo.error()); + } + if (data.diskUsage) { + system_info_rows.push_back( + { diskIcon, + "Disk", + std::format("{}/{}", BytesToGiB { data.diskUsage->used_bytes }, BytesToGiB { data.diskUsage->total_bytes }) } + ); + } else { + debug_at(data.diskUsage.error()); + } + if (data.shell) { + system_info_rows.push_back({ shellIcon, "Shell", *data.shell }); + } else { + debug_at(data.shell.error()); + } + if (data.packageCount) { + if (*data.packageCount > 0) { + system_info_rows.push_back({ packageIcon, "Packages", std::format("{}", *data.packageCount) }); + } else { + debug_log("Package count is 0, skipping"); } - )); - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + } else { + debug_at(data.packageCount.error()); + } - // Helper function for aligned rows - fn createRow = [&](const StringView& icon, const StringView& label, const StringView& value) { // NEW + // --- Section 3: Desktop Env / Window Manager --- + bool added_de = false; + if (data.desktopEnv && (!data.windowMgr || *data.desktopEnv != *data.windowMgr)) { + env_info_rows.push_back({ deIcon, "DE", *data.desktopEnv }); + added_de = true; + } else if (!data.desktopEnv) { /* Optional debug */ + } + if (data.windowMgr) { + if (!added_de || (data.desktopEnv && *data.desktopEnv != *data.windowMgr)) { + env_info_rows.push_back({ wmIcon, "WM", *data.windowMgr }); + } + } else { + debug_at(data.windowMgr.error()); + } + + // --- Section 4: Now Playing (Handled separately) --- + bool now_playing_active = false; + String np_text; + if (config.nowPlaying.enabled && data.nowPlaying) { + const String title = data.nowPlaying->title.value_or("Unknown Title"); + const String artist = data.nowPlaying->artist.value_or("Unknown Artist"); + np_text = artist + " - " + title; + now_playing_active = true; + } else if (config.nowPlaying.enabled) { /* Optional debug */ + } + + // --- Stage 2: Calculate max width needed for Icon + Label across relevant sections --- + usize maxActualLabelLen = 0; + auto find_max_label = [&](const std::vector& rows) { + usize max_len = 0; + for (const auto& row : rows) { max_len = std::max(max_len, row.label.length()); } + return max_len; + }; + + maxActualLabelLen = + std::max({ find_max_label(initial_rows), find_max_label(system_info_rows), find_max_label(env_info_rows) }); + // Note: We don't include "Playing" from Now Playing in this calculation + // as it's handled differently, but we could if we wanted perfect alignment. + + // --- Stage 2: Calculate max width needed PER SECTION --- + // Assume consistent icon width for simplicity (adjust if icons vary significantly) + usize iconLen = ui::ICON_TYPE.user.length() - 1; + // Optionally refine iconLen based on actual icons used, if needed + + usize maxLabelLen_initial = find_max_label_len(initial_rows); + usize maxLabelLen_system = find_max_label_len(system_info_rows); + usize maxLabelLen_env = find_max_label_len(env_info_rows); + + usize requiredWidth_initial = iconLen + maxLabelLen_initial; + usize requiredWidth_system = iconLen + maxLabelLen_system; + usize requiredWidth_env = iconLen + maxLabelLen_env; + + // --- Stage 3: Define the row creation function --- + auto createStandardRow = [&](const RowInfo& row, usize sectionRequiredWidth) { + Element leftPart = hbox( + { + text(String(row.icon)) | color(ui::DEFAULT_THEME.icon), + text(String(row.label)) | color(ui::DEFAULT_THEME.label), + } + ); return hbox( { - text(String(icon)) | color(ui::DEFAULT_THEME.icon), - text(String(label)) | color(ui::DEFAULT_THEME.label), + leftPart | size(WIDTH, EQUAL, static_cast(sectionRequiredWidth)), filler(), - text(String(value)) | color(ui::DEFAULT_THEME.value), + text(row.value) | color(ui::DEFAULT_THEME.value), text(" "), } ); }; - // System info rows - if (data.date) - content.push_back(createRow(calendarIcon, "Date", *data.date)); - else - error_at(data.date.error()); + // --- Stage 4: Build the final Elements list with explicit separators and section-specific widths --- + Elements content; - // Weather row - if (weather.enabled && data.weather) { - const weather::Output& weatherInfo = *data.weather; + // Greeting and Palette + content.push_back(text(String(userIcon) + "Hello " + name + "! ") | bold | color(Color::Cyan)); + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); // Separator after greeting + content.push_back(hbox({ text(String(paletteIcon)) | color(ui::DEFAULT_THEME.icon), CreateColorCircles() })); + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); // Separator after palette - if (weather.showTownName) - content.push_back(hbox( - { - text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon), - text("Weather") | color(ui::DEFAULT_THEME.label), - filler(), + // Determine section presence + bool section1_present = !initial_rows.empty(); + bool section2_present = !system_info_rows.empty(); + bool section3_present = !env_info_rows.empty(); + bool section4_present = now_playing_active; - hbox( - { - text(std::format("{}°F ", std::lround(weatherInfo.main.temp))), - text("in "), - text(weatherInfo.name), - text(" "), - } - ) | - color(ui::DEFAULT_THEME.value), - } - )); - else - content.push_back(hbox( - { - text(String(weatherIcon)) | color(ui::DEFAULT_THEME.icon), - text("Weather") | color(ui::DEFAULT_THEME.label), - filler(), - - hbox( - { - text(std::format("{}°F, {}", std::lround(weatherInfo.main.temp), weatherInfo.weather[0].description)), - text(" "), - } - ) | - color(ui::DEFAULT_THEME.value), - } - )); - } else if (weather.enabled) - error_at(data.weather.error()); - - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - - if (data.host && !data.host->empty()) - content.push_back(createRow(hostIcon, "Host", *data.host)); - else - error_at(data.host.error()); - - if (data.kernelVersion) - content.push_back(createRow(kernelIcon, "Kernel", *data.kernelVersion)); - else - error_at(data.kernelVersion.error()); - - if (data.osVersion) - content.push_back(createRow(String(osIcon), "OS", *data.osVersion)); - else - error_at(data.osVersion.error()); - - if (data.memInfo) - content.push_back(createRow(memoryIcon, "RAM", std::format("{}", BytesToGiB { *data.memInfo }))); - else - error_at(data.memInfo.error()); - - if (data.diskUsage) - content.push_back(createRow( - diskIcon, - "Disk", - std::format("{}/{}", BytesToGiB { data.diskUsage->used_bytes }, BytesToGiB { data.diskUsage->total_bytes }) - )); - else - error_at(data.diskUsage.error()); - - if (data.shell) - content.push_back(createRow(shellIcon, "Shell", *data.shell)); - else - error_at(data.shell.error()); - - if (data.packageCount) - content.push_back(createRow(packageIcon, "Packages", std::format("{}", *data.packageCount))); - else - error_at(data.packageCount.error()); - - content.push_back(separator() | color(ui::DEFAULT_THEME.border)); - - if (data.desktopEnv && *data.desktopEnv != data.windowMgr) - content.push_back(createRow(deIcon, "DE", *data.desktopEnv)); - - if (data.windowMgr) - content.push_back(createRow(wmIcon, "WM", *data.windowMgr)); - else - error_at(data.windowMgr.error()); - - if (config.nowPlaying.enabled && data.nowPlaying) { - const String title = data.nowPlaying->title.value_or("Unknown Title"); - const String artist = data.nowPlaying->artist.value_or("Unknown Artist"); - const String npText = artist + " - " + title; + // Add Section 1 (Date/Weather) - Use initial width + for (const auto& row : initial_rows) { content.push_back(createStandardRow(row, requiredWidth_initial)); } + // Separator before Section 2? + if (section1_present && (section2_present || section3_present || section4_present)) { content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + } + + // Add Section 2 (System Info) - Use system width + for (const auto& row : system_info_rows) { content.push_back(createStandardRow(row, requiredWidth_system)); } + + // Separator before Section 3? + if (section2_present && (section3_present || section4_present)) { + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + } + + // Add Section 3 (DE/WM) - Use env width + for (const auto& row : env_info_rows) { content.push_back(createStandardRow(row, requiredWidth_env)); } + + // Separator before Section 4? + if (section3_present && section4_present) { + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + } else if (!section3_present && (section1_present || section2_present) && section4_present) { + content.push_back(separator() | color(ui::DEFAULT_THEME.border)); + } + + // Add Section 4 (Now Playing) + if (section4_present) { + // Pad "Playing" label based on the max label length of the preceding section (Env) + usize playingLabelPadding = maxLabelLen_env; content.push_back(hbox( { text(String(musicIcon)) | color(ui::DEFAULT_THEME.icon), - text("Playing") | color(ui::DEFAULT_THEME.label), - text(" "), + // Pad only the label part + hbox({ text("Playing") | color(ui::DEFAULT_THEME.label) }) | + size(WIDTH, EQUAL, static_cast(playingLabelPadding)), + text(" "), // Space after label filler(), - paragraph(npText) | color(Color::Magenta) | size(WIDTH, LESS_THAN, ui::MAX_PARAGRAPH_LENGTH), + paragraph(np_text) | color(Color::Magenta) | size(WIDTH, LESS_THAN, ui::MAX_PARAGRAPH_LENGTH), text(" "), } )); diff --git a/src/os/freebsd.cpp b/src/os/freebsd.cpp new file mode 100644 index 0000000..93e72ab --- /dev/null +++ b/src/os/freebsd.cpp @@ -0,0 +1,444 @@ +#include // DBUS_TYPE_* +#include // DBUS_BUS_SESSION +#include // ifstream +#include // kenv +#include // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED +#include // statvfs +#include // sysctlbyname +#include +#include +#include +#include // utsname, uname +#include + +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/helpers.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" +#include "src/wrappers/dbus.hpp" +#include "src/wrappers/wayland.hpp" +#include "src/wrappers/xcb.hpp" + +#include "os.hpp" + +using namespace util::types; +using util::error::DracError, util::error::DracErrorCode; + +namespace { + fn GetX11WindowManager() -> Result { + using namespace xcb; + + const DisplayGuard conn; + + if (!conn) + if (const i32 err = connection_has_error(conn.get())) + return Err(DracError(DracErrorCode::ApiUnavailable, [&] -> String { + if (const Option connErr = getConnError(err)) { + switch (*connErr) { + case Generic: return "Stream/Socket/Pipe Error"; + case ExtNotSupported: return "Extension Not Supported"; + case MemInsufficient: return "Insufficient Memory"; + case ReqLenExceed: return "Request Length Exceeded"; + case ParseErr: return "Display String Parse Error"; + case InvalidScreen: return "Invalid Screen"; + case FdPassingFailed: return "FD Passing Failed"; + default: return std::format("Unknown Error Code ({})", err); + } + } + + return std::format("Unknown Error Code ({})", err); + }())); + + fn internAtom = [&conn](const StringView name) -> Result { + const ReplyGuard reply( + intern_atom_reply(conn.get(), intern_atom(conn.get(), 0, static_cast(name.size()), name.data()), nullptr) + ); + + if (!reply) + return Err( + DracError(DracErrorCode::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(DracError(DracErrorCode::PlatformSpecific, "Failed to get X11 atoms")); + } + + const ReplyGuard wmWindowReply(get_property_reply( + conn.get(), + get_property(conn.get(), 0, conn.rootScreen()->root, *supportingWmCheckAtom, ATOM_WINDOW, 0, 1), + nullptr + )); + + if (!wmWindowReply || wmWindowReply->type != ATOM_WINDOW || wmWindowReply->format != 32 || + get_property_value_length(wmWindowReply.get()) == 0) + return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_SUPPORTING_WM_CHECK property")); + + const window_t wmRootWindow = *static_cast(get_property_value(wmWindowReply.get())); + + const ReplyGuard wmNameReply(get_property_reply( + conn.get(), get_property(conn.get(), 0, wmRootWindow, *wmNameAtom, *utf8StringAtom, 0, 1024), nullptr + )); + + if (!wmNameReply || wmNameReply->type != *utf8StringAtom || get_property_value_length(wmNameReply.get()) == 0) + return Err(DracError(DracErrorCode::NotFound, "Failed to get _NET_WM_NAME property")); + + const char* nameData = static_cast(get_property_value(wmNameReply.get())); + const usize length = get_property_value_length(wmNameReply.get()); + + return String(nameData, length); + } + + fn GetWaylandCompositor() -> Result { + const wl::DisplayGuard display; + + if (!display) + return Err(DracError(DracErrorCode::NotFound, "Failed to connect to display (is Wayland running?)")); + + const i32 fileDescriptor = display.fd(); + if (fileDescriptor < 0) + return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get Wayland file descriptor")); + + xucred cred; + socklen_t len = sizeof(cred); + + if (getsockopt(fileDescriptor, SOL_SOCKET, LOCAL_PEERCRED, &cred, &len) == -1) + return Err(DracError::withErrno("Failed to get socket credentials (SO_PEERCRED)")); + + Array exeLinkPathBuf; + + const isize count = exeLinkPathBuf.size() - 1; + + std::format_to_n_result result = std::format_to_n(exeLinkPathBuf.data(), count, "/proc/{}/exe", cred.cr_uid); + + if (result.size >= count) + return Err(DracError(DracErrorCode::InternalError, "Failed to format /proc path (PID too large?)")); + + *result.out = '\0'; + + const char* exeLinkPath = exeLinkPathBuf.data(); + + Array exeRealPathBuf; // NOLINT(misc-include-cleaner) - PATH_MAX is in + + const isize bytesRead = readlink(exeLinkPath, exeRealPathBuf.data(), exeRealPathBuf.size() - 1); + + if (bytesRead == -1) + return Err(DracError::withErrno(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(DracError(DracErrorCode::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(DracError(DracErrorCode::NotFound, "Compositor name invalid after heuristic")); + + return String(cleanedView); + } + + return String(compositorNameView); + } +} // namespace + +namespace os { + using util::helpers::GetEnv; + + fn GetOSVersion() -> Result { + constexpr CStr path = "/etc/os-release"; + + std::ifstream file(path); + + if (!file) + return Err(DracError(DracErrorCode::NotFound, std::format("Failed to open {}", path))); + + String line; + constexpr StringView prefix = "NAME="; + + 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.empty()) + return Err( + DracError(DracErrorCode::ParseError, std::format("PRETTY_NAME value is empty or only quotes in {}", path)) + ); + + return value; + } + } + + return Err(DracError(DracErrorCode::NotFound, std::format("PRETTY_NAME line not found in {}", path))); + } + + fn GetMemInfo() -> Result { + u64 mem = 0; + usize size = sizeof(mem); + + sysctlbyname("hw.physmem", &mem, &size, nullptr, 0); + + return mem; + } + + fn GetNowPlaying() -> Result { + using namespace dbus; + + Result connectionResult = Connection::busGet(DBUS_BUS_SESSION); + if (!connectionResult) + return Err(connectionResult.error()); + + const Connection& connection = *connectionResult; + + Option activePlayer = None; + + { + Result listNamesResult = + Message::newMethodCall("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames"); + if (!listNamesResult) + return Err(listNamesResult.error()); + + Result listNamesReplyResult = connection.sendWithReplyAndBlock(*listNamesResult, 100); + if (!listNamesReplyResult) + return Err(listNamesReplyResult.error()); + + MessageIter iter = listNamesReplyResult->iterInit(); + if (!iter.isValid() || iter.getArgType() != DBUS_TYPE_ARRAY) + return Err(DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Expected array")); + + MessageIter subIter = iter.recurse(); + if (!subIter.isValid()) + return Err( + DracError(DracErrorCode::ParseError, "Invalid DBus ListNames reply format: Could not recurse into array") + ); + + while (subIter.getArgType() != DBUS_TYPE_INVALID) { + if (Option name = subIter.getString()) + if (name->starts_with("org.mpris.MediaPlayer2.")) { + activePlayer = std::move(*name); + break; + } + if (!subIter.next()) + break; + } + } + + if (!activePlayer) + return Err(DracError(DracErrorCode::NotFound, "No active MPRIS players found")); + + Result msgResult = Message::newMethodCall( + activePlayer->c_str(), "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get" + ); + + if (!msgResult) + return Err(msgResult.error()); + + Message& msg = *msgResult; + + if (!msg.appendArgs("org.mpris.MediaPlayer2.Player", "Metadata")) + return Err(DracError(DracErrorCode::InternalError, "Failed to append arguments to Properties.Get message")); + + Result replyResult = connection.sendWithReplyAndBlock(msg, 100); + + if (!replyResult) + return Err(replyResult.error()); + + Option title = None; + Option artist = None; + + MessageIter propIter = replyResult->iterInit(); + if (!propIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply has no arguments or invalid iterator")); + + if (propIter.getArgType() != DBUS_TYPE_VARIANT) + return Err(DracError(DracErrorCode::ParseError, "Properties.Get reply argument is not a variant")); + + MessageIter variantIter = propIter.recurse(); + if (!variantIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Could not recurse into variant")); + + if (variantIter.getArgType() != DBUS_TYPE_ARRAY || variantIter.getElementType() != DBUS_TYPE_DICT_ENTRY) + return Err(DracError(DracErrorCode::ParseError, "Metadata variant content is not a dictionary array (a{sv})")); + + MessageIter dictIter = variantIter.recurse(); + if (!dictIter.isValid()) + return Err(DracError(DracErrorCode::ParseError, "Could not recurse into metadata dictionary array")); + + while (dictIter.getArgType() == DBUS_TYPE_DICT_ENTRY) { + MessageIter entryIter = dictIter.recurse(); + if (!entryIter.isValid()) { + debug_log("Warning: Could not recurse into dict entry, skipping."); + if (!dictIter.next()) + break; + continue; + } + + Option key = entryIter.getString(); + if (!key) { + debug_log("Warning: Could not get key string from dict entry, skipping."); + if (!dictIter.next()) + break; + continue; + } + + if (!entryIter.next() || entryIter.getArgType() != DBUS_TYPE_VARIANT) { + if (!dictIter.next()) + break; + continue; + } + + MessageIter valueVariantIter = entryIter.recurse(); + if (!valueVariantIter.isValid()) { + if (!dictIter.next()) + break; + continue; + } + + if (*key == "xesam:title") { + title = valueVariantIter.getString(); + } else if (*key == "xesam:artist") { + if (valueVariantIter.getArgType() == DBUS_TYPE_ARRAY && valueVariantIter.getElementType() == DBUS_TYPE_STRING) { + if (MessageIter artistArrayIter = valueVariantIter.recurse(); artistArrayIter.isValid()) + artist = artistArrayIter.getString(); + } else { + debug_log("Warning: Artist value was not an array of strings as expected."); + } + } + + if (!dictIter.next()) + break; + } + + return MediaInfo(std::move(title), std::move(artist)); + } + + fn GetWindowManager() -> Result { + if (!GetEnv("DISPLAY") && !GetEnv("WAYLAND_DISPLAY") && !GetEnv("XDG_SESSION_TYPE")) + return Err(DracError(DracErrorCode::NotFound, "Could not find a graphical session")); + + if (Result waylandResult = GetWaylandCompositor()) + return *waylandResult; + + if (Result x11Result = GetX11WindowManager()) + return *x11Result; + + return Err(DracError(DracErrorCode::NotFound, "Could not detect window manager (Wayland/X11) or both failed")); + } + + fn GetDesktopEnvironment() -> Result { + if (!GetEnv("DISPLAY") && !GetEnv("WAYLAND_DISPLAY") && !GetEnv("XDG_SESSION_TYPE")) + return Err(DracError(DracErrorCode::NotFound, "Could not find a graphical session")); + + return GetEnv("XDG_CURRENT_DESKTOP") + .transform([](String xdgDesktop) -> String { + if (const usize colon = xdgDesktop.find(':'); colon != String::npos) + xdgDesktop.resize(colon); + + return xdgDesktop; + }) + .or_else([](const DracError&) -> Result { return GetEnv("DESKTOP_SESSION"); }); + } + + fn GetShell() -> Result { + 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 Err(DracError(DracErrorCode::NotFound, "Could not find SHELL environment variable")); + } + + fn GetHost() -> Result { + auto getKenv = [](const char* name) -> Result { + Array buffer {}; + int result = kenv(KENV_GET, name, buffer.data(), sizeof(buffer)); + + if (result == -1) + return Err(DracError(DracErrorCode::NotFound, std::format("Environment variable '{}' not found", name))); + + return std::string(buffer.data(), buffer.size()); + }; + + return getKenv("smbios.system.product"); + } + + fn GetKernelVersion() -> Result { + utsname uts; + + if (uname(&uts) == -1) + return Err(DracError::withErrno("uname call failed")); + + if (std::strlen(uts.release) == 0) + return Err(DracError(DracErrorCode::ParseError, "uname returned null kernel release")); + + return uts.release; + } + + fn GetDiskUsage() -> Result { + struct statvfs stat; + + if (statvfs("/", &stat) == -1) + return Err(DracError::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 shared::GetPackageCount(); } +} // namespace os diff --git a/src/util/logging.hpp b/src/util/logging.hpp index 2d9e84a..25a72e6 100644 --- a/src/util/logging.hpp +++ b/src/util/logging.hpp @@ -185,27 +185,24 @@ namespace util::logging { const String message = std::format(fmt, std::forward(args)...); - Array buffer {}; - - // Use the locally formatted timestamp string here - auto* iter = std::format_to( - buffer.begin(), - LogLevelConst::LOG_FORMAT, // "{timestamp} {level} {message}" + const String mainLogLine = std::format( + LogLevelConst::LOG_FORMAT, Colorize("[" + timestamp + "]", LogLevelConst::DEBUG_INFO_COLOR), GetLevelInfo().at(static_cast(level)), message ); + std::print("{}", mainLogLine); + #ifndef NDEBUG - const String fileLine = std::format("{}:{}", path(loc.file_name()).lexically_normal().string(), loc.line()); + const String fileLine = + std::format(LogLevelConst::FILE_LINE_FORMAT, path(loc.file_name()).lexically_normal().string(), loc.line()); const String fullDebugLine = std::format("{}{}", LogLevelConst::DEBUG_LINE_PREFIX, fileLine); - iter = std::format_to(iter, "\n{}", Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR))); + std::print("\n{}", Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR))); #endif - const usize length = std::distance(buffer.begin(), iter); - - std::println("{}", StringView(buffer.data(), length)); + std::println("{}", LogLevelConst::RESET_CODE); } template diff --git a/src/wrappers/dbus.hpp b/src/wrappers/dbus.hpp index f98e5a1..cc1e25a 100644 --- a/src/wrappers/dbus.hpp +++ b/src/wrappers/dbus.hpp @@ -1,7 +1,6 @@ #pragma once #include -#ifdef __linux__ // clang-format off #include // DBus Library @@ -385,5 +384,3 @@ namespace dbus { } }; } // namespace dbus - -#endif // __linux__ diff --git a/src/wrappers/wayland.hpp b/src/wrappers/wayland.hpp index 05e4fb7..f780c03 100644 --- a/src/wrappers/wayland.hpp +++ b/src/wrappers/wayland.hpp @@ -1,13 +1,10 @@ #pragma once -#ifdef __linux__ - -// clang-format off #include // Wayland client library #include "src/util/defs.hpp" +#include "src/util/logging.hpp" #include "src/util/types.hpp" -// clang-format on struct wl_display; @@ -31,7 +28,41 @@ namespace wl { /** * Opens a Wayland display connection */ - DisplayGuard() : m_display(connect(nullptr)) {} + DisplayGuard() { + wl_log_set_handler_client([](const char* fmt, va_list args) -> void { + using util::types::i32, util::types::StringView; + + va_list argsCopy; + va_copy(argsCopy, args); + i32 size = std::vsnprintf(nullptr, 0, fmt, argsCopy); + va_end(argsCopy); + + if (size < 0) { + error_log("Wayland: Internal log formatting error (vsnprintf size check failed)."); + return; + } + + std::vector buffer(static_cast(size) + 1); + + i32 writeSize = std::vsnprintf(buffer.data(), buffer.size(), fmt, args); + + if (writeSize < 0 || writeSize >= static_cast(buffer.size())) { + error_log("Wayland: Internal log formatting error (vsnprintf write failed)."); + return; + } + + StringView msgView(buffer.data(), static_cast(writeSize)); + + if (!msgView.empty() && msgView.back() == '\n') + msgView.remove_suffix(1); + + debug_log("Wayland {}", msgView); + }); + + // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) - needs to come after wl_log_set_handler_client + m_display = connect(nullptr); + } + ~DisplayGuard() { if (m_display) disconnect(m_display); @@ -60,5 +91,3 @@ namespace wl { [[nodiscard]] fn fd() const -> util::types::i32 { return get_fd(m_display); } }; } // namespace wl - -#endif // __linux__ diff --git a/src/wrappers/xcb.hpp b/src/wrappers/xcb.hpp index bdc171f..b062e27 100644 --- a/src/wrappers/xcb.hpp +++ b/src/wrappers/xcb.hpp @@ -1,7 +1,5 @@ #pragma once -#ifdef __linux__ - // clang-format off #include // XCB library @@ -170,5 +168,3 @@ namespace xcb { [[nodiscard]] fn operator*() const->T& { return *m_reply; } }; } // namespace xcb - -#endif // __linux__ diff --git a/subprojects/glaze.wrap b/subprojects/glaze.wrap index 9858a5f..3586878 100644 --- a/subprojects/glaze.wrap +++ b/subprojects/glaze.wrap @@ -2,4 +2,4 @@ source_url = https://github.com/stephenberry/glaze/archive/refs/tags/v5.1.1.tar.gz source_filename = glaze-5.1.1.tar.gz source_hash = 7fed59aae4c09b27761c6c94e1e450ed30ddc4d7303ddc70591ec268d90512f5 -directory = glaze +directory = glaze-5.1.1