diff --git a/README.md b/README.md index 9ca041d1..687f8234 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - River (Mapping mode, Tags, Focused window name) - Hyprland (Window Icons, Workspaces, Focused window name) - Niri (Workspaces, Focused window name, Language) +- Mango (Workspaces, Focused window name, Language, Keymode) - DWL (Tags, Focused window name) [requires dwl ipc patch](https://codeberg.org/dwl/dwl-patches/src/branch/main/patches/ipc) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time diff --git a/include/AGraph.hpp b/include/AGraph.hpp new file mode 100644 index 00000000..e2ada835 --- /dev/null +++ b/include/AGraph.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "AModule.hpp" + +namespace waybar { + +enum class GraphType { LINE, BAR, GAUGE }; + +class AGraph : public AModule { + public: + AGraph(const Json::Value&, const std::string&, const std::string&, uint16_t interval = 0, + bool enable_click = false, bool enable_scroll = false); + virtual ~AGraph() = default; + auto update() -> void override; + + protected: + Gtk::DrawingArea graph_; + std::deque values_; + uint16_t datapoints_ = 20; + GraphType graph_type_ = GraphType::LINE; + + void addValue(const int n); + + const std::chrono::seconds interval_; + + bool onDraw(const Cairo::RefPtr& cr); + + std::map submenus_; + std::map menuActionsMap_; + static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data); + + private: + void drawFilledArea(const Cairo::RefPtr& cr, + const std::vector>& points, double height, + const Gdk::RGBA& bg_color); + + void drawLine(const Cairo::RefPtr& cr, + const std::vector>& points, const Gdk::RGBA& fg_color); + + void drawPath(const Cairo::RefPtr& cr, + const std::vector>& points); + + void drawBars(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color); + + void drawGauge(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color); +}; + +} // namespace waybar diff --git a/include/AIconLabel.hpp b/include/AIconLabel.hpp index f0fb8614..80aa431a 100644 --- a/include/AIconLabel.hpp +++ b/include/AIconLabel.hpp @@ -14,10 +14,14 @@ class AIconLabel : public ALabel { bool enable_click = false, bool enable_scroll = false); virtual ~AIconLabel() = default; auto update() -> void override; + static std::tuple extractIcon(const std::string& input); protected: Gtk::Image image_; Gtk::Box box_; + unsigned app_icon_size_{24}; + + bool label_contains_icon{false}; bool iconEnabled() const; }; diff --git a/include/ALabel.hpp b/include/ALabel.hpp index b3852877..4b04c00d 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -4,6 +4,8 @@ #include #include +#include + #include "AModule.hpp" namespace waybar { @@ -25,12 +27,20 @@ class ALabel : public AModule { bool alt_ = false; std::string default_format_; + bool setLabelMarkup(const Glib::ustring& markup); + bool setTooltipMarkup(const Glib::ustring& markup); + bool handleToggle(GdkEventButton* const& e) override; + void copyToClipboard(const std::string&); virtual std::string getState(uint8_t value, bool lesser = false); std::map submenus_; std::map menuActionsMap_; static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data); + + private: + std::optional last_label_markup_; + std::optional last_tooltip_markup_; }; } // namespace waybar diff --git a/include/AModule.hpp b/include/AModule.hpp index a338ffe3..6eafde5b 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 @@ -41,13 +45,15 @@ class AModule : public IModule { const Json::Value& config_; Gtk::EventBox event_box_; - virtual void setCursor(Gdk::CursorType const& c); + virtual void setCursor(std::string const& c); virtual bool handleToggle(GdkEventButton* const& ev); virtual bool handleMouseEnter(GdkEventCrossing* const& ev); virtual bool handleMouseLeave(GdkEventCrossing* const& ev); virtual bool handleScroll(GdkEventScroll*); virtual bool handleRelease(GdkEventButton* const& ev); + + bool disable_on_sleep_{false}; GObject* menu_ = nullptr; private: @@ -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"}, @@ -78,7 +85,8 @@ class AModule : public IModule { {std::make_pair(9, GdkEventType::GDK_BUTTON_PRESS), "on-click-forward"}, {std::make_pair(9, GdkEventType::GDK_BUTTON_RELEASE), "on-click-forward-release"}, {std::make_pair(9, GdkEventType::GDK_2BUTTON_PRESS), "on-double-click-forward"}, - {std::make_pair(9, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click-forward"}}; + {std::make_pair(9, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click-forward"}, + {std::make_pair(10, GdkEventType::GDK_BUTTON_PRESS), "on-click-copy"}}; }; } // namespace waybar diff --git a/include/bar.hpp b/include/bar.hpp index 54b332bf..783dd282 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; @@ -99,6 +101,7 @@ class Bar : public sigc::trackable { void setMode(const bar_mode&); void setPassThrough(bool passthrough); void setPosition(Gtk::PositionType position); + void forceLayerCommit(); void onConfigure(GdkEventConfigure* ev); void configureGlobalOffset(int width, int height); void onOutputGeometryChanged(); diff --git a/include/client.hpp b/include/client.hpp index c3df3f21..09f9090f 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -12,6 +12,7 @@ struct zwp_idle_inhibitor_v1; struct zwp_idle_inhibit_manager_v1; +struct ext_idle_notifier_v1; namespace waybar { @@ -27,6 +28,7 @@ class Client { struct wl_registry* registry = nullptr; struct zxdg_output_manager_v1* xdg_output_manager = nullptr; struct zwp_idle_inhibit_manager_v1* idle_inhibit_manager = nullptr; + struct ext_idle_notifier_v1* idle_notifier = nullptr; std::vector> bars; Config config; std::string bar_id; @@ -44,6 +46,7 @@ class Client { const char* interface, uint32_t version); static void handleGlobalRemove(void* data, struct wl_registry* registry, uint32_t name); static void handleOutputDone(void*, struct zxdg_output_v1*); + void createBarsBatch(); static void handleOutputName(void*, struct zxdg_output_v1*, const char*); static void handleOutputDescription(void*, struct zxdg_output_v1*, const char*); void handleMonitorAdded(Glib::RefPtr monitor); @@ -58,6 +61,8 @@ class Client { std::string m_cssFile; sigc::connection monitor_added_connection_; sigc::connection monitor_removed_connection_; + std::list pending_outputs_; + bool bars_scheduled_ = false; }; } // namespace waybar diff --git a/include/group.hpp b/include/group.hpp index d5cbb289..c5faf7c3 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -10,6 +10,8 @@ namespace waybar { class Group : public AModule { + sigc::connection reveal_timeout_; + public: Group(const std::string&, const std::string&, const Json::Value&, bool); ~Group() override = default; @@ -26,6 +28,7 @@ class Group : public AModule { bool is_first_widget = true; bool is_drawer = false; bool click_to_reveal = false; + int reveal_delay = 0; std::string add_class_to_drawer_children; bool handleMouseEnter(GdkEventCrossing* const& ev) override; bool handleMouseLeave(GdkEventCrossing* const& ev) override; diff --git a/include/modules/battery.hpp b/include/modules/battery.hpp index 09c172b0..fb6d092f 100644 --- a/include/modules/battery.hpp +++ b/include/modules/battery.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -51,6 +52,11 @@ class Battery : public ALabel { bool warnFirstTime_{true}; bool weightedAverage_{true}; const Bar& bar_; + bool smoothPowerEnable_{false}; + double time_constant_s_{260.0}; + double smooth_power_{0.0}; // µW + std::chrono::steady_clock::time_point last_t_{std::chrono::steady_clock::now()}; + std::string old_status_raw_{""}; util::SleeperThread thread_; util::SleeperThread thread_battery_update_; diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index b89383a0..5e03b118 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -41,6 +41,7 @@ class Bluetooth : public ALabel { bool services_resolved; // NOTE: experimental feature in bluez std::optional battery_percentage; + std::optional battery_percentage_peripheral; }; public: @@ -59,6 +60,12 @@ class Bluetooth : public ALabel { gpointer) -> void; auto getDeviceBatteryPercentage(GDBusObject*) -> std::optional; + auto getDeviceGattBatteryLevels(GDBusObject*, std::optional&, + std::optional&) -> void; + static auto processBatteryServiceCharacteristics(GList*, const std::string&, const std::string&, + const std::string&, + std::optional&, + std::optional&) -> void; auto getDeviceProperties(GDBusObject*, DeviceInfo&) -> bool; auto getControllerProperties(GDBusObject*, ControllerInfo&) -> bool; diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index c3548063..5fd9df54 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -80,6 +80,7 @@ class Clock final : public ALabel { void cldShift_reset(); void tz_up(); void tz_down(); + void action_exec(const std::string& action); // Module Action Map static inline std::map actionMap_{ {"mode", &waybar::modules::Clock::cldModeSwitch}, @@ -88,6 +89,9 @@ class Clock final : public ALabel { {"shift_reset", &waybar::modules::Clock::cldShift_reset}, {"tz_up", &waybar::modules::Clock::tz_up}, {"tz_down", &waybar::modules::Clock::tz_down}}; + static inline std::map + actionWithArgsMap_{{"exec", &waybar::modules::Clock::action_exec}}; }; } // namespace waybar::modules diff --git a/include/modules/cpu_graph.hpp b/include/modules/cpu_graph.hpp new file mode 100644 index 00000000..cf74a3fe --- /dev/null +++ b/include/modules/cpu_graph.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "AGraph.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CpuGraph : public AGraph { + public: + CpuGraph(const std::string&, const Json::Value&); + virtual ~CpuGraph() = default; + auto update() -> void override; + + private: + static constexpr const char* MODERATE_CLASS = "cpu-moderate"; + static constexpr const char* HIGH_CLASS = "cpu-high"; + static constexpr const char* INTENSIVE_CLASS = "cpu-intensive"; + + std::vector> prev_times_; + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index a345a33b..442b1c37 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -5,14 +5,14 @@ #include #include -#include "ALabel.hpp" +#include "AIconLabel.hpp" #include "util/command.hpp" #include "util/json.hpp" #include "util/sleeper_thread.hpp" namespace waybar::modules { -class Custom : public ALabel { +class Custom : public AIconLabel { public: Custom(const std::string&, const std::string&, const Json::Value&, const std::string&); virtual ~Custom(); @@ -36,6 +36,9 @@ class Custom : public ALabel { std::string alt_; std::string tooltip_; std::string last_tooltip_markup_; + std::string image_path_; + std::string image_name_; + unsigned app_icon_size_{24}; const bool tooltip_format_enabled_; std::vector class_; int percentage_; diff --git a/include/modules/custom_graph.hpp b/include/modules/custom_graph.hpp new file mode 100644 index 00000000..a081f376 --- /dev/null +++ b/include/modules/custom_graph.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include + +#include "AGraph.hpp" +#include "util/command.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CustomGraph : public AGraph { + public: + CustomGraph(const std::string&, const std::string&, const Json::Value&, const std::string&); + virtual ~CustomGraph(); + auto update() -> void override; + void refresh(int /*signal*/) override; + + private: + void delayWorker(); + void continuousWorker(); + void waitingWorker(); + void parseOutputRaw(); + void parseOutputJson(); + void handleEvent(); + bool handleScroll(GdkEventScroll* e) override; + bool handleToggle(GdkEventButton* const& e) override; + + const std::string name_; + const std::string output_name_; + std::string text_; + std::string id_; + std::string alt_; + std::string tooltip_; + const bool tooltip_format_enabled_; + std::vector class_; + int percentage_; + FILE* fp_; + int pid_; + util::command::res output_; + util::JsonParser parser_; + + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/include/modules/disk.hpp b/include/modules/disk.hpp index b576e19f..1ebd1907 100644 --- a/include/modules/disk.hpp +++ b/include/modules/disk.hpp @@ -4,6 +4,7 @@ #include #include +#include #include "ALabel.hpp" #include "util/format.hpp" @@ -19,7 +20,9 @@ class Disk : public ALabel { private: util::SleeperThread thread_; - std::string path_; + std::string header_; + std::vector paths_; + std::string separator_; std::string unit_; float calc_specific_divisor(const std::string& divisor); diff --git a/include/modules/dwl/tags.hpp b/include/modules/dwl/tags.hpp index 856a35dd..4fee30fb 100644 --- a/include/modules/dwl/tags.hpp +++ b/include/modules/dwl/tags.hpp @@ -21,6 +21,8 @@ class Tags : public waybar::AModule { void handle_primary_clicked(uint32_t tag); bool handle_button_press(GdkEventButton* event_button, uint32_t tag); + void handle_active_output(zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active); + struct zdwl_ipc_manager_v2* status_manager_; struct wl_seat* seat_; @@ -28,6 +30,7 @@ class Tags : public waybar::AModule { const waybar::Bar& bar_; Gtk::Box box_; std::vector buttons_; + bool hide_vacant_; struct zdwl_ipc_output_v2* output_status_; }; diff --git a/include/modules/dwl/window.hpp b/include/modules/dwl/window.hpp index 5efd0abb..e731e8e4 100644 --- a/include/modules/dwl/window.hpp +++ b/include/modules/dwl/window.hpp @@ -19,6 +19,7 @@ class Window : public AAppIconLabel, public sigc::trackable { void handle_layout(const uint32_t layout); void handle_title(const char* title); void handle_appid(const char* ppid); + void handle_active(const uint32_t active); void handle_layout_symbol(const char* layout_symbol); void handle_frame(); @@ -30,6 +31,9 @@ class Window : public AAppIconLabel, public sigc::trackable { std::string title_; std::string appid_; std::string layout_symbol_; + bool active_; + bool hide_inactive_; + bool hide_empty_; uint32_t layout_; struct zdwl_ipc_output_v2* output_status_; diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index ec59e5c3..199356c1 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -30,6 +30,8 @@ class Language : public waybar::ALabel, public EventHandler { std::string short_description; }; + auto removeXkbLayoutCssClass() -> void; + auto addXkbLayoutCssClass() -> void; static auto getLayout(const std::string&) -> Layout; std::mutex mutex_; diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 1d80d331..cd3485c5 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -30,6 +30,7 @@ class Workspace { public: explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, const Json::Value& clients_data = Json::Value::nullRef); + ~Workspace(); std::string& selectIcon(std::map& icons_map); Gtk::Button& button() { return m_button; }; @@ -45,6 +46,15 @@ class Workspace { bool isUrgent() const { return m_isUrgent; }; bool handleClicked(GdkEventButton* bt) const; + + bool handleEnter(GdkEventCrossing* event); + bool handleLeave(GdkEventCrossing* event); + + void startHoverCheck(); + void stopHoverCheck(); + bool syncHoverClass(); + bool pointerInsideButton(); + void setActive(bool value = true) { m_isActive = value; }; void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; @@ -71,6 +81,7 @@ class Workspace { int m_id; std::string m_name; + std::string m_prevNameClass; std::string m_output; uint m_windows; bool m_isActive = false; @@ -80,6 +91,8 @@ class Workspace { bool m_isUrgent = false; bool m_isVisible = false; + sigc::connection m_hoverCheckConnection; + std::vector m_windowMap; Gtk::Button m_button; diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index be5bc66d..d91a43f3 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -39,6 +39,7 @@ class Workspaces : public AModule, public EventHandler { auto allOutputs() const -> bool { return m_allOutputs; } auto showSpecial() const -> bool { return m_showSpecial; } auto activeOnly() const -> bool { return m_activeOnly; } + auto hideActive() const -> bool { return m_hideActive; } auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } auto persistentOnly() const -> bool { return m_persistentOnly; } auto moveToMonitor() const -> bool { return m_moveToMonitor; } @@ -56,12 +57,15 @@ class Workspaces : public AModule, public EventHandler { auto taskbarReverseDirection() const -> bool { return m_taskbarReverseDirection; } auto onClickWindow() const -> std::string { return m_onClickWindow; } auto getIgnoredWindows() const -> std::vector { return m_ignoreWindows; } + auto maxWindows() const -> int { return m_maxWindows; } enum class ActiveWindowPosition { NONE, FIRST, LAST }; auto activeWindowPosition() const -> ActiveWindowPosition { return m_activeWindowPosition; } std::string getRewrite(const std::string& window_class, const std::string& window_title); std::string& getWindowSeparator() { return m_formatWindowSeparator; } + auto windowRewriteGroupThreshold() const -> int { return m_windowRewriteGroupThreshold; } + auto const& getWindowRewriteGroupFormat() const { return m_windowRewriteGroupFormat; } bool isWorkspaceIgnored(std::string const& workspace_name); bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } @@ -89,6 +93,7 @@ class Workspaces : public AModule, public EventHandler { auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; auto populateWindowRewriteConfig(const Json::Value& config) -> void; + auto populateMaxWindowsConfig(const Json::Value& config) -> void; auto populateWorkspaceTaskbarConfig(const Json::Value& config) -> void; void registerIpc(); @@ -146,6 +151,7 @@ class Workspaces : public AModule, public EventHandler { bool m_allOutputs = false; bool m_showSpecial = false; bool m_activeOnly = false; + bool m_hideActive = false; bool m_specialVisibleOnly = false; bool m_persistentOnly = false; bool m_moveToMonitor = false; @@ -173,6 +179,8 @@ class Workspaces : public AModule, public EventHandler { util::RegexCollection m_windowRewriteRules; bool m_anyWindowRewriteRuleUsesTitle = false; std::string m_formatWindowSeparator; + int m_windowRewriteGroupThreshold = 0; + std::string m_windowRewriteGroupFormat = "{icon}×{count}"; bool m_withIcon; uint64_t m_monitorId; @@ -202,6 +210,7 @@ class Workspaces : public AModule, public EventHandler { }; std::string m_onClickWindow; std::string m_currentActiveWindowAddress; + int m_maxWindows = 0; std::vector m_ignoreWorkspaces; std::vector m_ignoreWindows; diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 22bd808f..40c5227c 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -6,25 +6,35 @@ #include "bar.hpp" #include "client.hpp" +struct ext_idle_notification_v1; + namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; + ext_idle_notification_v1* idle_notification_; + uint32_t idle_timeout_ms_; public: IdleInhibitor(const std::string&, const waybar::Bar&, const Json::Value&); virtual ~IdleInhibitor(); auto update() -> void override; + auto refresh(int) -> void override; static std::list modules; static bool status; private: bool handleToggle(GdkEventButton* const& e) override; void toggleStatus(); + void setupIdleNotification(); + void teardownIdleNotification(); + static void handleIdled(void* data, ext_idle_notification_v1* notification); + static void handleResumed(void* data, ext_idle_notification_v1* notification); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; int pid_; + bool wait_for_activity_; }; } // namespace waybar::modules diff --git a/include/modules/keyboard_state.hpp b/include/modules/keyboard_state.hpp index db7f2550..c9f1afac 100644 --- a/include/modules/keyboard_state.hpp +++ b/include/modules/keyboard_state.hpp @@ -36,8 +36,7 @@ class KeyboardState : public AModule { std::string capslock_format_; std::string scrolllock_format_; const std::chrono::seconds interval_; - std::string icon_locked_; - std::string icon_unlocked_; + std::unordered_map> key_icon_states_; std::string devices_path_; struct libinput* libinput_; diff --git a/include/modules/mango/backend.hpp b/include/modules/mango/backend.hpp new file mode 100644 index 00000000..a1b0392e --- /dev/null +++ b/include/modules/mango/backend.hpp @@ -0,0 +1,67 @@ +// include/modules/mango/backend.hpp +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "util/json.hpp" + +namespace waybar::modules::mango { + +class EventHandler { + public: + virtual void onEvent(const Json::Value& ev) = 0; + virtual ~EventHandler() = default; +}; + +class IPC { + public: + static IPC& getInstance(); + IPC(const IPC&) = delete; + IPC& operator=(const IPC&) = delete; + + void registerForIPC(const std::string& ev, EventHandler* handler); + void unregisterForIPC(EventHandler* handler); + + static Json::Value send(const Json::Value& request); + static void sendAsync(const Json::Value& request); + + std::unique_lock lockData() { return std::unique_lock(data_mutex_); } + + std::unordered_map getMonitors() const; + Json::Value getMonitor(const std::string& name); + Json::Value getActiveClientForMonitor(const std::string& name) const; + std::string getKeyboardLayout() const; + std::string getKeymode() const; + std::string getLayoutSymbolForMonitor(const std::string& name) const; + + private: + IPC(); + ~IPC(); + void startIPC(); + static int connectToSocket(); + void parseIPC(const std::string& line); + + void handleMonitorUpdate(const Json::Value& mon); + void updateFocusingClient(const Json::Value& client); + void updateKeyboardLayout(const std::string& layout); + + static Json::Value sendCommand(const std::string& cmd); + + int sockfd_ = -1; + std::thread ipc_thread_; + mutable std::mutex data_mutex_; + std::unordered_map monitors_; + std::unordered_map clients_; + uint64_t focusing_client_id_ = 0; + std::string keyboard_layout_; + std::string keymode_; + Json::Value active_client_; + std::mutex callback_mutex_; + std::list> callbacks_; +}; +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/keymode.hpp b/include/modules/mango/keymode.hpp new file mode 100644 index 00000000..5e3ef685 --- /dev/null +++ b/include/modules/mango/keymode.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Keymode : public ALabel, public EventHandler { + public: + Keymode(const std::string&, const Bar&, const Json::Value&); + ~Keymode() override; + void update() override; + + private: + void onEvent(const Json::Value& ev) override; + void doUpdate(); + + std::mutex mutex_; + const Bar& bar_; + std::string last_keymode_; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/language.hpp b/include/modules/mango/language.hpp new file mode 100644 index 00000000..3d91515b --- /dev/null +++ b/include/modules/mango/language.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Language : public ALabel, public EventHandler { + public: + Language(const std::string&, const Bar&, const Json::Value&); + ~Language() override; + void update() override; + + private: + void updateFromIPC(); + void onEvent(const Json::Value& ev) override; + void doUpdate(); + + struct Layout { + std::string full_name; + std::string short_name; + std::string variant; + std::string short_description; + }; + + Layout getLayout(const std::string& fullName); + + std::mutex mutex_; + const Bar& bar_; + + std::vector layouts_; + unsigned current_idx_; + std::string last_short_name_; + + struct rxkb_context* rxkb_ctx_ = nullptr; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/layout.hpp b/include/modules/mango/layout.hpp new file mode 100644 index 00000000..4a349876 --- /dev/null +++ b/include/modules/mango/layout.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Layout : public ALabel, public EventHandler { + public: + Layout(const std::string&, const Bar&, const Json::Value&); + ~Layout() override; + void update() override; + + private: + void onEvent(const Json::Value& ev) override; + void doUpdate(); + + std::mutex mutex_; + const Bar& bar_; + std::string last_symbol_; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/window.hpp b/include/modules/mango/window.hpp new file mode 100644 index 00000000..987da34b --- /dev/null +++ b/include/modules/mango/window.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Window : public AAppIconLabel, public EventHandler { + public: + Window(const std::string&, const Bar&, const Json::Value&); + ~Window() override; + void update() override; + + private: + void onEvent(const Json::Value& ev) override; + void doUpdate(); + void setClass(const std::string& className, bool enable); + + const Bar& bar_; + std::string oldAppId_; + std::mutex mutex_; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/workspaces.hpp b/include/modules/mango/workspaces.hpp new file mode 100644 index 00000000..f5e52022 --- /dev/null +++ b/include/modules/mango/workspaces.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Workspaces : public AModule, public EventHandler { + public: + Workspaces(const std::string&, const Bar&, const Json::Value&); + ~Workspaces() override; + void update() override; + + private: + void onEvent(const Json::Value& ev) override; + void doUpdate(); + + Gtk::Button& addButton(uint64_t idx); + void updateButtonState(Gtk::Button& button, const Json::Value& tag, const Json::Value& monitor); + std::string getIcon(const std::string& value, const Json::Value& tag); + bool handleButtonClick(GdkEventButton* event, uint64_t idx, bool isOverview); + + const Bar& bar_; + Gtk::Box box_; + + std::unordered_map buttons_; + Gtk::Button* overview_button_ = nullptr; + + std::string on_click_left_; + std::string on_click_middle_; + std::string on_click_right_; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/memory.hpp b/include/modules/memory.hpp index c73ece23..0e405187 100644 --- a/include/modules/memory.hpp +++ b/include/modules/memory.hpp @@ -19,8 +19,6 @@ class Memory : public ALabel { private: void parseMeminfo(); - static float calc_divisor(const std::string& divisor); - std::unordered_map meminfo_; util::SleeperThread thread_; diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 9d81638e..7a777cf8 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -28,11 +28,14 @@ class MPD : public ALabel { unsigned timeout_; + unsigned playing_interval_; + detail::unique_connection connection_; detail::unique_status status_; mpd_state state_; detail::unique_song song_; + std::string ellipsis_; public: MPD(const std::string&, const Json::Value&); @@ -45,6 +48,10 @@ class MPD : public ALabel { void setLabel(); std::string getStateIcon() 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,11 +61,11 @@ 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; } inline bool paused() const { return connection_ && state_ == MPD_STATE_PAUSE; } + inline unsigned playing_interval() const { return playing_interval_; } }; #if !defined(MPD_NOINLINE) diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 2c9071b4..1da1471d 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -82,6 +82,7 @@ class Idle : public State { class Playing : public State { Context* const ctx_; sigc::connection timer_connection_; + sigc::connection idle_connection_; public: Playing(Context* const ctx) : ctx_{ctx} {} @@ -98,7 +99,10 @@ class Playing : public State { Playing(Playing const&) = delete; Playing& operator=(Playing const&) = delete; + void timer() noexcept; + void idle() noexcept; bool on_timer(); + bool on_io(Glib::IOCondition const&); }; class Paused : public State { @@ -194,10 +198,10 @@ class Context { bool is_paused() const; bool is_stopped() const; constexpr std::size_t interval() const; + unsigned playing_interval() const; 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..fc0964ef 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -1,13 +1,15 @@ #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 unsigned Context::playing_interval() const { return mpd_module_->playing_interval(); } 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 +17,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 a33db4d2..c5ce936a 100644 --- a/include/modules/mpris/mpris.hpp +++ b/include/modules/mpris/mpris.hpp @@ -38,6 +38,7 @@ class Mpris : public ALabel { std::optional artist; std::optional album; + std::optional album_artist; std::optional title; std::optional length; // as HH:MM:SS std::optional position; // same format @@ -76,6 +77,8 @@ class Mpris : public ALabel { std::string player_; std::vector ignored_players_; + bool prefer_album_artist_; + PlayerctlPlayerManager* manager; PlayerctlPlayer* player; PlayerctlPlayer* last_active_player_ = nullptr; diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index 08986412..ca5346d9 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -3,6 +3,8 @@ #include #include +#include + #include "AModule.hpp" #include "bar.hpp" #include "modules/niri/backend.hpp" @@ -18,13 +20,19 @@ class Workspaces : public AModule, public EventHandler { private: void onEvent(const Json::Value& ev) override; void doUpdate(); + void sortWorkspaces(std::vector& workspaces) const; Gtk::Button& addButton(const Json::Value& ws); std::string getIcon(const std::string& value, const Json::Value& ws); + bool handleScroll(GdkEventScroll* /*unused*/) override; const Bar& bar_; Gtk::Box box_; // Map from niri workspace id to button. std::unordered_map buttons_; + + bool sort_by_id_ = false; + bool sort_by_name_ = false; + bool sort_by_coordinates_ = false; }; } // namespace waybar::modules::niri diff --git a/include/modules/power_profiles_daemon.hpp b/include/modules/power_profiles_daemon.hpp index b2436171..1aa087a4 100644 --- a/include/modules/power_profiles_daemon.hpp +++ b/include/modules/power_profiles_daemon.hpp @@ -9,9 +9,18 @@ namespace waybar::modules { struct Profile { std::string name; + // Legacy driver field, kept for backward compatibility with the + // `{driver}` format placeholder and with older power-profiles-daemon + // versions that only expose a single `Driver` DBus property. std::string driver; + std::string cpuDriver; + std::string platformDriver; - Profile(std::string n, std::string d) : name(std::move(n)), driver(std::move(d)) {} + Profile(std::string n, std::string d, std::string cd, std::string pd) + : name(std::move(n)), + driver(std::move(d)), + cpuDriver(std::move(cd)), + platformDriver(std::move(pd)) {} }; class PowerProfilesDaemon : public ALabel { 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/pulseaudio.hpp b/include/modules/pulseaudio.hpp index eead664f..cfd300f7 100644 --- a/include/modules/pulseaudio.hpp +++ b/include/modules/pulseaudio.hpp @@ -22,6 +22,7 @@ class Pulseaudio : public ALabel { const std::vector getPulseIcon() const; std::shared_ptr backend = nullptr; + util::PulseaudioTarget target = util::PulseaudioTarget::Sink; }; } // namespace waybar::modules diff --git a/include/modules/pulseaudio_slider.hpp b/include/modules/pulseaudio_slider.hpp index 3ef44684..597b3cc8 100644 --- a/include/modules/pulseaudio_slider.hpp +++ b/include/modules/pulseaudio_slider.hpp @@ -6,11 +6,6 @@ #include "util/audio_backend.hpp" namespace waybar::modules { -enum class PulseaudioSliderTarget { - Sink, - Source, -}; - class PulseaudioSlider : public ASlider { public: PulseaudioSlider(const std::string&, const Json::Value&); @@ -21,7 +16,15 @@ class PulseaudioSlider : public ASlider { private: std::shared_ptr backend = nullptr; - PulseaudioSliderTarget target = PulseaudioSliderTarget::Sink; + util::PulseaudioTarget target = util::PulseaudioTarget::Sink; + + bool zero_on_mute = true; + bool unmute_on_volume_change = true; + // zero_on_mute and unmute_on_volume_change default to true + // in order to maintain previous behaviour when using a + // config in which these values are undefined + + bool previously_muted = false; }; -} // namespace waybar::modules \ No newline at end of file +} // namespace waybar::modules diff --git a/include/modules/river/tags.hpp b/include/modules/river/tags.hpp index 386ddecf..dca0873d 100644 --- a/include/modules/river/tags.hpp +++ b/include/modules/river/tags.hpp @@ -20,6 +20,8 @@ class Tags : public waybar::AModule { void handle_focused_tags(uint32_t tags); void handle_view_tags(struct wl_array* tags); void handle_urgent_tags(uint32_t tags); + void handle_focused_output(struct wl_output* output); + void handle_unfocused_output(struct wl_output* output); void handle_show(); void handle_primary_clicked(uint32_t tag); @@ -31,9 +33,11 @@ class Tags : public waybar::AModule { private: const waybar::Bar& bar_; + struct wl_output* output_; // stores the output this module belongs to Gtk::Box box_; std::vector buttons_; struct zriver_output_status_v1* output_status_; + struct zriver_seat_status_v1* seat_status_; }; } /* namespace waybar::modules::river */ diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index d76ec74a..7223b17a 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -14,11 +14,14 @@ namespace waybar::modules::SNI { class Host { public: - Host(const std::size_t id, const Json::Value&, const Bar&, + Host(const std::size_t id, const Json::Value&, const Bar&, const std::vector&, const std::function&)>&, const std::function&)>&, const std::function&); ~Host(); + void checkIgnoreList(const std::vector& ignore_list, + const std::function&)>& on_remove); + private: void busAcquired(const Glib::RefPtr&, Glib::ustring); void nameAppeared(const Glib::RefPtr&, Glib::ustring, @@ -43,8 +46,11 @@ class Host { std::size_t watcher_id_; GCancellable* cancellable_ = nullptr; SnWatcher* watcher_ = nullptr; + sigc::connection retry_connection_; + unsigned retry_count_ = 0; const Json::Value& config_; const Bar& bar_; + const std::vector ignore_list_; const std::function&)> on_add_; const std::function&)> on_remove_; const std::function on_update_; diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 74d54f4c..df87bfb7 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -46,6 +46,7 @@ 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; diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 3d90b3fd..2996078a 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -19,12 +19,14 @@ class Tray : public AModule { private: void onAdd(std::unique_ptr& item); void onRemove(std::unique_ptr& item); + void checkIgnoreList(std::unique_ptr* item); + std::vector parseIgnoreList(const Json::Value& config); void queueUpdate(); static inline std::size_t nb_hosts_ = 0; - bool show_passive_ = false; Gtk::Box box_; SNI::Watcher::singleton watcher_; + std::vector ignore_list_; SNI::Host host_; }; diff --git a/include/modules/sway/ipc/client.hpp b/include/modules/sway/ipc/client.hpp index f6eb7c40..2665caf9 100644 --- a/include/modules/sway/ipc/client.hpp +++ b/include/modules/sway/ipc/client.hpp @@ -1,14 +1,10 @@ #pragma once #include -#include -#include -#include -#include -#include +#include +#include #include -#include #include #include "ipc.hpp" @@ -41,8 +37,9 @@ class Ipc { static inline const std::string ipc_magic_ = "i3-ipc"; static inline const size_t ipc_header_size_ = ipc_magic_.size() + 8; - const std::string getSocketPath() const; - int open(const std::string&) const; + static std::string getSocketPath(); + static int open(const std::string&); + struct ipc_response send(int fd, uint32_t type, const std::string& payload = ""); struct ipc_response recv(int fd); diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index 14528841..cea8ade5 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -24,12 +24,15 @@ 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(const std::string& name); static int windowRewritePriorityFunction(std::string const& window_rule); + auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; + bool isWorkspaceIgnored(std::string const& name); void onCmd(const struct Ipc::ipc_response&); void onEvent(const struct Ipc::ipc_response&); bool filterButtons(); @@ -49,6 +52,7 @@ class Workspaces : public AModule, public sigc::trackable { std::vector workspaces_order_; Gtk::Box box_; std::string m_formatWindowSeparator; + std::vector m_ignoreWorkspaces; util::RegexCollection m_windowRewriteRules; util::JsonParser parser_; std::unordered_map buttons_; 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/wireplumber.hpp b/include/modules/wireplumber.hpp index 0565704f..9b7d5a7b 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -33,6 +33,7 @@ class Wireplumber : public ALabel { static void onDefaultNodesApiChanged(waybar::modules::Wireplumber* self); bool handleScroll(GdkEventScroll* e) override; + std::vector getWPIcon(); static std::list modules; @@ -54,6 +55,7 @@ class Wireplumber : public ALabel { bool source_muted_; double source_volume_; gchar* default_source_name_; + std::string form_factor_; }; } // namespace waybar::modules diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 413eae85..3ad41c6e 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -18,6 +18,7 @@ #include "AModule.hpp" #include "bar.hpp" #include "client.hpp" +#include "ext-workspace-v1-client-protocol.h" #include "giomm/desktopappinfo.h" #include "util/icon_loader.hpp" #include "util/json.hpp" @@ -80,6 +81,7 @@ class Task { std::string title_; std::string app_id_; uint32_t state_ = 0; + struct ext_workspace_handle_v1* workspace_ = nullptr; int32_t drag_start_x; int32_t drag_start_y; @@ -102,6 +104,9 @@ class Task { bool minimized() const { return state_ & MINIMIZED; } bool active() const { return state_ & ACTIVE; } bool fullscreen() const { return state_ & FULLSCREEN; } + bool visible() const { return button_visible_; } + struct ext_workspace_handle_v1* workspace() const { return workspace_; } + void set_workspace(struct ext_workspace_handle_v1* workspace) { workspace_ = workspace; } public: /* Callbacks for the wlr protocol */ @@ -142,6 +147,12 @@ using TaskPtr = std::unique_ptr; class Taskbar : public waybar::AModule { public: + struct WorkspaceState { + Taskbar* taskbar; + struct ext_workspace_handle_v1* handle; + uint32_t state = 0; + }; + Taskbar(const std::string&, const waybar::Bar&, const Json::Value&); ~Taskbar(); void update(); @@ -156,22 +167,35 @@ class Taskbar : public waybar::AModule { std::map app_ids_replace_map_; struct zwlr_foreign_toplevel_manager_v1* manager_; + struct ext_workspace_manager_v1* workspace_manager_; struct wl_seat* seat_; + std::vector workspace_groups_; + std::vector> workspaces_; + struct ext_workspace_handle_v1* current_workspace_ = nullptr; public: /* Callbacks for global registration */ void register_manager(struct wl_registry*, uint32_t name, uint32_t version); + void register_workspace_manager(struct wl_registry*, uint32_t name, uint32_t version); void register_seat(struct wl_registry*, uint32_t name, uint32_t version); /* Callbacks for the wlr protocol */ void handle_toplevel_create(struct zwlr_foreign_toplevel_handle_v1*); void handle_finished(); + void handle_workspace_group_create(struct ext_workspace_group_handle_v1*); + void handle_workspace_group_removed(struct ext_workspace_group_handle_v1*); + void handle_workspace_create(struct ext_workspace_handle_v1*); + void handle_workspace_done(); + void handle_workspace_finished(); + void handle_workspace_removed(struct ext_workspace_handle_v1*); public: void add_button(Gtk::Button&); void move_button(Gtk::Button&, int); void remove_button(Gtk::Button&); void remove_task(uint32_t); + void assign_current_workspace(Task&); + void update_bar_css_classes(); bool show_output(struct wl_output*) const; bool all_outputs() const; @@ -179,6 +203,9 @@ class Taskbar : public waybar::AModule { const IconLoader& icon_loader() const; const std::unordered_set& ignore_list() const; const std::map& app_ids_replace_map() const; + + private: + void set_bar_css_class(const std::string&, bool); }; } /* namespace waybar::modules::wlr */ diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp index afb0893e..b1e3a91a 100644 --- a/include/util/SafeSignal.hpp +++ b/include/util/SafeSignal.hpp @@ -12,11 +12,6 @@ #include #include -#ifdef __OpenBSD__ -#define SIGRTMIN SIGUSR1 - 1 -#define SIGRTMAX SIGUSR1 + 1 -#endif - namespace waybar { /** diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp index 3737ae26..ec732e1e 100644 --- a/include/util/audio_backend.hpp +++ b/include/util/audio_backend.hpp @@ -14,6 +14,11 @@ namespace waybar::util { +enum class PulseaudioTarget { + Sink, + Source, +}; + class AudioBackend { private: static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); @@ -22,12 +27,14 @@ class AudioBackend { static void sourceInfoCb(pa_context*, const pa_source_info* i, int, void* data); static void serverInfoCb(pa_context*, const pa_server_info*, void*); static void volumeModifyCb(pa_context*, int, void*); + static void sourceVolumeModifyCb(pa_context*, int, void*); void connectContext(); pa_threaded_mainloop* mainloop_; pa_mainloop_api* mainloop_api_; pa_context* context_; pa_cvolume pa_volume_; + pa_cvolume pa_source_volume_; // SINK uint32_t sink_idx_{0}; @@ -50,6 +57,7 @@ class AudioBackend { std::string default_source_name_; std::vector ignored_sinks_; + std::map sink_mapping_; std::function on_updated_cb_ = NOOP; @@ -67,10 +75,13 @@ class AudioBackend { AudioBackend(std::function on_updated_cb, private_constructor_tag tag); ~AudioBackend(); - void changeVolume(uint16_t volume, uint16_t min_volume = 0, uint16_t max_volume = 100); - void changeVolume(ChangeType change_type, double step = 1, uint16_t max_volume = 100); + void changeVolume(uint16_t volume, uint16_t min_volume = 0, uint16_t max_volume = 100, + PulseaudioTarget target = PulseaudioTarget::Sink); + void changeVolume(ChangeType change_type, double step = 1, uint16_t max_volume = 100, + PulseaudioTarget target = PulseaudioTarget::Sink); void setIgnoredSinks(const Json::Value& config); + void setSinkMapping(const Json::Value& config); std::string getSinkPortName() const { return port_name_; } std::string getFormFactor() const { return form_factor_; } @@ -92,7 +103,11 @@ class AudioBackend { void toggleSourceMute(); void toggleSourceMute(bool); + uint16_t getVolume(PulseaudioTarget) const; + bool getMuted(PulseaudioTarget) const; + void unmute(PulseaudioTarget); + bool isBluetooth(); }; -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util 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/format.hpp b/include/util/format.hpp index c8ed837a..2eb148c4 100644 --- a/include/util/format.hpp +++ b/include/util/format.hpp @@ -5,12 +5,13 @@ class pow_format { public: - pow_format(long long val, std::string&& unit, bool binary = false) - : val_(val), unit_(unit), binary_(binary) {}; + pow_format(long long val, std::string&& unit, bool binary = false, int min_pow_for_decimal = 0) + : val_(val), unit_(unit), binary_(binary), min_pow_for_decimal_(min_pow_for_decimal) {}; long long val_; std::string unit_; bool binary_; + int min_pow_for_decimal_; }; namespace fmt { @@ -74,7 +75,8 @@ struct formatter { break; case 0: default: - format = "{coefficient:.1f}{prefix}{unit}"; + format = pow < s.min_pow_for_decimal_ ? "{coefficient:.0f}{prefix}{unit}" + : "{coefficient:.1f}{prefix}{unit}"; break; } return fmt::format_to( diff --git a/include/util/hosts_check.hpp b/include/util/hosts_check.hpp new file mode 100644 index 00000000..4179f633 --- /dev/null +++ b/include/util/hosts_check.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace waybar::util { +bool valid_host(const Json::Value& config); +} // namespace waybar::util 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/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 1f3a372d..9d20f252 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -79,6 +79,12 @@ class SleeperThread { 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(); @@ -95,6 +101,12 @@ class SleeperThread { time_point) { 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); + }); + return condvar_.wait_until(lk, time_point, [this] { return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); }); @@ -122,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(); @@ -137,6 +160,7 @@ class SleeperThread { 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 42b9e046..0c71518c 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -27,6 +27,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: false ++ Option to use the battery design capacity instead of its current maximal capacity. +*full-at-plugged*: ++ + typeof: bool ++ + default: false ++ + When enabled, a battery that is *Full* while the adapter is online is reported with the *Plugged* status instead of *Full* (so you can style/format it separately). Disabled by default to preserve the existing *Full* behaviour. + *interval*: ++ typeof: integer ++ default: 60 ++ diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index fd7d5fb5..36c6443a 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -178,6 +178,9 @@ At the time of writing, the experimental features of BlueZ need to be turned on, *{device_battery_percentage}*: Battery percentage of the displayed device if available. Use only in the config options defined below. +*{device_battery_percentage_peripheral}*: Battery percentage of the peripheral half of a split keyboard (e.g., ZMK keyboards with separate central and peripheral batteries). ++ +This is read from GATT Battery Service characteristics that have a User Description descriptor. Use only in the config options defined below. + ## CONFIGURATION *format-connected-battery*: ++ @@ -220,6 +223,17 @@ At the time of writing, the experimental features of BlueZ need to be turned on, } ``` +Split keyboard with separate central/peripheral batteries (e.g., ZMK): + +``` +"bluetooth": { + "format-device-preference": [ "Keyball44" ], + "format": "", + "format-connected-battery": " {device_battery_percentage}%|{device_battery_percentage_peripheral}%", + "tooltip-format-connected": "{device_alias}\\nCentral: {device_battery_percentage}%\\nPeripheral: {device_battery_percentage_peripheral}%" +} +``` + # STYLE - *#bluetooth* diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 1b13dcb7..263469f0 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -138,6 +138,11 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ When enabled, the calendar follows the ISO 8601 standard: weeks begin on Monday, and the first week of the year is numbered 1. The default week format is '{:%V}'. +|[ *first-day-of-week* +:[ integer +:[ +:[ The first day of the week, where 0 is Sunday and 6 is Saturday. + When not set, the first day of the week is determined by the locale settings. 3. Addressed by *clock: calendar: format* [- *Option* @@ -180,6 +185,8 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ Switch to the next calendar month/year |[ *shift_down* :[ Switch to the previous calendar month/year +|[ *exec * +:[ Execute the specified command # FORMAT REPLACEMENTS @@ -207,11 +214,12 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe "format-alt": "{:%A, %B %d, %Y (%R)} 󰃰 ", "tooltip-format": "{calendar}", "calendar": { - "mode" : "year", - "mode-mon-col" : 3, - "weeks-pos" : "right", - "on-scroll" : 1, - "on-click-right": "mode", + "mode" : "year", + "mode-mon-col" : 3, + "weeks-pos" : "right", + "first-day-of-week": 1, + "on-scroll" : 1, + "on-click-right" : "mode", "format": { "months": "{}", "days": "{}", diff --git a/man/waybar-cpu-graph.5.scd b/man/waybar-cpu-graph.5.scd new file mode 100644 index 00000000..1877aeeb --- /dev/null +++ b/man/waybar-cpu-graph.5.scd @@ -0,0 +1,80 @@ +waybar-cpu-graph(5) + +# NAME + +waybar - cpu graph module + +# DESCRIPTION + +The *cpu graph* module displays a line graph with the CPU utilization. + +# CONFIGURATION + +*interval*: ++ + typeof: integer ++ + default: 10 ++ + The interval in which the information gets polled. + +*width*: ++ + typeof: integer ++ + The length in pixels the module should display. + +*datapoints*: ++ + typeof: integer ++ + How many data points to show. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# EXAMPLES + +Basic configuration: + +``` +"cpu_graph": { + "interval": 2, + "width": 10 +} +``` + +# STYLE + +- *#cpu_graph* +- *.cpu-intensive* +- *.cpu-high* +- *.cpu-moderate* diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index 40682372..3f68998f 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -83,6 +83,18 @@ The *cpu* module displays the current CPU utilization. default: true ++ Option to disable tooltip on hover. +*tooltip-format*: ++ + typeof: string ++ + The format of the tooltip shown on hover. Supports the same replacements as *format*. + +*format-*: ++ + typeof: string ++ + The format to use when the given *state* (see *states*) is active. Supports the same replacements as *format*. + +*tooltip-format-*: ++ + typeof: string ++ + The tooltip format to use when the given *state* (see *states*) is active. Takes precedence over *tooltip-format*. + *expand*: ++ typeof: bool ++ default: false ++ @@ -92,6 +104,12 @@ The *cpu* module displays the current CPU utilization. *{load}*: Current CPU load. +*{load1}*: CPU load average over the last minute. + +*{load5}*: CPU load average over the last 5 minutes. + +*{load15}*: CPU load average over the last 15 minutes. + *{usage}*: Current overall CPU usage. *{usage*{n}*}*: Current CPU core n usage. Cores are numbered from zero, so first core will be {usage0} and 4th will be {usage3}. @@ -106,6 +124,8 @@ The *cpu* module displays the current CPU utilization. *{icon*{n}*}*: Icon for CPU core n usage. Use like {icon0}. +*{icon0}{icon1}{icon2}{icon3}*: All per-core icons concatenated. Equivalent to {icon0}{icon1}...{icon*N*} but adapts to the number of cores automatically. + # EXAMPLES Basic configuration: @@ -128,6 +148,16 @@ CPU usage per core rendered as icons: }, ``` +Automatically determine number of icons according to number of logical cores: + +``` +"cpu": { + "interval": 1, + "format": "{icons} {usage:>2}% ", + "format-icons": ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"], +}, +``` + # STYLE - *#cpu* diff --git a/man/waybar-custom-graph.5.scd b/man/waybar-custom-graph.5.scd new file mode 100644 index 00000000..4f020a6c --- /dev/null +++ b/man/waybar-custom-graph.5.scd @@ -0,0 +1,189 @@ +waybar-custom-graph(5) +# NAME + +waybar - custom graph module + +# DESCRIPTION + +The *custom-graph* module displays a graph with the percentage output of a script. + +# CONFIGURATION + +Addressed by *custom-graph/* + +*exec*: ++ + typeof: string ++ + The path to the script, which should be executed. + +*exec-if*: ++ + typeof: string ++ + The path to a script, which determines if the script in *exec* should be executed. ++ + *exec* will be executed if the exit code of *exec-if* equals 0. + +*exec-on-event*: ++ + typeof: bool ++ + default: true ++ + If an event command is set (e.g. *on-click* or *on-scroll-up*) then re-execute the script after executing the event command. + +*return-type*: ++ + typeof: string ++ + See *return-type* + +*interval*: ++ + typeof: integer or float ++ + The interval (in seconds) in which the information gets polled. ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + Use *once* if you want to execute the module only on startup. ++ + You can update it manually with a signal. If no *interval* or *signal* is defined, it is assumed that the out script loops itself. ++ + If a *signal* is defined then the script will run once on startup and will only update with a signal. + +*restart-interval*: ++ + typeof: integer or float ++ + The restart interval (in seconds). ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + Can't be used with the *interval* option, so only with continuous scripts. ++ + Once the script exits, it'll be re-executed after the *restart-interval*. + +*signal*: ++ + typeof: integer ++ + The signal number used to update the module. ++ + The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ + If no interval is defined then a signal will be the only way to update the module. + +*format*: ++ + typeof: string ++ + default: {text} ++ + The format, how information should be displayed. On {text} data gets inserted. + +*format-icons*: ++ + typeof: array ++ + Based on the set percentage, the corresponding icon gets selected. The order is *low* to *high*. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label (in 90 degree increments). + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + The tooltip format. If specified, overrides any tooltip output from the script in *exec*. ++ + Uses the same format replacements as *format*. + +*escape*: ++ + typeof: bool ++ + default: false ++ + Option to enable escaping of script output. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# RETURN-TYPE + +When *return-type* is set to *json*, Waybar expects the *exec*-script to output its data in JSON format. +This should look like this: + +``` +{"text": "$text", "tooltip": "$tooltip", "class": "$class", "percentage": $percentage } +``` + +The *class* parameter also accepts an array of strings. + +If nothing or an invalid option is specified, Waybar expects i3blocks style output. Values are *newline* separated. +This should look like this: + +``` +$text\\n$tooltip\\n$class* +``` + +*class* is a CSS class, to apply different styles in *style.css* + +# FORMAT REPLACEMENTS + +*{text}*: Output of the script. + +*{percentage}* Percentage which can be set via a json return type. + +*{icon}*: An icon from 'format-icons' according to percentage. + +# EXAMPLES + +## Memory: + +``` +"custom-graph/memory": { + "interval": 60, + "graph_type": "gauge", + "width": 52, + "exec": "/path/mem.sh", + "signal": 8, + "return-type": "json" +}, +``` + +mem.sh: + +``` +#!/bin/bash + +mem_info=$(cat /proc/meminfo) +mem_total=$(echo "$mem_info" | grep '^MemTotal:' | awk '{print $2}') +mem_available=$(echo "$mem_info" | grep '^MemAvailable:' | awk '{print $2}') + +mem_used=$((mem_total - mem_available)) +mem_percent=$((mem_used * 100 / mem_total)) + +echo "{\"text\": \"${mem_percent}%\", \"percentage\": ${mem_percent},\"tooltip\": \"Memory: ${mem_used}KB used / ${mem_total}KB total\"}'" +``` + +# STYLE + +- *#custom-graph-* +- *#custom-graph-.* +- ** can be set by the script. For more information see *return-type* diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index 00af2b90..dab6af7e 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -6,17 +6,12 @@ waybar - disk module # DESCRIPTION -The *disk* module displays the current disk space used. +The *disk* module displays information of multiple disks. # CONFIGURATION Addressed by *disk* -*path*: ++ - typeof: string ++ - default: "/" ++ - Any path residing in the filesystem or mountpoint for which the information should be displayed. - *interval*: ++ typeof: integer++ default: 30 ++ @@ -25,7 +20,7 @@ Addressed by *disk* *format*: ++ typeof: string ++ default: "{percentage_used}%" ++ - The format, how information should be displayed. + The format, how information for each disk should be displayed. *rotate*: ++ typeof: integer ++ @@ -75,6 +70,26 @@ Addressed by *disk* typeof: string ++ Command to execute when scrolling down on the module. +*path*: ++ + typeof: string ++ + default: "/" ++ + Deprecated path of filesystem or mountpoint to monitor. + +*paths*: ++ + typeof: array ++ + default: ["/"] ++ + Array of paths residing in the filesystem or mountpoint for which the information should be displayed. + +*header*: ++ + typeof: string ++ + default: "" ++ + Text to appear before the disk information defined in the format. + +*separator*: ++ + typeof: string ++ + default: " " ++ + Separator string between multiple disk information. + *smooth-scrolling-threshold*: ++ typeof: double ++ Threshold to be used when scrolling. @@ -123,7 +138,7 @@ Addressed by *disk* *{free}*: Amount of available disk space for normal users. Automatically selects unit based on size remaining. -*{path}*: The path specified in the configuration. +*{path}*: The path for each disk specified in the configuration. *{specific_total}*: Total amount of space on the disk, partition, or mountpoint in a specific unit. Defaults to bytes. @@ -143,10 +158,22 @@ Addressed by *disk* ``` "disk": { "interval": 30, + "format": "{percentage_free}% free on {path}", + "header": "Disks: ", + "paths": ["/", "/home"], + "separator": " ", +} +``` + +``` +"disk": { + "interval": 30, + "paths": ["/"], "format": "{specific_free:0.2f} GB out of {specific_total:0.2f} GB available. Alternatively {free} out of {total} available", "unit": "GB" - // 1434.25 GB out of 2000.00 GB available. Alternatively 1.4TiB out of 1.9TiB available. } + + // 1434.25 GB out of 2000.00 GB available. Alternatively 1.4TiB out of 1.9TiB available. ``` # STYLE diff --git a/man/waybar-dwl-tags.5.scd b/man/waybar-dwl-tags.5.scd index 39d15b7a..c301f49a 100644 --- a/man/waybar-dwl-tags.5.scd +++ b/man/waybar-dwl-tags.5.scd @@ -21,6 +21,11 @@ Addressed by *dwl/tags* typeof: array ++ The label to display for each tag. +*hide-vacant*: ++ + typeof: bool ++ + default: false ++ + If set to true, tags without clients and that are not active will be hidden. + *disable-click*: ++ typeof: bool ++ default: false ++ @@ -46,8 +51,9 @@ Addressed by *dwl/tags* - *#tags button.empty* - *#tags button.focused* - *#tags button.urgent* +- *#tags button.output* -Note that occupied/focused/urgent status may overlap. That is, a tag may be +Note that occupied/focused/urgent/output status may overlap. That is, a tag may be both occupied and focused at the same time. # SEE ALSO diff --git a/man/waybar-dwl-window.5.scd b/man/waybar-dwl-window.5.scd index 9ac33d94..907d5332 100644 --- a/man/waybar-dwl-window.5.scd +++ b/man/waybar-dwl-window.5.scd @@ -17,6 +17,16 @@ Addressed by *dwl/window* default: {title} ++ The format, how information should be displayed. +*hide-empty*: ++ + typeof: bool ++ + default: false ++ + Option to hide the module when the content would be empty. + +*hide-inactive*: ++ + typeof: bool ++ + default: false ++ + Option to hide the module when the window is unfocused. + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label (in 90 degree increments). @@ -109,6 +119,10 @@ If no expression matches, the format output is left unchanged. Invalid expressions (e.g., mismatched parentheses) are skipped. +# STYLE + +- *#window.active* + # EXAMPLES ``` diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index 5a7ba941..88dd82d7 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -21,6 +21,10 @@ Addressed by *hyprland/language* typeof: string++ Provide an alternative name to display per language where is the language of your choosing. Can be passed multiple times with multiple languages as shown by the example below. +*format--* ++ + typeof: string ++ + Like *format-* but also matches the layout variant, taking precedence over *format-* when both the language and variant match. + *keyboard-name*: ++ typeof: string ++ Specifies which keyboard to use from hyprctl devices output. Using the option that begins with "at-translated-set..." is recommended. @@ -38,6 +42,24 @@ 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. + +*tooltip-format--*: ++ + typeof: string ++ + Like *tooltip-format-* but also matches the layout variant, taking precedence over *tooltip-format-* when both the language and variant match. + *expand*: ++ typeof: bool ++ default: false ++ @@ -66,6 +88,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-window.5.scd b/man/waybar-hyprland-window.5.scd index 34ebf89b..ecbacd4b 100644 --- a/man/waybar-hyprland-window.5.scd +++ b/man/waybar-hyprland-window.5.scd @@ -25,6 +25,15 @@ Addressed by *hyprland/window* typeof: bool ++ Show the active window of the monitor the bar belongs to, instead of the focused window. +*fallback*: ++ + typeof: string ++ + Text to display when the focused window title is empty (for example when no window is focused). + +*tooltip-format*: ++ + typeof: string ++ + default: {title} ++ + The format of the tooltip shown on hover. Supports the same replacements as *format*. Requires *tooltip* to be enabled. + *icon*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 5284ce99..759e76cc 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -41,6 +41,19 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. The separator to be used between windows in a workspace. ++ This setting is ignored if *workspace-taskbar.enable* is set to true. +*window-rewrite-group-threshold*: ++ + typeof: int ++ + default: 0 ++ + When a workspace contains at least this many windows with the same rewrite result, they are collapsed into a single one using *window-rewrite-group-format*. ++ + Set to 0 to disable grouping. ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + +*window-rewrite-group-format*: ++ + typeof: string ++ + default: "{icon}×{count}" ++ + The format used to represent a group of collapsed windows. Available placeholders are {icon} (the icon being grouped) and {count} (how many windows share it). ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + *workspace-taskbar*: ++ typeof: object ++ Contains settings for the workspace taskbar, an alternative mode for the workspaces module which displays the window icons as images instead of text. @@ -98,6 +111,11 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. - {button} Pressed button number, see https://api.gtkd.org/gdk.c.types.GdkEventButton.button.html. ++ See https://github.com/Alexays/Waybar/wiki/Module:-Hyprland#workspace-taskbars-example for a full example. +*max-windows*: ++ + typeof: int ++ + default: 0 (unlimited) ++ + Maximum number of windows to show per workspace. When set, newest windows beyond the limit are not shown. Set to 0 for unlimited windows. + *show-special*: ++ typeof: bool ++ default: false ++ @@ -113,6 +131,11 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. default: false ++ If set to true, only persistent workspaces will be shown on bar. +*persistent-workspaces*: ++ + typeof: object ++ + default: empty ++ + Lists workspaces that should always be shown, even when they do not exist. Keys are workspace names and values are arrays of output names on which the workspace should be shown (an empty array means all outputs). See the examples below. + *all-outputs*: ++ typeof: bool ++ default: false ++ @@ -123,6 +146,11 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. default: false ++ If set to true, only the active workspace will be shown. +*hide-active*: ++ + typeof: bool ++ + default: false ++ + If set to true, the active workspace will be hidden. Unless a workspace is persistent or special. + *move-to-monitor*: ++ typeof: bool ++ default: false ++ @@ -135,6 +163,14 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. 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-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 405c8fc5..cd6980f2 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -76,6 +76,17 @@ screensaver, also known as "presentation mode". typeof: double ++ The number of minutes the inhibition should last. +*wait-for-activity*: ++ + typeof: bool ++ + default: *false* ++ + When enabled, the idle inhibitor remains active as long as there is keyboard or mouse activity on the bar. If there is no activity for the duration specified in *timeout*, the inhibitor will automatically toggle off. This option requires *timeout* to be set. + +*signal*: ++ + typeof: integer ++ + The signal number used to toggle the idle inhibitor externally. ++ + The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ + Use `pkill -SIGRTMIN+N waybar` to toggle the idle inhibitor from scripts or keybindings. + *tooltip*: ++ typeof: bool ++ default: true ++ @@ -115,17 +126,46 @@ screensaver, also known as "presentation mode". # EXAMPLES +Basic usage with timeout: + ``` "idle_inhibitor": { "format": "{icon}", "format-icons": { - "activated": "", - "deactivated": "" + "activated": "", + "deactivated": "" }, "timeout": 30.5 } ``` +With external control via signals (can be toggled with `pkill -SIGRTMIN+8 waybar`): + +``` +"idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "", + "deactivated": "" + }, + "signal": 8 +} +``` + +With wait-for-activity feature: + +``` +"idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "", + "deactivated": "" + }, + "timeout": 5.0, + "wait-for-activity": true +} +``` + # STYLE - *#idle_inhibitor* diff --git a/man/waybar-keyboard-state.5.scd b/man/waybar-keyboard-state.5.scd index 79498414..7babe336 100644 --- a/man/waybar-keyboard-state.5.scd +++ b/man/waybar-keyboard-state.5.scd @@ -26,7 +26,10 @@ You must be a member of the input group to use this module. *format-icons*: ++ typeof: object ++ default: {"locked": "locked", "unlocked": "unlocked"} ++ - Based on the keyboard state, the corresponding icon gets selected. The same set of icons is used for number, caps, and scroll lock, but the icon is selected from the set independently for each. See *icons*. + Based on the keyboard state, the corresponding icon gets selected. Supports two syntaxes: + - Common format-icons: "locked" and "unlocked" keys apply to all lock types. + - Per-lock-type format-icons: per-lock-type objects with "numlock", "capslock", "scrolllock" keys, each containing "locked" and "unlocked" icons. + See *icons*. *numlock*: ++ typeof: bool ++ @@ -68,15 +71,21 @@ You must be a member of the input group to use this module. The following *format-icons* can be set. +## Common format-icons for all lock types: - *locked*: Will be shown when the keyboard state is locked. Default "locked". -- *unlocked*: Will be shown when the keyboard state is not locked. Default "unlocked" +- *unlocked*: Will be shown when the keyboard state is not locked. Default "unlocked". + +## Per-lock-type format-icons: +- *numlock*, *capslock*, *scrolllock*: Object containing "locked" and "unlocked" keys for lock-type-specific icons. Defaults to {"locked": "locked", "unlocked": "unlocked"} for each lock type. # EXAMPLE: +## Common format-icons for all lock types: ``` "keyboard-state": { "numlock": true, "capslock": true, + "scrolllock": true, "format": "{name} {icon}", "format-icons": { "locked": "", @@ -85,6 +94,30 @@ The following *format-icons* can be set. } ``` +## Per-lock-type format-icons: +``` +"keyboard-state": { + "numlock": true, + "capslock": true, + "scrolllock": true, + "format": "{name} {icon}", + "format-icons": { + "numlock": { + "locked": "1", + "unlocked": "0" + }, + "capslock": { + "locked": "A", + "unlocked": "a" + }, + "scrolllock": { + "locked": "S", + "unlocked": "s" + } + } +} +``` + # STYLE - *#keyboard-state* diff --git a/man/waybar-mango-keymode.5.scd b/man/waybar-mango-keymode.5.scd new file mode 100644 index 00000000..b96c1903 --- /dev/null +++ b/man/waybar-mango-keymode.5.scd @@ -0,0 +1,63 @@ +waybar-mango-keymode(5) + +# NAME + +waybar - mango keymode module + +# DESCRIPTION + +The *keymode* module displays the current keyboard mode (e.g. "resize", "default") in the Mango compositor. It is hidden when no mode is active. + +# CONFIGURATION + +Addressed by *mango/keymode* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format, how the mode should be displayed. *{mode}* is replaced by the current mode name. + +*format-*: ++ + typeof: string ++ + Provide a custom format for a specific keymode. ** is the mode name as reported by Mango (e.g. "resize"). The value can contain *{mode}* as a placeholder. + If this option is set, it overrides the main *format* for that mode. + +*menu*: ++ + typeof: string ++ + Action that pops up a menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. + +*menu-actions*: ++ + typeof: array ++ + Actions for the menu buttons. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables the module to consume all leftover space. + +# FORMAT REPLACEMENTS + +*{mode}*: The name of the current keymode. + +# EXAMPLES + +``` +"mango/keymode": { + "format": "[{mode}]", + "format-resize": " Resizing" +} +``` + +# STYLE + +- *#keymode* + +A CSS class with the current mode name (e.g. *.resize*) is added to the widget, allowing per‑mode styling: + +``` +#keymode.resize { background: #ff0000; } +``` diff --git a/man/waybar-mango-language.5.scd b/man/waybar-mango-language.5.scd new file mode 100644 index 00000000..b153f0d3 --- /dev/null +++ b/man/waybar-mango-language.5.scd @@ -0,0 +1,78 @@ +waybar-mango-language(5) + +# NAME + +waybar - mango language module + +# DESCRIPTION + +The *language* module displays the currently active keyboard layout in the Mango compositor. + +# CONFIGURATION + +Addressed by *mango/language* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format, how the layout should be displayed. See *FORMAT REPLACEMENTS*. + +*format-*: ++ + typeof: string ++ + Provide an alternative format string for a given language. + is the short description of the layout (e.g. "us", "de"). + The value is used as the replacement for *{}* in the main *format*. + This option can be repeated for multiple languages. + +*format--*: ++ + typeof: string ++ + Like *format-* but also matches the layout variant, taking precedence over *format-* when both the language and variant match. + +*menu*: ++ + typeof: string ++ + Action that pops up a menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. It must contain an element of type GtkMenu with id *menu*. + +*menu-actions*: ++ + typeof: array ++ + Actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all leftover space dynamically. + +# FORMAT REPLACEMENTS + +*{short}*: Short name of the layout (e.g. "us"). This is also the default when no format is specified. + +*{shortDescription}*: Short description of the layout (same as *{short}* in most cases). + +*{long}*: Full name of the layout as reported by Mango (e.g. "English (US)"). + +*{variant}*: Variant of the layout, if any. + +# EXAMPLES + +``` +"mango/language": { + "format": " {long} ", + "format-us": "US", + "format-de": "DE" +} +``` + +# STYLE + +- *#language* + +A CSS class matching the current layout's short name is added to the widget. +This allows per‑layout styling: + +``` +#language.us { color: #00ff00; } +#language.de { color: #ff0000; } +``` diff --git a/man/waybar-mango-layout.5.scd b/man/waybar-mango-layout.5.scd new file mode 100644 index 00000000..6cf3290b --- /dev/null +++ b/man/waybar-mango-layout.5.scd @@ -0,0 +1,75 @@ +waybar-mango-layout(5) + +# NAME + +waybar - mango layout module + +# DESCRIPTION + +The *layout* module displays the current layout symbol of the monitor (e.g. "S", "M") in the Mango compositor. +It supports dynamic CSS classes and custom formats based on the active layout symbol. + +# CONFIGURATION + +Addressed by *mango/layout* + +*format*: ++ + typeof: string ++ + default: {symbol} ++ + The default format, how the layout symbol should be displayed. *{symbol}* is replaced by the current layout symbol. + +*format-*: ++ + typeof: string ++ + default: *none* ++ + The custom format to use when a specific layout symbol is active (e.g., *format-S*, *format-M*). Note that the symbol string is strictly case-sensitive. If no match is found, it falls back to *format*. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables the module to consume all leftover space. + +# FORMAT REPLACEMENTS + +*{symbol}*: The layout symbol reported by Mango (e.g., "S", "M", "Dwindle"). + +# CUSTOM FORMATS + +You can define specific formats for different layouts by appending the exact layout symbol to the *format-* key in your configuration. + +For example, if your Mango compositor reports the symbol "S" for a spiral layout and "M" for a master layout, you can use *format-S* and *format-M* to define unique icons or text for each. Keep in mind that JSON keys are case-sensitive, so if the compositor sends "S", the key must be exactly *format-S*. + +# STYLE + +The layout module provides dynamic CSS classes based on the current layout symbol, allowing you to style each layout differently. + +* *#mango-layout* +* *.* - The current layout symbol is dynamically added as a CSS class name (e.g., *.S*, *.M*). + +# EXAMPLES + +``` +"mango/layout": { + "format": "[] {symbol}", + "format-S": "󰌌 {symbol}", + "format-M": "󰕰 {symbol}" +} +``` + +## CSS Example + +``` +#mango-layout { + color: #ffffff; + padding: 0 5px; +} + +/* Specific color for the "S" layout */ +#mango-layout.S { + color: #a6e3a1; +} + +/* Specific color for the "M" layout */ +#mango-layout.M { + color: #f38ba8; +} +``` diff --git a/man/waybar-mango-window.5.scd b/man/waybar-mango-window.5.scd new file mode 100644 index 00000000..7ee9b02c --- /dev/null +++ b/man/waybar-mango-window.5.scd @@ -0,0 +1,70 @@ +waybar-mango-window(5) + +# NAME + +waybar - mango window module + +# DESCRIPTION + +The *window* module displays the title and app ID of the currently focused window in the Mango compositor. + +# CONFIGURATION + +Addressed by *mango/window* + +*format*: ++ + typeof: string ++ + default: {title} ++ + The format string. See *FORMAT REPLACEMENTS*. + +*rewrite*: ++ + typeof: object ++ + Rules to rewrite the window title. Each key is a regular expression and its value is the replacement string. Captures can be used with *$1*, *$2*, etc. + +*icon*: ++ + typeof: bool ++ + default: false ++ + Whether to show the application icon. + +*icon-size*: ++ + typeof: integer ++ + default: 24 ++ + Size of the application icon in pixels. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables the module to consume all leftover space. + +# FORMAT REPLACEMENTS + +*{title}*: The current window title. + +*{app_id}*: The app ID of the focused window. + +# REWRITE RULES + +If the title matches a regular expression from the *rewrite* object, it is replaced by the corresponding value. Regular expression syntax follows ECMAScript rules. Unmatched titles are left unchanged. + +# EXAMPLES + +``` +"mango/window": { + "format": "{title}", + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + }, + "icon": true, + "icon-size": 20 +} +``` + +# STYLE + +- *#window* +- *#window.empty* – applied when no window is focused (module hidden by default) +- *#window.solo* – applied when only one window is present on the active workspace +- *#window.* – applied when a single window with the given app ID is on the workspace + +The classes *.empty*, *.solo*, and the app‑ID class are set on the module’s event box. \ No newline at end of file diff --git a/man/waybar-mango-workspaces.5.scd b/man/waybar-mango-workspaces.5.scd new file mode 100644 index 00000000..985398e7 --- /dev/null +++ b/man/waybar-mango-workspaces.5.scd @@ -0,0 +1,112 @@ +waybar-mango-workspaces(5) + +# NAME + +waybar - mango workspaces module + +# DESCRIPTION + +The *workspaces* module displays the tags (workspaces) of the Mango compositor. It shows an overview button when the overview mode is active (active tag is 0), and individual tag buttons otherwise. + +# CONFIGURATION + +Addressed by *mango/workspaces* + +*format*: ++ + typeof: string ++ + default: {value} ++ + The format for each tag button. See *FORMAT REPLACEMENTS*. + +*format-icons*: ++ + typeof: object ++ + Icons to be used instead of the workspace index or name. Keys can be a workspace index (as a string), or one of the following special state keys: *default*, *active*, *urgent*, *empty*. + +*disable-markup*: ++ + typeof: bool ++ + default: false ++ + If true, the button label will not be interpreted as Pango markup. + +*current-only*: ++ + typeof: bool ++ + default: false ++ + If true, only the currently active workspace button is shown. + +*hide-empty*: ++ + typeof: bool ++ + default: false ++ + If true, buttons for empty (client_count == 0) workspaces are hidden, unless the workspace is active. + +*on-click*: ++ + typeof: string ++ + Command to execute on left click. Typically set to *activate* or *toggle*. + +*on-click-middle*: ++ + typeof: string ++ + Command for middle click. Same actions as *on-click*. + +*on-click-right*: ++ + typeof: string ++ + Command for right click. Same actions as *on-click*. + +*overview-label*: ++ + typeof: string ++ + default: "OVERVIEW" ++ + Label shown on the overview button when the overview is active. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables the module to consume leftover space. + +# FORMAT REPLACEMENTS + +*{value}*: Workspace index (same as *{index}* for unnamed workspaces). + +*{name}*: Workspace name (equal to the index for unnamed workspaces in Mango). + +*{icon}*: Icon selected from *format-icons* based on workspace index and state. + +*{index}*: Numeric index of the workspace. + +*{output}*: Name of the output where the workspace is located. + +# CLICK ACTIONS + +When a tag button is clicked, the action from *on-click* (or its middle/right variants) is evaluated. +Supported actions: + +- *activate*: dispatch view, +- *toggle*: dispatch toggleview, + +When the overview button is clicked, the actions change to: + +- *activate*: dispatch overview +- *toggle*: dispatch toggleoverview + +# EXAMPLES + +``` +"mango/workspaces": { + "format": "{icon}", + "format-icons": { + "1": "一", + "2": "二", + "active": "", + "default": "", + "urgent": "", + "empty": "" + }, + "on-click": "activate", + "on-click-right": "toggle", + "overview-label": "" +} +``` + +# STYLE + +- *#workspaces button* +- *#workspaces button.active* – the workspace is active (visible) on its output. +- *#workspaces button.urgent* – the workspace has at least one urgent window. +- *#workspaces button.empty* – the workspace contains no clients. +- *#workspaces button.current_output* – the workspace belongs to the output where the bar is shown. +- *#workspaces button.overview* – the overview button (visible in overview mode). \ No newline at end of file diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index d310efd4..d5ba6405 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -29,6 +29,11 @@ Addressed by *mpd* default: 5 ++ The interval in which the connection to the MPD server is retried +*playing-interval*: ++ + typeof: integer++ + default: 1000 ++ + The interval (in milliseconds) in which the playing state is updated. + *timeout*: ++ typeof: integer++ default: 30 ++ diff --git a/man/waybar-niri-language.5.scd b/man/waybar-niri-language.5.scd index 78cf14f8..d58408d8 100644 --- a/man/waybar-niri-language.5.scd +++ b/man/waybar-niri-language.5.scd @@ -21,6 +21,10 @@ Addressed by *niri/language* typeof: string++ Provide an alternative name to display per language where is the language of your choosing. Can be passed multiple times with multiple languages as shown by the example below. +*format--* ++ + typeof: string ++ + Like *format-* but also matches the layout variant, taking precedence over *format-* when both the language and variant match. + *menu*: ++ typeof: string ++ Action that popups the menu. diff --git a/man/waybar-niri-window.5.scd b/man/waybar-niri-window.5.scd index 8e886c29..c836f057 100644 --- a/man/waybar-niri-window.5.scd +++ b/man/waybar-niri-window.5.scd @@ -48,6 +48,10 @@ See the output of "niri msg windows" for examples *{app_id}*: The current app ID of the focused window. +*{col}*: The current column of the focused window in the workspace. + +*{max_col}*: The maximum column inside the workspace of the focused window. + # REWRITE RULES *rewrite* is an object where keys are regular expressions and values are diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 4be85eb3..48c02df5 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -17,6 +17,28 @@ Addressed by *niri/workspaces* default: false ++ If set to false, workspaces will only be shown on the output they are on. If set to true all workspaces will be shown on every output. +*sort-by-name*: ++ + typeof: bool ++ + default: false ++ + Sort workspaces by name (numeric sort when all names are numbers). Unnamed workspaces fall back to their index on the output. + +*sort-by-coordinates*: ++ + typeof: bool ++ + default: false ++ + Sort workspaces by output and index. If both *sort-by-name* and *sort-by-coordinates* are true, sorting by name is applied. + +*sort-by-id*: ++ + typeof: bool ++ + default: false ++ + Sort workspaces by id, taking precedence over other sort options. + +*sort-by-number*: ++ + typeof: bool ++ + default: false ++ + Deprecated alias for *sort-by-id*; prefer *sort-by-id* instead. + +If none of the sorting options are enabled, workspaces keep their output/index order. + *format*: ++ typeof: string ++ default: {value} ++ @@ -31,6 +53,11 @@ Addressed by *niri/workspaces* default: false ++ If set to false, you can click to change workspace. If set to true this behaviour is disabled. +*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. + *disable-markup*: ++ typeof: bool ++ default: false ++ @@ -41,6 +68,11 @@ Addressed by *niri/workspaces* default: false ++ If set to true, only the active or focused workspace will be shown. +*hide-empty*: ++ + typeof: bool ++ + default: false ++ + If set to true, empty workspaces will not be shown. + *on-update*: ++ typeof: string ++ Command to execute when the module is updated. @@ -63,6 +95,8 @@ as defined by niri. *{output}*: Output where the workspace is located. +*{total}*: The total number of workspaces. + # ICONS Additional to workspace name matching, the following *format-icons* can be set. diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd index 6488767b..da331795 100644 --- a/man/waybar-power-profiles-daemon.5.scd +++ b/man/waybar-power-profiles-daemon.5.scd @@ -25,8 +25,8 @@ $XDG_CONFIG_HOME/waybar/config :[ Message displayed on the bar. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. |[ *tooltip-format* :[ string -:[ "Power profile: {profile}\\nDriver: {driver}" -:[ Messaged displayed in the module tooltip. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. +:[ "Power profile: {profile}\\nCPU driver: {cpu_driver}\\nPlatform driver: {platform_driver}" +:[ Messaged displayed in the module tooltip. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. {cpu_driver} and {platform_driver} are substituted with the CPU and platform drivers reported by recent power-profiles-daemon versions. {driver} is kept for backward compatibility: it resolves to the legacy single driver on older daemons and falls back to the CPU driver on recent ones. |[ *tooltip* :[ bool :[ true @@ -51,7 +51,7 @@ Compact display (default config): ``` "power-profiles-daemon": { "format": "{icon}", - "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip-format": "Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}", "tooltip": true, "format-icons": { "default": "", @@ -67,7 +67,7 @@ Display the full profile name: ``` "power-profiles-daemon": { "format": "{icon} {profile}", - "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip-format": "Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}", "tooltip": true, "format-icons": { "default": "", diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index 0271e7c5..ef2988ff 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -28,20 +28,50 @@ The volume can be controlled by dragging the slider across the bar or clicking o The orientation of the slider. Can be either `horizontal` or `vertical`. *expand*: ++ - typeof: bool ++ - default: false ++ - Enables this module to consume all left over space dynamically. + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +*zero-on-mute*: ++ + typeof: bool ++ + default: true ++ + `true` = The slider will be set to `min` when the source/sink is muted. ++ + `false` = The slider will continue to show the unmuted volume level when the source/sink is muted. + +*unmute-on-volume-change*: ++ + typeof: bool ++ + default: true ++ + Specifies whether to unmute a muted souce/sink when its volume is changed by the user moving the slider. + +*target*: ++ + typeof: string ++ + default: sink ++ + The audio target to control. Can be either `sink` (output/speakers) or `source` (input/microphone). + +*ignored-sinks*: ++ + typeof: array ++ + default: empty ++ + A list of sink descriptions to ignore when tracking the default sink, so switching to those sinks does not update the slider. # EXAMPLES ``` "modules-right": [ - "pulseaudio/slider", + "pulseaudio/slider#out", + "pulseaudio/slider#in", ], -"pulseaudio/slider": { +"pulseaudio/slider#out": { "min": 0, "max": 100, - "orientation": "horizontal" + "orientation": "horizontal", + "zero-on-mute": false, + "unmute-on-volume-change": false +}, +"pulseaudio/slider#in": { + "min": 0, + "max": 100, + "orientation": "horizontal", + "target": "source" } ``` @@ -52,6 +82,9 @@ The slider is a component with multiple CSS Nodes, of which the following are ex *#pulseaudio-slider*: ++ Controls the style of the box *around* the slider and bar. +*#pulseaudio-slider.muted*: ++ + Controls the style when the audio source/sink is muted. + *#pulseaudio-slider slider*: ++ Controls the style of the slider handle. @@ -85,4 +118,8 @@ The slider is a component with multiple CSS Nodes, of which the following are ex border-radius: 5px; background: green; } + +#pulseaudio-slider.muted highlight { + background-color: orange; +} ``` diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index a52d5b3e..553069c5 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -117,6 +117,10 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c typeof: array ++ Sinks in this list will not be shown as active sink by Waybar. Entries should be the sink's description field. +*sink-mapping*: ++ + typeof: object ++ + Sinks named by the values of this mapping will be considered to be the current sink instead of the sinks named by the respective keys. + *menu*: ++ typeof: string ++ Action that popups the menu. @@ -135,6 +139,11 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c default: false ++ Enables this module to consume all left over space dynamically. +*target*: ++ + typeof: string ++ + default: sink ++ + The audio target to control when scrolling. Can be either `sink` (output/speakers) or `source` (input/microphone). + # FORMAT REPLACEMENTS *{desc}*: Pulseaudio port's description, for bluetooth it'll be the device name. diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index 64621229..7d6e9c7c 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -26,6 +26,14 @@ Addressed by *river/tags* default: false ++ If set to false, you can left-click to set focused tag. Right-click to toggle tag focus. If set to true this behaviour is disabled. +*set-tags*: ++ + typeof: array ++ + An array of tag bitmasks, one per tag button. Left-clicking a tag sets the focused tags to the corresponding bitmask instead of the single tag. Requires *disable-click* to be false. + +*toggle-tags*: ++ + typeof: array ++ + An array of tag bitmasks, one per tag button. Right-clicking a tag toggles the corresponding bitmask in the focused tags instead of the single tag. Requires *disable-click* to be false. + *expand*: ++ typeof: bool ++ default: false ++ @@ -50,10 +58,14 @@ Addressed by *river/tags* - *#tags button.occupied* - *#tags button.focused* - *#tags button.urgent* +- *#tags button.output* +- *#tags button.tag-N* -Note that occupied/focused/urgent status may overlap. That is, a tag may be +Note that occupied/focused/urgent/output status may overlap. That is, a tag may be both occupied and focused at the same time. +The *output* style is applied when the river output (e.g. monitor) of the current bar is focused. + # SEE ALSO waybar(5), river(1) diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in index b11e15bd..b16ffdcf 100644 --- a/man/waybar-styles.5.scd.in +++ b/man/waybar-styles.5.scd.in @@ -8,13 +8,27 @@ waybar-styles - using stylesheets for waybar Waybar uses Cascading Style Sheets (CSS) to configure its appearance. -It uses the first file found in this search order: +When the system appearance is light, Waybar first looks for *style-light.css*. +When the system appearance is dark, Waybar first looks for *style-dark.css*. +If no appearance-specific stylesheet is found, it falls back to *style.css*. -- *$XDG_CONFIG_HOME/waybar/style.css* -- *~/.config/waybar/style.css* -- *~/waybar/style.css* -- */etc/xdg/waybar/style.css* -- *@sysconfdir@/xdg/waybar/style.css* +Waybar uses the first file found in this search order for each stylesheet name: + +- *$XDG_CONFIG_HOME/waybar/* +- *~/.config/waybar/* +- *~/waybar/* +- */etc/xdg/waybar/* +- *@sysconfdir@/xdg/waybar/* + +For example, a light theme is loaded from the first available file among: + +- *$XDG_CONFIG_HOME/waybar/style-light.css* +- *~/.config/waybar/style-light.css* +- *~/waybar/style-light.css* +- */etc/xdg/waybar/style-light.css* +- *@sysconfdir@/xdg/waybar/style-light.css* + +If no file is found there, Waybar repeats the same search using *style.css*. # EXAMPLE @@ -43,18 +57,17 @@ You can apply special styling to any module for when the cursor hovers it. Most, if not all, module types support setting the `cursor` option. This is configured in your `config.jsonc`. If set to `false`, when hovering the module a -"pointer"(as commonly known from web CSS styling `cursor: pointer`) style cursor +"pointer" (as commonly known from web CSS styling `cursor: pointer`) style cursor will not be shown. Default behavior is to indicate an interaction event is available. -There are more cursor types to choose from by setting the `cursor` option to -a number, see Gdk3 official docs for all possible cursor types: -https://docs.gtk.org/gdk3/enum.CursorType.html. -However, note that not all cursor options listed may be available on -your system. If you attempt to use a cursor which is not available, the -application will crash. +If set to a string value, it must be a valid cursor name +(e.g. `"pointer"`, `"default"`, `"grab"`, `"text"`, `"crosshair"`, etc.), +see the cursor-shape-v1 protocol for all possible cursor types: +https://wayland.app/protocols/cursor-shape-v1#wp_cursor_shape_device_v1:enum:shape. +Depending on the compositor and cursor theme used, cursors not listed in the protocol may also work. -Example of disabling pointer(`Gdk::Hand2`) cursor type on a custom module: +Example of disabling the cursor on a custom module: ``` "custom/my-custom-module": { @@ -63,13 +76,12 @@ Example of disabling pointer(`Gdk::Hand2`) cursor type on a custom module: } ``` -Example of setting cursor type to `Gdk::Boat`(according to -https://docs.gtk.org/gdk3/enum.CursorType.html#boat): +Example of setting the cursor type to `"grab"`: ``` "custom/my-custom-module": { ... - "cursor": 8, + "cursor": "grab", } ``` diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 3d4e3ad0..b90f860c 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -22,6 +22,11 @@ Addressed by *sway/workspaces* default: {value} ++ The format, how information should be displayed. +*format-for-negative-index*: ++ + typeof: string ++ + default: *format* ++ + An alternative format, which will be used for workspaces with no explict or negative index ("num" in sway terms). Requires *format* to be set. + *format-icons*: ++ typeof: array ++ Based on the workspace name and state, the corresponding icon gets selected. See *icons*. @@ -65,6 +70,11 @@ Addressed by *sway/workspaces* default: false ++ If set to true. Only focused workspaces will be shown. +*ignore-workspaces*: ++ + typeof: array ++ + default: empty ++ + List of regular expressions. Workspaces whose name matches any of the given patterns are not shown. + *persistent-workspaces*: ++ typeof: json (see below) ++ default: empty ++ @@ -110,6 +120,12 @@ warp-on-scroll: ++ default: false ++ Enables this module to consume all left over space dynamically. +*output-classes*: ++ + typeof: array ++ + Specify additional CSS classes to be added to workspace indicators based on the output on which the associated workspace is located. + Keys are output names and values are class names like *${output}: {output-class}*. + Assignment in config is used to keep the stylesheet independent of the available outputs. + # FORMAT REPLACEMENTS @@ -147,6 +163,7 @@ an empty list denoting all outputs. "3": [], // Always show a workspace with name '3', on all outputs if it does not exist "4": ["eDP-1"], // Always show a workspace with name '4', on output 'eDP-1' if it does not exist "5": ["eDP-1", "DP-2"] // Always show a workspace with name '5', on outputs 'eDP-1' and 'DP-2' if it does not exist + "6": ["MonitorMaker 3000 ABC0123"], // Always show a workspace with name '6' on outputs with an identifier 'MonitorMaker 3000 ABC0123' (usually a triple of vendor/model/serial) } } ``` @@ -187,6 +204,13 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge } ``` +``` +"sway/workspaces": { + "format": "{index} - {name}", + "format-for-negative-index": "{name}" +} +``` + # Style - *#workspaces button* @@ -197,3 +221,4 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge - *#workspaces button.empty* - *#workspaces button.current_output* - *#workspaces button#sway-workspace-${name}* +- *#workspaces button.${output-class}* diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 1e19ec82..0494e989 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -27,9 +27,20 @@ Addressed by *temperature* The path of the hwmon-directory of the device, e.g. */sys/devices/pci0000:00/0000:00:18.3/hwmon*. (Note that the subdirectory *hwmon/hwmon#*, where *#* is a number is not part of the path!) Has to be used together with *input-filename*. This can also be an array of strings, for which, it just works like *hwmon-path*. +*hwmon-name*: ++ + typeof: string ++ + Select a hwmon device by its name (from /sys/class/hwmon/\*/name), e.g. *amdgpu*. + Requires *input-filename* to be set. + Cannot be used together with *hwmon-path* or *hwmon-path-abs*. + *input-filename*: ++ typeof: string ++ - The temperature filename of your *hwmon-path-abs*, e.g. *temp1_input* + The temperature filename of your *hwmon-path-abs* (also used by *hwmon-by-name*), e.g. *temp1_input* + +*hwmon-by-name*: ++ + typeof: string ++ + The substring to search for in */sys/class/hwmon/hwmonX/name* (where hwmonX is any folder in */sys/class/hwmon/*). + Waybar will search for every directory in */sys/class/hwmon/* and uses the directory in which the *name* matches *hwmon-by-name*. *warning-threshold*: ++ typeof: integer ++ diff --git a/man/waybar-wayfire-workspaces.5.scd b/man/waybar-wayfire-workspaces.5.scd index 53a179e8..04f3be52 100644 --- a/man/waybar-wayfire-workspaces.5.scd +++ b/man/waybar-wayfire-workspaces.5.scd @@ -26,6 +26,16 @@ Addressed by *wayfire/workspaces* default: false ++ If set to false, you can click to change workspace. If set to true this behaviour is disabled. +*disable-scroll*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can scroll to cycle through workspaces. If set to true this behaviour is disabled. + +*enable-bar-scroll*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can't scroll to cycle through workspaces from the entire bar. If set to true this behaviour is enabled. + *disable-markup*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index 8d97d7af..c1a69e18 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -63,6 +63,11 @@ The *wireplumber* module displays the current volume reported by WirePlumber. default: 1.0 ++ The speed at which to change the volume when scrolling. +*scroll-scale*: ++ + typeof: string ++ + default: cubic_percent + The scale to use for the scrolling volume change. Options are 'linear', 'db', 'cubic', and 'cubic_percent'. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. @@ -107,7 +112,13 @@ The *wireplumber* module displays the current volume reported by WirePlumber. # FORMAT REPLACEMENTS -*{volume}*: Volume in percentage. +*{volume}*: Volume in percentage, cubic scale as integer. + +*{volume_linear}*: Volume in linear scale as float. + +*{volume_cubic}*: Volume in cubic scale as float. + +*{volume_db}*: Volume in decibel scale as float. *{node_name}*: The node's nickname as reported by WirePlumber (*node.nick* property) @@ -123,7 +134,7 @@ The *wireplumber* module displays the current volume reported by WirePlumber. } ``` -## Separate Sink and Source Widgets +## Separate Sink and Source Widgets ``` "wireplumber#sink": { @@ -143,6 +154,16 @@ The *wireplumber* module displays the current volume reported by WirePlumber. } ``` +## Use a Different Scale + +``` +"wireplumber": { + "format": "{volume_db:.2f}dB", + "scroll-scale": "db", + "scroll-step": 2.5 +} +``` + # STYLE - *#wireplumber* diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index af1ba97f..734c7915 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -18,6 +18,13 @@ Addressed by *wlr/taskbar* default: false ++ If set to false applications on the waybar's current output will be shown. Otherwise, all applications are shown. +*bar-css-states*: ++ + typeof: bool ++ + default: false ++ + If set to true, application state is exposed as CSS classes on the Waybar + window. Maximized and fullscreen state is aggregated across applications + known to belong to the active workspace. See *Bar state style* below. + *format*: ++ typeof: string ++ default: {icon} ++ @@ -52,11 +59,26 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, always reorder the tasks in the taskbar so that the currently active one is first. Otherwise don't reorder. +*active-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only the currently active application button is shown. + Other applications remain tracked and reappear when activated. + *sort-by-app-id*: ++ typeof: bool ++ default: false ++ If set to true, group tasks by their app_id. Cannot be used with 'active-first'. +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's box, allowing options 'left', 'right', or 'center' to define the positioning. + +*expand*: ++ + typeof: bool ++ + default: false ++ + If set to true, task buttons stretch to fill the available space in the taskbar and long titles are ellipsized to fit. Only takes effect on a horizontal bar; on a vertical bar the buttons keep their content-based size. If set to false, buttons are sized to their content. + *on-click*: ++ typeof: string ++ The action which should be triggered when clicking on the application button with the left mouse button. @@ -156,3 +178,35 @@ Invalid expressions (e.g., mismatched parentheses) are skipped. - *#taskbar button.minimized* - *#taskbar button.active* - *#taskbar button.fullscreen* + +# Bar state style + +When *bar-css-states* is enabled, the following classes are added to +*window#waybar*: + +- *window#waybar.toplevel-active* +- *window#waybar.toplevel-maximized* +- *window#waybar.toplevel-minimized* +- *window#waybar.toplevel-fullscreen* + +The active, minimized classes describe the active application. The maximized +and fullscreen classes are set if any non-minimized application known to belong +to the active workspace has that state. + +Workspace membership is learned when an application is activated and requires +the compositor to support *ext-workspace-v1*. Before an application has been +activated during the current Waybar session, its workspace may be unknown. On +compositors without *ext-workspace-v1*, these classes fall back to the active +application's state. + +For example: + +``` +window#waybar { + background-color: rgba(0, 0, 0, 0.5); +} + +window#waybar.toplevel-maximized { + background-color: rgba(0, 0, 0, 1); +} +``` diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 8b923eef..25a1da97 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -19,7 +19,7 @@ Valid locations for this file are: A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config.jsonc Also, a minimal example configuration can be found at the bottom of this man page. -The visual display elements for waybar use a CSS stylesheet, see *waybar-styles(5)* for details. +The visual display elements for waybar use a CSS stylesheet. Waybar supports *style.css* and the appearance-specific *style-light.css* and *style-dark.css* files; see *waybar-styles(5)* for details. # BAR CONFIGURATION @@ -168,6 +168,7 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( # MODULE FORMAT You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). +Tooltip appearance is generally controlled globally via CSS and cannot be truly scoped per module. Some modules (such as the clock) provide limited workarounds for customization. e.g. @@ -374,6 +375,13 @@ A group may hide all but one element, showing them only on mouse hover. In order Defines the direction of the transition animation. If true, the hidden elements will slide from left to right. If false, they will slide from right to left. When the bar is vertical, it reads as top-to-bottom. +*reveal-by-default*: ++ + typeof: bool ++ + default: false ++ + Whether the child should be revealed when Waybar starts up. This has to be used with click-to-reveal to take effect. + +Group drawers are also given the `.expanded` CSS class when they are expanded. + ``` "group/power": { "orientation": "inherit", diff --git a/meson.build b/meson.build index 0c494eb2..a0fad843 100644 --- a/meson.build +++ b/meson.build @@ -111,6 +111,10 @@ gtk_layer_shell = dependency('gtk-layer-shell-0', version: ['>=0.9.0'], default_options: ['introspection=false', 'vapi=false'], fallback: ['gtk-layer-shell', 'gtk_layer_shell']) systemd = dependency('systemd', required: get_option('systemd')) +libsystemd = dependency('libsystemd', required: get_option('systemd')) +if libsystemd.found() + add_project_arguments('-DHAVE_LIBSYSTEMD', language: 'cpp') +endif cpp_lib_chrono = compiler.compute_int('__cpp_lib_chrono', prefix : '#include ') have_chrono_timezones = cpp_lib_chrono >= 201611 @@ -159,11 +163,13 @@ endif src_files = files( 'src/factory.cpp', + 'src/AGraph.cpp', 'src/AModule.cpp', 'src/ALabel.cpp', 'src/AIconLabel.cpp', 'src/AAppIconLabel.cpp', 'src/modules/custom.cpp', + 'src/modules/custom_graph.cpp', 'src/modules/disk.cpp', 'src/modules/idle_inhibitor.cpp', 'src/modules/image.cpp', @@ -182,11 +188,13 @@ src_files = files( 'src/util/ustring_clen.cpp', 'src/util/sanitize_str.cpp', 'src/util/rewrite_string.cpp', + 'src/util/hosts_check.cpp', 'src/util/gtk_icon.cpp', 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', 'src/util/css_reload_helper.cpp', - 'src/util/transform_8bit_to_rgba.cpp' + 'src/util/transform_8bit_to_rgba.cpp', + 'src/util/utf8_string.cpp' ) man_files = files( @@ -210,6 +218,7 @@ if is_linux 'src/modules/bluetooth.cpp', 'src/modules/cffi.cpp', 'src/modules/cpu.cpp', + 'src/modules/cpu_graph.cpp', 'src/modules/cpu_frequency/common.cpp', 'src/modules/cpu_frequency/linux.cpp', 'src/modules/cpu_usage/common.cpp', @@ -234,6 +243,7 @@ elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd src_files += files( 'src/modules/cffi.cpp', 'src/modules/cpu.cpp', + 'src/modules/cpu_graph.cpp', 'src/modules/cpu_frequency/bsd.cpp', 'src/modules/cpu_frequency/common.cpp', 'src/modules/cpu_usage/bsd.cpp', @@ -348,6 +358,25 @@ if get_option('niri') ) endif +if get_option('mango') + add_project_arguments('-DHAVE_MANGO', language: 'cpp') + src_files += files( + 'src/modules/mango/backend.cpp', + 'src/modules/mango/language.cpp', + 'src/modules/mango/keymode.cpp', + 'src/modules/mango/window.cpp', + 'src/modules/mango/workspaces.cpp', + 'src/modules/mango/layout.cpp' + ) + man_files += files( + 'man/waybar-mango-language.5.scd', + 'man/waybar-mango-keymode.5.scd', + 'man/waybar-mango-window.5.scd', + 'man/waybar-mango-workspaces.5.scd', + 'man/waybar-mango-layout.5.scd' + ) +endif + if true add_project_arguments('-DHAVE_WAYFIRE', language: 'cpp') src_files += files( @@ -550,6 +579,7 @@ executable( upower_glib, pipewire, playerctl, + libsystemd, libpulse, libjack, libwireplumber, diff --git a/meson_options.txt b/meson_options.txt index c0f63f28..278fdd02 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -12,7 +12,7 @@ option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable supp option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') option('mpd', type: 'feature', value: 'auto', description: 'Enable support for the Music Player Daemon') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') -option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio') +option('sndio', type: 'feature', value: 'auto', description: 'Enable support for volume control via sndio') option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind') option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('experimental', type : 'boolean', value : false, description: 'Enable experimental features') @@ -20,5 +20,6 @@ option('jack', type: 'feature', value: 'auto', description: 'Enable support for option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') option('cava', type: 'feature', value: 'auto', description: 'Enable support for Cava') option('niri', type: 'boolean', description: 'Enable support for niri') +option('mango', type: 'boolean', description: 'Enable support for mango') option('login-proxy', type: 'boolean', description: 'Enable interfacing with dbus login interface') option('gps', type: 'feature', value: 'auto', description: 'Enable support for gps') diff --git a/protocol/ext-idle-notify-v1.xml b/protocol/ext-idle-notify-v1.xml new file mode 100644 index 00000000..db7d9c16 --- /dev/null +++ b/protocol/ext-idle-notify-v1.xml @@ -0,0 +1,131 @@ + + + + Copyright © 2015 Martin Gräßlin + Copyright © 2022 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This interface allows clients to monitor user idle status. + + After binding to this global, clients can create ext_idle_notification_v1 + objects to get notified when the user is idle for a given amount of time. + + + + + Destroy the manager object. All objects created via this interface + remain valid. + + + + + + Create a new idle notification object. + + The notification object has a minimum timeout duration and is tied to a + seat. The client will be notified if the seat is inactive for at least + the provided timeout. See ext_idle_notification_v1 for more details. + + A zero timeout is valid and means the client wants to be notified as + soon as possible when the seat is inactive. + + + + + + + + + + + Create a new idle notification object to track input from the + user, such as keyboard and mouse movement. Because this object is + meant to track user input alone, it ignores idle inhibitors. + + The notification object has a minimum timeout duration and is tied to a + seat. The client will be notified if the seat is inactive for at least + the provided timeout. See ext_idle_notification_v1 for more details. + + A zero timeout is valid and means the client wants to be notified as + soon as possible when the seat is inactive. + + + + + + + + + + + This interface is used by the compositor to send idle notification events + to clients. + + Initially the notification object is not idle. The notification object + becomes idle when no user activity has happened for at least the timeout + duration, starting from the creation of the notification object. User + activity may include input events or a presence sensor, but is + compositor-specific. + + How this notification responds to idle inhibitors depends on how + it was constructed. If constructed from the + get_idle_notification request, then if an idle inhibitor is + active (e.g. another client has created a zwp_idle_inhibitor_v1 + on a visible surface), the compositor must not make the + notification object idle. However, if constructed from the + get_input_idle_notification request, then idle inhibitors are + ignored, and only input from the user, e.g. from a keyboard or + mouse, counts as activity. + + When the notification object becomes idle, an idled event is sent. When + user activity starts again, the notification object stops being idle, + a resumed event is sent and the timeout is restarted. + + + + + Destroy the notification object. + + + + + + This event is sent when the notification object becomes idle. + + It's a compositor protocol error to send this event twice without a + resumed event in-between. + + + + + + This event is sent when the notification object stops being idle. + + It's a compositor protocol error to send this event twice without an + idled event in-between. It's a compositor protocol error to send this + event prior to any idled event. + + + + diff --git a/protocol/meson.build b/protocol/meson.build index b16113b2..3da6ee47 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -29,6 +29,7 @@ client_protocols = [ ['river-status-unstable-v1.xml'], ['river-control-unstable-v1.xml'], ['dwl-ipc-unstable-v2.xml'], + ['ext-idle-notify-v1.xml'], ] if wayland_protos.version().version_compare('>=1.39') diff --git a/resources/config.jsonc b/resources/config.jsonc index 9ba8b97a..b616d182 100644 --- a/resources/config.jsonc +++ b/resources/config.jsonc @@ -155,7 +155,7 @@ }, "power-profiles-daemon": { "format": "{icon}", - "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip-format": "Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}", "tooltip": true, "format-icons": { "default": "", 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/resources/custom_modules/power_menu.xml b/resources/custom_modules/power_menu.xml index aa2a42ca..972499d7 100644 --- a/resources/custom_modules/power_menu.xml +++ b/resources/custom_modules/power_menu.xml @@ -2,27 +2,27 @@ - - Suspend - - - - - Hibernate - - + + Suspend + + - - Shutdown - + + Hibernate + + + + + Shutdown + - - Reboot - + + Reboot + diff --git a/src/AGraph.cpp b/src/AGraph.cpp new file mode 100644 index 00000000..f79e7218 --- /dev/null +++ b/src/AGraph.cpp @@ -0,0 +1,298 @@ +#include "AGraph.hpp" + +#include +#include + +#include +#include +#include +#include + +#include "config.hpp" + +namespace waybar { + +AGraph::AGraph(const Json::Value& config, const std::string& name, const std::string& id, + uint16_t interval, bool enable_click, bool enable_scroll) + : AModule(config, name, id, + config["format-alt"].isString() || config["menu"].isString() || enable_click, + enable_scroll), + interval_(config_["interval"] == "once" + ? std::chrono::seconds::max() + : std::chrono::seconds( + config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)) { + graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw)); + graph_.set_name(name); + if (!id.empty()) { + graph_.get_style_context()->add_class(id); + } + graph_.get_style_context()->add_class(MODULE_CLASS); + if (config_["width"].isUInt()) { + graph_.set_size_request(config_["width"].asUInt(), -1); + } else { + graph_.set_size_request(100, -1); + } + + event_box_.add(graph_); + + if (config_["datapoints"].isUInt() && config_["datapoints"].asUInt() > 0) { + datapoints_ = config_["datapoints"].asUInt(); + } + + if (config_["graph_type"].isString()) { + std::string type = config_["graph_type"].asString(); + if (type == "line") { + graph_type_ = GraphType::LINE; + } else if (type == "bar") { + graph_type_ = GraphType::BAR; + } else if (type == "gauge") { + graph_type_ = GraphType::GAUGE; + } + } + + // If a GTKMenu is requested in the config + if (config_["menu"].isString()) { + // Create the GTKMenu widget + try { + // Check that the file exists + std::string menuFile = config_["menu-file"].asString(); + + // there might be "~" or "$HOME" in original path, try to expand it. + auto result = Config::tryExpandPath(menuFile, ""); + if (result.empty()) { + throw std::runtime_error("Failed to expand file: " + menuFile); + } + + menuFile = result.front(); + // Read the menu descriptor file + std::ifstream file(menuFile); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + menuFile); + } + std::stringstream fileContent; + fileContent << file.rdbuf(); + GtkBuilder* builder = gtk_builder_new(); + + // Make the GtkBuilder and check for errors in his parsing + if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) { + throw std::runtime_error("Error found in the file " + menuFile); + } + + menu_ = gtk_builder_get_object(builder, "menu"); + if (menu_ == nullptr) { + throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); + } + submenus_ = std::map(); + menuActionsMap_ = std::map(); + + // Linking actions to the GTKMenu based on + 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())); + menuActionsMap_[key] = it->asString(); + g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), + (gpointer)menuActionsMap_[key].c_str()); + } + } catch (std::runtime_error& e) { + spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what()); + } + } +} + +auto AGraph::update() -> void { + graph_.queue_draw(); + AModule::update(); +} + +void AGraph::handleGtkMenuEvent(GtkMenuItem* /*menuitem*/, gpointer data) { + waybar::util::command::res res = waybar::util::command::exec((char*)data, "GtkMenu"); +} + +void AGraph::addValue(const int n) { + if (datapoints_ > 0 && values_.size() >= datapoints_) { + values_.pop_front(); + } + values_.push_back(n); +} + +bool AGraph::onDraw(const Cairo::RefPtr& cr) { + const int width = graph_.get_allocated_width(); + const int height = graph_.get_allocated_height() - 1; + + if (values_.empty() || width <= 0 || height <= 0) { + return false; + } + + auto style_context = graph_.get_style_context(); + Gdk::RGBA fg_color = style_context->get_color(Gtk::STATE_FLAG_NORMAL); + Gdk::RGBA bg_color = fg_color; + bg_color.set_alpha(0.3); + + cr->set_line_width(1.0); + + const double step_width = static_cast(width) / datapoints_; + const int values_count = values_.size(); + const int empty_space = datapoints_ - values_count; + + std::vector> points; + points.reserve(values_count); + + for (int i = empty_space; i < datapoints_; ++i) { + double x = i * step_width; + int value_index = i - empty_space; + int value = values_[value_index]; + double y = height - (static_cast(value) / 100.0 * height); + points.emplace_back(x, y); + } + if (!points.empty()) { + switch (graph_type_) { + case GraphType::LINE: + drawFilledArea(cr, points, height, bg_color); + drawLine(cr, points, fg_color); + break; + case GraphType::BAR: + drawBars(cr, width, height, values_.empty() ? 0 : values_.back(), fg_color); + break; + case GraphType::GAUGE: + drawGauge(cr, width, height, values_.empty() ? 0 : values_.back(), fg_color); + break; + } + } + + return false; +} +void AGraph::drawFilledArea(const Cairo::RefPtr& cr, + const std::vector>& points, double height, + const Gdk::RGBA& bg_color) { + if (points.empty()) return; + + double first_x = points.front().first; + double last_x = points.back().first; + + drawPath(cr, points); + + cr->line_to(last_x, height); + cr->line_to(first_x, height); + cr->close_path(); + + cr->set_source_rgba(bg_color.get_red(), bg_color.get_green(), bg_color.get_blue(), + bg_color.get_alpha()); + cr->fill(); +} + +void AGraph::drawLine(const Cairo::RefPtr& cr, + const std::vector>& points, + const Gdk::RGBA& fg_color) { + if (points.empty()) return; + + cr->begin_new_path(); + drawPath(cr, points); + + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + fg_color.get_alpha()); + cr->stroke(); +} + +void AGraph::drawPath(const Cairo::RefPtr& cr, + const std::vector>& points) { + if (points.empty()) return; + + bool first_point = true; + for (const auto& point : points) { + if (first_point) { + cr->move_to(point.first, point.second); + first_point = false; + } else { + cr->line_to(point.first, point.second); + } + } +} + +void AGraph::drawBars(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color) { + current_value = std::min(100, std::max(0, current_value)); + + double green_height = height * (std::min(current_value, 40) / 100.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.5); + cr->rectangle(0, height - green_height, width, green_height); + cr->fill(); + + if (current_value > 40) { + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.7); + cr->rectangle(0, height - green_height - yellow_height, width, yellow_height); + cr->fill(); + } + + if (current_value > 75) { + double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.85); + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + cr->rectangle(0, height - green_height - yellow_height - orange_height, width, orange_height); + cr->fill(); + } + + if (current_value > 85) { + double red_height = height * (current_value - 85) / 100.0; + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 1.0); + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; + cr->rectangle(0, height - green_height - yellow_height - orange_height - red_height, width, + red_height); + cr->fill(); + } + + double value_height = height * (current_value / 100.0); + cr->set_source_rgba(0.2, 0.2, 0.2, 0.8); + cr->rectangle(0, height - value_height, width, 2); + cr->fill(); +} + +void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color) { + double center_x = width / 2.0; + double center_y = height; + double radius = height / 2.0; + + cr->set_line_width(10.0); + + double angle1 = M_PI; + double angle2 = angle1 + 0.3 * angle1; + + // Green section (0-33%) + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.5); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Yellow section (33-66%) + angle1 = angle2; + angle2 = angle1 + 0.3 * angle1; + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.75); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Red section (66-100%) + angle1 = angle2; + angle2 = 0.0; + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 1.0); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Draw needle + double percentage = std::min(100, std::max(0, current_value)) / 100.0; + double needle_angle = M_PI * percentage; + double needle_length = radius; + + double needle_x = center_x - needle_length * cos(needle_angle); + double needle_y = center_y - needle_length * sin(needle_angle); + + cr->set_source_rgba(1.0, 1.0, 1.0, 1.0); + cr->set_line_width(2.0); + cr->begin_new_path(); + cr->move_to(center_x, center_y); + cr->line_to(needle_x, needle_y); + cr->stroke(); +} + +} // namespace waybar diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index 5c7df0ed..c17eb8d4 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include namespace waybar { @@ -9,6 +11,10 @@ AIconLabel::AIconLabel(const Json::Value& config, const std::string& name, const const std::string& format, uint16_t interval, bool ellipsize, bool enable_click, bool enable_scroll) : ALabel(config, name, id, format, interval, ellipsize, enable_click, enable_scroll) { + if (config["icon-size"].isUInt()) { + app_icon_size_ = config["icon-size"].asUInt(); + } + image_.set_pixel_size(app_icon_size_); event_box_.remove(); label_.unset_name(); label_.get_style_context()->remove_class(MODULE_CLASS); @@ -55,13 +61,54 @@ AIconLabel::AIconLabel(const Json::Value& config, const std::string& name, const event_box_.add(box_); } +std::tuple AIconLabel::extractIcon(const std::string& input) { + std::string icon_result = ""; + std::string label_result = input; + try { + static const std::regex icon_search(R"((?=\\0icon\\1f).+?(?=\\n))"); + std::smatch icon_match; + if (std::regex_search(input, icon_match, icon_search)) { + icon_result = icon_match[0].str().substr(9); + + static const std::regex clean_label_pattern(R"(\\0icon\\1f.+?\\n)"); + label_result = std::regex_replace(input, clean_label_pattern, ""); + } + } catch (const std::exception& e) { + spdlog::warn("Error while parsing icon from label. {}", e.what()); + } + + return std::make_tuple(icon_result, label_result); +} + auto AIconLabel::update() -> void { + label_contains_icon = false; + + auto [iconLabel, cleanLabel] = extractIcon(label_.get_label().c_str()); + label_contains_icon = iconLabel.length() > 0; + + if (label_contains_icon) { + label_.set_markup(cleanLabel); + + if (iconLabel.front() == '/') { + int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); + auto pixbuf = Gdk::Pixbuf::create_from_file(iconLabel, 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); + } else { + image_.set_from_icon_name(iconLabel, Gtk::ICON_SIZE_INVALID); + image_.set_visible(true); + } + } + image_.set_visible(image_.get_visible() && iconEnabled()); ALabel::update(); } bool AIconLabel::iconEnabled() const { - return config_["icon"].isBool() ? config_["icon"].asBool() : false; + return label_contains_icon || (config_["icon"].isBool() ? config_["icon"].asBool() : false); } } // namespace waybar diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 795f87f1..39ebe9e4 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -117,7 +117,7 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st submenus_[key] = GTK_MENU_ITEM(item); menuActionsMap_[key] = it->asString(); g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), - (gpointer)menuActionsMap_[key].c_str()); + (gpointer)g_strdup(menuActionsMap_[key].c_str())); } g_object_unref(builder); } catch (std::runtime_error& e) { @@ -139,6 +139,26 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st auto ALabel::update() -> void { AModule::update(); } +bool ALabel::setLabelMarkup(const Glib::ustring& markup) { + if (last_label_markup_ == markup) { + return false; + } + + label_.set_markup(markup); + last_label_markup_ = markup; + return true; +} + +bool ALabel::setTooltipMarkup(const Glib::ustring& markup) { + if (last_tooltip_markup_ == markup) { + return false; + } + + label_.set_tooltip_markup(markup); + last_tooltip_markup_ = markup; + return true; +} + std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_t max) { auto format_icons = config_["format-icons"]; if (format_icons.isObject()) { @@ -189,6 +209,10 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& return ""; } +void ALabel::copyToClipboard(const std::string& literal) { + Gtk::Clipboard::get()->set_text(literal); +} + bool waybar::ALabel::handleToggle(GdkEventButton* const& e) { if (config_["format-alt-click"].isUInt() && e->button == config_["format-alt-click"].asUInt()) { alt_ = !alt_; @@ -198,6 +222,10 @@ bool waybar::ALabel::handleToggle(GdkEventButton* const& e) { format_ = default_format_; } } + + if (config_["on-click-copy"].isBool() && config_["on-click-copy"].asBool()) { + copyToClipboard(label_.get_text()); + } return AModule::handleToggle(e); } diff --git a/src/AModule.cpp b/src/AModule.cpp index a5ba69d3..ec29576f 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())) { @@ -42,7 +46,7 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) { // True if there is any non-release type event return eventEntry.first.second != GdkEventType::GDK_BUTTON_RELEASE && - config[eventEntry.second].isString(); + (config[eventEntry.second].isString() || config[eventEntry.second].isBool()); }) != eventMap_.cend(); if (enable_click || hasUserEvents) { @@ -72,10 +76,14 @@ 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); - } else if (config_["cursor"].isInt()) { - setCursor(Gdk::CursorType(config_["cursor"].asInt())); + if (config_["cursor"].isBool()) { + if (config_["cursor"].asBool()) { + setCursor("pointer"); + } else { + setCursor("default"); + } + } else if (config_["cursor"].isString()) { + setCursor(config_["cursor"].asString()); } else { spdlog::warn("unknown cursor option configured on module {}", name_); } @@ -83,6 +91,9 @@ 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); @@ -110,15 +121,15 @@ auto AModule::doAction(const std::string& name) -> void { } } -void AModule::setCursor(Gdk::CursorType const& c) { +void AModule::setCursor(std::string const& c) { auto gdk_window = event_box_.get_window(); if (gdk_window) { - auto cursor = Gdk::Cursor::create(c); + auto cursor = Gdk::Cursor::create(gdk_window->get_display(), c); gdk_window->set_cursor(cursor); } 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; @@ -134,7 +145,7 @@ bool AModule::handleMouseEnter(GdkEventCrossing* const& e) { // Default behavior indicating event availability if (hasUserEvents_ && !config_.isMember("cursor")) { - setCursor(Gdk::HAND2); + setCursor("pointer"); } return false; @@ -147,7 +158,7 @@ bool AModule::handleMouseLeave(GdkEventCrossing* const& e) { // Default behavior indicating event availability if (hasUserEvents_ && !config_.isMember("cursor")) { - setCursor(Gdk::ARROW); + setCursor("default"); } return false; diff --git a/src/bar.cpp b/src/bar.cpp index 7ff73c4f..bea77db7 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -3,12 +3,14 @@ #include #include +#include #include #include "client.hpp" #include "factory.hpp" #include "group.hpp" #include "util/enum.hpp" +#include "util/hosts_check.hpp" #include "util/kill_signal.hpp" #ifdef HAVE_SWAY @@ -172,6 +174,10 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) right_.set_spacing(spacing); } + if (config.isMember("height") && !config["height"].isUInt()) { + spdlog::warn("Invalid type for 'height', expected unsigned integer"); + } + height_ = config["height"].isUInt() ? config["height"].asUInt() : 0; width_ = config["width"].isUInt() ? config["width"].asUInt() : 0; @@ -263,6 +269,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; @@ -309,6 +325,19 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) setupWidgets(); window.show_all(); + /* + * If gtk-layer-shell's synchronous wait for the initial configure timed out, show_all() can + * return with a configured but not-yet-presented surface. Kick GTK/layer-shell once control has + * returned to the main loop, when any late initial configure has been dispatched and widgets have + * had a chance to allocate/draw. + */ + Glib::signal_idle().connect(sigc::track_obj([this] { + window.queue_resize(); + window.queue_draw(); + forceLayerCommit(); + return false; + }, *this)); + if (spdlog::should_log(spdlog::level::debug)) { // Unfortunately, this function isn't in the C++ bindings, so we have to call the C version. char* gtk_tree = gtk_style_context_to_string( @@ -374,7 +403,14 @@ void waybar::Bar::setMode(const struct bar_mode& mode) { * gtk-layer-shell schedules a commit on the next frame event in GTK, but this could fail in * certain scenarios, such as fully occluded bar. */ - gtk_layer_try_force_commit(gtk_window); + forceLayerCommit(); +} + +void waybar::Bar::forceLayerCommit() { + auto* gtk_window = window.gobj(); + if (gtk_window != nullptr && gtk_widget_get_realized(GTK_WIDGET(gtk_window))) { + gtk_layer_try_force_commit(gtk_window); + } wl_display_flush(Client::inst()->wl_display); } @@ -531,6 +567,11 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, for (const auto& name : module_list) { try { auto ref = name.asString(); + + if (config[ref].isMember("hosts") && !waybar::util::valid_host(config[ref])) { + continue; + } + AModule* module; if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) { @@ -545,8 +586,8 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, if (group_config["modules"].isNull()) { spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); } - auto group_module = std::make_unique( - id_name, class_name, group_config, vertical); + auto group_module = + std::make_unique(id_name, class_name, group_config, vertical); getModules(factory, ref, group_module.get()); module = group_module.release(); @@ -652,6 +693,17 @@ void waybar::Bar::onConfigure(GdkEventConfigure* ev) { configureGlobalOffset(ev->width, ev->height); spdlog::info(BAR_SIZE_MSG, ev->width, ev->height, output->name); + + /* + * gtk-layer-shell waits for the compositor's initial configure while realizing the window. On a + * busy compositor (common during session startup) that wait can time out even though the initial + * configure arrives shortly afterwards. In that case GTK may not schedule the frame commit that + * presents the first layer-surface buffer, leaving an otherwise configured bar invisible. Force a + * commit after every configure so late initial configures, and later compositor-driven resizes, + * always result in a submitted surface state. + */ + window.queue_draw(); + forceLayerCommit(); } void waybar::Bar::configureGlobalOffset(int width, int height) { @@ -696,3 +748,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 d1489186..71aa06ef 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -3,10 +3,12 @@ #include #include +#include #include #include #include "gtkmm/icontheme.h" +#include "ext-idle-notify-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" #include "util/format.hpp" @@ -39,6 +41,12 @@ void waybar::Client::handleGlobal(void* data, struct wl_registry* registry, uint client->idle_inhibit_manager = static_cast( wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1)); + } else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) { + // Bind version 2 if available (for get_input_idle_notification), otherwise version 1 + auto bind_version = std::min(version, 2u); + client->idle_notifier = static_cast( + wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, bind_version)); + spdlog::debug("Bound ext-idle-notifier-v1 at version {}", bind_version); } } @@ -91,11 +99,14 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o output.xdg_output.reset(); spdlog::debug("Output detection done: {} ({})", output.name, output.identifier); - auto configs = client->getOutputConfigs(output); - if (!configs.empty()) { - for (const auto& config : configs) { - client->bars.emplace_back(std::make_unique(&output, config)); - } + client->pending_outputs_.push_back(&output); + + if (!client->bars_scheduled_) { + client->bars_scheduled_ = true; + + Glib::signal_idle().connect_once([client]() { + client->createBarsBatch(); + }, Glib::PRIORITY_HIGH_IDLE); } } } catch (const std::exception& e) { @@ -103,6 +114,25 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } } +void waybar::Client::createBarsBatch() { + pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [&output](const auto& o) { return &o == output; }); }); + for (auto* output : pending_outputs_) { + try { + auto configs = getOutputConfigs(*output); + if (!configs.empty()) { + for (const auto& config : configs) { + bars.emplace_back(std::make_unique(output, config)); + } + } + } catch (const std::exception& e) { + spdlog::warn("Error creating bar: {}", e.what()); + } + } + + pending_outputs_.clear(); + bars_scheduled_ = false; +} + void waybar::Client::handleOutputName(void* data, struct zxdg_output_v1* /*xdg_output*/, const char* name) { auto* client = waybar::Client::inst(); @@ -306,9 +336,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/factory.cpp b/src/factory.cpp index 2fd3e3b8..6969da6b 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -42,6 +42,13 @@ #include "modules/niri/window.hpp" #include "modules/niri/workspaces.hpp" #endif +#ifdef HAVE_MANGO +#include "modules/mango/keymode.hpp" +#include "modules/mango/language.hpp" +#include "modules/mango/layout.hpp" +#include "modules/mango/window.hpp" +#include "modules/mango/workspaces.hpp" +#endif #ifdef HAVE_WAYFIRE #include "modules/wayfire/window.hpp" #include "modules/wayfire/workspaces.hpp" @@ -52,6 +59,7 @@ #if defined(HAVE_CPU_LINUX) || defined(HAVE_CPU_BSD) #include "modules/cpu.hpp" #include "modules/cpu_frequency.hpp" +#include "modules/cpu_graph.hpp" #include "modules/cpu_usage.hpp" #include "modules/load.hpp" #endif @@ -117,6 +125,7 @@ #include "modules/cava/cava_frontend.hpp" #include "modules/cffi.hpp" #include "modules/custom.hpp" +#include "modules/custom_graph.hpp" #include "modules/image.hpp" #include "modules/temperature.hpp" #include "modules/user.hpp" @@ -231,6 +240,23 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, return new waybar::modules::niri::Workspaces(id, bar_, config_[name]); } #endif +#ifdef HAVE_MANGO + if (ref == "mango/window") { + return new waybar::modules::mango::Window(id, bar_, config_[name]); + } + if (ref == "mango/workspaces") { + return new waybar::modules::mango::Workspaces(id, bar_, config_[name]); + } + if (ref == "mango/language") { + return new waybar::modules::mango::Language(id, bar_, config_[name]); + } + if (ref == "mango/keymode") { + return new waybar::modules::mango::Keymode(id, bar_, config_[name]); + } + if (ref == "mango/layout") { + return new waybar::modules::mango::Layout(id, bar_, config_[name]); + } +#endif #ifdef HAVE_WAYFIRE if (ref == "wayfire/window") { return new waybar::modules::wayfire::Window(id, bar_, config_[name]); @@ -251,6 +277,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "cpu") { return new waybar::modules::Cpu(id, config_[name]); } + if (ref == "cpu_graph") { + return new waybar::modules::CpuGraph(id, config_[name]); + } #if defined(HAVE_CPU_LINUX) if (ref == "cpu_frequency") { return new waybar::modules::CpuFrequency(id, config_[name]); @@ -358,6 +387,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name], bar_.output->name); } + if (ref.compare(0, 13, "custom-graph/") == 0 && ref.size() > 13) { + return new waybar::modules::CustomGraph(ref.substr(13), id, config_[name], bar_.output->name); + } if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) { return new waybar::modules::CFFI(ref.substr(5), id, config_[name]); } diff --git a/src/group.cpp b/src/group.cpp index acd7bd42..3c72a522 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -62,7 +62,12 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& const bool left_to_right = (drawer_config["transition-left-to-right"].isBool() ? drawer_config["transition-left-to-right"].asBool() : true); + const bool reveal_by_default = + (drawer_config["reveal-by-default"].isBool() ? drawer_config["reveal-by-default"].asBool() + : false); + click_to_reveal = drawer_config["click-to-reveal"].asBool(); + reveal_delay = drawer_config["reveal-delay"].asInt(); const bool start_expanded = (drawer_config["start-expanded"].isBool() ? drawer_config["start-expanded"].asBool() @@ -72,10 +77,11 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& revealer.set_transition_type(transition_type); revealer.set_transition_duration(transition_duration); - revealer.set_reveal_child(start_expanded); - - if (start_expanded) { + if ((click_to_reveal && reveal_by_default) || start_expanded) { box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + revealer.set_reveal_child(true); + } else { + revealer.set_reveal_child(false); } revealer.get_style_context()->add_class("drawer"); @@ -95,22 +101,41 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& void Group::show_group() { box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); revealer.set_reveal_child(true); + box.get_style_context()->add_class("expanded"); } void Group::hide_group() { box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); revealer.set_reveal_child(false); + box.get_style_context()->remove_class("expanded"); } bool Group::handleMouseEnter(GdkEventCrossing* const& e) { if (!click_to_reveal) { - show_group(); + if (reveal_delay > 0) { + if (reveal_timeout_.connected()) { + reveal_timeout_.disconnect(); + } + + reveal_timeout_ = Glib::signal_timeout().connect( + [this]() { + show_group(); + return false; + }, + reveal_delay); + } else { + show_group(); + } } return false; } bool Group::handleMouseLeave(GdkEventCrossing* const& e) { if (!click_to_reveal && e->detail != GDK_NOTIFY_INFERIOR) { + if (reveal_delay > 0 && reveal_timeout_.connected()) { + reveal_timeout_.disconnect(); + } + hide_group(); } return false; diff --git a/src/main.cpp b/src/main.cpp index 9d7d0ba2..5e070b5a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,17 @@ #include #include +#ifdef HAVE_LIBSYSTEMD +#include +#include + +#include +#include +#include +#include +#include +#endif + #include "bar.hpp" #include "client.hpp" #include "util/SafeSignal.hpp" @@ -55,9 +66,11 @@ static void catchSignals(waybar::SafeSignal& signal_handler) { std::signal(SIGINT, writeSignalToPipe); std::signal(SIGCHLD, writeSignalToPipe); +#ifdef SIGRTMIN for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { std::signal(sig, writeSignalToPipe); } +#endif while (true) { int signum; @@ -119,12 +132,14 @@ void handleUserSignal(int signal, bool& reload) { // If this signal should restart or close the bar, this function will write // `true` or `false`, respectively, into `reload`. static void handleSignalMainThread(int signum, bool& reload) { +#ifdef SIGRTMIN if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) { for (auto& bar : waybar::Client::inst()->bars) { bar->handleSignal(signum); } return; } +#endif switch (signum) { case SIGUSR1: @@ -158,7 +173,48 @@ static void handleSignalMainThread(int signum, bool& reload) { } } +static void logToJournalIfRunAsService() { +#ifdef HAVE_LIBSYSTEMD + /* Implementation of automatic protocol upgrading (from stderr to journal) + ** as described in https://systemd.io/JOURNAL_NATIVE_PROTOCOL */ + char const* journal_stream = std::getenv("JOURNAL_STREAM"); + + if (journal_stream != nullptr) { + dev_t device; + ino_t inode; + size_t len = std::strlen(journal_stream); + + auto result = std::from_chars(journal_stream, journal_stream + len, device); + if (result.ec == std::errc{}) + result = std::from_chars(result.ptr + 1, journal_stream + len, inode); + if (result.ec != std::errc{}) { + spdlog::warn("malformed JOURNAL_STREAM (\"{}\"): {}, logging to console", journal_stream, + std::make_error_condition(result.ec).message()); + } + + struct stat f_stderr; + + if (fstat(STDERR_FILENO, &f_stderr) != 0) { + spdlog::warn("unable to check stderr device and inode numbers: {}", strerror(errno)); + } else if (device == f_stderr.st_dev && inode == f_stderr.st_ino) { + auto journald = spdlog::systemd_logger_st("native_journal", "waybar", false); + /* systemd_logger_st is thread-safe with enable_formatter = false + ** thanks to underlying sd_journal_send being thread-safe + ** https://github.com/gabime/spdlog/issues/2320#issuecomment-1079766037 + */ + spdlog::set_default_logger(journald); + } else { + spdlog::info("JOURNAL_STREAM does not point to stderr, logging to console"); + } + } else { + spdlog::info("no JOURNAL_STREAM, logging to console"); + } +#endif +} + int main(int argc, char* argv[]) { + logToJournalIfRunAsService(); + try { auto* client = waybar::Client::inst(); diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 8f0d583f..367d6434 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -36,9 +36,15 @@ auto waybar::modules::Backlight::update() -> void { if (best->get_powered()) { event_box_.show(); + const uint8_t percent = best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max()); + const uint8_t percent_exp = + best->get_max() == 0 + ? 100 + : roundf(powf((float)best->get_actual() / best->get_max(), 1.0f / 2.718f) * 100); + // Get the state and apply state-specific format if available auto state = getState(percent); std::string current_format = format_; @@ -49,8 +55,10 @@ auto waybar::modules::Backlight::update() -> void { } } - std::string desc = fmt::format(fmt::runtime(current_format), fmt::arg("percent", percent), - fmt::arg("icon", getIcon(percent))); + std::string desc = + fmt::format(fmt::runtime(current_format), fmt::arg("percent", percent), + fmt::arg("percent_exp", percent_exp), fmt::arg("icon", getIcon(percent)), + fmt::arg("icon_exp", getIcon(percent_exp))); label_.set_markup(desc); if (tooltipEnabled()) { std::string tooltip_format; diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 25436baf..2fa4ded0 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include "util/command.hpp" #if defined(__FreeBSD__) @@ -33,6 +36,11 @@ waybar::modules::Battery::Battery(const std::string& id, const Bar& bar, const J } udev_monitor_enable_receiving(mon_.get()); + if (config_["smooth-power"].isBool()) { + smoothPowerEnable_ = config_["smooth-power"].asBool(); + if (smoothPowerEnable_ && config_["smooth-power-time-constant"].isNumeric()) + time_constant_s_ = std::max(1.0, config_["smooth-power-time-constant"].asDouble()); + } if (config_["weighted-average"].isBool()) weightedAverage_ = config_["weighted-average"].asBool(); #endif spdlog::debug("battery: worker interval is {}", interval_.count()); @@ -153,7 +161,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()) { @@ -288,63 +296,73 @@ waybar::modules::Battery::getInfos() { int32_t _current_now_int = 0; bool current_now_exists = false; if (std::ifstream current_now_f{bat / "current_now"}) { - current_now_exists = true; - current_now_f >> _current_now_int; + if (current_now_f >> _current_now_int) { + current_now_exists = true; + } } else if (std::ifstream current_avg_f{bat / "current_avg"}) { - current_now_exists = true; - current_avg_f >> _current_now_int; + 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 (std::ifstream f{bat / "time_to_empty_now"}) { - time_to_empty_now_exists = true; - f >> time_to_empty_now; + if (f >> time_to_empty_now) { + time_to_empty_now_exists = true; + } } if (std::ifstream f{bat / "time_to_full_now"}) { - time_to_full_now_exists = true; - f >> 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 (std::ifstream voltage_now_f{bat / "voltage_now"}) { - voltage_now_exists = true; - voltage_now_f >> voltage_now; + if (voltage_now_f >> voltage_now) { + voltage_now_exists = true; + } } else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) { - voltage_now_exists = true; - voltage_avg_f >> voltage_now; + if (voltage_avg_f >> voltage_now) { + voltage_now_exists = true; + } } uint32_t charge_full = 0; bool charge_full_exists = false; if (std::ifstream f{bat / "charge_full"}) { - charge_full_exists = true; - f >> charge_full; + if (f >> charge_full) { + charge_full_exists = true; + } } uint32_t charge_full_design = 0; bool charge_full_design_exists = false; if (std::ifstream f{bat / "charge_full_design"}) { - charge_full_design_exists = true; - f >> charge_full_design; + if (f >> charge_full_design) { + charge_full_design_exists = true; + } } uint32_t charge_now = 0; bool charge_now_exists = false; if (std::ifstream f{bat / "charge_now"}) { - charge_now_exists = true; - f >> 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 (std::ifstream f{bat / "power_now"}) { - power_now_exists = true; - f >> _power_now_int; + if (f >> _power_now_int) { + power_now_exists = true; + } } // Some drivers (example: Qualcomm) exposes use a negative value when // discharging, positive value when charging. @@ -353,22 +371,25 @@ waybar::modules::Battery::getInfos() { uint32_t energy_now = 0; bool energy_now_exists = false; if (std::ifstream f{bat / "energy_now"}) { - energy_now_exists = true; - f >> energy_now; + if (f >> energy_now) { + energy_now_exists = true; + } } uint32_t energy_full = 0; bool energy_full_exists = false; if (std::ifstream f{bat / "energy_full"}) { - energy_full_exists = true; - f >> energy_full; + if (f >> energy_full) { + energy_full_exists = true; + } } uint32_t energy_full_design = 0; bool energy_full_design_exists = false; if (std::ifstream f{bat / "energy_full_design"}) { - energy_full_design_exists = true; - f >> energy_full_design; + if (f >> energy_full_design) { + energy_full_design_exists = true; + } } uint16_t cycleCount = 0; @@ -404,8 +425,9 @@ waybar::modules::Battery::getInfos() { capacity_exists = true; capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; } else if (std::ifstream f{bat / "capacity"}) { - capacity_exists = true; - f >> capacity; + if (f >> capacity) { + capacity_exists = true; + } } if (!voltage_now_exists) { @@ -563,11 +585,31 @@ waybar::modules::Battery::getInfos() { if (online && current_status != "Discharging") status = "Plugged"; } + if (total_energy_exists && total_power_exists && total_power != 0) { + if (!smoothPowerEnable_) { + smooth_power_ = total_power; + } else { + if (status != old_status_raw_) { + smooth_power_ = total_power; + last_t_ = std::chrono::steady_clock::now(); + } else { + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + double dt_s = + std::chrono::duration_cast >(now - last_t_).count(); + smooth_power_ = smooth_power_ + ((1 - std::exp(-dt_s / time_constant_s_)) * + (total_power - smooth_power_)); + last_t_ = now; + } + + old_status_raw_ = status; + } + } + float time_remaining{0.0f}; if (status == "Discharging" && time_to_empty_now_exists) { if (time_to_empty_now != 0) time_remaining = (float)time_to_empty_now / 3600.0f; } else if (status == "Discharging" && total_power_exists && total_energy_exists) { - if (total_power != 0) time_remaining = (float)total_energy / total_power; + if (smooth_power_ != 0) time_remaining = (float)total_energy / smooth_power_; } else if (status == "Charging" && time_to_full_now_exists) { if (time_to_full_now_exists && (time_to_full_now != 0)) time_remaining = -(float)time_to_full_now / 3600.0f; @@ -585,11 +627,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); + } } } @@ -686,13 +729,20 @@ auto waybar::modules::Battery::update() -> void { if (status == "Unknown") { status = getAdapterStatus(capacity); } + auto state = getState(capacity, true); + bool adapter_online = false; + if (!adapter_.empty()) { + std::ifstream(adapter_ / "online") >> adapter_online; + } + if (config_["full-at-plugged"].asBool() && status == "Full" && adapter_online) { + status = "Plugged"; + } auto status_pretty = status; // Transform to lowercase and replace space with dash std::ranges::transform(status.begin(), status.end(), status.begin(), [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); auto format = format_; - auto state = getState(capacity, true); processEvents(state, status, capacity); setBarClass(state); auto time_remaining_formatted = formatTimeRemaining(time_remaining); diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index c59af3b5..d54c4d07 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -83,6 +83,68 @@ auto getUcharProperty(GDBusProxy* proxy, const char* property_name) -> unsigned return 0; } +auto isChildPath(const std::string& child, const std::string& parent) -> bool { + return child.starts_with(parent); +} + +auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional { + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + + GError* error = nullptr; + GVariant* gvar = + g_dbus_proxy_call_sync(proxy_char, "ReadValue", g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (error != nullptr) { + g_error_free(error); + return std::nullopt; + } + if (gvar == nullptr) { + return std::nullopt; + } + + GVariant* value_array = g_variant_get_child_value(gvar, 0); + gsize n_elements; + const auto* data = static_cast( + g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar))); + + std::optional result; + if (data != nullptr && n_elements > 0) { + result = data[0]; + } + + g_variant_unref(value_array); + g_variant_unref(gvar); + return result; +} + +auto hasUserDescriptionDescriptor(GList* objects, const std::string& char_path, + const std::string& user_description_uuid) -> bool { + for (GList* n = objects; n != nullptr; n = n->next) { + GDBusObject* desc_object = G_DBUS_OBJECT(n->data); + std::string desc_path = g_dbus_object_get_object_path(desc_object); + + if (!isChildPath(desc_path, char_path)) { + continue; + } + + GDBusProxy* proxy_desc = + G_DBUS_PROXY(g_dbus_object_get_interface(desc_object, "org.bluez.GattDescriptor1")); + if (proxy_desc == nullptr) { + continue; + } + + auto desc_uuid = getOptionalStringProperty(proxy_desc, "UUID"); + g_object_unref(proxy_desc); + + if (desc_uuid.has_value() && + desc_uuid.value().find(user_description_uuid) != std::string::npos) { + return true; + } + } + return false; +} + } // namespace waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& config) @@ -232,8 +294,9 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::arg("device_address", cur_focussed_device_.address), fmt::arg("device_address_type", cur_focussed_device_.address_type), fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_label), - fmt::arg("device_battery_percentage", - cur_focussed_device_.battery_percentage.value_or(0)))); + fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + cur_focussed_device_.battery_percentage_peripheral.value_or(0)))); } if (tooltipEnabled()) { @@ -258,7 +321,9 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::runtime(enumerate_format), fmt::arg("device_address", dev.address), fmt::arg("device_address_type", dev.address_type), fmt::arg("device_alias", dev.alias), fmt::arg("icon", enumerate_icon), - fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0))); + fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + dev.battery_percentage_peripheral.value_or(0))); } } device_enumerate_ = ss.str(); @@ -278,6 +343,8 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::arg("device_address_type", cur_focussed_device_.address_type), fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_tooltip), fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + cur_focussed_device_.battery_percentage_peripheral.value_or(0)), fmt::arg("device_enumerate", device_enumerate_))); } @@ -398,6 +465,85 @@ auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object) return std::nullopt; } +auto waybar::modules::Bluetooth::getDeviceGattBatteryLevels( + GDBusObject* device_object, std::optional& central_battery, + std::optional& peripheral_battery) -> void { + const std::string BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb"; + const std::string BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb"; + const std::string USER_DESCRIPTION_UUID = "00002901-0000-1000-8000-00805f9b34fb"; + + GList* objects = g_dbus_object_manager_get_objects(manager_.get()); + std::string device_path = g_dbus_object_get_object_path(device_object); + + for (GList* l = objects; l != nullptr; l = l->next) { + GDBusObject* service_object = G_DBUS_OBJECT(l->data); + std::string service_path = g_dbus_object_get_object_path(service_object); + + if (!isChildPath(service_path, device_path)) { + continue; + } + + GDBusProxy* proxy_service = + G_DBUS_PROXY(g_dbus_object_get_interface(service_object, "org.bluez.GattService1")); + if (proxy_service == nullptr) { + continue; + } + + auto service_uuid = getOptionalStringProperty(proxy_service, "UUID"); + g_object_unref(proxy_service); + + if (!service_uuid.has_value() || + service_uuid.value().find(BATTERY_SERVICE_UUID) == std::string::npos) { + continue; + } + + processBatteryServiceCharacteristics(objects, service_path, BATTERY_LEVEL_UUID, + USER_DESCRIPTION_UUID, central_battery, + peripheral_battery); + } + + g_list_free_full(objects, g_object_unref); +} + +auto waybar::modules::Bluetooth::processBatteryServiceCharacteristics( + GList* objects, const std::string& service_path, const std::string& battery_level_uuid, + const std::string& user_description_uuid, std::optional& central_battery, + std::optional& peripheral_battery) -> void { + for (GList* m = objects; m != nullptr; m = m->next) { + GDBusObject* char_object = G_DBUS_OBJECT(m->data); + std::string char_path = g_dbus_object_get_object_path(char_object); + + if (!isChildPath(char_path, service_path)) { + continue; + } + + GDBusProxy* proxy_char = + G_DBUS_PROXY(g_dbus_object_get_interface(char_object, "org.bluez.GattCharacteristic1")); + if (proxy_char == nullptr) { + continue; + } + + auto char_uuid = getOptionalStringProperty(proxy_char, "UUID"); + if (!char_uuid.has_value() || char_uuid.value().find(battery_level_uuid) == std::string::npos) { + g_object_unref(proxy_char); + continue; + } + + auto battery_value = readBatteryCharacteristicValue(proxy_char); + g_object_unref(proxy_char); + + if (!battery_value.has_value()) { + continue; + } + + if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) { + peripheral_battery = battery_value.value(); + } else { + central_battery = battery_value.value(); + } + } +} + auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, DeviceInfo& device_info) -> bool { GDBusProxy* proxy_device = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Device1")); @@ -418,6 +564,8 @@ auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, Device g_object_unref(proxy_device); device_info.battery_percentage = getDeviceBatteryPercentage(object); + getDeviceGattBatteryLevels(object, device_info.battery_percentage, + device_info.battery_percentage_peripheral); return true; } diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 5fe5407b..6050df31 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -9,6 +9,7 @@ #include #include +#include "util/command.hpp" #include "util/ustring_clen.hpp" #ifdef HAVE_LANGINFO_1STDAY @@ -187,6 +188,45 @@ auto waybar::modules::Clock::update() -> void { } m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(now)); + + // Pango doesn't support CSS classes but to continue using it while staying + // backwards compatible this approach uses post-posting to replace fake + // classes with attributes Pango does understand. + // + // The benefit of this approach is anyone using the original styling choices + // can continue doing that and folks can optionally opt into using classes. + // + // It's also forwards compatible to where if this implemention ever changes + // to support proper classes anyone using them will continue to work. + auto context = label_.get_style_context(); + + static const std::vector> calendar_class_map = { + {"calendar-today", "class='today'"}, + {"calendar-days", "class='days'"}, + {"calendar-weeks", "class='weeks'"}, + {"calendar-weekdays", "class='weekdays'"}, + {"calendar-months", "class='months'"}}; + + for (const auto& [css_class, search_str] : calendar_class_map) { + try { + context->add_class(css_class); + const Gdk::RGBA color = context->get_color(); + context->remove_class(css_class); + + const std::string replace_str = fmt::format( + "color='#{:02x}{:02x}{:02x}'", static_cast(color.get_red() * 255), + static_cast(color.get_green() * 255), static_cast(color.get_blue() * 255)); + + m_tlpText_ = std::regex_replace(m_tlpText_, std::regex(search_str), replace_str); + } catch (const Glib::Error& e) { + spdlog::warn("Clock: Failed to fetch CSS color for {}: {}", css_class, e.what().raw()); + continue; + } catch (...) { + // Catch-all for any other weirdness. + continue; + } + } + m_tooltip_->set_markup(m_tlpText_); label_.trigger_tooltip_query(); } @@ -402,8 +442,13 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea data = g_utf8_find_next_char(data, end); } } + // Note: the stream's default fill character is already a space (L' ' on + // libstdc++'s wide FormatStream, ' ' on libc++'s narrow one), so no + // std::setfill is needed. Passing std::setfill(' ')/std::setfill(L' ') + // here is not portable because the fill char type must match the + // FormatStream's char type, which differs between standard libraries. os << Glib::ustring::format( - (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(L' '), + (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), calendarLine); @@ -472,6 +517,8 @@ auto waybar::modules::Clock::local_zone() -> const time_zone* { auto waybar::modules::Clock::doAction(const std::string& name) -> void { if (actionMap_[name]) { (this->*actionMap_[name])(); + } else if (auto key = name.substr(0, name.find(" ")); actionWithArgsMap_[key]) { + (this->*actionWithArgsMap_[key])(name); } else spdlog::error("Clock. Unsupported action \"{0}\"", name); } @@ -498,6 +545,10 @@ void waybar::modules::Clock::tz_down() { if (tzSize == 1) return; tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1; } +void waybar::modules::Clock::action_exec(const std::string& action) { + auto cmd = action.substr(strlen("exec ")); + pid_children_.push_back(util::command::forkExec(cmd)); +} #ifdef HAVE_LANGINFO_1STDAY template @@ -509,6 +560,18 @@ using deleting_unique_ptr = std::unique_ptr>; // Computations done similarly to Linux cal utility. auto waybar::modules::Clock::first_day_of_week() -> weekday { + const auto firstdow = config_[kCldPlaceholder]["first-day-of-week"]; + if (firstdow.isInt()) { + const int firstDay = firstdow.asInt(); + if (!(firstDay >= 0 && firstDay <= 6)) { + spdlog::warn( + "Clock calender configuration first-day-of-week = {0} must be in range [0, 6]. Default " + "value is used instead", + firstDay); + } else { + return weekday{static_cast(firstDay)}; + } + } if (iso8601Calendar_) { return Monday; } diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 8cfda2c1..1a64b4a3 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -41,6 +41,9 @@ auto waybar::modules::Cpu::update() -> void { auto icons = std::vector{state}; fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("load", load1)); + store.push_back(fmt::arg("load1", load1)); + store.push_back(fmt::arg("load5", load5)); + store.push_back(fmt::arg("load15", load15)); store.push_back(fmt::arg("usage", total_usage)); store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); store.push_back(fmt::arg("max_frequency", max_frequency)); @@ -48,19 +51,28 @@ auto waybar::modules::Cpu::update() -> void { store.push_back(fmt::arg("avg_frequency", avg_frequency)); std::vector arg_names; arg_names.reserve(cpu_usage.size() * 2); + std::string all_icons; for (size_t i = 1; i < cpu_usage.size(); ++i) { auto core_i = i - 1; arg_names.push_back(fmt::format("usage{}", core_i)); store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i])); + auto core_icon = getIcon(cpu_usage[i], icons); + all_icons += core_icon; arg_names.push_back(fmt::format("icon{}", core_i)); - store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons))); + store.push_back(fmt::arg(arg_names.back().c_str(), core_icon)); } + store.push_back(fmt::arg("icons", all_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)); + std::string tooltip_format; + if (!state.empty() && config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } else if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + label_.set_tooltip_markup(fmt::vformat(tooltip_format, store)); } else { label_.set_tooltip_markup(tooltip); } diff --git a/src/modules/cpu_graph.cpp b/src/modules/cpu_graph.cpp new file mode 100644 index 00000000..a9a29dd2 --- /dev/null +++ b/src/modules/cpu_graph.cpp @@ -0,0 +1,47 @@ +#include "modules/cpu_graph.hpp" + +#include "modules/cpu_frequency.hpp" +#include "modules/cpu_usage.hpp" +#include "modules/load.hpp" + +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + +waybar::modules::CpuGraph::CpuGraph(const std::string& id, const Json::Value& config) + : AGraph(config, "cpu_graph", id, 5) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; +} + +auto waybar::modules::CpuGraph::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()) { + graph_.set_tooltip_text(tooltip); + } + auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; + addValue(total_usage); + + graph_.get_style_context()->remove_class(MODERATE_CLASS); + graph_.get_style_context()->remove_class(HIGH_CLASS); + graph_.get_style_context()->remove_class(INTENSIVE_CLASS); + + if (total_usage > 90) { + graph_.get_style_context()->add_class(INTENSIVE_CLASS); + } else if (total_usage > 70) { + graph_.get_style_context()->add_class(HIGH_CLASS); + } else if (total_usage > 30) { + graph_.get_style_context()->add_class(MODERATE_CLASS); + } + + // Call parent update + AGraph::update(); +} diff --git a/src/modules/cpu_usage/common.cpp b/src/modules/cpu_usage/common.cpp index 1571cb74..79ce6941 100644 --- a/src/modules/cpu_usage/common.cpp +++ b/src/modules/cpu_usage/common.cpp @@ -38,13 +38,17 @@ auto waybar::modules::CpuUsage::update() -> void { store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); std::vector arg_names; arg_names.reserve(cpu_usage.size() * 2); + std::string all_icons; for (size_t i = 1; i < cpu_usage.size(); ++i) { auto core_i = i - 1; arg_names.push_back(fmt::format("usage{}", core_i)); store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i])); + auto core_icon = getIcon(cpu_usage[i], icons); + all_icons += core_icon; arg_names.push_back(fmt::format("icon{}", core_i)); - store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons))); + store.push_back(fmt::arg(arg_names.back().c_str(), core_icon)); } + store.push_back(fmt::arg("icons", all_icons)); label_.set_markup(fmt::vformat(format, store)); if (tooltipEnabled()) { diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 28def8c9..37198e3c 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -8,7 +8,7 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, const Json::Value& config, const std::string& output_name) - : ALabel(config, "custom-" + name, id, "{}"), + : AIconLabel(config, "custom-" + name, id, "{}"), name_(name), output_name_(output_name), id_(id), @@ -28,6 +28,15 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, } else if (config_["exec"].isString()) { continuousWorker(); } + if (config_["image-path"].isString()) { + image_path_ = config_["image-path"].asString(); + } + if (config_["image-name"].isString()) { + image_name_ = config_["image-name"].asString(); + } + if (config["icon-size"].isUInt()) { + app_icon_size_ = config["icon-size"].asUInt(); + } } waybar::modules::Custom::~Custom() { @@ -39,6 +48,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; @@ -138,9 +152,11 @@ void waybar::modules::Custom::waitingWorker() { } void waybar::modules::Custom::refresh(int sig) { +#ifdef SIGRTMIN if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); } +#endif } void waybar::modules::Custom::handleEvent() { @@ -177,7 +193,8 @@ auto waybar::modules::Custom::update() -> void { auto str = fmt::format(fmt::runtime(format_), fmt::arg("text", text_), fmt::arg("alt", alt_), fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); - if ((config_["hide-empty-text"].asBool() && text_.empty()) || str.empty()) { + if ((config_["hide-empty-text"].asBool() && text_.empty()) || + (str.empty() && image_path_.empty() && image_name_.empty())) { event_box_.hide(); } else { label_.set_markup(str); @@ -212,7 +229,19 @@ auto waybar::modules::Custom::update() -> void { style->add_class("flat"); style->add_class("text-button"); style->add_class(MODULE_CLASS); + auto image_style = image_.get_style_context(); + image_style->add_class("image-button"); event_box_.show(); + if (!image_path_.empty()) { + auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); + image_.set(pixbuf); + } else if (!image_name_.empty()) { + image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID); + image_.set_pixel_size(app_icon_size_); + } + + image_.set_visible(!image_name_.empty() || !image_path_.empty()); + label_.set_visible(!str.empty()); } } catch (const fmt::format_error& e) { if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) @@ -224,7 +253,7 @@ auto waybar::modules::Custom::update() -> void { } } // Call parent update - ALabel::update(); + AIconLabel::update(); } void waybar::modules::Custom::parseOutputRaw() { @@ -290,6 +319,7 @@ void waybar::modules::Custom::parseOutputJson() { class_.push_back(c.asString()); } } + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { percentage_ = (int)lround(parsed["percentage"].asFloat()); } else { diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp new file mode 100644 index 00000000..2a12f0c1 --- /dev/null +++ b/src/modules/custom_graph.cpp @@ -0,0 +1,281 @@ +#include "modules/custom_graph.hpp" + +#include + +#include "util/scope_guard.hpp" + +waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, + const Json::Value& config, const std::string& output_name) + : AGraph(config, "custom-graph-" + name, id), + name_(name), + output_name_(output_name), + id_(id), + tooltip_format_enabled_{config_["tooltip-format"].isString()}, + percentage_(0), + fp_(nullptr), + pid_(-1) { + if (config.isNull()) { + spdlog::warn("There is no configuration for 'custom-graph/{}', element will be hidden", name); + } + dp.emit(); + if (!config_["signal"].empty() && config_["interval"].empty() && + config_["restart-interval"].empty()) { + waitingWorker(); + } else if (interval_.count() > 0) { + delayWorker(); + } else if (config_["exec"].isString()) { + continuousWorker(); + } +} + +waybar::modules::CustomGraph::~CustomGraph() { + if (pid_ != -1) { + killpg(pid_, SIGTERM); + waitpid(pid_, NULL, 0); + pid_ = -1; + } +} + +void waybar::modules::CustomGraph::delayWorker() { + thread_ = [this] { + for (int i : this->pid_children_) { + int status; + waitpid(i, &status, 0); + } + + this->pid_children_.clear(); + + bool can_update = true; + if (config_["exec-if"].isString()) { + output_ = util::command::execNoRead(config_["exec-if"].asString()); + if (output_.exit_code != 0) { + can_update = false; + dp.emit(); + } + } + if (can_update) { + if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString(), output_name_); + } + dp.emit(); + } + thread_.sleep_for(interval_); + }; +} + +void waybar::modules::CustomGraph::continuousWorker() { + auto cmd = config_["exec"].asString(); + pid_ = -1; + fp_ = util::command::open(cmd, pid_, output_name_); + if (!fp_) { + throw std::runtime_error("Unable to open " + cmd); + } + thread_ = [this, cmd] { + char* buff = nullptr; + waybar::util::ScopeGuard buff_deleter([&buff]() { + if (buff) { + free(buff); + } + }); + size_t len = 0; + if (getline(&buff, &len, fp_) == -1) { + int exit_code = 1; + if (fp_) { + exit_code = WEXITSTATUS(util::command::close(fp_, pid_)); + fp_ = nullptr; + } + if (exit_code != 0) { + output_ = {exit_code, ""}; + dp.emit(); + spdlog::error("{} stopped unexpectedly, is it endless?", name_); + } + if (config_["restart-interval"].isUInt()) { + pid_ = -1; + thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt())); + fp_ = util::command::open(cmd, pid_, output_name_); + if (!fp_) { + throw std::runtime_error("Unable to open " + cmd); + } + } else { + thread_.stop(); + return; + } + } else { + std::string output = buff; + + // Remove last newline + if (!output.empty() && output[output.length() - 1] == '\n') { + output.erase(output.length() - 1); + } + output_ = {0, output}; + dp.emit(); + } + }; +} + +void waybar::modules::CustomGraph::waitingWorker() { + thread_ = [this] { + bool can_update = true; + if (config_["exec-if"].isString()) { + output_ = util::command::execNoRead(config_["exec-if"].asString()); + if (output_.exit_code != 0) { + can_update = false; + dp.emit(); + } + } + if (can_update) { + if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString(), output_name_); + } + dp.emit(); + } + thread_.sleep(); + }; +} + +void waybar::modules::CustomGraph::refresh(int sig) { + if (sig == SIGRTMIN + config_["signal"].asInt()) { + thread_.wake_up(); + } +} + +void waybar::modules::CustomGraph::handleEvent() { + if (!config_["exec-on-event"].isBool() || config_["exec-on-event"].asBool()) { + thread_.wake_up(); + } +} + +bool waybar::modules::CustomGraph::handleScroll(GdkEventScroll* e) { + auto ret = AGraph::handleScroll(e); + handleEvent(); + return ret; +} + +bool waybar::modules::CustomGraph::handleToggle(GdkEventButton* const& e) { + auto ret = AGraph::handleToggle(e); + handleEvent(); + return ret; +} + +auto waybar::modules::CustomGraph::update() -> void { + // Hide label if output is empty + if ((config_["exec"].isString() || config_["exec-if"].isString()) && + (output_.out.empty() || output_.exit_code != 0)) { + event_box_.hide(); + } else { + if (config_["return-type"].asString() == "json") { + parseOutputJson(); + } else { + parseOutputRaw(); + } + + try { + addValue(percentage_); + + if (tooltipEnabled()) { + 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("percentage", percentage_)); + graph_.set_tooltip_markup(tooltip); + } else { + if (graph_.get_tooltip_markup() != tooltip_) { + graph_.set_tooltip_markup(tooltip_); + } + } + } + auto style = graph_.get_style_context(); + auto classes = style->list_classes(); + for (auto const& c : classes) { + if (c == id_) continue; + style->remove_class(c); + } + for (auto const& c : class_) { + style->add_class(c); + } + style->add_class("flat"); + style->add_class(MODULE_CLASS); + event_box_.show(); + } catch (const fmt::format_error& e) { + if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) + throw; + + throw fmt::format_error( + "mixing manual and automatic argument indexing is no longer supported; " + "try replacing \"{}\" with \"{text}\" in your format specifier"); + } + } + // Call parent update + AGraph::update(); +} + +void waybar::modules::CustomGraph::parseOutputRaw() { + std::istringstream output(output_.out); + std::string line; + int i = 0; + while (getline(output, line)) { + Glib::ustring validated_line = line; + if (!validated_line.validate()) { + validated_line = validated_line.make_valid(); + } + + if (i == 0) { + if (config_["escape"].isBool() && config_["escape"].asBool()) { + text_ = Glib::Markup::escape_text(validated_line); + tooltip_ = Glib::Markup::escape_text(validated_line); + } else { + text_ = validated_line; + tooltip_ = validated_line; + } + class_.clear(); + } else if (i == 1) { + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(validated_line); + } else { + tooltip_ = validated_line; + } + } else if (i == 2) { + class_.push_back(validated_line); + } else { + break; + } + i++; + } +} + +void waybar::modules::CustomGraph::parseOutputJson() { + std::istringstream output(output_.out); + std::string line; + class_.clear(); + while (getline(output, line)) { + auto parsed = parser_.parse(line); + if (config_["escape"].isBool() && config_["escape"].asBool()) { + text_ = Glib::Markup::escape_text(parsed["text"].asString()); + } else { + text_ = parsed["text"].asString(); + } + if (config_["escape"].isBool() && config_["escape"].asBool()) { + alt_ = Glib::Markup::escape_text(parsed["alt"].asString()); + } else { + alt_ = parsed["alt"].asString(); + } + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString()); + } else { + tooltip_ = parsed["tooltip"].asString(); + } + if (parsed["class"].isString()) { + class_.push_back(parsed["class"].asString()); + } else if (parsed["class"].isArray()) { + for (auto const& c : parsed["class"]) { + class_.push_back(c.asString()); + } + } + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { + percentage_ = (int)lround(parsed["percentage"].asFloat()); + } else { + percentage_ = 0; + } + break; + } +} diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index 462940b2..a4f37a00 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -1,15 +1,32 @@ #include "modules/disk.hpp" +#include + using namespace waybar::util; waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config) - : ALabel(config, "disk", id, "{}%", 30), path_("/") { + : ALabel(config, "disk", id, "{}%", 30), header_(""), paths_(), separator_(" ") { thread_ = [this] { dp.emit(); thread_.sleep_for(interval_); }; - if (config["path"].isString()) { - path_ = config["path"].asString(); + if (config["header"].isString()) { + header_ = config["header"].asString(); + } + if (config["path"].isString() && !config["paths"].isArray()) { + spdlog::warn("Disk: path is deprecated use paths instead!"); + paths_.push_back(config["path"].asString()); + } + if (config["paths"].isArray()) { + for (const auto& path : config["paths"]) { + paths_.push_back(path.asString()); + } + } + if (!config["path"].isString() && !config["paths"].isArray()) { + paths_.emplace_back("/"); + } + if (config["separator"].isString()) { + separator_ = config["separator"].asString(); } if (config["unit"].isString()) { unit_ = config["unit"].asString(); @@ -17,76 +34,108 @@ waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config) } auto waybar::modules::Disk::update() -> void { - struct statvfs /* { - unsigned long f_bsize; // filesystem block size - unsigned long f_frsize; // fragment size - fsblkcnt_t f_blocks; // size of fs in f_frsize units - fsblkcnt_t f_bfree; // # free blocks - fsblkcnt_t f_bavail; // # free blocks for unprivileged users - fsfilcnt_t f_files; // # inodes - fsfilcnt_t f_ffree; // # free inodes - fsfilcnt_t f_favail; // # free inodes for unprivileged users - unsigned long f_fsid; // filesystem ID - unsigned long f_flag; // mount flags - unsigned long f_namemax; // maximum filename length - }; */ - stats; - int err = statvfs(path_.c_str(), &stats); + std::string tooltip_label; + std::string label = header_; - /* Conky options - fs_bar - Bar that shows how much space is used - fs_free - Free space on a file system - fs_free_perc - Free percentage of space - fs_size - File system size - fs_used - File system used space - */ + bool had_valid_disk = false; - if (err != 0 || stats.f_blocks == 0) { - event_box_.hide(); - return; - } + for (size_t i = 0; i < paths_.size(); ++i) { + const auto& path = paths_[i]; - float specific_free, specific_used, specific_total, divisor; + struct statvfs /* { + unsigned long f_bsize; // filesystem block size + unsigned long f_frsize; // fragment size + fsblkcnt_t f_blocks; // size of fs in f_frsize units + fsblkcnt_t f_bfree; // # free blocks + fsblkcnt_t f_bavail; // # free blocks for unprivileged users + fsfilcnt_t f_files; // # inodes + fsfilcnt_t f_ffree; // # free inodes + fsfilcnt_t f_favail; // # free inodes for unprivileged users + unsigned long f_fsid; // filesystem ID + unsigned long f_flag; // mount flags + unsigned long f_namemax; // maximum filename length + }; */ + stats; - divisor = calc_specific_divisor(unit_); - specific_free = (stats.f_bavail * stats.f_frsize) / divisor; - specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor; - specific_total = (stats.f_blocks * stats.f_frsize) / divisor; + int err = statvfs(path.c_str(), &stats); - auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true); - auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true); - auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true); - auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks; + /* Conky options + fs_bar - Bar that shows how much space is used + fs_free - Free space on a file system + fs_free_perc - Free percentage of space + fs_size - File system size + fs_used - File system used space + */ - auto format = format_; - auto state = getState(percentage_used); - if (!state.empty() && config_["format-" + state].isString()) { - format = config_["format-" + state].asString(); - } + if (err != 0 || stats.f_blocks == 0) { + spdlog::warn("Disk: statvfs failed for path '{}' (errno={})", path, errno); + continue; + } - if (format.empty()) { - event_box_.hide(); - } else { - event_box_.show(); - label_.set_markup(fmt::format( - fmt::runtime(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), - fmt::arg("path", path_), fmt::arg("specific_free", specific_free), - fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total))); - } + float specific_free, specific_used, specific_total, divisor; + + divisor = calc_specific_divisor(unit_); + specific_free = (stats.f_bavail * stats.f_frsize) / divisor; + specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor; + specific_total = (stats.f_blocks * stats.f_frsize) / divisor; + + auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true); + auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true); + auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true); + auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks; + + std::string disk_format = format_; + auto state = getState(percentage_used); + if (!state.empty() && config_["format-" + state].isString()) { + disk_format = config_["format-" + state].asString(); + } + + if (!disk_format.empty()) { + if (had_valid_disk) { + label += separator_; + } + + label += fmt::format( + fmt::runtime(disk_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), fmt::arg("path", path), + fmt::arg("specific_free", specific_free), fmt::arg("specific_used", specific_used), + fmt::arg("specific_total", specific_total)); + } - if (tooltipEnabled()) { std::string tooltip_format = "{used} used out of {total} on {path} ({percentage_used}%)"; if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - 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), - fmt::arg("path", path_), fmt::arg("specific_free", specific_free), - fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total))); + + if (!tooltip_format.empty()) { + if (had_valid_disk) { + tooltip_label += "\n"; + } + + tooltip_label += 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), fmt::arg("path", path), + fmt::arg("specific_free", specific_free), fmt::arg("specific_used", specific_used), + fmt::arg("specific_total", specific_total)); + } + + had_valid_disk = true; + } + if (had_valid_disk) { + event_box_.show(); + } else { + event_box_.hide(); + } + + label_.set_markup(label); + + if (tooltipEnabled() && !tooltip_label.empty()) { + label_.set_tooltip_markup(tooltip_label); } // Call parent update ALabel::update(); @@ -109,7 +158,7 @@ float waybar::modules::Disk::calc_specific_divisor(const std::string& divisor) { return 1000.0 * 1000.0 * 1000.0 * 1000.0; } else if (divisor == "TiB") { return 1024.0 * 1024.0 * 1024.0 * 1024.0; - } else { // default to Bytes if it is anything that we don't recongnise + } else { // default to Bytes if it is anything that we don't recognise return 1.0; } } diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp index 243aaa92..3e6696af 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -26,7 +26,7 @@ static void toggle_visibility(void* data, zdwl_ipc_output_v2* zdwl_output_v2) { } static void active(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) { - // Intentionally empty + static_cast(data)->handle_active_output(zdwl_output_v2, active); } static void set_tag(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t tag, uint32_t state, @@ -70,32 +70,31 @@ 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) { - auto* self = static_cast(data); + 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; -} + 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; + self->status_manager_ = static_cast( + wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1)); } - version = std::min(version, 1); + if (std::strcmp(interface, wl_seat_interface.name) == 0) { + auto* self = static_cast(data); - self->seat_ = static_cast( - wl_registry_bind(registry, name, &wl_seat_interface, version)); -} + 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 */ @@ -110,7 +109,11 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con seat_{nullptr}, bar_(bar), box_{bar.orientation, 0}, + hide_vacant_(false), output_status_{nullptr} { + if (config_["hide-vacant"].asBool()) { + hide_vacant_ = config_["hide-vacant"].asBool(); + } struct wl_display* display = Client::inst()->wl_display; struct wl_registry* registry = wl_display_get_registry(display); @@ -163,7 +166,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con i <<= 1; } - struct wl_output* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); output_status_ = zdwl_ipc_manager_v2_get_output(status_manager_, output); zdwl_ipc_output_v2_add_listener(output_status_, &output_status_listener_impl, this); @@ -221,6 +224,24 @@ void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint } else { button.get_style_context()->remove_class("urgent"); } + + if (hide_vacant_ && !clients && !(state & TAG_ACTIVE)) { + button.set_visible(false); + } else { + button.set_visible(true); + } +} + +void Tags::handle_active_output(zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) { + if (output_status_ == zdwl_output_v2) { + for (size_t i = 0; i < buttons_.size(); ++i) { + if (active == 0) { + buttons_[i].get_style_context()->remove_class("output"); + } else { + buttons_[i].get_style_context()->add_class("output"); + } + } + } } } /* namespace waybar::modules::dwl */ diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp index 56e67895..15104b2e 100644 --- a/src/modules/dwl/window.cpp +++ b/src/modules/dwl/window.cpp @@ -19,7 +19,7 @@ static void toggle_visibility(void* data, zdwl_ipc_output_v2* zdwl_output_v2) { } static void active(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) { - // Intentionally empty + static_cast(data)->handle_active(active); } static void set_tag(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t tag, uint32_t state, @@ -74,7 +74,17 @@ static const wl_registry_listener registry_listener_impl = {.global = handle_glo .global_remove = handle_global_remove}; Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) - : AAppIconLabel(config, "window", id, "{}", 0, true), bar_(bar) { + : AAppIconLabel(config, "window", id, "{}", 0, true), + bar_(bar), + active_(false), + hide_inactive_(false), + hide_empty_(false) { + if (config_["hide-inactive"].isBool()) { + hide_inactive_ = config["hide-inactive"].asBool(); + } + if (config_["hide-empty"].isBool()) { + hide_empty_ = config["hide-empty"].asBool(); + } struct wl_display* display = Client::inst()->wl_display; struct wl_registry* registry = wl_display_get_registry(display); @@ -102,6 +112,8 @@ void Window::handle_title(const char* title) { title_ = Glib::Markup::escape_tex void Window::handle_appid(const char* appid) { appid_ = Glib::Markup::escape_text(appid); } +void Window::handle_active(const uint32_t active) { active_ = active != 0; } + void Window::handle_layout_symbol(const char* layout_symbol) { layout_symbol_ = Glib::Markup::escape_text(layout_symbol); } @@ -118,6 +130,19 @@ void Window::handle_frame() { if (tooltipEnabled()) { label_.set_tooltip_markup(title_); } + if (hide_empty_ && title_.empty()) { + box_.set_visible(false); + } else { + if (active_) { + box_.get_style_context()->add_class("active"); + box_.set_visible(true); + } else { + box_.get_style_context()->remove_class("active"); + if (hide_inactive_) { + box_.set_visible(false); + } + } + } } } // namespace waybar::modules::dwl diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 08cf97c1..f38f8216 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -320,7 +320,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri // New format: /dispatch hl.dsp.focus({ workspace = "1" }) // // Old format: dispatch focusworkspaceoncurrentmonitor 2 - // New format: /dispatch hl.dsp.focus({ workspace = "2", monitor = "current" }) + // 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") @@ -329,7 +329,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; } if (dispatcher == "focusworkspaceoncurrentmonitor") { - return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", monitor = \"current\" })"; + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })"; } if (dispatcher == "togglespecialworkspace") { if (arg.empty()) { diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 6e0fe23d..ff7cad64 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -48,16 +48,62 @@ 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(); } + // Tooltip support + if (tooltipEnabled()) { + std::string tooltipFormat; + if (config_["tooltip-format"].isString()) { + tooltipFormat = config_["tooltip-format"].asString(); + } else { + tooltipFormat = "{long}"; + } + auto tooltipText = trim(fmt::format( + fmt::runtime(tooltipFormat), + fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); + label_.set_tooltip_text(tooltipText); + } else { + label_.set_tooltip_text(""); + } + ALabel::update(); } @@ -74,8 +120,6 @@ void Language::onEvent(const std::string& ev) { spdlog::warn("hyprland language received malformed event payload: {}", ev); return; } - std::string kbName = payload.substr(0, kbSeparator); - // Last comma before variants parenthesis, eg: // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., // with dead keys) @@ -93,12 +137,21 @@ void Language::onEvent(const std::string& ev) { } 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); + removeXkbLayoutCssClass(); layout_ = getLayout(layoutName); + addXkbLayoutCssClass(); spdlog::debug("hyprland language onevent with {}", layoutName); @@ -120,6 +173,7 @@ void Language::initLanguage() { searcher = waybar::util::sanitize_string(searcher); layout_ = getLayout(searcher); + addXkbLayoutCssClass(); spdlog::debug("hyprland language initLanguage found {}", layout_.full_name); @@ -129,6 +183,15 @@ void Language::initLanguage() { } } +auto Language::removeXkbLayoutCssClass() -> void { + label_.get_style_context()->remove_class(layout_.short_name); + spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); +} +auto Language::addXkbLayoutCssClass() -> void { + label_.get_style_context()->add_class(layout_.short_name); + spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name); +} + auto Language::getLayout(const std::string& fullName) -> Layout { auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); rxkb_context_parse_default_ruleset(context); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 2e3e0bb9..029f9722 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -1,5 +1,6 @@ #include "modules/hyprland/window.hpp" +#include #include #include #include @@ -53,8 +54,15 @@ auto Window::update() -> void { std::string label_text; if (!format_.empty()) { label_.show(); + + // If the focused window name is empty and fallback is configured, use fallback text + std::string displayTitle = windowName; + if (displayTitle.empty() && config_["fallback"].isString()) { + displayTitle = config_["fallback"].asString(); + } + label_text = waybar::util::rewriteString( - fmt::format(fmt::runtime(format_), fmt::arg("title", windowName), + fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle), fmt::arg("initialTitle", windowData_.initial_title), fmt::arg("class", windowData_.class_name), fmt::arg("initialClass", windowData_.initial_class_name)), @@ -108,21 +116,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, [&](const 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{ @@ -132,7 +133,8 @@ 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()) { diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 753893f2..5046688f 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,6 +1,9 @@ +#include #include #include +#include +#include #include #include #include @@ -9,6 +12,33 @@ #include "util/command.hpp" #include "util/icon_loader.hpp" +namespace { +constexpr std::string_view kCssClassPrefix = "ws-"; + +// Convert a workspace name to a valid CSS class name. +// Lowercases, replaces non-alphanumeric runs with single hyphens, +// and prefixes digit-leading names (CSS classes can't start with a digit). +std::string sanitizeCssClass(const std::string& name) { + std::string result; + result.reserve(name.size() + kCssClassPrefix.size()); + for (auto c : name) { + auto uc = static_cast(c); + if (std::isalnum(uc)) { + result += static_cast(std::tolower(uc)); + } else if (!result.empty() && result.back() != '-') { + result += '-'; + } + } + if (!result.empty() && result.back() == '-') { + result.pop_back(); + } + if (!result.empty() && std::isdigit(static_cast(result.front()))) { + result.insert(0, kCssClassPrefix); + } + return result; +} +} // namespace + namespace waybar::modules::hyprland { Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, @@ -30,6 +60,11 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma } m_button.add_events(Gdk::BUTTON_PRESS_MASK); + m_button.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); + + m_button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleEnter)); + m_button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleLeave)); + m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), false); @@ -45,6 +80,12 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma initializeWindowMap(clients_data); } +Workspace::~Workspace() { + // Disconnect the hover-check timeout so it can't fire on this destroyed + // instance (Workspaces are removed at runtime while a check may be armed). + stopHoverCheck(); +} + void addOrRemoveClass(const Glib::RefPtr& context, bool condition, const std::string& class_name) { if (condition) { @@ -66,6 +107,100 @@ std::optional Workspace::closeWindow(WindowAddress const& addr) { return std::nullopt; } +bool Workspace::pointerInsideButton() { + auto display = Gdk::Display::get_default(); + if (!display) { + return false; + } + + auto seat = display->get_default_seat(); + if (!seat) { + return false; + } + + auto pointer = seat->get_pointer(); + if (!pointer) { + return false; + } + + Glib::RefPtr screen; + int pointerRootX = 0; + int pointerRootY = 0; + + pointer->get_position(screen, pointerRootX, pointerRootY); + + Gtk::Widget* toplevel = m_button.get_toplevel(); + if (toplevel == nullptr || !toplevel->get_window()) { + return false; + } + + int buttonX = 0; + int buttonY = 0; + + if (!m_button.translate_coordinates(*toplevel, 0, 0, buttonX, buttonY)) { + return false; + } + + int windowRootX = 0; + int windowRootY = 0; + toplevel->get_window()->get_root_origin(windowRootX, windowRootY); + + const auto allocation = m_button.get_allocation(); + + const int buttonRootX = windowRootX + buttonX; + const int buttonRootY = windowRootY + buttonY; + const int buttonWidth = allocation.get_width(); + const int buttonHeight = allocation.get_height(); + + return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY && + pointerRootX < buttonRootX + buttonWidth && pointerRootY < buttonRootY + buttonHeight; +} + +bool Workspace::syncHoverClass() { + auto styleContext = m_button.get_style_context(); + + if (pointerInsideButton()) { + styleContext->add_class("workspace-hover"); + return true; + } + + styleContext->remove_class("workspace-hover"); + stopHoverCheck(); + return false; +} + +void Workspace::startHoverCheck() { + if (m_hoverCheckConnection.connected()) { + return; + } + + m_hoverCheckConnection = + Glib::signal_timeout().connect(sigc::mem_fun(*this, &Workspace::syncHoverClass), 50); +} + +void Workspace::stopHoverCheck() { + if (m_hoverCheckConnection.connected()) { + m_hoverCheckConnection.disconnect(); + } +} + +bool Workspace::handleEnter(GdkEventCrossing* /*event*/) { + m_button.get_style_context()->add_class("workspace-hover"); + startHoverCheck(); + return false; +} + +bool Workspace::handleLeave(GdkEventCrossing* /*event*/) { + /* + * Do not remove immediately. + * Workspace taskbar children can fire misleading leave events while the + * pointer is still visually inside the workspace button. + * + * The polling check will remove the class once the pointer really leaves. + */ + startHoverCheck(); + return false; +} bool Workspace::handleClicked(GdkEventButton* bt) const { if (bt->type == GDK_BUTTON_PRESS) { try { @@ -115,7 +250,10 @@ void Workspace::setActiveWindow(WindowAddress const& addr) { } auto activeWindowPos = m_workspaceManager.activeWindowPosition(); - if (activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE) { + const bool has_active_window = + activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE; + + if (has_active_window) { auto window = std::move(m_windowMap[*activeIdx]); m_windowMap.erase(m_windowMap.begin() + *activeIdx); if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) { @@ -130,7 +268,9 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) { if (!create_window_payload.isEmpty(m_workspaceManager)) { auto repr = create_window_payload.repr(m_workspaceManager); - if (!repr.empty() || m_workspaceManager.enableTaskbar()) { + const bool should_display = !repr.empty() || m_workspaceManager.enableTaskbar(); + + if (should_display) { auto addr = create_window_payload.getAddress(); auto it = std::ranges::find_if( m_windowMap, [&addr](const auto& window) { return window.address == addr; }); @@ -142,7 +282,7 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) { } } } -}; +} bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) { if (create_window_payload.getWorkspaceName() == name()) { @@ -155,13 +295,34 @@ bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payloa std::string& Workspace::selectIcon(std::map& icons_map) { spdlog::trace("Selecting icon for workspace {}", name()); if (isUrgent()) { + auto urgentNamedIconIt = icons_map.find("urgent:" + name()); + if (urgentNamedIconIt != icons_map.end()) { + return urgentNamedIconIt->second; + } + auto urgentIconIt = icons_map.find("urgent"); if (urgentIconIt != icons_map.end()) { return urgentIconIt->second; } } + if (isActive() && isSpecial()) { + auto activeIconIt = icons_map.find("active:" + name()); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; + } + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; + } + } + if (isActive()) { + auto activeNamedIconIt = icons_map.find("active:" + name()); + if (activeNamedIconIt != icons_map.end()) { + return activeNamedIconIt->second; + } + auto activeIconIt = icons_map.find("active"); if (activeIconIt != icons_map.end()) { return activeIconIt->second; @@ -169,6 +330,11 @@ std::string& Workspace::selectIcon(std::map& icons_map } if (isSpecial()) { + auto specialNamedIconIt = icons_map.find("special:" + name()); + if (specialNamedIconIt != icons_map.end()) { + return specialNamedIconIt->second; + } + auto specialIconIt = icons_map.find("special"); if (specialIconIt != icons_map.end()) { return specialIconIt->second; @@ -215,6 +381,15 @@ void Workspace::update(const std::string& workspace_icon) { return; } // clang-format off + if (this->m_workspaceManager.hideActive() && \ + this->isActive() && \ + !this->isPersistent() && \ + !this->isSpecial()) { + // clang-format on + m_button.hide(); + return; + } + // clang-format off if (this->m_workspaceManager.activeOnly() && \ !this->isActive() && \ !this->isPersistent() && \ @@ -234,26 +409,79 @@ void Workspace::update(const std::string& workspace_icon) { auto styleContext = m_button.get_style_context(); addOrRemoveClass(styleContext, isActive(), "active"); addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isSpecial(), name()); addOrRemoveClass(styleContext, isEmpty(), "empty"); addOrRemoveClass(styleContext, isPersistent(), "persistent"); addOrRemoveClass(styleContext, isUrgent(), "urgent"); addOrRemoveClass(styleContext, isVisible(), "visible"); addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); + // Add workspace name as CSS class for per-workspace styling + if (!m_prevNameClass.empty()) { + styleContext->remove_class(m_prevNameClass); + m_prevNameClass.clear(); + } + auto nameClass = sanitizeCssClass(name()); + if (!nameClass.empty()) { + styleContext->add_class(nameClass); + m_prevNameClass = nameClass; + } + std::string windows; // Optimization: The {windows} substitution string is only possible if the taskbar is disabled, no // need to compute this if enableTaskbar() is true if (!m_workspaceManager.enableTaskbar()) { auto windowSeparator = m_workspaceManager.getWindowSeparator(); + auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold(); - bool isNotFirst = false; + auto end_it = m_workspaceManager.maxWindows() == 0 + ? m_windowMap.end() + : m_windowMap.begin() + m_workspaceManager.maxWindows(); - for (const auto& window_repr : m_windowMap) { - if (isNotFirst) { - windows.append(windowSeparator); + if (groupThreshold > 0) { + // Build ordered counts of each unique icon (including singular ones when threshold set to 1) + std::vector> iconCounts; + for (auto it = m_windowMap.begin(); it != end_it; ++it) { + const auto& window_repr = *it; + auto found = std::ranges::find_if( + iconCounts, [&](const auto& p) { return p.first == window_repr.repr_rewrite; }); + if (found != iconCounts.end()) { + found->second++; + } else { + iconCounts.emplace_back(window_repr.repr_rewrite, 1); + } + } + + // Format the group string + auto groupFormat = m_workspaceManager.getWindowRewriteGroupFormat(); + bool isNotFirst = false; + for (const auto& [icon, count] : iconCounts) { + if (count >= groupThreshold) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + try { + windows.append(fmt::format(fmt::runtime(groupFormat), fmt::arg("icon", icon), + fmt::arg("count", count))); + } catch (const fmt::format_error& e) { + spdlog::warn("Formatting window-rewrite-group-format error: {}", e.what()); + windows.append(icon); + } + } else { + for (int i = 0; i < count; ++i) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + windows.append(icon); + } + } + } + } else { + // Not grouping icons + bool isNotFirst = false; + for (auto it = m_windowMap.begin(); it != end_it; ++it) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + windows.append(it->repr_rewrite); } - isNotFirst = true; - windows.append(window_repr.repr_rewrite); } } @@ -270,7 +498,9 @@ void Workspace::update(const std::string& workspace_icon) { bool Workspace::isEmpty() const { auto ignore_list = m_workspaceManager.getIgnoredWindows(); - if (ignore_list.empty()) { + const bool no_ignore_rules = ignore_list.empty(); + + if (no_ignore_rules) { return m_windows == 0; } // If there are windows but they are all ignored, consider the workspace empty @@ -301,15 +531,17 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { auto window_box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); window_box->set_tooltip_markup(window_repr.window_title); - window_box->get_style_context()->add_class("taskbar-window"); + + 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,22 +566,32 @@ 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()) { - for (auto it = m_windowMap.rbegin(); it != m_windowMap.rend(); ++it) { + auto rend_it = m_workspaceManager.maxWindows() == 0 + ? m_windowMap.rend() + : m_windowMap.rbegin() + m_workspaceManager.maxWindows(); + + for (auto it = m_windowMap.rbegin(); it != rend_it; ++it) { processWindow(*it); } } else { - for (const auto& window_repr : m_windowMap) { - processWindow(window_repr); + auto end_it = m_workspaceManager.maxWindows() == 0 + ? m_windowMap.end() + : m_windowMap.begin() + m_workspaceManager.maxWindows(); + + for (auto it = m_windowMap.begin(); it != end_it; ++it) { + processWindow(*it); } } auto formatAfter = m_workspaceManager.formatAfter(); - if (!formatAfter.empty()) { + const bool has_format_after = !formatAfter.empty(); + + if (has_format_after) { m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()), fmt::arg("name", name()), fmt::arg("icon", workspace_icon))); diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 762060e2..2887372f 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -50,7 +50,8 @@ void Workspaces::init() { if (m_scrollEventConnection_.connected()) { m_scrollEventConnection_.disconnect(); } - if (barScroll()) { + 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_ = @@ -326,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; @@ -673,6 +678,7 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly); populateBoolConfig(config, "persistent-only", m_persistentOnly); populateBoolConfig(config, "active-only", m_activeOnly); + populateBoolConfig(config, "hide-active", m_hideActive); populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); populateBoolConfig(config, "enable-bar-scroll", m_barScroll); @@ -680,7 +686,18 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { populateSortByConfig(config); populateIgnoreWorkspacesConfig(config); populateFormatWindowSeparatorConfig(config); + + const auto& groupThreshold = config["window-rewrite-group-threshold"]; + if (groupThreshold.isInt()) { + m_windowRewriteGroupThreshold = groupThreshold.asInt(); + } + const auto& groupFormat = config["window-rewrite-group-format"]; + if (groupFormat.isString()) { + m_windowRewriteGroupFormat = groupFormat.asString(); + } + populateWindowRewriteConfig(config); + populateMaxWindowsConfig(config); if (withWindows) { populateWorkspaceTaskbarConfig(config); @@ -762,6 +779,15 @@ auto Workspaces::populateWindowRewriteConfig(const Json::Value& config) -> void [this](std::string& window_rule) { return windowRewritePriorityFunction(window_rule); }); } +auto Workspaces::populateMaxWindowsConfig(const Json::Value& config) -> void { + if (config["max-windows"].isInt()) { + m_maxWindows = config["max-windows"].asInt(); + if (m_maxWindows < 0) { + m_maxWindows = 0; + } + } +} + auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value& config) -> void { const auto& workspaceTaskbar = config["workspace-taskbar"]; if (!workspaceTaskbar.isObject()) { @@ -1204,6 +1230,12 @@ bool Workspaces::handleScroll(GdkEventScroll* e) { 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; diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index a5fc9ac7..9638d0fc 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -1,5 +1,6 @@ #include "modules/idle_inhibitor.hpp" +#include "ext-idle-notify-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/command.hpp" @@ -11,11 +12,24 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& : ALabel(config, "idle_inhibitor", id, "{status}", 0, false, true), bar_(bar), idle_inhibitor_(nullptr), - pid_(-1) { + idle_notification_(nullptr), + idle_timeout_ms_(0), + pid_(-1), + wait_for_activity_(false) { if (waybar::Client::inst()->idle_inhibit_manager == nullptr) { throw std::runtime_error("idle-inhibit not available"); } + // Read the wait-for-activity config option + if (config_["wait-for-activity"].isBool()) { + wait_for_activity_ = config_["wait-for-activity"].asBool(); + + // Check if ext-idle-notify protocol is available when wait-for-activity is enabled + if (wait_for_activity_ && waybar::Client::inst()->idle_notifier == nullptr) { + throw std::runtime_error("wait-for-activity requires ext-idle-notify-v1 protocol support"); + } + } + if (waybar::modules::IdleInhibitor::modules.empty() && config_["start-activated"].isBool() && config_["start-activated"].asBool() != status) { toggleStatus(); @@ -32,6 +46,8 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& } waybar::modules::IdleInhibitor::~IdleInhibitor() { + teardownIdleNotification(); + if (idle_inhibitor_ != nullptr) { zwp_idle_inhibitor_v1_destroy(idle_inhibitor_); idle_inhibitor_ = nullptr; @@ -77,6 +93,17 @@ auto waybar::modules::IdleInhibitor::update() -> void { ALabel::update(); } +auto waybar::modules::IdleInhibitor::refresh(int sig) -> void { + if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { + toggleStatus(); + + // Make all other idle inhibitor modules update + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } + } +} + void waybar::modules::IdleInhibitor::toggleStatus() { status = !status; @@ -88,21 +115,34 @@ void waybar::modules::IdleInhibitor::toggleStatus() { if (status && config_["timeout"].isNumeric()) { auto timeoutMins = config_["timeout"].asDouble(); int timeoutSecs = timeoutMins * 60; + idle_timeout_ms_ = timeoutSecs * 1000; - timeout_ = Glib::signal_timeout().connect_seconds( - []() { - /* intentionally not tied to a module instance lifetime - * as the output with `this` can be disconnected - */ - spdlog::info("deactivating idle_inhibitor by timeout"); - status = false; - for (auto const& module : waybar::modules::IdleInhibitor::modules) { - module->update(); - } - /* disconnect */ - return false; - }, - timeoutSecs); + // If wait-for-activity is enabled, set up idle notification + if (wait_for_activity_) { + spdlog::debug("idle_inhibitor: wait-for-activity enabled, timeout: {} ms", idle_timeout_ms_); + // Tear down any existing notification first to ensure fresh setup + teardownIdleNotification(); + setupIdleNotification(); + } else { + // Original behavior: simple timeout + timeout_ = Glib::signal_timeout().connect_seconds( + []() { + /* intentionally not tied to a module instance lifetime + * as the output with `this` can be disconnected + */ + spdlog::info("deactivating idle_inhibitor by timeout"); + status = false; + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } + /* disconnect */ + return false; + }, + timeoutSecs); + } + } else { + // When deactivated, tear down idle notification + teardownIdleNotification(); } } @@ -121,3 +161,96 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { ALabel::handleToggle(e); return true; } + +void waybar::modules::IdleInhibitor::handleIdled(void* data, + ext_idle_notification_v1* /*notification*/) { + spdlog::info("deactivating idle_inhibitor due to user inactivity"); + status = false; + + // Clean up the notification since we're deactivating + auto* self = static_cast(data); + if (self != nullptr) { + self->teardownIdleNotification(); + } + + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } +} + +void waybar::modules::IdleInhibitor::handleResumed(void* data, + ext_idle_notification_v1* /*notification*/) { + // User became active again - notification will continue monitoring + spdlog::debug("user activity detected, idle_inhibitor still active"); +} + +void waybar::modules::IdleInhibitor::setupIdleNotification() { + spdlog::debug("idle_inhibitor: setting up idle notification"); + + // Clean up any existing notification first + if (idle_notification_ != nullptr) { + spdlog::debug("idle_inhibitor: cleaning up existing notification before setup"); + teardownIdleNotification(); + } + + auto* client = waybar::Client::inst(); + if (client->idle_notifier == nullptr) { + spdlog::error("ext-idle-notify protocol not available"); + return; + } + + // Get the wayland seat from the display + auto* gdk_seat = gdk_display_get_default_seat(client->gdk_display->gobj()); + if (gdk_seat == nullptr) { + spdlog::error("failed to get default seat"); + return; + } + auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat); + + // Check protocol version to determine which function to use + uint32_t version = + wl_proxy_get_version(reinterpret_cast(client->idle_notifier)); + + spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})", + idle_timeout_ms_, version); + + if (version >= 2) { + // Version 2+: Use get_input_idle_notification which ignores idle inhibitors + // This allows us to detect actual user inactivity even while the inhibitor is active + spdlog::debug("idle_inhibitor: using get_input_idle_notification (ignores inhibitors)"); + idle_notification_ = ext_idle_notifier_v1_get_input_idle_notification( + client->idle_notifier, idle_timeout_ms_, wl_seat); + } else { + // Version 1: Fall back to get_idle_notification + // WARNING: This respects idle inhibitors, so it won't fire while inhibitor is active + spdlog::warn( + "idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support " + "get_input_idle_notification, " + "wait-for-activity may not work correctly", + version); + idle_notification_ = ext_idle_notifier_v1_get_idle_notification(client->idle_notifier, + idle_timeout_ms_, wl_seat); + } + + if (idle_notification_ == nullptr) { + spdlog::error("idle_inhibitor: failed to create idle notification"); + return; + } + + static const struct ext_idle_notification_v1_listener idle_notification_listener = { + .idled = &IdleInhibitor::handleIdled, + .resumed = &IdleInhibitor::handleResumed, + }; + + ext_idle_notification_v1_add_listener(idle_notification_, &idle_notification_listener, this); + wl_display_roundtrip(client->wl_display); + spdlog::debug("idle_inhibitor: idle notification setup complete"); +} + +void waybar::modules::IdleInhibitor::teardownIdleNotification() { + if (idle_notification_ != nullptr) { + spdlog::debug("idle_inhibitor: tearing down idle notification"); + ext_idle_notification_v1_destroy(idle_notification_); + idle_notification_ = nullptr; + } +} diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 98bf3c46..554afd71 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -1,5 +1,7 @@ #include "modules/image.hpp" +#include + waybar::modules::Image::Image(const std::string& id, const Json::Value& config) : AModule(config, "image", id), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { box_.pack_start(image_); @@ -35,6 +37,13 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config) size_ = 16; } + if (config_["path"].isString()) { + auto result = Config::tryExpandPath(config_["path"].asString(), ""); + path_ = result.empty() ? "" : result.front(); + } else { + path_.clear(); + } + delayWorker(); } @@ -46,19 +55,20 @@ void waybar::modules::Image::delayWorker() { } void waybar::modules::Image::refresh(int sig) { +#ifdef SIGRTMIN if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); } +#endif } auto waybar::modules::Image::update() -> void { - if (config_["path"].isString()) { - path_ = config_["path"].asString(); - } else if (config_["exec"].isString()) { + if (config_["exec"].isString()) { output_ = util::command::exec(config_["exec"].asString(), ""); parseOutputRaw(); - } else { - path_ = ""; + // expand path if "~" or "$HOME" is present in original path + auto result = Config::tryExpandPath(path_, ""); + path_ = result.empty() ? "" : result.front(); } if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) { diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index a2207fdd..49cf863f 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -78,6 +78,53 @@ auto supportsLockStates(const libevdev* dev) -> bool { libevdev_has_event_code(dev, EV_LED, LED_SCROLLL); } +auto isCommonFormatIcons(const Json::Value& config) -> bool { + return config["format-icons"].isObject() && (config["format-icons"]["locked"].isString() || + config["format-icons"]["unlocked"].isString()); +} + +auto keyStateToIcons(const Json::Value& config) + -> std::unordered_map> { + std::unordered_map> key_icon_states; + std::vector default_icons = {"unlocked", "locked"}; + + if (isCommonFormatIcons(config)) { + std::vector icons = { + config["format-icons"]["unlocked"].isString() + ? config["format-icons"]["unlocked"].asString() + : "unlocked", + config["format-icons"]["locked"].isString() ? config["format-icons"]["locked"].asString() + : "locked", + }; + key_icon_states["Lock"] = icons; + return key_icon_states; + } + + bool found_any = false; + for (const auto& key : std::vector{"numlock", "capslock", "scrolllock"}) { + std::string map_key = key.substr(0, key.length() - 4); + map_key[0] = std::toupper(map_key[0]); + if (config["format-icons"].isObject() && config["format-icons"][key].isObject()) { + std::string unlocked = config["format-icons"][key]["unlocked"].isString() + ? config["format-icons"][key]["unlocked"].asString() + : "unlocked"; + std::string locked = config["format-icons"][key]["locked"].isString() + ? config["format-icons"][key]["locked"].asString() + : "locked"; + key_icon_states[map_key] = {unlocked, locked}; + found_any = true; + } + } + + if (!found_any) { + key_icon_states["Num"] = default_icons; + key_icon_states["Caps"] = default_icons; + key_icon_states["Scroll"] = default_icons; + } + + return key_icon_states; +} + waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& bar, const Json::Value& config) : AModule(config, "keyboard-state", id, false, !config["disable-scroll"].asBool()), @@ -98,12 +145,7 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& : "{name} {icon}"), interval_( std::chrono::seconds(config_["interval"].isUInt() ? config_["interval"].asUInt() : 1)), - icon_locked_(config_["format-icons"]["locked"].isString() - ? config_["format-icons"]["locked"].asString() - : "locked"), - icon_unlocked_(config_["format-icons"]["unlocked"].isString() - ? config_["format-icons"]["unlocked"].asString() - : "unlocked"), + key_icon_states_(keyStateToIcons(config_)), devices_path_("/dev/input/"), libinput_(nullptr), libinput_devices_({}) { @@ -290,7 +332,7 @@ auto waybar::modules::KeyboardState::update() -> void { bool state; Gtk::Label& label; const std::string& format; - const char* name; + const std::string name; } label_states[] = { {(bool)numl, numlock_label_, numlock_format_, "Num"}, {(bool)capsl, capslock_label_, capslock_format_, "Caps"}, @@ -298,8 +340,21 @@ auto waybar::modules::KeyboardState::update() -> void { }; for (auto& label_state : label_states) { std::string text; + std::string map_key = isCommonFormatIcons(config_) ? "Lock" : label_state.name; + + if (key_icon_states_.find(map_key) == key_icon_states_.end()) { + spdlog::warn("keyboard-state: Missing icon configuration for '{}'", map_key); + continue; + } + + auto& icons = key_icon_states_[map_key]; + if (icons.size() < 2) { + spdlog::warn("keyboard-state: Invalid icon vector size for '{}'", map_key); + continue; + } + text = fmt::format(fmt::runtime(label_state.format), - fmt::arg("icon", label_state.state ? icon_locked_ : icon_unlocked_), + fmt::arg("icon", label_state.state ? icons[1] : icons[0]), fmt::arg("name", label_state.name)); label_state.label.set_markup(text); if (label_state.state) { diff --git a/src/modules/mango/backend.cpp b/src/modules/mango/backend.cpp new file mode 100644 index 00000000..a8976f21 --- /dev/null +++ b/src/modules/mango/backend.cpp @@ -0,0 +1,317 @@ +#include "modules/mango/backend.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "util/scoped_fd.hpp" + +namespace waybar::modules::mango { + +int IPC::connectToSocket() { + const char* socket_path = getenv("MANGO_INSTANCE_SIGNATURE"); + if (!socket_path) { + throw std::runtime_error("Mango IPC: MANGO_INSTANCE_SIGNATURE not set"); + } + + struct sockaddr_un addr; + util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0)); + if (fd == -1) throw std::runtime_error("socket() failed"); + + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + + if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { + throw std::runtime_error("connect() failed"); + } + return fd.release(); +} + +Json::Value IPC::sendCommand(const std::string& cmd) { + util::ScopedFd fd(IPC::connectToSocket()); + std::string full_cmd = cmd + "\n"; + + ssize_t total_written = 0; + while (total_written < (ssize_t)full_cmd.size()) { + ssize_t res = write(fd, full_cmd.c_str() + total_written, full_cmd.size() - total_written); + if (res < 0) { + if (errno == EINTR) continue; + throw std::runtime_error("Failed to write command"); + } + total_written += res; + } + + char buf[4096]; + std::string response; + while (true) { + ssize_t n = read(fd, buf, sizeof(buf) - 1); + if (n <= 0) { + if (n == 0) break; + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; + throw std::runtime_error("Read error"); + } + buf[n] = '\0'; + response += buf; + if (response.find('\n') != std::string::npos) break; + } + + Json::Value root; + std::istringstream iss(response); + Json::CharReaderBuilder builder; + std::string errors; + if (!Json::parseFromStream(builder, iss, &root, &errors)) { + throw std::runtime_error("JSON parse error: " + errors); + } + return root; +} + +Json::Value IPC::send(const Json::Value& request) { + if (!request.isMember("command")) { + throw std::runtime_error("Mango IPC: request must have 'command' field"); + } + return sendCommand(request["command"].asString()); +} + +void IPC::sendAsync(const Json::Value& request) { + if (!request.isMember("command")) { + spdlog::error("Mango IPC: request must have 'command' field"); + return; + } + std::string cmd = request["command"].asString(); + + std::thread([cmd]() { + try { + IPC::sendCommand(cmd); + } catch (const std::exception& e) { + spdlog::error("IPC async send failed: {}", e.what()); + } + }).detach(); +} + +IPC::IPC() : sockfd_(-1), active_client_(Json::nullValue) { startIPC(); } + +IPC::~IPC() { + if (sockfd_ != -1) close(sockfd_); + if (ipc_thread_.joinable()) ipc_thread_.join(); +} + +void IPC::startIPC() { + sockfd_ = IPC::connectToSocket(); + + ipc_thread_ = std::thread([this]() { + spdlog::info("Mango IPC thread started"); + + struct pollfd pfd; + pfd.fd = sockfd_; + pfd.events = POLLIN; + + const std::vector subs = {"watch all-monitors"}; + for (const auto& cmd : subs) { + if (write(sockfd_, cmd.c_str(), cmd.size()) != (ssize_t)cmd.size() || + write(sockfd_, "\n", 1) != 1) { + spdlog::error("Failed to subscribe to {}", cmd); + return; + } + } + + char buf[4096]; + std::string buffer; + while (true) { + int ret = poll(&pfd, 1, 1000); + if (ret == 0) continue; + if (ret < 0) { + if (errno == EINTR) continue; + spdlog::error("IPC poll error: {}", strerror(errno)); + break; + } + + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + spdlog::info("Mango IPC socket closed or invalid"); + break; + } + + if (pfd.revents & POLLIN) { + ssize_t n = read(sockfd_, buf, sizeof(buf)); + if (n == 0) { + spdlog::info("Mango IPC connection closed"); + break; + } + if (n < 0) { + if (errno == EINTR) continue; + spdlog::error("IPC read error: {}", strerror(errno)); + break; + } + buffer.append(buf, n); + + size_t pos; + while ((pos = buffer.find('\n')) != std::string::npos) { + std::string line = buffer.substr(0, pos); + buffer.erase(0, pos + 1); + if (line.empty()) continue; + try { + parseIPC(line); + } catch (const std::exception& e) { + spdlog::warn("Failed to parse IPC line: {} - {}", line, e.what()); + } + } + } + } + }); +} + +void IPC::parseIPC(const std::string& line) { + Json::Value root; + Json::CharReaderBuilder builder; + std::string errors; + std::istringstream iss(line); + if (!Json::parseFromStream(builder, iss, &root, &errors)) { + throw std::runtime_error("JSON parse error: " + errors); + } + + if (root.isMember("monitors") && root["monitors"].isArray()) { + for (const auto& mon : root["monitors"]) { + handleMonitorUpdate(mon); + } + + Json::Value active_monitor; + for (const auto& mon : root["monitors"]) { + if (mon["active"].asBool()) { + active_monitor = mon; + break; + } + } + + if (!active_monitor.isNull()) { + const auto& active_client = active_monitor["active_client"]; + updateFocusingClient(active_client); + + if (active_monitor.isMember("keyboardlayout")) { + updateKeyboardLayout(active_monitor["keyboardlayout"].asString()); + } + + if (active_monitor.isMember("keymode")) { + std::lock_guard lock(data_mutex_); + keymode_ = active_monitor["keymode"].asString(); + } + } + + std::vector handlers_to_notify; + { + std::lock_guard lock(callback_mutex_); + for (auto& [ev, handler] : callbacks_) { + if (ev == "monitor") { + handlers_to_notify.push_back(handler); + } + } + } + + for (auto* handler : handlers_to_notify) { + handler->onEvent(root); + } + + return; + } + + spdlog::debug("Unhandled IPC message: {}", line); +} + +std::unordered_map IPC::getMonitors() const { + std::lock_guard lock(data_mutex_); + return monitors_; +} + +IPC& IPC::getInstance() { + static IPC instance; + return instance; +} + +Json::Value IPC::getMonitor(const std::string& name) { + std::lock_guard lock(data_mutex_); + auto it = monitors_.find(name); + if (it != monitors_.end()) { + return it->second; + } + return Json::nullValue; +} + +std::string IPC::getKeyboardLayout() const { + std::lock_guard lock(data_mutex_); + return keyboard_layout_; +} + +std::string IPC::getKeymode() const { + std::lock_guard lock(data_mutex_); + return keymode_; +} + +Json::Value IPC::getActiveClientForMonitor(const std::string& name) const { + std::lock_guard lock(data_mutex_); + auto it = monitors_.find(name); + if (it != monitors_.end() && it->second.isMember("active_client")) { + return it->second["active_client"]; + } + return Json::nullValue; +} + +std::string IPC::getLayoutSymbolForMonitor(const std::string& name) const { + std::lock_guard lock(data_mutex_); + auto it = monitors_.find(name); + if (it != monitors_.end() && it->second.isMember("layout_symbol")) { + return it->second["layout_symbol"].asString(); + } + return {}; +} + +void IPC::handleMonitorUpdate(const Json::Value& mon) { + std::lock_guard lock(data_mutex_); + monitors_[mon["name"].asString()] = mon; +} + +void IPC::updateFocusingClient(const Json::Value& client) { + { + std::lock_guard lock(data_mutex_); + active_client_ = client; + + if (client.isNull() || !client.isObject() || client["id"].isNull()) { + focusing_client_id_ = 0; + } else { + focusing_client_id_ = client["id"].asUInt64(); + clients_[focusing_client_id_] = client; + } + } +} + +void IPC::updateKeyboardLayout(const std::string& layout) { + { + std::lock_guard lock(data_mutex_); + keyboard_layout_ = layout; + } +} + +void IPC::registerForIPC(const std::string& ev, EventHandler* handler) { + if (!handler) return; + std::lock_guard lock(callback_mutex_); + callbacks_.emplace_back(ev, handler); +} + +void IPC::unregisterForIPC(EventHandler* handler) { + if (!handler) return; + std::lock_guard lock(callback_mutex_); + for (auto it = callbacks_.begin(); it != callbacks_.end();) { + if (it->second == handler) + it = callbacks_.erase(it); + else + ++it; + } +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/keymode.cpp b/src/modules/mango/keymode.cpp new file mode 100644 index 00000000..16009712 --- /dev/null +++ b/src/modules/mango/keymode.cpp @@ -0,0 +1,58 @@ +#include "modules/mango/keymode.hpp" + +#include + +namespace waybar::modules::mango { + +Keymode::Keymode(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "keymode", id, "{}", 0, false), bar_(bar) { + IPC::getInstance().registerForIPC("monitor", this); + dp.emit(); +} + +Keymode::~Keymode() { IPC::getInstance().unregisterForIPC(this); } + +void Keymode::onEvent(const Json::Value& ev) { dp.emit(); } + +void Keymode::doUpdate() { + std::lock_guard lock(mutex_); + + std::string current = IPC::getInstance().getKeymode(); + + // if keymode is empty, hide the label + if (current.empty()) { + label_.hide(); + last_keymode_.clear(); + return; + } + + // if keymode is the same as last time, skip style changes + if (current != last_keymode_) { + if (!last_keymode_.empty()) label_.get_style_context()->remove_class(last_keymode_); + label_.get_style_context()->add_class(current); + last_keymode_ = current; + } + + // support config's format-keymode custom format (such as format-default, format-resize, etc.) + std::string text; + std::string format_key = "format-" + current; + if (config_.isMember(format_key)) { + text = fmt::format(fmt::runtime(config_[format_key].asString()), fmt::arg("mode", current)); + } else { + text = fmt::format(fmt::runtime(format_), fmt::arg("mode", current)); + } + + if (!text.empty()) { + label_.show(); + label_.set_markup(text); + } else { + label_.hide(); + } +} + +void Keymode::update() { + doUpdate(); + ALabel::update(); +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/language.cpp b/src/modules/mango/language.cpp new file mode 100644 index 00000000..9415002a --- /dev/null +++ b/src/modules/mango/language.cpp @@ -0,0 +1,148 @@ +#include "modules/mango/language.hpp" + +#include +#include +#include + +#include "util/string.hpp" + +namespace waybar::modules::mango { + +Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "language", id, "{}", 0, false), bar_(bar), rxkb_ctx_(nullptr) { + rxkb_ctx_ = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); + if (rxkb_ctx_) { + rxkb_context_parse_default_ruleset(rxkb_ctx_); + } + + IPC::getInstance().registerForIPC("monitor", this); + updateFromIPC(); + dp.emit(); +} + +Language::~Language() { + IPC::getInstance().unregisterForIPC(this); + if (rxkb_ctx_) rxkb_context_unref(rxkb_ctx_); +} + +void Language::updateFromIPC() { + std::lock_guard lock(mutex_); + std::string layout = IPC::getInstance().getKeyboardLayout(); + + layouts_.clear(); + if (!layout.empty()) { + Layout l = getLayout(layout); + layouts_.push_back(l); + current_idx_ = 0; + } else { + current_idx_ = 0; + } +} + +void Language::doUpdate() { + std::lock_guard lock(mutex_); + if (layouts_.empty() || current_idx_ >= layouts_.size()) { + label_.hide(); + return; + } + const auto& layout = layouts_[current_idx_]; + + if (!last_short_name_.empty()) label_.get_style_context()->remove_class(last_short_name_); + if (!layout.short_name.empty()) { + label_.get_style_context()->add_class(layout.short_name); + last_short_name_ = layout.short_name; + } + + std::string layoutName; + std::string variant_key = "format-" + layout.short_description + "-" + layout.variant; + if (!layout.variant.empty() && config_.isMember(variant_key)) { + layoutName = + fmt::format(fmt::runtime(config_[variant_key].asString()), + 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 (config_.isMember("format-" + layout.short_description)) { + std::string key = "format-" + layout.short_description; + layoutName = + fmt::format(fmt::runtime(config_[key].asString()), fmt::arg("long", layout.full_name), + fmt::arg("short", layout.short_name), + fmt::arg("shortDescription", layout.short_description), + fmt::arg("variant", layout.variant)); + } else { + layoutName = fmt::format(fmt::runtime(format_), fmt::arg("long", layout.full_name), + fmt::arg("short", layout.short_name), + fmt::arg("shortDescription", layout.short_description), + fmt::arg("variant", layout.variant)); + } + + if (!layoutName.empty()) { + label_.show(); + label_.set_markup(layoutName); + } else { + label_.hide(); + } +} + +void Language::update() { + updateFromIPC(); + doUpdate(); + ALabel::update(); +} + +void Language::onEvent(const Json::Value& ev) { + updateFromIPC(); + dp.emit(); +} + +Language::Layout Language::getLayout(const std::string& fullName) { + if (rxkb_ctx_) { + rxkb_layout* layout = rxkb_layout_first(rxkb_ctx_); + while (layout != nullptr) { + std::string desc = rxkb_layout_get_description(layout); + if (desc == fullName) { + std::string short_name = rxkb_layout_get_name(layout); + const char* variant_ptr = rxkb_layout_get_variant(layout); + std::string variant = variant_ptr ? variant_ptr : ""; + const char* brief_ptr = rxkb_layout_get_brief(layout); + std::string short_description = brief_ptr ? brief_ptr : ""; + + if (short_description.empty()) { + short_description = short_name; + } + + short_description = short_name; + + Layout info{desc, short_name, variant, short_description}; + return info; + } + layout = rxkb_layout_next(layout); + } + } + + spdlog::warn("mango language: rxkb failed to find layout '{}', using string parsing fallback", + fullName); + + Layout l; + l.full_name = fullName; + l.variant = ""; + + size_t paren_start = fullName.find('('); + size_t paren_end = fullName.find(')'); + if (paren_start != std::string::npos && paren_end != std::string::npos && + paren_end > paren_start) { + l.short_name = fullName.substr(paren_start + 1, paren_end - paren_start - 1); + } else if (fullName.length() >= 2) { + l.short_name = fullName.substr(0, 2); + } else { + l.short_name = fullName; + } + + std::transform(l.short_name.begin(), l.short_name.end(), l.short_name.begin(), + [](unsigned char c) { return std::tolower(c); }); + + l.short_description = l.short_name; + + return l; +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/layout.cpp b/src/modules/mango/layout.cpp new file mode 100644 index 00000000..124d26cb --- /dev/null +++ b/src/modules/mango/layout.cpp @@ -0,0 +1,56 @@ +#include "modules/mango/layout.hpp" + +#include + +namespace waybar::modules::mango { + +Layout::Layout(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "layout", id, "{}", 0, false), bar_(bar) { + IPC::getInstance().registerForIPC("monitor", this); + dp.emit(); +} + +Layout::~Layout() { IPC::getInstance().unregisterForIPC(this); } + +void Layout::onEvent(const Json::Value& ev) { dp.emit(); } + +void Layout::doUpdate() { + std::lock_guard lock(mutex_); + + std::string symbol = IPC::getInstance().getLayoutSymbolForMonitor(bar_.output->name); + + if (symbol.empty()) { + label_.hide(); + last_symbol_.clear(); + return; + } + + if (symbol != last_symbol_) { + if (!last_symbol_.empty()) label_.get_style_context()->remove_class(last_symbol_); + label_.get_style_context()->add_class(symbol); + last_symbol_ = symbol; + } + + std::string text; + std::string format_key = "format-" + symbol; + + if (config_.isMember(format_key)) { + text = fmt::format(fmt::runtime(config_[format_key].asString()), fmt::arg("symbol", symbol)); + } else { + text = fmt::format(fmt::runtime(format_), fmt::arg("symbol", symbol)); + } + + if (!text.empty()) { + label_.show(); + label_.set_markup(text); + } else { + label_.hide(); + } +} + +void Layout::update() { + doUpdate(); + ALabel::update(); +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/window.cpp b/src/modules/mango/window.cpp new file mode 100644 index 00000000..54f677bb --- /dev/null +++ b/src/modules/mango/window.cpp @@ -0,0 +1,93 @@ +#include "modules/mango/window.hpp" + +#include + +#include "util/rewrite_string.hpp" +#include "util/sanitize_str.hpp" + +namespace waybar::modules::mango { + +Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) + : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) { + IPC::getInstance().registerForIPC("monitor", this); + dp.emit(); +} + +Window::~Window() { IPC::getInstance().unregisterForIPC(this); } + +void Window::onEvent(const Json::Value& ev) { dp.emit(); } + +void Window::doUpdate() { + std::lock_guard lock(mutex_); + + const Json::Value& client = IPC::getInstance().getActiveClientForMonitor(bar_.output->name); + + // judge whether to hide: active_client is null or title field is null + if (client.isNull() || !client.isObject() || client["title"].isNull()) { + event_box_.hide(); + label_.hide(); + updateAppIconName("", ""); + setClass("empty", true); + if (!oldAppId_.empty()) setClass(oldAppId_, false); + oldAppId_.clear(); + return; + } + + // if we have a valid client, show the label and update content + event_box_.show(); + label_.show(); + setClass("empty", false); + + std::string title = client["title"].asString(); + std::string appid = client["appid"].asString(); + std::string sanitized_title = waybar::util::sanitize_string(title); + std::string sanitized_appid = waybar::util::sanitize_string(appid); + + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", sanitized_title), + fmt::arg("app_id", sanitized_appid)), + config_["rewrite"])); + + updateAppIconName(appid, ""); + if (tooltipEnabled()) label_.set_tooltip_markup(title); + + // Solo judgment + bool solo = false; + if (client.isMember("tags") && client["tags"].isArray() && client["tags"].size() == 1) { + int tag_idx = client["tags"][0].asInt(); + const auto& monitors = IPC::getInstance().getMonitors(); + auto mon_it = monitors.find(client["monitor"].asString()); + if (mon_it != monitors.end()) { + const auto& tags = mon_it->second["tags"]; + for (const auto& tag : tags) { + if (tag["index"].asInt() == tag_idx) { + solo = (tag["client_count"].asInt() == 1); + break; + } + } + } + } + setClass("solo", solo); + if (!appid.empty()) setClass(appid, solo); + + if (oldAppId_ != appid) { + if (!oldAppId_.empty()) setClass(oldAppId_, false); + oldAppId_ = appid; + } +} + +void Window::update() { + doUpdate(); + AAppIconLabel::update(); +} + +void Window::setClass(const std::string& className, bool enable) { + auto style_context = event_box_.get_style_context(); + if (enable) { + if (!style_context->has_class(className)) style_context->add_class(className); + } else { + style_context->remove_class(className); + } +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/workspaces.cpp b/src/modules/mango/workspaces.cpp new file mode 100644 index 00000000..f8849228 --- /dev/null +++ b/src/modules/mango/workspaces.cpp @@ -0,0 +1,252 @@ +#include "modules/mango/workspaces.hpp" + +#include + +#include + +namespace waybar::modules::mango { + +Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config) + : AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) { + box_.set_name("workspaces"); + if (!id.empty()) box_.get_style_context()->add_class(id); + box_.get_style_context()->add_class(MODULE_CLASS); + event_box_.add(box_); + + if (config_["on-click"].isString()) on_click_left_ = config_["on-click"].asString(); + if (config_["on-click-middle"].isString()) + on_click_middle_ = config_["on-click-middle"].asString(); + if (config_["on-click-right"].isString()) on_click_right_ = config_["on-click-right"].asString(); + + overview_button_ = new Gtk::Button("OVERVIEW"); + overview_button_->set_relief(Gtk::RELIEF_NONE); + box_.pack_start(*overview_button_, false, false, 0); + + if (!on_click_left_.empty() || !on_click_middle_.empty() || !on_click_right_.empty()) { + overview_button_->add_events(Gdk::BUTTON_PRESS_MASK); + overview_button_->signal_button_press_event().connect( + [this](GdkEventButton* event) -> bool { return handleButtonClick(event, 0, true); }, false); + } + + IPC::getInstance().registerForIPC("monitor", this); + dp.emit(); +} + +Workspaces::~Workspaces() { + IPC::getInstance().unregisterForIPC(this); + + if (overview_button_) { + box_.remove(*overview_button_); + delete overview_button_; + overview_button_ = nullptr; + } + + for (auto& [idx, btn] : buttons_) { + box_.remove(btn); + } + buttons_.clear(); +} + +void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); } + +void Workspaces::doUpdate() { + Json::Value monitor = IPC::getInstance().getMonitor(bar_.output->name); + if (monitor.isNull()) return; + + const auto& tags = monitor["tags"]; + + bool overview_mode = false; + if (monitor.isMember("active_tags") && monitor["active_tags"].isArray()) { + const auto& active_tags = monitor["active_tags"]; + if (active_tags.size() == 1 && active_tags[0].asInt() == 0) { + overview_mode = true; + } + } + + for (auto& [idx, btn] : buttons_) { + btn.hide(); + } + + if (overview_mode) { + overview_button_->show(); + auto style = overview_button_->get_style_context(); + style->add_class("overview"); + if (monitor["active"].asBool()) + style->add_class("current_output"); + else + style->remove_class("current_output"); + + std::string label = + config_["overview-label"].isString() ? config_["overview-label"].asString() : "OVERVIEW"; + + if (!config_["disable-markup"].asBool()) { + if (auto gtk_label = dynamic_cast(overview_button_->get_child())) { + gtk_label->set_markup(label); + } + } else { + overview_button_->set_label(label); + } + } else { + overview_button_->hide(); + + for (auto btn_it = buttons_.begin(); btn_it != buttons_.end();) { + uint64_t id = btn_it->first; + bool found = std::any_of(tags.begin(), tags.end(), [id](const Json::Value& tag) { + return tag["index"].asUInt64() == id; + }); + + if (!found) { + box_.remove(btn_it->second); + btn_it = buttons_.erase(btn_it); + } else { + ++btn_it; + } + } + + for (const auto& tag : tags) { + uint64_t idx = tag["index"].asUInt64(); + auto btn_it = buttons_.find(idx); + Gtk::Button& button = (btn_it == buttons_.end()) ? addButton(idx) : btn_it->second; + updateButtonState(button, tag, monitor); + } + + std::vector indices; + for (const auto& tag : tags) indices.push_back(tag["index"].asUInt64()); + std::sort(indices.begin(), indices.end()); + int pos = 0; + for (uint64_t idx : indices) { + box_.reorder_child(buttons_[idx], pos + 1); + pos++; + } + } +} + +void Workspaces::update() { + doUpdate(); + AModule::update(); +} + +Gtk::Button& Workspaces::addButton(uint64_t idx) { + auto [it, _] = buttons_.emplace(idx, std::to_string(idx)); + auto& button = it->second; + box_.pack_start(button, false, false, 0); + button.set_relief(Gtk::RELIEF_NONE); + + if (!on_click_left_.empty() || !on_click_middle_.empty() || !on_click_right_.empty()) { + button.add_events(Gdk::BUTTON_PRESS_MASK); + button.signal_button_press_event().connect( + [this, idx](GdkEventButton* event) -> bool { return handleButtonClick(event, idx, false); }, + false); + } + + button.show_all(); + return button; +} + +void Workspaces::updateButtonState(Gtk::Button& button, const Json::Value& tag, + const Json::Value& monitor) { + auto style = button.get_style_context(); + bool active = tag["is_active"].asBool(); + bool urgent = tag["is_urgent"].asBool(); + bool empty = (tag["client_count"].asInt() == 0); + + if (active) + style->add_class("active"); + else + style->remove_class("active"); + + if (urgent) + style->add_class("urgent"); + else + style->remove_class("urgent"); + + if (empty) + style->add_class("empty"); + else + style->remove_class("empty"); + + if (monitor["active"].asBool()) + style->add_class("current_output"); + else + style->remove_class("current_output"); + + uint64_t idx = tag["index"].asUInt64(); + std::string name = std::to_string(idx); + if (config_["format"].isString()) { + name = fmt::format(fmt::runtime(config_["format"].asString()), + fmt::arg("icon", getIcon(name, tag)), fmt::arg("value", name), + fmt::arg("index", idx), fmt::arg("output", monitor["name"].asString())); + } + + if (!config_["disable-markup"].asBool()) { + if (auto gtk_label = dynamic_cast(button.get_child())) { + gtk_label->set_markup(name); + } + } else { + button.set_label(name); + } + + if (config_["current-only"].asBool()) { + if (active) + button.show(); + else + button.hide(); + } else if (config_["hide-empty"].asBool() && empty && !active) { + button.hide(); + } else { + button.show(); + } +} + +std::string Workspaces::getIcon(const std::string& value, const Json::Value& tag) { + const auto& icons = config_["format-icons"]; + if (!icons) return value; + + if (tag["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString(); + if (tag["is_active"].asBool() && icons["active"]) return icons["active"].asString(); + if (tag["client_count"].asInt() == 0 && icons["empty"]) return icons["empty"].asString(); + + std::string idx = std::to_string(tag["index"].asUInt()); + if (icons[idx]) return icons[idx].asString(); + if (icons["default"]) return icons["default"].asString(); + return value; +} + +bool Workspaces::handleButtonClick(GdkEventButton* event, uint64_t idx, bool isOverview) { + std::string action; + if (event->button == 1) + action = on_click_left_; + else if (event->button == 2) + action = on_click_middle_; + else if (event->button == 3) + action = on_click_right_; + + if (action.empty()) return true; + + try { + std::string cmd; + if (isOverview) { + if (action == "activate") + cmd = "dispatch overview"; + else if (action == "toggle") + cmd = "dispatch toggleoverview"; + } else { + if (action == "activate") + cmd = "dispatch view," + std::to_string(idx); + else if (action == "toggle") + cmd = "dispatch toggleview," + std::to_string(idx); + } + + if (!cmd.empty()) { + Json::Value req; + req["command"] = cmd; + IPC::sendAsync(req); + } + } catch (const std::exception& e) { + spdlog::error("Error sending IPC command: {}", e.what()); + } + + return true; +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 731f0b08..d41b4fc2 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -1,5 +1,18 @@ #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] { @@ -8,6 +21,11 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config }; if (config["unit"].isString()) { unit_ = config["unit"].asString(); + if (!kUnits.contains(unit_)) { + unit_ = "GiB"; + } + } else { + unit_ = "GiB"; } } @@ -40,7 +58,7 @@ auto waybar::modules::Memory::update() -> void { used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; } - float divisor = calc_divisor(unit_); + float divisor = kUnits.at(unit_); float total_ram = memtotal / divisor; float total_swap = swaptotal / divisor; float used_ram = (memtotal - memfree) / divisor; @@ -82,7 +100,7 @@ auto waybar::modules::Memory::update() -> void { fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), fmt::arg("swapAvail", available_swap))); } else { - label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram, 1)); + label_.set_tooltip_markup(fmt::format("{:.{}f}{} used", used_ram, 1, unit_)); } } } else { @@ -91,25 +109,3 @@ auto waybar::modules::Memory::update() -> void { // Call parent update ALabel::update(); } - -float waybar::modules::Memory::calc_divisor(const std::string& divisor) { - if (divisor == "kB") { - return 1.0; - } else if (divisor == "kiB") { - return 1.024; - } else if (divisor == "MB") { - return 1.000 * 1000.0; - } else if (divisor == "MiB") { - return 1.024 * 1024.0; - } else if (divisor == "GB") { - return 1.000 * 1000.0 * 1000.0; - } else if (divisor == "GiB") { - return 1.024 * 1024.0 * 1024.0; - } else if (divisor == "TB") { - return 1.000 * 1000.0 * 1000.0 * 1000.0; - } else if (divisor == "TiB") { - return 1.024 * 1024.0 * 1024.0 * 1024.0; - } else { // default to GiB if it is anything that we don't recongnise - return 1.024 * 1024.0 * 1024.0; - } -} diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..407fa599 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" @@ -22,9 +23,12 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) port_(config_["port"].isUInt() ? config["port"].asUInt() : 0), password_(config_["password"].empty() ? "" : config_["password"].asString()), timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000), + playing_interval_(config_["playing-interval"].isUInt() ? config_["playing-interval"].asUInt() + : 1'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_); } @@ -33,6 +37,10 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) spdlog::warn("{}: `timeout` configuration should be an unsigned int", module_name_); } + if (!config_["playing-interval"].isNull() && !config_["playing-interval"].isUInt()) { + spdlog::warn("{}: `playing-interval` configuration should be an unsigned int", module_name_); + } + if (!config["server"].isNull()) { if (!config_["server"].isString()) { spdlog::warn("{}:`server` configuration should be a string", module_name_); @@ -51,21 +59,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"; @@ -98,7 +91,7 @@ void waybar::modules::MPD::setLabel() { ? config_["format-disconnected"].asString() : "disconnected"; if (format.empty()) { - label_.set_markup(format); + setLabelMarkup(format); label_.show(); } else { label_.hide(); @@ -110,22 +103,22 @@ void waybar::modules::MPD::setLabel() { ? config_["tooltip-format-disconnected"].asString() : "MPD (disconnected)"; // Nothing to format - label_.set_tooltip_markup(tooltip_format); + setTooltipMarkup(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"); @@ -137,18 +130,15 @@ void waybar::modules::MPD::setLabel() { label_.get_style_context()->add_class("playing"); label_.get_style_context()->remove_class("paused"); } else if (paused()) { - format = config_["format-paused"].isString() ? config_["format-paused"].asString() - : config_["format"].asString(); + if (config_["format-paused"].isString()) { + format = config_["format-paused"].asString(); + } label_.get_style_context()->add_class("paused"); label_.get_style_context()->remove_class("playing"); } 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 +160,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), @@ -190,7 +175,7 @@ void waybar::modules::MPD::setLabel() { label_.hide(); } else { label_.show(); - label_.set_markup(text); + setLabelMarkup(text); } } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error: {}", e.what()); @@ -202,15 +187,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_markup(tooltip_text); + setTooltipMarkup(tooltip_text); } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error (tooltip): {}", e.what()); } @@ -257,6 +242,38 @@ std::string waybar::modules::MPD::getOptionIcon(const std::string& optionName, } } +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()) { diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 3d7c8561..16cd529b 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -19,39 +19,37 @@ auto format_as(enum mpd_idle val) { namespace waybar::modules::detail { -#define IDLE_RUN_NOIDLE_AND_CMD(...) \ - if (idle_connection_.connected()) { \ - idle_connection_.disconnect(); \ - auto conn = ctx_->connection().get(); \ - if (!mpd_run_noidle(conn)) { \ - if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \ - spdlog::error("mpd: Idle: failed to unregister for IDLE events"); \ - ctx_->checkErrors(conn); \ - } \ - } \ - __VA_ARGS__; \ +#define RUN_NOIDLE_AND_CMD(STATE, ...) \ + if (idle_connection_.connected()) { \ + idle_connection_.disconnect(); \ + auto conn = ctx_->connection().get(); \ + if (!mpd_run_noidle(conn)) { \ + if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \ + spdlog::error("mpd: STATE: failed to unregister for IDLE events"); \ + ctx_->checkErrors(conn); \ + } \ + } \ + __VA_ARGS__; \ } void Idle::play() { - IDLE_RUN_NOIDLE_AND_CMD(mpd_run_play(conn)); + RUN_NOIDLE_AND_CMD(Idle, mpd_run_play(conn)); ctx_->setState(std::make_unique(ctx_)); } void Idle::pause() { - IDLE_RUN_NOIDLE_AND_CMD(mpd_run_pause(conn, true)); + RUN_NOIDLE_AND_CMD(Idle, mpd_run_pause(conn, true)); ctx_->setState(std::make_unique(ctx_)); } void Idle::stop() { - IDLE_RUN_NOIDLE_AND_CMD(mpd_run_stop(conn)); + RUN_NOIDLE_AND_CMD(Idle, mpd_run_stop(conn)); ctx_->setState(std::make_unique(ctx_)); } -#undef IDLE_RUN_NOIDLE_AND_CMD - void Idle::update() noexcept { // This is intentionally blank. } @@ -97,19 +95,16 @@ bool Idle::on_io(Glib::IOCondition const&) { } ctx_->fetchState(); + ctx_->emit(); mpd_state state = ctx_->state(); if (state == MPD_STATE_STOP) { - ctx_->emit(); ctx_->setState(std::make_unique(ctx_)); } else if (state == MPD_STATE_PLAY) { - ctx_->emit(); ctx_->setState(std::make_unique(ctx_)); } else if (state == MPD_STATE_PAUSE) { - ctx_->emit(); ctx_->setState(std::make_unique(ctx_)); } else { - ctx_->emit(); // self transition ctx_->setState(std::make_unique(ctx_)); } @@ -118,15 +113,46 @@ bool Idle::on_io(Glib::IOCondition const&) { } void Playing::entry() noexcept { - sigc::slot timer_slot = sigc::mem_fun(*this, &Playing::on_timer); - timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1); - spdlog::debug("mpd: Playing: enabled 1 second periodic timer."); + timer(); + idle(); + spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval()); } void Playing::exit() noexcept { + if (idle_connection_.connected()) { + idle_connection_.disconnect(); + spdlog::debug("mpd: Playing: unwatching FD"); + } + if (timer_connection_.connected()) { timer_connection_.disconnect(); - spdlog::debug("mpd: Playing: disabled 1 second periodic timer."); + spdlog::debug("mpd: Playing: disabled {}ms periodic timer.", ctx_->playing_interval()); + } +} + +void Playing::timer() noexcept { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + } + + sigc::slot timer_slot = sigc::mem_fun(*this, &Playing::on_timer); + timer_connection_ = Glib::signal_timeout().connect(timer_slot, ctx_->playing_interval()); +} + +void Playing::idle() noexcept { + auto conn = ctx_->connection().get(); + assert(conn != nullptr); + + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Playing: failed to register for IDLE events"); + } else if (!idle_connection_.connected()) { + spdlog::trace("mpd: Playing: watching FD"); + sigc::slot idle_slot = sigc::mem_fun(*this, &Playing::on_io); + idle_connection_ = + Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), + Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); } } @@ -141,9 +167,12 @@ bool Playing::on_timer() { return false; } + RUN_NOIDLE_AND_CMD(Playing); + ctx_->fetchState(); if (!ctx_->is_playing()) { + ctx_->emit(); if (ctx_->is_paused()) { ctx_->setState(std::make_unique(ctx_)); } else { @@ -152,8 +181,51 @@ bool Playing::on_timer() { return false; } - ctx_->queryMPD(); ctx_->emit(); + + idle(); + } catch (std::exception const& e) { + spdlog::warn("mpd: Playing: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); + return false; + } + + return true; +} + +bool Playing::on_io(Glib::IOCondition const&) { + auto conn = ctx_->connection().get(); + + // callback should do this: + enum mpd_idle events = mpd_recv_idle(conn, /* ignore_timeout?= */ false); + spdlog::debug("mpd: Playing: recv_idle events -> {}", events); + + mpd_response_finish(conn); + try { + ctx_->checkErrors(conn); + + ctx_->fetchState(); + + if (!ctx_->is_playing()) { + ctx_->emit(); + if (ctx_->is_paused()) { + ctx_->setState(std::make_unique(ctx_)); + } else { + ctx_->setState(std::make_unique(ctx_)); + } + return false; + } + + ctx_->emit(); + + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Playing: failed to register for IDLE events"); + } + + // Defer the next timer + timer(); } catch (std::exception const& e) { spdlog::warn("mpd: Playing: error: {}", e.what()); ctx_->setState(std::make_unique(ctx_)); @@ -164,21 +236,23 @@ bool Playing::on_timer() { } void Playing::stop() { - if (timer_connection_.connected()) { - timer_connection_.disconnect(); + RUN_NOIDLE_AND_CMD( + Playing, if (timer_connection_.connected()) { + timer_connection_.disconnect(); - mpd_run_stop(ctx_->connection().get()); - } + mpd_run_stop(ctx_->connection().get()); + }); ctx_->setState(std::make_unique(ctx_)); } void Playing::pause() { - if (timer_connection_.connected()) { - timer_connection_.disconnect(); + RUN_NOIDLE_AND_CMD( + Playing, if (timer_connection_.connected()) { + timer_connection_.disconnect(); - mpd_run_pause(ctx_->connection().get(), true); - } + mpd_run_pause(ctx_->connection().get(), true); + }); ctx_->setState(std::make_unique(ctx_)); } @@ -212,7 +286,6 @@ bool Paused::on_timer() { } ctx_->fetchState(); - ctx_->emit(); if (ctx_->is_paused()) { @@ -283,7 +356,6 @@ bool Stopped::on_timer() { } ctx_->fetchState(); - ctx_->emit(); if (ctx_->is_stopped()) { @@ -387,4 +459,5 @@ bool Disconnected::on_timer() { void Disconnected::update() noexcept { ctx_->do_update(); } +#undef RUN_NOIDLE_AND_CMD } // namespace waybar::modules::detail diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index e0a27c92..ce084723 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 @@ -30,6 +31,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) dynamic_separator_(" - "), truncate_hours_(true), tooltip_len_limits_(false), + prefer_album_artist_(false), // this character is used in Gnome so it's fine to use it here ellipsis_("\u2026"), player_("playerctld"), @@ -67,6 +69,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) if (config_["enable-tooltip-len-limits"].isBool()) { tooltip_len_limits_ = config["enable-tooltip-len-limits"].asBool(); } + if (config_["prefer-album-artist"].isBool()) { + prefer_album_artist_ = config["prefer-album-artist"].asBool(); + } } if (config["artist-len"].isUInt()) { @@ -202,69 +207,28 @@ 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 (prefer_album_artist_) { + auto album_artist = info.album_artist.value_or(std::string()); + if (!album_artist.empty()) { + artist = album_artist; + } + } + 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; } @@ -292,9 +256,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(); @@ -313,7 +277,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; @@ -567,6 +531,7 @@ auto Mpris::getPlayerInfo() -> std::optional { .status_string = player_status, .artist = std::nullopt, .album = std::nullopt, + .album_artist = std::nullopt, .title = std::nullopt, .length = std::nullopt, }; @@ -578,6 +543,13 @@ auto Mpris::getPlayerInfo() -> std::optional { } if (error) goto errorexit; + if (auto* album_artist_ = + playerctl_player_print_metadata_prop(player, "xesam:albumArtist", &error)) { + spdlog::debug("mpris[{}]: albumArtist = {}", info.name, album_artist_); + info.album_artist = album_artist_; + g_free(album_artist_); + } + if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); info.album = album_; @@ -775,9 +747,12 @@ 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)), diff --git a/src/modules/network.cpp b/src/modules/network.cpp index f43893c5..0981d571 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -315,19 +315,26 @@ auto waybar::modules::Network::update() -> void { elapsed_seconds = std::chrono::duration(interval_).count(); } + auto threshold_state = getState(signal_strength_); + if (!alt_) { auto state = getNetworkState(); if (!state_.empty() && label_.get_style_context()->has_class(state_)) { label_.get_style_context()->remove_class(state_); } - if (config_["format-" + state].isString()) { + if (!threshold_state.empty() && config_["format-" + state + "-" + threshold_state].isString()) { + default_format_ = config_["format-" + state + "-" + threshold_state].asString(); + } else if (config_["format-" + state].isString()) { default_format_ = config_["format-" + state].asString(); } else if (config_["format"].isString()) { default_format_ = config_["format"].asString(); } else { default_format_ = DEFAULT_FORMAT; } - if (config_["tooltip-format-" + state].isString()) { + if (!threshold_state.empty() && + config_["tooltip-format-" + state + "-" + threshold_state].isString()) { + tooltip_format = config_["tooltip-format-" + state + "-" + threshold_state].asString(); + } else if (config_["tooltip-format-" + state].isString()) { tooltip_format = config_["tooltip-format-" + state].asString(); } if (!label_.get_style_context()->has_class(state)) { @@ -336,7 +343,6 @@ auto waybar::modules::Network::update() -> void { format_ = default_format_; state_ = state; } - getState(signal_strength_); std::string final_ipaddr_; if (addr_pref_ == ip_addr_pref::IPV4) { @@ -368,6 +374,10 @@ auto waybar::modules::Network::update() -> void { 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("bandwidthDownBytesCompact", + pow_format(bandwidth_down / elapsed_seconds, "B", false, 2)), + fmt::arg("bandwidthUpBytesCompact", + pow_format(bandwidth_up / elapsed_seconds, "B", false, 2)), fmt::arg("bandwidthTotalBytes", pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (text.compare(label_.get_label()) != 0) { @@ -401,6 +411,10 @@ auto waybar::modules::Network::update() -> void { 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("bandwidthDownBytesCompact", + pow_format(bandwidth_down / elapsed_seconds, "B", false, 2)), + fmt::arg("bandwidthUpBytesCompact", + pow_format(bandwidth_up / elapsed_seconds, "B", false, 2)), fmt::arg("bandwidthTotalBytes", pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (label_.get_tooltip_text() != tooltip_text) { diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index de3f8a9a..0900e092 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -196,6 +196,18 @@ void IPC::parseIPC(const std::string& line) { for (auto& win : windows_) { win["is_focused"] = focused && win["id"].asUInt64() == id; } + } else if (const auto &payload = ev["WindowLayoutsChanged"]) { + const auto &values = payload["changes"]; + for (const auto &changed : values) { + const auto id = changed[0].asUInt64(); + const auto &change = changed[1]; + for (auto &win : windows_) { + if (win["id"].asUInt64() == id) { + win["layout"] = change; + break; + } + } + } } } diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp index 14ca555f..d739ce57 100644 --- a/src/modules/niri/language.cpp +++ b/src/modules/niri/language.cpp @@ -85,7 +85,7 @@ void Language::doUpdate() { spdlog::debug("niri language formatted layout name {}", layoutName); - if (!format_.empty()) { + if (!layoutName.empty()) { label_.show(); label_.set_markup(layoutName); } else { diff --git a/src/modules/niri/window.cpp b/src/modules/niri/window.cpp index 61b3a7dd..38ce2f49 100644 --- a/src/modules/niri/window.cpp +++ b/src/modules/niri/window.cpp @@ -17,6 +17,7 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) gIPC->registerForIPC("WindowOpenedOrChanged", this); gIPC->registerForIPC("WindowClosed", this); gIPC->registerForIPC("WindowFocusChanged", this); + gIPC->registerForIPC("WindowLayoutsChanged", this); dp.emit(); } @@ -54,15 +55,25 @@ void Window::doUpdate() { if (it != windows.cend()) { const auto& window = *it; + auto max_col = -1; + for (const auto &win : windows) { + if (win["workspace_id"].asUInt64() != window["workspace_id"].asUInt64()) { + continue; + } + const auto col = win["layout"]["pos_in_scrolling_layout"][0].asInt64(); + if (col > max_col) max_col = col; + } const auto title = window["title"].asString(); const auto appId = window["app_id"].asString(); + const auto col = window["layout"]["pos_in_scrolling_layout"][0].asInt64(); const auto sanitizedTitle = waybar::util::sanitize_string(title); const auto sanitizedAppId = waybar::util::sanitize_string(appId); label_.show(); label_.set_markup(waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", sanitizedTitle), - fmt::arg("app_id", sanitizedAppId)), + fmt::arg("app_id", sanitizedAppId), fmt::arg("col", col), + fmt::arg("max_col", max_col)), config_["rewrite"])); updateAppIconName(appId, ""); @@ -82,7 +93,12 @@ void Window::doUpdate() { oldAppId_ = appId; } } else { - label_.hide(); + label_.show(); + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", ""), + fmt::arg("app_id", "")), + config_["rewrite"])); + updateAppIconName("", ""); setClass("solo", false); if (!oldAppId_.empty()) setClass(oldAppId_, false); diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 97d15215..f6d0553d 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -4,10 +4,34 @@ #include #include +#include +#include + namespace waybar::modules::niri { Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config) : AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) { + const auto config_sort_by_number = config_["sort-by-number"]; + if (config_sort_by_number.isBool()) { + spdlog::warn("[niri/workspaces]: Prefer sort-by-id instead of sort-by-number"); + sort_by_id_ = config_sort_by_number.asBool(); + } + + const auto config_sort_by_id = config_["sort-by-id"]; + if (config_sort_by_id.isBool()) { + sort_by_id_ = config_sort_by_id.asBool(); + } + + const auto config_sort_by_name = config_["sort-by-name"]; + if (config_sort_by_name.isBool()) { + sort_by_name_ = config_sort_by_name.asBool(); + } + + const auto config_sort_by_coordinates = config_["sort-by-coordinates"]; + if (config_sort_by_coordinates.isBool()) { + sort_by_coordinates_ = config_sort_by_coordinates.asBool(); + } + box_.set_name("workspaces"); if (!id.empty()) { box_.get_style_context()->add_class(id); @@ -22,6 +46,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); gIPC->registerForIPC("WorkspaceUrgencyChanged", this); + if (config["enable-bar-scroll"].asBool()) { + auto& window = const_cast(bar_).window; + window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); + } + dp.emit(); } @@ -41,6 +71,8 @@ void Workspaces::doUpdate() { return ws["output"].asString() == bar_.output->name; }); + sortWorkspaces(my_workspaces); + // Remove buttons for removed workspaces. for (auto it = buttons_.begin(); it != buttons_.end();) { auto ws = std::find_if(my_workspaces.begin(), my_workspaces.end(), @@ -97,13 +129,15 @@ void Workspaces::doUpdate() { if (config_["format"].isString()) { auto format = config_["format"].asString(); - name = fmt::format(fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), - fmt::arg("value", name), fmt::arg("name", ws["name"].asString()), - fmt::arg("index", ws["idx"].asUInt()), - fmt::arg("output", ws["output"].asString())); + name = fmt::format( + fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name), + fmt::arg("name", ws["name"].asString()), fmt::arg("index", ws["idx"].asUInt()), + fmt::arg("output", ws["output"].asString()), fmt::arg("total", my_workspaces.size())); } 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); } @@ -116,9 +150,9 @@ void Workspaces::doUpdate() { button.hide(); } else if (config_["hide-empty"].asBool()) { if (ws["active_window_id"].isNull() && !ws["is_focused"].asBool()) - button.hide(); + button.hide(); else - button.show(); + button.show(); } else { button.show(); } @@ -128,8 +162,7 @@ void Workspaces::doUpdate() { for (auto it = my_workspaces.cbegin(); it != my_workspaces.cend(); ++it) { const auto& ws = *it; - auto pos = ws["idx"].asUInt() - 1; - if (alloutputs) pos = it - my_workspaces.cbegin(); + const auto pos = static_cast(std::distance(my_workspaces.cbegin(), it)); auto& button = buttons_[ws["id"].asUInt64()]; box_.reorder_child(button, pos); @@ -198,4 +231,98 @@ std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) return value; } +bool Workspaces::handleScroll(GdkEventScroll* e) { + if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) { + /** + * Ignore emulated scroll events on window + */ + return false; + } + + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + + std::string action_name; + + if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) { + action_name = "FocusWorkspaceDown"; + } else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) { + action_name = "FocusWorkspaceUp"; + } else { + return true; + } + + action[action_name] = Json::Value(Json::objectValue); + + IPC::send(request); + + } catch (const std::exception& e) { + spdlog::error("Workspaces: {}", e.what()); + return false; + } + + return true; +} + +void Workspaces::sortWorkspaces(std::vector& workspaces) const { + auto get_name = [](const Json::Value& ws) -> std::string { + if (ws["name"]) return ws["name"].asString(); + return std::to_string(ws["idx"].asUInt()); + }; + + auto is_numeric = [](const std::string& value) { + return !value.empty() && + std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c); }); + }; + + const bool names_are_numeric = + std::all_of(workspaces.begin(), workspaces.end(), + [&](const auto& ws) { return is_numeric(get_name(ws)); }); + + auto compare_numeric_strings = [](const std::string& a, const std::string& b) { + if (a.size() != b.size()) return a.size() < b.size(); + return a < b; + }; + + std::sort(workspaces.begin(), workspaces.end(), [&](const auto& a, const auto& b) { + if (sort_by_id_) { + return a["id"].asUInt64() < b["id"].asUInt64(); + } + + if (sort_by_name_) { + const auto a_name = get_name(a); + const auto b_name = get_name(b); + if (a_name == b_name) return a["id"].asUInt64() < b["id"].asUInt64(); + if (names_are_numeric) return compare_numeric_strings(a_name, b_name); + return a_name < b_name; + } + + if (sort_by_coordinates_) { + const auto& a_output = a["output"].asString(); + const auto& b_output = b["output"].asString(); + if (a_output == b_output) { + const auto a_idx = a["idx"].asUInt(); + const auto b_idx = b["idx"].asUInt(); + if (a_idx == b_idx) return a["id"].asUInt64() < b["id"].asUInt64(); + return a_idx < b_idx; + } + return a_output < b_output; + } + + // Default to sorting by workspace index on each output. + const auto& a_output = a["output"].asString(); + const auto& b_output = b["output"].asString(); + const auto a_idx = a["idx"].asUInt(); + const auto b_idx = b["idx"].asUInt(); + if (a_output == b_output) return a_idx < b_idx; + return a_output < b_output; + }); +} + } // namespace waybar::modules::niri diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index 566787f3..d7eb5bcd 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -12,7 +12,8 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu if (config_["tooltip-format"].isString()) { tooltipFormat_ = config_["tooltip-format"].asString(); } else { - tooltipFormat_ = "Power profile: {profile}\nDriver: {driver}"; + tooltipFormat_ = + "Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}"; } // Fasten your seatbelt, we're up for quite a ride. The rest of the // init is performed asynchronously. There's 2 callbacks involved. @@ -95,15 +96,51 @@ void PowerProfilesDaemon::populateInitState() { powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles"); for (auto& variantDict : profilesVariant.get()) { Glib::ustring name; + // Legacy single `Driver` property, still exposed by older + // power-profiles-daemon versions. Glib::ustring driver; + Glib::ustring cpuDriver; + Glib::ustring platformDriver; if (auto p = variantDict.find("Profile"); p != variantDict.end()) { name = p->second.get(); } if (auto d = variantDict.find("Driver"); d != variantDict.end()) { driver = d->second.get(); } + if (auto cd = variantDict.find("CpuDriver"); cd != variantDict.end()) { + cpuDriver = cd->second.get(); + } + if (auto pd = variantDict.find("PlatformDriver"); pd != variantDict.end()) { + platformDriver = pd->second.get(); + } + + // Recent power-profiles-daemon versions split the single `Driver` + // property into `CpuDriver` and `PlatformDriver`. When talking to an + // older daemon that only exposes `Driver`, fall back to it so the new + // {cpu_driver}/{platform_driver} placeholders still resolve. + if (cpuDriver.empty()) { + cpuDriver = driver; + } + if (platformDriver.empty()) { + platformDriver = driver; + } + // Conversely, keep the legacy {driver} placeholder working against a + // recent daemon that no longer exposes `Driver` by deriving it from + // the CPU driver. + if (driver.empty()) { + driver = cpuDriver; + } + + if (driver.empty()) { + driver = "Unavailable"; + cpuDriver = "Unavailable"; + platformDriver = "Unavailable"; + spdlog::warn("Cannot find power profiles daemon driver."); + } + if (!name.empty()) { - availableProfiles_.emplace_back(std::move(name), std::move(driver)); + availableProfiles_.emplace_back(std::move(name), std::move(driver), std::move(cpuDriver), + std::move(platformDriver)); } else { spdlog::error( "Power profiles daemon: power-profiles-daemon sent us an empty power profile name. " @@ -153,7 +190,10 @@ auto PowerProfilesDaemon::update() -> void { // Set label fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("profile", profile.name)); + // Legacy placeholder, kept for backward compatibility with existing configs. store.push_back(fmt::arg("driver", profile.driver)); + store.push_back(fmt::arg("cpu_driver", profile.cpuDriver)); + store.push_back(fmt::arg("platform_driver", profile.platformDriver)); store.push_back(fmt::arg("icon", getIcon(0, profile.name))); label_.set_markup(fmt::vformat(format_, store)); if (tooltipEnabled()) { 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 4d63ff3c..931ecb34 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -7,6 +7,11 @@ waybar::modules::Pulseaudio::Pulseaudio(const std::string& id, const Json::Value backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); backend->setIgnoredSinks(config_["ignored-sinks"]); + backend->setSinkMapping(config_["sink-mapping"]); + + if (config_["target"].isString() && config_["target"].asString() == "source") { + target = util::PulseaudioTarget::Source; + } } bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll* e) { @@ -33,7 +38,7 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll* e) { ? util::ChangeType::Increase : util::ChangeType::Decrease; - backend->changeVolume(change_type, step, max_volume); + backend->changeVolume(change_type, step, max_volume, target); return true; } diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp index bf85584e..3fb930db 100644 --- a/src/modules/pulseaudio_slider.cpp +++ b/src/modules/pulseaudio_slider.cpp @@ -6,77 +6,56 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con : ASlider(config, "pulseaudio-slider", id) { backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); backend->setIgnoredSinks(config_["ignored-sinks"]); + backend->setSinkMapping(config_["sink-mapping"]); if (config_["target"].isString()) { std::string target = config_["target"].asString(); if (target == "sink") { - this->target = PulseaudioSliderTarget::Sink; + this->target = util::PulseaudioTarget::Sink; } else if (target == "source") { - this->target = PulseaudioSliderTarget::Source; + this->target = util::PulseaudioTarget::Source; } } + + if (config_["zero-on-mute"].isBool()) { + zero_on_mute = config_["zero-on-mute"].asBool(); + } + + if (config_["unmute-on-volume-change"].isBool()) { + unmute_on_volume_change = config_["unmute-on-volume-change"].asBool(); + } } void PulseaudioSlider::update() { - switch (target) { - case PulseaudioSliderTarget::Sink: - if (backend->getSinkMuted()) { - scale_.set_value(min_); - } else { - scale_.set_value(backend->getSinkVolume()); - } - break; + uint16_t display_value = backend->getVolume(target); + bool is_muted = backend->getMuted(target); - case PulseaudioSliderTarget::Source: - if (backend->getSourceMuted()) { - scale_.set_value(min_); - } else { - scale_.set_value(backend->getSourceVolume()); - } - break; + if (is_muted) { + if (zero_on_mute) { + display_value = min_; + } + if (!previously_muted) { + scale_.get_style_context()->add_class("muted"); + } + } else if (previously_muted) { + scale_.get_style_context()->remove_class("muted"); } + + scale_.set_value(display_value); + + previously_muted = is_muted; } void PulseaudioSlider::onValueChanged() { - bool is_mute = false; + uint16_t slider_value = scale_.get_value(); - switch (target) { - case PulseaudioSliderTarget::Sink: - if (backend->getSinkMuted()) { - is_mute = true; - } - break; - - case PulseaudioSliderTarget::Source: - if (backend->getSourceMuted()) { - is_mute = true; - } - break; - } - - uint16_t volume = scale_.get_value(); - - if (is_mute) { - // Avoid setting sink/source to volume 0 if the user muted if via another mean. - if (volume == 0) { - return; - } - - // If the sink/source is mute, but the user clicked the slider, unmute it! - else { - switch (target) { - case PulseaudioSliderTarget::Sink: - backend->toggleSinkMute(false); - break; - - case PulseaudioSliderTarget::Source: - backend->toggleSourceMute(false); - break; - } + // Avoid setting sink/source to volume 0 if the user muted it via other means. + if (!backend->getMuted(target) || slider_value != 0) { + if (unmute_on_volume_change) { + backend->unmute(target); } + backend->changeVolume(slider_value, min_, max_, target); } - - backend->changeVolume(volume, min_, max_); } -} // namespace waybar::modules \ No newline at end of file +} // namespace waybar::modules diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index f06565f6..f08fdac5 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -33,6 +33,33 @@ static const zriver_output_status_v1_listener output_status_listener_impl{ .urgent_tags = listen_urgent_tags, }; +static void listen_focused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1, + struct wl_output* output) { + static_cast(data)->handle_focused_output(output); +} + +static void listen_unfocused_output(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1, + struct wl_output* output) { + static_cast(data)->handle_unfocused_output(output); +} + +static void listen_focused_view(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1, + const char* title) { + // This module doesn't care +} + +static void listen_mode(void* data, struct zriver_seat_status_v1* zriver_seat_status_v1, + const char* mode) { + // This module doesn't care +} + +static const zriver_seat_status_v1_listener seat_status_listener_impl{ + .focused_output = listen_focused_output, + .unfocused_output = listen_unfocused_output, + .focused_view = listen_focused_view, + .mode = listen_mode, +}; + static void listen_command_success(void* data, struct zriver_command_callback_v1* zriver_command_callback_v1, const char* output) { @@ -87,8 +114,10 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con control_{nullptr}, seat_{nullptr}, bar_(bar), + output_{nullptr}, box_{bar.orientation, 0}, - output_status_{nullptr} { + output_status_{nullptr}, + seat_status_{nullptr} { struct wl_display* display = Client::inst()->wl_display; struct wl_registry* registry = wl_display_get_registry(display); wl_registry_add_listener(registry, ®istry_listener_impl, this); @@ -109,6 +138,11 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con return; } + // Store the output this module belongs to; the river_output_status and + // river_seat_status objects (and their listeners) are created lazily in + // handle_show() to avoid leaking objects without listeners here. + output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + box_.set_name("tags"); if (!id.empty()) { box_.get_style_context()->add_class(id); @@ -149,6 +183,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con button.signal_button_press_event().connect( sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), (1 << tag))); } + button.get_style_context()->add_class("tag-" + std::to_string(tag + 1)); button.show(); } @@ -160,6 +195,10 @@ Tags::~Tags() { zriver_output_status_v1_destroy(output_status_); } + if (seat_status_) { + zriver_seat_status_v1_destroy(seat_status_); + } + if (control_) { zriver_control_v1_destroy(control_); } @@ -171,10 +210,12 @@ 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); + 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); + seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); + zriver_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this); + zriver_status_manager_v1_destroy(status_manager_); status_manager_ = nullptr; } @@ -266,4 +307,20 @@ void Tags::handle_urgent_tags(uint32_t tags) { } } +void Tags::handle_focused_output(struct wl_output* output) { + if (output_ == output) { + for (size_t i = 0; i < buttons_.size(); ++i) { + buttons_[i].get_style_context()->add_class("output"); + } + } +} + +void Tags::handle_unfocused_output(struct wl_output* output) { + if (output_ == output) { + for (size_t i = 0; i < buttons_.size(); ++i) { + buttons_[i].get_style_context()->remove_class("output"); + } + } +} + } /* namespace waybar::modules::river */ diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 567fbf9f..f77d26a2 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -6,7 +6,11 @@ namespace waybar::modules::SNI { +static const unsigned RETRY_DELAY_MS = 200; +static const unsigned MAX_RETRIES = 10; + Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, + const std::vector& ignore_list, const std::function&)>& on_add, const std::function&)>& on_remove, const std::function& on_update) @@ -17,11 +21,13 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, sigc::mem_fun(*this, &Host::busAcquired))), config_(config), bar_(bar), + ignore_list_(ignore_list), on_add_(on_add), on_remove_(on_remove), on_update_(on_update) {} Host::~Host() { + retry_connection_.disconnect(); if (bus_name_id_ > 0) { Gio::DBus::unown_name(bus_name_id_); bus_name_id_ = 0; @@ -35,6 +41,42 @@ Host::~Host() { g_clear_object(&watcher_); } +void Host::checkIgnoreList(const std::vector& ignore_list, + const std::function&)>& on_remove) { + spdlog::debug("Host::checkIgnoreList - checking {} items against {} patterns", items_.size(), + ignore_list.size()); + + for (auto it = items_.begin(); it != items_.end();) { + auto& item = *it; + spdlog::debug(" Checking item: bus_name='{}', category='{}', icon_name='{}', title='{}'", + item->bus_name, item->category, item->icon_name, item->title); + + bool should_remove = false; + + for (const auto& ignored : ignore_list) { + if (item->bus_name.find(ignored) != std::string::npos || + item->category.find(ignored) != std::string::npos || + item->icon_name.find(ignored) != std::string::npos || + item->id.find(ignored) != std::string::npos || + item->title.find(ignored) != std::string::npos) { + spdlog::info( + "Host: Ignoring item bus_name='{}', category='{}', icon_name='{}', title='{}' - " + "matched pattern '{}'", + item->bus_name, item->category, item->icon_name, item->title, ignored); + on_remove(item); + should_remove = true; + break; + } + } + + if (should_remove) { + it = items_.erase(it); + } else { + ++it; + } + } +} + void Host::busAcquired(const Glib::RefPtr& conn, Glib::ustring name) { watcher_id_ = Gio::DBus::watch_name(conn, "org.kde.StatusNotifierWatcher", sigc::mem_fun(*this, &Host::nameAppeared), @@ -53,6 +95,8 @@ void Host::nameAppeared(const Glib::RefPtr& conn, const G } void Host::nameVanished(const Glib::RefPtr& conn, const Glib::ustring name) { + retry_connection_.disconnect(); + retry_count_ = 0; g_cancellable_cancel(cancellable_); g_clear_object(&cancellable_); g_clear_object(&watcher_); @@ -72,11 +116,31 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { return; } auto host = static_cast(data); - host->watcher_ = watcher; if (error != nullptr) { spdlog::error("Host: {}", error->message); + g_clear_object(&host->cancellable_); + if (host->retry_count_ >= MAX_RETRIES) { + spdlog::warn("Host: giving up on watcher proxy creation after {} retries", + host->retry_count_); + return; + } + host->retry_count_ += 1; + // Store the timeout connection so it is disconnected in ~Host, avoiding a + // use-after-free if the Host is destroyed before the retry fires. + host->retry_connection_ = Glib::signal_timeout().connect( + [host]() { + if (host->watcher_ != nullptr) { + return false; + } + auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); + host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + return false; + }, + RETRY_DELAY_MS); return; } + host->retry_count_ = 0; + host->watcher_ = watcher; sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_, &Host::registerHost, data); } @@ -102,7 +166,9 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { g_signal_connect(host->watcher_, "item-unregistered", G_CALLBACK(&Host::itemUnregistered), data); auto items = sn_watcher_dup_registered_items(host->watcher_); if (items != nullptr) { + spdlog::info("Host: Found {} pre-registered SNI items", g_strv_length(items)); for (uint32_t i = 0; items[i] != nullptr; i += 1) { + spdlog::info("Host: Processing pre-registered item: {}", items[i]); host->addRegisteredItem(items[i]); } } @@ -111,7 +177,10 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { void Host::itemRegistered(SnWatcher* watcher, const gchar* service, gpointer data) { auto host = static_cast(data); + spdlog::info("Host::itemRegistered called with service: {}", service); host->addRegisteredItem(service); + // host->checkIgnoreList(host->ignore_list_, std::bind(&Host::itemUnregistered, host, + // std::placeholders::_1, std::placeholders::_2, data)); } void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer data) { @@ -172,17 +241,25 @@ std::tuple Host::getBusNameAndObjectPath(const std::st } void Host::addRegisteredItem(const std::string& service) { + // Check service string directly before parsing + for (const auto& ignored : ignore_list_) { + if (service.find(ignored) != std::string::npos) { + spdlog::info("Host: Ignoring service '{}' - matched pattern '{}'", service, ignored); + return; + } + } std::string bus_name, object_path; std::tie(bus_name, object_path) = getBusNameAndObjectPath(service); + spdlog::debug("SNI item registered: bus_name={}, object_path={}, full_service={}", bus_name, + object_path, 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()) { + spdlog::debug("Adding SNI item: {}", bus_name); items_.emplace_back(std::make_unique( - bus_name, object_path, config_, bar_, - [this](Item& item) { itemReady(item); }, - [this](Item& item) { itemInvalidated(item); }, - on_update_)); + 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 2f368083..8e2e2570 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -190,9 +190,17 @@ 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") { @@ -270,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; } } } diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 114aba78..ca3ee277 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -8,11 +8,29 @@ namespace waybar::modules::SNI { +std::vector Tray::parseIgnoreList(const Json::Value& config) { + std::vector ignore_list; + if (config["ignore-list"].isArray()) { + spdlog::info("Tray: Found ignore-list with {} items", config["ignore-list"].size()); + for (const auto& item : config["ignore-list"]) { + if (item.isString()) { + ignore_list.push_back(item.asString()); + spdlog::info("Tray: Adding to ignore list: {}", item.asString()); + } + } + } else { + spdlog::info("Tray: No ignore-list configured"); + } + return ignore_list; +} + Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) : AModule(config, "tray", id), box_(bar.orientation, 0), watcher_(SNI::Watcher::getInstance()), - host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1), + ignore_list_(parseIgnoreList(config)), + host_(nb_hosts_, config, bar, ignore_list_, + std::bind(&Tray::onAdd, this, std::placeholders::_1), std::bind(&Tray::onRemove, this, std::placeholders::_1), std::bind(&Tray::queueUpdate, this)) { box_.set_name("tray"); @@ -24,9 +42,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"]); @@ -34,14 +49,28 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) dp.emit(); } +void Tray::checkIgnoreList(std::unique_ptr* item_ptr) { + // Delegate to Host's checkIgnoreList method + host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); +} + void Tray::queueUpdate() { dp.emit(); } void Tray::onAdd(std::unique_ptr& item) { + spdlog::info("Tray::onAdd - item bus_name='{}', category='{}', icon_name='{}', title='{}'", + item->bus_name, item->category, item->icon_name, item->title); + if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) { box_.pack_end(item->event_box); } else { box_.pack_start(item->event_box); } + + spdlog::debug("Tray::onAdd deferred check - checking ignore list"); + host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); + + item->event_box.signal_show().connect([this] { dp.emit(); }); + item->event_box.signal_hide().connect([this] { dp.emit(); }); dp.emit(); } @@ -51,17 +80,15 @@ 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"); - })); + // Check if any items should be ignored now that properties have loaded + if (!ignore_list_.empty()) { + spdlog::debug("Tray::update() - checking ignore list"); + host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); } - // Call parent update + std::vector children = box_.get_children(); + 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/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index 3ebccccd..dcbe7fa3 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -2,13 +2,46 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include + +#include "modules/sway/ipc/ipc.hpp" namespace waybar::modules::sway { +namespace { + +void sendAll(int fd, const char* data, size_t size, const char* what) { + size_t total = 0; + while (total < size) { + const auto res = ::send(fd, data + total, size - total, 0); + if (res < 0) { + if (errno == EINTR || errno == EAGAIN) { + continue; + } + throw std::runtime_error(what); + } + if (res == 0) { + throw std::runtime_error(what); + } + total += static_cast(res); + } +} + +} // namespace Ipc::Ipc() { - const std::string& socketPath = getSocketPath(); + const std::string socketPath = getSocketPath(); fd_ = util::ScopedFd(open(socketPath)); fd_event_ = util::ScopedFd(open(socketPath)); } @@ -29,49 +62,49 @@ Ipc::~Ipc() { } } -void Ipc::setWorker(std::function&& func) { thread_ = func; } +void Ipc::setWorker(std::function&& func) { thread_ = std::move(func); } -const std::string Ipc::getSocketPath() const { +std::string Ipc::getSocketPath() { const char* env = getenv("SWAYSOCK"); - if (env != nullptr) { - return std::string(env); + if (env != nullptr && env[0] != '\0') { + return {env}; } + + FILE* in = popen("sway --get-socketpath 2>/dev/null", "r"); + if (in == nullptr) { + throw std::runtime_error("Failed to get socket path"); + } + std::string str; - { - std::string str_buf; - FILE* in; - char buf[512] = {0}; - if ((in = popen("sway --get-socketpath 2>/dev/null", "r")) == nullptr) { - throw std::runtime_error("Failed to get socket path"); - } - while (fgets(buf, sizeof(buf), in) != nullptr) { - str_buf.append(buf, sizeof(buf)); - } - pclose(in); - str = str_buf; - if (str.empty()) { - throw std::runtime_error("Socket path is empty"); - } + char buf[512] = {0}; + while (fgets(buf, sizeof(buf), in) != nullptr) { + str.append(buf); } - if (str.back() == '\n') { + + if (pclose(in) == -1) { + throw std::runtime_error("Failed to get socket path"); + } + + if (str.ends_with('\n')) { str.pop_back(); } + if (str.empty()) { + throw std::runtime_error("Socket path is empty"); + } + return str; } -int Ipc::open(const std::string& socketPath) const { +int Ipc::open(const std::string& socketPath) { util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0)); if (fd == -1) { throw std::runtime_error("Unable to open Unix socket"); } (void)fcntl(fd, F_SETFD, FD_CLOEXEC); - struct sockaddr_un addr; - memset(&addr, 0, sizeof(struct sockaddr_un)); - addr.sun_family = AF_UNIX; + struct sockaddr_un addr{.sun_family = AF_UNIX}; 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(fd, reinterpret_cast(&addr), l) == -1) { + if (::connect(fd, reinterpret_cast(&addr), sizeof addr) == -1) { throw std::runtime_error("Unable to connect to Sway"); } return fd.release(); @@ -80,55 +113,70 @@ int Ipc::open(const std::string& socketPath) const { struct Ipc::ipc_response Ipc::recv(int fd) { std::string header; header.resize(ipc_header_size_); - auto data32 = reinterpret_cast(header.data() + ipc_magic_.size()); - size_t total = 0; + size_t total = 0; while (total < ipc_header_size_) { - auto res = ::recv(fd, header.data() + total, ipc_header_size_ - total, 0); + const ssize_t res = ::recv(fd, header.data() + total, ipc_header_size_ - total, 0); if (fd_event_ == -1 || fd_ == -1) { // IPC is closed so just return an empty response - return {0, 0, ""}; + return {.size = 0, .type = 0, .payload = ""}; } - if (res <= 0) { + if (res < 0) { + if (errno == EINTR || errno == EAGAIN) { + continue; + } throw std::runtime_error("Unable to receive IPC header"); } - total += res; + if (res == 0) { + throw std::runtime_error("Unable to receive IPC header"); + } + total += static_cast(res); } - auto magic = std::string(header.data(), header.data() + ipc_magic_.size()); - if (magic != ipc_magic_) { + if (std::string_view(header.data(), ipc_magic_.size()) != ipc_magic_) { throw std::runtime_error("Invalid IPC magic"); } - total = 0; + uint32_t payload_size = 0; + uint32_t payload_type = 0; + memcpy(&payload_size, header.data() + ipc_magic_.size(), sizeof payload_size); + memcpy(&payload_type, header.data() + ipc_magic_.size() + sizeof payload_size, + sizeof payload_type); + std::string payload; - payload.resize(data32[0]); - while (total < data32[0]) { - auto res = ::recv(fd, payload.data() + total, data32[0] - total, 0); + payload.resize(payload_size); + + total = 0; + while (total < payload_size) { + const ssize_t res = ::recv(fd, payload.data() + total, payload_size - total, 0); if (res < 0) { if (errno == EINTR || errno == EAGAIN) { continue; } throw std::runtime_error("Unable to receive IPC payload"); } - total += res; + if (res == 0) { + throw std::runtime_error("Unable to receive IPC payload"); + } + total += static_cast(res); } - return {data32[0], data32[1], &payload.front()}; + + return {.size = payload_size, .type = payload_type, .payload = std::move(payload)}; } struct Ipc::ipc_response Ipc::send(int fd, uint32_t type, const std::string& payload) { std::string header; header.resize(ipc_header_size_); - auto data32 = reinterpret_cast(header.data() + ipc_magic_.size()); - memcpy(header.data(), ipc_magic_.c_str(), ipc_magic_.size()); - data32[0] = payload.size(); - data32[1] = type; + memcpy(header.data(), ipc_magic_.data(), ipc_magic_.size()); + if (payload.size() > std::numeric_limits::max()) { + throw std::runtime_error("IPC payload is too large"); + } + const auto payload_size = static_cast(payload.size()); + memcpy(header.data() + ipc_magic_.size(), &payload_size, sizeof payload_size); + memcpy(header.data() + ipc_magic_.size() + sizeof payload_size, &type, sizeof type); + + sendAll(fd, header.data(), ipc_header_size_, "Unable to send IPC header"); + sendAll(fd, payload.data(), payload.size(), "Unable to send IPC payload"); - if (::send(fd, header.data(), ipc_header_size_, 0) == -1) { - throw std::runtime_error("Unable to send IPC header"); - } - if (::send(fd, payload.c_str(), payload.size(), 0) == -1) { - throw std::runtime_error("Unable to send IPC payload"); - } return Ipc::recv(fd); } diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index 2e1103d3..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()); } @@ -125,8 +145,15 @@ auto Language::update() -> 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/window.cpp b/src/modules/sway/window.cpp index 2908b85c..2b62da14 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -94,12 +94,12 @@ auto Window::update() -> void { old_app_id_ = app_id_; } - label_.set_markup(waybar::util::rewriteString( + setLabelMarkup(waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_), fmt::arg("shell", shell_), fmt::arg("marks", marks_)), config_["rewrite"])); if (tooltipEnabled()) { - label_.set_tooltip_markup(window_); + setTooltipMarkup(window_); } updateAppIcon(); diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 6abe5383..3fc410d2 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -69,6 +69,7 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& m_windowRewriteRules = waybar::util::RegexCollection( windowRewrite, std::move(windowRewriteDefault), windowRewritePriorityFunction); } + populateIgnoreWorkspacesConfig(config); ipc_.subscribe(R"(["workspace"])"); ipc_.subscribe(R"(["window"])"); ipc_.signal_event.connect(sigc::mem_fun(*this, &Workspaces::onEvent)); @@ -97,6 +98,36 @@ void Workspaces::onEvent(const struct Ipc::ipc_response& res) { } } +auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value& config) -> void { + auto ignoreWorkspaces = config["ignore-workspaces"]; + if (ignoreWorkspaces.isArray()) { + for (const auto& workspaceRegex : ignoreWorkspaces) { + if (workspaceRegex.isString()) { + std::string ruleString = workspaceRegex.asString(); + try { + const std::regex rule{ruleString, std::regex_constants::icase}; + m_ignoreWorkspaces.emplace_back(rule); + } catch (const std::regex_error& e) { + spdlog::error("Invalid rule {}: {}", ruleString, e.what()); + } + } else { + spdlog::error("Not a string: '{}'", workspaceRegex); + } + } + } +} + +bool Workspaces::isWorkspaceIgnored(std::string const& name) { + for (auto& rule : m_ignoreWorkspaces) { + if (std::regex_match(name, rule)) { + return true; + break; + } + } + + return false; +} + void Workspaces::onCmd(const struct Ipc::ipc_response& res) { if (res.type == IPC_GET_TREE) { try { @@ -118,8 +149,9 @@ void Workspaces::onCmd(const struct Ipc::ipc_response& res) { }); for (auto& output : outputs) { - std::copy(output["nodes"].begin(), output["nodes"].end(), - std::back_inserter(workspaces_)); + std::copy_if( + output["nodes"].begin(), output["nodes"].end(), std::back_inserter(workspaces_), + [&](const auto& node) { return !(isWorkspaceIgnored(node["name"].asString())); }); std::copy(output["floating_nodes"].begin(), output["floating_nodes"].end(), std::back_inserter(workspaces_)); } @@ -143,7 +175,8 @@ void Workspaces::onCmd(const struct Ipc::ipc_response& res) { if (p_w.isArray() && !p_w.empty()) { // Adding to target outputs for (const Json::Value& output : p_w) { - if (output.asString() == bar_.output->name) { + auto output_name = output.asString(); + if (output_name == bar_.output->name || output_name == bar_.output->identifier) { Json::Value v; v["name"] = p_w_name; v["target_output"] = bar_.output->name; @@ -229,7 +262,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 { @@ -321,6 +357,18 @@ auto Workspaces::update() -> void { button.get_style_context()->remove_class("empty"); } if ((*it)["output"].isString()) { + // Simply attempt to remove all output classes every time to reset output classes. This works + // even if a class has not been previously added to the style context. + for (const auto &oclass : config_["output-classes"]) { + button.get_style_context()->remove_class(oclass.asString()); + } + // If output-classes contains a class for output associated with current workspace button, add + // the class to its style context. + std::string output_name = (*it)["output"].asString(); + if (config_["output-classes"].isMember(output_name) && + config_["output-classes"][output_name].isString()) { + button.get_style_context()->add_class(config_["output-classes"][output_name].asString()); + } if (((*it)["output"].asString()) == bar_.output->name) { button.get_style_context()->add_class("current_output"); } else { @@ -329,24 +377,44 @@ auto Workspaces::update() -> void { } else { button.get_style_context()->remove_class("current_output"); } - std::string output = (*it)["name"].asString(); + std::string full_name; + if (!config_["disable-markup"].asBool()) { + full_name = g_markup_escape_text((*it)["name"].asString().c_str(), -1); + } else { + full_name = (*it)["name"].asString(); + } + std::string windows = ""; if (config_["window-rewrite"].isObject()) { updateWindows((*it), windows); } + + auto index = (*it)["num"].asInt(); + if (config_["format"].isString()) { - auto format = config_["format"].asString(); - output = fmt::format( - fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)), fmt::arg("value", output), - fmt::arg("name", trimWorkspaceName(output)), fmt::arg("index", (*it)["num"].asString()), - fmt::arg("windows", - windows.substr(0, windows.length() - m_formatWindowSeparator.length())), - fmt::arg("output", (*it)["output"].asString())); + std::string format; + if (config_["format-for-negative-index"].isString() && index < 0) { + format = config_["format-for-negative-index"].asString(); + } else { + format = config_["format"].asString(); + } + + auto name = trimWorkspaceName(full_name); + auto output = (*it)["output"].asString(); + auto icon = getIcon(full_name, *it); + auto separated_windows = + windows.substr(0, windows.length() - m_formatWindowSeparator.length()); + + full_name = + fmt::format(fmt::runtime(format), fmt::arg("index", index), fmt::arg("name", name), + fmt::arg("value", full_name), fmt::arg("output", output), + fmt::arg("icon", icon), fmt::arg("windows", separated_windows)); } + if (!config_["disable-markup"].asBool()) { - static_cast(button.get_children()[0])->set_markup(output); + static_cast(button.get_children()[0])->set_markup(full_name); } else { - button.set_label(output); + button.set_label(full_name); } onButtonReady(*it, button); } @@ -369,11 +437,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()); diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 8d7e866f..a20580f5 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -84,6 +84,9 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& } } + 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(); @@ -103,7 +106,7 @@ 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); @@ -122,7 +125,9 @@ void SystemdFailedUnits::RequestSystemState() { system_state_ = load("systemwide", system_props_proxy_); user_state_ = load("user", user_props_proxy_); - if (system_state_ == "running" && user_state_ == "running") + + if ((system_state_ == "running" || system_state_ == "ignored") && + (user_state_ == "running" || user_state_ == "ignored")) overall_state_ = "ok"; else overall_state_ = "degraded"; @@ -292,7 +297,7 @@ auto SystemdFailedUnits::update() -> void { fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_), fmt::arg("failed_units_list", failed_list))); } else { - label_.set_tooltip_text(""); + label_.set_tooltip_markup(""); } } ALabel::update(); diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index bb5ff02c..1d5e6522 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -1,6 +1,8 @@ #include "modules/temperature.hpp" #include +#include +#include #include #if defined(__FreeBSD__) @@ -20,12 +22,59 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val if (check_set_path(item.asString())) break; }; - // if hwmon_path is an array, loop to find first valid item - traverseAsArray(config_["hwmon-path"], [this](const std::string& path) { - if (!std::filesystem::exists(path)) return false; - file_path_ = path; - return true; - }); + if (config_["hwmon-by-name"].isString() && config_["input-filename"].isString()) { + auto name = config_["hwmon-by-name"].asString(); + auto input_filename = config_["input-filename"].asString(); + for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon/")) { + if (std::filesystem::is_directory(entry) && file_path_.empty()) { + auto name_filepath = entry.path().string() + "/name"; + auto input_filepath = entry.path().string() + "/" + input_filename; + + if (std::filesystem::exists(name_filepath) && std::filesystem::exists(input_filepath)) { + std::ifstream name_file(name_filepath); + if (!name_file.is_open()) { + throw std::runtime_error("Error: Could not open file " + name_filepath); + } + + std::string line; + while (std::getline(name_file, line)) { + if (line.find(name) != std::string::npos) { + file_path_ = input_filepath; + break; + } + } + } + } + } + if (file_path_.empty()) throw std::runtime_error("Could not find hwmon by name " + name); + } + + auto find_hwmon_by_name = [](const std::string& name) -> std::optional { + for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon")) { + std::ifstream f(entry.path() / "name"); + std::string hwname; + if (f >> hwname && hwname == name) { + return entry.path(); + } + } + return std::nullopt; + }; + + // ensure either hwmon-name OR old paths are used, not both + if (config_["hwmon-name"].isString() && + (!config_["hwmon-path"].isNull() || !config_["hwmon-path-abs"].isNull())) { + throw std::runtime_error( + "hwmon-name cannot be used together with hwmon-path or hwmon-path-abs"); + } + + if (file_path_.empty()) { + // if hwmon_path is an array, loop to find first valid item + traverseAsArray(config_["hwmon-path"], [this](const std::string& path) { + if (!std::filesystem::exists(path)) return false; + file_path_ = path; + return true; + }); + } if (file_path_.empty() && config_["input-filename"].isString()) { // fallback to hwmon_paths-abs @@ -40,6 +89,20 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val }); } + if (file_path_.empty() && config_["hwmon-name"].isString()) { + if (!config_["input-filename"].isString()) { + throw std::runtime_error("hwmon-name requires input-filename to be set"); + } + + auto hwmon = find_hwmon_by_name(config_["hwmon-name"].asString()); + + if (!hwmon) { + throw std::runtime_error("hwmon-name '" + config_["hwmon-name"].asString() + "' not found"); + } + + file_path_ = hwmon->string() + "/" + config_["input-filename"].asString(); + } + if (file_path_.empty()) { auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); @@ -155,3 +218,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/user.cpp b/src/modules/user.cpp index 3ca5e431..87f5ea0f 100644 --- a/src/modules/user.cpp +++ b/src/modules/user.cpp @@ -7,7 +7,6 @@ #include #include -#include "gdkmm/cursor.h" #include "gdkmm/event.h" #include "gdkmm/types.h" #include "glibmm/fileutils.h" diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 42976d20..037a61e1 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -347,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/wireplumber.cpp b/src/modules/wireplumber.cpp index 0c0d1dde..a2ce8ed1 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -2,6 +2,9 @@ #include +#include +#include + bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; } std::list waybar::modules::Wireplumber::modules; @@ -25,7 +28,8 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val source_node_id_(0), source_muted_(false), source_volume_(0.0), - default_source_name_(nullptr) { + default_source_name_(nullptr), + form_factor_("") { waybar::modules::Wireplumber::modules.push_back(this); wp_init(WP_INIT_PIPEWIRE); @@ -108,6 +112,21 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* : description != nullptr ? description : "Unknown node name"; spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_); + + // find form-factor + const auto* devid = wp_properties_get(properties, "device.id"); + spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); + + auto* dev = static_cast(wp_object_manager_lookup( + self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", devid, nullptr)); + + if (const auto* ff = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), "device.form-factor")) { + self->form_factor_ = ff; + spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); + } else { + self->form_factor_ = ""; + } } void waybar::modules::Wireplumber::updateSourceName(waybar::modules::Wireplumber* self, @@ -211,6 +230,7 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* return; } } + return; } spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_, @@ -369,6 +389,8 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { "=s", self->type_, nullptr); wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", "Audio/Source", nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", + "=s", "Audio/Device", nullptr); } void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, @@ -407,7 +429,7 @@ void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* r spdlog::debug("[{}]: loaded mixer API", self->name_); g_ptr_array_add(self->apis_, ({ WpPlugin* p = wp_plugin_find(self->wp_core_, "mixer-api"); - g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, nullptr); + g_object_set(G_OBJECT(p), "scale", 0 /* linear */, nullptr); p; })); @@ -426,13 +448,55 @@ void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { this); } +static const std::array ports = { + "headphone", "speaker", "headset", "hands-free", "portable", "car", "hifi", +}; + +std::vector waybar::modules::Wireplumber::getWPIcon() { + std::vector res; + if (muted_) { + res.emplace_back(node_name_ + "-muted"); + } + res.push_back(node_name_); + res.push_back(source_name_); + std::transform(form_factor_.begin(), form_factor_.end(), form_factor_.begin(), ::tolower); + for (auto const& port : ports) { + if (form_factor_.find(port) != std::string::npos) { + if (muted_) { + res.emplace_back(port + "-muted"); + } + res.push_back(port); + break; + } + } + if (muted_) { + res.emplace_back("default-muted"); + } + return res; +} + auto waybar::modules::Wireplumber::update() -> void { auto format = format_; std::string tooltipFormat; + std::string format_name = "format"; + + // Handle sink bluetooth state + const std::string name = default_node_name_ != nullptr ? default_node_name_ : ""; + + auto bt = name.find("bluez") != std::string::npos || name.find("a2dp-sink") != std::string::npos; + if (bt) { + format_name += "-bluetooth"; + label_.get_style_context()->add_class("bluetooth"); + } else { + label_.get_style_context()->remove_class("bluetooth"); + } // Handle sink mute state if (muted_) { - format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; + // Check muted bluetooth format exists, otherwise fall back to default muted format. + if (format_name != "format" && !config_[format_name + "-muted"].isString()) + format_name = "format"; + format_name += "-muted"; label_.get_style_context()->add_class("muted"); label_.get_style_context()->add_class("sink-muted"); } else { @@ -447,18 +511,21 @@ auto waybar::modules::Wireplumber::update() -> void { label_.get_style_context()->remove_class("source-muted"); } - int vol = round(volume_ * 100.0); - int source_vol = round(source_volume_ * 100.0); + double vol_cube = pow(volume_, 3); + double source_vol_cube = pow(source_volume_, 3); + + int vol = round(vol_cube * 100.0); + int source_vol = round(source_vol_cube * 100.0); + + double vol_db = 20.0 * log10(volume_); + double source_vol_db = 20.0 * log10(source_volume_); // Get the state and apply state-specific format if available auto state = getState(vol); - if (!state.empty()) { - std::string format_name = muted_ ? "format-muted" : "format"; - std::string state_format_name = format_name + "-" + state; - if (config_[state_format_name].isString()) { - format = config_[state_format_name].asString(); - } - } + if (!state.empty() && config_[format_name + "-" + state].isString()) + format = config_[format_name + "-" + state].asString(); + else if (config_[format_name].isString()) + format = config_[format_name].asString(); // Prepare source format string (similar to PulseAudio) std::string format_source = "{volume}%"; @@ -476,10 +543,14 @@ auto waybar::modules::Wireplumber::update() -> void { std::string formatted_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_vol)); - std::string markup = - fmt::format(fmt::runtime(format), 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_)); + std::string markup = fmt::format( + fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), + fmt::arg("icon", getIcon(vol, getWPIcon())), fmt::arg("format_source", formatted_source), + fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_), + fmt::arg("volume_linear", volume_), fmt::arg("volume_cubic", vol_cube), + fmt::arg("volume_db", vol_db), fmt::arg("source_volume_linear", source_volume_), + fmt::arg("source_volume_cubic", source_vol_cube), + fmt::arg("source_volume_db", source_vol_db)); label_.set_markup(markup); if (tooltipEnabled()) { @@ -490,8 +561,12 @@ auto waybar::modules::Wireplumber::update() -> void { if (!tooltipFormat.empty()) { 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_))); + fmt::arg("icon", getIcon(vol, getWPIcon())), fmt::arg("format_source", formatted_source), + fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_), + fmt::arg("volume_linear", volume_), fmt::arg("volume_cubic", vol_cube), + fmt::arg("volume_db", vol_db), fmt::arg("source_volume_linear", source_volume_), + fmt::arg("source_volume_cubic", source_vol_cube), + fmt::arg("source_volume_db", source_vol_db))); } else { label_.set_tooltip_markup(node_name_); } @@ -510,28 +585,58 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { return true; } double maxVolume = 1; - double step = 1.0 / 100.0; + double step = 1.0; if (config_["scroll-step"].isDouble()) { - step = config_["scroll-step"].asDouble() / 100.0; + step = config_["scroll-step"].asDouble(); } if (config_["max-volume"].isDouble()) { - maxVolume = config_["max-volume"].asDouble() / 100.0; + maxVolume = config_["max-volume"].asDouble(); } - if (step < min_step_) step = min_step_; + double vol = volume_; + std::string scale = "cubic_percent"; + if (config_["scroll-scale"].isString()) { + scale = config_["scroll-scale"].asString(); + } - double newVol = volume_; + if (scale == "cubic") { + vol = pow(vol, 3); + } else if (scale == "db") { + vol = log10(vol) * 20.0; + } else if (scale == "cubic_percent") { + vol = pow(vol, 3) * 100.0; + } + + double newVol = vol; if (dir == SCROLL_DIR::UP) { - if (volume_ < maxVolume) { - newVol = volume_ + step; - if (newVol > maxVolume) newVol = maxVolume; + newVol = vol + step; + } else if (dir == SCROLL_DIR::DOWN) { + newVol = vol - step; + } + + if (scale == "cubic") { + newVol = cbrt(newVol); + } else if (scale == "db") { + newVol = exp10(newVol / 20.0); + } else if (scale == "cubic_percent") { + newVol = cbrt(newVol / 100.0); + } + + if (dir == SCROLL_DIR::UP) { + if (volume_ + min_step_ > newVol) { + newVol = volume_ + min_step_; } } else if (dir == SCROLL_DIR::DOWN) { - if (volume_ > 0) { - newVol = volume_ - step; - if (newVol < 0) newVol = 0; + if (volume_ - min_step_ < newVol) { + newVol = volume_ - min_step_; } } + + if (newVol < 0) + newVol = 0; + else if (newVol > maxVolume) + newVol = maxVolume; + if (newVol != volume_) { if (mixer_api_ == nullptr) return true; GVariant* variant = g_variant_new_double(newVol); diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index bde90910..e6170325 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -94,9 +94,46 @@ Task::Task(const waybar::Bar& bar, const Json::Value& config, Taskbar* tbar, button.set_relief(Gtk::RELIEF_NONE); - content_.add(text_before_); - content_.add(icon_); - content_.add(text_after_); + /* When "expand" is enabled the buttons stretch to fill the taskbar and the + * titles ellipsize to fit within the available space. This only makes sense + * on a horizontal bar; on a vertical bar the box grows along the vertical + * axis, so ellipsizing/forcing width_chars(1) would truncate every title to + * "…". Keep the historical behavior (buttons sized to their content) as the + * default and when the bar is vertical. */ + bool expand = config_["expand"].isBool() && config_["expand"].asBool(); + bool horizontal = bar.orientation == Gtk::ORIENTATION_HORIZONTAL; + + if (expand && horizontal) { + button.set_hexpand(true); + content_.set_hexpand(true); + text_before_.set_ellipsize(Pango::ELLIPSIZE_END); + text_before_.set_single_line_mode(true); + text_before_.set_width_chars(1); + text_before_.set_xalign(0.0); + text_after_.set_ellipsize(Pango::ELLIPSIZE_END); + text_after_.set_single_line_mode(true); + text_after_.set_width_chars(1); + text_after_.set_xalign(0.0); + + content_.pack_start(text_before_, true, true, 0); + content_.pack_start(icon_, false, false, 0); + content_.pack_start(text_after_, true, true, 0); + } else { + content_.add(text_before_); + content_.add(icon_); + content_.add(text_after_); + } + + if (config_["justify"].isString()) { + auto justify_str = config_["justify"].asString(); + if (justify_str == "left") { + content_.set_halign(Gtk::ALIGN_START); + } else if (justify_str == "right") { + content_.set_halign(Gtk::ALIGN_END); + } else if (justify_str == "center") { + content_.set_halign(Gtk::ALIGN_CENTER); + } + } content_.show(); button.add(content_); @@ -293,9 +330,12 @@ void Task::handle_output_enter(struct wl_output* output) { button.signal_size_allocate().connect_notify( sigc::mem_fun(this, &Task::on_button_size_allocated)); tbar_->add_button(button); - button.show(); + if (!config_["active-only"].asBool() || active()) { + button.show(); + } button_visible_ = true; spdlog::debug("{} now visible on {}", repr(), bar_.output->name); + tbar_->update_bar_css_classes(); } } @@ -308,6 +348,7 @@ void Task::handle_output_leave(struct wl_output* output) { button.hide(); button_visible_ = false; spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); + tbar_->update_bar_css_classes(); } } @@ -350,6 +391,20 @@ void Task::handle_done() { button.get_style_context()->remove_class("fullscreen"); } + if (button_visible_ && config_["active-only"].asBool()) { + if (active()) { + button.show(); + } else { + button.hide(); + } + } + + if (active()) { + tbar_->assign_current_workspace(*this); + } + + tbar_->update_bar_css_classes(); + if (config_["active-first"].isBool() && config_["active-first"].asBool() && active()) tbar_->move_button(button, 0); @@ -551,6 +606,8 @@ static void handle_global(void* data, struct wl_registry* registry, uint32_t nam const char* interface, uint32_t version) { if (std::strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { static_cast(data)->register_manager(registry, name, version); + } else if (std::strcmp(interface, ext_workspace_manager_v1_interface.name) == 0) { + static_cast(data)->register_workspace_manager(registry, name, version); } else if (std::strcmp(interface, wl_seat_interface.name) == 0) { static_cast(data)->register_seat(registry, name, version); } @@ -568,6 +625,7 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu bar_(bar), box_{bar.orientation, 0}, manager_{nullptr}, + workspace_manager_{nullptr}, seat_{nullptr} { box_.set_name("taskbar"); if (!id.empty()) { @@ -623,6 +681,27 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu } Taskbar::~Taskbar() { + for (auto& workspace : workspaces_) { + ext_workspace_handle_v1_destroy(workspace->handle); + } + workspaces_.clear(); + for (auto* group : workspace_groups_) { + ext_workspace_group_handle_v1_destroy(group); + } + workspace_groups_.clear(); + + if (workspace_manager_) { + struct wl_display* display = Client::inst()->wl_display; + ext_workspace_manager_v1_stop(workspace_manager_); + wl_display_roundtrip(display); + + if (workspace_manager_) { + spdlog::warn("Workspace manager destroyed before .finished event"); + ext_workspace_manager_v1_destroy(workspace_manager_); + workspace_manager_ = nullptr; + } + } + if (manager_) { struct wl_display* display = Client::inst()->wl_display; /* @@ -639,6 +718,13 @@ Taskbar::~Taskbar() { manager_ = nullptr; } } + + if (config_["bar-css-states"].asBool()) { + set_bar_css_class("toplevel-active", false); + set_bar_css_class("toplevel-maximized", false); + set_bar_css_class("toplevel-minimized", false); + set_bar_css_class("toplevel-fullscreen", false); + } } void Taskbar::update() { @@ -674,6 +760,80 @@ static const struct zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_i .finished = tm_handle_finished, }; +static void workspace_handle_id(void*, struct ext_workspace_handle_v1*, const char*) {} +static void workspace_handle_name(void*, struct ext_workspace_handle_v1*, const char*) {} +static void workspace_handle_coordinates(void*, struct ext_workspace_handle_v1*, struct wl_array*) { +} + +static void workspace_handle_state(void* data, struct ext_workspace_handle_v1*, uint32_t state) { + static_cast(data)->state = state; +} + +static void workspace_handle_capabilities(void*, struct ext_workspace_handle_v1*, uint32_t) {} + +static void workspace_handle_removed(void* data, struct ext_workspace_handle_v1* handle) { + static_cast(data)->taskbar->handle_workspace_removed(handle); +} + +static const struct ext_workspace_handle_v1_listener workspace_handle_impl = { + .id = workspace_handle_id, + .name = workspace_handle_name, + .coordinates = workspace_handle_coordinates, + .state = workspace_handle_state, + .capabilities = workspace_handle_capabilities, + .removed = workspace_handle_removed, +}; + +static void workspace_group_handle_capabilities(void*, struct ext_workspace_group_handle_v1*, + uint32_t) {} +static void workspace_group_handle_output_enter(void*, struct ext_workspace_group_handle_v1*, + struct wl_output*) {} +static void workspace_group_handle_output_leave(void*, struct ext_workspace_group_handle_v1*, + struct wl_output*) {} +static void workspace_group_handle_workspace_enter(void*, struct ext_workspace_group_handle_v1*, + struct ext_workspace_handle_v1*) {} +static void workspace_group_handle_workspace_leave(void*, struct ext_workspace_group_handle_v1*, + struct ext_workspace_handle_v1*) {} + +static void workspace_group_handle_removed(void* data, + struct ext_workspace_group_handle_v1* group) { + static_cast(data)->handle_workspace_group_removed(group); +} + +static const struct ext_workspace_group_handle_v1_listener workspace_group_impl = { + .capabilities = workspace_group_handle_capabilities, + .output_enter = workspace_group_handle_output_enter, + .output_leave = workspace_group_handle_output_leave, + .workspace_enter = workspace_group_handle_workspace_enter, + .workspace_leave = workspace_group_handle_workspace_leave, + .removed = workspace_group_handle_removed, +}; + +static void workspace_manager_handle_group(void* data, struct ext_workspace_manager_v1*, + struct ext_workspace_group_handle_v1* group) { + static_cast(data)->handle_workspace_group_create(group); +} + +static void workspace_manager_handle_workspace(void* data, struct ext_workspace_manager_v1*, + struct ext_workspace_handle_v1* workspace) { + static_cast(data)->handle_workspace_create(workspace); +} + +static void workspace_manager_handle_done(void* data, struct ext_workspace_manager_v1*) { + static_cast(data)->handle_workspace_done(); +} + +static void workspace_manager_handle_finished(void* data, struct ext_workspace_manager_v1*) { + static_cast(data)->handle_workspace_finished(); +} + +static const struct ext_workspace_manager_v1_listener workspace_manager_impl = { + .workspace_group = workspace_manager_handle_group, + .workspace = workspace_manager_handle_workspace, + .done = workspace_manager_handle_done, + .finished = workspace_manager_handle_finished, +}; + void Taskbar::register_manager(struct wl_registry* registry, uint32_t name, uint32_t version) { if (manager_) { spdlog::warn("Register foreign toplevel manager again although already existing!"); @@ -698,6 +858,18 @@ void Taskbar::register_manager(struct wl_registry* registry, uint32_t name, uint spdlog::debug("Failed to register manager"); } +void Taskbar::register_workspace_manager(struct wl_registry* registry, uint32_t name, + uint32_t version) { + if (workspace_manager_) { + return; + } + + version = std::min(version, ext_workspace_manager_v1_interface.version); + workspace_manager_ = static_cast( + wl_registry_bind(registry, name, &ext_workspace_manager_v1_interface, version)); + ext_workspace_manager_v1_add_listener(workspace_manager_, &workspace_manager_impl, this); +} + void Taskbar::register_seat(struct wl_registry* registry, uint32_t name, uint32_t version) { if (seat_) { spdlog::warn("Register seat again although already existing!"); @@ -717,8 +889,74 @@ void Taskbar::handle_finished() { manager_ = nullptr; } +void Taskbar::handle_workspace_group_create(struct ext_workspace_group_handle_v1* handle) { + ext_workspace_group_handle_v1_add_listener(handle, &workspace_group_impl, this); + workspace_groups_.push_back(handle); +} + +void Taskbar::handle_workspace_group_removed(struct ext_workspace_group_handle_v1* handle) { + const auto group = std::find(workspace_groups_.begin(), workspace_groups_.end(), handle); + if (group != workspace_groups_.end()) { + ext_workspace_group_handle_v1_destroy(*group); + workspace_groups_.erase(group); + } +} + +void Taskbar::handle_workspace_create(struct ext_workspace_handle_v1* handle) { + auto workspace = std::make_unique(WorkspaceState{this, handle}); + ext_workspace_handle_v1_add_listener(handle, &workspace_handle_impl, workspace.get()); + workspaces_.push_back(std::move(workspace)); +} + +void Taskbar::handle_workspace_done() { + const auto active_workspace = + std::find_if(workspaces_.begin(), workspaces_.end(), [](const auto& workspace) { + return workspace->state & EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE; + }); + current_workspace_ = + active_workspace == workspaces_.end() ? nullptr : (*active_workspace)->handle; + if (current_workspace_) { + const auto active_task = + std::find_if(tasks_.begin(), tasks_.end(), [](const auto& task) { return task->active(); }); + if (active_task != tasks_.end()) { + (*active_task)->set_workspace(current_workspace_); + } + } + update_bar_css_classes(); +} + +void Taskbar::handle_workspace_finished() { workspace_manager_ = nullptr; } + +void Taskbar::handle_workspace_removed(struct ext_workspace_handle_v1* handle) { + if (current_workspace_ == handle) { + current_workspace_ = nullptr; + } + for (auto& task : tasks_) { + if (task->workspace() == handle) { + task->set_workspace(nullptr); + } + } + + const auto workspace = + std::find_if(workspaces_.begin(), workspaces_.end(), + [handle](const auto& workspace) { return workspace->handle == handle; }); + if (workspace != workspaces_.end()) { + ext_workspace_handle_v1_destroy((*workspace)->handle); + workspaces_.erase(workspace); + } + update_bar_css_classes(); +} + void Taskbar::add_button(Gtk::Button& bt) { - box_.pack_start(bt, false, false); + /* Only let the buttons expand to fill the taskbar when "expand" is enabled + * and the bar is horizontal (see the Task constructor for details). */ + bool expand = config_["expand"].isBool() && config_["expand"].asBool(); + bool horizontal = bar_.orientation == Gtk::ORIENTATION_HORIZONTAL; + if (expand && horizontal) { + box_.pack_start(bt, true, true); + } else { + box_.pack_start(bt, false, false); + } box_.get_style_context()->remove_class("empty"); } @@ -741,6 +979,56 @@ void Taskbar::remove_task(uint32_t id) { } tasks_.erase(it); + update_bar_css_classes(); +} + +void Taskbar::assign_current_workspace(Task& task) { + if (current_workspace_) { + task.set_workspace(current_workspace_); + } +} + +void Taskbar::update_bar_css_classes() { + if (!config_["bar-css-states"].asBool()) { + return; + } + + const auto active_task = std::find_if(tasks_.begin(), tasks_.end(), [](const TaskPtr& task) { + return task->visible() && task->active(); + }); + + const bool has_active_task = active_task != tasks_.end(); + const auto on_current_workspace = [this](const TaskPtr& task) { + if (!current_workspace_) { + return task->active(); + } + return task->workspace() == current_workspace_; + }; + const bool has_maximized_task = + std::any_of(tasks_.begin(), tasks_.end(), [&on_current_workspace](const TaskPtr& task) { + return task->visible() && !task->minimized() && on_current_workspace(task) && + task->maximized(); + }); + const bool has_fullscreen_task = + std::any_of(tasks_.begin(), tasks_.end(), [&on_current_workspace](const TaskPtr& task) { + return task->visible() && !task->minimized() && on_current_workspace(task) && + task->fullscreen(); + }); + set_bar_css_class("toplevel-active", has_active_task); + set_bar_css_class("toplevel-maximized", has_maximized_task); + set_bar_css_class("toplevel-minimized", has_active_task && (*active_task)->minimized()); + set_bar_css_class("toplevel-fullscreen", has_fullscreen_task); +} + +void Taskbar::set_bar_css_class(const std::string& class_name, bool enabled) { + const auto style = bar_.window.get_style_context(); + if (enabled && !style->has_class(class_name)) { + spdlog::trace("Adding bar class: {}", class_name); + style->add_class(class_name); + } else if (!enabled && style->has_class(class_name)) { + spdlog::trace("Removing bar class: {}", class_name); + style->remove_class(class_name); + } } bool Taskbar::show_output(struct wl_output* output) const { diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 4087e096..482d7886 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -24,8 +24,9 @@ AudioBackend::AudioBackend(std::function on_updated_cb, private_construc source_volume_(0), source_muted_(false), on_updated_cb_(std::move(on_updated_cb)) { - // Initialize pa_volume_ with safe defaults + // Initialize pa_volume_ and pa_source_volume_ with safe defaults pa_cvolume_init(&pa_volume_); + pa_cvolume_init(&pa_source_volume_); mainloop_ = pa_threaded_mainloop_new(); if (mainloop_ == nullptr) { throw std::runtime_error("pa_mainloop_new() failed."); @@ -99,15 +100,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_); - pa_context_unref(backend->context_); - backend->context_ = nullptr; + 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: @@ -155,6 +158,19 @@ void AudioBackend::volumeModifyCb(pa_context* c, int success, void* data) { } } +void AudioBackend::sourceVolumeModifyCb(pa_context* c, int success, void* data) { + auto* backend = static_cast(data); + if (success != 0) { + if ((backend->context_ != nullptr) && + pa_context_get_state(backend->context_) == PA_CONTEXT_READY) { + pa_context_get_source_info_by_index(backend->context_, backend->source_idx_, sourceInfoCb, + data); + } + } else { + spdlog::debug("Source volume modification failed"); + } +} + /* * Called when the requested sink information is ready. */ @@ -182,10 +198,18 @@ void AudioBackend::sinkInfoCb(pa_context* /*context*/, const pa_sink_info* i, in } } + if (const auto mapping = backend->sink_mapping_.find(backend->current_sink_name_); + mapping != backend->sink_mapping_.end()) { + if (i->name == mapping->second) { + backend->current_sink_name_ = i->name; + } + } + backend->default_sink_running_ = backend->default_sink_name == i->name && (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); - if (i->name != backend->default_sink_name && !backend->default_sink_running_) { + if (i->name != backend->default_sink_name && i->name != backend->current_sink_name_ && + !backend->default_sink_running_) { return; } @@ -234,6 +258,11 @@ void AudioBackend::sourceInfoCb(pa_context* /*context*/, const pa_source_info* i void* data) { auto* backend = static_cast(data); if (i != nullptr && backend->default_source_name_ == i->name) { + if (pa_cvolume_valid(&i->volume) != 0) { + backend->pa_source_volume_ = i->volume; + } else { + pa_cvolume_init(&backend->pa_source_volume_); + } auto source_volume = static_cast(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM}; backend->source_volume_ = std::round(source_volume * 100.0F); backend->source_idx_ = i->index; @@ -259,25 +288,39 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo pa_context_get_source_info_list(context, sourceInfoCb, data); } -void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) { +uint16_t AudioBackend::getVolume(PulseaudioTarget target) const { + if (target == PulseaudioTarget::Source) { + return source_volume_; + } else { + return volume_; + } +} + +void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume, + PulseaudioTarget target) { // Early return if context is not ready if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { spdlog::error("PulseAudio context not ready"); return; } + bool is_source = target == PulseaudioTarget::Source; + + // Select the appropriate stored volume structure + auto& ref_volume = is_source ? pa_source_volume_ : pa_volume_; + // Prepare volume structure pa_cvolume pa_volume; pa_cvolume_init(&pa_volume); // Use existing volume structure if valid, otherwise create a safe default - if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) { - pa_volume = pa_volume_; + if ((pa_cvolume_valid(&ref_volume) != 0) && (pa_channels_valid(ref_volume.channels) != 0)) { + pa_volume = ref_volume; } else { - // Set stereo as a safe default - pa_volume.channels = 2; - spdlog::debug("Using default stereo volume structure"); + // Mono for sources (microphones), stereo for sinks + pa_volume.channels = is_source ? 1 : 2; + spdlog::debug("Using default volume structure ({} channels)", pa_volume.channels); } // Set the volume safely @@ -291,39 +334,56 @@ void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t m // Apply the volume change pa_threaded_mainloop_lock(mainloop_); - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (is_source) { + pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb, + this); + } else { + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + } pa_threaded_mainloop_unlock(mainloop_); } -void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume) { +void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume, + PulseaudioTarget target) { // Early return if context is not ready if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { spdlog::error("PulseAudio context not ready"); return; } + bool is_source = target == PulseaudioTarget::Source; + + // Select the appropriate stored volume structure and current volume + auto& ref_volume = is_source ? pa_source_volume_ : pa_volume_; + auto current_volume = is_source ? source_volume_ : volume_; + // Prepare volume structure pa_cvolume pa_volume; pa_cvolume_init(&pa_volume); // Use existing volume structure if valid, otherwise create a safe default - if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) { - pa_volume = pa_volume_; + if ((pa_cvolume_valid(&ref_volume) != 0) && (pa_channels_valid(ref_volume.channels) != 0)) { + pa_volume = ref_volume; } else { - // Set stereo as a safe default - pa_volume.channels = 2; - spdlog::debug("Using default stereo volume structure"); + // Mono for sources (microphones), stereo for sinks + pa_volume.channels = is_source ? 1 : 2; + spdlog::debug("Using default volume structure ({} channels)", pa_volume.channels); // Initialize all channels to current volume level double volume_tick = static_cast(PA_VOLUME_NORM) / 100; - pa_volume_t vol = volume_ * volume_tick; + pa_volume_t vol = current_volume * volume_tick; for (uint8_t i = 0; i < pa_volume.channels; i++) { pa_volume.values[i] = vol; } // No need to continue with volume change if we had to create a new structure pa_threaded_mainloop_lock(mainloop_); - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (is_source) { + pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb, + this); + } else { + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + } pa_threaded_mainloop_unlock(mainloop_); return; } @@ -333,10 +393,10 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma pa_volume_t change; max_volume = std::min(max_volume, static_cast(PA_VOLUME_UI_MAX)); - if (change_type == ChangeType::Increase && volume_ < max_volume) { + if (change_type == ChangeType::Increase && current_volume < max_volume) { // Calculate how much to increase - if (volume_ + step > max_volume) { - change = round((max_volume - volume_) * volume_tick); + if (current_volume + step > max_volume) { + change = round((max_volume - current_volume) * volume_tick); } else { change = round(step * volume_tick); } @@ -345,10 +405,10 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma for (uint8_t i = 0; i < pa_volume.channels; i++) { pa_volume.values[i] = std::min(pa_volume.values[i] + change, PA_VOLUME_MAX); } - } else if (change_type == ChangeType::Decrease && volume_ > 0) { + } else if (change_type == ChangeType::Decrease && current_volume > 0) { // Calculate how much to decrease - if (volume_ - step < 0) { - change = round(volume_ * volume_tick); + if (current_volume - step < 0) { + change = round(current_volume * volume_tick); } else { change = round(step * volume_tick); } @@ -364,10 +424,23 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma // Apply the volume change pa_threaded_mainloop_lock(mainloop_); - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (is_source) { + pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb, + this); + } else { + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + } pa_threaded_mainloop_unlock(mainloop_); } +bool AudioBackend::getMuted(PulseaudioTarget target) const { + if (target == PulseaudioTarget::Source) { + return source_muted_; + } else { + return muted_; + } +} + void AudioBackend::toggleSinkMute() { if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = !muted_; @@ -404,6 +477,14 @@ void AudioBackend::toggleSourceMute(bool mute) { pa_threaded_mainloop_unlock(mainloop_); } +void AudioBackend::unmute(PulseaudioTarget target) { + if (target == PulseaudioTarget::Source) { + toggleSourceMute(false); + } else { + toggleSinkMute(false); + } +} + bool AudioBackend::isBluetooth() { return monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio monitor_.find("a2dp-sink") != std::string::npos || // PipeWire @@ -420,4 +501,14 @@ void AudioBackend::setIgnoredSinks(const Json::Value& config) { } } +void AudioBackend::setSinkMapping(const Json::Value& config) { + if (config.isObject()) { + for (auto it = config.begin(); it != config.end(); ++it) { + if (it.key().isString() && it->isString()) { + sink_mapping_.emplace(it.key().asString(), it->asString()); + } + } + } +} + } // namespace waybar::util diff --git a/src/util/css_reload_helper.cpp b/src/util/css_reload_helper.cpp index be8c0a68..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); } } diff --git a/src/util/hosts_check.cpp b/src/util/hosts_check.cpp new file mode 100644 index 00000000..1769dd78 --- /dev/null +++ b/src/util/hosts_check.cpp @@ -0,0 +1,17 @@ +#include "util/hosts_check.hpp" + +#include +#include + +namespace waybar::util { +bool valid_host(const Json::Value& config) { + if (config.isMember("hosts") && config["hosts"].isArray()) { + const auto hostname = Glib::get_host_name(); + + if (!std::ranges::any_of(config["hosts"].begin(), config["hosts"].end(), + [&](const auto& h) { return h.asString() == hostname; })) + return false; + } + return true; +} +} // namespace waybar::util diff --git a/src/util/rewrite_string.cpp b/src/util/rewrite_string.cpp index 3f6ae4ca..8c377d3f 100644 --- a/src/util/rewrite_string.cpp +++ b/src/util/rewrite_string.cpp @@ -18,7 +18,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) { // malformated regexes will cause an exception. // in this case, log error and try the next rule. const std::regex rule{it.key().asString(), std::regex_constants::icase}; - if (std::regex_match(value, rule)) { + if (std::regex_match(res, rule)) { res = std::regex_replace(res, rule, it->asString()); } } catch (const std::regex_error& e) { 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/test/hyprland/backend.cpp b/test/hyprland/backend.cpp index 62d23ae4..f2e10dae 100644 --- a/test/hyprland/backend.cpp +++ b/test/hyprland/backend.cpp @@ -160,7 +160,7 @@ TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch] IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3"); REQUIRE( result == - "/dispatch hl.dsp.focus({ workspace = \"3\", monitor = \"current\" })"); + "/dispatch hl.dsp.focus({ workspace = \"3\", on_current_monitor = true })"); } TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") { 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++; }