diff --git a/.gitignore b/.gitignore index f42bf85e..f21700d6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ result-* .ccls-cache _codeql_detected_source_root +heaptrack* diff --git a/include/AModule.hpp b/include/AModule.hpp index 6b29045c..f697c012 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -25,6 +25,10 @@ class AModule : public IModule { bool expandEnabled() const; + virtual void suspend() {}; + virtual void resume() {}; + bool shouldSuspend() const { return disable_on_sleep_; } + protected: // Don't need to make an object directly // Derived classes are able to use it @@ -48,7 +52,9 @@ class AModule : public IModule { virtual bool handleMouseLeave(GdkEventCrossing* const& ev); virtual bool handleScroll(GdkEventScroll*); virtual bool handleRelease(GdkEventButton* const& ev); - GObject* menu_; + + bool disable_on_sleep_{false}; + GObject* menu_ = nullptr; private: bool handleUserEvent(GdkEventButton* const& ev); @@ -57,6 +63,7 @@ class AModule : public IModule { bool hasUserEvents_; gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; + sigc::connection cursor_timeout_conn_; std::map eventActionMap_; static const inline std::map, std::string> eventMap_{ {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, diff --git a/include/bar.hpp b/include/bar.hpp index 54b332bf..8f23d6e8 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -75,6 +75,8 @@ class Bar : public sigc::trackable { util::KillSignalAction getOnSigusr1Action(); util::KillSignalAction getOnSigusr2Action(); + void toggleSuspend(bool suspend); + struct waybar_output* output; Json::Value config; struct wl_surface* surface; diff --git a/include/group.hpp b/include/group.hpp index 41cb5ee0..d5cbb289 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -30,6 +30,7 @@ class Group : public AModule { bool handleMouseEnter(GdkEventCrossing* const& ev) override; bool handleMouseLeave(GdkEventCrossing* const& ev) override; bool handleToggle(GdkEventButton* const& ev) override; + bool handleScroll(GdkEventScroll* e) override; void show_group(); void hide_group(); }; diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index 6c17c6e4..a345a33b 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -35,6 +35,7 @@ class Custom : public ALabel { std::string id_; std::string alt_; std::string tooltip_; + std::string last_tooltip_markup_; const bool tooltip_format_enabled_; std::vector class_; int percentage_; diff --git a/include/modules/disk.hpp b/include/modules/disk.hpp index 1b4f3176..b576e19f 100644 --- a/include/modules/disk.hpp +++ b/include/modules/disk.hpp @@ -22,7 +22,7 @@ class Disk : public ALabel { std::string path_; std::string unit_; - float calc_specific_divisor(const std::string divisor); + float calc_specific_divisor(const std::string& divisor); }; } // namespace waybar::modules diff --git a/include/modules/gamemode.hpp b/include/modules/gamemode.hpp index f63861a3..89f66272 100644 --- a/include/modules/gamemode.hpp +++ b/include/modules/gamemode.hpp @@ -26,7 +26,7 @@ class Gamemode : public AModule { const std::string DEFAULT_FORMAT = "{glyph}"; const std::string DEFAULT_FORMAT_ALT = "{glyph} {count}"; const std::string DEFAULT_TOOLTIP_FORMAT = "Games running: {count}"; - const std::string DEFAULT_GLYPH = ""; + const std::string DEFAULT_GLYPH = "󰊴"; void appear(const Glib::RefPtr& connection, const Glib::ustring& name, const Glib::ustring& name_owner); diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index 2e0ef657..7c6369da 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include #include #include #include @@ -34,19 +36,33 @@ class IPC { Json::Value getSocket1JsonReply(const std::string& rq); static std::filesystem::path getSocketFolder(const char* instanceSig); + /// Dispatch a Hyprland command. Automatically uses the correct protocol + /// (legacy text or Lua-based) depending on the running Hyprland version. + static std::string dispatch(const std::string& dispatcher, const std::string& arg); + + /// Build a Lua-format dispatch command string. + static std::string buildLuaDispatch(const std::string& dispatcher, const std::string& arg); + protected: static std::filesystem::path socketFolder_; + /// Detect whether the running Hyprland uses the Lua-based IPC protocol. + /// Returns true for Hyprland >= 0.54 (Lua config), false for older versions. + static bool isLuaProtocol(); + + static std::optional s_luaProtocolDetected_; // cached detection result + private: void socketListener(); void parseIPC(const std::string&); std::thread ipcThread_; std::mutex callbackMutex_; + std::mutex socketMutex_; util::JsonParser parser_; std::list> callbacks_; - int socketfd_; // the hyprland socket file descriptor - pid_t socketOwnerPid_; - bool running_ = true; // the ipcThread will stop running when this is false + int socketfd_ = -1; // the hyprland socket file descriptor + pid_t socketOwnerPid_ = -1; + std::atomic running_ = true; // the ipcThread will stop running when this is false }; }; // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index 2be64594..9725d33a 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -20,8 +20,8 @@ class Window : public waybar::AAppIconLabel, public EventHandler { private: struct Workspace { - int id; - int windows; + int id = 0; + int windows = 0; std::string last_window; std::string last_window_title; @@ -29,14 +29,14 @@ class Window : public waybar::AAppIconLabel, public EventHandler { }; struct WindowData { - bool floating; + bool floating = false; int monitor = -1; std::string class_name; std::string initial_class_name; std::string title; std::string initial_title; - bool fullscreen; - bool grouped; + bool fullscreen = false; + bool grouped = false; static auto parse(const Json::Value&) -> WindowData; }; @@ -47,7 +47,7 @@ class Window : public waybar::AAppIconLabel, public EventHandler { void queryActiveWorkspace(); void setClass(const std::string&, bool enable); - bool separateOutputs_; + bool separateOutputs_ = false; std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; @@ -55,11 +55,11 @@ class Window : public waybar::AAppIconLabel, public EventHandler { Workspace workspace_; std::string soloClass_; std::string lastSoloClass_; - bool solo_; - bool allFloating_; - bool swallowing_; - bool fullscreen_; - bool focused_; + bool solo_ = false; + bool allFloating_ = false; + bool swallowing_ = false; + bool fullscreen_ = false; + bool focused_ = false; IPC& m_ipc; }; diff --git a/include/modules/hyprland/windowcreationpayload.hpp b/include/modules/hyprland/windowcreationpayload.hpp index 226a7c41..906be805 100644 --- a/include/modules/hyprland/windowcreationpayload.hpp +++ b/include/modules/hyprland/windowcreationpayload.hpp @@ -40,10 +40,11 @@ struct WindowRepr { class WindowCreationPayload { public: - WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + WindowCreationPayload(const std::string& workspace_name, WindowAddress window_address, WindowRepr window_repr); - WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_class, std::string window_title, bool is_active); + WindowCreationPayload(const std::string& workspace_name, WindowAddress window_address, + const std::string& window_class, const std::string& window_title, + bool is_active); WindowCreationPayload(Json::Value const& client_data); int incrementTimeSpentUncreated(); diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index a5d94bbf..03548ccb 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -43,6 +44,7 @@ class Workspaces : public AModule, public EventHandler { auto moveToMonitor() const -> bool { return m_moveToMonitor; } auto enableTaskbar() const -> bool { return m_enableTaskbar; } auto taskbarWithIcon() const -> bool { return m_taskbarWithIcon; } + auto barScroll() const -> bool { return m_barScroll; } auto getBarOutput() const -> std::string { return m_bar.output->name; } auto formatBefore() const -> std::string { return m_formatBefore; } @@ -58,7 +60,7 @@ class Workspaces : public AModule, public EventHandler { enum class ActiveWindowPosition { NONE, FIRST, LAST }; auto activeWindowPosition() const -> ActiveWindowPosition { return m_activeWindowPosition; } - std::string getRewrite(std::string window_class, std::string window_title); + std::string getRewrite(const std::string& window_class, const std::string& window_title); std::string& getWindowSeparator() { return m_formatWindowSeparator; } bool isWorkspaceIgnored(std::string const& workspace_name); @@ -122,6 +124,8 @@ class Workspaces : public AModule, public EventHandler { static std::pair splitDoublePayload(std::string const& payload); static std::tuple splitTriplePayload( std::string const& payload); + // scroll events + bool handleScroll(GdkEventScroll* e) override; // Update methods void doUpdate(); @@ -145,6 +149,7 @@ class Workspaces : public AModule, public EventHandler { bool m_specialVisibleOnly = false; bool m_persistentOnly = false; bool m_moveToMonitor = false; + bool m_barScroll = false; Json::Value m_persistentWorkspaceConfig; // Map for windows stored in workspaces not present in the current bar. @@ -204,6 +209,7 @@ class Workspaces : public AModule, public EventHandler { std::mutex m_mutex; const Bar& m_bar; Gtk::Box m_box; + sigc::connection m_scrollEventConnection_; IPC& m_ipc; }; diff --git a/include/modules/keyboard_state.hpp b/include/modules/keyboard_state.hpp index be90eee4..db7f2550 100644 --- a/include/modules/keyboard_state.hpp +++ b/include/modules/keyboard_state.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -41,6 +42,7 @@ class KeyboardState : public AModule { struct libinput* libinput_; std::unordered_map libinput_devices_; + std::mutex devices_mutex_; // protects libinput_devices_ std::set binding_keys; util::SleeperThread libinput_thread_, hotplug_thread_; diff --git a/include/modules/memory.hpp b/include/modules/memory.hpp index 3b6342b3..0e405187 100644 --- a/include/modules/memory.hpp +++ b/include/modules/memory.hpp @@ -22,6 +22,8 @@ class Memory : public ALabel { std::unordered_map meminfo_; util::SleeperThread thread_; + + std::string unit_; }; } // namespace waybar::modules diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 32d526e9..554f2977 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -33,6 +33,7 @@ class MPD : public ALabel { detail::unique_status status_; mpd_state state_; detail::unique_song song_; + std::string ellipsis_; public: MPD(const std::string&, const Json::Value&); @@ -44,7 +45,11 @@ class MPD : public ALabel { std::string getFilename() const; void setLabel(); std::string getStateIcon() const; - std::string getOptionIcon(std::string optionName, bool activated) const; + std::string getOptionIcon(const std::string& optionName, bool activated) const; + std::string getArtistStr(bool truncated) const; + std::string getAlbumArtistStr(bool truncated) const; + std::string getAlbumStr(bool truncated) const; + std::string getTitleStr(bool truncated) const; // GUI-side methods bool handlePlayPause(GdkEventButton* const&); @@ -54,7 +59,6 @@ class MPD : public ALabel { void tryConnect(); void checkErrors(mpd_connection* conn); void fetchState(); - void queryMPD(); inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 2c9071b4..0624c196 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -197,7 +197,6 @@ class Context { void tryConnect() const; void checkErrors(mpd_connection*) const; void do_update(); - void queryMPD() const; void fetchState() const; constexpr mpd_state state() const; void emit() const; diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp index 895970e6..cf4a4beb 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -1,13 +1,14 @@ #pragma once namespace detail { +using namespace std::literals::chrono_literals; inline bool Context::is_connected() const { return mpd_module_->connection_ != nullptr; } inline bool Context::is_playing() const { return mpd_module_->playing(); } inline bool Context::is_paused() const { return mpd_module_->paused(); } inline bool Context::is_stopped() const { return mpd_module_->stopped(); } -constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_.count(); } +constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_ / 1s; } inline void Context::tryConnect() const { mpd_module_->tryConnect(); } inline unique_connection& Context::connection() { return mpd_module_->connection_; } constexpr inline mpd_state Context::state() const { return mpd_module_->state_; } @@ -15,7 +16,6 @@ constexpr inline mpd_state Context::state() const { return mpd_module_->state_; inline void Context::do_update() { mpd_module_->setLabel(); } inline void Context::checkErrors(mpd_connection* conn) const { mpd_module_->checkErrors(conn); } -inline void Context::queryMPD() const { mpd_module_->queryMPD(); } inline void Context::fetchState() const { mpd_module_->fetchState(); } inline void Context::emit() const { mpd_module_->emit(); } diff --git a/include/modules/mpris/mpris.hpp b/include/modules/mpris/mpris.hpp index ad4dac1e..a33db4d2 100644 --- a/include/modules/mpris/mpris.hpp +++ b/include/modules/mpris/mpris.hpp @@ -78,6 +78,7 @@ class Mpris : public ALabel { PlayerctlPlayerManager* manager; PlayerctlPlayer* player; + PlayerctlPlayer* last_active_player_ = nullptr; std::string lastStatus; std::string lastPlayer; diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 4abe26bd..66fc6d04 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -70,6 +70,9 @@ class Network : public ALabel { unsigned long long bandwidth_down_total_{0}; unsigned long long bandwidth_up_total_{0}; + unsigned long long bandwidth_down_prev_{0}; + unsigned long long bandwidth_up_prev_{0}; + std::chrono::steady_clock::time_point bandwidth_last_sample_time_; std::string state_; std::string essid_; diff --git a/include/modules/niri/backend.hpp b/include/modules/niri/backend.hpp index 42b9ff7f..07be039a 100644 --- a/include/modules/niri/backend.hpp +++ b/include/modules/niri/backend.hpp @@ -17,7 +17,7 @@ class EventHandler { class IPC { public: - IPC() { startIPC(); } + IPC(); void registerForIPC(const std::string& ev, EventHandler* ev_handler); void unregisterForIPC(EventHandler* handler); diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index 68d2bf19..729fcc6a 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -28,6 +28,7 @@ class Privacy : public AModule { // Config Gtk::Box box_; + std::vector modules_; uint iconSpacing = 4; uint iconSize = 20; uint transition_duration = 250; diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index 6c62ac31..d76ec74a 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -16,7 +16,7 @@ class Host { public: Host(const std::size_t id, const Json::Value&, const Bar&, const std::function&)>&, - const std::function&)>&); + const std::function&)>&, const std::function&); ~Host(); private: @@ -28,9 +28,13 @@ class Host { static void registerHost(GObject*, GAsyncResult*, gpointer); static void itemRegistered(SnWatcher*, const gchar*, gpointer); static void itemUnregistered(SnWatcher*, const gchar*, gpointer); + void itemReady(Item&); + void itemInvalidated(Item&); + void removeItem(std::vector>::iterator); + void clearItems(); std::tuple getBusNameAndObjectPath(const std::string); - void addRegisteredItem(std::string service); + void addRegisteredItem(const std::string& service); std::vector> items_; const std::string bus_name_; @@ -43,6 +47,7 @@ class Host { const Bar& bar_; const std::function&)> on_add_; const std::function&)> on_remove_; + const std::function on_update_; }; } // namespace waybar::modules::SNI diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 503ab637..df87bfb7 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -25,9 +26,13 @@ struct ToolTip { class Item : public sigc::trackable { public: - Item(const std::string&, const std::string&, const Json::Value&, const Bar&); + Item(const std::string&, const std::string&, const Json::Value&, const Bar&, + const std::function&, const std::function&, + const std::function&); ~Item(); + bool isReady() const; + std::string bus_name; std::string object_path; @@ -41,9 +46,12 @@ class Item : public sigc::trackable { std::string title; std::string icon_name; Glib::RefPtr icon_pixmap; + bool has_custom_icon_ = false; Glib::RefPtr icon_theme; std::string overlay_icon_name; + Glib::RefPtr overlay_icon_pixmap; std::string attention_icon_name; + Glib::RefPtr attention_icon_pixmap; std::string attention_movie_name; std::string icon_theme_path; std::string menu; @@ -62,6 +70,8 @@ class Item : public sigc::trackable { void proxyReady(Glib::RefPtr& result); void setProperty(const Glib::ustring& name, Glib::VariantBase& value); void setStatus(const Glib::ustring& value); + void setReady(); + void invalidate(); void setCustomIcon(const std::string& id); void getUpdatedProperties(); void processUpdatedProperties(Glib::RefPtr& result); @@ -69,8 +79,13 @@ class Item : public sigc::trackable { const Glib::VariantContainerBase& arguments); void updateImage(); - Glib::RefPtr extractPixBuf(GVariant* variant); + static Glib::RefPtr extractPixBuf(GVariant* variant); Glib::RefPtr getIconPixbuf(); + Glib::RefPtr getAttentionIconPixbuf(); + Glib::RefPtr getOverlayIconPixbuf(); + Glib::RefPtr loadIconFromNameOrFile(const std::string& name, bool log_failure); + static Glib::RefPtr overlayPixbufs(const Glib::RefPtr&, + const Glib::RefPtr&); Glib::RefPtr getIconByName(const std::string& name, int size); double getScaledIconSize(); static void onMenuDestroyed(Item* self, GObject* old_menu_pointer); @@ -86,8 +101,13 @@ class Item : public sigc::trackable { gdouble distance_scrolled_y_ = 0; // visibility of items with Status == Passive bool show_passive_ = false; + bool ready_ = false; + Glib::ustring status_ = "active"; const Bar& bar_; + const std::function on_ready_; + const std::function on_invalidate_; + const std::function on_updated_; Glib::RefPtr proxy_; Glib::RefPtr cancellable_; diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 5f12d7f2..755aa7b3 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -19,9 +19,9 @@ class Tray : public AModule { private: void onAdd(std::unique_ptr& item); void onRemove(std::unique_ptr& item); + void queueUpdate(); static inline std::size_t nb_hosts_ = 0; - bool show_passive_ = false; Gtk::Box box_; SNI::Watcher::singleton watcher_; SNI::Host host_; diff --git a/include/modules/sway/bar.hpp b/include/modules/sway/bar.hpp index fd48e5a3..381c6c37 100644 --- a/include/modules/sway/bar.hpp +++ b/include/modules/sway/bar.hpp @@ -37,7 +37,7 @@ class BarIpcClient { void onModeUpdate(bool visible_by_modifier); void onUrgencyUpdate(bool visible_by_urgency); void update(); - bool isModuleEnabled(std::string name); + bool isModuleEnabled(const std::string& name); Bar& bar_; util::JsonParser parser_; diff --git a/include/modules/sway/ipc/client.hpp b/include/modules/sway/ipc/client.hpp index 9033a688..f6eb7c40 100644 --- a/include/modules/sway/ipc/client.hpp +++ b/include/modules/sway/ipc/client.hpp @@ -12,6 +12,8 @@ #include #include "ipc.hpp" +#include "util/SafeSignal.hpp" +#include "util/scoped_fd.hpp" #include "util/sleeper_thread.hpp" namespace waybar::modules::sway { @@ -27,8 +29,8 @@ class Ipc { std::string payload; }; - sigc::signal signal_event; - sigc::signal signal_cmd; + ::waybar::SafeSignal signal_event; + ::waybar::SafeSignal signal_cmd; void sendCmd(uint32_t type, const std::string& payload = ""); void subscribe(const std::string& payload); @@ -44,8 +46,8 @@ class Ipc { struct ipc_response send(int fd, uint32_t type, const std::string& payload = ""); struct ipc_response recv(int fd); - int fd_; - int fd_event_; + util::ScopedFd fd_; + util::ScopedFd fd_event_; std::mutex mutex_; util::SleeperThread thread_; }; diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index 91aa181d..470b9879 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -47,7 +47,7 @@ class Language : public ALabel, public sigc::trackable { void onEvent(const struct Ipc::ipc_response&); void onCmd(const struct Ipc::ipc_response&); - auto set_current_layout(std::string current_layout) -> void; + auto set_current_layout(const std::string& current_layout) -> void; auto init_layouts_map(const std::vector& used_layouts) -> void; const static std::string XKB_LAYOUT_NAMES_KEY; diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index d8a9e18a..1f8d8ed0 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -24,10 +24,11 @@ class Workspaces : public AModule, public sigc::trackable { private: static constexpr std::string_view workspace_switch_cmd_ = "workspace {} \"{}\""; + static constexpr std::string_view workspace_switch_number_cmd_ = "workspace {} number {}"; static constexpr std::string_view persistent_workspace_switch_cmd_ = R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; - static int convertWorkspaceNameToNum(std::string name); + static int convertWorkspaceNameToNum(const std::string& name); static int windowRewritePriorityFunction(std::string const& window_rule); void onCmd(const struct Ipc::ipc_response&); @@ -40,7 +41,7 @@ class Workspaces : public AModule, public sigc::trackable { std::string getIcon(const std::string&, const Json::Value&); std::string getCycleWorkspace(std::vector::iterator, bool prev) const; uint16_t getWorkspaceIndex(const std::string& name) const; - static std::string trimWorkspaceName(std::string); + static std::string trimWorkspaceName(const std::string&); bool handleScroll(GdkEventScroll* /*unused*/) override; const Bar& bar_; diff --git a/include/modules/systemd_failed_units.hpp b/include/modules/systemd_failed_units.hpp index ffb25082..3eda799a 100644 --- a/include/modules/systemd_failed_units.hpp +++ b/include/modules/systemd_failed_units.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "ALabel.hpp" @@ -11,23 +12,42 @@ namespace waybar::modules { class SystemdFailedUnits : public ALabel { public: SystemdFailedUnits(const std::string&, const Json::Value&); - virtual ~SystemdFailedUnits(); + virtual ~SystemdFailedUnits() = default; auto update() -> void override; private: - bool hide_on_ok; - std::string format_ok; + struct FailedUnit { + std::string name; + std::string description; + std::string load_state; + std::string active_state; + std::string sub_state; + std::string scope; + }; - bool update_pending; - std::string system_state, user_state, overall_state; - uint32_t nr_failed_system, nr_failed_user, nr_failed; - std::string last_status; - Glib::RefPtr system_proxy, user_proxy; + bool hide_on_ok_; + std::string format_ok_; + std::string tooltip_format_; + std::string tooltip_format_ok_; + std::string tooltip_unit_format_; + + bool update_pending_; + std::string system_state_, user_state_, overall_state_; + uint32_t nr_failed_system_, nr_failed_user_, nr_failed_; + std::string last_status_; + Glib::RefPtr system_props_proxy_, user_props_proxy_; + Glib::RefPtr system_manager_proxy_, user_manager_proxy_; + std::vector failed_units_; void notify_cb(const Glib::ustring& sender_name, const Glib::ustring& signal_name, const Glib::VariantContainerBase& arguments); void RequestFailedUnits(); + void RequestFailedUnitsList(); void RequestSystemState(); + std::vector LoadFailedUnitsList(const char* kind, + Glib::RefPtr& proxy, + const std::string& scope); + std::string BuildTooltipFailedList() const; void updateData(); }; diff --git a/include/modules/temperature.hpp b/include/modules/temperature.hpp index 918281be..d65ef84d 100644 --- a/include/modules/temperature.hpp +++ b/include/modules/temperature.hpp @@ -14,6 +14,8 @@ class Temperature : public ALabel { Temperature(const std::string&, const Json::Value&); virtual ~Temperature() = default; auto update() -> void override; + void suspend() override; + void resume() override; private: float getTemperature(); diff --git a/include/modules/wayfire/backend.hpp b/include/modules/wayfire/backend.hpp index 9d55c820..e1f259cc 100644 --- a/include/modules/wayfire/backend.hpp +++ b/include/modules/wayfire/backend.hpp @@ -12,6 +12,8 @@ #include #include +#include "util/scoped_fd.hpp" + namespace waybar::modules::wayfire { using EventHandler = std::function; @@ -71,25 +73,9 @@ struct State { auto update_view(const Json::Value& view) -> void; }; -struct Sock { - int fd; +using Sock = util::ScopedFd; - Sock(int fd) : fd{fd} {} - ~Sock() { close(fd); } - Sock(const Sock&) = delete; - auto operator=(const Sock&) = delete; - Sock(Sock&& rhs) noexcept { - fd = rhs.fd; - rhs.fd = -1; - } - auto& operator=(Sock&& rhs) noexcept { - fd = rhs.fd; - rhs.fd = -1; - return *this; - } -}; - -class IPC { +class IPC : public std::enable_shared_from_this { static std::weak_ptr instance; Json::CharReaderBuilder reader_builder; Json::StreamWriterBuilder writer_builder; @@ -98,7 +84,7 @@ class IPC { State state; std::mutex state_mutex; - IPC() { start(); } + IPC() = default; static auto connect() -> Sock; auto receive(Sock& sock) -> Json::Value; diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp index 340f74ee..afb0893e 100644 --- a/include/util/SafeSignal.hpp +++ b/include/util/SafeSignal.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,12 @@ struct SafeSignal : sigc::signal...)> { public: SafeSignal() { dp_.connect(sigc::mem_fun(*this, &SafeSignal::handle_event)); } + void set_max_queued_events(std::size_t max_queued_events) { + std::unique_lock lock(mutex_); + max_queued_events_ = max_queued_events; + trim_queue_locked(); + } + template void emit(EmitArgs&&... args) { if (main_tid_ == std::this_thread::get_id()) { @@ -41,6 +48,9 @@ struct SafeSignal : sigc::signal...)> { } else { { std::unique_lock lock(mutex_); + if (max_queued_events_ != 0 && queue_.size() >= max_queued_events_) { + queue_.pop(); + } queue_.emplace(std::forward(args)...); } dp_.emit(); @@ -60,6 +70,15 @@ struct SafeSignal : sigc::signal...)> { using signal_t::emit_reverse; using signal_t::make_slot; + void trim_queue_locked() { + if (max_queued_events_ == 0) { + return; + } + while (queue_.size() > max_queued_events_) { + queue_.pop(); + } + } + void handle_event() { for (std::unique_lock lock(mutex_); !queue_.empty(); lock.lock()) { auto args = queue_.front(); @@ -72,6 +91,7 @@ struct SafeSignal : sigc::signal...)> { Glib::Dispatcher dp_; std::mutex mutex_; std::queue queue_; + std::size_t max_queued_events_ = 4096; const std::thread::id main_tid_ = std::this_thread::get_id(); // cache functor for signal emission to avoid recreating it on each event const slot_t cached_fn_ = make_slot(); diff --git a/include/util/command.hpp b/include/util/command.hpp index 58c59a96..f6d2cabf 100644 --- a/include/util/command.hpp +++ b/include/util/command.hpp @@ -20,6 +20,8 @@ extern std::list reap; namespace waybar::util::command { +constexpr int kExecFailureExitCode = 127; + struct res { int exit_code; std::string out; @@ -59,6 +61,7 @@ inline int close(FILE* fp, pid_t pid) { spdlog::debug("Cmd continued"); } else if (ret == -1) { spdlog::debug("waitpid failed: {}", strerror(errno)); + break; } else { break; } @@ -113,7 +116,9 @@ inline FILE* open(const std::string& cmd, int& pid, const std::string& output_na setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); } execlp("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0); - exit(0); + const int saved_errno = errno; + spdlog::error("execlp(/bin/sh) failed in open: {}", strerror(saved_errno)); + _exit(kExecFailureExitCode); } else { ::close(fd[1]); } @@ -161,7 +166,9 @@ inline int32_t forkExec(const std::string& cmd, const std::string& output_name) setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); } execl("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0); - exit(0); + const int saved_errno = errno; + spdlog::error("execl(/bin/sh) failed in forkExec: {}", strerror(saved_errno)); + _exit(kExecFailureExitCode); } else { reap_mtx.lock(); reap.push_back(pid); @@ -172,8 +179,6 @@ inline int32_t forkExec(const std::string& cmd, const std::string& output_name) return pid; } -inline int32_t forkExec(const std::string& cmd) { - return forkExec(cmd, ""); -} +inline int32_t forkExec(const std::string& cmd) { return forkExec(cmd, ""); } } // namespace waybar::util::command diff --git a/include/util/css_reload_helper.hpp b/include/util/css_reload_helper.hpp index 032b2382..e198a5b0 100644 --- a/include/util/css_reload_helper.hpp +++ b/include/util/css_reload_helper.hpp @@ -14,12 +14,14 @@ struct pollfd; namespace waybar { class CssReloadHelper { public: - CssReloadHelper(std::string cssFile, std::function callback); + CssReloadHelper(std::string cssFile, std::function callback); virtual ~CssReloadHelper() = default; virtual void monitorChanges(); + virtual void changeCssFile(const std::string& newCssFile); + protected: std::vector parseImports(const std::string& cssFile); @@ -42,7 +44,7 @@ class CssReloadHelper { private: std::string m_cssFile; - std::function m_callback; + std::function m_callback; std::vector>> m_fileMonitors; }; diff --git a/include/util/hex_checker.hpp b/include/util/hex_checker.hpp new file mode 100644 index 00000000..27a84d1f --- /dev/null +++ b/include/util/hex_checker.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +/** + * Result of transforming 8-bit hex codes to rgba(). + */ +struct TransformResult { + std::string css; + bool was_transformed; +}; + +/** + * Reads a CSS file, searches for 8-bit hex codes (#RRGGBBAA), + * and transforms them into GTK-compatible rgba() syntax. + */ +TransformResult transform_8bit_to_hex(const std::string& file_path); diff --git a/include/util/json.hpp b/include/util/json.hpp index 5f756f35..5f45ae92 100644 --- a/include/util/json.hpp +++ b/include/util/json.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #if (FMT_VERSION >= 90000) @@ -26,14 +27,19 @@ class JsonParser { Json::Value root; // replace all occurrences of "\x" with "\u00", because JSON doesn't allow "\x" escape sequences - std::string modifiedJsonStr = replaceHexadecimalEscape(jsonStr); + std::string modifiedJsonStr; + const std::string* json = &jsonStr; + if (jsonStr.find("\\x") != std::string::npos) { + modifiedJsonStr = replaceHexadecimalEscape(jsonStr); + json = &modifiedJsonStr; + } - std::istringstream jsonStream(modifiedJsonStr); std::string errs; // Use local CharReaderBuilder for thread safety - the IPC singleton's // parser can be called concurrently from multiple module threads Json::CharReaderBuilder readerBuilder; - if (!Json::parseFromStream(readerBuilder, jsonStream, &root, &errs)) { + auto reader = std::unique_ptr(readerBuilder.newCharReader()); + if (!reader->parse(json->data(), json->data() + json->size(), &root, &errs)) { throw std::runtime_error("Error parsing JSON: " + errs); } return root; diff --git a/include/util/scoped_fd.hpp b/include/util/scoped_fd.hpp new file mode 100644 index 00000000..e970109e --- /dev/null +++ b/include/util/scoped_fd.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +namespace waybar::util { + +class ScopedFd { + public: + explicit ScopedFd(int fd = -1) : fd_(fd) {} + ~ScopedFd() { + if (fd_ != -1) { + close(fd_); + } + } + + // ScopedFd is non-copyable + ScopedFd(const ScopedFd&) = delete; + ScopedFd& operator=(const ScopedFd&) = delete; + + // ScopedFd is moveable + ScopedFd(ScopedFd&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; } + ScopedFd& operator=(ScopedFd&& other) noexcept { + if (this != &other) { + if (fd_ != -1) { + close(fd_); + } + fd_ = other.fd_; + other.fd_ = -1; + } + return *this; + } + + int get() const { return fd_; } + + operator int() const { return fd_; } + + void reset(int fd = -1) { + if (fd_ != -1) { + close(fd_); + } + fd_ = fd; + } + + int release() { + int fd = fd_; + fd_ = -1; + return fd; + } + + private: + int fd_; +}; + +} // namespace waybar::util diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 62d12931..9d20f252 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -31,8 +32,8 @@ class SleeperThread { SleeperThread(std::function func) : thread_{[this, func] { - while (do_run_) { - signal_ = false; + while (do_run_.load(std::memory_order_relaxed)) { + signal_.store(false, std::memory_order_relaxed); func(); } }} { @@ -42,9 +43,18 @@ class SleeperThread { } SleeperThread& operator=(std::function func) { + if (thread_.joinable()) { + stop(); + thread_.join(); + } + { + std::lock_guard lck(mutex_); + do_run_.store(true, std::memory_order_relaxed); + signal_.store(false, std::memory_order_relaxed); + } thread_ = std::thread([this, func] { - while (do_run_) { - signal_ = false; + while (do_run_.load(std::memory_order_relaxed)) { + signal_.store(false, std::memory_order_relaxed); func(); } }); @@ -56,24 +66,34 @@ class SleeperThread { return *this; } - bool isRunning() const { return do_run_; } + bool isRunning() const { return do_run_.load(std::memory_order_relaxed); } auto sleep() { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; - return condvar_.wait(lk, [this] { return signal_ || !do_run_; }); + return condvar_.wait(lk, [this] { + return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); + }); } auto sleep_for(std::chrono::system_clock::duration dur) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; + + condvar_.wait(lk, [this] { + return !is_paused_ || signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); + constexpr auto max_time_point = std::chrono::steady_clock::time_point::max(); auto wait_end = max_time_point; auto now = std::chrono::steady_clock::now(); if (now < max_time_point - dur) { wait_end = now + dur; } - return condvar_.wait_until(lk, wait_end, [this] { return signal_ || !do_run_; }); + return condvar_.wait_until(lk, wait_end, [this] { + return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); + }); } auto sleep_until( @@ -81,22 +101,30 @@ class SleeperThread { time_point) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; - return condvar_.wait_until(lk, time_point, [this] { return signal_ || !do_run_; }); + + condvar_.wait(lk, [this] { + return !is_paused_ || signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); + + return condvar_.wait_until(lk, time_point, [this] { + return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); + }); } void wake_up() { { std::lock_guard lck(mutex_); - signal_ = true; + signal_.store(true, std::memory_order_relaxed); } condvar_.notify_all(); } - auto stop() { + void stop() { { std::lock_guard lck(mutex_); - signal_ = true; - do_run_ = false; + signal_.store(true, std::memory_order_relaxed); + do_run_.store(false, std::memory_order_relaxed); } condvar_.notify_all(); auto handle = thread_.native_handle(); @@ -106,6 +134,17 @@ class SleeperThread { } } + void pause() { + std::lock_guard lock(mutex_); + is_paused_ = true; + } + + void resume() { + std::lock_guard lock(mutex_); + is_paused_ = false; + condvar_.notify_all(); + } + ~SleeperThread() { connection_.disconnect(); stop(); @@ -118,9 +157,10 @@ class SleeperThread { std::thread thread_; std::condition_variable condvar_; std::mutex mutex_; - bool do_run_ = true; - bool signal_ = false; + std::atomic do_run_ = true; + std::atomic signal_ = false; sigc::connection connection_; + bool is_paused_{false}; }; } // namespace waybar::util diff --git a/include/util/utf8_string.hpp b/include/util/utf8_string.hpp new file mode 100644 index 00000000..07a04466 --- /dev/null +++ b/include/util/utf8_string.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace waybar::util { +size_t utf8_width(const std::string& str); +void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len); +} // namespace waybar::util diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 6d98fd4e..42b9e046 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -182,6 +182,7 @@ Every entry in the *events* object consists of a ** (typeof: *string - *on--* - *on--* +- *on-* Where: @@ -203,7 +204,9 @@ Where: "events": { "on-discharging-warning": "notify-send -u normal 'Low Battery'", "on-discharging-critical": "notify-send -u critical 'Very Low Battery'", - "on-charging-100": "notify-send -u normal 'Battery Full!'" + "on-charging-100": "notify-send -u normal 'Battery Full!'", + "on-discharging": "notify-send -u normal 'Power Switch' Discharging", + "on-charging": "notify-send -u normal 'Power Switch' Charging'" }, "format": "{capacity}% {icon}", "format-icons": ["", "", "", "", ""], diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index b4b5d5b7..1b13dcb7 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -204,7 +204,7 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe ``` "clock": { "format": "{:%H:%M}  ", - "format-alt": "{:%A, %B %d, %Y (%R)}  ", + "format-alt": "{:%A, %B %d, %Y (%R)} 󰃰 ", "tooltip-format": "{calendar}", "calendar": { "mode" : "year", @@ -259,7 +259,19 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe "tooltip-format": "{tz_list}" } ``` +5. Simple calendar tooltip +``` +"clock": { +"format": "{:%H:%M}", +"tooltip-format": "{calendar}", +"calendar": { +"format": { +"today": "{}" +} +} +} +``` # STYLE - *#clock* @@ -287,7 +299,7 @@ Example of working config ``` "clock": { "format": "{:%H:%M}  ", - "format-alt": "{:%A, %B %d, %Y (%R)}  ", + "format-alt": "{:%A, %B %d, %Y (%R)} 󰃰 ", "tooltip-format": "\n{calendar}", "calendar": { "mode" : "year", diff --git a/man/waybar-gamemode.5.scd b/man/waybar-gamemode.5.scd index a6ca9af0..bb82e0aa 100644 --- a/man/waybar-gamemode.5.scd +++ b/man/waybar-gamemode.5.scd @@ -43,7 +43,7 @@ Feral Gamemode optimizations. *glyph*: ++ typeof: string ++ - default:  ++ + default: 󰊴 ++ The string icon to display. Only visible if *use-icon* is set to false. *icon-name*: ++ @@ -82,7 +82,7 @@ Feral Gamemode optimizations. "gamemode": { "format": "{glyph}", "format-alt": "{glyph} {count}", - "glyph": "", + "glyph": "󰊴", "hide-not-running": true, "use-icon": true, "icon-name": "input-gaming-symbolic", diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index 5a7ba941..b5ce80aa 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -38,6 +38,20 @@ Addressed by *hyprland/language* typeof: array ++ The actions corresponding to the buttons of the menu. +*tooltip*: ++ + typeof: boolean ++ + default: true ++ + Enables or disables the tooltip for the language module. By default, the tooltip is enabled. Set to *false* to disable. + +*tooltip-format*: ++ + typeof: string ++ + default: {long} ++ + Specifies the format of the tooltip when it is enabled. It follows the same format replacement rules as the *format* key. + +*tooltip-format-*: ++ + typeof: string ++ + Allows specifying a different tooltip format for each language. The ** should be replaced with the language code. This can be used to provide a custom tooltip for each language. + *expand*: ++ typeof: bool ++ default: false ++ @@ -66,6 +80,28 @@ Addressed by *hyprland/language* } ``` +``` +"hyprland/language": { + "format": "{}", + "format-en": "US", + "format-es": "ES", + "tooltip": true, + "tooltip-format": "{long}" +} +``` + +``` +"hyprland/language": { + "format": "{}", + "format-en": "US", + "format-es": "ES", + "tooltip": true, + "tooltip-format": "{}", + "tooltip-format-es": "{Español}", + "tooltip-format-en": "{English (american)}" +} +``` + # STYLE - *#language* diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 1d04157b..c852f2ba 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -130,6 +130,19 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. Otherwise, the workspace will open on the monitor where it was previously assigned. Analog to using `focusworkspaceoncurrentmonitor` dispatcher instead of `workspace` in Hyprland. +*enable-bar-scroll*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can't scroll to cycle throughout workspaces from the entire bar. If set to true this behaviour is enabled. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. This replaces the default behaviour of workspace cycling. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. This replaces the default behaviour of workspace cycling. + *ignore-workspaces*: ++ typeof: array ++ default: [] ++ diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index 8c991265..8be182a6 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -26,7 +26,8 @@ The *image* module displays an image from a path. *interval*: ++ typeof: integer or float ++ The interval (in seconds) to re-render the image. ++ - Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + If set to a positive value, the minimum is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + Zero or negative values are treated as "once". ++ This is useful if the contents of *path* changes. ++ If no *interval* is defined, the image will only be rendered once. diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index 567c2c72..99f57299 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -102,23 +102,29 @@ Addressed by *memory* default: false ++ Enables this module to consume all left over space dynamically. +*unit*: ++ + typeof: string ++ + default: GiB ++ + Used to specify unit for total, swapTotal, used, swapUsed, avail, swapAvail, + and swapState. Accepts B, kB, kiB, MB, MiB, GB, GiB, TB, and TiB. + # FORMAT REPLACEMENTS *{percentage}*: Percentage of memory in use. *{swapPercentage}*: Percentage of swap in use. -*{total}*: Amount of total memory available in GiB. +*{total}*: Amount of total memory available. Defaults to GiB. -*{swapTotal}*: Amount of total swap available in GiB. +*{swapTotal}*: Amount of total swap available. Defaults to GiB. -*{used}*: Amount of used memory in GiB. +*{used}*: Amount of used memory. Defaults to GiB. -*{swapUsed}*: Amount of used swap in GiB. +*{swapUsed}*: Amount of used swap. Defaults to GiB. -*{avail}*: Amount of available memory in GiB. +*{avail}*: Amount of available memory. Defaults to GiB. -*{swapAvail}*: Amount of available swap in GiB. +*{swapAvail}*: Amount of available swap. Defaults to GiB. *{swapState}*: Signals if swap is activated or not diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index f555fd4d..a52d5b3e 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -178,8 +178,8 @@ to be selected when the corresponding audio device is muted. This applies to *de "alsa_output.pci-0000_00_1f.3.analog-stereo": "", "alsa_output.pci-0000_00_1f.3.analog-stereo-muted": "", "headphone": "", - "hands-free": "", - "headset": "", + "hands-free": "󰂑", + "headset": "󰂑", "phone": "", "phone-muted": "", "portable": "", diff --git a/man/waybar-river-mode.5.scd b/man/waybar-river-mode.5.scd index 5837411d..ac209f2c 100644 --- a/man/waybar-river-mode.5.scd +++ b/man/waybar-river-mode.5.scd @@ -87,7 +87,7 @@ Addressed by *river/mode* ``` "river/mode": { - "format": " {}" + "format": " {}" } ``` diff --git a/man/waybar-sway-mode.5.scd b/man/waybar-sway-mode.5.scd index 52827376..2c5ba7e6 100644 --- a/man/waybar-sway-mode.5.scd +++ b/man/waybar-sway-mode.5.scd @@ -92,7 +92,7 @@ Addressed by *sway/mode* ``` "sway/mode": { - "format": " {}", + "format": " {}", "max-length": 50 } ``` diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd index 8d7c980a..aaeee2d1 100644 --- a/man/waybar-systemd-failed-units.5.scd +++ b/man/waybar-systemd-failed-units.5.scd @@ -19,7 +19,7 @@ Addressed by *systemd-failed-units* *format-ok*: ++ typeof: string ++ - This format is used when there is no failing units. + This format is used when there are no failing units. *user*: ++ typeof: bool ++ @@ -34,15 +34,30 @@ Addressed by *systemd-failed-units* *hide-on-ok*: ++ typeof: bool ++ default: *true* ++ - Option to hide this module when there is no failing units. + Option to hide this module when there are no failed units. + +*tooltip-format*: ++ + typeof: string ++ + default: *System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n{failed_units_list}* ++ + Tooltip format shown when there are failed units. + +*tooltip-format-ok*: ++ + typeof: string ++ + default: *System: {system_state}\nUser: {user_state}* ++ + Tooltip format used when there are no failed units. + +*tooltip-unit-format*: ++ + typeof: string ++ + default: *{name}: {description}* ++ + Format used to render each failed unit inside the tooltip. Each item is prefixed with a bullet. *menu*: ++ typeof: string ++ - Action that popups the menu. + Action that pops up the menu. *menu-file*: ++ typeof: string ++ - Location of the menu descriptor file. There need to be an element of type + Location of the menu descriptor file. There needs to be an element of type GtkMenu with id *menu* *menu-actions*: ++ @@ -52,7 +67,7 @@ Addressed by *systemd-failed-units* *expand*: ++ typeof: bool ++ default: false ++ - Enables this module to consume all left over space dynamically. + Enables this module to consume all leftover space dynamically. # FORMAT REPLACEMENTS @@ -62,11 +77,23 @@ Addressed by *systemd-failed-units* *{nr_failed}*: Number of total failed units. -*{systemd_state}:* State of the systemd system session +*{system_state}:* State of the systemd system session. -*{user_state}:* State of the systemd user session +*{user_state}:* State of the systemd user session. -*{overall_state}:* Overall state of the systemd and user session. ("Ok" or "Degraded") +*{overall_state}:* Overall state of the systemd and user session. ("ok" or "degraded") + +*{failed_units_list}:* Bulleted list of failed units using *tooltip-unit-format*. Empty when +there are no failed units. + +The *tooltip-unit-format* string supports the following replacements: + +*{name}*: Unit name ++ +*{description}*: Unit description ++ +*{load_state}*: Unit load state ++ +*{active_state}*: Unit active state ++ +*{sub_state}*: Unit sub state ++ +*{scope}*: Either *system* or *user* depending on where the unit originated # EXAMPLES @@ -77,6 +104,8 @@ Addressed by *systemd-failed-units* "format-ok": "✓", "system": true, "user": false, + "tooltip-format": "{nr_failed} failed units:\n{failed_units_list}", + "tooltip-unit-format": "{scope}: {name} ({active_state})", } ``` diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index ae78f184..8d97d7af 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -128,7 +128,7 @@ The *wireplumber* module displays the current volume reported by WirePlumber. ``` "wireplumber#sink": { "format": "{volume}% {icon}", - "format-muted": "", + "format-muted": "󰅶", "format-icons": ["", "", ""], "on-click": "helvum", "on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 1b799275..8b923eef 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -36,7 +36,7 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( *expand-right* ++ typeof: bool ++ default: false ++ - Enables the modules-left to consume all left over space dynamically. + Enables the modules-right to consume all left over space dynamically. *layer* ++ typeof: string ++ @@ -363,6 +363,11 @@ A group may hide all but one element, showing them only on mouse hover. In order default: false ++ Whether left click should reveal the content rather than mouse over. Note that grouped modules may still process their own on-click events. +*start-expanded*: ++ + typeof: bool ++ + default: false ++ + Defines whether the drawer should initialize in an expanded state. + *transition-left-to-right*: ++ typeof: bool ++ default: true ++ diff --git a/meson.build b/meson.build index db9407eb..e528a6da 100644 --- a/meson.build +++ b/meson.build @@ -185,7 +185,9 @@ src_files = files( 'src/util/gtk_icon.cpp', 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', - 'src/util/css_reload_helper.cpp' + 'src/util/css_reload_helper.cpp', + 'src/util/transform_8bit_to_rgba.cpp', + 'src/util/utf8_string.cpp' ) man_files = files( diff --git a/nix/default.nix b/nix/default.nix index 43b6e097..1850a414 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -6,13 +6,13 @@ }: let libcava = rec { - version = "0.10.7-beta"; + version = "0.10.7"; src = pkgs.fetchFromGitHub { owner = "LukashonakV"; repo = "cava"; # NOTE: Needs to match the cava.wrap - tag = "v${version}"; - hash = "sha256-IX1B375gTwVDRjpRfwKGuzTAZOV2pgDWzUd4bW2cTDU="; + tag = "${version}"; + hash = "sha256-zkyj1vBzHtoypX4Bxdh1Vmwh967DKKxN751v79hzmgQ="; }; }; in diff --git a/resources/config.jsonc b/resources/config.jsonc index 67d5ff5b..9ba8b97a 100644 --- a/resources/config.jsonc +++ b/resources/config.jsonc @@ -128,7 +128,7 @@ "critical-threshold": 80, // "format-critical": "{temperatureC}°C {icon}", "format": "{temperatureC}°C {icon}", - "format-icons": ["", "", ""] + "format-icons": ["󰉬", "", "󰉪"] }, "backlight": { // "device": "acpi_video1", @@ -143,7 +143,7 @@ }, "format": "{capacity}% {icon}", "format-full": "{capacity}% {icon}", - "format-charging": "{capacity}% ", + "format-charging": "{capacity}% 󰃨", "format-plugged": "{capacity}% ", "format-alt": "{time} {icon}", // "format-good": "", // An empty format will hide the module @@ -167,9 +167,9 @@ "network": { // "interface": "wlp2*", // (Optional) To force the use of this interface "format-wifi": "{essid} ({signalStrength}%) ", - "format-ethernet": "{ipaddr}/{cidr} ", - "tooltip-format": "{ifname} via {gwaddr} ", - "format-linked": "{ifname} (No IP) ", + "format-ethernet": "{ipaddr}/{cidr} 󰊗", + "tooltip-format": "{ifname} via {gwaddr} 󰊗", + "format-linked": "{ifname} (No IP) 󰊗", "format-disconnected": "Disconnected ⚠", "format-alt": "{ifname}: {ipaddr}/{cidr}" }, @@ -177,14 +177,14 @@ // "scroll-step": 1, // %, can be a float "format": "{volume}% {icon} {format_source}", "format-bluetooth": "{volume}% {icon} {format_source}", - "format-bluetooth-muted": " {icon} {format_source}", - "format-muted": " {format_source}", + "format-bluetooth-muted": "󰅶 {icon} {format_source}", + "format-muted": "󰅶 {format_source}", "format-source": "{volume}% ", "format-source-muted": "", "format-icons": { "headphone": "", - "hands-free": "", - "headset": "", + "hands-free": "󰂑", + "headset": "󰂑", "phone": "", "portable": "", "car": "", diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 524d4d2a..4de1edf8 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -105,24 +105,26 @@ class PlayerManager: current_player = self.get_first_playing_player() if current_player is not None: self.on_metadata_changed(current_player, current_player.props.metadata) - else: + else: self.clear_output() def on_metadata_changed(self, player, metadata, _=None): logger.debug(f"Metadata changed for player {player.props.player_name}") player_name = player.props.player_name artist = player.get_artist() - artist = artist.replace("&", "&") + artist = artist and artist.replace("&", "&") title = player.get_title() - title = title.replace("&", "&") + title = title and title.replace("&", "&") track_info = "" if player_name == "spotify" and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]: track_info = "Advertisement" elif artist is not None and title is not None: track_info = f"{artist} - {title}" - else: + elif title is not None: track_info = title + elif artist is not None: + track_info = artist if track_info: if player.props.status == "Playing": diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index a309a6e0..784f30c2 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -33,21 +33,21 @@ std::string toLowerCase(const std::string& input) { std::optional getFileBySuffix(const std::string& dir, const std::string& suffix, bool check_lower_case) { - if (!std::filesystem::exists(dir)) { - return {}; - } - for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { - if (entry.is_regular_file()) { - std::string filename = entry.path().filename().string(); - if (filename.size() < suffix.size()) { - continue; - } - if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) || - (check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(), - toLowerCase(suffix)) == 0)) { - return entry.path().string(); + try { + for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + if (filename.size() < suffix.size()) { + continue; + } + if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) || + (check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(), + toLowerCase(suffix)) == 0)) { + return entry.path().string(); + } } } + } catch (const std::filesystem::filesystem_error&) { } return {}; @@ -151,19 +151,24 @@ void AAppIconLabel::updateAppIconName(const std::string& app_identifier, } void AAppIconLabel::updateAppIcon() { - if (update_app_icon_) { + if (update_app_icon_ || (!iconEnabled() && image_.get_visible())) { update_app_icon_ = false; if (app_icon_name_.empty()) { image_.set_visible(false); } else if (app_icon_name_.front() == '/') { - auto pixbuf = Gdk::Pixbuf::create_from_file(app_icon_name_); - int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); - pixbuf = Gdk::Pixbuf::create_from_file(app_icon_name_, scaled_icon_size, scaled_icon_size); + try { + int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); + auto pixbuf = + Gdk::Pixbuf::create_from_file(app_icon_name_, scaled_icon_size, scaled_icon_size); - auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), - image_.get_window()); - image_.set(surface); - image_.set_visible(true); + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), + image_.get_window()); + image_.set(surface); + image_.set_visible(true); + } catch (const Glib::Exception& e) { + spdlog::warn("Failed to load app icon {}: {}", app_icon_name_, std::string(e.what())); + image_.set_visible(false); + } } else { image_.set_from_icon_name(app_icon_name_, Gtk::ICON_SIZE_INVALID); image_.set_visible(true); diff --git a/src/ALabel.cpp b/src/ALabel.cpp index d251d896..795f87f1 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -26,7 +26,7 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st : std::chrono::milliseconds( (config_["interval"].isNumeric() ? std::max(1L, // Minimum 1ms due to millisecond precision - static_cast(config_["interval"].asDouble()) * 1000) + static_cast(config_["interval"].asDouble() * 1000)) : 1000 * (long)interval))), default_format_(format_) { label_.set_name(name); @@ -91,13 +91,17 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st // Make the GtkBuilder and check for errors in his parsing if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) { + g_object_unref(builder); throw std::runtime_error("Error found in the file " + menuFile); } menu_ = gtk_builder_get_object(builder, "menu"); if (menu_ == nullptr) { + g_object_unref(builder); throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); } + // Keep the menu alive after dropping the transient GtkBuilder. + g_object_ref(menu_); submenus_ = std::map(); menuActionsMap_ = std::map(); @@ -105,11 +109,17 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st for (Json::Value::const_iterator it = config_["menu-actions"].begin(); it != config_["menu-actions"].end(); ++it) { std::string key = it.key().asString(); - submenus_[key] = GTK_MENU_ITEM(gtk_builder_get_object(builder, key.c_str())); + auto* item = gtk_builder_get_object(builder, key.c_str()); + if (item == nullptr) { + spdlog::warn("Menu item '{}' not found in builder file", key); + continue; + } + submenus_[key] = GTK_MENU_ITEM(item); menuActionsMap_[key] = it->asString(); g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), (gpointer)menuActionsMap_[key].c_str()); } + g_object_unref(builder); } catch (std::runtime_error& e) { spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what()); } @@ -141,7 +151,8 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ if (format_icons.isArray()) { auto size = format_icons.size(); if (size != 0U) { - auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); + auto divisor = std::max(1U, (max == 0 ? 100U : static_cast(max)) / size); + auto idx = std::clamp(percentage / divisor, 0U, size - 1); format_icons = format_icons[idx]; } } @@ -167,7 +178,8 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& if (format_icons.isArray()) { auto size = format_icons.size(); if (size != 0U) { - auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); + auto divisor = std::max(1U, (max == 0 ? 100U : static_cast(max)) / size); + auto idx = std::clamp(percentage / divisor, 0U, size - 1); format_icons = format_icons[idx]; } } diff --git a/src/AModule.cpp b/src/AModule.cpp index c6fdff3e..353d0dea 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -17,10 +17,14 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false}, distance_scrolled_y_(0.0), - distance_scrolled_x_(0.0) { + distance_scrolled_x_(0.0), + cursor_timeout_conn_() { // Configure module action Map const Json::Value actions{config_["actions"]}; + disable_on_sleep_ = + config_["disable-on-sleep"].isBool() ? config_["disable-on-sleep"].asBool() : false; + for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) { if (it.key().isString() && it->isString()) if (!eventActionMap_.contains(it.key().asString())) { @@ -72,8 +76,12 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: // Respect user configuration of cursor if (config_.isMember("cursor")) { - if (config_["cursor"].isBool() && config_["cursor"].asBool()) { - setCursor(Gdk::HAND2); + if (config_["cursor"].isBool()) { + if (config_["cursor"].asBool()) { + setCursor(Gdk::HAND2); + } else { + setCursor(Gdk::ARROW); + } } else if (config_["cursor"].isInt()) { setCursor(Gdk::CursorType(config_["cursor"].asInt())); } else { @@ -83,11 +91,18 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } AModule::~AModule() { + if (cursor_timeout_conn_.connected()) { + cursor_timeout_conn_.disconnect(); + } for (const auto& pid : pid_children_) { if (pid != -1) { killpg(pid, SIGTERM); } } + if (menu_ != nullptr) { + g_object_unref(menu_); + menu_ = nullptr; + } } auto AModule::update() -> void { @@ -114,7 +129,7 @@ void AModule::setCursor(Gdk::CursorType const& c) { } else { // window may not be accessible yet, in this case, // schedule another call for setting the cursor in 1 sec - Glib::signal_timeout().connect_seconds( + cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds( [this, c]() { setCursor(c); return false; @@ -166,9 +181,9 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { } // Check that a menu has been configured - if (config_["menu"].isString()) { + if (rec != eventMap_.cend() && config_["menu"].isString()) { // Check if the event is the one specified for the "menu" option - if (rec->second == config_["menu"].asString()) { + if (rec->second == config_["menu"].asString() && menu_ != nullptr) { // Popup the menu gtk_widget_show_all(GTK_WIDGET(menu_)); gtk_menu_popup_at_pointer(GTK_MENU(menu_), reinterpret_cast(e)); diff --git a/src/bar.cpp b/src/bar.cpp index 6a78707e..44d02c5d 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -263,6 +263,16 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); + window.signal_unmap().connect([this]() { + spdlog::debug("Output {} unmapped (DPMS off), suspending modules", output->name); + toggleSuspend(true); + }); + + window.signal_map().connect([this]() { + spdlog::debug("Output {} mapped (DPMS on), resuming modules", output->name); + toggleSuspend(false); + }); + #if HAVE_SWAY if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) { bar_id = Client::inst()->bar_id; @@ -541,13 +551,15 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, auto vertical = (group != nullptr ? group->getBox().get_orientation() : box_.get_orientation()) == Gtk::ORIENTATION_VERTICAL; - auto group_config = config[ref]; + const Json::Value& group_config = config[ref]; if (group_config["modules"].isNull()) { spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); } - auto* group_module = new waybar::Group(id_name, class_name, group_config, vertical); - getModules(factory, ref, group_module); - module = group_module; + auto group_module = + std::make_unique(id_name, class_name, group_config, vertical); + + getModules(factory, ref, group_module.get()); + module = group_module.release(); } else { module = factory.makeModule(ref, pos); } @@ -694,3 +706,22 @@ void waybar::Bar::configureGlobalOffset(int width, int height) { void waybar::Bar::onOutputGeometryChanged() { configureGlobalOffset(window.get_width(), window.get_height()); } + +void waybar::Bar::toggleSuspend(bool suspend) { + auto process_modules = [suspend](Gtk::Box& module_box) { + for (auto* widget : module_box.get_children()) { + auto* module = dynamic_cast(widget); + if (module && module->shouldSuspend()) { + if (suspend) { + module->suspend(); + } else { + module->resume(); + } + } + } + }; + + process_modules(left_); + process_modules(center_); + process_modules(right_); +} diff --git a/src/client.cpp b/src/client.cpp index 103b093f..c6de74f2 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -10,6 +10,7 @@ #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" #include "util/format.hpp" +#include "util/hex_checker.hpp" waybar::Client* waybar::Client::inst() { static auto* c = new Client(); @@ -19,11 +20,23 @@ waybar::Client* waybar::Client::inst() { void waybar::Client::handleGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { auto* client = static_cast(data); + if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) { + if (client->xdg_output_manager != nullptr) { + zxdg_output_manager_v1_destroy(client->xdg_output_manager); + client->xdg_output_manager = nullptr; + } + client->xdg_output_manager = static_cast(wl_registry_bind( registry, name, &zxdg_output_manager_v1_interface, ZXDG_OUTPUT_V1_NAME_SINCE_VERSION)); + } else if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) { + if (client->idle_inhibit_manager != nullptr) { + zwp_idle_inhibit_manager_v1_destroy(client->idle_inhibit_manager); + client->idle_inhibit_manager = nullptr; + } + client->idle_inhibit_manager = static_cast( wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1)); } @@ -194,11 +207,15 @@ auto waybar::Client::setupCss(const std::string& css_file) -> void { } css_provider_ = Gtk::CssProvider::create(); - if (!css_provider_->load_from_path(css_file)) { - css_provider_.reset(); - throw std::runtime_error("Can't open style file"); + auto [modified_css, was_transformed] = transform_8bit_to_hex(css_file); + if (was_transformed) { + css_provider_->load_from_data(modified_css); + } else { + if (!css_provider_->load_from_path(css_file)) { + css_provider_.reset(); + throw std::runtime_error("Can't open style file"); + } } - Gtk::StyleContext::add_provider_for_screen(screen, css_provider_, GTK_STYLE_PROVIDER_PRIORITY_USER); } @@ -289,9 +306,10 @@ int waybar::Client::main(int argc, char* argv[]) { } m_cssFile = getStyle(style_opt); setupCss(m_cssFile); - m_cssReloadHelper = std::make_unique(m_cssFile, [&]() { setupCss(m_cssFile); }); + m_cssReloadHelper = std::make_unique(m_cssFile, [&](const std::string& css_file) { setupCss(css_file); }); portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) { auto css_file = getStyle(style_opt, appearance); + m_cssReloadHelper->changeCssFile(css_file); setupCss(css_file); }); diff --git a/src/group.cpp b/src/group.cpp index 294743d7..acd7bd42 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -64,11 +64,19 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& : true); click_to_reveal = drawer_config["click-to-reveal"].asBool(); + const bool start_expanded = + (drawer_config["start-expanded"].isBool() ? drawer_config["start-expanded"].asBool() + : false); + auto transition_type = getPreferredTransitionType(vertical); revealer.set_transition_type(transition_type); revealer.set_transition_duration(transition_duration); - revealer.set_reveal_child(false); + revealer.set_reveal_child(start_expanded); + + if (start_expanded) { + box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + } revealer.get_style_context()->add_class("drawer"); @@ -124,6 +132,11 @@ auto Group::update() -> void { // noop } +bool Group::handleScroll(GdkEventScroll* e) { + // no scroll. + return true; +} + Gtk::Box& Group::getBox() { return is_drawer ? (is_first_widget ? box : revealer_box) : box; } void Group::addWidget(Gtk::Widget& widget) { diff --git a/src/main.cpp b/src/main.cpp index 021f8e1a..9d7d0ba2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,7 +33,10 @@ static void writeSignalToPipe(int signum) { // to `signal_handler`. static void catchSignals(waybar::SafeSignal& signal_handler) { int fd[2]; - pipe(fd); + if (pipe(fd) != 0) { + spdlog::error("Failed to create signal pipe: {}", strerror(errno)); + return; + } int signal_pipe_read_fd = fd[0]; signal_pipe_write_fd = fd[1]; @@ -137,15 +140,16 @@ static void handleSignalMainThread(int signum, bool& reload) { break; case SIGCHLD: spdlog::debug("Received SIGCHLD in signalThread"); - if (!reap.empty()) { - reap_mtx.lock(); - for (auto it = reap.begin(); it != reap.end(); ++it) { + { + std::lock_guard lock(reap_mtx); + for (auto it = reap.begin(); it != reap.end();) { if (waitpid(*it, nullptr, WNOHANG) == *it) { spdlog::debug("Reaped child with PID: {}", *it); it = reap.erase(it); + } else { + ++it; } } - reap_mtx.unlock(); } break; default: diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index aabfe3f1..8f0d583f 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -58,11 +58,11 @@ auto waybar::modules::Backlight::update() -> void { tooltip_format = config_["tooltip-format"].asString(); } if (!tooltip_format.empty()) { - label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format), - fmt::arg("percent", percent), - fmt::arg("icon", getIcon(percent)))); + label_.set_tooltip_markup(fmt::format(fmt::runtime(tooltip_format), + fmt::arg("percent", percent), + fmt::arg("icon", getIcon(percent)))); } else { - label_.set_tooltip_text(desc); + label_.set_tooltip_markup(desc); } } } else { diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index bb1bb6fa..53305068 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -117,17 +117,16 @@ void waybar::modules::Battery::refreshBatteries() { if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) && (fs::exists(node.path() / "capacity") || fs::exists(node.path() / "charge_now")) && fs::exists(node.path() / "uevent") && - (fs::exists(node.path() / "status") || bat_compatibility) && - fs::exists(node.path() / "type")) { + (fs::exists(node.path() / "status") || bat_compatibility)) { std::string type; - std::ifstream(node.path() / "type") >> type; - - if (!type.compare("Battery")) { + if (std::ifstream{node.path() / "type"} >> type && !type.compare("Battery")) { // Ignore non-system power supplies unless explicitly requested - if (!bat_defined && fs::exists(node.path() / "scope")) { + if (!bat_defined) { std::string scope; - std::ifstream(node.path() / "scope") >> scope; - if (g_ascii_strcasecmp(scope.data(), "device") == 0) { + // for hotplug-in device, access it is always unstable because you may remove the + // device anytime so just allow failure happen and do nothing + if (std::ifstream{node.path() / "scope"} >> scope && + g_ascii_strcasecmp(scope.data(), "device") == 0) { continue; } } @@ -139,7 +138,9 @@ void waybar::modules::Battery::refreshBatteries() { auto event_path = (node.path() / "uevent"); auto wd = inotify_add_watch(battery_watch_fd_, event_path.c_str(), IN_ACCESS); if (wd < 0) { - throw std::runtime_error("Could not watch events for " + node.path().string()); + spdlog::warn("Could not watch events for {} (device may have been removed)", + node.path().string()); + continue; } batteries_[node.path()] = wd; } @@ -152,7 +153,7 @@ void waybar::modules::Battery::refreshBatteries() { } } } catch (fs::filesystem_error& e) { - throw std::runtime_error(e.what()); + spdlog::warn("Battery directory tracking failed: {}", e.what()); } if (warnFirstTime_ && batteries_.empty()) { if (config_["bat"].isString()) { @@ -273,11 +274,11 @@ waybar::modules::Battery::getInfos() { auto bat = item.first; std::string _status; - /* Check for adapter status if battery is not available */ - if (!std::ifstream(bat / "status")) { - std::getline(std::ifstream(adapter_ / "status"), _status); - } else { - std::getline(std::ifstream(bat / "status"), _status); + { + std::ifstream f{bat / "status"}; + if (!std::getline(f, _status)) { + std::getline(std::ifstream(adapter_ / "status"), _status); + } } // Some battery will report current and charge in μA/μAh. @@ -286,64 +287,74 @@ waybar::modules::Battery::getInfos() { uint32_t current_now = 0; int32_t _current_now_int = 0; bool current_now_exists = false; - if (fs::exists(bat / "current_now")) { - current_now_exists = true; - std::ifstream(bat / "current_now") >> _current_now_int; - } else if (fs::exists(bat / "current_avg")) { - current_now_exists = true; - std::ifstream(bat / "current_avg") >> _current_now_int; + if (std::ifstream current_now_f{bat / "current_now"}) { + if (current_now_f >> _current_now_int) { + current_now_exists = true; + } + } else if (std::ifstream current_avg_f{bat / "current_avg"}) { + if (current_avg_f >> _current_now_int) { + current_now_exists = true; + } } // Documentation ABI allows a negative value when discharging, positive // value when charging. current_now = std::abs(_current_now_int); - if (fs::exists(bat / "time_to_empty_now")) { - time_to_empty_now_exists = true; - std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now; + if (std::ifstream f{bat / "time_to_empty_now"}) { + if (f >> time_to_empty_now) { + time_to_empty_now_exists = true; + } } - if (fs::exists(bat / "time_to_full_now")) { - time_to_full_now_exists = true; - std::ifstream(bat / "time_to_full_now") >> time_to_full_now; + if (std::ifstream f{bat / "time_to_full_now"}) { + if (f >> time_to_full_now) { + time_to_full_now_exists = true; + } } uint32_t voltage_now = 0; bool voltage_now_exists = false; - if (fs::exists(bat / "voltage_now")) { - voltage_now_exists = true; - std::ifstream(bat / "voltage_now") >> voltage_now; - } else if (fs::exists(bat / "voltage_avg")) { - voltage_now_exists = true; - std::ifstream(bat / "voltage_avg") >> voltage_now; + if (std::ifstream voltage_now_f{bat / "voltage_now"}) { + if (voltage_now_f >> voltage_now) { + voltage_now_exists = true; + } + } else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) { + if (voltage_avg_f >> voltage_now) { + voltage_now_exists = true; + } } uint32_t charge_full = 0; bool charge_full_exists = false; - if (fs::exists(bat / "charge_full")) { - charge_full_exists = true; - std::ifstream(bat / "charge_full") >> charge_full; + if (std::ifstream f{bat / "charge_full"}) { + if (f >> charge_full) { + charge_full_exists = true; + } } uint32_t charge_full_design = 0; bool charge_full_design_exists = false; - if (fs::exists(bat / "charge_full_design")) { - charge_full_design_exists = true; - std::ifstream(bat / "charge_full_design") >> charge_full_design; + if (std::ifstream f{bat / "charge_full_design"}) { + if (f >> charge_full_design) { + charge_full_design_exists = true; + } } uint32_t charge_now = 0; bool charge_now_exists = false; - if (fs::exists(bat / "charge_now")) { - charge_now_exists = true; - std::ifstream(bat / "charge_now") >> charge_now; + if (std::ifstream f{bat / "charge_now"}) { + if (f >> charge_now) { + charge_now_exists = true; + } } uint32_t power_now = 0; int32_t _power_now_int = 0; bool power_now_exists = false; - if (fs::exists(bat / "power_now")) { - power_now_exists = true; - std::ifstream(bat / "power_now") >> _power_now_int; + if (std::ifstream f{bat / "power_now"}) { + if (f >> _power_now_int) { + power_now_exists = true; + } } // Some drivers (example: Qualcomm) exposes use a negative value when // discharging, positive value when charging. @@ -351,28 +362,31 @@ waybar::modules::Battery::getInfos() { uint32_t energy_now = 0; bool energy_now_exists = false; - if (fs::exists(bat / "energy_now")) { - energy_now_exists = true; - std::ifstream(bat / "energy_now") >> energy_now; + if (std::ifstream f{bat / "energy_now"}) { + if (f >> energy_now) { + energy_now_exists = true; + } } uint32_t energy_full = 0; bool energy_full_exists = false; - if (fs::exists(bat / "energy_full")) { - energy_full_exists = true; - std::ifstream(bat / "energy_full") >> energy_full; + if (std::ifstream f{bat / "energy_full"}) { + if (f >> energy_full) { + energy_full_exists = true; + } } uint32_t energy_full_design = 0; bool energy_full_design_exists = false; - if (fs::exists(bat / "energy_full_design")) { - energy_full_design_exists = true; - std::ifstream(bat / "energy_full_design") >> energy_full_design; + if (std::ifstream f{bat / "energy_full_design"}) { + if (f >> energy_full_design) { + energy_full_design_exists = true; + } } uint16_t cycleCount = 0; - if (fs::exists(bat / "cycle_count")) { - std::ifstream(bat / "cycle_count") >> cycleCount; + if (std::ifstream f{bat / "cycle_count"}) { + f >> cycleCount; } if (charge_full_design >= largestDesignCapacity) { largestDesignCapacity = charge_full_design; @@ -402,9 +416,10 @@ waybar::modules::Battery::getInfos() { } else if (energy_now_exists && energy_full_exists && energy_full != 0) { capacity_exists = true; capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; - } else if (fs::exists(bat / "capacity")) { - capacity_exists = true; - std::ifstream(bat / "capacity") >> capacity; + } else if (std::ifstream f{bat / "capacity"}) { + if (f >> capacity) { + capacity_exists = true; + } } if (!voltage_now_exists) { @@ -584,11 +599,12 @@ waybar::modules::Battery::getInfos() { float calculated_capacity{0.0f}; if (total_capacity_exists) { - if (total_capacity > 0.0f) + if (total_capacity > 0.0f) { calculated_capacity = (float)total_capacity / batteries_.size(); - else if (total_energy_full_exists && total_energy_exists) { - if (total_energy_full > 0.0f) + } else if (total_energy_full_exists && total_energy_exists) { + if (total_energy_full > 0.0f) { calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full); + } } } @@ -694,7 +710,7 @@ auto waybar::modules::Battery::update() -> void { status = "Plugged"; } auto status_pretty = status; - puts(status.c_str()); + // Transform to lowercase and replace space with dash std::ranges::transform(status.begin(), status.end(), status.begin(), [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); @@ -797,16 +813,19 @@ void waybar::modules::Battery::processEvents(std::string& state, std::string& st if (!events.isObject() || events.empty()) { return; } - std::string event_name = fmt::format("on-{}-{}", status == "discharging" ? status : "charging", - state.empty() ? std::to_string(capacity) : state); + auto exec = [](Json::Value const& event) { + if (!event.isString()) return; + if (auto command = event.asString(); !command.empty()) { + util::command::exec(command, ""); + } + }; + std::string status_name = status == "discharging" ? "on-discharging" : "on-charging"; + std::string event_name = status_name + '-' + (state.empty() ? std::to_string(capacity) : state); if (last_event_ != event_name) { spdlog::debug("battery: triggering event {}", event_name); - if (events[event_name].isString()) { - std::string exec = events[event_name].asString(); - // Execute the command if it is not empty - if (!exec.empty()) { - util::command::exec(exec, ""); - } + exec(events[event_name]); + if (!last_event_.empty() && last_event_[3] != event_name[3]) { + exec(events[status_name]); } last_event_ = event_name; } diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index f491f19b..c59af3b5 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -111,13 +111,16 @@ waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& findConnectedDevices(cur_controller_->path, connected_devices_); } - g_signal_connect(manager_.get(), "object-added", G_CALLBACK(onObjectAdded), this); - g_signal_connect(manager_.get(), "object-removed", G_CALLBACK(onObjectRemoved), this); - g_signal_connect(manager_.get(), "interface-proxy-properties-changed", - G_CALLBACK(onInterfaceProxyPropertiesChanged), this); - g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), this); - g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), - this); + if (manager_) { + g_signal_connect(manager_.get(), "object-added", G_CALLBACK(onObjectAdded), this); + g_signal_connect(manager_.get(), "object-removed", G_CALLBACK(onObjectRemoved), this); + g_signal_connect(manager_.get(), "interface-proxy-properties-changed", + G_CALLBACK(onInterfaceProxyPropertiesChanged), this); + g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), + this); + g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), + this); + } #ifdef WANT_RFKILL rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); @@ -446,6 +449,10 @@ auto waybar::modules::Bluetooth::getControllerProperties(GDBusObject* object, auto waybar::modules::Bluetooth::findCurController() -> std::optional { std::optional controller_info; + if (!manager_) { + return controller_info; + } + GList* objects = g_dbus_object_manager_get_objects(manager_.get()); for (GList* l = objects; l != NULL; l = l->next) { GDBusObject* object = G_DBUS_OBJECT(l->data); @@ -465,6 +472,9 @@ auto waybar::modules::Bluetooth::findCurController() -> std::optional& connected_devices) -> void { + if (!manager_) { + return; + } GList* objects = g_dbus_object_manager_get_objects(manager_.get()); for (GList* l = objects; l != NULL; l = l->next) { GDBusObject* object = G_DBUS_OBJECT(l->data); diff --git a/src/modules/cava/cavaRaw.cpp b/src/modules/cava/cavaRaw.cpp index d6a9e001..2b0aca72 100644 --- a/src/modules/cava/cavaRaw.cpp +++ b/src/modules/cava/cavaRaw.cpp @@ -26,6 +26,7 @@ void waybar::modules::cava::Cava::pause_resume() { backend_->doPauseResume(); } auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void { Glib::signal_idle().connect_once([this, input]() { if (silence_) { + silence_ = false; label_.get_style_context()->remove_class("silent"); if (!label_.get_style_context()->has_class("updated")) label_.get_style_context()->add_class("updated"); @@ -38,7 +39,6 @@ auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void { label_.show(); ALabel::update(); }); - silence_ = false; } auto waybar::modules::cava::Cava::onSilence() -> void { diff --git a/src/modules/cava/cava_backend.cpp b/src/modules/cava/cava_backend.cpp index c576f0cf..6eb540c9 100644 --- a/src/modules/cava/cava_backend.cpp +++ b/src/modules/cava/cava_backend.cpp @@ -218,7 +218,7 @@ void waybar::modules::cava::CavaBackend::loadConfig() { prm_.input = ::cava::input_method_by_name(config_["method"].asString().c_str()); if (config_["source"].isString()) { if (prm_.audio_source) free(prm_.audio_source); - prm_.audio_source = config_["source"].asString().data(); + prm_.audio_source = strdup(config_["source"].asString().c_str()); } if (config_["sample_rate"].isNumeric()) prm_.samplerate = config_["sample_rate"].asLargestInt(); if (config_["sample_bits"].isInt()) prm_.samplebits = config_["sample_bits"].asInt(); diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp index 5c095f46..c3f1398f 100644 --- a/src/modules/cffi.cpp +++ b/src/modules/cffi.cpp @@ -35,7 +35,7 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co throw std::runtime_error{std::string{"Missing wbcffi_init function: "} + dlerror()}; } hooks_.deinit = reinterpret_cast(dlsym(handle, "wbcffi_deinit")); - if (!hooks_.init) { + if (!hooks_.deinit) { throw std::runtime_error{std::string{"Missing wbcffi_deinit function: "} + dlerror()}; } // Optional functions @@ -71,6 +71,7 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co // Prepare config_entries array std::vector config_entries; + config_entries.reserve(keys.size()); for (size_t i = 0; i < keys.size(); i++) { config_entries.push_back({keys[i].c_str(), config_entries_stringstor[i].c_str()}); } diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 0703eaf7..8cfda2c1 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -26,9 +26,7 @@ auto waybar::modules::Cpu::update() -> void { auto [load1, load5, load15] = Load::getLoad(); auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); - if (tooltipEnabled()) { - label_.set_tooltip_text(tooltip); - } + auto format = format_; auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; auto state = getState(total_usage); @@ -48,14 +46,25 @@ auto waybar::modules::Cpu::update() -> void { store.push_back(fmt::arg("max_frequency", max_frequency)); store.push_back(fmt::arg("min_frequency", min_frequency)); store.push_back(fmt::arg("avg_frequency", avg_frequency)); + std::vector arg_names; + arg_names.reserve(cpu_usage.size() * 2); for (size_t i = 1; i < cpu_usage.size(); ++i) { auto core_i = i - 1; - auto core_format = fmt::format("usage{}", core_i); - store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i])); - auto icon_format = fmt::format("icon{}", core_i); - store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); + arg_names.push_back(fmt::format("usage{}", core_i)); + store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i])); + arg_names.push_back(fmt::format("icon{}", core_i)); + store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons))); } label_.set_markup(fmt::vformat(format, store)); + + if (tooltipEnabled()) { + if (config_["tooltip-format"].isString()) { + tooltip = config_["tooltip-format"].asString(); + label_.set_tooltip_markup(fmt::vformat(tooltip, store)); + } else { + label_.set_tooltip_markup(tooltip); + } + } } // Call parent update diff --git a/src/modules/cpu_frequency/common.cpp b/src/modules/cpu_frequency/common.cpp index e47364ba..05adc2b3 100644 --- a/src/modules/cpu_frequency/common.cpp +++ b/src/modules/cpu_frequency/common.cpp @@ -20,12 +20,7 @@ waybar::modules::CpuFrequency::CpuFrequency(const std::string& id, const Json::V auto waybar::modules::CpuFrequency::update() -> void { // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); - if (tooltipEnabled()) { - auto tooltip = - fmt::format("Minimum frequency: {}\nAverage frequency: {}\nMaximum frequency: {}\n", - min_frequency, avg_frequency, max_frequency); - label_.set_tooltip_text(tooltip); - } + auto format = format_; auto state = getState(avg_frequency); if (!state.empty() && config_["format-" + state].isString()) { @@ -43,6 +38,18 @@ auto waybar::modules::CpuFrequency::update() -> void { store.push_back(fmt::arg("min_frequency", min_frequency)); store.push_back(fmt::arg("avg_frequency", avg_frequency)); label_.set_markup(fmt::vformat(format, store)); + + if (tooltipEnabled()) { + std::string tooltip; + if (config_["tooltip-format"].isString()) { + tooltip = config_["tooltip-format"].asString(); + label_.set_tooltip_markup(fmt::vformat(tooltip, store)); + } else { + tooltip = "Minimum frequency: {}\nAverage frequency: {}\nMaximum frequency: {}\n"; + label_.set_tooltip_markup( + fmt::format(fmt::runtime(tooltip), min_frequency, avg_frequency, max_frequency)); + } + } } // Call parent update diff --git a/src/modules/cpu_frequency/linux.cpp b/src/modules/cpu_frequency/linux.cpp index 1f368789..7b927cdc 100644 --- a/src/modules/cpu_frequency/linux.cpp +++ b/src/modules/cpu_frequency/linux.cpp @@ -23,23 +23,21 @@ std::vector waybar::modules::CpuFrequency::parseCpuFrequencies() { if (frequencies.size() <= 0) { std::string cpufreq_dir = "/sys/devices/system/cpu/cpufreq"; - if (std::filesystem::exists(cpufreq_dir)) { + try { std::vector frequency_files = {"/cpuinfo_min_freq", "/cpuinfo_max_freq"}; for (auto& p : std::filesystem::directory_iterator(cpufreq_dir)) { - for (auto freq_file : frequency_files) { + for (const auto& freq_file : frequency_files) { std::string freq_file_path = p.path().string() + freq_file; - if (std::filesystem::exists(freq_file_path)) { - std::string freq_value; - std::ifstream freq(freq_file_path); - if (freq.is_open()) { - getline(freq, freq_value); - float frequency = std::strtol(freq_value.c_str(), nullptr, 10); - frequencies.push_back(frequency / 1000); - freq.close(); - } + std::string freq_value; + std::ifstream freq(freq_file_path); + if (freq.is_open()) { + getline(freq, freq_value); + float frequency = std::strtol(freq_value.c_str(), nullptr, 10); + frequencies.push_back(frequency / 1000); } } } + } catch (const std::filesystem::filesystem_error&) { } } diff --git a/src/modules/cpu_usage/common.cpp b/src/modules/cpu_usage/common.cpp index e3947967..1571cb74 100644 --- a/src/modules/cpu_usage/common.cpp +++ b/src/modules/cpu_usage/common.cpp @@ -20,9 +20,7 @@ waybar::modules::CpuUsage::CpuUsage(const std::string& id, const Json::Value& co auto waybar::modules::CpuUsage::update() -> void { // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); - if (tooltipEnabled()) { - label_.set_tooltip_text(tooltip); - } + auto format = format_; auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; auto state = getState(total_usage); @@ -38,14 +36,25 @@ auto waybar::modules::CpuUsage::update() -> void { fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("usage", total_usage)); store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); + std::vector arg_names; + arg_names.reserve(cpu_usage.size() * 2); for (size_t i = 1; i < cpu_usage.size(); ++i) { auto core_i = i - 1; - auto core_format = fmt::format("usage{}", core_i); - store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i])); - auto icon_format = fmt::format("icon{}", core_i); - store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); + arg_names.push_back(fmt::format("usage{}", core_i)); + store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i])); + arg_names.push_back(fmt::format("icon{}", core_i)); + store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons))); } label_.set_markup(fmt::vformat(format, store)); + + if (tooltipEnabled()) { + if (config_["tooltip-format"].isString()) { + tooltip = config_["tooltip-format"].asString(); + label_.set_tooltip_markup(fmt::vformat(tooltip, store)); + } else { + label_.set_tooltip_markup(tooltip); + } + } } // Call parent update @@ -71,7 +80,8 @@ std::tuple, std::string> waybar::modules::CpuUsage::getCpu auto [prev_idle, prev_total] = prev_times[0]; const float delta_idle = curr_idle - prev_idle; const float delta_total = curr_total - prev_total; - uint16_t tmp = 100 * (1 - delta_idle / delta_total); + uint16_t tmp = + (delta_total > 0) ? static_cast(100 * (1 - delta_idle / delta_total)) : 0; tooltip = fmt::format("Total: {}%\nCores: (pending)", tmp); usage.push_back(tmp); } else { @@ -93,7 +103,8 @@ std::tuple, std::string> waybar::modules::CpuUsage::getCpu } const float delta_idle = curr_idle - prev_idle; const float delta_total = curr_total - prev_total; - uint16_t tmp = 100 * (1 - delta_idle / delta_total); + uint16_t tmp = + (delta_total > 0) ? static_cast(100 * (1 - delta_idle / delta_total)) : 0; if (i == 0) { tooltip = fmt::format("Total: {}%", tmp); } else { diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 0a8d9cf6..1d9b320c 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -2,6 +2,8 @@ #include +#include + #include "util/scope_guard.hpp" waybar::modules::Custom::Custom(const std::string& name, const std::string& id, @@ -37,6 +39,11 @@ waybar::modules::Custom::~Custom() { } void waybar::modules::Custom::delayWorker() { + if (!config_["exec"].isString() && !config_["exec-if"].isString()) { + dp.emit(); + return; + } + thread_ = [this] { for (int i : this->pid_children_) { int status; @@ -180,16 +187,22 @@ auto waybar::modules::Custom::update() -> void { } else { label_.set_markup(str); if (tooltipEnabled()) { + std::string tooltip_markup; if (tooltip_format_enabled_) { auto tooltip = config_["tooltip-format"].asString(); - tooltip = fmt::format( - fmt::runtime(tooltip), fmt::arg("text", text_), fmt::arg("alt", alt_), - fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); - label_.set_tooltip_markup(tooltip); + tooltip_markup = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_), + fmt::arg("tooltip", tooltip_), fmt::arg("alt", alt_), + fmt::arg("icon", getIcon(percentage_, alt_)), + fmt::arg("percentage", percentage_)); } else if (text_ == tooltip_) { - label_.set_tooltip_markup(str); + tooltip_markup = str; } else { - label_.set_tooltip_markup(tooltip_); + tooltip_markup = tooltip_; + } + + if (last_tooltip_markup_ != tooltip_markup) { + label_.set_tooltip_markup(tooltip_markup); + last_tooltip_markup_ = std::move(tooltip_markup); } } auto style = label_.get_style_context(); diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index ef257b72..462940b2 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -41,7 +41,7 @@ auto waybar::modules::Disk::update() -> void { fs_used - File system used space */ - if (err != 0) { + if (err != 0 || stats.f_blocks == 0) { event_box_.hide(); return; } @@ -81,7 +81,7 @@ auto waybar::modules::Disk::update() -> void { if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text(fmt::format( + label_.set_tooltip_markup(fmt::format( fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used), fmt::arg("percentage_used", percentage_used), fmt::arg("total", total), @@ -92,7 +92,7 @@ auto waybar::modules::Disk::update() -> void { ALabel::update(); } -float waybar::modules::Disk::calc_specific_divisor(std::string divisor) { +float waybar::modules::Disk::calc_specific_divisor(const std::string& divisor) { if (divisor == "kB") { return 1000.0; } else if (divisor == "kiB") { @@ -112,4 +112,4 @@ float waybar::modules::Disk::calc_specific_divisor(std::string divisor) { } else { // default to Bytes if it is anything that we don't recongnise return 1.0; } -} \ No newline at end of file +} diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp index fb065650..243aaa92 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -70,17 +70,33 @@ static const zdwl_ipc_output_v2_listener output_status_listener_impl{ static void handle_global(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { - if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) { - static_cast(data)->status_manager_ = static_cast( - (zdwl_ipc_manager_v2*)wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1)); - } - if (std::strcmp(interface, wl_seat_interface.name) == 0) { - version = std::min(version, 1); - static_cast(data)->seat_ = - static_cast(wl_registry_bind(registry, name, &wl_seat_interface, version)); - } + +if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) { + auto* self = static_cast(data); + + if (self->status_manager_) { + zdwl_ipc_manager_v2_destroy(self->status_manager_); + self->status_manager_ = nullptr; } + self->status_manager_ = static_cast( + wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1)); +} + +if (std::strcmp(interface, wl_seat_interface.name) == 0) { + auto* self = static_cast(data); + + if (self->seat_) { + wl_seat_destroy(self->seat_); + self->seat_ = nullptr; + } + + version = std::min(version, 1); + + self->seat_ = static_cast( + wl_registry_bind(registry, name, &wl_seat_interface, version)); +} +} static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) { /* Ignore event */ } @@ -179,6 +195,7 @@ bool Tags::handle_button_press(GdkEventButton* event_button, uint32_t tag) { } void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused) { + if (tag >= buttons_.size()) return; // First clear all occupied state auto& button = buttons_[tag]; if (clients) { diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp index 2b679c9f..56e67895 100644 --- a/src/modules/dwl/window.cpp +++ b/src/modules/dwl/window.cpp @@ -116,7 +116,7 @@ void Window::handle_frame() { updateAppIconName(appid_, ""); updateAppIcon(); if (tooltipEnabled()) { - label_.set_tooltip_text(title_); + label_.set_tooltip_markup(title_); } } diff --git a/src/modules/ext/workspace_manager.cpp b/src/modules/ext/workspace_manager.cpp index 637177d3..b4471c14 100644 --- a/src/modules/ext/workspace_manager.cpp +++ b/src/modules/ext/workspace_manager.cpp @@ -143,7 +143,9 @@ void WorkspaceManager::handle_finished() { ext_manager_ = nullptr; } -void WorkspaceManager::commit() const { ext_workspace_manager_v1_commit(ext_manager_); } +void WorkspaceManager::commit() const { + if (ext_manager_) ext_workspace_manager_v1_commit(ext_manager_); +} void WorkspaceManager::update() { spdlog::debug("[ext/workspaces]: Updating state"); diff --git a/src/modules/gamemode.cpp b/src/modules/gamemode.cpp index 72ef9503..691a2844 100644 --- a/src/modules/gamemode.cpp +++ b/src/modules/gamemode.cpp @@ -128,9 +128,9 @@ void Gamemode::getData() { Glib::VariantContainerBase data = gamemode_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); + g_variant_get(const_cast(data.gobj()), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_INT32)) { - g_variant_get(variant.gobj_copy(), "i", &gameCount); + g_variant_get(const_cast(variant.gobj()), "i", &gameCount); return; } } @@ -158,7 +158,7 @@ void Gamemode::prepareForSleep_cb(const Glib::RefPtr& con const Glib::VariantContainerBase& parameters) { if (parameters.is_of_type(Glib::VariantType("(b)"))) { gboolean sleeping; - g_variant_get(parameters.gobj_copy(), "(b)", &sleeping); + g_variant_get(const_cast(parameters.gobj()), "(b)", &sleeping); if (!sleeping) { getData(); dp.emit(); @@ -212,7 +212,7 @@ auto Gamemode::update() -> void { // Tooltip if (tooltip) { std::string text = fmt::format(fmt::runtime(tooltip_format), fmt::arg("count", gameCount)); - box_.set_tooltip_text(text); + box_.set_tooltip_markup(text); } // Label format diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 7060d304..f38f8216 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -9,44 +9,47 @@ #include #include +#include +#include +#include #include +#include #include +#include "util/scoped_fd.hpp" + namespace waybar::modules::hyprland { std::filesystem::path IPC::socketFolder_; +std::optional IPC::s_luaProtocolDetected_; std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { static std::mutex folderMutex; std::unique_lock lock(folderMutex); - // socket path, specified by EventManager of Hyprland - if (!socketFolder_.empty()) { - return socketFolder_; + if (socketFolder_.empty()) { + const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR"); + std::filesystem::path xdgRuntimeDir; + // Only set path if env variable is set + if (xdgRuntimeDirEnv != nullptr) { + xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv); + } + + if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) { + socketFolder_ = xdgRuntimeDir / "hypr"; + } else { + spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr"); + socketFolder_ = std::filesystem::path("/tmp") / "hypr"; + } } - const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR"); - std::filesystem::path xdgRuntimeDir; - // Only set path if env variable is set - if (xdgRuntimeDirEnv != nullptr) { - xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv); - } - - if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) { - socketFolder_ = xdgRuntimeDir / "hypr"; - } else { - spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr"); - socketFolder_ = std::filesystem::path("/tmp") / "hypr"; - } - - socketFolder_ = socketFolder_ / instanceSig; - return socketFolder_; + return socketFolder_ / instanceSig; } IPC::IPC() { // will start IPC and relay events to parseIPC - ipcThread_ = std::thread([this]() { socketListener(); }); socketOwnerPid_ = getpid(); + ipcThread_ = std::thread([this]() { socketListener(); }); } IPC::~IPC() { @@ -54,19 +57,20 @@ IPC::~IPC() { // failed exec()) exits. if (getpid() != socketOwnerPid_) return; - running_ = false; + running_.store(false, std::memory_order_relaxed); spdlog::info("Hyprland IPC stopping..."); - if (socketfd_ != -1) { - spdlog::trace("Shutting down socket"); - if (shutdown(socketfd_, SHUT_RDWR) == -1) { - spdlog::error("Hyprland IPC: Couldn't shutdown socket"); - } - spdlog::trace("Closing socket"); - if (close(socketfd_) == -1) { - spdlog::error("Hyprland IPC: Couldn't close socket"); + { + std::lock_guard lock(socketMutex_); + if (socketfd_ != -1) { + spdlog::trace("Shutting down socket"); + if (shutdown(socketfd_, SHUT_RDWR) == -1 && errno != ENOTCONN) { + spdlog::error("Hyprland IPC: Couldn't shutdown socket"); + } } } - ipcThread_.join(); + if (ipcThread_.joinable()) { + ipcThread_.join(); + } } IPC& IPC::inst() { @@ -85,10 +89,10 @@ void IPC::socketListener() { spdlog::info("Hyprland IPC starting"); - struct sockaddr_un addr; - socketfd_ = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un addr = {}; + const int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); - if (socketfd_ == -1) { + if (socketfd == -1) { spdlog::error("Hyprland IPC: socketfd failed"); return; } @@ -96,44 +100,76 @@ void IPC::socketListener() { addr.sun_family = AF_UNIX; auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock"; + if (socketPath.native().size() >= sizeof(addr.sun_path)) { + spdlog::error("Hyprland IPC: Socket path is too long: {}", socketPath.string()); + close(socketfd); + return; + } strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); - addr.sun_path[sizeof(addr.sun_path) - 1] = 0; - int l = sizeof(struct sockaddr_un); - if (connect(socketfd_, (struct sockaddr*)&addr, l) == -1) { - spdlog::error("Hyprland IPC: Unable to connect?"); + if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { + spdlog::error("Hyprland IPC: Unable to connect? {}", std::strerror(errno)); + close(socketfd); return; } - auto* file = fdopen(socketfd_, "r"); - if (file == nullptr) { - spdlog::error("Hyprland IPC: Couldn't open file descriptor"); - return; + + { + std::lock_guard lock(socketMutex_); + socketfd_ = socketfd; } - while (running_) { + + std::string pending; + while (running_.load(std::memory_order_relaxed)) { std::array buffer; // Hyprland socket2 events are max 1024 bytes + const ssize_t bytes_read = read(socketfd, buffer.data(), buffer.size()); - auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file); - - if (receivedCharPtr == nullptr) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - continue; + if (bytes_read == 0) { + if (running_.load(std::memory_order_relaxed)) { + spdlog::warn("Hyprland IPC: Socket closed by peer"); + } + break; } - std::string messageReceived(buffer.data()); - messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n')); - spdlog::debug("hyprland IPC received {}", messageReceived); - - try { - parseIPC(messageReceived); - } catch (std::exception& e) { - spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what()); - } catch (...) { - throw; + if (bytes_read < 0) { + if (errno == EINTR) { + continue; + } + if (!running_.load(std::memory_order_relaxed)) { + break; + } + spdlog::error("Hyprland IPC: read failed: {}", std::strerror(errno)); + break; } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + pending.append(buffer.data(), static_cast(bytes_read)); + for (auto newline_pos = pending.find('\n'); newline_pos != std::string::npos; + newline_pos = pending.find('\n')) { + std::string messageReceived = pending.substr(0, newline_pos); + pending.erase(0, newline_pos + 1); + if (messageReceived.empty()) { + continue; + } + spdlog::debug("hyprland IPC received {}", messageReceived); + + try { + parseIPC(messageReceived); + } catch (std::exception& e) { + spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what()); + } catch (...) { + throw; + } + } + } + { + std::lock_guard lock(socketMutex_); + if (socketfd_ != -1) { + if (close(socketfd_) == -1) { + spdlog::error("Hyprland IPC: Couldn't close socket"); + } + socketfd_ = -1; + } } spdlog::debug("Hyprland IPC stopped"); } @@ -178,7 +214,7 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) { std::string IPC::getSocket1Reply(const std::string& rq) { // basically hyprctl - const auto serverSocket = socket(AF_UNIX, SOCK_STREAM, 0); + util::ScopedFd serverSocket(socket(AF_UNIX, SOCK_STREAM, 0)); if (serverSocket < 0) { throw std::runtime_error("Hyprland IPC: Couldn't open a socket (1)"); @@ -198,8 +234,10 @@ std::string IPC::getSocket1Reply(const std::string& rq) { std::string socketPath = IPC::getSocketFolder(instanceSig) / ".socket.sock"; // Use snprintf to copy the socketPath string into serverAddress.sun_path - if (snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()) < - 0) { + const auto socketPathLength = + snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()); + if (socketPathLength < 0 || + socketPathLength >= static_cast(sizeof(serverAddress.sun_path))) { throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)"); } @@ -208,28 +246,39 @@ std::string IPC::getSocket1Reply(const std::string& rq) { throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); } - auto sizeWritten = write(serverSocket, rq.c_str(), rq.length()); + std::size_t totalWritten = 0; + while (totalWritten < rq.length()) { + const auto sizeWritten = + write(serverSocket, rq.c_str() + totalWritten, rq.length() - totalWritten); - if (sizeWritten < 0) { - spdlog::error("Hyprland IPC: Couldn't write (4)"); - return ""; + if (sizeWritten < 0) { + if (errno == EINTR) { + continue; + } + spdlog::error("Hyprland IPC: Couldn't write (4)"); + return ""; + } + if (sizeWritten == 0) { + spdlog::error("Hyprland IPC: Socket write made no progress"); + return ""; + } + totalWritten += static_cast(sizeWritten); } std::array buffer = {0}; std::string response; + ssize_t sizeWritten = 0; do { sizeWritten = read(serverSocket, buffer.data(), 8192); if (sizeWritten < 0) { spdlog::error("Hyprland IPC: Couldn't read (5)"); - close(serverSocket); return ""; } response.append(buffer.data(), sizeWritten); } while (sizeWritten > 0); - close(serverSocket); return response; } @@ -243,4 +292,69 @@ Json::Value IPC::getSocket1JsonReply(const std::string& rq) { return parser_.parse(reply); } +bool IPC::isLuaProtocol() { + if (s_luaProtocolDetected_.has_value()) { + return *s_luaProtocolDetected_; + } + + // Probe: send a harmless old-style dispatch and check the error. + // In Lua-based Hyprland (>= 0.54) the error contains "hl.dispatch". + // In older versions it returns "ok" or a different error. + auto reply = getSocket1Reply("dispatch workspace __waybar_probe__"); + bool luaProto = reply.find("hl.dispatch") != std::string::npos; + + if (luaProto) { + spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol (Hyprland >= 0.54)"); + } else { + spdlog::info("Hyprland IPC: detected legacy dispatch protocol"); + } + + s_luaProtocolDetected_ = luaProto; + return luaProto; +} + +std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::string& arg) { + // Map old-style dispatchers to the new Lua hl.dsp API. + // + // Old format: dispatch workspace 1 + // New format: /dispatch hl.dsp.focus({ workspace = "1" }) + // + // Old format: dispatch focusworkspaceoncurrentmonitor 2 + // New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true }) + // + // Old format: dispatch togglespecialworkspace name + // New format: /dispatch hl.dsp.workspace.toggle_special("name") + + if (dispatcher == "workspace") { + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; + } + if (dispatcher == "focusworkspaceoncurrentmonitor") { + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })"; + } + if (dispatcher == "togglespecialworkspace") { + if (arg.empty()) { + return "/dispatch hl.dsp.workspace.toggle_special()"; + } + return "/dispatch hl.dsp.workspace.toggle_special(\"" + arg + "\")"; + } + + // Fallback for any other dispatcher: try the old format wrapped in dispatch(). + // This may not work for all dispatchers, but it's a reasonable default. + spdlog::warn("Hyprland IPC: unknown dispatcher '{}' in Lua mode, attempting generic format", + dispatcher); + return "/dispatch hl.dsp." + dispatcher + "(\"" + arg + "\")"; +} + +std::string IPC::dispatch(const std::string& dispatcher, const std::string& arg) { + if (isLuaProtocol()) { + return getSocket1Reply(buildLuaDispatch(dispatcher, arg)); + } + // Legacy format: "dispatch " + std::string cmd = "dispatch " + dispatcher; + if (!arg.empty()) { + cmd += " " + arg; + } + return getSocket1Reply(cmd); +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 25f6789d..e413f578 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -48,12 +48,39 @@ auto Language::update() -> void { fmt::arg("shortDescription", layout_.short_description), fmt::arg("variant", layout_.variant))); } - spdlog::debug("hyprland language formatted layout name {}", layoutName); + std::string tooltipContent = std::string{}; + bool tooltip_enabled = tooltipEnabled(); + if (tooltip_enabled) { + if (config_.isMember("tooltip-format")) { + auto tooltip_format = config_["tooltip-format"].asString(); + if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) { + const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant; + tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); + } else if (config_.isMember("tooltip-format-" + layout_.short_description)) { + const auto propName = "tooltip-format-" + layout_.short_description; + tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); + } else { + tooltipContent = + trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); + } + } else { + // if no tooltip format is provided, use the same text as the module + tooltipContent = layoutName; + } + spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); + } + if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); + if (tooltip_enabled) { + label_.set_tooltip_markup(tooltipContent); + } } else { label_.hide(); } @@ -63,22 +90,43 @@ auto Language::update() -> void { void Language::onEvent(const std::string& ev) { std::lock_guard lg(mutex_); - std::string kbName(begin(ev) + ev.find_last_of('>') + 1, begin(ev) + ev.find_first_of(',')); - + const auto payloadStart = ev.find(">>"); + if (payloadStart == std::string::npos) { + spdlog::warn("hyprland language received malformed event: {}", ev); + return; + } + const auto payload = ev.substr(payloadStart + 2); + const auto kbSeparator = payload.find(','); + if (kbSeparator == std::string::npos) { + spdlog::warn("hyprland language received malformed event payload: {}", ev); + return; + } // Last comma before variants parenthesis, eg: // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., // with dead keys) std::string beforeParenthesis; - auto parenthesisPos = ev.find_last_of('('); + auto parenthesisPos = payload.find_last_of('('); if (parenthesisPos == std::string::npos) { - beforeParenthesis = ev; + beforeParenthesis = payload; } else { - beforeParenthesis = std::string(begin(ev), begin(ev) + parenthesisPos); + beforeParenthesis = payload.substr(0, parenthesisPos); } - auto layoutName = ev.substr(beforeParenthesis.find_last_of(',') + 1); + const auto layoutSeparator = beforeParenthesis.find_last_of(','); + if (layoutSeparator == std::string::npos) { + spdlog::warn("hyprland language received malformed layout payload: {}", ev); + return; + } + auto layoutName = payload.substr(layoutSeparator + 1); - if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) - return; // ignore + if (config_.isMember("keyboard-name")) { + const auto keyboardName = config_["keyboard-name"].asString(); + // The keyboard name itself can contain commas, so match it as a full prefix + // (followed by the ',' separator) rather than comparing against the substring + // before the first comma, which would truncate such names and drop the event. + if (payload.size() <= keyboardName.size() || payload[keyboardName.size()] != ',' || + payload.compare(0, keyboardName.size(), keyboardName) != 0) + return; // ignore + } layoutName = waybar::util::sanitize_string(layoutName); diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp index 5d587d02..36b9bf79 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -60,7 +60,7 @@ auto Submap::update() -> void { } else { label_.set_markup(fmt::format(fmt::runtime(format_), submap_)); if (tooltipEnabled()) { - label_.set_tooltip_text(submap_); + label_.set_tooltip_markup(submap_); } event_box_.show(); } @@ -75,7 +75,12 @@ void Submap::onEvent(const std::string& ev) { return; } - auto submapName = ev.substr(ev.find_first_of('>') + 2); + const auto separator = ev.find(">>"); + if (separator == std::string::npos) { + spdlog::warn("hyprland submap received malformed event: {}", ev); + return; + } + auto submapName = ev.substr(separator + 2); submap_ = submapName; diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index e02a7691..4a855018 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -19,21 +19,19 @@ std::shared_mutex windowIpcSmtx; Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), m_ipc(IPC::inst()) { - std::unique_lock windowIpcUniqueLock(windowIpcSmtx); - separateOutputs_ = config["separate-outputs"].asBool(); + update(); + // register for hyprland ipc + std::unique_lock windowIpcUniqueLock(windowIpcSmtx); m_ipc.registerForIPC("activewindow", this); m_ipc.registerForIPC("closewindow", this); m_ipc.registerForIPC("movewindow", this); m_ipc.registerForIPC("changefloatingmode", this); m_ipc.registerForIPC("fullscreen", this); - windowIpcUniqueLock.unlock(); - queryActiveWorkspace(); - update(); dp.emit(); } @@ -72,13 +70,13 @@ auto Window::update() -> void { tooltip_format = config_["tooltip-format"].asString(); } if (!tooltip_format.empty()) { - label_.set_tooltip_text( + label_.set_tooltip_markup( fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName), fmt::arg("initialTitle", windowData_.initial_title), fmt::arg("class", windowData_.class_name), fmt::arg("initialClass", windowData_.initial_class_name))); } else if (!label_text.empty()) { - label_.set_tooltip_text(label_text); + label_.set_tooltip_markup(label_text); } } @@ -110,21 +108,14 @@ auto Window::update() -> void { AAppIconLabel::update(); } -auto Window::getActiveWorkspace() -> Workspace { - const auto workspace = IPC::inst().getSocket1JsonReply("activeworkspace"); - - if (workspace.isObject()) { - return Workspace::parse(workspace); - } - - return {}; -} +auto Window::getActiveWorkspace() -> Workspace { return getActiveWorkspace(""); } auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { const auto monitors = IPC::inst().getSocket1JsonReply("monitors"); if (monitors.isArray()) { - auto monitor = std::ranges::find_if( - monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + auto monitor = std::ranges::find_if(monitors, [&](const Json::Value& monitor) { + return monitorName.empty() ? monitor["focused"].asBool() : monitor["name"] == monitorName; + }); if (monitor == std::end(monitors)) { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{ @@ -134,12 +125,13 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { .last_window_title = "", }; } - const int id = (*monitor)["activeWorkspace"]["id"].asInt(); + const int special_id = (*monitor)["specialWorkspace"]["id"].asInt(); + const int id = special_id != 0 ? special_id : (*monitor)["activeWorkspace"]["id"].asInt(); const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces"); if (workspaces.isArray()) { auto workspace = std::ranges::find_if( - workspaces, [&](Json::Value workspace) { return workspace["id"] == id; }); + workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; }); if (workspace == std::end(workspaces)) { spdlog::warn("No workspace with id {}", id); return Workspace{ @@ -177,69 +169,69 @@ auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData { } void Window::queryActiveWorkspace() { - std::shared_lock windowIpcShareLock(windowIpcSmtx); - if (separateOutputs_) { workspace_ = getActiveWorkspace(this->bar_.output->name); } else { workspace_ = getActiveWorkspace(); } + focused_ = false; + windowData_ = WindowData{}; + allFloating_ = false; + swallowing_ = false; + fullscreen_ = false; + solo_ = false; + soloClass_.clear(); + + if (workspace_.windows <= 0) { + return; + } + + const auto clients = m_ipc.getSocket1JsonReply("clients"); + if (!clients.isArray()) { + return; + } + + auto activeWindow = std::ranges::find_if(clients, [&](const Json::Value& window) { + return window["address"] == workspace_.last_window; + }); + + if (activeWindow == std::end(clients)) { + return; + } + focused_ = true; - if (workspace_.windows > 0) { - const auto clients = m_ipc.getSocket1JsonReply("clients"); - if (clients.isArray()) { - auto activeWindow = std::ranges::find_if( - clients, [&](Json::Value window) { return window["address"] == workspace_.last_window; }); - - if (activeWindow == std::end(clients)) { - focused_ = false; - return; - } - - windowData_ = WindowData::parse(*activeWindow); - updateAppIconName(windowData_.class_name, windowData_.initial_class_name); - std::vector workspaceWindows; - std::ranges::copy_if(clients, std::back_inserter(workspaceWindows), [&](Json::Value window) { + windowData_ = WindowData::parse(*activeWindow); + updateAppIconName(windowData_.class_name, windowData_.initial_class_name); + std::vector workspaceWindows; + std::ranges::copy_if( + clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) { return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); }); - swallowing_ = std::ranges::any_of(workspaceWindows, [&](Json::Value window) { - return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; - }); - std::vector visibleWindows; - std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), - [&](Json::Value window) { return !window["hidden"].asBool(); }); - solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(), - [&](Json::Value window) { return !window["floating"].asBool(); }); - allFloating_ = std::ranges::all_of( - visibleWindows, [&](Json::Value window) { return window["floating"].asBool(); }); - fullscreen_ = windowData_.fullscreen; + swallowing_ = std::ranges::any_of(workspaceWindows, [&](const Json::Value& window) { + return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; + }); + std::vector visibleWindows; + std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), + [&](const Json::Value& window) { return !window["hidden"].asBool(); }); + solo_ = 1 == std::count_if( + visibleWindows.begin(), visibleWindows.end(), + [&](const Json::Value& window) { return !window["floating"].asBool(); }); + allFloating_ = std::ranges::all_of( + visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); }); + fullscreen_ = windowData_.fullscreen; - // Fullscreen windows look like they are solo - if (fullscreen_) { - solo_ = true; - } + // Fullscreen windows look like they are solo + if (fullscreen_) { + solo_ = true; + } - if (solo_) { - soloClass_ = windowData_.class_name; - } else { - soloClass_ = ""; - } - } - } else { - focused_ = false; - windowData_ = WindowData{}; - allFloating_ = false; - swallowing_ = false; - fullscreen_ = false; - solo_ = false; - soloClass_ = ""; + if (solo_) { + soloClass_ = windowData_.class_name; } } -void Window::onEvent(const std::string& ev) { - dp.emit(); -} +void Window::onEvent(const std::string& ev) { dp.emit(); } void Window::setClass(const std::string& classname, bool enable) { if (enable) { diff --git a/src/modules/hyprland/windowcount.cpp b/src/modules/hyprland/windowcount.cpp index 487b0083..5f5db80c 100644 --- a/src/modules/hyprland/windowcount.cpp +++ b/src/modules/hyprland/windowcount.cpp @@ -38,7 +38,7 @@ WindowCount::~WindowCount() { auto WindowCount::update() -> void { std::lock_guard lg(mutex_); - + queryActiveWorkspace(); std::string format = config_["format"].asString(); @@ -58,7 +58,7 @@ auto WindowCount::update() -> void { } else if (!format.empty()) { label_.set_markup(fmt::format(fmt::runtime(format), workspace_.windows)); } else { - label_.set_text(fmt::format("{}", workspace_.windows)); + label_.set_markup(fmt::format("{}", workspace_.windows)); } label_.show(); @@ -79,7 +79,7 @@ auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspac const auto monitors = m_ipc.getSocket1JsonReply("monitors"); if (monitors.isArray()) { auto monitor = std::ranges::find_if( - monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + monitors, [&](const Json::Value& monitor) { return monitor["name"] == monitorName; }); if (monitor == std::end(monitors)) { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{ @@ -93,7 +93,7 @@ auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspac const auto workspaces = m_ipc.getSocket1JsonReply("workspaces"); if (workspaces.isArray()) { auto workspace = std::ranges::find_if( - workspaces, [&](Json::Value workspace) { return workspace["id"] == id; }); + workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; }); if (workspace == std::end(workspaces)) { spdlog::warn("No workspace with id {}", id); return Workspace{ @@ -125,9 +125,7 @@ void WindowCount::queryActiveWorkspace() { } } -void WindowCount::onEvent(const std::string& ev) { - dp.emit(); -} +void WindowCount::onEvent(const std::string& ev) { dp.emit(); } void WindowCount::setClass(const std::string& classname, bool enable) { if (enable) { diff --git a/src/modules/hyprland/windowcreationpayload.cpp b/src/modules/hyprland/windowcreationpayload.cpp index 61ea606f..5ace5b32 100644 --- a/src/modules/hyprland/windowcreationpayload.cpp +++ b/src/modules/hyprland/windowcreationpayload.cpp @@ -19,7 +19,7 @@ WindowCreationPayload::WindowCreationPayload(Json::Value const& client_data) clearWorkspaceName(); } -WindowCreationPayload::WindowCreationPayload(std::string workspace_name, +WindowCreationPayload::WindowCreationPayload(const std::string& workspace_name, WindowAddress window_address, WindowRepr window_repr) : m_window(std::move(window_repr)), m_windowAddress(std::move(window_address)), @@ -28,9 +28,10 @@ WindowCreationPayload::WindowCreationPayload(std::string workspace_name, clearWorkspaceName(); } -WindowCreationPayload::WindowCreationPayload(std::string workspace_name, - WindowAddress window_address, std::string window_class, - std::string window_title, bool is_active) +WindowCreationPayload::WindowCreationPayload(const std::string& workspace_name, + WindowAddress window_address, + const std::string& window_class, + const std::string& window_title, bool is_active) : m_window(std::make_pair(std::move(window_class), std::move(window_title))), m_windowAddress(std::move(window_address)), m_workspaceName(std::move(workspace_name)), diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 4cdd8910..ca427dc9 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -71,20 +71,20 @@ bool Workspace::handleClicked(GdkEventButton* bt) const { try { if (id() > 0) { // normal if (m_workspaceManager.moveToMonitor()) { - m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); + IPC::dispatch("focusworkspaceoncurrentmonitor", std::to_string(id())); } else { - m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id())); + IPC::dispatch("workspace", std::to_string(id())); } } else if (!isSpecial()) { // named (this includes persistent) if (m_workspaceManager.moveToMonitor()) { - m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); + IPC::dispatch("focusworkspaceoncurrentmonitor", "name:" + name()); } else { - m_ipc.getSocket1Reply("dispatch workspace name:" + name()); + IPC::dispatch("workspace", "name:" + name()); } } else if (id() != -99) { // named special - m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name()); + IPC::dispatch("togglespecialworkspace", name()); } else { // special - m_ipc.getSocket1Reply("dispatch togglespecialworkspace"); + IPC::dispatch("togglespecialworkspace", ""); } return true; } catch (const std::exception& e) { @@ -96,7 +96,7 @@ bool Workspace::handleClicked(GdkEventButton* bt) const { void Workspace::initializeWindowMap(const Json::Value& clients_data) { m_windowMap.clear(); - for (auto client : clients_data) { + for (const auto& client : clients_data) { if (client["workspace"]["id"].asInt() == id()) { insertWindow({client}); } @@ -300,16 +300,19 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { } auto window_box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); - window_box->set_tooltip_text(window_repr.window_title); - window_box->get_style_context()->add_class("taskbar-window"); + window_box->set_tooltip_markup(window_repr.window_title); + + auto button = Gtk::manage(new Gtk::Button()); + button->set_relief(Gtk::RELIEF_NONE); + button->add(*window_box); + button->get_style_context()->add_class("taskbar-window"); if (window_repr.isActive) { - window_box->get_style_context()->add_class("active"); + button->get_style_context()->add_class("active"); } - auto event_box = Gtk::manage(new Gtk::EventBox()); - event_box->add(*window_box); if (m_workspaceManager.onClickWindow() != "") { - event_box->signal_button_press_event().connect( - sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address)); + button->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), + false); } auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), @@ -334,8 +337,8 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { window_box->pack_start(*window_label_after, true, true); } - m_content.pack_start(*event_box, true, false); - event_box->show_all(); + m_content.pack_start(*button, true, false); + button->show_all(); }; if (m_workspaceManager.taskbarReverseDirection()) { diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 6fa38260..3fa92920 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -34,6 +34,9 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& } Workspaces::~Workspaces() { + if (m_scrollEventConnection_.connected()) { + m_scrollEventConnection_.disconnect(); + } m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(m_mutex); @@ -43,6 +46,18 @@ void Workspaces::init() { m_activeWorkspaceId = m_ipc.getSocket1JsonReply("activeworkspace")["id"].asInt(); initializeWorkspaces(); + + if (m_scrollEventConnection_.connected()) { + m_scrollEventConnection_.disconnect(); + } + bool hasScrollConfig = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString(); + if (barScroll() || hasScrollConfig) { + auto& window = const_cast(m_bar).window; + window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + m_scrollEventConnection_ = + window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); + } + dp.emit(); } @@ -148,7 +163,8 @@ void Workspaces::extendOrphans(int workspaceId, Json::Value const& clientsJson) } } -std::string Workspaces::getRewrite(std::string window_class, std::string window_title) { +std::string Workspaces::getRewrite(const std::string& window_class, + const std::string& window_title) { std::string windowReprKey; if (windowRewriteConfigUsesTitle()) { windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title); @@ -189,7 +205,7 @@ void Workspaces::initializeWorkspaces() { auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); auto const clientsJson = m_ipc.getSocket1JsonReply("clients"); - for (Json::Value workspaceJson : workspacesJson) { + for (const auto& workspaceJson : workspacesJson) { std::string workspaceName = workspaceJson["name"].asString(); if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && (!workspaceName.starts_with("special") || showSpecial()) && @@ -263,7 +279,7 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const& clientsJs // key is the workspace and value is array of monitors to create on for (const Json::Value& monitor : value) { if (monitor.isString() && monitor.asString() == currentMonitor) { - persistentWorkspacesToCreate.emplace_back(currentMonitor); + persistentWorkspacesToCreate.emplace_back(key); break; } } @@ -311,6 +327,10 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c // 2. the rule's monitor is the current monitor // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { + // => skip ignore-workspaces even if its a persistent + if(isWorkspaceIgnored(workspace)) { + continue; + } // => persistent workspace should be shown on this monitor auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); workspaceData["persistent-rule"] = true; @@ -324,8 +344,13 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c void Workspaces::onEvent(const std::string& ev) { std::lock_guard lock(m_mutex); - std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); - std::string payload = ev.substr(eventName.size() + 2); + const auto separator = ev.find(">>"); + if (separator == std::string::npos) { + spdlog::warn("Malformed Hyprland workspace event: {}", ev); + return; + } + std::string eventName = ev.substr(0, separator); + std::string payload = ev.substr(separator + 2); if (eventName == "workspacev2") { onWorkspaceActivated(payload); @@ -393,7 +418,7 @@ void Workspaces::onWorkspaceCreated(std::string const& payload, Json::Value cons auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules"); auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); - for (Json::Value workspaceJson : workspacesJson) { + for (auto workspaceJson : workspacesJson) { const auto currentId = workspaceJson["id"].asInt(); if (currentId == *workspaceId) { std::string workspaceName = workspaceJson["name"].asString(); @@ -488,19 +513,21 @@ void Workspaces::onMonitorFocused(std::string const& payload) { void Workspaces::onWindowOpened(std::string const& payload) { spdlog::trace("Window opened: {}", payload); updateWindowCount(); - size_t lastCommaIdx = 0; - size_t nextCommaIdx = payload.find(','); - std::string windowAddress = payload.substr(lastCommaIdx, nextCommaIdx - lastCommaIdx); + const auto firstComma = payload.find(','); + const auto secondComma = + firstComma == std::string::npos ? std::string::npos : payload.find(',', firstComma + 1); + const auto thirdComma = + secondComma == std::string::npos ? std::string::npos : payload.find(',', secondComma + 1); + if (firstComma == std::string::npos || secondComma == std::string::npos || + thirdComma == std::string::npos) { + spdlog::warn("Malformed Hyprland openwindow payload: {}", payload); + return; + } - lastCommaIdx = nextCommaIdx; - nextCommaIdx = payload.find(',', nextCommaIdx + 1); - std::string workspaceName = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1); - - lastCommaIdx = nextCommaIdx; - nextCommaIdx = payload.find(',', nextCommaIdx + 1); - std::string windowClass = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1); - - std::string windowTitle = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx); + std::string windowAddress = payload.substr(0, firstComma); + std::string workspaceName = payload.substr(firstComma + 1, secondComma - firstComma - 1); + std::string windowClass = payload.substr(secondComma + 1, thirdComma - secondComma - 1); + std::string windowTitle = payload.substr(thirdComma + 1); bool isActive = m_currentActiveWindowAddress == windowAddress; m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle, isActive); @@ -636,6 +663,7 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { populateBoolConfig(config, "persistent-only", m_persistentOnly); populateBoolConfig(config, "active-only", m_activeOnly); populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); + populateBoolConfig(config, "enable-bar-scroll", m_barScroll); m_persistentWorkspaceConfig = config.get("persistent-workspaces", Json::Value()); populateSortByConfig(config); @@ -993,10 +1021,12 @@ void Workspaces::sortWorkspaces() { void Workspaces::setUrgentWorkspace(std::string const& windowaddress) { const Json::Value clientsJson = m_ipc.getSocket1JsonReply("clients"); + const std::string normalizedAddress = + windowaddress.starts_with("0x") ? windowaddress : fmt::format("0x{}", windowaddress); int workspaceId = -1; - for (Json::Value clientJson : clientsJson) { - if (clientJson["address"].asString().ends_with(windowaddress)) { + for (const auto& clientJson : clientsJson) { + if (clientJson["address"].asString() == normalizedAddress) { workspaceId = clientJson["workspace"]["id"].asInt(); break; } @@ -1125,7 +1155,11 @@ std::string Workspaces::makePayload(Args const&... args) { } std::pair Workspaces::splitDoublePayload(std::string const& payload) { - const std::string part1 = payload.substr(0, payload.find(',')); + const auto separator = payload.find(','); + if (separator == std::string::npos) { + throw std::invalid_argument("Expected a two-part Hyprland payload"); + } + const std::string part1 = payload.substr(0, separator); const std::string part2 = payload.substr(part1.size() + 1); return {part1, part2}; } @@ -1134,6 +1168,9 @@ std::tuple Workspaces::splitTriplePayload std::string const& payload) { const size_t firstComma = payload.find(','); const size_t secondComma = payload.find(',', firstComma + 1); + if (firstComma == std::string::npos || secondComma == std::string::npos) { + throw std::invalid_argument("Expected a three-part Hyprland payload"); + } const std::string part1 = payload.substr(0, firstComma); const std::string part2 = payload.substr(firstComma + 1, secondComma - (firstComma + 1)); @@ -1151,4 +1188,37 @@ std::optional Workspaces::parseWorkspaceId(std::string const& workspaceIdSt } } +bool Workspaces::handleScroll(GdkEventScroll* e) { + // Ignore emulated scroll events on window + if (gdk_event_get_pointer_emulated((GdkEvent*)e)) { + return false; + } + + // Check for custom scroll commands first; delegate to base class + if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { + return AModule::handleScroll(e); + } + + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + + if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) { + if (allOutputs()) { + IPC::dispatch("workspace", "e+1"); + } else { + IPC::dispatch("workspace", "m+1"); + } + } else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) { + if (allOutputs()) { + IPC::dispatch("workspace", "e-1"); + } else { + IPC::dispatch("workspace", "m-1"); + } + } + + return true; +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 189deee6..98bf3c46 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -14,22 +14,27 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config) size_ = config["size"].asInt(); - interval_ = config_["interval"] == "once" - ? std::chrono::milliseconds::max() - : std::chrono::milliseconds(std::max( - 1L, // Minimum 1ms due to millisecond precision - static_cast( - (config_["interval"].isNumeric() ? config_["interval"].asDouble() : 0) * - 1000))); + const auto once = std::chrono::milliseconds::max(); + if (!config_.isMember("interval") || config_["interval"].isNull() || + config_["interval"] == "once") { + interval_ = once; + } else if (config_["interval"].isNumeric()) { + const auto interval_seconds = config_["interval"].asDouble(); + if (interval_seconds <= 0) { + interval_ = once; + } else { + interval_ = + std::chrono::milliseconds(std::max(1L, // Minimum 1ms due to millisecond precision + static_cast(interval_seconds * 1000))); + } + } else { + interval_ = once; + } if (size_ == 0) { size_ = 16; } - if (interval_.count() == 0) { - interval_ = std::chrono::milliseconds::max(); - } - delayWorker(); } diff --git a/src/modules/inhibitor.cpp b/src/modules/inhibitor.cpp index fe2a4be4..43c1f83e 100644 --- a/src/modules/inhibitor.cpp +++ b/src/modules/inhibitor.cpp @@ -85,7 +85,8 @@ auto getInhibitors(const Json::Value& config) -> std::string { if (config["what"].isArray()) { inhibitors = checkInhibitor(config["what"][0].asString()); for (decltype(config["what"].size()) i = 1; i < config["what"].size(); ++i) { - inhibitors += ":" + checkInhibitor(config["what"][i].asString()); + inhibitors.append(":"); + inhibitors.append(checkInhibitor(config["what"][i].asString())); } return inhibitors; } @@ -123,7 +124,7 @@ auto Inhibitor::update() -> void { label_.get_style_context()->add_class(status_text); if (tooltipEnabled()) { - label_.set_tooltip_text(status_text); + label_.set_tooltip_markup(status_text); } return ALabel::update(); diff --git a/src/modules/jack.cpp b/src/modules/jack.cpp index 678f986b..578fb4e0 100644 --- a/src/modules/jack.cpp +++ b/src/modules/jack.cpp @@ -53,7 +53,7 @@ std::string JACK::JACKState() { auto JACK::update() -> void { std::string format; std::string state = JACKState(); - float latency = 1000 * (float)bufsize_ / (float)samplerate_; + float latency = samplerate_ > 0 ? 1000.0f * (float)bufsize_ / (float)samplerate_ : 0.0f; if (label_.get_style_context()->has_class("xrun")) { label_.get_style_context()->remove_class("xrun"); @@ -80,7 +80,7 @@ auto JACK::update() -> void { if (tooltipEnabled()) { std::string tooltip_format = "{bufsize}/{samplerate} {latency}ms"; if (config_["tooltip-format"].isString()) tooltip_format = config_["tooltip-format"].asString(); - label_.set_tooltip_text(fmt::format( + label_.set_tooltip_markup(fmt::format( fmt::runtime(tooltip_format), fmt::arg("load", std::round(load_)), fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_), fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_))); @@ -91,16 +91,19 @@ auto JACK::update() -> void { } int JACK::bufSize(jack_nframes_t size) { + std::lock_guard lock(mutex_); bufsize_ = size; return 0; } int JACK::sampleRate(jack_nframes_t rate) { + std::lock_guard lock(mutex_); samplerate_ = rate; return 0; } int JACK::xrun() { + std::lock_guard lock(mutex_); xruns_ += 1; state_ = "xrun"; return 0; diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 18ce0a7c..a2207fdd 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -232,9 +232,12 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& } tryAddDevice(dev_path); } else if (event->mask & IN_DELETE) { + std::lock_guard lock(devices_mutex_); auto it = libinput_devices_.find(dev_path); if (it != libinput_devices_.end()) { spdlog::info("Keyboard {} has been removed.", dev_path); + libinput_path_remove_device(it->second); + libinput_device_unref(it->second); libinput_devices_.erase(it); } } @@ -245,6 +248,7 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& } waybar::modules::KeyboardState::~KeyboardState() { + std::lock_guard lock(devices_mutex_); for (const auto& [_, dev_ptr] : libinput_devices_) { libinput_path_remove_device(dev_ptr); } @@ -256,11 +260,17 @@ auto waybar::modules::KeyboardState::update() -> void { try { std::string dev_path; - if (config_["device-path"].isString() && - libinput_devices_.find(config_["device-path"].asString()) != libinput_devices_.end()) { - dev_path = config_["device-path"].asString(); - } else { - dev_path = libinput_devices_.begin()->first; + { + std::lock_guard lock(devices_mutex_); + if (libinput_devices_.empty()) { + return; + } + if (config_["device-path"].isString() && + libinput_devices_.find(config_["device-path"].asString()) != libinput_devices_.end()) { + dev_path = config_["device-path"].asString(); + } else { + dev_path = libinput_devices_.begin()->first; + } } int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); auto dev = openDevice(fd); @@ -308,10 +318,15 @@ auto waybar::modules ::KeyboardState::tryAddDevice(const std::string& dev_path) auto dev = openDevice(fd); if (supportsLockStates(dev)) { spdlog::info("Found device {} at '{}'", libevdev_get_name(dev), dev_path); + std::lock_guard lock(devices_mutex_); if (libinput_devices_.find(dev_path) == libinput_devices_.end()) { auto device = libinput_path_add_device(libinput_, dev_path.c_str()); - libinput_device_ref(device); - libinput_devices_[dev_path] = device; + if (device) { + libinput_device_ref(device); + libinput_devices_[dev_path] = device; + } else { + spdlog::warn("keyboard-state: Failed to add device to libinput: {}", dev_path); + } } } libevdev_free(dev); diff --git a/src/modules/load.cpp b/src/modules/load.cpp index 69a37b4e..72627405 100644 --- a/src/modules/load.cpp +++ b/src/modules/load.cpp @@ -22,7 +22,7 @@ auto waybar::modules::Load::update() -> void { auto [load1, load5, load15] = Load::getLoad(); if (tooltipEnabled()) { auto tooltip = fmt::format("Load 1: {}\nLoad 5: {}\nLoad 15: {}", load1, load5, load15); - label_.set_tooltip_text(tooltip); + label_.set_tooltip_markup(tooltip); } auto format = format_; auto state = getState(load1); diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index a83f6526..d41b4fc2 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -1,11 +1,32 @@ #include "modules/memory.hpp" +namespace { +const std::unordered_map kUnits = { + {"kB", 1.000}, + {"kiB", 1.024}, + {"MB", 1.000 * 1000.0}, + {"MiB", 1.024 * 1024.0}, + {"GB", 1.000 * 1000.0 * 1000.0}, + {"GiB", 1.024 * 1024.0 * 1024.0}, + {"TB", 1.000 * 1000.0 * 1000.0 * 1000.0}, + {"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0} +}; +} + waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) : ALabel(config, "memory", id, "{}%", 30) { thread_ = [this] { dp.emit(); thread_.sleep_for(interval_); }; + if (config["unit"].isString()) { + unit_ = config["unit"].asString(); + if (!kUnits.contains(unit_)) { + unit_ = "GiB"; + } + } else { + unit_ = "GiB"; + } } auto waybar::modules::Memory::update() -> void { @@ -13,15 +34,15 @@ auto waybar::modules::Memory::update() -> void { unsigned long memtotal = meminfo_["MemTotal"]; unsigned long swaptotal = 0; - if (meminfo_.count("SwapTotal")) { + if (meminfo_.contains("SwapTotal")) { swaptotal = meminfo_["SwapTotal"]; } unsigned long memfree; unsigned long swapfree = 0; - if (meminfo_.count("SwapFree")) { + if (meminfo_.contains("SwapFree")) { swapfree = meminfo_["SwapFree"]; } - if (meminfo_.count("MemAvailable")) { + if (meminfo_.contains("MemAvailable")) { // New kernels (3.4+) have an accurate available memory field. memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"]; } else { @@ -31,18 +52,19 @@ auto waybar::modules::Memory::update() -> void { } if (memtotal > 0 && memfree >= 0) { - float total_ram_gigabytes = - 0.01 * round(memtotal / 10485.76); // 100*10485.76 = 2^20 = 1024^2 = GiB/KiB - float total_swap_gigabytes = 0.01 * round(swaptotal / 10485.76); int used_ram_percentage = 100 * (memtotal - memfree) / memtotal; int used_swap_percentage = 0; - if (swaptotal) { + if ((bool) swaptotal) { used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; } - float used_ram_gigabytes = 0.01 * round((memtotal - memfree) / 10485.76); - float used_swap_gigabytes = 0.01 * round((swaptotal - swapfree) / 10485.76); - float available_ram_gigabytes = 0.01 * round(memfree / 10485.76); - float available_swap_gigabytes = 0.01 * round(swapfree / 10485.76); + + float divisor = kUnits.at(unit_); + float total_ram = memtotal / divisor; + float total_swap = swaptotal / divisor; + float used_ram = (memtotal - memfree) / divisor; + float used_swap = (swaptotal - swapfree) / divisor; + float available_ram = memfree / divisor; + float available_swap = swapfree / divisor; auto format = format_; auto state = getState(used_ram_percentage); @@ -58,27 +80,27 @@ auto waybar::modules::Memory::update() -> void { label_.set_markup(fmt::format( fmt::runtime(format), used_ram_percentage, fmt::arg("icon", getIcon(used_ram_percentage, icons)), - fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), + fmt::arg("total", total_ram), fmt::arg("swapTotal", total_swap), fmt::arg("percentage", used_ram_percentage), fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), - fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), - fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), - fmt::arg("swapAvail", available_swap_gigabytes))); + fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram), + fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), + fmt::arg("swapAvail", available_swap))); } if (tooltipEnabled()) { if (config_["tooltip-format"].isString()) { auto tooltip_format = config_["tooltip-format"].asString(); - label_.set_tooltip_text(fmt::format( + label_.set_tooltip_markup(fmt::format( fmt::runtime(tooltip_format), used_ram_percentage, - fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), + fmt::arg("total", total_ram), fmt::arg("swapTotal", total_swap), fmt::arg("percentage", used_ram_percentage), fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), - fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), - fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), - fmt::arg("swapAvail", available_swap_gigabytes))); + fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram), + fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), + fmt::arg("swapAvail", available_swap))); } else { - label_.set_tooltip_text(fmt::format("{:.{}f}GiB used", used_ram_gigabytes, 1)); + label_.set_tooltip_markup(fmt::format("{:.{}f}{} used", used_ram, 1, unit_)); } } } else { diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 8e6bbb25..00179ae9 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -6,6 +6,7 @@ #include #include +#include using namespace waybar::util; #include "modules/mpd/state.hpp" @@ -24,7 +25,8 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000), connection_(nullptr, &mpd_connection_free), status_(nullptr, &mpd_status_free), - song_(nullptr, &mpd_song_free) { + song_(nullptr, &mpd_song_free), + ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "\u2026") { if (!config_["port"].isNull() && !config_["port"].isUInt()) { spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_); } @@ -51,21 +53,6 @@ auto waybar::modules::MPD::update() -> void { ALabel::update(); } -void waybar::modules::MPD::queryMPD() { - if (connection_ != nullptr) { - spdlog::trace("{}: fetching state information", module_name_); - try { - fetchState(); - spdlog::trace("{}: fetch complete", module_name_); - } catch (std::exception const& e) { - spdlog::error("{}: {}", module_name_, e.what()); - state_ = MPD_STATE_UNKNOWN; - } - - dp.emit(); - } -} - std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const { std::string result = config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A"; @@ -110,22 +97,22 @@ void waybar::modules::MPD::setLabel() { ? config_["tooltip-format-disconnected"].asString() : "MPD (disconnected)"; // Nothing to format - label_.set_tooltip_text(tooltip_format); + label_.set_tooltip_markup(tooltip_format); } return; } label_.get_style_context()->remove_class("disconnected"); auto format = format_; - Glib::ustring artist, album_artist, album, title; std::string date, filename, uri; int song_pos = 0, queue_length = 0, volume = 0; std::chrono::seconds elapsedTime, totalTime; std::string stateIcon = ""; bool no_song = song_.get() == nullptr; - if (stopped() || no_song) { - if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped."); + bool is_stopped = stopped(); + if (is_stopped || no_song) { + if (no_song && !is_stopped) spdlog::warn("mpd: no current song while state is not stopped"); format = config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped"; label_.get_style_context()->add_class("stopped"); @@ -145,10 +132,6 @@ void waybar::modules::MPD::setLabel() { stateIcon = getStateIcon(); - artist = sanitize_string(getTag(MPD_TAG_ARTIST)); - album_artist = sanitize_string(getTag(MPD_TAG_ALBUM_ARTIST)); - album = sanitize_string(getTag(MPD_TAG_ALBUM)); - title = sanitize_string(getTag(MPD_TAG_TITLE)); date = sanitize_string(getTag(MPD_TAG_DATE)); filename = sanitize_string(getFilename()); uri = mpd_song_get_uri(song_.get()); @@ -170,17 +153,12 @@ void waybar::modules::MPD::setLabel() { std::string repeatIcon = getOptionIcon("repeat", repeatActivated); bool singleActivated = mpd_status_get_single(status_.get()); std::string singleIcon = getOptionIcon("single", singleActivated); - if (config_["artist-len"].isInt()) artist = artist.substr(0, config_["artist-len"].asInt()); - if (config_["album-artist-len"].isInt()) - album_artist = album_artist.substr(0, config_["album-artist-len"].asInt()); - if (config_["album-len"].isInt()) album = album.substr(0, config_["album-len"].asInt()); - if (config_["title-len"].isInt()) title = title.substr(0, config_["title-len"].asInt()); try { auto text = fmt::format( - fmt::runtime(format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::runtime(format), fmt::arg("artist", getArtistStr(true)), + fmt::arg("albumArtist", getAlbumArtistStr(true)), fmt::arg("album", getAlbumStr(true)), + fmt::arg("title", getTitleStr(true)), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), @@ -202,15 +180,15 @@ void waybar::modules::MPD::setLabel() { : "MPD (connected)"; try { auto tooltip_text = fmt::format( - fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::runtime(tooltip_format), fmt::arg("artist", getArtistStr(false)), + fmt::arg("albumArtist", getAlbumArtistStr(false)), fmt::arg("album", getAlbumStr(false)), + fmt::arg("title", getTitleStr(false)), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon), fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri)); - label_.set_tooltip_text(tooltip_text); + label_.set_tooltip_markup(tooltip_text); } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error (tooltip): {}", e.what()); } @@ -239,7 +217,8 @@ std::string waybar::modules::MPD::getStateIcon() const { } } -std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool activated) const { +std::string waybar::modules::MPD::getOptionIcon(const std::string& optionName, + bool activated) const { if (!config_[optionName + "-icons"].isObject()) { return ""; } @@ -256,6 +235,38 @@ std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool act } } +std::string waybar::modules::MPD::getArtistStr(bool truncated) const { + std::string artist = getTag(MPD_TAG_ARTIST); + if (truncated && config_["artist-len"].isInt()) { + waybar::util::utf8_truncate(artist, ellipsis_, config_["artist-len"].asInt()); + } + return sanitize_string(artist); +} + +std::string waybar::modules::MPD::getAlbumArtistStr(bool truncated) const { + std::string album_artist = getTag(MPD_TAG_ALBUM_ARTIST); + if (truncated && config_["album-artist-len"].isInt()) { + waybar::util::utf8_truncate(album_artist, ellipsis_, config_["album-artist-len"].asInt()); + } + return sanitize_string(album_artist); +} + +std::string waybar::modules::MPD::getAlbumStr(bool truncated) const { + std::string album = getTag(MPD_TAG_ALBUM); + if (truncated && config_["album-len"].isInt()) { + waybar::util::utf8_truncate(album, ellipsis_, config_["album-len"].asInt()); + } + return sanitize_string(album); +} + +std::string waybar::modules::MPD::getTitleStr(bool truncated) const { + std::string title = getTag(MPD_TAG_TITLE); + if (truncated && config_["title-len"].isInt()) { + waybar::util::utf8_truncate(title, ellipsis_, config_["title-len"].asInt()); + } + return sanitize_string(title); +} + static bool isServerUnavailable(const std::error_code& ec) { if (ec.category() == std::system_category()) { switch (ec.value()) { @@ -323,6 +334,7 @@ void waybar::modules::MPD::checkErrors(mpd_connection* conn) { case MPD_ERROR_SYSTEM: if (auto ec = mpd_connection_get_system_error(conn); ec != 0) { mpd_connection_clear_error(conn); + connection_.reset(); throw std::system_error(ec, std::system_category()); } G_GNUC_FALLTHROUGH; diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 3d7c8561..eb95665f 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -152,7 +152,6 @@ bool Playing::on_timer() { return false; } - ctx_->queryMPD(); ctx_->emit(); } catch (std::exception const& e) { spdlog::warn("mpd: Playing: error: {}", e.what()); diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 2b345fc5..13ba836d 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -7,6 +7,7 @@ #include #include "util/scope_guard.hpp" +#include "util/utf8_string.hpp" extern "C" { #include @@ -109,6 +110,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) player_ = config_["player"].asString(); } if (config_["ignored-players"].isArray()) { + ignored_players_.reserve(config_["ignored-players"].size()); for (const auto& item : config_["ignored-players"]) { if (item.isString()) { ignored_players_.push_back(item.asString()); @@ -117,7 +119,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error) { g_error_free(error); } @@ -161,8 +163,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) if (player) { g_object_connect(player, "signal::play", G_CALLBACK(onPlayerPlay), this, "signal::pause", G_CALLBACK(onPlayerPause), this, "signal::stop", G_CALLBACK(onPlayerStop), - this, "signal::stop", G_CALLBACK(onPlayerStop), this, "signal::metadata", - G_CALLBACK(onPlayerMetadata), this, NULL); + this, "signal::metadata", G_CALLBACK(onPlayerMetadata), this, NULL); } // allow setting an interval count that triggers periodic refreshes @@ -178,8 +179,17 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } Mpris::~Mpris() { - if (manager != nullptr) g_object_unref(manager); - if (player != nullptr) g_object_unref(player); + if (manager != nullptr) { + g_signal_handlers_disconnect_by_data(manager, this); + } + if (player != nullptr) { + g_signal_handlers_disconnect_by_data(player, this); + } + if (last_active_player_ != nullptr && last_active_player_ != player) { + g_object_unref(last_active_player_); + } + g_clear_object(&manager); + g_clear_object(&player); } auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) -> std::string { @@ -193,69 +203,22 @@ auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) -> // Wide characters count as two, zero-width characters count as zero // Modifies str in-place (unless width = std::string::npos) // Returns the total width of the string pre-truncating -size_t utf8_truncate(std::string& str, size_t width = std::string::npos) { - if (str.length() == 0) return 0; - - const gchar* trunc_end = nullptr; - - size_t total_width = 0; - - for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) { - gunichar c = g_utf8_get_char_validated(data, end - data); - if (c == -1U || c == -2U) { - // invalid unicode, treat string as ascii - if (width != std::string::npos && str.length() > width) str.resize(width); - return str.length(); - } else if (g_unichar_iswide(c)) { - total_width += 2; - } else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen - total_width += 1; - } - - data = g_utf8_find_next_char(data, end); - if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c)) - trunc_end = data; - } - - if (trunc_end) str.resize(trunc_end - str.data()); - - return total_width; -} - -size_t utf8_width(const std::string& str) { return utf8_truncate(const_cast(str)); } - -void truncate(std::string& s, const std::string& ellipsis, size_t max_len) { - if (max_len == 0) { - s.resize(0); - return; - } - size_t len = utf8_truncate(s, max_len); - if (len > max_len) { - size_t ellipsis_len = utf8_width(ellipsis); - if (max_len >= ellipsis_len) { - if (ellipsis_len) utf8_truncate(s, max_len - ellipsis_len); - s += ellipsis; - } else { - s.resize(0); - } - } -} auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string { auto artist = info.artist.value_or(std::string()); - if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_); + if (truncated && artist_len_ >= 0) waybar::util::utf8_truncate(artist, ellipsis_, artist_len_); return artist; } auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string { auto album = info.album.value_or(std::string()); - if (truncated && album_len_ >= 0) truncate(album, ellipsis_, album_len_); + if (truncated && album_len_ >= 0) waybar::util::utf8_truncate(album, ellipsis_, album_len_); return album; } auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string { auto title = info.title.value_or(std::string()); - if (truncated && title_len_ >= 0) truncate(title, ellipsis_, title_len_); + if (truncated && title_len_ >= 0) waybar::util::utf8_truncate(title, ellipsis_, title_len_); return title; } @@ -283,9 +246,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> // keep position format same as length format auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6); - size_t artistLen = utf8_width(artist); - size_t albumLen = utf8_width(album); - size_t titleLen = utf8_width(title); + size_t artistLen = waybar::util::utf8_width(artist); + size_t albumLen = waybar::util::utf8_width(album); + size_t titleLen = waybar::util::utf8_width(title); size_t lengthLen = length.length(); size_t posLen = position.length(); @@ -304,7 +267,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> // Since the first element doesn't present a separator and we don't know a priori which one // it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the // separatorLen to all the other lengths. - size_t separatorLen = utf8_width(dynamic_separator_); + size_t separatorLen = waybar::util::utf8_width(dynamic_separator_); size_t dynamicLen = dynamic_len_ + separatorLen; if (showArtist) artistLen += separatorLen; if (showAlbum) albumLen += separatorLen; @@ -410,11 +373,14 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye return; } + if (mpris->player != nullptr) { + g_signal_handlers_disconnect_by_data(mpris->player, mpris); + g_clear_object(&mpris->player); + } mpris->player = playerctl_player_new_from_name(player_name, nullptr); g_object_connect(mpris->player, "signal::play", G_CALLBACK(onPlayerPlay), mpris, "signal::pause", G_CALLBACK(onPlayerPause), mpris, "signal::stop", G_CALLBACK(onPlayerStop), - mpris, "signal::stop", G_CALLBACK(onPlayerStop), mpris, "signal::metadata", - G_CALLBACK(onPlayerMetadata), mpris, NULL); + mpris, "signal::metadata", G_CALLBACK(onPlayerMetadata), mpris, NULL); mpris->dp.emit(); } @@ -458,10 +424,7 @@ auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void { if (!mpris) return; spdlog::debug("mpris: player-stop callback"); - - // hide widget - mpris->event_box_.set_visible(false); - // update widget + // update widget (update() handles visibility) mpris->dp.emit(); } @@ -480,7 +443,7 @@ auto Mpris::getPlayerInfo() -> std::optional { } GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error) { g_error_free(error); } @@ -488,7 +451,12 @@ auto Mpris::getPlayerInfo() -> std::optional { char* player_status = nullptr; auto player_playback_status = PLAYERCTL_PLAYBACK_STATUS_STOPPED; - g_object_get(player, "status", &player_status, "playback-status", &player_playback_status, NULL); + + // Clean up previous fallback player + if (last_active_player_ && last_active_player_ != player) { + g_object_unref(last_active_player_); + last_active_player_ = nullptr; + } std::string player_name = player_; if (player_name == "playerctld") { @@ -498,19 +466,52 @@ auto Mpris::getPlayerInfo() -> std::optional { } // > get the list of players [..] in order of activity // https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249 - players = g_list_first(players); - if (players) - player_name = static_cast(players->data)->name; - else - return std::nullopt; // no players found, hide the widget - } - - if (std::any_of(ignored_players_.begin(), ignored_players_.end(), - [&](const std::string& pn) { return player_name == pn; })) { + PlayerctlPlayer* first_valid_player = nullptr; + std::string first_valid_name; + for (auto* p = g_list_first(players); p != nullptr; p = p->next) { + auto* pn = static_cast(p->data); + std::string name = pn->name; + if (std::any_of(ignored_players_.begin(), ignored_players_.end(), + [&](const std::string& ignored) { return name == ignored; })) { + spdlog::warn("mpris[{}]: ignoring player update", name); + continue; + } + auto* tmp = playerctl_player_new_from_name(pn, &error); + if (error || !tmp) continue; + if (!first_valid_player) { + first_valid_player = tmp; + first_valid_name = name; + } + PlayerctlPlaybackStatus status; + g_object_get(tmp, "playback-status", &status, NULL); + if (status == PLAYERCTL_PLAYBACK_STATUS_PLAYING) { + if (tmp != first_valid_player) g_object_unref(first_valid_player); + last_active_player_ = tmp; + player_name = name; + break; + } + if (tmp != first_valid_player) g_object_unref(tmp); + } + if (!last_active_player_) { + if (!first_valid_player) return std::nullopt; + last_active_player_ = first_valid_player; + player_name = first_valid_name; + } + } else if (std::any_of(ignored_players_.begin(), ignored_players_.end(), + [&](const std::string& pn) { return player_name == pn; })) { spdlog::warn("mpris[{}]: ignoring player update", player_name); return std::nullopt; + } else { + last_active_player_ = player; } + g_object_get(last_active_player_, "status", &player_status, "playback-status", + &player_playback_status, NULL); + + if (!player_status) { + spdlog::error("mpris: failed to get player status"); + return std::nullopt; + } // make status lowercase player_status[0] = std::tolower(player_status[0]); @@ -524,28 +525,29 @@ auto Mpris::getPlayerInfo() -> std::optional { .length = std::nullopt, }; - if (auto* artist_ = playerctl_player_get_artist(player, &error)) { + if (auto* artist_ = playerctl_player_get_artist(last_active_player_, &error)) { spdlog::debug("mpris[{}]: artist = {}", info.name, artist_); info.artist = artist_; g_free(artist_); } if (error) goto errorexit; - if (auto* album_ = playerctl_player_get_album(player, &error)) { + if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); info.album = album_; g_free(album_); } if (error) goto errorexit; - if (auto* title_ = playerctl_player_get_title(player, &error)) { + if (auto* title_ = playerctl_player_get_title(last_active_player_, &error)) { spdlog::debug("mpris[{}]: title = {}", info.name, title_); info.title = title_; g_free(title_); } if (error) goto errorexit; - if (auto* length_ = playerctl_player_print_metadata_prop(player, "mpris:length", &error)) { + if (auto* length_ = + playerctl_player_print_metadata_prop(last_active_player_, "mpris:length", &error)) { spdlog::debug("mpris[{}]: mpris:length = {}", info.name, length_); auto len = std::chrono::microseconds(std::strtol(length_, nullptr, 10)); auto len_h = std::chrono::duration_cast(len); @@ -557,7 +559,7 @@ auto Mpris::getPlayerInfo() -> std::optional { if (error) goto errorexit; { - auto position_ = playerctl_player_get_position(player, &error); + auto position_ = playerctl_player_get_position(last_active_player_, &error); if (error) { // it's fine to have an error here because not all players report a position g_error_free(error); @@ -609,12 +611,13 @@ bool Mpris::handleToggle(GdkEventButton* const& e) { }); // Command pattern: encapsulate each button's action + auto* target = last_active_player_ ? last_active_player_ : player; const ButtonAction actions[] = { - {1, "on-click", [&]() { playerctl_player_play_pause(player, &error); }}, - {2, "on-click-middle", [&]() { playerctl_player_previous(player, &error); }}, - {3, "on-click-right", [&]() { playerctl_player_next(player, &error); }}, - {8, "on-click-backward", [&]() { playerctl_player_previous(player, &error); }}, - {9, "on-click-forward", [&]() { playerctl_player_next(player, &error); }}, + {1, "on-click", [&]() { playerctl_player_play_pause(target, &error); }}, + {2, "on-click-middle", [&]() { playerctl_player_previous(target, &error); }}, + {3, "on-click-right", [&]() { playerctl_player_next(target, &error); }}, + {8, "on-click-backward", [&]() { playerctl_player_previous(target, &error); }}, + {9, "on-click-forward", [&]() { playerctl_player_next(target, &error); }}, }; for (const auto& action : actions) { @@ -726,15 +729,18 @@ auto Mpris::update() -> void { auto tooltip_text = fmt::format( fmt::runtime(tooltipstr), fmt::arg("player", info.name), fmt::arg("status", info.status_string), - fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)), - fmt::arg("title", getTitleStr(info, tooltip_len_limits_)), - fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)), + fmt::arg("artist", + std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))), + fmt::arg("title", + std::string(Glib::Markup::escape_text(getTitleStr(info, tooltip_len_limits_)))), + fmt::arg("album", + std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))), fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition), fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)), fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string))); - label_.set_tooltip_text(tooltip_text); + label_.set_tooltip_markup(tooltip_text); } catch (fmt::format_error const& e) { spdlog::warn("mpris: format error (tooltip): {}", e.what()); } diff --git a/src/modules/network.cpp b/src/modules/network.cpp index c33e750d..f43893c5 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -45,7 +45,8 @@ waybar::modules::Network::readBandwidthUsage() { std::istringstream iss(line); std::string ifacename; - iss >> ifacename; // ifacename contains "eth0:" + iss >> ifacename; // ifacename contains "eth0:" + if (ifacename.empty()) continue; ifacename.pop_back(); // remove trailing ':' if (ifacename != ifname_) { continue; @@ -104,6 +105,7 @@ waybar::modules::Network::Network(const std::string& id, const Json::Value& conf bandwidth_down_total_ = 0; bandwidth_up_total_ = 0; } + bandwidth_last_sample_time_ = std::chrono::steady_clock::now(); if (!config_["interface"].isString()) { // "interface" isn't configured, then try to guess the external @@ -276,19 +278,41 @@ const std::string waybar::modules::Network::getNetworkState() const { auto waybar::modules::Network::update() -> void { std::lock_guard lock(mutex_); std::string tooltip_format; + auto now = std::chrono::steady_clock::now(); + auto elapsed_seconds = std::chrono::duration(now - bandwidth_last_sample_time_).count(); - auto bandwidth = readBandwidthUsage(); auto bandwidth_down = 0ull; auto bandwidth_up = 0ull; - if (bandwidth.has_value()) { - auto down_octets = (*bandwidth).first; - auto up_octets = (*bandwidth).second; - bandwidth_down = down_octets - bandwidth_down_total_; - bandwidth_down_total_ = down_octets; + // Only recalculate bandwidth when enough time has elapsed since the last + // sample. Event-driven dp.emit() calls (link/addr/route changes) can + // trigger update() between timer intervals, which would consume the byte + // delta prematurely and show near-zero bandwidth. + auto min_elapsed = std::chrono::duration(interval_).count() * 0.5; + if (elapsed_seconds >= min_elapsed) { + if (elapsed_seconds <= 0.0) { + elapsed_seconds = std::chrono::duration(interval_).count(); + } + bandwidth_last_sample_time_ = now; - bandwidth_up = up_octets - bandwidth_up_total_; - bandwidth_up_total_ = up_octets; + auto bandwidth = readBandwidthUsage(); + if (bandwidth.has_value()) { + auto down_octets = (*bandwidth).first; + auto up_octets = (*bandwidth).second; + + bandwidth_down = down_octets - bandwidth_down_total_; + bandwidth_down_total_ = down_octets; + + bandwidth_up = up_octets - bandwidth_up_total_; + bandwidth_up_total_ = up_octets; + + bandwidth_down_prev_ = bandwidth_down; + bandwidth_up_prev_ = bandwidth_up; + } + } else { + bandwidth_down = bandwidth_down_prev_; + bandwidth_up = bandwidth_up_prev_; + elapsed_seconds = std::chrono::duration(interval_).count(); } if (!alt_) { @@ -320,6 +344,7 @@ auto waybar::modules::Network::update() -> void { } else if (addr_pref_ == ip_addr_pref::IPV6) { final_ipaddr_ = ipaddr6_; } else if (addr_pref_ == ip_addr_pref::IPV4_6) { + final_ipaddr_.reserve(ipaddr_.length() + ipaddr6_.length() + 1); final_ipaddr_ = ipaddr_; final_ipaddr_ += '\n'; final_ipaddr_ += ipaddr6_; @@ -333,23 +358,18 @@ auto waybar::modules::Network::update() -> void { fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), - fmt::arg("bandwidthDownBits", - pow_format(bandwidth_down * 8ull / (interval_.count() / 1000.0), "b/s")), - fmt::arg("bandwidthUpBits", - pow_format(bandwidth_up * 8ull / (interval_.count() / 1000.0), "b/s")), - fmt::arg( - "bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / (interval_.count() / 1000.0), "b/s")), - fmt::arg("bandwidthDownOctets", - pow_format(bandwidth_down / (interval_.count() / 1000.0), "o/s")), - fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / (interval_.count() / 1000.0), "o/s")), + fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthTotalBits", + pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")), + fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")), fmt::arg("bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "o/s")), - fmt::arg("bandwidthDownBytes", - pow_format(bandwidth_down / (interval_.count() / 1000.0), "B/s")), - fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / (interval_.count() / 1000.0), "B/s")), + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), + fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), + fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "B/s"))); + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (text.compare(label_.get_label()) != 0) { label_.set_markup(text); if (text.empty()) { @@ -371,19 +391,18 @@ auto waybar::modules::Network::update() -> void { fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), - fmt::arg("bandwidthDownBits", - pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), - fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")), + fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), fmt::arg("bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / interval_.count(), "b/s")), - fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / interval_.count(), "o/s")), - fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / interval_.count(), "o/s")), + pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")), + fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")), fmt::arg("bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "o/s")), - fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / interval_.count(), "B/s")), - fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / interval_.count(), "B/s")), + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), + fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), + fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "B/s"))); + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (label_.get_tooltip_text() != tooltip_text) { label_.set_tooltip_markup(tooltip_text); } @@ -625,18 +644,31 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) { case IFA_LOCAL: char ipaddr[INET6_ADDRSTRLEN]; if (!is_del_event) { + bool addr_changed = false; + std::string changed_ipaddr; + int changed_cidr = 0; if ((net->addr_pref_ == ip_addr_pref::IPV4 || net->addr_pref_ == ip_addr_pref::IPV4_6) && net->cidr_ == 0 && ifa->ifa_family == AF_INET) { - net->ipaddr_ = - inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); - net->cidr_ = ifa->ifa_prefixlen; + if (inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)) != + nullptr) { + net->ipaddr_ = ipaddr; + net->cidr_ = ifa->ifa_prefixlen; + addr_changed = true; + changed_ipaddr = net->ipaddr_; + changed_cidr = net->cidr_; + } } else if ((net->addr_pref_ == ip_addr_pref::IPV6 || net->addr_pref_ == ip_addr_pref::IPV4_6) && net->cidr6_ == 0 && ifa->ifa_family == AF_INET6) { - net->ipaddr6_ = - inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); - net->cidr6_ = ifa->ifa_prefixlen; + if (inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)) != + nullptr) { + net->ipaddr6_ = ipaddr; + net->cidr6_ = ifa->ifa_prefixlen; + addr_changed = true; + changed_ipaddr = net->ipaddr6_; + changed_cidr = net->cidr6_; + } } switch (ifa->ifa_family) { @@ -656,14 +688,20 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) { net->netmask6_ = inet_ntop(ifa->ifa_family, &netmask6, ipaddr, sizeof(ipaddr)); } } - spdlog::debug("network: {}, new addr {}/{}", net->ifname_, net->ipaddr_, net->cidr_); + if (addr_changed) { + spdlog::debug("network: {}, new addr {}/{}", net->ifname_, changed_ipaddr, + changed_cidr); + } } else { - net->ipaddr_.clear(); - net->ipaddr6_.clear(); - net->cidr_ = 0; - net->cidr6_ = 0; - net->netmask_.clear(); - net->netmask6_.clear(); + if (ifa->ifa_family == AF_INET) { + net->ipaddr_.clear(); + net->cidr_ = 0; + net->netmask_.clear(); + } else if (ifa->ifa_family == AF_INET6) { + net->ipaddr6_.clear(); + net->cidr6_ = 0; + net->netmask6_.clear(); + } spdlog::debug("network: {} addr deleted {}/{}", net->ifname_, inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)), ifa->ifa_prefixlen); @@ -718,16 +756,20 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) { /* The destination address. * Should be either missing, or maybe all 0s. Accept both. */ - const uint32_t nr_zeroes = (family == AF_INET) ? 4 : 16; - unsigned char c = 0; - size_t dstlen = RTA_PAYLOAD(attr); - if (dstlen != nr_zeroes) { - break; + auto* dest = (const unsigned char*)RTA_DATA(attr); + size_t dest_size = RTA_PAYLOAD(attr); + for (size_t i = 0; i < dest_size; ++i) { + if (dest[i] != 0) { + has_destination = true; + break; + } } - for (uint32_t i = 0; i < dstlen; i += 1) { - c |= *((unsigned char*)RTA_DATA(attr) + i); + + if (rtm->rtm_dst_len != 0) { + // We have found a destination like 0.0.0.0/24, this is not a + // default gateway route. + has_destination = true; } - has_destination = (c == 0); break; } case RTA_OIF: diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 1ee1bf3f..de3f8a9a 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -17,19 +17,21 @@ #include "giomm/dataoutputstream.h" #include "giomm/unixinputstream.h" #include "giomm/unixoutputstream.h" +#include "util/scoped_fd.hpp" namespace waybar::modules::niri { +IPC::IPC() { startIPC(); } + int IPC::connectToSocket() { const char* socket_path = getenv("NIRI_SOCKET"); if (socket_path == nullptr) { - spdlog::warn("Niri is not running, niri IPC will not be available."); - return -1; + throw std::runtime_error("Niri IPC: NIRI_SOCKET was not set! (Is Niri running?)"); } struct sockaddr_un addr; - int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); + util::ScopedFd socketfd(socket(AF_UNIX, SOCK_STREAM, 0)); if (socketfd == -1) { throw std::runtime_error("socketfd failed"); @@ -44,26 +46,18 @@ int IPC::connectToSocket() { int l = sizeof(struct sockaddr_un); if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { - close(socketfd); throw std::runtime_error("unable to connect"); } - return socketfd; + return socketfd.release(); } void IPC::startIPC() { // will start IPC and relay events to parseIPC - std::thread([&]() { - int socketfd; - try { - socketfd = connectToSocket(); - } catch (std::exception& e) { - spdlog::error("Niri IPC: failed to start, reason: {}", e.what()); - return; - } - if (socketfd == -1) return; + int socketfd = connectToSocket(); + std::thread([this, socketfd]() { spdlog::info("Niri IPC starting"); auto unix_istream = Gio::UnixInputStream::create(socketfd, true); @@ -241,8 +235,7 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) { } Json::Value IPC::send(const Json::Value& request) { - int socketfd = connectToSocket(); - if (socketfd == -1) throw std::runtime_error("Niri is not running"); + util::ScopedFd socketfd(connectToSocket()); auto unix_istream = Gio::UnixInputStream::create(socketfd, true); auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false); diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp index 3359ef4c..14ca555f 100644 --- a/src/modules/niri/language.cpp +++ b/src/modules/niri/language.cpp @@ -32,6 +32,7 @@ void Language::updateFromIPC() { auto ipcLock = gIPC->lockData(); layouts_.clear(); + layouts_.reserve(gIPC->keyboardLayoutNames().size()); for (const auto& fullName : gIPC->keyboardLayoutNames()) layouts_.push_back(getLayout(fullName)); current_idx_ = gIPC->keyboardLayoutCurrent(); diff --git a/src/modules/niri/window.cpp b/src/modules/niri/window.cpp index c3537769..61b3a7dd 100644 --- a/src/modules/niri/window.cpp +++ b/src/modules/niri/window.cpp @@ -67,7 +67,7 @@ void Window::doUpdate() { updateAppIconName(appId, ""); - if (tooltipEnabled()) label_.set_tooltip_text(title); + if (tooltipEnabled()) label_.set_tooltip_markup(title); const auto id = window["id"].asUInt64(); const auto workspaceId = window["workspace_id"].asUInt64(); diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 8eb912bc..a817ab9c 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -103,7 +103,9 @@ void Workspaces::doUpdate() { fmt::arg("output", ws["output"].asString())); } if (!config_["disable-markup"].asBool()) { - static_cast(button.get_children()[0])->set_markup(name); + auto* child = gtk_bin_get_child(GTK_BIN(button.gobj())); + if (child != nullptr && GTK_IS_LABEL(child)) + gtk_label_set_markup(GTK_LABEL(child), name.c_str()); } else { button.set_label(name); } @@ -114,6 +116,11 @@ void Workspaces::doUpdate() { button.show(); else button.hide(); + } else if (config_["hide-empty"].asBool()) { + if (ws["active_window_id"].isNull() && !ws["is_focused"].asBool()) + button.hide(); + else + button.show(); } else { button.show(); } @@ -174,11 +181,11 @@ std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) if (ws["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString(); - if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString(); + if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].asString(); - if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); + if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString(); if (ws["name"]) { const auto& name = ws["name"].asString(); diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index abad763d..566787f3 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -157,7 +157,7 @@ auto PowerProfilesDaemon::update() -> void { store.push_back(fmt::arg("icon", getIcon(0, profile.name))); label_.set_markup(fmt::vformat(format_, store)); if (tooltipEnabled()) { - label_.set_tooltip_text(fmt::vformat(tooltipFormat_, store)); + label_.set_tooltip_markup(fmt::vformat(tooltipFormat_, store)); } // Set CSS class @@ -176,6 +176,10 @@ auto PowerProfilesDaemon::update() -> void { bool PowerProfilesDaemon::handleToggle(GdkEventButton* const& e) { if (e->type == GdkEventType::GDK_BUTTON_PRESS && connected_) { + if (availableProfiles_.empty()) return true; + if (activeProfile_ == availableProfiles_.end()) { + activeProfile_ = availableProfiles_.begin(); + } if (e->button == 1) /* left click */ { activeProfile_++; if (activeProfile_ == availableProfiles_.end()) { diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 904c8fd9..4e613971 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -71,6 +71,7 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat auto* item = Gtk::make_managed(module, nodeType, nodePtr, orientation, pos, iconSize, transition_duration); box_.add(*item); + modules_.push_back(item); } } @@ -148,9 +149,7 @@ auto Privacy::update() -> void { bool useAudioOut = false; mutex_.lock(); - for (Gtk::Widget* widget : box_.get_children()) { - auto* module = dynamic_cast(widget); - if (module == nullptr) continue; + for (PrivacyItem* module : modules_) { switch (module->privacy_type) { case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: setScreenshare = true; diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index f60c39ff..4d63ff3c 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -138,7 +138,7 @@ auto waybar::modules::Pulseaudio::update() -> void { fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc), fmt::arg("icon", getIcon(sink_volume, getPulseIcon())))); } else { - label_.set_tooltip_text(sink_desc); + label_.set_tooltip_markup(sink_desc); } } diff --git a/src/modules/river/layout.cpp b/src/modules/river/layout.cpp index 31358f80..3f1745fe 100644 --- a/src/modules/river/layout.cpp +++ b/src/modules/river/layout.cpp @@ -118,6 +118,7 @@ Layout::Layout(const std::string& id, const waybar::Bar& bar, const Json::Value& if (!seat_) { spdlog::error("wl_seat not advertised"); + return; } label_.hide(); diff --git a/src/modules/river/mode.cpp b/src/modules/river/mode.cpp index 10f5089e..13545030 100644 --- a/src/modules/river/mode.cpp +++ b/src/modules/river/mode.cpp @@ -77,6 +77,7 @@ Mode::Mode(const std::string& id, const waybar::Bar& bar, const Json::Value& con if (!seat_) { spdlog::error("wl_seat not advertised"); + return; } label_.hide(); diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index fc4d98f9..f06565f6 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -101,10 +101,12 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con if (!control_) { spdlog::error("river_control_v1 not advertised"); + return; } if (!seat_) { spdlog::error("wl_seat not advertised"); + return; } box_.set_name("tags"); @@ -168,6 +170,7 @@ Tags::~Tags() { } void Tags::handle_show() { + if (!status_manager_) return; struct wl_output* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output); zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this); diff --git a/src/modules/river/window.cpp b/src/modules/river/window.cpp index 1f41ab4c..2daed002 100644 --- a/src/modules/river/window.cpp +++ b/src/modules/river/window.cpp @@ -78,6 +78,7 @@ Window::Window(const std::string& id, const waybar::Bar& bar, const Json::Value& if (!seat_) { spdlog::error("wl_seat not advertised"); + return; } label_.hide(); // hide the label until populated diff --git a/src/modules/simpleclock.cpp b/src/modules/simpleclock.cpp index b6a96ecc..59a2b1c1 100644 --- a/src/modules/simpleclock.cpp +++ b/src/modules/simpleclock.cpp @@ -25,9 +25,9 @@ auto waybar::modules::Clock::update() -> void { if (config_["tooltip-format"].isString()) { auto tooltip_format = config_["tooltip-format"].asString(); auto tooltip_text = fmt::format(fmt::runtime(tooltip_format), localtime); - label_.set_tooltip_text(tooltip_text); + label_.set_tooltip_markup(tooltip_text); } else { - label_.set_tooltip_text(text); + label_.set_tooltip_markup(text); } } // Call parent update diff --git a/src/modules/sndio.cpp b/src/modules/sndio.cpp index 9779cd36..d878c4a4 100644 --- a/src/modules/sndio.cpp +++ b/src/modules/sndio.cpp @@ -102,7 +102,9 @@ Sndio::~Sndio() { sioctl_close(hdl_); } auto Sndio::update() -> void { auto format = format_; - unsigned int vol = 100. * static_cast(volume_) / static_cast(maxval_); + unsigned int vol = (maxval_ > 0) ? static_cast(100. * static_cast(volume_) / + static_cast(maxval_)) + : 0; if (volume_ == 0) { label_.get_style_context()->add_class("muted"); diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 54faa16c..567fbf9f 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -8,7 +8,8 @@ namespace waybar::modules::SNI { Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, const std::function&)>& on_add, - const std::function&)>& on_remove) + const std::function&)>& on_remove, + const std::function& on_update) : bus_name_("org.kde.StatusNotifierHost-" + std::to_string(getpid()) + "-" + std::to_string(id)), object_path_("/StatusNotifierHost/" + std::to_string(id)), @@ -17,7 +18,8 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, config_(config), bar_(bar), on_add_(on_add), - on_remove_(on_remove) {} + on_remove_(on_remove), + on_update_(on_update) {} Host::~Host() { if (bus_name_id_ > 0) { @@ -54,12 +56,12 @@ void Host::nameVanished(const Glib::RefPtr& conn, const G g_cancellable_cancel(cancellable_); g_clear_object(&cancellable_); g_clear_object(&watcher_); - items_.clear(); + clearItems(); } void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error != nullptr) { g_error_free(error); } @@ -81,7 +83,7 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error != nullptr) { g_error_free(error); } @@ -117,13 +119,50 @@ void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer d auto [bus_name, object_path] = host->getBusNameAndObjectPath(service); for (auto it = host->items_.begin(); it != host->items_.end(); ++it) { if ((*it)->bus_name == bus_name && (*it)->object_path == object_path) { - host->on_remove_(*it); - host->items_.erase(it); + host->removeItem(it); break; } } } +void Host::itemReady(Item& item) { + auto it = std::find_if(items_.begin(), items_.end(), + [&item](const auto& candidate) { return candidate.get() == &item; }); + if (it != items_.end() && (*it)->isReady()) { + on_add_(*it); + } +} + +void Host::itemInvalidated(Item& item) { + auto it = std::find_if(items_.begin(), items_.end(), + [&item](const auto& candidate) { return candidate.get() == &item; }); + if (it != items_.end()) { + removeItem(it); + } +} + +void Host::removeItem(std::vector>::iterator it) { + if ((*it)->isReady()) { + on_remove_(*it); + } + items_.erase(it); +} + +void Host::clearItems() { + bool removed_ready_item = false; + for (auto& item : items_) { + if (item->isReady()) { + on_remove_(item); + removed_ready_item = true; + } + } + bool had_items = !items_.empty(); + items_.clear(); + if (had_items && !removed_ready_item) { + on_update_(); + } +} + std::tuple Host::getBusNameAndObjectPath(const std::string service) { auto it = service.find('/'); if (it != std::string::npos) { @@ -132,15 +171,18 @@ std::tuple Host::getBusNameAndObjectPath(const std::st return {service, "/StatusNotifierItem"}; } -void Host::addRegisteredItem(std::string service) { +void Host::addRegisteredItem(const std::string& service) { std::string bus_name, object_path; std::tie(bus_name, object_path) = getBusNameAndObjectPath(service); auto it = std::find_if(items_.begin(), items_.end(), [&bus_name, &object_path](const auto& item) { return bus_name == item->bus_name && object_path == item->object_path; }); if (it == items_.end()) { - items_.emplace_back(new Item(bus_name, object_path, config_, bar_)); - on_add_(items_.back()); + items_.emplace_back(std::make_unique( + bus_name, object_path, config_, bar_, + [this](Item& item) { itemReady(item); }, + [this](Item& item) { itemInvalidated(item); }, + on_update_)); } } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index ef2543b5..8e2e2570 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -37,13 +38,18 @@ namespace waybar::modules::SNI { static const Glib::ustring SNI_INTERFACE_NAME = sn_item_interface_info()->name; static const unsigned UPDATE_DEBOUNCE_TIME = 10; -Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar) +Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar, + const std::function& on_ready, + const std::function& on_invalidate, const std::function& on_updated) : bus_name(bn), object_path(op), icon_size(16), effective_icon_size(0), icon_theme(Gtk::IconTheme::create()), - bar_(bar) { + bar_(bar), + on_ready_(on_ready), + on_invalidate_(on_invalidate), + on_updated_(on_updated) { if (config["icon-size"].isUInt()) { icon_size = config["icon-size"].asUInt(); } @@ -85,6 +91,8 @@ Item::~Item() { } } +bool Item::isReady() const { return ready_; } + bool Item::handleMouseEnter(GdkEventCrossing* const& e) { event_box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); return false; @@ -112,14 +120,18 @@ void Item::proxyReady(Glib::RefPtr& result) { if (this->id.empty() || this->category.empty()) { spdlog::error("Invalid Status Notifier Item: {}, {}", bus_name, object_path); + invalidate(); return; } this->updateImage(); + setReady(); } catch (const Glib::Error& err) { spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what()); + invalidate(); } catch (const std::exception& err) { spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what()); + invalidate(); } } @@ -178,17 +190,25 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } else if (name == "Status") { setStatus(get_variant(value)); } else if (name == "IconName") { - icon_name = get_variant(value); + if (has_custom_icon_) { + spdlog::trace("Item '{}': ignoring IconName update, custom icon is set", id); + } else { + icon_name = get_variant(value); + } } else if (name == "IconPixmap") { - icon_pixmap = this->extractPixBuf(value.gobj()); + if (has_custom_icon_) { + spdlog::trace("Item '{}': ignoring IconPixmap update, custom icon is set", id); + } else { + icon_pixmap = this->extractPixBuf(value.gobj()); + } } else if (name == "OverlayIconName") { overlay_icon_name = get_variant(value); } else if (name == "OverlayIconPixmap") { - // TODO: overlay_icon_pixmap + overlay_icon_pixmap = extractPixBuf(value.gobj()); } else if (name == "AttentionIconName") { attention_icon_name = get_variant(value); } else if (name == "AttentionIconPixmap") { - // TODO: attention_icon_pixmap + attention_icon_pixmap = extractPixBuf(value.gobj()); } else if (name == "AttentionMovieName") { attention_movie_name = get_variant(value); } else if (name == "ToolTip") { @@ -217,18 +237,35 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } void Item::setStatus(const Glib::ustring& value) { - Glib::ustring lower = value.lowercase(); - event_box.set_visible(show_passive_ || lower.compare("passive") != 0); + status_ = value.lowercase(); + event_box.set_visible(show_passive_ || status_.compare("passive") != 0); auto style = event_box.get_style_context(); for (const auto& class_name : style->list_classes()) { style->remove_class(class_name); } - if (lower.compare("needsattention") == 0) { + auto css_class = status_; + if (css_class.compare("needsattention") == 0) { // convert status to dash-case for CSS - lower = "needs-attention"; + css_class = "needs-attention"; } - style->add_class(lower); + style->add_class(css_class); + on_updated_(); +} + +void Item::setReady() { + if (ready_) { + return; + } + ready_ = true; + on_ready_(*this); +} + +void Item::invalidate() { + if (ready_) { + ready_ = false; + } + on_invalidate_(*this); } void Item::setCustomIcon(const std::string& id) { @@ -241,11 +278,13 @@ void Item::setCustomIcon(const std::string& id) { Glib::RefPtr custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon); icon_name = ""; // icon_name has priority over pixmap icon_pixmap = custom_pixbuf; + has_custom_icon_ = true; } catch (const Glib::Error& e) { spdlog::error("Failed to load custom icon {}: {}", custom_icon, e.what()); } } else { // if file doesn't exist it's most likely an icon_name icon_name = custom_icon; + has_custom_icon_ = true; } } } @@ -287,8 +326,8 @@ void Item::processUpdatedProperties(Glib::RefPtr& _result) { static const std::map> signal2props = { {"NewTitle", {"Title"}}, {"NewIcon", {"IconName", "IconPixmap"}}, - // {"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}}, - // {"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}}, + {"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}}, + {"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}}, {"NewIconThemePath", {"IconThemePath"}}, {"NewToolTip", {"ToolTip"}}, {"NewStatus", {"Status"}}, @@ -336,11 +375,20 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { if (array != nullptr) { g_free(array); } -#if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 68 - array = static_cast(g_memdup2(data, size)); -#else - array = static_cast(g_memdup(data, size)); -#endif + // We must allocate our own array because the data from GVariant is read-only + // and we need to modify it to convert ARGB to RGBA. + array = static_cast(g_malloc(size)); + + // Copy and convert ARGB to RGBA in one pass to avoid g_memdup2 overhead + const guchar* src = static_cast(data); + for (gsize i = 0; i < size; i += 4) { + guchar alpha = src[i]; + array[i] = src[i + 1]; + array[i + 1] = src[i + 2]; + array[i + 2] = src[i + 3]; + array[i + 3] = alpha; + } + lwidth = width; lheight = height; } @@ -349,14 +397,6 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { } g_variant_iter_free(it); if (array != nullptr) { - /* argb to rgba */ - for (uint32_t i = 0; i < 4U * lwidth * lheight; i += 4) { - guchar alpha = array[i]; - array[i] = array[i + 1]; - array[i + 1] = array[i + 2]; - array[i + 2] = array[i + 3]; - array[i + 3] = alpha; - } return Gdk::Pixbuf::create_from_data(array, Gdk::Colorspace::COLORSPACE_RGB, true, 8, lwidth, lheight, 4 * lwidth, &pixbuf_data_deleter); } @@ -365,47 +405,36 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { void Item::updateImage() { auto pixbuf = getIconPixbuf(); + if (!pixbuf) return; auto scaled_icon_size = getScaledIconSize(); // If the loaded icon is not square, assume that the icon height should match the // requested icon size, but the width is allowed to be different. As such, if the // height of the image does not match the requested icon size, resize the icon such that // the aspect ratio is maintained, but the height matches the requested icon size. - if (pixbuf->get_height() != scaled_icon_size) { + if (pixbuf->get_height() > 0 && pixbuf->get_height() != scaled_icon_size) { int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); } + pixbuf = overlayPixbufs(pixbuf, getOverlayIconPixbuf()); + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image.get_scale_factor(), image.get_window()); image.set(surface); } Glib::RefPtr Item::getIconPixbuf() { - if (!icon_name.empty()) { - try { - std::ifstream temp(icon_name); - if (temp.is_open()) { - return Gdk::Pixbuf::create_from_file(icon_name); - } - } catch (Glib::Error& e) { - // Ignore because we want to also try different methods of getting an icon. - // - // But a warning is logged, as the file apparently exists, but there was - // a failure in creating a pixbuf out of it. - - spdlog::warn("Item '{}': {}", id, static_cast(e.what())); - } - - try { - // Will throw if it can not find an icon. - return getIconByName(icon_name, getScaledIconSize()); - } catch (Glib::Error& e) { - spdlog::trace("Item '{}': {}", id, static_cast(e.what())); + if (status_ == "needsattention") { + if (auto attention_pixbuf = getAttentionIconPixbuf()) { + return attention_pixbuf; } } - // Return the pixmap only if an icon for the given name could not be found. + if (auto pixbuf = loadIconFromNameOrFile(icon_name, true)) { + return pixbuf; + } + if (icon_pixmap) { return icon_pixmap; } @@ -420,9 +449,78 @@ Glib::RefPtr Item::getIconPixbuf() { return getIconByName("image-missing", getScaledIconSize()); } -Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { - icon_theme->rescan_if_needed(); +Glib::RefPtr Item::getAttentionIconPixbuf() { + if (auto pixbuf = loadIconFromNameOrFile(attention_icon_name, false)) { + return pixbuf; + } + if (auto pixbuf = loadIconFromNameOrFile(attention_movie_name, false)) { + return pixbuf; + } + return attention_icon_pixmap; +} +Glib::RefPtr Item::getOverlayIconPixbuf() { + if (auto pixbuf = loadIconFromNameOrFile(overlay_icon_name, false)) { + return pixbuf; + } + return overlay_icon_pixmap; +} + +Glib::RefPtr Item::loadIconFromNameOrFile(const std::string& name, bool log_failure) { + if (name.empty()) { + return {}; + } + + try { + std::ifstream temp(name); + if (temp.is_open()) { + return Gdk::Pixbuf::create_from_file(name); + } + } catch (const Glib::Error& e) { + if (log_failure) { + spdlog::warn("Item '{}': {}", id, static_cast(e.what())); + } + } + + try { + return getIconByName(name, getScaledIconSize()); + } catch (const Glib::Error& e) { + if (log_failure) { + spdlog::trace("Item '{}': {}", id, static_cast(e.what())); + } + } + + return {}; +} + +Glib::RefPtr Item::overlayPixbufs(const Glib::RefPtr& base, + const Glib::RefPtr& overlay) { + if (!base || !overlay) { + return base; + } + + auto composed = base->copy(); + if (!composed) { + return base; + } + + int overlay_target_size = + std::max(1, std::min(composed->get_width(), composed->get_height()) / 2); + auto scaled_overlay = overlay->scale_simple(overlay_target_size, overlay_target_size, + Gdk::InterpType::INTERP_BILINEAR); + if (!scaled_overlay) { + return composed; + } + + int dest_x = std::max(0, composed->get_width() - scaled_overlay->get_width()); + int dest_y = std::max(0, composed->get_height() - scaled_overlay->get_height()); + scaled_overlay->composite(composed, dest_x, dest_y, scaled_overlay->get_width(), + scaled_overlay->get_height(), dest_x, dest_y, 1.0, 1.0, + Gdk::InterpType::INTERP_BILINEAR, 255); + return composed; +} + +Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { if (!icon_theme_path.empty()) { auto icon_info = icon_theme->lookup_icon(name.c_str(), request_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); @@ -464,6 +562,9 @@ void Item::makeMenu() { } bool Item::handleClick(GdkEventButton* const& ev) { + if (!proxy_) { + return false; + } auto parameters = Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(ev->x_root + bar_.x_global), Glib::Variant::create(ev->y_root + bar_.y_global)}); @@ -491,6 +592,9 @@ bool Item::handleClick(GdkEventButton* const& ev) { } bool Item::handleScroll(GdkEventScroll* const& ev) { + if (!proxy_) { + return false; + } int dx = 0, dy = 0; switch (ev->direction) { case GDK_SCROLL_UP: diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 34a3c05f..19de5174 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -13,7 +13,8 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) box_(bar.orientation, 0), watcher_(SNI::Watcher::getInstance()), host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1), - std::bind(&Tray::onRemove, this, std::placeholders::_1)) { + std::bind(&Tray::onRemove, this, std::placeholders::_1), + std::bind(&Tray::queueUpdate, this)) { box_.set_name("tray"); event_box_.add(box_); if (!id.empty()) { @@ -23,9 +24,6 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) if (config_["spacing"].isUInt()) { box_.set_spacing(config_["spacing"].asUInt()); } - if (config["show-passive-items"].isBool()) { - show_passive_ = config["show-passive-items"].asBool(); - } nb_hosts_ += 1; if (config_["icons"].isObject()) { IconManager::instance().setIconsConfig(config_["icons"]); @@ -33,12 +31,16 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) dp.emit(); } +void Tray::queueUpdate() { dp.emit(); } + void Tray::onAdd(std::unique_ptr& item) { if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) { box_.pack_end(item->event_box); } else { box_.pack_start(item->event_box); } + item->event_box.signal_show().connect([this] { dp.emit(); }); + item->event_box.signal_hide().connect([this] { dp.emit(); }); dp.emit(); } @@ -48,17 +50,9 @@ void Tray::onRemove(std::unique_ptr& item) { } auto Tray::update() -> void { - // Show tray only when items are available std::vector children = box_.get_children(); - if (show_passive_) { - event_box_.set_visible(!children.empty()); - } else { - event_box_.set_visible(!std::all_of(children.begin(), children.end(), [](Gtk::Widget* child) { - return child->get_style_context()->has_class("passive"); - })); - } - - // Call parent update + event_box_.set_visible(std::any_of(children.begin(), children.end(), + [](Gtk::Widget* child) { return child->get_visible(); })); AModule::update(); } diff --git a/src/modules/sni/watcher.cpp b/src/modules/sni/watcher.cpp index 324bd9f5..969806cf 100644 --- a/src/modules/sni/watcher.cpp +++ b/src/modules/sni/watcher.cpp @@ -31,7 +31,7 @@ Watcher::~Watcher() { void Watcher::busAcquired(const Glib::RefPtr& conn, Glib::ustring name) { GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error) { g_error_free(error); } @@ -69,7 +69,7 @@ gboolean Watcher::handleRegisterHost(Watcher* obj, GDBusMethodInvocation* invoca if (watch != nullptr) { g_warning("Status Notifier Host with bus name '%s' and object path '%s' is already registered", bus_name, object_path); - sn_watcher_complete_register_item(obj->watcher_, invocation); + sn_watcher_complete_register_host(obj->watcher_, invocation); return TRUE; } watch = gfWatchNew(GF_WATCH_TYPE_HOST, service, bus_name, object_path, obj); @@ -98,8 +98,8 @@ gboolean Watcher::handleRegisterItem(Watcher* obj, GDBusMethodInvocation* invoca } auto watch = gfWatchFind(obj->items_, bus_name, object_path); if (watch != nullptr) { - g_warning("Status Notifier Item with bus name '%s' and object path '%s' is already registered", - bus_name, object_path); + spdlog::debug("Ignoring duplicate Status Notifier Item registration for '{}' at '{}'", bus_name, + object_path); sn_watcher_complete_register_item(obj->watcher_, invocation); return TRUE; } @@ -158,7 +158,7 @@ void Watcher::nameVanished(GDBusConnection* connection, const char* name, gpoint watch->watcher->hosts_ = g_slist_remove(watch->watcher->hosts_, watch); if (watch->watcher->hosts_ == nullptr) { sn_watcher_set_is_host_registered(watch->watcher->watcher_, FALSE); - sn_watcher_emit_host_registered(watch->watcher->watcher_); + sn_watcher_emit_host_unregistered(watch->watcher->watcher_); } } else if (watch->type == GF_WATCH_TYPE_ITEM) { watch->watcher->items_ = g_slist_remove(watch->watcher->items_, watch); @@ -167,6 +167,7 @@ void Watcher::nameVanished(GDBusConnection* connection, const char* name, gpoint sn_watcher_emit_item_unregistered(watch->watcher->watcher_, tmp); g_free(tmp); } + gfWatchFree(watch); } void Watcher::updateRegisteredItems(SnWatcher* obj) { diff --git a/src/modules/sway/bar.cpp b/src/modules/sway/bar.cpp index f28b0502..f374f1c1 100644 --- a/src/modules/sway/bar.cpp +++ b/src/modules/sway/bar.cpp @@ -60,7 +60,7 @@ BarIpcClient::BarIpcClient(waybar::Bar& bar) : bar_{bar} { }); } -bool BarIpcClient::isModuleEnabled(std::string name) { +bool BarIpcClient::isModuleEnabled(const std::string& name) { for (const auto& section : {"modules-left", "modules-center", "modules-right"}) { if (const auto& modules = bar_.config.get(section, {}); modules.isArray()) { for (const auto& module : modules) { diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index 4139a53b..3ebccccd 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -9,8 +9,8 @@ namespace waybar::modules::sway { Ipc::Ipc() { const std::string& socketPath = getSocketPath(); - fd_ = open(socketPath); - fd_event_ = open(socketPath); + fd_ = util::ScopedFd(open(socketPath)); + fd_event_ = util::ScopedFd(open(socketPath)); } Ipc::~Ipc() { @@ -21,15 +21,11 @@ Ipc::~Ipc() { if (write(fd_, "close-sway-ipc", 14) == -1) { spdlog::error("Failed to close sway IPC"); } - close(fd_); - fd_ = -1; } if (fd_event_ > 0) { if (write(fd_event_, "close-sway-ipc", 14) == -1) { spdlog::error("Failed to close sway IPC event handler"); } - close(fd_event_); - fd_event_ = -1; } } @@ -64,7 +60,7 @@ const std::string Ipc::getSocketPath() const { } int Ipc::open(const std::string& socketPath) const { - int32_t fd = socket(AF_UNIX, SOCK_STREAM, 0); + util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0)); if (fd == -1) { throw std::runtime_error("Unable to open Unix socket"); } @@ -78,7 +74,7 @@ int Ipc::open(const std::string& socketPath) const { if (::connect(fd, reinterpret_cast(&addr), l) == -1) { throw std::runtime_error("Unable to connect to Sway"); } - return fd; + return fd.release(); } struct Ipc::ipc_response Ipc::recv(int fd) { diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index f4cfa6c3..28f1ef24 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -54,21 +54,28 @@ void Language::onCmd(const struct Ipc::ipc_response& res) { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); std::vector used_layouts; - // Display current layout of a device with a maximum count of layouts, expecting that all will - // be OK + // Union layout names across every keyboard input so hot-plugged devices contribute their + // layouts to the map. Track the device with the most layouts to seed the initially displayed + // layout, matching the previous behaviour at startup. Json::ArrayIndex max_id = 0, max = 0; for (Json::ArrayIndex i = 0; i < payload.size(); i++) { - auto size = payload[i][XKB_LAYOUT_NAMES_KEY].size(); - if (size > max) { - max = size; + if (payload[i]["type"].asString() != "keyboard") continue; + const auto& names = payload[i][XKB_LAYOUT_NAMES_KEY]; + if (names.size() > max) { + max = names.size(); max_id = i; } + for (const auto& layout : names) { + const auto name = layout.asString(); + if (std::find(used_layouts.begin(), used_layouts.end(), name) == used_layouts.end()) { + used_layouts.push_back(name); + } + } } - for (const auto& layout : payload[max_id][XKB_LAYOUT_NAMES_KEY]) { - used_layouts.push_back(layout.asString()); - } - + // Rebuild from scratch so init_layouts_map's duplicate-suffix pass doesn't compound across + // refreshes (e.g. "us" -> "us1" -> "us11"). + layouts_map_.clear(); init_layouts_map(used_layouts); set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); dp.emit(); @@ -82,13 +89,26 @@ void Language::onEvent(const struct Ipc::ipc_response& res) { return; } + bool refresh_inputs = false; try { - std::lock_guard lock(mutex_); - auto payload = parser_.parse(res.payload)["input"]; - if (payload["type"].asString() == "keyboard") { - set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + { + std::lock_guard lock(mutex_); + auto root = parser_.parse(res.payload); + auto change = root["change"].asString(); + auto payload = root["input"]; + if (payload["type"].asString() == "keyboard") { + // A device was added or its keymap changed - the layout set may have grown, so refresh + // layouts_map_ via IPC_GET_INPUTS once we've released mutex_. + refresh_inputs = (change == "added" || change == "xkb_keymap"); + set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + } + dp.emit(); + } + // sendCmd is synchronous: it blocks on the IPC reply and then emits signal_cmd on this same + // thread, which lands in onCmd and re-locks mutex_. Must call it with mutex_ released. + if (refresh_inputs) { + ipc_.sendCmd(IPC_GET_INPUTS); } - dp.emit(); } catch (const std::exception& e) { spdlog::error("Language: {}", e.what()); } @@ -124,9 +144,16 @@ auto Language::update() -> void { ALabel::update(); } -auto Language::set_current_layout(std::string current_layout) -> void { +auto Language::set_current_layout(const std::string& current_layout) -> void { + // Guard against unknown / empty layout names: transient virtual keyboards (e.g. wtype) and + // hot-plugged devices whose layouts haven't made it into the map yet would otherwise blank out + // layout_ via map::operator[]'s default-construct-on-miss. + auto it = layouts_map_.find(current_layout); + if (it == layouts_map_.end()) { + return; + } label_.get_style_context()->remove_class(layout_.short_name); - layout_ = layouts_map_[current_layout]; + layout_ = it->second; label_.get_style_context()->add_class(layout_.short_name); } diff --git a/src/modules/sway/mode.cpp b/src/modules/sway/mode.cpp index b81735e5..1f5f071a 100644 --- a/src/modules/sway/mode.cpp +++ b/src/modules/sway/mode.cpp @@ -44,7 +44,7 @@ auto Mode::update() -> void { } else { label_.set_markup(fmt::format(fmt::runtime(format_), mode_)); if (tooltipEnabled()) { - label_.set_tooltip_text(mode_); + label_.set_tooltip_markup(mode_); } event_box_.show(); } diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index 68655a76..2908b85c 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -99,7 +99,7 @@ auto Window::update() -> void { fmt::arg("shell", shell_), fmt::arg("marks", marks_)), config_["rewrite"])); if (tooltipEnabled()) { - label_.set_tooltip_text(window_); + label_.set_tooltip_markup(window_); } updateAppIcon(); @@ -184,9 +184,9 @@ std::tuple getWindowInfo( continue; } if (!marks.empty()) { - marks += ','; + marks.append(","); } - marks += m.asString(); + marks.append(m.asString()); } } return {app_id, app_class, shell, marks}; diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index e0255893..6cb3ee48 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -10,7 +10,7 @@ namespace waybar::modules::sway { // Helper function to assign a number to a workspace, just like sway. In fact // this is taken quite verbatim from `sway/ipc-json.c`. -int Workspaces::convertWorkspaceNameToNum(std::string name) { +int Workspaces::convertWorkspaceNameToNum(const std::string& name) { if (isdigit(name[0]) != 0) { errno = 0; char* endptr = nullptr; @@ -229,7 +229,10 @@ bool Workspaces::filterButtons() { auto ws = std::find_if(workspaces_.begin(), workspaces_.end(), [it](const auto& node) { return node["name"].asString() == it->first; }); if (ws == workspaces_.end() || - (!config_["all-outputs"].asBool() && (*ws)["output"].asString() != bar_.output->name)) { + ((*ws).isMember("target_output") ? (*ws)["target_output"].asString() != bar_.output->name && + (*ws)["target_output"].asString() != "" + : !config_["all-outputs"].asBool() && + (*ws)["output"].asString() != bar_.output->name)) { it = buttons_.erase(it); needReorder = true; } else { @@ -329,7 +332,12 @@ auto Workspaces::update() -> void { } else { button.get_style_context()->remove_class("current_output"); } - std::string output = (*it)["name"].asString(); + std::string output; + if (!config_["disable-markup"].asBool()) { + output = g_markup_escape_text((*it)["name"].asString().c_str(), -1); + } else { + output = (*it)["name"].asString(); + } std::string windows = ""; if (config_["window-rewrite"].isObject()) { updateWindows((*it), windows); @@ -369,11 +377,14 @@ Gtk::Button& Workspaces::addButton(const Json::Value& node) { node["name"].asString(), node["target_output"].asString(), "--no-auto-back-and-forth", node["name"].asString())); } else { - ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"", - config_["disable-auto-back-and-forth"].asBool() - ? "--no-auto-back-and-forth" - : "", - node["name"].asString())); + std::string flag = config_["disable-auto-back-and-forth"].asBool() + ? "--no-auto-back-and-forth" + : ""; + if (node["num"].asInt() >= 0) { + ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_number_cmd_, flag, node["num"].asInt())); + } else { + ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, flag, node["name"].asString())); + } } } catch (const std::exception& e) { spdlog::error("Workspaces: {}", e.what()); @@ -487,7 +498,7 @@ std::string Workspaces::getCycleWorkspace(std::vector::iterator it, return (*it)["name"].asString(); } -std::string Workspaces::trimWorkspaceName(std::string name) { +std::string Workspaces::trimWorkspaceName(const std::string& name) { std::size_t found = name.find(':'); if (found != std::string::npos) { return name.substr(found + 1); diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 90f33be7..829a7776 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -1,10 +1,15 @@ #include "modules/systemd_failed_units.hpp" +#include #include +#include #include #include #include +#include +#include +#include static const unsigned UPDATE_DEBOUNCE_TIME_MS = 1000; @@ -12,56 +17,86 @@ 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), - nr_failed(0), - last_status() { + hide_on_ok_(true), + tooltip_format_( + "System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n" + "{failed_units_list}"), + tooltip_format_ok_("System: {system_state}\nUser: {user_state}"), + tooltip_unit_format_("{name}: {description}"), + update_pending_(false), + nr_failed_system_(0), + nr_failed_user_(0), + nr_failed_(0), + last_status_() { if (config["hide-on-ok"].isBool()) { - hide_on_ok = config["hide-on-ok"].asBool(); + hide_on_ok_ = config["hide-on-ok"].asBool(); } if (config["format-ok"].isString()) { - format_ok = config["format-ok"].asString(); + format_ok_ = config["format-ok"].asString(); } else { - format_ok = format_; + format_ok_ = format_; + } + + if (config["tooltip-format"].isString()) { + tooltip_format_ = config["tooltip-format"].asString(); + } + if (config["tooltip-format-ok"].isString()) { + tooltip_format_ok_ = config["tooltip-format-ok"].asString(); + } + if (config["tooltip-unit-format"].isString()) { + tooltip_unit_format_ = config["tooltip-unit-format"].asString(); } /* Default to enable both "system" and "user". */ if (!config["system"].isBool() || config["system"].asBool()) { - system_proxy = Gio::DBus::Proxy::create_for_bus_sync( + system_props_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) { + if (!system_props_proxy_) { throw std::runtime_error("Unable to connect to systemwide systemd DBus!"); } - system_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + system_props_proxy_->signal_signal().connect( + sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + try { + system_manager_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager"); + } catch (const Glib::Error& e) { + spdlog::warn("Unable to connect to systemwide systemd Manager interface: {}", + e.what().c_str()); + } } if (!config["user"].isBool() || config["user"].asBool()) { - user_proxy = Gio::DBus::Proxy::create_for_bus_sync( + user_props_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) { + if (!user_props_proxy_) { throw std::runtime_error("Unable to connect to user systemd DBus!"); } - user_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + user_props_proxy_->signal_signal().connect( + sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + try { + user_manager_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager"); + } catch (const Glib::Error& e) { + spdlog::warn("Unable to connect to user systemd Manager interface: {}", e.what().c_str()); + } } + if (!user_props_proxy_ && !system_props_proxy_) + throw std::runtime_error("Neither system nor user status is requested."); + 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; + 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); @@ -71,15 +106,15 @@ auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, void SystemdFailedUnits::RequestSystemState() { auto load = [](const char* kind, Glib::RefPtr& proxy) -> std::string { try { - if (!proxy) return "unknown"; + if (!proxy) return "ignored"; auto parameters = Glib::VariantContainerBase( g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState")); 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); + g_variant_get(const_cast(data.gobj()), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_STRING)) { - return g_variant_get_string(variant.gobj_copy(), NULL); + return g_variant_get_string(const_cast(variant.gobj()), NULL); } } } catch (Glib::Error& e) { @@ -88,12 +123,14 @@ void SystemdFailedUnits::RequestSystemState() { return "unknown"; }; - system_state = load("systemwide", system_proxy); - user_state = load("user", user_proxy); - if (system_state == "running" && user_state == "running") - overall_state = "ok"; + system_state_ = load("systemwide", system_props_proxy_); + user_state_ = load("user", user_props_proxy_); + + if ((system_state_ == "running" || system_state_ == "ignored") && + (user_state_ == "running" || user_state_ == "ignored")) + overall_state_ = "ok"; else - overall_state = "degraded"; + overall_state_ = "degraded"; } void SystemdFailedUnits::RequestFailedUnits() { @@ -105,9 +142,9 @@ void SystemdFailedUnits::RequestFailedUnits() { 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); + g_variant_get(const_cast(data.gobj()), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_UINT32)) { - return g_variant_get_uint32(variant.gobj_copy()); + return g_variant_get_uint32(const_cast(variant.gobj())); } } } catch (Glib::Error& e) { @@ -116,46 +153,153 @@ void SystemdFailedUnits::RequestFailedUnits() { return 0; }; - nr_failed_system = load("systemwide", system_proxy); - nr_failed_user = load("user", user_proxy); - nr_failed = nr_failed_system + nr_failed_user; + nr_failed_system_ = load("systemwide", system_props_proxy_); + nr_failed_user_ = load("user", user_props_proxy_); + nr_failed_ = nr_failed_system_ + nr_failed_user_; +} + +void SystemdFailedUnits::RequestFailedUnitsList() { + failed_units_.clear(); + if (!tooltipEnabled() || nr_failed_ == 0) { + return; + } + + if (system_manager_proxy_) { + auto units = LoadFailedUnitsList("systemwide", system_manager_proxy_, "system"); + failed_units_.insert(failed_units_.end(), units.begin(), units.end()); + } + if (user_manager_proxy_) { + auto units = LoadFailedUnitsList("user", user_manager_proxy_, "user"); + failed_units_.insert(failed_units_.end(), units.begin(), units.end()); + } +} + +auto SystemdFailedUnits::LoadFailedUnitsList(const char* kind, + Glib::RefPtr& proxy, + const std::string& scope) -> std::vector { + // org.freedesktop.systemd1.Manager.ListUnits returns + // (name, description, load_state, active_state, sub_state, followed, unit_path, job_id, + // job_type, job_path). + using UnitRow = std::tuple; + using ListUnitsReply = Glib::Variant>>; + + std::vector units; + if (!proxy) { + return units; + } + + try { + auto data = proxy->call_sync("ListUnits"); + if (!data) return units; + if (!data.is_of_type(ListUnitsReply::variant_type())) { + spdlog::warn("Unexpected DBus signature for ListUnits: {}", data.get_type_string()); + return units; + } + + auto [rows] = Glib::VariantBase::cast_dynamic(data).get(); + for (const auto& row : rows) { + const auto& name = std::get<0>(row); + const auto& description = std::get<1>(row); + const auto& load_state = std::get<2>(row); + const auto& active_state = std::get<3>(row); + const auto& sub_state = std::get<4>(row); + if (active_state == "failed" || sub_state == "failed") { + units.push_back({name, description, load_state, active_state, sub_state, scope}); + } + } + } catch (const Glib::Error& e) { + spdlog::error("Failed to list {} units: {}", kind, e.what().c_str()); + } + + return units; +} + +std::string SystemdFailedUnits::BuildTooltipFailedList() const { + if (failed_units_.empty()) { + return ""; + } + + std::string list; + list.reserve(failed_units_.size() * 16); + bool first = true; + for (const auto& unit : failed_units_) { + try { + auto line = fmt::format( + fmt::runtime(tooltip_unit_format_), + fmt::arg("name", Glib::Markup::escape_text(unit.name).raw()), + fmt::arg("description", Glib::Markup::escape_text(unit.description).raw()), + fmt::arg("load_state", unit.load_state), fmt::arg("active_state", unit.active_state), + fmt::arg("sub_state", unit.sub_state), fmt::arg("scope", unit.scope)); + if (!first) { + list += "\n"; + } + first = false; + list += "- "; + list += line; + } catch (const std::exception& e) { + spdlog::warn("Failed to format tooltip for unit {}: {}", unit.name, e.what()); + } + } + + return list; } void SystemdFailedUnits::updateData() { - update_pending = false; + update_pending_ = false; RequestSystemState(); - if (overall_state == "degraded") RequestFailedUnits(); + if (overall_state_ == "degraded") { + RequestFailedUnits(); + RequestFailedUnitsList(); + } else { + nr_failed_system_ = nr_failed_user_ = nr_failed_ = 0; + failed_units_.clear(); + } dp.emit(); } auto SystemdFailedUnits::update() -> void { - if (last_status == overall_state) return; - // Hide if needed. - if (overall_state == "ok" && hide_on_ok) { + if (overall_state_ == "ok" && hide_on_ok_) { event_box_.set_visible(false); + last_status_ = overall_state_; return; } event_box_.set_visible(true); // Set state class. - if (!last_status.empty() && label_.get_style_context()->has_class(last_status)) { - label_.get_style_context()->remove_class(last_status); + 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(overall_state)) { - label_.get_style_context()->add_class(overall_state); + if (!label_.get_style_context()->has_class(overall_state_)) { + label_.get_style_context()->add_class(overall_state_); } - last_status = overall_state; + last_status_ = overall_state_; 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), - fmt::arg("system_state", system_state), fmt::arg("user_state", user_state), - fmt::arg("overall_state", overall_state))); + 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_), + fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_), + fmt::arg("overall_state", overall_state_))); + if (tooltipEnabled()) { + std::string failed_list = BuildTooltipFailedList(); + auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_; + if (!tooltip_template.empty()) { + label_.set_tooltip_markup(fmt::format( + fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_), + fmt::arg("nr_failed_system", nr_failed_system_), + fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), + fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_), + fmt::arg("failed_units_list", failed_list))); + } else { + label_.set_tooltip_text(""); + } + } ALabel::update(); } diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index bb5ff02c..dcfb0b2f 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -155,3 +155,7 @@ bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) { return config_["critical-threshold"].isInt() && temperature_c >= config_["critical-threshold"].asInt(); } + +void waybar::modules::Temperature::suspend() { thread_.pause(); } + +void waybar::modules::Temperature::resume() { thread_.resume(); } diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp index 7530890c..d260ba83 100644 --- a/src/modules/upower.cpp +++ b/src/modules/upower.cpp @@ -59,10 +59,13 @@ UPower::UPower(const std::string& id, const Json::Value& config) sigc::mem_fun(*this, &UPower::getConn_cb)); // Make UPower client - GError** gErr = NULL; - upClient_ = up_client_new_full(NULL, gErr); - if (upClient_ == NULL) - spdlog::error("Upower. UPower client connection error. {}", (*gErr)->message); + GError* gErr = NULL; + upClient_ = up_client_new_full(NULL, &gErr); + if (upClient_ == NULL) { + spdlog::error("Upower. UPower client connection error. {}", + gErr ? gErr->message : "unknown error"); + if (gErr) g_error_free(gErr); + } // Subscribe UPower events g_signal_connect(upClient_, "device-added", G_CALLBACK(deviceAdded_cb), this); @@ -92,7 +95,7 @@ UPower::~UPower() { removeDevices(); } -static const std::string getDeviceStatus(UpDeviceState& state) { +static std::string_view getDeviceStatus(UpDeviceState& state) { switch (state) { case UP_DEVICE_STATE_CHARGING: case UP_DEVICE_STATE_PENDING_CHARGE: @@ -109,7 +112,7 @@ static const std::string getDeviceStatus(UpDeviceState& state) { } } -static const std::string getDeviceIcon(UpDeviceKind& kind) { +static std::string_view getDeviceIcon(UpDeviceKind& kind) { switch (kind) { case UP_DEVICE_KIND_LINE_POWER: return "ac-adapter-symbolic"; @@ -209,7 +212,8 @@ auto UPower::update() -> void { // Remove last status if it exists if (!lastStatus_.empty() && box_.get_style_context()->has_class(lastStatus_)) box_.get_style_context()->remove_class(lastStatus_); - if (!box_.get_style_context()->has_class(status)) box_.get_style_context()->add_class(status); + if (!box_.get_style_context()->has_class(std::string(status))) + box_.get_style_context()->add_class(std::string(status)); lastStatus_ = status; if (devices_.size() == 0 && !upDeviceValid && hideIfEmpty_) { diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 5a9c0c1a..037a61e1 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -26,13 +27,19 @@ inline auto byteswap(uint32_t x) -> uint32_t { auto pack_and_write(Sock& sock, std::string&& buf) -> void { uint32_t len = buf.size(); if constexpr (std::endian::native != std::endian::little) len = byteswap(len); - (void)write(sock.fd, &len, 4); - (void)write(sock.fd, buf.data(), buf.size()); + (void)write(sock, &len, 4); + (void)write(sock, buf.data(), buf.size()); } auto read_exact(Sock& sock, size_t n) -> std::string { auto buf = std::string(n, 0); - for (size_t i = 0; i < n;) i += read(sock.fd, &buf[i], n - i); + for (size_t i = 0; i < n;) { + auto r = read(sock, &buf[i], n - i); + if (r <= 0) { + throw std::runtime_error("Wayfire IPC: read failed"); + } + i += static_cast(r); + } return buf; } @@ -53,6 +60,9 @@ auto State::Wset::locate_ws(const Json::Value& geo) -> Workspace& { } auto State::Wset::locate_ws(const Json::Value& geo) const -> const Workspace& { + if (!output.has_value()) { + throw std::runtime_error("Wayfire IPC: wset has no output assigned"); + } const auto& out = output.value().get(); auto [qx, rx] = std::div(geo["x"].asInt(), out.w); auto [qy, ry] = std::div(geo["y"].asInt(), out.h); @@ -88,7 +98,10 @@ auto State::update_view(const Json::Value& view) -> void { auto IPC::get_instance() -> std::shared_ptr { auto p = instance.lock(); - if (!p) instance = p = std::shared_ptr(new IPC); + if (!p) { + instance = p = std::shared_ptr(new IPC); + p->start(); + } return p; } @@ -98,7 +111,7 @@ auto IPC::connect() -> Sock { throw std::runtime_error{"Wayfire IPC: ipc not available"}; } - auto sock = socket(AF_UNIX, SOCK_STREAM, 0); + util::ScopedFd sock(socket(AF_UNIX, SOCK_STREAM, 0)); if (sock == -1) { throw std::runtime_error{"Wayfire IPC: socket() failed"}; } @@ -108,15 +121,16 @@ auto IPC::connect() -> Sock { addr.sun_path[sizeof(addr.sun_path) - 1] = 0; if (::connect(sock, (const sockaddr*)&addr, sizeof(addr)) == -1) { - close(sock); throw std::runtime_error{"Wayfire IPC: connect() failed"}; } - return {sock}; + return sock; } auto IPC::receive(Sock& sock) -> Json::Value { - auto len = *reinterpret_cast(read_exact(sock, 4).data()); + auto len_buf = read_exact(sock, 4); + uint32_t len; + std::memcpy(&len, len_buf.data(), sizeof(len)); if constexpr (std::endian::native != std::endian::little) len = byteswap(len); auto buf = read_exact(sock, len); @@ -153,15 +167,15 @@ auto IPC::start() -> void { send("window-rules/get-focused-view", {}); send("window-rules/get-focused-output", {}); - std::thread([&] { + std::thread([self = shared_from_this()] { auto sock = connect(); { Json::Value json; json["method"] = "window-rules/events/watch"; - pack_and_write(sock, Json::writeString(writer_builder, json)); - if (receive(sock)["result"] != "ok") { + pack_and_write(sock, Json::writeString(self->writer_builder, json)); + if (self->receive(sock)["result"] != "ok") { spdlog::error( "Wayfire IPC: method \"window-rules/events/watch\"" " have failed"); @@ -169,10 +183,10 @@ auto IPC::start() -> void { } } - while (auto json = receive(sock)) { + while (auto json = self->receive(sock)) { auto ev = json["event"].asString(); spdlog::debug("Wayfire IPC: received event \"{}\"", ev); - root_event_handler(ev, json); + self->root_event_handler(ev, json); } }).detach(); } @@ -333,10 +347,13 @@ auto IPC::update_state_handler(const std::string& event, const Json::Value& data if (event == "output-wset-changed") { // data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output } - auto& output = state.outputs.at(data["output-data"]["name"].asString()); - auto wset_idx = data["new-wset-data"]["index"].asUInt(); - state.wsets.at(wset_idx).output = output; - output.wset_idx = wset_idx; + try { + auto& output = state.outputs.at(data["output-data"]["name"].asString()); + auto wset_idx = data["new-wset-data"]["index"].asUInt(); + state.wsets.at(wset_idx).output = output; + output.wset_idx = wset_idx; + } catch (const std::exception&) { + } return; } diff --git a/src/modules/wayfire/window.cpp b/src/modules/wayfire/window.cpp index fbcde6ec..8634f090 100644 --- a/src/modules/wayfire/window.cpp +++ b/src/modules/wayfire/window.cpp @@ -34,8 +34,12 @@ auto Window::update() -> void { auto Window::update_icon_label() -> void { auto _ = ipc->lock_state(); - const auto& output = ipc->get_outputs().at(bar_.output->name); - const auto& wset = ipc->get_wsets().at(output.wset_idx); + auto out_it = ipc->get_outputs().find(bar_.output->name); + if (out_it == ipc->get_outputs().end()) return; + const auto& output = out_it->second; + auto wset_it = ipc->get_wsets().find(output.wset_idx); + if (wset_it == ipc->get_wsets().end()) return; + const auto& wset = wset_it->second; const auto& views = ipc->get_views(); auto ctx = bar_.window.get_style_context(); diff --git a/src/modules/wayfire/workspaces.cpp b/src/modules/wayfire/workspaces.cpp index 4c4cd6c1..724a19f8 100644 --- a/src/modules/wayfire/workspaces.cpp +++ b/src/modules/wayfire/workspaces.cpp @@ -70,8 +70,12 @@ auto Workspaces::handleScroll(GdkEventScroll* e) -> bool { Json::Value data; { auto _ = ipc->lock_state(); - const auto& output = ipc->get_outputs().at(bar_.output->name); - const auto& wset = ipc->get_wsets().at(output.wset_idx); + auto out_it = ipc->get_outputs().find(bar_.output->name); + if (out_it == ipc->get_outputs().end()) return true; + const auto& output = out_it->second; + auto wset_it = ipc->get_wsets().find(output.wset_idx); + if (wset_it == ipc->get_wsets().end()) return true; + const auto& wset = wset_it->second; auto n = wset.ws_w * wset.ws_h; auto i = (wset.ws_idx() + delta + n) % n; data["x"] = Json::Value((uint64_t)i % wset.ws_w); @@ -92,8 +96,12 @@ auto Workspaces::update_box() -> void { auto _ = ipc->lock_state(); const auto& output_name = bar_.output->name; - const auto& output = ipc->get_outputs().at(output_name); - const auto& wset = ipc->get_wsets().at(output.wset_idx); + auto out_it = ipc->get_outputs().find(output_name); + if (out_it == ipc->get_outputs().end()) return; + const auto& output = out_it->second; + auto wset_it = ipc->get_wsets().find(output.wset_idx); + if (wset_it == ipc->get_wsets().end()) return; + const auto& wset = wset_it->second; auto output_focused = ipc->get_focused_output_name() == output_name; auto ws_w = wset.ws_w; diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index bb3005bd..0c0d1dde 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -54,6 +54,15 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val waybar::modules::Wireplumber::~Wireplumber() { waybar::modules::Wireplumber::modules.remove(this); + if (mixer_api_ != nullptr) { + g_signal_handlers_disconnect_by_data(mixer_api_, this); + } + if (def_nodes_api_ != nullptr) { + g_signal_handlers_disconnect_by_data(def_nodes_api_, this); + } + if (om_ != nullptr) { + g_signal_handlers_disconnect_by_data(om_, this); + } wp_core_disconnect(wp_core_); g_clear_pointer(&apis_, g_ptr_array_unref); g_clear_object(&om_); @@ -81,7 +90,7 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* if (proxy == nullptr) { auto err = fmt::format("Object '{}' not found\n", id); spdlog::error("[{}]: {}", self->name_, err); - throw std::runtime_error(err); + return; } g_autoptr(WpProperties) properties = @@ -153,7 +162,7 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se if (variant == nullptr) { auto err = fmt::format("Node {} does not support volume\n", id); spdlog::error("[{}]: {}", self->name_, err); - throw std::runtime_error(err); + return; } g_variant_lookup(variant, "volume", "d", &self->volume_); @@ -287,14 +296,14 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir if (self->def_nodes_api_ == nullptr) { spdlog::error("[{}]: default nodes api is not loaded.", self->name_); - throw std::runtime_error("Default nodes API is not loaded\n"); + return; } self->mixer_api_ = wp_plugin_find(self->wp_core_, "mixer-api"); if (self->mixer_api_ == nullptr) { spdlog::error("[{}]: mixer api is not loaded.", self->name_); - throw std::runtime_error("Mixer api is not loaded\n"); + return; } // Get default sink @@ -336,7 +345,7 @@ void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* if (wp_object_activate_finish(p, res, &error) == 0) { spdlog::error("[{}]: error activating plugin: {}", self->name_, error->message); - throw std::runtime_error(error->message); + return; } if (--self->pending_plugins_ == 0) { @@ -373,7 +382,7 @@ void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncRe if (success == FALSE) { spdlog::error("[{}]: default nodes API load failed", self->name_); - throw std::runtime_error(error->message); + return; } spdlog::debug("[{}]: loaded default nodes api", self->name_); g_ptr_array_add(self->apis_, wp_plugin_find(self->wp_core_, "default-nodes-api")); @@ -392,7 +401,7 @@ void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* r if (success == FALSE) { spdlog::error("[{}]: mixer API load failed", self->name_); - throw std::runtime_error(error->message); + return; } spdlog::debug("[{}]: loaded mixer API", self->name_); @@ -479,12 +488,12 @@ auto waybar::modules::Wireplumber::update() -> void { } if (!tooltipFormat.empty()) { - label_.set_tooltip_text(fmt::format( + label_.set_tooltip_markup(fmt::format( fmt::runtime(tooltipFormat), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_))); } else { - label_.set_tooltip_text(node_name_); + label_.set_tooltip_markup(node_name_); } } @@ -524,9 +533,11 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { } } if (newVol != volume_) { + if (mixer_api_ == nullptr) return true; GVariant* variant = g_variant_new_double(newVol); gboolean ret; g_signal_emit_by_name(mixer_api_, "set-volume", node_id_, variant, &ret); + g_variant_unref(variant); } return true; } diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index d58303da..bde90910 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -198,7 +198,7 @@ void Task::handle_title(const char* title) { title_ = title; hide_if_ignored(); - if (!with_icon_ && !with_name_ || app_info_) { + if ((!with_icon_ && !with_name_) || app_info_) { return; } @@ -437,7 +437,9 @@ void Task::handle_drag_data_get(const Glib::RefPtr& context, void Task::handle_drag_data_received(const Glib::RefPtr& context, int x, int y, Gtk::SelectionData selection_data, guint info, guint time) { spdlog::debug("drag_data_received"); - gpointer handle = *(gpointer*)selection_data.get_data(); + auto* raw = selection_data.get_data(); + if (!raw || selection_data.get_length() < static_cast(sizeof(gpointer))) return; + gpointer handle = *(gpointer*)raw; auto dragged_button = (Gtk::Button*)handle; if (dragged_button == &this->button) return; @@ -509,7 +511,7 @@ void Task::update() { if (markup) button.set_tooltip_markup(txt); else - button.set_tooltip_text(txt); + button.set_tooltip_markup(txt); } } diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 342d40b2..fb368ca5 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -40,12 +40,16 @@ AudioBackend::AudioBackend(std::function on_updated_cb, private_construc } AudioBackend::~AudioBackend() { - if (context_ != nullptr) { - pa_context_disconnect(context_); - } - if (mainloop_ != nullptr) { - mainloop_api_->quit(mainloop_api_, 0); + // Lock the mainloop so we can safely disconnect the context. + // This must be done before stopping the thread. + pa_threaded_mainloop_lock(mainloop_); + if (context_ != nullptr) { + pa_context_disconnect(context_); + pa_context_unref(context_); + context_ = nullptr; + } + pa_threaded_mainloop_unlock(mainloop_); pa_threaded_mainloop_stop(mainloop_); pa_threaded_mainloop_free(mainloop_); } @@ -73,7 +77,14 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { auto* backend = static_cast(data); switch (pa_context_get_state(c)) { case PA_CONTEXT_TERMINATED: - backend->mainloop_api_->quit(backend->mainloop_api_, 0); + // Only quit the mainloop if this is still the active context. + // During reconnection, the old context fires TERMINATED after the new one + // has already been created; quitting in that case would kill the new context. + // Note: context_ is only written from PA callbacks (while the mainloop lock is + // held), so this comparison is safe within any PA callback. + if (backend->context_ == nullptr || backend->context_ == c) { + backend->mainloop_api_->quit(backend->mainloop_api_, 0); + } break; case PA_CONTEXT_READY: pa_context_get_server_info(c, serverInfoCb, data); @@ -88,13 +99,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { nullptr, nullptr); break; case PA_CONTEXT_FAILED: - // When pulseaudio server restarts, the connection is "failed". Try to reconnect. - // pa_threaded_mainloop_lock is already acquired in callback threads. - // So there is no need to lock it again. - if (backend->context_ != nullptr) { - pa_context_disconnect(backend->context_); + if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) { + // When pulseaudio server restarts, the connection is "failed". Try to reconnect. + // pa_threaded_mainloop_lock is already acquired in callback threads. + // So there is no need to lock it again. + if (backend->context_ != nullptr) { + pa_context_disconnect(backend->context_); + pa_context_unref(backend->context_); + backend->context_ = nullptr; + } + backend->connectContext(); } - backend->connectContext(); break; case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: @@ -237,9 +252,10 @@ void AudioBackend::sourceInfoCb(pa_context* /*context*/, const pa_source_info* i */ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, void* data) { auto* backend = static_cast(data); - backend->current_sink_name_ = i->default_sink_name; - backend->default_sink_name = i->default_sink_name; - backend->default_source_name_ = i->default_source_name; + if (i == nullptr) return; + backend->current_sink_name_ = i->default_sink_name ? i->default_sink_name : ""; + backend->default_sink_name = i->default_sink_name ? i->default_sink_name : ""; + backend->default_source_name_ = i->default_source_name ? i->default_source_name : ""; pa_context_get_sink_info_list(context, sinkInfoCb, data); pa_context_get_source_info_list(context, sourceInfoCb, data); @@ -355,6 +371,7 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma } void AudioBackend::toggleSinkMute() { + if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = !muted_; pa_threaded_mainloop_lock(mainloop_); pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, @@ -363,6 +380,7 @@ void AudioBackend::toggleSinkMute() { } void AudioBackend::toggleSinkMute(bool mute) { + if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = mute; pa_threaded_mainloop_lock(mainloop_); pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, @@ -371,7 +389,8 @@ void AudioBackend::toggleSinkMute(bool mute) { } void AudioBackend::toggleSourceMute() { - source_muted_ = !muted_; + if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; + source_muted_ = !source_muted_; pa_threaded_mainloop_lock(mainloop_); pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), nullptr, nullptr); @@ -379,6 +398,7 @@ void AudioBackend::toggleSourceMute() { } void AudioBackend::toggleSourceMute(bool mute) { + if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; source_muted_ = mute; pa_threaded_mainloop_lock(mainloop_); pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp index 48473dd2..61eb9b43 100644 --- a/src/util/backlight_backend.cpp +++ b/src/util/backlight_backend.cpp @@ -79,18 +79,38 @@ static void upsert_device(std::vector& devices, udev_device* de }); if (found != devices.end()) { if (actual != nullptr) { - found->set_actual(std::stoi(actual)); + try { + found->set_actual(std::stoi(actual)); + } catch (const std::exception&) { + } } if (max != nullptr) { - found->set_max(std::stoi(max)); + try { + found->set_max(std::stoi(max)); + } catch (const std::exception&) { + } } if (power != nullptr) { - found->set_powered(std::stoi(power) == 0); + try { + found->set_powered(std::stoi(power) == 0); + } catch (const std::exception&) { + } } } else { - const int actual_int = actual == nullptr ? 0 : std::stoi(actual); - const int max_int = max == nullptr ? 0 : std::stoi(max); - const bool power_bool = power == nullptr ? true : std::stoi(power) == 0; + int actual_int = 0, max_int = 0; + bool power_bool = true; + try { + if (actual != nullptr) actual_int = std::stoi(actual); + } catch (const std::exception&) { + } + try { + if (max != nullptr) max_int = std::stoi(max); + } catch (const std::exception&) { + } + try { + if (power != nullptr) power_bool = std::stoi(power) == 0; + } catch (const std::exception&) { + } devices.emplace_back(name, actual_int, max_int, power_bool); } } @@ -261,6 +281,11 @@ void BacklightBackend::set_brightness(const std::string& preferred_device, Chang void BacklightBackend::set_brightness_internal(const std::string& device_name, int brightness, int max_brightness) { + if (!login_proxy_) { + spdlog::error("Login proxy not available, cannot set brightness"); + return; + } + brightness = std::clamp(brightness, 0, max_brightness); auto call_args = Glib::VariantContainerBase( @@ -273,6 +298,7 @@ int BacklightBackend::get_scaled_brightness(const std::string& preferred_device) GET_BEST_DEVICE(best, (*this), preferred_device); if (best != nullptr) { + if (best->get_max() == 0) return 0; return static_cast(std::round(best->get_actual() * 100.0F / best->get_max())); } diff --git a/src/util/css_reload_helper.cpp b/src/util/css_reload_helper.cpp index 06381d60..9c7b8a1f 100644 --- a/src/util/css_reload_helper.cpp +++ b/src/util/css_reload_helper.cpp @@ -23,7 +23,8 @@ namespace { const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)"); } -waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, std::function callback) +waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, + std::function callback) : m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {} std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) { @@ -88,6 +89,12 @@ void waybar::CssReloadHelper::monitorChanges() { } } +void waybar::CssReloadHelper::changeCssFile(const std::string& newCssFile) { + m_fileMonitors.clear(); + m_cssFile = newCssFile; + monitorChanges(); +} + void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr const& file, Glib::RefPtr const& other_type, Gio::FileMonitorEvent event_type) { @@ -95,7 +102,7 @@ void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr const& fi // fire for one if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { spdlog::debug("Reloading style, file changed: {}", file->get_path()); - m_callback(); + m_callback(m_cssFile); } } @@ -115,11 +122,15 @@ std::vector waybar::CssReloadHelper::parseImports(const std::string auto maxIterations = 100U; do { previousSize = imports.size(); + std::vector to_parse; for (const auto& [file, parsed] : imports) { if (!parsed) { - parseImports(file, imports); + to_parse.push_back(file); } } + for (const auto& file : to_parse) { + parseImports(file, imports); + } } while (imports.size() > previousSize && maxIterations-- > 0); diff --git a/src/util/gtk_icon.cpp b/src/util/gtk_icon.cpp index 73f77284..1b2fca57 100644 --- a/src/util/gtk_icon.cpp +++ b/src/util/gtk_icon.cpp @@ -21,7 +21,6 @@ Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon( const std::lock_guard lock(default_theme_mutex); auto default_theme = Gtk::IconTheme::get_default(); - default_theme->rescan_if_needed(); auto icon_info = default_theme->lookup_icon(name, tmp_size, flags); diff --git a/src/util/icon_loader.cpp b/src/util/icon_loader.cpp index fe534f69..1d0bd067 100644 --- a/src/util/icon_loader.cpp +++ b/src/util/icon_loader.cpp @@ -5,8 +5,11 @@ std::vector IconLoader::search_prefix() { std::vector prefixes = {""}; - std::string home_dir = std::getenv("HOME"); - prefixes.push_back(home_dir + "/.local/share/"); + const char* home_env = std::getenv("HOME"); + std::string home_dir = home_env ? home_env : ""; + if (!home_dir.empty()) { + prefixes.push_back(home_dir + "/.local/share/"); + } auto xdg_data_dirs = std::getenv("XDG_DATA_DIRS"); if (!xdg_data_dirs) { @@ -139,7 +142,7 @@ bool IconLoader::image_load_icon(Gtk::Image& image, const Glib::RefPtrget_width() != scaled_icon_size) { + if (pixbuf->get_width() != scaled_icon_size && pixbuf->get_height() > 0) { int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); } diff --git a/src/util/pipewire/pipewire_backend.cpp b/src/util/pipewire/pipewire_backend.cpp index a88a3fbe..82ec0c0f 100644 --- a/src/util/pipewire/pipewire_backend.cpp +++ b/src/util/pipewire/pipewire_backend.cpp @@ -54,11 +54,17 @@ PipewireBackend::PipewireBackend(PrivateConstructorTag tag) context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0); if (context_ == nullptr) { pw_thread_loop_unlock(mainloop_); + pw_thread_loop_destroy(mainloop_); + mainloop_ = nullptr; throw std::runtime_error("pa_context_new() failed."); } core_ = pw_context_connect(context_, nullptr, 0); if (core_ == nullptr) { pw_thread_loop_unlock(mainloop_); + pw_context_destroy(context_); + context_ = nullptr; + pw_thread_loop_destroy(mainloop_); + mainloop_ = nullptr; throw std::runtime_error("pw_context_connect() failed"); } registry_ = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0); @@ -136,7 +142,10 @@ void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permission pw_proxy_add_object_listener(proxy, &pNodeInfo->object_listener, &NODE_EVENTS, pNodeInfo); - privacy_nodes.insert_or_assign(id, pNodeInfo); + { + std::lock_guard lock(mutex_); + privacy_nodes.insert_or_assign(id, pNodeInfo); + } } void PipewireBackend::handleRegistryEventGlobalRemove(uint32_t id) { diff --git a/src/util/portal.cpp b/src/util/portal.cpp index 6df2a6b6..e2be97c7 100644 --- a/src/util/portal.cpp +++ b/src/util/portal.cpp @@ -58,21 +58,27 @@ void waybar::Portal::refreshAppearance() { // xdg-desktop-portal 1.17 will fix this issue with a new `ReadOne` method, // but this version is not yet released. // TODO(xdg-desktop-portal v1.17): switch to ReadOne - auto container = Glib::VariantBase::cast_dynamic(response); - Glib::VariantBase modev; - container.get_child(modev, 0); - auto mode = - Glib::VariantBase::cast_dynamic>>>(modev) - .get() - .get() - .get(); - auto newMode = Appearance(mode); - if (newMode == currentMode) { + try { + auto container = Glib::VariantBase::cast_dynamic(response); + Glib::VariantBase modev; + container.get_child(modev, 0); + auto mode = + Glib::VariantBase::cast_dynamic>>>( + modev) + .get() + .get() + .get(); + auto newMode = Appearance(mode); + if (newMode == currentMode) { + return; + } + spdlog::info("Discovered appearance '{}'", newMode); + currentMode = newMode; + m_signal_appearance_changed.emit(currentMode); + } catch (const std::bad_cast& e) { + spdlog::error("Unexpected appearance variant format: {}", e.what()); return; } - spdlog::info("Discovered appearance '{}'", newMode); - currentMode = newMode; - m_signal_appearance_changed.emit(currentMode); } waybar::Appearance waybar::Portal::getAppearance() { return currentMode; }; diff --git a/src/util/transform_8bit_to_rgba.cpp b/src/util/transform_8bit_to_rgba.cpp new file mode 100644 index 00000000..dc87a88e --- /dev/null +++ b/src/util/transform_8bit_to_rgba.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +namespace fs = std::filesystem; + +struct TransformResult { + std::string css; + bool was_transformed; +}; + +TransformResult transform_8bit_to_hex(const std::string& file_path) { + std::ifstream f(file_path, std::ios::in | std::ios::binary); + const auto size = fs::file_size(file_path); + std::string result(size, '\0'); + if (!f.is_open() || !f.good()) { + throw std::runtime_error("Cannot open file: " + file_path); + } + + if (size == 0) { + return {.css = result, .was_transformed = false}; + } + + f.read(result.data(), size); + + static std::regex pattern( + R"(\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2}))"); + std::string final_output; + + auto it = std::sregex_iterator(result.begin(), result.end(), pattern); + auto eof = std::sregex_iterator(); + + if (it == eof) { + return {.css = result, .was_transformed = false}; + } + + std::smatch match; + while (it != eof) { + match = *it; + + final_output += match.prefix().str(); + + int r = stoi(match[1].str(), nullptr, 16); + int g = stoi(match[2].str(), nullptr, 16); + int b = stoi(match[3].str(), nullptr, 16); + double a = (stoi(match[4].str(), nullptr, 16) / 255.0); + + std::stringstream ss; + ss << "rgba(" << r << "," << g << "," << b << "," << std::fixed << std::setprecision(2) << a + << ")"; + final_output += ss.str(); + + ++it; + } + + final_output += match.suffix().str(); + + return {.css = final_output, .was_transformed = true}; +} diff --git a/src/util/utf8_string.cpp b/src/util/utf8_string.cpp new file mode 100644 index 00000000..f96f2bd1 --- /dev/null +++ b/src/util/utf8_string.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include + +namespace waybar::util { + +namespace { +// Wide characters count as two, zero-width characters count as zero +// Modifies str in-place (unless width = std::string::npos) +// Returns the total width of the string pre-truncating +size_t measure_and_truncate(std::string& str, size_t width = std::string::npos) { + if (str.length() == 0) return 0; + + const gchar* trunc_end = nullptr; + + size_t total_width = 0; + + for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) { + gunichar c = g_utf8_get_char_validated(data, end - data); + if (c == -1U || c == -2U) { + // invalid unicode, treat string as ascii + if (width != std::string::npos && str.length() > width) str.resize(width); + return str.length(); + } else if (g_unichar_iswide(c)) { + total_width += 2; + } else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen + total_width += 1; + } + + data = g_utf8_find_next_char(data, end); + if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c)) + trunc_end = data; + } + + if (trunc_end) str.resize(trunc_end - str.data()); + + return total_width; +} +} // namespace + +size_t utf8_width(const std::string& str) { + return measure_and_truncate(const_cast(str)); +} + +void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len) { + if (max_len == 0) { + s.resize(0); + return; + } + size_t len = measure_and_truncate(s, max_len); + if (len > max_len) { + size_t ellipsis_len = utf8_width(ellipsis); + if (max_len >= ellipsis_len) { + if (ellipsis_len) measure_and_truncate(s, max_len - ellipsis_len); + s += ellipsis; + } else { + s.resize(0); + } + } +} +} // namespace waybar::util diff --git a/subprojects/libcava.wrap b/subprojects/libcava.wrap index 466e2262..d66d657b 100644 --- a/subprojects/libcava.wrap +++ b/subprojects/libcava.wrap @@ -4,9 +4,9 @@ #depth = 1 [wrap-file] -directory = cava-0.10.7-beta -source_url = https://github.com/LukashonakV/cava/archive/v0.10.7-beta.tar.gz +directory = cava-0.10.7 +source_url = https://github.com/LukashonakV/cava/archive/0.10.7.tar.gz source_filename = cava-0.10.7.tar.gz -source_hash = 8915d7214f2046554c158fe6f2ae518881dfb573e421ea848727be11a5dfa8c4 +source_hash = 50cc6413e9c96c503657f814744a2baf429a24ff9fed31a8343e0ed285269eff [provide] libcava = cava_dep diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp index b83b839c..f2e10dae 100644 --- a/test/hyprland/backend.cpp +++ b/test/hyprland/backend.cpp @@ -4,56 +4,211 @@ #include #endif -#include "fixtures/IPCTestFixture.hpp" +#include + +#include "modules/hyprland/backend.hpp" namespace fs = std::filesystem; namespace hyprland = waybar::modules::hyprland; -TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirExists", "[getSocketFolder]") { +namespace { +class IPCTestHelper : public hyprland::IPC { + public: + static void resetSocketFolder() { socketFolder_.clear(); } + static void resetLuaProtocolDetection() { s_luaProtocolDetected_.reset(); } + static void setLuaProtocolDetected(bool value) { s_luaProtocolDetected_ = value; } + using hyprland::IPC::buildLuaDispatch; + using hyprland::IPC::isLuaProtocol; +}; + +std::size_t countOpenFds() { +#if defined(__linux__) + std::size_t count = 0; + for (const auto& _ : fs::directory_iterator("/proc/self/fd")) { + (void)_; + ++count; + } + return count; +#else + return 0; +#endif +} +} // namespace + +TEST_CASE("XDGRuntimeDirExists", "[getSocketFolder]") { // Test case: XDG_RUNTIME_DIR exists and contains "hypr" directory // Arrange - tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + constexpr auto instanceSig = "instance_sig"; + const fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + std::error_code ec; + fs::remove_all(tempDir, ec); fs::path expectedPath = tempDir / "hypr" / instanceSig; - fs::create_directories(tempDir / "hypr" / instanceSig); + fs::create_directories(expectedPath); setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); + IPCTestHelper::resetSocketFolder(); // Act - fs::path actualPath = getSocketFolder(instanceSig); + fs::path actualPath = hyprland::IPC::getSocketFolder(instanceSig); // Assert expected result REQUIRE(actualPath == expectedPath); + fs::remove_all(tempDir, ec); } -TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirDoesNotExist", "[getSocketFolder]") { +TEST_CASE("XDGRuntimeDirDoesNotExist", "[getSocketFolder]") { // Test case: XDG_RUNTIME_DIR does not exist // Arrange + constexpr auto instanceSig = "instance_sig"; unsetenv("XDG_RUNTIME_DIR"); fs::path expectedPath = fs::path("/tmp") / "hypr" / instanceSig; + IPCTestHelper::resetSocketFolder(); // Act - fs::path actualPath = getSocketFolder(instanceSig); + fs::path actualPath = hyprland::IPC::getSocketFolder(instanceSig); // Assert expected result REQUIRE(actualPath == expectedPath); } -TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirExistsNoHyprDir", "[getSocketFolder]") { +TEST_CASE("XDGRuntimeDirExistsNoHyprDir", "[getSocketFolder]") { // Test case: XDG_RUNTIME_DIR exists but does not contain "hypr" directory // Arrange + constexpr auto instanceSig = "instance_sig"; fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + std::error_code ec; + fs::remove_all(tempDir, ec); fs::create_directories(tempDir); setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); fs::path expectedPath = fs::path("/tmp") / "hypr" / instanceSig; + IPCTestHelper::resetSocketFolder(); // Act - fs::path actualPath = getSocketFolder(instanceSig); + fs::path actualPath = hyprland::IPC::getSocketFolder(instanceSig); // Assert expected result REQUIRE(actualPath == expectedPath); + fs::remove_all(tempDir, ec); } -TEST_CASE_METHOD(IPCTestFixture, "getSocket1Reply throws on no socket", "[getSocket1Reply]") { +TEST_CASE("Socket folder is resolved per instance signature", "[getSocketFolder]") { + const fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + std::error_code ec; + fs::remove_all(tempDir, ec); + fs::create_directories(tempDir / "hypr"); + setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); + IPCTestHelper::resetSocketFolder(); + + const auto firstPath = hyprland::IPC::getSocketFolder("instance_a"); + const auto secondPath = hyprland::IPC::getSocketFolder("instance_b"); + + REQUIRE(firstPath == tempDir / "hypr" / "instance_a"); + REQUIRE(secondPath == tempDir / "hypr" / "instance_b"); + REQUIRE(firstPath != secondPath); + + fs::remove_all(tempDir, ec); +} + +TEST_CASE("getSocket1Reply throws on no socket", "[getSocket1Reply]") { + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + IPCTestHelper::resetSocketFolder(); std::string request = "test_request"; - CHECK_THROWS(getSocket1Reply(request)); + CHECK_THROWS(hyprland::IPC::getSocket1Reply(request)); +} + +#if defined(__linux__) +TEST_CASE("getSocket1Reply failure paths do not leak fds", "[getSocket1Reply][fd-leak]") { + const auto baseline = countOpenFds(); + + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + for (int i = 0; i < 16; ++i) { + IPCTestHelper::resetSocketFolder(); + CHECK_THROWS(hyprland::IPC::getSocket1Reply("test_request")); + } + const auto after_missing_signature = countOpenFds(); + REQUIRE(after_missing_signature == baseline); + + setenv("HYPRLAND_INSTANCE_SIGNATURE", "definitely-not-running", 1); + for (int i = 0; i < 16; ++i) { + IPCTestHelper::resetSocketFolder(); + CHECK_THROWS(hyprland::IPC::getSocket1Reply("test_request")); + } + const auto after_connect_failures = countOpenFds(); + REQUIRE(after_connect_failures == baseline); +} +#endif + +// --- Tests for new Lua IPC dispatch functions --- + +TEST_CASE("buildLuaDispatch workspace", "[buildLuaDispatch]") { + SECTION("numeric workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "1"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"1\" })"); + } + SECTION("named workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "name:term"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"name:term\" })"); + } + SECTION("relative workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "e+1"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"e+1\" })"); + } +} + +TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch]") { + auto result = + IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3"); + REQUIRE( + result == + "/dispatch hl.dsp.focus({ workspace = \"3\", on_current_monitor = true })"); +} + +TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") { + SECTION("with name") { + auto result = + IPCTestHelper::buildLuaDispatch("togglespecialworkspace", "scratchpad"); + REQUIRE(result == + "/dispatch hl.dsp.workspace.toggle_special(\"scratchpad\")"); + } + SECTION("empty arg") { + auto result = + IPCTestHelper::buildLuaDispatch("togglespecialworkspace", ""); + REQUIRE(result == "/dispatch hl.dsp.workspace.toggle_special()"); + } +} + +TEST_CASE("buildLuaDispatch unknown dispatcher fallback", "[buildLuaDispatch]") { + auto result = + IPCTestHelper::buildLuaDispatch("unknown_dispatcher", "some_arg"); + REQUIRE(result == + "/dispatch hl.dsp.unknown_dispatcher(\"some_arg\")"); +} + +TEST_CASE("dispatch throws when Hyprland is not running", "[dispatch]") { + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + IPCTestHelper::resetSocketFolder(); + IPCTestHelper::resetLuaProtocolDetection(); + + CHECK_THROWS(hyprland::IPC::dispatch("workspace", "1")); +} + +TEST_CASE("isLuaProtocol uses cached value and avoids socket call", + "[isLuaProtocol]") { + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + IPCTestHelper::resetSocketFolder(); + + SECTION("cached false") { + IPCTestHelper::setLuaProtocolDetected(false); + // Should return false without throwing (no socket call needed) + REQUIRE(IPCTestHelper::isLuaProtocol() == false); + } + + SECTION("cached true") { + IPCTestHelper::setLuaProtocolDetected(true); + // Should return true without throwing (no socket call needed) + REQUIRE(IPCTestHelper::isLuaProtocol() == true); + } + + // Cleanup: reset detection so other tests aren't affected + IPCTestHelper::resetLuaProtocolDetection(); } diff --git a/test/hyprland/fixtures/IPCTestFixture.hpp b/test/hyprland/fixtures/IPCTestFixture.hpp deleted file mode 100644 index caa92975..00000000 --- a/test/hyprland/fixtures/IPCTestFixture.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "modules/hyprland/backend.hpp" - -namespace fs = std::filesystem; -namespace hyprland = waybar::modules::hyprland; - -class IPCTestFixture : public hyprland::IPC { - public: - IPCTestFixture() : IPC() { IPC::socketFolder_ = ""; } - ~IPCTestFixture() { fs::remove_all(tempDir); } - - protected: - const char* instanceSig = "instance_sig"; - fs::path tempDir = fs::temp_directory_path() / "hypr_test"; - - private: -}; - -class IPCMock : public IPCTestFixture { - public: - // Mock getSocket1Reply to return an empty string - static std::string getSocket1Reply(const std::string& rq) { return ""; } - - protected: - const char* instanceSig = "instance_sig"; -}; diff --git a/test/utils/SafeSignal.cpp b/test/utils/SafeSignal.cpp index e7e096b0..1502b7d9 100644 --- a/test/utils/SafeSignal.cpp +++ b/test/utils/SafeSignal.cpp @@ -9,6 +9,7 @@ #endif #include #include +#include #include "fixtures/GlibTestsFixture.hpp" @@ -141,3 +142,33 @@ TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal copy/move counter", "[signal][thr producer.join(); REQUIRE(count == NUM_EVENTS); } + +TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal queue stays bounded under burst load", + "[signal][thread][util][perf]") { + constexpr int NUM_EVENTS = 200; + constexpr std::size_t MAX_QUEUED_EVENTS = 8; + std::vector received; + + SafeSignal test_signal; + test_signal.set_max_queued_events(MAX_QUEUED_EVENTS); + + setTimeout(500); + + test_signal.connect([&](auto value) { received.push_back(value); }); + + run([&]() { + std::thread producer([&]() { + for (int i = 1; i <= NUM_EVENTS; ++i) { + test_signal.emit(i); + } + }); + producer.join(); + + Glib::signal_timeout().connect_once([this]() { this->quit(); }, 50); + }); + + REQUIRE(received.size() <= MAX_QUEUED_EVENTS); + REQUIRE_FALSE(received.empty()); + REQUIRE(received.back() == NUM_EVENTS); + REQUIRE(received.front() == NUM_EVENTS - static_cast(received.size()) + 1); +} diff --git a/test/utils/command.cpp b/test/utils/command.cpp new file mode 100644 index 00000000..053a2b77 --- /dev/null +++ b/test/utils/command.cpp @@ -0,0 +1,57 @@ +#if __has_include() +#include +#else +#include +#endif + +#include +#include + +#include +#include +#include + +std::mutex reap_mtx; +std::list reap; + +extern "C" int waybar_test_execl(const char* path, const char* arg, ...); +extern "C" int waybar_test_execlp(const char* file, const char* arg, ...); + +#define execl waybar_test_execl +#define execlp waybar_test_execlp +#include "util/command.hpp" +#undef execl +#undef execlp + +extern "C" int waybar_test_execl(const char* path, const char* arg, ...) { + (void)path; + (void)arg; + errno = ENOENT; + return -1; +} + +extern "C" int waybar_test_execlp(const char* file, const char* arg, ...) { + (void)file; + (void)arg; + errno = ENOENT; + return -1; +} + +TEST_CASE("command::execNoRead returns 127 when shell exec fails", "[util][command]") { + const auto result = waybar::util::command::execNoRead("echo should-not-run"); + REQUIRE(result.exit_code == waybar::util::command::kExecFailureExitCode); + REQUIRE(result.out.empty()); +} + +TEST_CASE("command::forkExec child exits 127 when shell exec fails", "[util][command]") { + const auto pid = waybar::util::command::forkExec("echo should-not-run", "test-output"); + REQUIRE(pid > 0); + + int status = -1; + REQUIRE(waitpid(pid, &status, 0) == pid); + REQUIRE(WIFEXITED(status)); + REQUIRE(WEXITSTATUS(status) == waybar::util::command::kExecFailureExitCode); + + std::scoped_lock lock(reap_mtx); + reap.remove(pid); +} diff --git a/test/utils/css_reload_helper.cpp b/test/utils/css_reload_helper.cpp index f3888a83..0ba0bde4 100644 --- a/test/utils/css_reload_helper.cpp +++ b/test/utils/css_reload_helper.cpp @@ -10,7 +10,7 @@ class CssReloadHelperTest : public waybar::CssReloadHelper { public: - CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this]() { callback(); }) {} + CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this](const std::string&) { callback(); }) {} void callback() { m_callbackCounter++; } diff --git a/test/utils/meson.build b/test/utils/meson.build index b7b3665a..e8dd37fa 100644 --- a/test/utils/meson.build +++ b/test/utils/meson.build @@ -13,6 +13,8 @@ test_src = files( '../../src/config.cpp', 'JsonParser.cpp', 'SafeSignal.cpp', + 'sleeper_thread.cpp', + 'command.cpp', 'css_reload_helper.cpp', '../../src/util/css_reload_helper.cpp', ) diff --git a/test/utils/sleeper_thread.cpp b/test/utils/sleeper_thread.cpp new file mode 100644 index 00000000..9e1a1ef3 --- /dev/null +++ b/test/utils/sleeper_thread.cpp @@ -0,0 +1,80 @@ +#if __has_include() +#include +#else +#include +#endif + +#include +#include + +#include +#include + +#include "util/sleeper_thread.hpp" + +namespace waybar::util { +SafeSignal& prepare_for_sleep() { + static SafeSignal signal; + return signal; +} +} // namespace waybar::util + +namespace { +int run_in_subprocess(int (*task)()) { + const auto pid = fork(); + if (pid < 0) { + return -1; + } + if (pid == 0) { + alarm(5); + _exit(task()); + } + + int status = -1; + if (waitpid(pid, &status, 0) != pid) { + return -1; + } + if (!WIFEXITED(status)) { + return -1; + } + return WEXITSTATUS(status); +} + +int run_reassignment_regression() { + waybar::util::SleeperThread thread; + thread = [] { std::this_thread::sleep_for(std::chrono::milliseconds(10)); }; + thread = [] { std::this_thread::sleep_for(std::chrono::milliseconds(1)); }; + return 0; +} + +int run_control_flag_stress() { + for (int i = 0; i < 200; ++i) { + waybar::util::SleeperThread thread; + thread = [&thread] { thread.sleep_for(std::chrono::milliseconds(1)); }; + + std::thread waker([&thread] { + for (int j = 0; j < 100; ++j) { + thread.wake_up(); + std::this_thread::yield(); + } + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + thread.stop(); + waker.join(); + if (thread.isRunning()) { + return 1; + } + } + return 0; +} +} // namespace + +TEST_CASE("SleeperThread reassignment does not terminate process", "[util][sleeper_thread]") { + REQUIRE(run_in_subprocess(run_reassignment_regression) == 0); +} + +TEST_CASE("SleeperThread control flags are stable under concurrent wake and stop", + "[util][sleeper_thread]") { + REQUIRE(run_in_subprocess(run_control_flag_stress) == 0); +}