diff --git a/meson.build b/meson.build index 368b5ea..734cfe8 100644 --- a/meson.build +++ b/meson.build @@ -1,3 +1,4 @@ +# ----------------------- # # Project Configuration # # ----------------------- # project( @@ -9,7 +10,7 @@ project( 'buildtype=release', 'b_vscrt=mt', 'b_lto=true', - 'b_ndebug=if-release', + 'b_ndebug=false', 'warning_level=3', ], ) @@ -92,6 +93,7 @@ platform_sources = { 'freebsd' : ['src/os/bsd.cpp'], 'netbsd' : ['src/os/bsd.cpp'], 'dragonfly' : ['src/os/bsd.cpp'], + 'haiku' : ['src/os/haiku.cpp'], 'darwin' : ['src/os/macos.cpp', 'src/os/macos/bridge.mm'], 'windows' : ['src/os/windows.cpp'], } @@ -177,6 +179,8 @@ if host_system == 'darwin' objc_args += ['-fobjc-arc'] elif host_system == 'linux' link_args += ['-static'] +elif host_system == 'haiku' + link_args += ['-lpackage', '-lbe'] endif # ------------------- # diff --git a/src/main.cpp b/src/main.cpp index 7e2741e..b378c3d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,7 @@ #include // ftxui::Color #include // ftxui::{Screen, Dimension::Full} #include // ftxui::string_width -#include // std::println +#include #include // std::ranges::{iota, to, transform} #include "src/config/config.hpp" @@ -116,12 +116,14 @@ namespace { }; fn CreateColorCircles() -> Element { - return hbox( - std::views::iota(0, 16) | std::views::transform([](i32 colorIndex) { - return hbox({ text("◯") | bold | color(static_cast(colorIndex)), text(" ") }); - }) | - std::ranges::to() - ); + fn color_view = std::views::iota(0, 16) | std::views::transform([](i32 colorIndex) { + return ftxui::hbox({ ftxui::text("◯") | ftxui::bold | ftxui::color(static_cast(colorIndex)), + ftxui::text(" ") }); + }); + + Elements elements_container(std::ranges::begin(color_view), std::ranges::end(color_view)); + + return hbox(elements_container); } fn get_visual_width(const String& str) -> usize { return ftxui::string_width(str); } @@ -354,7 +356,7 @@ fn main() -> i32 { Render(screen, document); screen.Print(); - std::println(); + std::cout << '\n'; return 0; } diff --git a/src/os/haiku.cpp b/src/os/haiku.cpp new file mode 100644 index 0000000..30186be --- /dev/null +++ b/src/os/haiku.cpp @@ -0,0 +1,463 @@ +#ifdef __HAIKU__ + +// clang-format off +#include // For BFile +#include // For BAppFileInfo and version_info +#include // For BString (optional, can use std::string +#include // Haiku specific: Defines B_OK and strerror function +#include // Haiku specific: Defines get_system_info, system_info, status_t +#include // PATH_MAX +#include // std::strlen +#include // DBUS_TYPE_* +#include // DBUS_BUS_SESSION +#include // Should define typedef ... BPackageKit::BPackageInfoSet; +#include // Defines BPackageKit::BPackageInfo (template argument) +#include // Defines BPackageKit::BPackageRoster +#include // For B_OK, status_t, strerror +#include // ucred, getsockopt, SOL_SOCKET, SO_PEERCRED +#include // statvfs +#include // std::move + +#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" +// clang-format on + +using namespace util::types; +using util::error::DracError, util::error::DracErrorCode; +using util::helpers::GetEnv; + +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")); + + ucred cred; + socklen_t len = sizeof(cred); + + if (getsockopt(fileDescriptor, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) + return Err(DracError::withErrno("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(DracError(DracErrorCode::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(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 { + fn GetOSVersion() -> Result { + BFile file; + status_t status = file.SetTo("/boot/system/lib/libbe.so", B_READ_ONLY); + + if (status != B_OK) { + return Err(DracError(DracErrorCode::InternalError, "Error opening /boot/system/lib/libbe.so")); + } + + BAppFileInfo appInfo; + status = appInfo.SetTo(&file); + + if (status != B_OK) { + return Err(DracError(DracErrorCode::InternalError, "Error initializing BAppFileInfo")); + } + + version_info versionInfo; + status = appInfo.GetVersionInfo(&versionInfo, B_APP_VERSION_KIND); + + if (status != B_OK) { + return Err(DracError(DracErrorCode::InternalError, "Error reading version info attribute")); + } + + std::string versionShortString = versionInfo.short_info; + + if (versionShortString.empty()) { + return Err(DracError(DracErrorCode::InternalError, "Version info short_info is empty")); + } + + return std::format("Haiku {}", versionShortString); + } + + fn GetMemInfo() -> Result { + system_info sysinfo; + const status_t status = get_system_info(&sysinfo); + + if (status != B_OK) + return Err(DracError(DracErrorCode::InternalError, std::format("get_system_info failed: {}", strerror(status)))); + + return static_cast(sysinfo.max_pages) * B_PAGE_SIZE; + } + + 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 (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 { + 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 { + Array hostnameBuffer {}; + + if (gethostname(hostnameBuffer.data(), hostnameBuffer.size()) != 0) + return Err(DracError( + DracErrorCode::ApiUnavailable, + std::format("gethostname() failed: {} (errno {})", strerror(errno), errno) + )); + + hostnameBuffer.at(HOST_NAME_MAX) = '\0'; + + return String(hostnameBuffer.data(), hostnameBuffer.size()); + } + + fn GetKernelVersion() -> Result { + system_info sysinfo; + const status_t status = get_system_info(&sysinfo); + + if (status != B_OK) + return Err(DracError(DracErrorCode::InternalError, std::format("get_system_info failed: {}", strerror(status)))); + + return std::to_string(sysinfo.kernel_version); + } + + fn GetDiskUsage() -> Result { + struct statvfs stat; + + if (statvfs("/boot", &stat) == -1) + return Err(DracError::withErrno(std::format("Failed to get filesystem stats for '/boot' (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 { + u64 count = 0; + + BPackageKit::BPackageRoster roster; + BPackageKit::BPackageInfoSet packageList; + + const status_t status = roster.GetActivePackages(BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, packageList); + + if (status != B_OK) + return Err(DracError(DracErrorCode::ApiUnavailable, "Failed to get active package list")); + + count += static_cast(packageList.CountInfos()); + + if (Result sharedCount = shared::GetPackageCount()) + count += *sharedCount; + else + debug_at(sharedCount.error()); + + return count; + } +} // namespace os + +#endif // __HAIKU__ diff --git a/src/util/logging.hpp b/src/util/logging.hpp index b2bacf2..3a9a6cb 100644 --- a/src/util/logging.hpp +++ b/src/util/logging.hpp @@ -5,7 +5,7 @@ #include // std::format #include // ftxui::Color #include // std::{mutex, lock_guard} -#include // std::print +#include // std::cout #include // std::forward #ifndef NDEBUG @@ -196,17 +196,17 @@ namespace util::logging { message ); - std::print("{}", mainLogLine); + std::cout << mainLogLine; #ifndef NDEBUG 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); - std::print("\n{}", Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR))); + std::cout << '\n' << Italic(Colorize(fullDebugLine, LogLevelConst::DEBUG_INFO_COLOR)); #endif - std::println("{}", LogLevelConst::RESET_CODE); + std::cout << LogLevelConst::RESET_CODE << '\n'; } template diff --git a/src/wrappers/dbus.hpp b/src/wrappers/dbus.hpp index 6149cb2..188ee4b 100644 --- a/src/wrappers/dbus.hpp +++ b/src/wrappers/dbus.hpp @@ -1,6 +1,6 @@ #pragma once -#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) || defined(__HAIKU__) // clang-format off #include @@ -386,4 +386,4 @@ namespace dbus { }; } // namespace dbus -#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__ +#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__ || __HAIKU__ diff --git a/src/wrappers/wayland.hpp b/src/wrappers/wayland.hpp index 384be68..2d859fe 100644 --- a/src/wrappers/wayland.hpp +++ b/src/wrappers/wayland.hpp @@ -1,6 +1,6 @@ #pragma once -#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) || defined(__HAIKU__) // clang-format off #include // Wayland client library @@ -96,4 +96,4 @@ namespace wl { }; } // namespace wl -#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__ +#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__ || __HAIKU__ diff --git a/src/wrappers/xcb.hpp b/src/wrappers/xcb.hpp index c61b26d..fa1f080 100644 --- a/src/wrappers/xcb.hpp +++ b/src/wrappers/xcb.hpp @@ -1,6 +1,6 @@ #pragma once -#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) || defined(__HAIKU__) // clang-format off #include // XCB library @@ -171,4 +171,4 @@ namespace xcb { }; } // namespace xcb -#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__ +#endif // __linux__ || __FreeBSD__ || __DragonFly__ || __NetBSD__ || __HAIKU__