diff --git a/.envrc.sample b/.envrc similarity index 100% rename from .envrc.sample rename to .envrc diff --git a/README.md b/README.md index 3441ff8c..07b11152 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ #### Current features - Sway (Workspaces, Binding mode, Focused window name) - River (Mapping mode, Tags, Focused window name) -- Hyprland (Focused window name) +- Hyprland (Window Icons, Workspaces, Focused window name) - DWL (Tags) [requires dwl ipc patch](https://github.com/djpohly/dwl/wiki/ipc) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time diff --git a/flake.lock b/flake.lock index 4bf02e17..25f12644 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1701253981, - "narHash": "sha256-ztaDIyZ7HrTAfEEUt9AtTDNoCYxUdSd6NrRHaYOIxtk=", + "lastModified": 1704538339, + "narHash": "sha256-1734d3mQuux9ySvwf6axRWZRBhtcZA9Q8eftD6EZg6U=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e92039b55bcd58469325ded85d4f58dd5a4eaf58", + "rev": "46ae0210ce163b3cba6c7da08840c1d63de9c701", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index cc830c7e..ebaeb81f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Highly customizable Wayland bar for Sway and Wlroots based compositors."; + description = "Highly customizable Wayland bar for Sway and Wlroots based compositors"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; @@ -15,7 +15,8 @@ genSystems = func: lib.genAttrs [ "x86_64-linux" "aarch64-linux" - ] (system: func (import nixpkgs { inherit system; })); + ] + (system: func (import nixpkgs { inherit system; })); mkDate = longDate: (lib.concatStringsSep "-" [ (builtins.substring 0 4 longDate) @@ -24,6 +25,27 @@ ]); in { + devShells = genSystems + (pkgs: + { + default = + pkgs.mkShell + { + name = "waybar-shell"; + + # inherit attributes from upstream nixpkgs derivation + inherit (pkgs.waybar) buildInputs depsBuildBuild depsBuildBuildPropagated depsBuildTarget + depsBuildTargetPropagated depsHostHost depsHostHostPropagated depsTargetTarget + depsTargetTargetPropagated propagatedBuildInputs propagatedNativeBuildInputs strictDeps; + + # overrides for local development + nativeBuildInputs = pkgs.waybar.nativeBuildInputs ++ (with pkgs; [ + clang-tools + gdb + ]); + }; + }); + overlays.default = final: prev: { waybar = final.callPackage ./nix/default.nix { # take the first "version: '...'" from meson.build @@ -35,27 +57,11 @@ + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); }; }; + packages = genSystems (pkgs: let packages = self.overlays.default pkgs pkgs; in packages // { default = packages.waybar; }); - } // - genSystems (pkgs: { - devShells.default = - pkgs.mkShell { - name = "waybar-shell"; - - # most of these aren't actually used in the waybar derivation, this is just in case - # they will ever start being used - inherit (pkgs.waybar) buildInputs depsBuildBuild depsBuildBuildPropagated depsBuildTarget - depsBuildTargetPropagated depsHostHost depsHostHostPropagated depsTargetTarget - depsTargetTargetPropagated propagatedBuildInputs propagatedNativeBuildInputs strictDeps; - - nativeBuildInputs = pkgs.waybar.nativeBuildInputs ++ (with pkgs; [ - clang-tools - gdb - ]); - }; - }); + }; } diff --git a/include/factory.hpp b/include/factory.hpp index b54fcb2e..9ce680d7 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -93,6 +93,9 @@ #ifdef HAVE_LIBCAVA #include "modules/cava.hpp" #endif +#ifdef HAVE_SYSTEMD_MONITOR +#include "modules/systemd_failed_units.hpp" +#endif #include "bar.hpp" #include "modules/cffi.hpp" #include "modules/custom.hpp" diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 0109149e..d2006fcc 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -145,7 +145,8 @@ class Workspaces : public AModule, public EventHandler { // workspace events void onWorkspaceActivated(std::string const& payload); void onWorkspaceDestroyed(std::string const& payload); - void onWorkspaceCreated(std::string const& payload); + void onWorkspaceCreated(std::string const& workspaceName, + Json::Value const& clientsData = Json::Value::nullRef); void onWorkspaceMoved(std::string const& payload); void onWorkspaceRenamed(std::string const& payload); @@ -163,10 +164,18 @@ class Workspaces : public AModule, public EventHandler { void doUpdate(); + void extendOrphans(int workspaceId, Json::Value const& clientsJson); + void registerOrphanWindow(WindowCreationPayload create_window_paylod); + bool m_allOutputs = false; bool m_showSpecial = false; bool m_activeOnly = false; + // Map for windows stored in workspaces not present in the current bar. + // This happens when the user has multiple monitors (hence, multiple bars) + // and doesn't share windows accross bars (a.k.a `all-outputs` = false) + std::map m_orphanWindowMap; + enum class SortMethod { ID, NAME, NUMBER, DEFAULT }; util::EnumParser m_enumParser; SortMethod m_sortBy = SortMethod::DEFAULT; @@ -191,7 +200,7 @@ class Workspaces : public AModule, public EventHandler { uint64_t m_monitorId; std::string m_activeWorkspaceName; std::vector> m_workspaces; - std::vector m_workspacesToCreate; + std::vector> m_workspacesToCreate; std::vector m_workspacesToRemove; std::vector m_windowsToCreate; diff --git a/include/modules/systemd_failed_units.hpp b/include/modules/systemd_failed_units.hpp new file mode 100644 index 00000000..7e0b1a91 --- /dev/null +++ b/include/modules/systemd_failed_units.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "ALabel.hpp" + +namespace waybar::modules { + +class SystemdFailedUnits : public ALabel { + public: + SystemdFailedUnits(const std::string&, const Json::Value&); + virtual ~SystemdFailedUnits(); + auto update() -> void override; + + private: + bool hide_on_ok; + std::string format_ok; + + bool update_pending; + std::string last_status; + uint32_t nr_failed_system, nr_failed_user; + Glib::RefPtr system_proxy, user_proxy; + + void notify_cb(const Glib::ustring &sender_name, const Glib::ustring &signal_name, + const Glib::VariantContainerBase &arguments); + void updateData(); +}; + +} // namespace waybar::modules diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd new file mode 100644 index 00000000..ac92c533 --- /dev/null +++ b/man/waybar-systemd-failed-units.5.scd @@ -0,0 +1,63 @@ +waybar-systemd-failed-units(5) + +# NAME + +waybar - systemd failed units monitor module + +# DESCRIPTION + +The *systemd-failed-units* module displays the number of failed systemd units. + +# CONFIGURATION + +Addressed by *systemd-failed-units* + +*format*: ++ + typeof: string ++ + default: *{nr_failed} failed* ++ + The format, how information should be displayed. This format is used when other formats aren't specified. + +*format-ok*: ++ + typeof: string ++ + This format is used when there is no failing units. + +*user*: ++ + typeof: bool ++ + default: *true* ++ + Option to count user systemd units. + +*system*: ++ + typeof: bool ++ + default: *true* ++ + Option to count systemwide (PID=1) systemd units. + +*hide-on-ok*: ++ + typeof: bool ++ + default: *true* ++ + Option to hide this module when there is no failing units. + +# FORMAT REPLACEMENTS + +*{nr_failed_system}*: Number of failed units from systemwide (PID=1) systemd. + +*{nr_failed_user}*: Number of failed units from user systemd. + +*{nr_failed}*: Number of total failed units. + +# EXAMPLES + +``` +"systemd-failed-units": { + "hide-on-ok": false, + "format": "✗ {nr_failed}", + "format-ok": "✓", + "system": true, + "user": false, +} +``` + +# STYLE + +- *#systemd-failed-units* +- *#systemd-failed-units.ok* +- *#systemd-failed-units.degraded* diff --git a/meson.build b/meson.build index c4f40205..c1ae48b5 100644 --- a/meson.build +++ b/meson.build @@ -204,6 +204,7 @@ inc_dirs = ['include'] if is_linux add_project_arguments('-DHAVE_CPU_LINUX', language: 'cpp') add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp') + add_project_arguments('-DHAVE_SYSTEMD_MONITOR', language: 'cpp') src_files += files( 'src/modules/battery.cpp', 'src/modules/cffi.cpp', @@ -214,6 +215,7 @@ if is_linux 'src/modules/cpu_usage/linux.cpp', 'src/modules/memory/common.cpp', 'src/modules/memory/linux.cpp', + 'src/modules/systemd_failed_units.cpp', ) elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd add_project_arguments('-DHAVE_CPU_BSD', language: 'cpp') @@ -495,6 +497,7 @@ if scdoc.found() 'waybar-sway-scratchpad.5.scd', 'waybar-sway-window.5.scd', 'waybar-sway-workspaces.5.scd', + 'waybar-systemd-failed-units.5.scd', 'waybar-temperature.5.scd', 'waybar-tray.5.scd', 'waybar-states.5.scd', diff --git a/nix/default.nix b/nix/default.nix index 5efa5da4..e2643084 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,9 +1,20 @@ { lib +, pkgs , waybar , version }: - -waybar.overrideAttrs (prev: { +let + catch2_3 = { + src = pkgs.fetchFromGitHub + { + owner = "catchorg"; + repo = "Catch2"; + rev = "v3.5.1"; + hash = "sha256-OyYNUfnu6h1+MfCF8O+awQ4Usad0qrdCtdZhYgOY+Vw="; + }; + }; +in +(waybar.overrideAttrs (oldAttrs: rec { inherit version; src = lib.cleanSourceWith { @@ -11,3 +22,9 @@ waybar.overrideAttrs (prev: { src = lib.cleanSource ../.; }; }) +).override { + catch2_3 = pkgs.catch2_3.overrideAttrs (oldAttrs: { + version = "3.5.1"; + src = catch2_3.src; + }); +} diff --git a/src/factory.cpp b/src/factory.cpp index 1828fb46..2ad5b6fa 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -200,6 +200,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "cava") { return new waybar::modules::Cava(id, config_[name]); } +#endif +#ifdef HAVE_SYSTEMD_MONITOR + if (ref == "systemd-failed-units") { + return new waybar::modules::SystemdFailedUnits(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 3d8a5932..b05ce134 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -128,6 +128,12 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); } +void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_paylod) { + if (!create_window_paylod.isEmpty(*this)) { + m_orphanWindowMap[create_window_paylod.getAddress()] = create_window_paylod.repr(*this); + } +} + auto Workspaces::registerIpc() -> void { gIPC->registerForIPC("workspace", this); gIPC->registerForIPC("createworkspace", this); @@ -164,8 +170,8 @@ void Workspaces::doUpdate() { m_workspacesToRemove.clear(); // add workspaces that wait to be created - for (auto &elem : m_workspacesToCreate) { - createWorkspace(elem); + for (auto &[workspaceData, clientsData] : m_workspacesToCreate) { + createWorkspace(workspaceData, clientsData); } m_workspacesToCreate.clear(); @@ -215,6 +221,8 @@ void Workspaces::doUpdate() { static auto const WINDOW_CREATION_TIMEOUT = 2; if (windowPayload.incrementTimeSpentUncreated() < WINDOW_CREATION_TIMEOUT) { notCreated.push_back(windowPayload); + } else { + registerOrphanWindow(windowPayload); } } } @@ -293,16 +301,17 @@ void Workspaces::onWorkspaceDestroyed(std::string const &payload) { } } -void Workspaces::onWorkspaceCreated(std::string const &payload) { +void Workspaces::onWorkspaceCreated(std::string const &workspaceName, + Json::Value const &clientsData) { const Json::Value workspacesJson = gIPC->getSocket1JsonReply("workspaces"); - if (!isWorkspaceIgnored(payload)) { + if (!isWorkspaceIgnored(workspaceName)) { for (Json::Value workspaceJson : workspacesJson) { std::string name = workspaceJson["name"].asString(); - if (name == payload && + if (name == workspaceName && (allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && - (showSpecial() || !name.starts_with("special")) && !isDoubleSpecial(payload)) { - m_workspacesToCreate.push_back(workspaceJson); + (showSpecial() || !name.starts_with("special")) && !isDoubleSpecial(workspaceName)) { + m_workspacesToCreate.emplace_back(workspaceJson, clientsData); break; } } @@ -310,20 +319,14 @@ void Workspaces::onWorkspaceCreated(std::string const &payload) { } void Workspaces::onWorkspaceMoved(std::string const &payload) { - std::string workspace = payload.substr(0, payload.find(',')); - std::string newOutput = payload.substr(payload.find(',') + 1); - bool shouldShow = showSpecial() || !workspace.starts_with("special"); - if (shouldShow && m_bar.output->name == newOutput) { // TODO: implement this better - const Json::Value workspacesJson = gIPC->getSocket1JsonReply("workspaces"); - for (Json::Value workspaceJson : workspacesJson) { - std::string name = workspaceJson["name"].asString(); - if (name == workspace && m_bar.output->name == workspaceJson["monitor"].asString()) { - m_workspacesToCreate.push_back(workspaceJson); - break; - } - } + std::string workspaceName = payload.substr(0, payload.find(',')); + std::string monitorName = payload.substr(payload.find(',') + 1); + + if (m_bar.output->name == monitorName) { + Json::Value clientsData = gIPC->getSocket1JsonReply("clients"); + onWorkspaceCreated(workspaceName, clientsData); } else { - m_workspacesToRemove.push_back(workspace); + onWorkspaceDestroyed(workspaceName); } } @@ -402,18 +405,47 @@ void Workspaces::onWindowMoved(std::string const &payload) { } } - // ...and add it to the new workspace + // ...if it was empty, check if the window is an orphan... + if (windowRepr.empty() && m_orphanWindowMap.contains(windowAddress)) { + windowRepr = m_orphanWindowMap[windowAddress]; + } + + // ...and then add it to the new workspace if (!windowRepr.empty()) { m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowRepr); } } void Workspaces::onWindowTitleEvent(std::string const &payload) { - auto windowWorkspace = - std::find_if(m_workspaces.begin(), m_workspaces.end(), - [payload](auto &workspace) { return workspace->containsWindow(payload); }); + std::optional> inserter; - if (windowWorkspace != m_workspaces.end()) { + // If the window was an orphan, rename it at the orphan's vector + if (m_orphanWindowMap.contains(payload)) { + inserter = [this](WindowCreationPayload wcp) { this->registerOrphanWindow(std::move(wcp)); }; + } else { + auto windowWorkspace = + std::find_if(m_workspaces.begin(), m_workspaces.end(), + [payload](auto &workspace) { return workspace->containsWindow(payload); }); + + // If the window exists on a workspace, rename it at the workspace's window + // map + if (windowWorkspace != m_workspaces.end()) { + inserter = [windowWorkspace](WindowCreationPayload wcp) { + (*windowWorkspace)->insertWindow(std::move(wcp)); + }; + } else { + auto queuedWindow = std::find_if( + m_windowsToCreate.begin(), m_windowsToCreate.end(), + [payload](auto &windowPayload) { return windowPayload.getAddress() == payload; }); + + // If the window was queued, rename it in the queue + if (queuedWindow != m_windowsToCreate.end()) { + inserter = [queuedWindow](WindowCreationPayload wcp) { *queuedWindow = std::move(wcp); }; + } + } + } + + if (inserter.has_value()) { Json::Value clientsData = gIPC->getSocket1JsonReply("clients"); std::string jsonWindowAddress = fmt::format("0x{}", payload); @@ -423,7 +455,7 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) { }); if (!client->empty()) { - (*windowWorkspace)->insertWindow({*client}); + (*inserter)({*client}); } } } @@ -605,6 +637,14 @@ void Workspaces::createPersistentWorkspaces() { } } +void Workspaces::extendOrphans(int workspaceId, Json::Value const &clientsJson) { + for (const auto &client : clientsJson) { + if (client["workspace"]["id"].asInt() == workspaceId) { + registerOrphanWindow({client}); + } + } +} + void Workspaces::init() { m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); @@ -629,6 +669,8 @@ void Workspaces::init() { (!workspaceName.starts_with("special") || showSpecial()) && !isWorkspaceIgnored(workspaceName)) { createWorkspace(workspaceJson, clientsJson); + } else { + extendOrphans(workspaceJson["id"].asInt(), clientsJson); } } @@ -981,6 +1023,11 @@ void WindowCreationPayload::clearWorkspaceName() { m_workspaceName = m_workspaceName.substr( SPECIAL_QUALIFIER_PREFIX_LEN, m_workspaceName.length() - SPECIAL_QUALIFIER_PREFIX_LEN); } + + std::size_t spaceFound = m_workspaceName.find(' '); + if (spaceFound != std::string::npos) { + m_workspaceName.erase(m_workspaceName.begin() + spaceFound, m_workspaceName.end()); + } } void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp new file mode 100644 index 00000000..382eea4a --- /dev/null +++ b/src/modules/systemd_failed_units.cpp @@ -0,0 +1,133 @@ +#include "modules/systemd_failed_units.hpp" + +#include +#include +#include +#include + +static const unsigned UPDATE_DEBOUNCE_TIME_MS = 1000; + +namespace waybar::modules { + +SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& config) + : ALabel(config, "systemd-failed-units", id, "{nr_failed} failed", 1), + hide_on_ok(true), + update_pending(false), + nr_failed_system(0), + nr_failed_user(0), + last_status() { + if (config["hide-on-ok"].isBool()) { + hide_on_ok = config["hide-on-ok"].asBool(); + } + if (config["format-ok"].isString()) { + format_ok = config["format-ok"].asString(); + } else { + format_ok = format_; + } + + /* Default to enable both "system" and "user". */ + if (!config["system"].isBool() || config["system"].asBool()) { + system_proxy = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties"); + if (!system_proxy) { + throw std::runtime_error("Unable to connect to systemwide systemd DBus!"); + } + system_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + } + if (!config["user"].isBool() || config["user"].asBool()) { + user_proxy = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties"); + if (!user_proxy) { + throw std::runtime_error("Unable to connect to user systemd DBus!"); + } + user_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + } + + updateData(); + /* Always update for the first time. */ + dp.emit(); +} + +SystemdFailedUnits::~SystemdFailedUnits() { + if (system_proxy) system_proxy.reset(); + if (user_proxy) user_proxy.reset(); +} + +auto SystemdFailedUnits::notify_cb( + const Glib::ustring &sender_name, + const Glib::ustring &signal_name, + const Glib::VariantContainerBase &arguments) -> void { + if (signal_name == "PropertiesChanged" && !update_pending) { + update_pending = true; + /* The fail count may fluctuate due to restarting. */ + Glib::signal_timeout().connect_once( + sigc::mem_fun(*this, &SystemdFailedUnits::updateData), + UPDATE_DEBOUNCE_TIME_MS); + } +} + +void SystemdFailedUnits::updateData() { + update_pending = false; + + auto load = [](const char* kind, Glib::RefPtr &proxy) -> uint32_t { + try { + auto parameters = Glib::VariantContainerBase( + g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "NFailedUnits")); + Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); + if (data && data.is_of_type(Glib::VariantType("(v)"))) { + Glib::VariantBase variant; + g_variant_get(data.gobj_copy(), "(v)", &variant); + if (variant && variant.is_of_type(Glib::VARIANT_TYPE_UINT32)) { + uint32_t value = 0; + g_variant_get(variant.gobj_copy(), "u", &value); + return value; + } + } + } catch (Glib::Error& e) { + spdlog::error("Failed to get {} failed units: {}", kind, e.what().c_str()); + } + return 0; + }; + + if (system_proxy) { + nr_failed_system = load("systemwide", system_proxy); + } + if (user_proxy) { + nr_failed_user = load("user", user_proxy); + } + dp.emit(); +} + +auto SystemdFailedUnits::update() -> void { + uint32_t nr_failed = nr_failed_system + nr_failed_user; + + // Hide if needed. + if (nr_failed == 0 && hide_on_ok) { + event_box_.set_visible(false); + return; + } + if (!event_box_.get_visible()) { + event_box_.set_visible(true); + } + + // Set state class. + const std::string status = nr_failed == 0 ? "ok" : "degraded"; + if (!last_status.empty() && label_.get_style_context()->has_class(last_status)) { + label_.get_style_context()->remove_class(last_status); + } + if (!label_.get_style_context()->has_class(status)) { + label_.get_style_context()->add_class(status); + } + last_status = status; + + label_.set_markup(fmt::format( + fmt::runtime(nr_failed == 0 ? format_ok : format_), + fmt::arg("nr_failed", nr_failed), + fmt::arg("nr_failed_system", nr_failed_system), + fmt::arg("nr_failed_user", nr_failed_user))); + ALabel::update(); +} + +} // namespace waybar::modules::systemd_failed_units