diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 8af7a04..d5064fe 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -4,12 +4,16 @@ on: [ push, pull_request ] jobs: clang: - runs-on: macos-latest # until https://github.com/actions/runner/issues/385 + # Run actions in a FreeBSD vm on the macos-10.15 runner + # https://github.com/actions/runner/issues/385 - for FreeBSD runner support + # https://github.com/actions/virtual-environments/issues/4060 - for lack of VirtualBox on MacOS 11 runners + runs-on: macos-10.15 steps: - uses: actions/checkout@v2 - name: Test in FreeBSD VM - uses: vmactions/freebsd-vm@v0.1.4 # aka FreeBSD 12.2 + uses: vmactions/freebsd-vm@v0.1.5 # aka FreeBSD 13.0 with: + mem: 2048 usesh: true prepare: | export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio diff --git a/README.md b/README.md index 98b99a2..c5806e3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > Highly customizable Wayland bar for Sway and Wlroots based compositors.
> Available in Arch [community](https://www.archlinux.org/packages/community/x86_64/waybar/) or -[AUR](https://aur.archlinux.org/packages/waybar-git/), [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/waybar), and [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=waybar)
+[AUR](https://aur.archlinux.org/packages/waybar-git/), [Gentoo](https://packages.gentoo.org/packages/gui-apps/waybar), [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/waybar), and [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=waybar)
> *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)* #### Current features @@ -69,6 +69,7 @@ libdbusmenu-gtk3 [Tray module] libmpdclient [MPD module] libsndio [sndio module] libevdev [KeyboardState module] +xkbregistry ``` **Build dependencies** @@ -101,7 +102,8 @@ sudo apt install \ libsigc++-2.0-dev \ libspdlog-dev \ libwayland-dev \ - scdoc + scdoc \ + libxkbregistry-dev ``` diff --git a/include/bar.hpp b/include/bar.hpp index 6f3dfcf..01a9d03 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -8,6 +8,9 @@ #include #include +#include +#include + #include "AModule.hpp" #include "xdg-output-unstable-v1-client-protocol.h" @@ -36,6 +39,19 @@ struct bar_margins { int left = 0; }; +struct bar_mode { + bar_layer layer; + bool exclusive; + bool passthrough; + bool visible; +}; + +#ifdef HAVE_SWAY +namespace modules::sway { +class BarIpcClient; +} +#endif // HAVE_SWAY + class BarSurface { protected: BarSurface() = default; @@ -54,38 +70,56 @@ class BarSurface { class Bar { public: + using bar_mode_map = std::map; + static const bar_mode_map PRESET_MODES; + static const std::string_view MODE_DEFAULT; + static const std::string_view MODE_INVISIBLE; + Bar(struct waybar_output *w_output, const Json::Value &); Bar(const Bar &) = delete; - ~Bar() = default; + ~Bar(); + void setMode(const std::string_view &); void setVisible(bool visible); void toggle(); void handleSignal(int); struct waybar_output *output; Json::Value config; - struct wl_surface * surface; - bool exclusive = true; + struct wl_surface *surface; bool visible = true; bool vertical = false; Gtk::Window window; +#ifdef HAVE_SWAY + std::string bar_id; +#endif + private: void onMap(GdkEventAny *); auto setupWidgets() -> void; - void getModules(const Factory &, const std::string &); + void getModules(const Factory &, const std::string &, Gtk::Box*); void setupAltFormatKeyForModule(const std::string &module_name); void setupAltFormatKeyForModuleList(const char *module_list_name); + void setMode(const bar_mode &); + + /* Copy initial set of modes to allow customization */ + bar_mode_map configured_modes = PRESET_MODES; + std::string last_mode_{MODE_DEFAULT}; std::unique_ptr surface_impl_; - bar_layer layer_; Gtk::Box left_; Gtk::Box center_; Gtk::Box right_; Gtk::Box box_; - std::vector> modules_left_; - std::vector> modules_center_; - std::vector> modules_right_; + std::vector> modules_left_; + std::vector> modules_center_; + std::vector> modules_right_; +#ifdef HAVE_SWAY + using BarIpcClient = modules::sway::BarIpcClient; + std::unique_ptr _ipc_client; +#endif + std::vector> modules_all_; }; } // namespace waybar diff --git a/include/client.hpp b/include/client.hpp index bd80d0b..7fc3dce 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -29,6 +29,7 @@ class Client { struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; std::vector> bars; Config config; + std::string bar_id; private: Client() = default; diff --git a/include/factory.hpp b/include/factory.hpp index 4b9f32a..3855ce2 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -14,6 +14,7 @@ #endif #ifdef HAVE_WLR #include "modules/wlr/taskbar.hpp" +#include "modules/wlr/workspace_manager.hpp" #endif #ifdef HAVE_RIVER #include "modules/river/tags.hpp" @@ -50,6 +51,9 @@ #ifdef HAVE_LIBSNDIO #include "modules/sndio.hpp" #endif +#ifdef HAVE_GIO_UNIX +#include "modules/inhibitor.hpp" +#endif #include "bar.hpp" #include "modules/custom.hpp" #include "modules/temperature.hpp" diff --git a/include/group.hpp b/include/group.hpp new file mode 100644 index 0000000..f282f9c --- /dev/null +++ b/include/group.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include "AModule.hpp" +#include "bar.hpp" +#include "factory.hpp" + +namespace waybar { + +class Group : public AModule { + public: + Group(const std::string&, const Bar&, const Json::Value&); + ~Group() = default; + auto update() -> void; + operator Gtk::Widget &(); + Gtk::Box box; +}; + +} // namespace waybar diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index 17752e4..9f95019 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -17,6 +17,8 @@ struct waybar_time { date::zoned_seconds ztime; }; +const std::string kCalendarPlaceholder = "calendar"; + class Clock : public ALabel { public: Clock(const std::string&, const Json::Value&); @@ -26,18 +28,19 @@ class Clock : public ALabel { private: util::SleeperThread thread_; std::locale locale_; - const date::time_zone* time_zone_; - bool fixed_time_zone_; - int time_zone_idx_; + std::vector time_zones_; + int current_time_zone_idx_; date::year_month_day cached_calendar_ymd_ = date::January/1/0; std::string cached_calendar_text_; + bool is_calendar_in_tooltip_; bool handleScroll(GdkEventScroll* e); auto calendar_text(const waybar_time& wtime) -> std::string; auto weekdays_header(const date::weekday& first_dow, std::ostream& os) -> void; auto first_day_of_week() -> date::weekday; - bool setTimeZone(Json::Value zone_name); + const date::time_zone* current_timezone(); + bool is_timezone_fixed(); }; } // namespace waybar::modules diff --git a/include/modules/inhibitor.hpp b/include/modules/inhibitor.hpp new file mode 100644 index 0000000..aa2f97d --- /dev/null +++ b/include/modules/inhibitor.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +#include "ALabel.hpp" +#include "bar.hpp" + +namespace waybar::modules { + +class Inhibitor : public ALabel { + public: + Inhibitor(const std::string&, const waybar::Bar&, const Json::Value&); + ~Inhibitor() override; + auto update() -> void; + auto activated() -> bool; + + private: + auto handleToggle(::GdkEventButton* const& e) -> bool; + + const std::unique_ptr<::GDBusConnection, void(*)(::GDBusConnection*)> dbus_; + const std::string inhibitors_; + int handle_ = -1; +}; + +} // namespace waybar::modules diff --git a/include/modules/network.hpp b/include/modules/network.hpp index ad28520..7b8281b 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -43,6 +43,7 @@ class Network : public ALabel { const std::string getNetworkState() const; void clearIface(); bool wildcardMatch(const std::string& pattern, const std::string& text) const; + std::optional> readBandwidthUsage(); int ifid_; sa_family_t family_; @@ -72,7 +73,8 @@ class Network : public ALabel { int cidr_; int32_t signal_strength_dbm_; uint8_t signal_strength_; - uint32_t frequency_; + std::string signal_strength_app_; + float frequency_; uint32_t route_priority; util::SleeperThread thread_; diff --git a/include/modules/river/tags.hpp b/include/modules/river/tags.hpp index 9b75fbd..c49ec60 100644 --- a/include/modules/river/tags.hpp +++ b/include/modules/river/tags.hpp @@ -6,6 +6,7 @@ #include "AModule.hpp" #include "bar.hpp" #include "river-status-unstable-v1-client-protocol.h" +#include "river-control-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h" namespace waybar::modules::river { @@ -20,7 +21,12 @@ class Tags : public waybar::AModule { void handle_view_tags(struct wl_array *tags); void handle_urgent_tags(uint32_t tags); + void handle_primary_clicked(uint32_t tag); + bool handle_button_press(GdkEventButton *event_button, uint32_t tag); + struct zriver_status_manager_v1 *status_manager_; + struct zriver_control_v1 *control_; + struct wl_seat *seat_; private: const waybar::Bar & bar_; diff --git a/include/modules/sway/bar.hpp b/include/modules/sway/bar.hpp new file mode 100644 index 0000000..c4381a4 --- /dev/null +++ b/include/modules/sway/bar.hpp @@ -0,0 +1,49 @@ +#pragma once +#include + +#include "modules/sway/ipc/client.hpp" +#include "util/SafeSignal.hpp" +#include "util/json.hpp" + +namespace waybar { + +class Bar; + +namespace modules::sway { + +/* + * Supported subset of i3/sway IPC barconfig object + */ +struct swaybar_config { + std::string id; + std::string mode; + std::string hidden_state; +}; + +/** + * swaybar IPC client + */ +class BarIpcClient { + public: + BarIpcClient(waybar::Bar& bar); + + private: + void onInitialConfig(const struct Ipc::ipc_response& res); + void onIpcEvent(const struct Ipc::ipc_response&); + void onConfigUpdate(const swaybar_config& config); + void onVisibilityUpdate(bool visible_by_modifier); + void update(); + + Bar& bar_; + util::JsonParser parser_; + Ipc ipc_; + + swaybar_config bar_config_; + bool visible_by_modifier_ = false; + + SafeSignal signal_visible_; + SafeSignal signal_config_; +}; + +} // namespace modules::sway +} // namespace waybar diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index 1faf52b..92e2bba 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -32,6 +32,7 @@ class Language : public ALabel, public sigc::trackable { std::string short_name; std::string variant; std::string short_description; + std::string country_flag() const; }; class XKBContext { @@ -54,7 +55,7 @@ class Language : public ALabel, public sigc::trackable { const static std::string XKB_LAYOUT_NAMES_KEY; const static std::string XKB_ACTIVE_LAYOUT_NAME_KEY; - + Layout layout_; std::string tooltip_format_ = ""; std::map layouts_map_; diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 891ad55..973d15d 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -3,11 +3,13 @@ #include "AModule.hpp" #include "bar.hpp" #include "client.hpp" +#include "giomm/desktopappinfo.h" #include "util/json.hpp" #include #include #include +#include #include #include @@ -61,15 +63,18 @@ class Task Gtk::Image icon_; Gtk::Label text_before_; Gtk::Label text_after_; - bool button_visible_; - bool ignored_; + Glib::RefPtr app_info_; + bool button_visible_ = false; + bool ignored_ = false; - bool with_icon_; + bool with_icon_ = false; + bool with_name_ = false; std::string format_before_; std::string format_after_; std::string format_tooltip_; + std::string name_; std::string title_; std::string app_id_; uint32_t state_ = 0; @@ -77,6 +82,9 @@ class Task private: std::string repr() const; std::string state_string(bool = false) const; + void set_app_info_from_app_id_list(const std::string& app_id_list); + bool image_load_icon(Gtk::Image& image, const Glib::RefPtr& icon_theme, Glib::RefPtr app_info, int size); + void hide_if_ignored(); public: /* Getter functions */ @@ -135,6 +143,7 @@ class Taskbar : public waybar::AModule std::vector> icon_themes_; std::unordered_set ignore_list_; + std::map app_ids_replace_map_; struct zwlr_foreign_toplevel_manager_v1 *manager_; struct wl_seat *seat_; @@ -157,8 +166,9 @@ class Taskbar : public waybar::AModule bool show_output(struct wl_output *) const; bool all_outputs() const; - std::vector> icon_themes() const; + const std::vector>& icon_themes() const; const std::unordered_set& ignore_list() const; + const std::map& app_ids_replace_map() const; }; } /* namespace waybar::modules::wlr */ diff --git a/include/modules/wlr/workspace_manager.hpp b/include/modules/wlr/workspace_manager.hpp new file mode 100644 index 0000000..e4cdb4d --- /dev/null +++ b/include/modules/wlr/workspace_manager.hpp @@ -0,0 +1,160 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "ext-workspace-unstable-v1-client-protocol.h" + +namespace waybar::modules::wlr { + +class WorkspaceManager; +class WorkspaceGroup; + +class Workspace { + public: + Workspace(const waybar::Bar &bar, const Json::Value &config, WorkspaceGroup &workspace_group, + zext_workspace_handle_v1 *workspace, uint32_t id); + ~Workspace(); + auto update() -> void; + + auto id() const -> uint32_t { return id_; } + auto is_active() const -> bool { return state_ & static_cast(State::ACTIVE); } + auto is_urgent() const -> bool { return state_ & static_cast(State::URGENT); } + auto is_hidden() const -> bool { return state_ & static_cast(State::HIDDEN); } + // wlr stuff + auto handle_name(const std::string &name) -> void; + auto handle_coordinates(const std::vector &coordinates) -> void; + auto handle_state(const std::vector &state) -> void; + auto handle_remove() -> void; + + auto handle_done() -> void; + auto handle_clicked(GdkEventButton *bt) -> bool; + auto show() -> void; + auto hide() -> void; + auto get_button_ref() -> Gtk::Button & { return button_; } + auto get_name() -> std::string & { return name_; } + auto get_coords() -> std::vector & { return coordinates_; } + + enum class State { + ACTIVE = (1 << 0), + URGENT = (1 << 1), + HIDDEN = (1 << 2), + }; + + private: + auto get_icon() -> std::string; + + const Bar &bar_; + const Json::Value &config_; + WorkspaceGroup &workspace_group_; + + // wlr stuff + zext_workspace_handle_v1 *workspace_handle_; + uint32_t state_ = 0; + + uint32_t id_; + std::string name_; + std::vector coordinates_; + static std::map icons_map_; + std::string format_; + bool with_icon_ = false; + + Gtk::Button button_; + Gtk::Box content_; + Gtk::Label label_; +}; + +class WorkspaceGroup { + public: + WorkspaceGroup(const waybar::Bar &bar, Gtk::Box &box, const Json::Value &config, + WorkspaceManager &manager, zext_workspace_group_handle_v1 *workspace_group_handle, + uint32_t id); + ~WorkspaceGroup(); + auto update() -> void; + + auto id() const -> uint32_t { return id_; } + auto is_visible() const -> bool; + auto remove_workspace(uint32_t id_) -> void; + auto active_only() const -> bool; + auto creation_delayed() const -> bool; + auto workspaces() -> std::vector> & { return workspaces_; } + + auto sort_workspaces() -> void; + auto set_need_to_sort() -> void { need_to_sort = true; } + auto add_button(Gtk::Button &button) -> void; + auto remove_button(Gtk::Button &button) -> void; + + // wlr stuff + auto handle_workspace_create(zext_workspace_handle_v1 *workspace_handle) -> void; + auto handle_remove() -> void; + auto handle_output_enter(wl_output *output) -> void; + auto handle_output_leave() -> void; + auto handle_done() -> void; + auto commit() -> void; + + private: + static uint32_t workspace_global_id; + const waybar::Bar &bar_; + Gtk::Box &box_; + const Json::Value &config_; + WorkspaceManager &workspace_manager_; + + // wlr stuff + zext_workspace_group_handle_v1 *workspace_group_handle_; + wl_output *output_ = nullptr; + + uint32_t id_; + std::vector> workspaces_; + bool need_to_sort = false; +}; + +class WorkspaceManager : public AModule { + public: + WorkspaceManager(const std::string &id, const waybar::Bar &bar, const Json::Value &config); + ~WorkspaceManager() override; + auto update() -> void override; + + auto all_outputs() const -> bool { return all_outputs_; } + auto active_only() const -> bool { return active_only_; } + auto workspace_comparator() const + -> std::function &, std::unique_ptr &)>; + auto creation_delayed() const -> bool { return creation_delayed_; } + + auto sort_workspaces() -> void; + auto remove_workspace_group(uint32_t id_) -> void; + + // wlr stuff + auto register_manager(wl_registry *registry, uint32_t name, uint32_t version) -> void; + auto handle_workspace_group_create(zext_workspace_group_handle_v1 *workspace_group_handle) + -> void; + auto handle_done() -> void; + auto handle_finished() -> void; + auto commit() -> void; + + private: + const waybar::Bar &bar_; + Gtk::Box box_; + std::vector> groups_; + + // wlr stuff + zext_workspace_manager_v1 *workspace_manager_ = nullptr; + + static uint32_t group_global_id; + + bool sort_by_name_ = true; + bool sort_by_coordinates_ = true; + bool all_outputs_ = false; + bool active_only_ = false; + bool creation_delayed_ = false; +}; + +} // namespace waybar::modules::wlr diff --git a/include/modules/wlr/workspace_manager_binding.hpp b/include/modules/wlr/workspace_manager_binding.hpp new file mode 100644 index 0000000..0bfe663 --- /dev/null +++ b/include/modules/wlr/workspace_manager_binding.hpp @@ -0,0 +1,8 @@ +#include "ext-workspace-unstable-v1-client-protocol.h" + +namespace waybar::modules::wlr { + void add_registry_listener(void *data); + void add_workspace_listener(zext_workspace_handle_v1 *workspace_handle, void *data); + void add_workspace_group_listener(zext_workspace_group_handle_v1 *workspace_group_handle, void *data); + zext_workspace_manager_v1* workspace_manager_bind(wl_registry *registry, uint32_t name, uint32_t version, void *data); +} diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp new file mode 100644 index 0000000..3b68653 --- /dev/null +++ b/include/util/SafeSignal.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace waybar { + +/** + * Thread-safe signal wrapper. + * Uses Glib::Dispatcher to pass events to another thread and locked queue to pass the arguments. + */ +template +struct SafeSignal : sigc::signal...)> { + public: + SafeSignal() { dp_.connect(sigc::mem_fun(*this, &SafeSignal::handle_event)); } + + template + void emit(EmitArgs&&... args) { + if (main_tid_ == std::this_thread::get_id()) { + /* + * Bypass the queue if the method is called the main thread. + * Ensures that events emitted from the main thread are processed synchronously and saves a + * few CPU cycles on locking/queuing. + * As a downside, this makes main thread events prioritized over the other threads and + * disrupts chronological order. + */ + signal_t::emit(std::forward(args)...); + } else { + { + std::unique_lock lock(mutex_); + queue_.emplace(std::forward(args)...); + } + dp_.emit(); + } + } + + template + inline void operator()(EmitArgs&&... args) { + emit(std::forward(args)...); + } + + protected: + using signal_t = sigc::signal...)>; + using slot_t = decltype(std::declval().make_slot()); + using arg_tuple_t = std::tuple...>; + // ensure that unwrapped methods are not accessible + using signal_t::emit_reverse; + using signal_t::make_slot; + + void handle_event() { + for (std::unique_lock lock(mutex_); !queue_.empty(); lock.lock()) { + auto args = queue_.front(); + queue_.pop(); + lock.unlock(); + std::apply(cached_fn_, args); + } + } + + Glib::Dispatcher dp_; + std::mutex mutex_; + std::queue queue_; + const std::thread::id main_tid_ = std::this_thread::get_id(); + // cache functor for signal emission to avoid recreating it on each event + const slot_t cached_fn_ = make_slot(); +}; + +} // namespace waybar diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 48c2ee1..e8053c9 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -120,7 +120,7 @@ The two arguments are: # CUSTOM FORMATS -The *battery* module allows to define custom formats based on up to two factors. The best fitting format will be selected. +The *battery* module allows one to define custom formats based on up to two factors. The best fitting format will be selected. *format-*: With *states*, a custom format can be set depending on the capacity of your battery. diff --git a/man/waybar-inhibitor.5.scd b/man/waybar-inhibitor.5.scd new file mode 100644 index 0000000..0838f4d --- /dev/null +++ b/man/waybar-inhibitor.5.scd @@ -0,0 +1,92 @@ +waybar-inhibitor(5) + +# NAME + +waybar - inhibitor module + +# DESCRIPTION + +The *inhibitor* module allows to take an inhibitor lock that logind provides. +See *systemd-inhibit*(1) for more information. + +# CONFIGURATION + +*what*: ++ + typeof: string or array ++ + The inhibitor lock or locks that should be taken when active. The available inhibitor locks are *idle*, *shutdown*, *sleep*, *handle-power-key*, *handle-suspend-key*, *handle-hibernate-key* and *handle-lid-switch*. + +*format*: ++ + typeof: string ++ + The format, how the state should be displayed. + +*format-icons*: ++ + typeof: array ++ + Based on the current state, the corresponding icon gets selected. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. A click also toggles the state + +*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 clicked 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. + +# FORMAT REPLACEMENTS + +*{status}*: status (*activated* or *deactivated*) + +*{icon}*: Icon, as defined in *format-icons* + +# EXAMPLES + +``` +"inhibitor": { + "what": "handle-lid-switch", + "format": "{icon}", + "format-icons": { + "activated": "", + "deactivated": "" + } +} +``` diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index 65b9033..7814ee4 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -21,6 +21,11 @@ Addressed by *river/tags* typeof: array ++ The label to display for each tag. +*disable-click*: ++ + typeof: bool ++ + 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. + # EXAMPLE ``` diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd index 92a647e..1c88314 100644 --- a/man/waybar-sway-language.5.scd +++ b/man/waybar-sway-language.5.scd @@ -37,6 +37,8 @@ Addressed by *sway/language* *{variant}*: Variant of layout (e.g. "dvorak"). +*{flag}*: Country flag of layout. + # EXAMPLES ``` diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index f2808b9..b575e09 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -69,10 +69,6 @@ Addressed by *sway/workspaces* typeof: string ++ Command to execute when the module is updated. -*numeric-first*: ++ - typeof: bool ++ - Whether to put workspaces starting with numbers before workspaces that do not start with a number. - *disable-auto-back-and-forth*: ++ typeof: bool ++ Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration. @@ -120,7 +116,6 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge "sway/workspaces": { "disable-scroll": true, "all-outputs": true, - "numeric-first": false, "format": "{name}: {icon}", "format-icons": { "1": "", diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index 0e86238..40be7be 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -70,12 +70,18 @@ Addressed by *wlr/taskbar* *ignore-list*: ++ typeof: array ++ - List of app_id to be invisible. + List of app_id/titles to be invisible. + +*app_ids-mapping*: ++ + typeof: object ++ + Dictionary of app_id to be replaced with # FORMAT REPLACEMENTS *{icon}*: The icon of the application. +*{title}*: The application name as in desktop file if appropriate desktop fils found, otherwise same as {app_id} + *{title}*: The title of the application. *{app_id}*: The app_id (== application name) of the application. @@ -105,7 +111,10 @@ Addressed by *wlr/taskbar* "on-click-middle": "close", "ignore-list": [ "Alacritty" - ] + ], + "app_ids-mapping": { + "firefoxdeveloperedition": "firefox-developer-edition" + } } ``` diff --git a/man/waybar-wlr-workspaces.5.scd b/man/waybar-wlr-workspaces.5.scd new file mode 100644 index 0000000..f0df5e9 --- /dev/null +++ b/man/waybar-wlr-workspaces.5.scd @@ -0,0 +1,87 @@ +waybar-wlr-workspaces(5) + +# NAME + +waybar - wlr workspaces module + +# DESCRIPTION + +The *workspaces* module displays the currently used workspaces in wayland compositor. + +# CONFIGURATION + +Addressed by *wlr/workspaces* + +*format*: ++ + typeof: string ++ + default: {name} ++ + The format, how information should be displayed. + +*format-icons*: ++ + typeof: array ++ + Based on the workspace name and state, the corresponding icon gets selected. See *icons*. + +*sort-by-name*: ++ + typeof: bool ++ + default: true ++ + Should workspaces be sorted by name. + +*sort-by-coordinates*: ++ + typeof: bool ++ + default: true ++ + Should workspaces be sorted by coordinates. + Note that if both *sort-by-name* and *sort-by-coordinates* are true sort by name will be first. + If both are false - sort by id will be performed. + +*all-outputs*: ++ + typeof: bool ++ + default: false ++ + If set to false workspaces group will be shown only in assigned output. Otherwise all workspace groups are shown. + +*active-only*: ++ + typeof: bool ++ + default: false ++ + If set to true only active or urgent workspaces will be shown. + +# FORMAT REPLACEMENTS + +*{name}*: Name of workspace assigned by compositor + +*{icon}*: Icon, as defined in *format-icons*. + +# CLICK ACTIONS + +*activate*: Switch to workspace. +*close*: Close the workspace. + +# ICONS + +Additional to workspace name matching, the following *format-icons* can be set. + +- *default*: Will be shown, when no string match is found. +- *focused*: Will be shown, when workspace is focused + +# EXAMPLES + +``` +"wlr/workspaces": { + "format": "{name}: {icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "focused": "", + "default": "" + } +} +``` + +# Style + +- *#workspaces* +- *#workspaces button* +- *#workspaces button.active* +- *#workspaces button.urgent* +- *#workspaces button.hidden* diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index eaa8d78..249ca7e 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -72,14 +72,25 @@ Also a minimal example configuration can be found on the at the bottom of this m typeof: string ++ Optional name added as a CSS class, for styling multiple waybars. +*mode* ++ + typeof: string ++ + Selects one of the preconfigured display modes. This is an equivalent of the sway-bar(5) *mode* command and supports the same values: *dock*, *hide*, *invisible*, *overlay*. ++ + Note: *hide* and *invisible* modes may be not as useful without Sway IPC. + *exclusive* ++ typeof: bool ++ - default: *true* unless the layer is set to *overlay* ++ + default: *true* ++ Option to request an exclusive zone from the compositor. Disable this to allow drawing application windows underneath or on top of the bar. +*fixed-center* ++ + typeof: bool ++ + default: *true* + Prefer fixed center position for the `modules-center` block. The center block will stay in the middle of the bar whenever possible. It can still be pushed around if other blocks need more space. + When false, the center block is centered in the space between the left and right block. + *passthrough* ++ typeof: bool ++ - default: *false* unless the layer is set to *overlay* ++ + default: *false* ++ Option to pass any pointer events to the window under the bar. Intended to be used with either *top* or *overlay* layers and without exclusive zone. @@ -89,6 +100,16 @@ Also a minimal example configuration can be found on the at the bottom of this m Option to disable the use of gtk-layer-shell for popups. Only functional if compiled with gtk-layer-shell support. +*ipc* ++ + typeof: bool ++ + default: false ++ + Option to subscribe to the Sway IPC bar configuration and visibility events and control waybar with *swaymsg bar* commands. ++ + Requires *bar_id* value from sway configuration to be either passed with the *-b* commandline argument or specified with the *id* option. + +*id* ++ + typeof: string ++ + *bar_id* for the Sway IPC. Use this if you need to override the value passed with the *-b bar_id* commandline argument for the specific bar instance. + *include* ++ typeof: string|array ++ Paths to additional configuration files. @@ -203,6 +224,28 @@ When positioning Waybar on the left or right side of the screen, sometimes it's Valid options for the "rotate" property are: 0, 90, 180 and 270. +## Grouping modules + +Module groups allow stacking modules in the direction orthogonal to the bar direction. When the bar is positioned on the top or bottom of the screen, modules in a group are stacked vertically. Likewise, when positioned on the left or right, modules in a group are stacked horizontally. + +A module group is defined by specifying a module named "group/some-group-name". The group must also be configured with a list of contained modules. Example: + +``` +{ + "modules-right": ["group/hardware", "clock"], + + "group/hardware": { + "modules": [ + "cpu", + "memory", + "battery" + ] + }, + + ... +} +``` + # SUPPORTED MODULES - *waybar-backlight(5)* @@ -224,5 +267,6 @@ Valid options for the "rotate" property are: 0, 90, 180 and 270. - *waybar-sway-window(5)* - *waybar-sway-workspaces(5)* - *waybar-wlr-taskbar(5)* +- *waybar-wlr-workspaces(5)* - *waybar-temperature(5)* - *waybar-tray(5)* diff --git a/meson.build b/meson.build index c09fbb6..2c43426 100644 --- a/meson.build +++ b/meson.build @@ -86,7 +86,7 @@ wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols') gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0']) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk')) -giounix = dependency('gio-unix-2.0', required: get_option('dbusmenu-gtk')) +giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or get_option('logind').enabled())) jsoncpp = dependency('jsoncpp') sigcpp = dependency('sigc++-2.0') libepoll = dependency('epoll-shim', required: false) @@ -150,6 +150,7 @@ src_files = files( 'src/bar.cpp', 'src/client.cpp', 'src/config.cpp', + 'src/group.cpp', 'src/util/ustring_clen.cpp' ) @@ -177,6 +178,7 @@ endif add_project_arguments('-DHAVE_SWAY', language: 'cpp') src_files += [ 'src/modules/sway/ipc/client.cpp', + 'src/modules/sway/bar.cpp', 'src/modules/sway/mode.cpp', 'src/modules/sway/language.cpp', 'src/modules/sway/window.cpp', @@ -186,6 +188,8 @@ src_files += [ if true add_project_arguments('-DHAVE_WLR', language: 'cpp') src_files += 'src/modules/wlr/taskbar.cpp' + src_files += 'src/modules/wlr/workspace_manager.cpp' + src_files += 'src/modules/wlr/workspace_manager_binding.cpp' endif if true @@ -238,6 +242,11 @@ if libsndio.found() src_files += 'src/modules/sndio.cpp' endif +if (giounix.found() and not get_option('logind').disabled()) + add_project_arguments('-DHAVE_GIO_UNIX', language: 'cpp') + src_files += 'src/modules/inhibitor.cpp' +endif + if get_option('rfkill').enabled() if is_linux add_project_arguments('-DWANT_RFKILL', language: 'cpp') @@ -255,6 +264,10 @@ else src_files += 'src/modules/simpleclock.cpp' endif +if get_option('experimental') + add_project_arguments('-DUSE_EXPERIMENTAL', language: 'cpp') +endif + subdir('protocol') executable( @@ -282,7 +295,7 @@ executable( gtk_layer_shell, libsndio, tz_dep, - xkbregistry + xkbregistry ], include_directories: [include_directories('include')], install: true, @@ -334,10 +347,15 @@ if scdoc.found() 'waybar-tray.5.scd', 'waybar-states.5.scd', 'waybar-wlr-taskbar.5.scd', + 'waybar-wlr-workspaces.5.scd', 'waybar-bluetooth.5.scd', 'waybar-sndio.5.scd', ] + if (giounix.found() and not get_option('logind').disabled()) + man_files += 'waybar-inhibitor.5.scd' + endif + foreach file : man_files path = '@0@'.format(file) basename = path.split('/')[-1] @@ -380,3 +398,4 @@ if clangtidy.found() '-p', meson.build_root() ] + src_files) endif + diff --git a/meson_options.txt b/meson_options.txt index 81e4468..230a53d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -10,4 +10,6 @@ option('mpd', type: 'feature', value: 'auto', description: 'Enable support for t option('gtk-layer-shell', type: 'feature', value: 'auto', description: 'Use gtk-layer-shell library for popups support') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') option('sndio', type: 'feature', value: 'auto', description: 'Enable support for 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') diff --git a/protocol/ext-workspace-unstable-v1.xml b/protocol/ext-workspace-unstable-v1.xml new file mode 100644 index 0000000..24410b6 --- /dev/null +++ b/protocol/ext-workspace-unstable-v1.xml @@ -0,0 +1,306 @@ + + + + Copyright © 2019 Christopher Billington + Copyright © 2020 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Workspaces, also called virtual desktops, are groups of surfaces. A + compositor with a concept of workspaces may only show some such groups of + surfaces (those of 'active' workspaces) at a time. 'Activating' a + workspace is a request for the compositor to display that workspace's + surfaces as normal, whereas the compositor may hide or otherwise + de-emphasise surfaces that are associated only with 'inactive' workspaces. + Workspaces are grouped by which sets of outputs they correspond to, and + may contain surfaces only from those outputs. In this way, it is possible + for each output to have its own set of workspaces, or for all outputs (or + any other arbitrary grouping) to share workspaces. Compositors may + optionally conceptually arrange each group of workspaces in an + N-dimensional grid. + + The purpose of this protocol is to enable the creation of taskbars and + docks by providing them with a list of workspaces and their properties, + and allowing them to activate and deactivate workspaces. + + After a client binds the zext_workspace_manager_v1, each workspace will be + sent via the workspace event. + + + + + This event is emitted whenever a new workspace group has been created. + + All initial details of the workspace group (workspaces, outputs) will be + sent immediately after this event via the corresponding events in + zext_workspace_group_handle_v1. + + + + + + + The client must send this request after it has finished sending other + requests. The compositor must process a series of requests preceding a + commit request atomically. + + This allows changes to the workspace properties to be seen as atomic, + even if they happen via multiple events, and even if they involve + multiple zext_workspace_handle_v1 objects, for example, deactivating one + workspace and activating another. + + + + + + This event is sent after all changes in all workspace groups have been + sent. + + This allows changes to one or more zext_workspace_group_handle_v1 + properties to be seen as atomic, even if they happen via multiple + events. In particular, an output moving from one workspace group to + another sends an output_enter event and an output_leave event to the two + zext_workspace_group_handle_v1 objects in question. The compositor sends + the done event only after updating the output information in both + workspace groups. + + + + + + This event indicates that the compositor is done sending events to the + zext_workspace_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + Indicates the client no longer wishes to receive events for new + workspace groups. However the compositor may emit further workspace + events, until the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + + A zext_workspace_group_handle_v1 object represents a a workspace group + that is assigned a set of outputs and contains a number of workspaces. + + The set of outputs assigned to the workspace group is conveyed to the client via + output_enter and output_leave events, and its workspaces are conveyed with + workspace events. + + For example, a compositor which has a set of workspaces for each output may + advertise a workspace group (and its workspaces) per output, whereas a compositor + where a workspace spans all outputs may advertise a single workspace group for all + outputs. + + + + + This event is emitted whenever an output is assigned to the workspace + group. + + + + + + + This event is emitted whenever an output is removed from the workspace + group. + + + + + + + This event is emitted whenever a new workspace has been created. + + All initial details of the workspace (name, coordinates, state) will + be sent immediately after this event via the corresponding events in + zext_workspace_handle_v1. + + + + + + + This event means the zext_workspace_group_handle_v1 has been destroyed. + It is guaranteed there won't be any more events for this + zext_workspace_group_handle_v1. The zext_workspace_group_handle_v1 becomes + inert so any requests will be ignored except the destroy request. + + The compositor must remove all workspaces belonging to a workspace group + before removing the workspace group. + + + + + + Request that the compositor create a new workspace with the given name. + + There is no guarantee that the compositor will create a new workspace, + or that the created workspace will have the provided name. + + + + + + + Destroys the zext_workspace_handle_v1 object. + + This request should be called either when the client does not want to + use the workspace object any more or after the remove event to finalize + the destruction of the object. + + + + + + + A zext_workspace_handle_v1 object represents a a workspace that handles a + group of surfaces. + + Each workspace has a name, conveyed to the client with the name event; a + list of states, conveyed to the client with the state event; and + optionally a set of coordinates, conveyed to the client with the + coordinates event. The client may request that the compositor activate or + deactivate the workspace. + + Each workspace can belong to only a single workspace group. + Depepending on the compositor policy, there might be workspaces with + the same name in different workspace groups, but these workspaces are still + separate (e.g. one of them might be active while the other is not). + + + + + This event is emitted immediately after the zext_workspace_handle_v1 is + created and whenever the name of the workspace changes. + + + + + + + This event is used to organize workspaces into an N-dimensional grid + within a workspace group, and if supported, is emitted immediately after + the zext_workspace_handle_v1 is created and whenever the coordinates of + the workspace change. Compositors may not send this event if they do not + conceptually arrange workspaces in this way. If compositors simply + number workspaces, without any geometric interpretation, they may send + 1D coordinates, which clients should not interpret as implying any + geometry. Sending an empty array means that the compositor no longer + orders the workspace geometrically. + + Coordinates have an arbitrary number of dimensions N with an uint32 + position along each dimension. By convention if N > 1, the first + dimension is X, the second Y, the third Z, and so on. The compositor may + chose to utilize these events for a more novel workspace layout + convention, however. No guarantee is made about the grid being filled or + bounded; there may be a workspace at coordinate 1 and another at + coordinate 1000 and none in between. Within a workspace group, however, + workspaces must have unique coordinates of equal dimensionality. + + + + + + + This event is emitted immediately after the zext_workspace_handle_v1 is + created and each time the workspace state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + The different states that a workspace can have. + + + + + + + The workspace is not visible in its workspace group, and clients + attempting to visualize the compositor workspace state should not + display such workspaces. + + + + + + + This event means the zext_workspace_handle_v1 has been destroyed. It is + guaranteed there won't be any more events for this + zext_workspace_handle_v1. The zext_workspace_handle_v1 becomes inert so + any requests will be ignored except the destroy request. + + + + + + Destroys the zext_workspace_handle_v1 object. + + This request should be called either when the client does not want to + use the workspace object any more or after the remove event to finalize + the destruction of the object. + + + + + + Request that this workspace be activated. + + There is no guarantee the workspace will be actually activated, and + behaviour may be compositor-dependent. For example, activating a + workspace may or may not deactivate all other workspaces in the same + group. + + + + + + Request that this workspace be deactivated. + + There is no guarantee the workspace will be actually deactivated. + + + + + + Request that this workspace be removed. + + There is no guarantee the workspace will be actually removed. + + + + diff --git a/protocol/meson.build b/protocol/meson.build index 07d524a..6e82d63 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -27,7 +27,9 @@ client_protocols = [ [wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'], ['wlr-layer-shell-unstable-v1.xml'], ['wlr-foreign-toplevel-management-unstable-v1.xml'], + ['ext-workspace-unstable-v1.xml'], ['river-status-unstable-v1.xml'], + ['river-control-unstable-v1.xml'], ] client_protos_src = [] diff --git a/protocol/river-control-unstable-v1.xml b/protocol/river-control-unstable-v1.xml new file mode 100644 index 0000000..b8faa45 --- /dev/null +++ b/protocol/river-control-unstable-v1.xml @@ -0,0 +1,85 @@ + + + + Copyright 2020 The River Developers + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + + + This interface allows clients to run compositor commands and receive a + success/failure response with output or a failure message respectively. + + Each command is built up in a series of add_argument requests and + executed with a run_command request. The first argument is the command + to be run. + + A complete list of commands should be made available in the man page of + the compositor. + + + + + This request indicates that the client will not use the + river_control object any more. Objects that have been created + through this instance are not affected. + + + + + + Arguments are stored by the server in the order they were sent until + the run_command request is made. + + + + + + + Execute the command built up using the add_argument request for the + given seat. + + + + + + + + + This object is created by the run_command request. Exactly one of the + success or failure events will be sent. This object will be destroyed + by the compositor after one of the events is sent. + + + + + Sent when the command has been successfully received and executed by + the compositor. Some commands may produce output, in which case the + output argument will be a non-empty string. + + + + + + + Sent when the command could not be carried out. This could be due to + sending a non-existent command, no command, not enough arguments, too + many arguments, invalid arguments, etc. + + + + + diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index fa9aa58..1630d97 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -110,6 +110,7 @@ def main(): signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGPIPE, signal.SIG_DFL) for player in manager.props.player_names: if arguments.player is not None and arguments.player != player.name: diff --git a/src/bar.cpp b/src/bar.cpp index a8b230e..9685c7a 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -9,8 +9,13 @@ #include "bar.hpp" #include "client.hpp" #include "factory.hpp" +#include "group.hpp" #include "wlr-layer-shell-unstable-v1-client-protocol.h" +#ifdef HAVE_SWAY +#include "modules/sway/bar.hpp" +#endif + namespace waybar { static constexpr const char* MIN_HEIGHT_MSG = "Requested height: {} is less than the minimum height: {} required by the modules"; @@ -23,6 +28,84 @@ static constexpr const char* BAR_SIZE_MSG = "Bar configured (width: {}, height: static constexpr const char* SIZE_DEFINED = "{} size is defined in the config file so it will stay like that"; +const Bar::bar_mode_map Bar::PRESET_MODES = { // + {"default", + {// Special mode to hold the global bar configuration + .layer = bar_layer::BOTTOM, + .exclusive = true, + .passthrough = false, + .visible = true}}, + {"dock", + {// Modes supported by the sway config; see man sway-bar(5) + .layer = bar_layer::BOTTOM, + .exclusive = true, + .passthrough = false, + .visible = true}}, + {"hide", + {// + .layer = bar_layer::TOP, + .exclusive = false, + .passthrough = false, + .visible = true}}, + {"invisible", + {// + .layer = bar_layer::BOTTOM, + .exclusive = false, + .passthrough = true, + .visible = false}}, + {"overlay", + {// + .layer = bar_layer::TOP, + .exclusive = false, + .passthrough = true, + .visible = true}}}; + +const std::string_view Bar::MODE_DEFAULT = "default"; +const std::string_view Bar::MODE_INVISIBLE = "invisible"; +const std::string_view DEFAULT_BAR_ID = "bar-0"; + +/* Deserializer for enum bar_layer */ +void from_json(const Json::Value& j, bar_layer& l) { + if (j == "bottom") { + l = bar_layer::BOTTOM; + } else if (j == "top") { + l = bar_layer::TOP; + } else if (j == "overlay") { + l = bar_layer::OVERLAY; + } +} + +/* Deserializer for struct bar_mode */ +void from_json(const Json::Value& j, bar_mode& m) { + if (j.isObject()) { + if (auto v = j["layer"]; v.isString()) { + from_json(v, m.layer); + } + if (auto v = j["exclusive"]; v.isBool()) { + m.exclusive = v.asBool(); + } + if (auto v = j["passthrough"]; v.isBool()) { + m.passthrough = v.asBool(); + } + if (auto v = j["visible"]; v.isBool()) { + m.visible = v.asBool(); + } + } +} + +/* Deserializer for JSON Object -> map + * Assumes that all the values in the object are deserializable to the same type. + */ +template ::value>> +void from_json(const Json::Value& j, std::map& m) { + if (j.isObject()) { + for (auto it = j.begin(); it != j.end(); ++it) { + from_json(*it, m[it.key().asString()]); + } + } +} + #ifdef HAVE_GTK_LAYER_SHELL struct GLSSurfaceImpl : public BarSurface, public sigc::trackable { GLSSurfaceImpl(Gtk::Window& window, struct waybar_output& output) : window_{window} { @@ -391,7 +474,6 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) : output(w_output), config(w_config), window{Gtk::WindowType::WINDOW_TOPLEVEL}, - layer_{bar_layer::BOTTOM}, left_(Gtk::ORIENTATION_HORIZONTAL, 0), center_(Gtk::ORIENTATION_HORIZONTAL, 0), right_(Gtk::ORIENTATION_HORIZONTAL, 0), @@ -403,27 +485,6 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) window.get_style_context()->add_class(config["name"].asString()); window.get_style_context()->add_class(config["position"].asString()); - if (config["layer"] == "top") { - layer_ = bar_layer::TOP; - } else if (config["layer"] == "overlay") { - layer_ = bar_layer::OVERLAY; - } - - if (config["exclusive"].isBool()) { - exclusive = config["exclusive"].asBool(); - } else if (layer_ == bar_layer::OVERLAY) { - // swaybar defaults: overlay mode does not reserve an exclusive zone - exclusive = false; - } - - bool passthrough = false; - if (config["passthrough"].isBool()) { - passthrough = config["passthrough"].asBool(); - } else if (layer_ == bar_layer::OVERLAY) { - // swaybar defaults: overlay mode does not accept pointer events. - passthrough = true; - } - auto position = config["position"].asString(); if (position == "right" || position == "left") { @@ -505,15 +566,43 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) surface_impl_ = std::make_unique(window, *output); } - surface_impl_->setLayer(layer_); - surface_impl_->setExclusiveZone(exclusive); surface_impl_->setMargins(margins_); - surface_impl_->setPassThrough(passthrough); surface_impl_->setPosition(position); surface_impl_->setSize(width, height); + /* Read custom modes if available */ + if (auto modes = config.get("modes", {}); modes.isObject()) { + from_json(modes, configured_modes); + } + + /* Update "default" mode with the global bar options */ + from_json(config, configured_modes[MODE_DEFAULT]); + + if (auto mode = config.get("mode", {}); mode.isString()) { + setMode(config["mode"].asString()); + } else { + setMode(MODE_DEFAULT); + } + window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); +#if HAVE_SWAY + if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) { + bar_id = Client::inst()->bar_id; + if (auto id = config["id"]; id.isString()) { + bar_id = id.asString(); + } + if (bar_id.empty()) { + bar_id = DEFAULT_BAR_ID; + } + try { + _ipc_client = std::make_unique(*this); + } catch (const std::exception& exc) { + spdlog::warn("Failed to open bar ipc connection: {}", exc.what()); + } + } +#endif + setupWidgets(); window.show_all(); @@ -528,6 +617,44 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) } } +/* Need to define it here because of forward declared members */ +waybar::Bar::~Bar() = default; + +void waybar::Bar::setMode(const std::string_view& mode) { + using namespace std::literals::string_literals; + + auto style = window.get_style_context(); + /* remove styles added by previous setMode calls */ + style->remove_class("mode-"s + last_mode_); + + auto it = configured_modes.find(mode); + if (it != configured_modes.end()) { + last_mode_ = mode; + style->add_class("mode-"s + last_mode_); + setMode(it->second); + } else { + spdlog::warn("Unknown mode \"{}\" requested", mode); + last_mode_ = MODE_DEFAULT; + style->add_class("mode-"s + last_mode_); + setMode(configured_modes.at(MODE_DEFAULT)); + } +} + +void waybar::Bar::setMode(const struct bar_mode& mode) { + surface_impl_->setLayer(mode.layer); + surface_impl_->setExclusiveZone(mode.exclusive); + surface_impl_->setPassThrough(mode.passthrough); + + if (mode.visible) { + window.get_style_context()->remove_class("hidden"); + window.set_opacity(1); + } else { + window.get_style_context()->add_class("hidden"); + window.set_opacity(0); + } + surface_impl_->commit(); +} + void waybar::Bar::onMap(GdkEventAny*) { /* * Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor). @@ -538,17 +665,7 @@ void waybar::Bar::onMap(GdkEventAny*) { void waybar::Bar::setVisible(bool value) { visible = value; - if (!visible) { - window.get_style_context()->add_class("hidden"); - window.set_opacity(0); - surface_impl_->setLayer(bar_layer::BOTTOM); - } else { - window.get_style_context()->remove_class("hidden"); - window.set_opacity(1); - surface_impl_->setLayer(layer_); - } - surface_impl_->setExclusiveZone(exclusive && visible); - surface_impl_->commit(); + setMode(visible ? MODE_DEFAULT : MODE_INVISIBLE); } void waybar::Bar::toggle() { setVisible(!visible); } @@ -594,19 +711,7 @@ void waybar::Bar::setupAltFormatKeyForModuleList(const char* module_list_name) { } void waybar::Bar::handleSignal(int signal) { - for (auto& module : modules_left_) { - auto* custom = dynamic_cast(module.get()); - if (custom != nullptr) { - custom->refresh(signal); - } - } - for (auto& module : modules_center_) { - auto* custom = dynamic_cast(module.get()); - if (custom != nullptr) { - custom->refresh(signal); - } - } - for (auto& module : modules_right_) { + for (auto& module : modules_all_) { auto* custom = dynamic_cast(module.get()); if (custom != nullptr) { custom->refresh(signal); @@ -614,19 +719,36 @@ void waybar::Bar::handleSignal(int signal) { } } -void waybar::Bar::getModules(const Factory& factory, const std::string& pos) { - if (config[pos].isArray()) { - for (const auto& name : config[pos]) { +void waybar::Bar::getModules(const Factory& factory, const std::string& pos, Gtk::Box* group = nullptr) { + auto module_list = group ? config[pos]["modules"] : config[pos]; + if (module_list.isArray()) { + for (const auto& name : module_list) { try { - auto module = factory.makeModule(name.asString()); - if (pos == "modules-left") { - modules_left_.emplace_back(module); + auto ref = name.asString(); + AModule* module; + + if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) { + auto group_module = new waybar::Group(ref, *this, config[ref]); + getModules(factory, ref, &group_module->box); + module = group_module; + } else { + module = factory.makeModule(ref); } - if (pos == "modules-center") { - modules_center_.emplace_back(module); - } - if (pos == "modules-right") { - modules_right_.emplace_back(module); + + std::shared_ptr module_sp(module); + modules_all_.emplace_back(module_sp); + if (group) { + group->pack_start(*module, false, false); + } else { + if (pos == "modules-left") { + modules_left_.emplace_back(module_sp); + } + if (pos == "modules-center") { + modules_center_.emplace_back(module_sp); + } + if (pos == "modules-right") { + modules_right_.emplace_back(module_sp); + } } module->dp.connect([module, &name] { try { @@ -645,7 +767,11 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos) { auto waybar::Bar::setupWidgets() -> void { window.add(box_); box_.pack_start(left_, false, false); - box_.set_center_widget(center_); + if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) { + box_.set_center_widget(center_); + } else { + box_.pack_start(center_, true, false); + } box_.pack_end(right_, false, false); // Convert to button code for every module that is used. diff --git a/src/client.cpp b/src/client.cpp index 95f5a29..8adbeac 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -199,7 +199,6 @@ int waybar::Client::main(int argc, char *argv[]) { bool show_version = false; std::string config_opt; std::string style_opt; - std::string bar_id; std::string log_level; auto cli = clara::detail::Help(show_help) | clara::detail::Opt(show_version)["-v"]["--version"]("Show version") | diff --git a/src/factory.cpp b/src/factory.cpp index 9836354..900653b 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -30,6 +30,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "wlr/taskbar") { return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]); } +#ifdef USE_EXPERIMENTAL + if (ref == "wlr/workspaces") { + return new waybar::modules::wlr::WorkspaceManager(id, bar_, config_[name]); + } +#endif #endif #ifdef HAVE_RIVER if (ref == "river/tags") { @@ -89,6 +94,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "sndio") { return new waybar::modules::Sndio(id, config_[name]); } +#endif +#ifdef HAVE_GIO_UNIX + if (ref == "inhibitor") { + return new waybar::modules::Inhibitor(id, bar_, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/group.cpp b/src/group.cpp new file mode 100644 index 0000000..9d2188c --- /dev/null +++ b/src/group.cpp @@ -0,0 +1,19 @@ +#include "group.hpp" +#include +#include + +namespace waybar { + +Group::Group(const std::string& name, const Bar& bar, const Json::Value& config) + : AModule(config, name, "", false, false), + box{bar.vertical ? Gtk::ORIENTATION_HORIZONTAL : Gtk::ORIENTATION_VERTICAL, 0} + { +} + +auto Group::update() -> void { + // noop +} + +Group::operator Gtk::Widget&() { return box; } + +} // namespace waybar diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 7c94c45..7e7d742 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -14,17 +14,51 @@ using waybar::modules::waybar_time; waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) - : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true), fixed_time_zone_(false) { + : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true), + current_time_zone_idx_(0), + is_calendar_in_tooltip_(false) +{ if (config_["timezones"].isArray() && !config_["timezones"].empty()) { - time_zone_idx_ = 0; - setTimeZone(config_["timezones"][time_zone_idx_]); - } else { - setTimeZone(config_["timezone"]); + for (const auto& zone_name: config_["timezones"]) { + if (!zone_name.isString() || zone_name.asString().empty()) { + time_zones_.push_back(nullptr); + continue; + } + time_zones_.push_back( + date::locate_zone( + zone_name.asString() + ) + ); + } + } else if (config_["timezone"].isString() && !config_["timezone"].asString().empty()) { + time_zones_.push_back( + date::locate_zone( + config_["timezone"].asString() + ) + ); } - if (fixed_time_zone_) { + + // If all timezones are parsed and no one is good, add nullptr to the timezones vector, to mark that local time should be shown. + if (!time_zones_.size()) { + time_zones_.push_back(nullptr); + } + + if (!is_timezone_fixed()) { spdlog::warn("As using a timezone, some format args may be missing as the date library haven't got a release since 2018."); } + // Check if a particular placeholder is present in the tooltip format, to know what to calculate on update. + if (config_["tooltip-format"].isString()) { + std::string trimmed_format = config_["tooltip-format"].asString(); + trimmed_format.erase(std::remove_if(trimmed_format.begin(), + trimmed_format.end(), + [](unsigned char x){return std::isspace(x);}), + trimmed_format.end()); + if (trimmed_format.find("{" + kCalendarPlaceholder + "}") != std::string::npos) { + is_calendar_in_tooltip_ = true; + } + } + if (config_["locale"].isString()) { locale_ = std::locale(config_["locale"].asString()); } else { @@ -40,53 +74,46 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) }; } +const date::time_zone* waybar::modules::Clock::current_timezone() { + return time_zones_[current_time_zone_idx_] ? time_zones_[current_time_zone_idx_] : date::current_zone(); +} + +bool waybar::modules::Clock::is_timezone_fixed() { + return time_zones_[current_time_zone_idx_] != nullptr; +} + auto waybar::modules::Clock::update() -> void { - if (!fixed_time_zone_) { - // Time zone can change. Be sure to pick that. - time_zone_ = date::current_zone(); - } - - auto now = std::chrono::system_clock::now(); + auto time_zone = current_timezone(); + auto now = std::chrono::system_clock::now(); waybar_time wtime = {locale_, - date::make_zoned(time_zone_, date::floor(now))}; - - std::string text; - if (!fixed_time_zone_) { + date::make_zoned(time_zone, date::floor(now))}; + std::string text = ""; + if (!is_timezone_fixed()) { // As date dep is not fully compatible, prefer fmt tzset(); auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now)); text = fmt::format(format_, localtime); - label_.set_markup(text); } else { text = fmt::format(format_, wtime); - label_.set_markup(text); } + label_.set_markup(text); if (tooltipEnabled()) { if (config_["tooltip-format"].isString()) { - const auto calendar = calendar_text(wtime); - auto tooltip_format = config_["tooltip-format"].asString(); - auto tooltip_text = fmt::format(tooltip_format, wtime, fmt::arg("calendar", calendar)); - label_.set_tooltip_markup(tooltip_text); - } else { - label_.set_tooltip_markup(text); + std::string calendar_lines = ""; + if (is_calendar_in_tooltip_) { + calendar_lines = calendar_text(wtime); + } + auto tooltip_format = config_["tooltip-format"].asString(); + text = fmt::format(tooltip_format, wtime, fmt::arg(kCalendarPlaceholder.c_str(), calendar_lines)); } } + + label_.set_tooltip_markup(text); // Call parent update ALabel::update(); } -bool waybar::modules::Clock::setTimeZone(Json::Value zone_name) { - if (!zone_name.isString() || zone_name.asString().empty()) { - fixed_time_zone_ = false; - return false; - } - - time_zone_ = date::locate_zone(zone_name.asString()); - fixed_time_zone_ = true; - return true; -} - bool waybar::modules::Clock::handleScroll(GdkEventScroll *e) { // defer to user commands if set if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { @@ -97,17 +124,18 @@ bool waybar::modules::Clock::handleScroll(GdkEventScroll *e) { if (dir != SCROLL_DIR::UP && dir != SCROLL_DIR::DOWN) { return true; } - if (!config_["timezones"].isArray() || config_["timezones"].empty()) { + if (time_zones_.size() == 1) { return true; } - auto nr_zones = config_["timezones"].size(); + + auto nr_zones = time_zones_.size(); if (dir == SCROLL_DIR::UP) { - size_t new_idx = time_zone_idx_ + 1; - time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx; + size_t new_idx = current_time_zone_idx_ + 1; + current_time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx; } else { - time_zone_idx_ = time_zone_idx_ == 0 ? nr_zones - 1 : time_zone_idx_ - 1; + current_time_zone_idx_ = current_time_zone_idx_ == 0 ? nr_zones - 1 : current_time_zone_idx_ - 1; } - setTimeZone(config_["timezones"][time_zone_idx_]); + update(); return true; } diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index e63db47..b4e5053 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -45,9 +45,9 @@ auto waybar::modules::Disk::update() -> void { } auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true); - auto used = pow_format((stats.f_blocks - 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_bavail) * 100 / stats.f_blocks; + auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks; auto format = format_; auto state = getState(percentage_used); diff --git a/src/modules/inhibitor.cpp b/src/modules/inhibitor.cpp new file mode 100644 index 0000000..1e3f2d3 --- /dev/null +++ b/src/modules/inhibitor.cpp @@ -0,0 +1,175 @@ +#include "modules/inhibitor.hpp" + +#include +#include +#include + +namespace { + +using DBus = std::unique_ptr; + +auto dbus() -> DBus { + GError *error = nullptr; + GDBusConnection* connection = + g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + + if (error) { + spdlog::error("g_bus_get_sync() failed: {}", error->message); + g_error_free(error); + connection = nullptr; + } + + auto destructor = [](GDBusConnection* connection) { + GError *error = nullptr; + g_dbus_connection_close_sync(connection, nullptr, &error); + if (error) { + spdlog::error( + "g_bus_connection_close_sync failed(): {}", + error->message); + g_error_free(error); + } + }; + + return DBus{connection, destructor}; +} + +auto getLocks(const DBus& bus, const std::string& inhibitors) -> int { + GError *error = nullptr; + GUnixFDList* fd_list; + int handle; + + auto reply = g_dbus_connection_call_with_unix_fd_list_sync(bus.get(), + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + "Inhibit", + g_variant_new( + "(ssss)", + inhibitors.c_str(), + "waybar", + "Asked by user", + "block"), + G_VARIANT_TYPE("(h)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + nullptr, + &fd_list, + nullptr, + &error); + if (error) { + spdlog::error( + "g_dbus_connection_call_with_unix_fd_list_sync() failed: {}", + error->message); + g_error_free(error); + handle = -1; + } else { + gint index; + g_variant_get(reply, "(h)", &index); + g_variant_unref(reply); + handle = g_unix_fd_list_get(fd_list, index, nullptr); + g_object_unref(fd_list); + } + + return handle; +} + +auto checkInhibitor(const std::string& inhibitor) -> const std::string& { + static const auto inhibitors = std::array{ + "idle", + "shutdown", + "sleep", + "handle-power-key", + "handle-suspend-key", + "handle-hibernate-key", + "handle-lid-switch" + }; + + if (std::find(inhibitors.begin(), inhibitors.end(), inhibitor) + == inhibitors.end()) { + throw std::runtime_error("invalid logind inhibitor " + inhibitor); + } + + return inhibitor; +} + +auto getInhibitors(const Json::Value& config) -> std::string { + std::string inhibitors = "idle"; + + if (config["what"].empty()) { + return inhibitors; + } + + if (config["what"].isString()) { + return checkInhibitor(config["what"].asString()); + } + + if (config["what"].isArray()) { + inhibitors = checkInhibitor(config["what"][0].asString()); + for (decltype(config["what"].size()) i = 1; i < config["what"].size(); ++i) { + inhibitors += ":" + checkInhibitor(config["what"][i].asString()); + } + return inhibitors; + } + + return inhibitors; +} + +} + +namespace waybar::modules { + +Inhibitor::Inhibitor(const std::string& id, const Bar& bar, + const Json::Value& config) + : ALabel(config, "inhibitor", id, "{status}", true), + dbus_(::dbus()), + inhibitors_(::getInhibitors(config)) { + event_box_.add_events(Gdk::BUTTON_PRESS_MASK); + event_box_.signal_button_press_event().connect( + sigc::mem_fun(*this, &Inhibitor::handleToggle)); + dp.emit(); +} + +Inhibitor::~Inhibitor() { + if (handle_ != -1) { + ::close(handle_); + } +} + +auto Inhibitor::activated() -> bool { + return handle_ != -1; +} + +auto Inhibitor::update() -> void { + std::string status_text = activated() ? "activated" : "deactivated"; + + label_.get_style_context()->remove_class( + activated() ? "deactivated" : "activated"); + label_.set_markup( + fmt::format(format_, fmt::arg("status", status_text), + fmt::arg("icon", getIcon(0, status_text)))); + label_.get_style_context()->add_class(status_text); + + if (tooltipEnabled()) { + label_.set_tooltip_text(status_text); + } + + return ALabel::update(); +} + +auto Inhibitor::handleToggle(GdkEventButton* const& e) -> bool { + if (e->button == 1) { + if (activated()) { + ::close(handle_); + handle_ = -1; + } else { + handle_ = ::getLocks(dbus_, inhibitors_); + if (handle_ == -1) { + spdlog::error("cannot get inhibitor locks"); + } + } + } + + return ALabel::handleToggle(e); +} + +} // waybar::modules diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 0a7c970..6d27286 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -131,6 +131,9 @@ void waybar::modules::MPD::setLabel() { date = getTag(MPD_TAG_DATE); song_pos = mpd_status_get_song_pos(status_.get()); volume = mpd_status_get_volume(status_.get()); + if (volume < 0) { + volume = 0; + } queue_length = mpd_status_get_queue_length(status_.get()); elapsedTime = std::chrono::seconds(mpd_status_get_elapsed_time(status_.get())); totalTime = std::chrono::seconds(mpd_status_get_total_time(status_.get())); diff --git a/src/modules/network.cpp b/src/modules/network.cpp index d5f8012..4fd8e3e 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -1,87 +1,77 @@ -#include "modules/network.hpp" +#include #include #include -#include -#include + #include +#include +#include #include + +#include "modules/network.hpp" #include "util/format.hpp" #ifdef WANT_RFKILL #include "util/rfkill.hpp" #endif namespace { - using namespace waybar::util; - -constexpr const char *NETSTAT_FILE = - "/proc/net/netstat"; // std::ifstream does not take std::string_view as param -constexpr std::string_view BANDWIDTH_CATEGORY = "IpExt"; -constexpr std::string_view BANDWIDTH_DOWN_TOTAL_KEY = "InOctets"; -constexpr std::string_view BANDWIDTH_UP_TOTAL_KEY = "OutOctets"; constexpr const char *DEFAULT_FORMAT = "{ifname}"; - -std::ifstream netstat(NETSTAT_FILE); -std::optional read_netstat(std::string_view category, std::string_view key) { - if (!netstat) { - spdlog::warn("Failed to open netstat file {}", NETSTAT_FILE); - return {}; - } - netstat.seekg(std::ios_base::beg); - - // finding corresponding line (category) - // looks into the file for the first line starting by the 'category' string - auto starts_with = [](const std::string &str, std::string_view start) { - return start == std::string_view{str.data(), std::min(str.size(), start.size())}; - }; - - std::string read; - while (std::getline(netstat, read) && !starts_with(read, category)) - ; - if (!starts_with(read, category)) { - spdlog::warn("Category '{}' not found in netstat file {}", category, NETSTAT_FILE); - return {}; - } - - // finding corresponding column (key) - // looks into the fetched line for the first word (space separated) equal to 'key' - int index = 0; - auto r_it = read.begin(); - auto k_it = key.begin(); - while (k_it != key.end() && r_it != read.end()) { - if (*r_it != *k_it) { - r_it = std::find(r_it, read.end(), ' '); - if (r_it != read.end()) { - ++r_it; - } - k_it = key.begin(); - ++index; - } else { - ++r_it; - ++k_it; - } - } - - if (r_it == read.end() && k_it != key.end()) { - spdlog::warn( - "Key '{}' not found in category '{}' of netstat file {}", key, category, NETSTAT_FILE); - return {}; - } - - // finally accessing value - // accesses the line right under the fetched one - std::getline(netstat, read); - assert(starts_with(read, category)); - std::istringstream iss(read); - while (index--) { - std::getline(iss, read, ' '); - } - unsigned long long value; - iss >> value; - return value; -} } // namespace +constexpr const char *NETDEV_FILE = + "/proc/net/dev"; // std::ifstream does not take std::string_view as param +std::optional> +waybar::modules::Network::readBandwidthUsage() { + std::ifstream netdev(NETDEV_FILE); + if (!netdev) { + spdlog::warn("Failed to open netdev file {}", NETDEV_FILE); + return {}; + } + + std::string line; + // skip the headers (first two lines) + std::getline(netdev, line); + std::getline(netdev, line); + + unsigned long long receivedBytes = 0ull; + unsigned long long transmittedBytes = 0ull; + while (std::getline(netdev, line)) { + std::istringstream iss(line); + + std::string ifacename; + iss >> ifacename; // ifacename contains "eth0:" + ifacename.pop_back(); // remove trailing ':' + if (!checkInterface(ifacename)) { + continue; + } + + // The rest of the line consists of whitespace separated counts divided + // into two groups (receive and transmit). Each group has the following + // columns: bytes, packets, errs, drop, fifo, frame, compressed, multicast + // + // We only care about the bytes count, so we'll just ignore the 7 other + // columns. + unsigned long long r = 0ull; + unsigned long long t = 0ull; + // Read received bytes + iss >> r; + // Skip all the other columns in the received group + for (int colsToSkip = 7; colsToSkip > 0; colsToSkip--) { + // skip whitespace between columns + while (iss.peek() == ' ') { iss.ignore(); } + // skip the irrelevant column + while (iss.peek() != ' ') { iss.ignore(); } + } + // Read transmit bytes + iss >> t; + + receivedBytes += r; + transmittedBytes += t; + } + + return {{receivedBytes, transmittedBytes}}; +} + waybar::modules::Network::Network(const std::string &id, const Json::Value &config) : ALabel(config, "network", id, DEFAULT_FORMAT, 60), ifid_(-1), @@ -98,7 +88,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf #ifdef WANT_RFKILL rfkill_{RFKILL_TYPE_WLAN}, #endif - frequency_(0) { + frequency_(0.0) { // Start with some "text" in the module's label_, update() will then // update it. Since the text should be different, update() will be able @@ -106,17 +96,12 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf // the module start with no text, but the the event_box_ is shown. label_.set_markup(""); - auto down_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_DOWN_TOTAL_KEY); - auto up_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_UP_TOTAL_KEY); - if (down_octets) { - bandwidth_down_total_ = *down_octets; + auto bandwidth = readBandwidthUsage(); + if (bandwidth.has_value()) { + bandwidth_down_total_ = (*bandwidth).first; + bandwidth_up_total_ = (*bandwidth).second; } else { bandwidth_down_total_ = 0; - } - - if (up_octets) { - bandwidth_up_total_ = *up_octets; - } else { bandwidth_up_total_ = 0; } @@ -303,20 +288,21 @@ const std::string waybar::modules::Network::getNetworkState() const { auto waybar::modules::Network::update() -> void { std::lock_guard lock(mutex_); std::string tooltip_format; - auto down_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_DOWN_TOTAL_KEY); - auto up_octets = read_netstat(BANDWIDTH_CATEGORY, BANDWIDTH_UP_TOTAL_KEY); - unsigned long long bandwidth_down = 0; - if (down_octets) { - bandwidth_down = *down_octets - bandwidth_down_total_; - bandwidth_down_total_ = *down_octets; + auto bandwidth = readBandwidthUsage(); + auto bandwidth_down = 0ull; + auto bandwidth_up = 0ull; + if (bandwidth.has_value()) { + auto down_octets = (*bandwidth).first; + auto up_octets = (*bandwidth).second; + + bandwidth_down = down_octets - bandwidth_down_total_; + bandwidth_down_total_ = down_octets; + + bandwidth_up = up_octets - bandwidth_up_total_; + bandwidth_up_total_ = up_octets; } - unsigned long long bandwidth_up = 0; - if (up_octets) { - bandwidth_up = *up_octets - bandwidth_up_total_; - bandwidth_up_total_ = *up_octets; - } if (!alt_) { auto state = getNetworkState(); if (!state_.empty() && label_.get_style_context()->has_class(state_)) { @@ -345,12 +331,13 @@ auto waybar::modules::Network::update() -> void { fmt::arg("essid", essid_), fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), + fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), - fmt::arg("frequency", frequency_), + fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")), @@ -374,12 +361,13 @@ auto waybar::modules::Network::update() -> void { fmt::arg("essid", essid_), fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), + fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), - fmt::arg("frequency", frequency_), + fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), @@ -417,7 +405,8 @@ void waybar::modules::Network::clearIface() { cidr_ = 0; signal_strength_dbm_ = 0; signal_strength_ = 0; - frequency_ = 0; + signal_strength_app_.clear(); + frequency_ = 0.0; } int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { @@ -484,7 +473,8 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { net->essid_.clear(); net->signal_strength_dbm_ = 0; net->signal_strength_ = 0; - net->frequency_ = 0; + net->signal_strength_app_.clear(); + net->frequency_ = 0.0; } } net->carrier_ = carrier.value(); @@ -802,13 +792,30 @@ void waybar::modules::Network::parseSignal(struct nlattr **bss) { if (bss[NL80211_BSS_SIGNAL_MBM] != nullptr) { // signalstrength in dBm from mBm signal_strength_dbm_ = nla_get_s32(bss[NL80211_BSS_SIGNAL_MBM]) / 100; + // WiFi-hardware usually operates in the range -90 to -30dBm. - // WiFi-hardware usually operates in the range -90 to -20dBm. - const int hardwareMax = -20; + // If a signal is too strong, it can overwhelm receiving circuity that is designed + // to pick up and process a certain signal level. The following percentage is scaled to + // punish signals that are too strong (>= -45dBm) or too weak (<= -45 dBm). + const int hardwareOptimum = -45; const int hardwareMin = -90; const int strength = - ((signal_strength_dbm_ - hardwareMin) / double{hardwareMax - hardwareMin}) * 100; - signal_strength_ = std::clamp(strength, 0, 100); + 100 - ((abs(signal_strength_dbm_ - hardwareOptimum) / double{hardwareOptimum - hardwareMin}) * 100); + signal_strength_ = std::clamp(strength, 0, 100); + + if (signal_strength_dbm_ >= -50) { + signal_strength_app_ = "Great Connectivity"; + } else if (signal_strength_dbm_ >= -60) { + signal_strength_app_ = "Good Connectivity"; + } else if (signal_strength_dbm_ >= -67) { + signal_strength_app_ = "Streaming"; + } else if (signal_strength_dbm_ >= -70) { + signal_strength_app_ = "Web Surfing"; + } else if (signal_strength_dbm_ >= -80) { + signal_strength_app_ = "Basic Connectivity"; + } else { + signal_strength_app_ = "Poor Connectivity"; + } } if (bss[NL80211_BSS_SIGNAL_UNSPEC] != nullptr) { signal_strength_ = nla_get_u8(bss[NL80211_BSS_SIGNAL_UNSPEC]); @@ -817,8 +824,8 @@ void waybar::modules::Network::parseSignal(struct nlattr **bss) { void waybar::modules::Network::parseFreq(struct nlattr **bss) { if (bss[NL80211_BSS_FREQUENCY] != nullptr) { - // in MHz - frequency_ = nla_get_u32(bss[NL80211_BSS_FREQUENCY]); + // in GHz + frequency_ = (double) nla_get_u32(bss[NL80211_BSS_FREQUENCY]) / 1000; } } diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index cf42780..3c32060 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -79,6 +79,13 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { if (dir == SCROLL_DIR::NONE) { return true; } + if (config_["reverse-scrolling"].asInt() == 1){ + if (dir == SCROLL_DIR::UP) { + dir = SCROLL_DIR::DOWN; + } else if (dir == SCROLL_DIR::DOWN) { + dir = SCROLL_DIR::UP; + } + } double volume_tick = static_cast(PA_VOLUME_NORM) / 100; pa_volume_t change = volume_tick; pa_cvolume pa_volume = pa_volume_; @@ -211,7 +218,7 @@ static const std::array ports = { }; const std::vector waybar::modules::Pulseaudio::getPulseIcon() const { - std::vector res = {default_source_name_}; + std::vector res = {current_sink_name_, default_source_name_}; std::string nameLC = port_name_ + form_factor_; std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower); for (auto const &port : ports) { diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index 2628af2..87655be 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -7,7 +7,6 @@ #include "client.hpp" #include "modules/river/tags.hpp" -#include "river-status-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h" namespace waybar::modules::river { @@ -33,6 +32,23 @@ static const zriver_output_status_v1_listener output_status_listener_impl{ .urgent_tags = listen_urgent_tags, }; +static void listen_command_success(void *data, + struct zriver_command_callback_v1 *zriver_command_callback_v1, + const char *output) { + // Do nothing but keep listener to avoid crashing when command was successful +} + +static void listen_command_failure(void *data, + struct zriver_command_callback_v1 *zriver_command_callback_v1, + const char *output) { + spdlog::error("failure when selecting/toggling tags {}", output); +} + +static const zriver_command_callback_v1_listener command_callback_listener_impl { + .success = listen_command_success, + .failure = listen_command_failure, +}; + static void handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) { @@ -43,18 +59,33 @@ static void handle_global(void *data, struct wl_registry *registry, uint32_t nam static_cast(data)->status_manager_ = static_cast( wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version)); } + + if (std::strcmp(interface, zriver_control_v1_interface.name) == 0) { + version = std::min(version, 1); + static_cast(data)->control_ = static_cast( + wl_registry_bind(registry, name, &zriver_control_v1_interface, version)); + } + + if (std::strcmp(interface, wl_seat_interface.name) == 0) { + version = std::min(version, 1); + static_cast(data)->seat_ = static_cast( + wl_registry_bind(registry, name, &wl_seat_interface, version)); + } } static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { /* Ignore event */ } + static const wl_registry_listener registry_listener_impl = {.global = handle_global, .global_remove = handle_global_remove}; Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &config) : waybar::AModule(config, "tags", id, false, false), status_manager_{nullptr}, + control_{nullptr}, + seat_{nullptr}, bar_(bar), box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, output_status_{nullptr} { @@ -68,6 +99,14 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con return; } + if (!control_) { + spdlog::error("river_control_v1 not advertised"); + } + + if (!seat_) { + spdlog::error("wl_seat not advertised"); + } + box_.set_name("tags"); if (!id.empty()) { box_.get_style_context()->add_class(id); @@ -89,11 +128,17 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con } } + uint32_t i = 1; for (const auto &tag_label : tag_labels) { Gtk::Button &button = buttons_.emplace_back(tag_label); button.set_relief(Gtk::RELIEF_NONE); box_.pack_start(button, false, false, 0); + if (!config_["disable-click"].asBool()) { + button.signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Tags::handle_primary_clicked), i)); + button.signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), i)); + } button.show(); + i <<= 1; } struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); @@ -107,6 +152,31 @@ Tags::~Tags() { if (output_status_) { zriver_output_status_v1_destroy(output_status_); } + + if (control_) { + zriver_control_v1_destroy(control_); + } +} + +void Tags::handle_primary_clicked(uint32_t tag) { + // Send river command to select tag on left mouse click + zriver_command_callback_v1 *callback; + zriver_control_v1_add_argument(control_, "set-focused-tags"); + zriver_control_v1_add_argument(control_, std::to_string(tag).c_str()); + callback = zriver_control_v1_run_command(control_, seat_); + zriver_command_callback_v1_add_listener(callback, &command_callback_listener_impl, nullptr); +} + +bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) { + if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) { + // Send river command to toggle tag on right mouse click + zriver_command_callback_v1 *callback; + zriver_control_v1_add_argument(control_, "toggle-focused-tags"); + zriver_control_v1_add_argument(control_, std::to_string(tag).c_str()); + callback = zriver_control_v1_run_command(control_, seat_); + zriver_command_callback_v1_add_listener(callback, &command_callback_listener_impl, nullptr); + } + return true; } void Tags::handle_focused_tags(uint32_t tags) { diff --git a/src/modules/sway/bar.cpp b/src/modules/sway/bar.cpp new file mode 100644 index 0000000..6ad74af --- /dev/null +++ b/src/modules/sway/bar.cpp @@ -0,0 +1,107 @@ +#include "modules/sway/bar.hpp" + +#include +#include + +#include + +#include "bar.hpp" +#include "modules/sway/ipc/ipc.hpp" + +namespace waybar::modules::sway { + +BarIpcClient::BarIpcClient(waybar::Bar& bar) : bar_{bar} { + { + sigc::connection handle = + ipc_.signal_cmd.connect(sigc::mem_fun(*this, &BarIpcClient::onInitialConfig)); + ipc_.sendCmd(IPC_GET_BAR_CONFIG, bar_.bar_id); + + handle.disconnect(); + } + + signal_config_.connect(sigc::mem_fun(*this, &BarIpcClient::onConfigUpdate)); + signal_visible_.connect(sigc::mem_fun(*this, &BarIpcClient::onVisibilityUpdate)); + + ipc_.subscribe(R"(["bar_state_update", "barconfig_update"])"); + ipc_.signal_event.connect(sigc::mem_fun(*this, &BarIpcClient::onIpcEvent)); + // Launch worker + ipc_.setWorker([this] { + try { + ipc_.handleEvent(); + } catch (const std::exception& e) { + spdlog::error("BarIpcClient::handleEvent {}", e.what()); + } + }); +} + +struct swaybar_config parseConfig(const Json::Value& payload) { + swaybar_config conf; + if (auto id = payload["id"]; id.isString()) { + conf.id = id.asString(); + } + if (auto mode = payload["mode"]; mode.isString()) { + conf.mode = mode.asString(); + } + if (auto hs = payload["hidden_state"]; hs.isString()) { + conf.hidden_state = hs.asString(); + } + return conf; +} + +void BarIpcClient::onInitialConfig(const struct Ipc::ipc_response& res) { + auto payload = parser_.parse(res.payload); + if (auto success = payload.get("success", true); !success.asBool()) { + auto err = payload.get("error", "Unknown error"); + throw std::runtime_error(err.asString()); + } + auto config = parseConfig(payload); + onConfigUpdate(config); +} + +void BarIpcClient::onIpcEvent(const struct Ipc::ipc_response& res) { + try { + auto payload = parser_.parse(res.payload); + if (auto id = payload["id"]; id.isString() && id.asString() != bar_.bar_id) { + spdlog::trace("swaybar ipc: ignore event for {}", id.asString()); + return; + } + if (payload.isMember("visible_by_modifier")) { + // visibility change for hidden bar + signal_visible_(payload["visible_by_modifier"].asBool()); + } else { + // configuration update + auto config = parseConfig(payload); + signal_config_(std::move(config)); + } + } catch (const std::exception& e) { + spdlog::error("BarIpcClient::onEvent {}", e.what()); + } +} + +void BarIpcClient::onConfigUpdate(const swaybar_config& config) { + spdlog::info("config update for {}: id {}, mode {}, hidden_state {}", + bar_.bar_id, + config.id, + config.mode, + config.hidden_state); + bar_config_ = config; + update(); +} + +void BarIpcClient::onVisibilityUpdate(bool visible_by_modifier) { + spdlog::debug("visiblity update for {}: {}", bar_.bar_id, visible_by_modifier); + visible_by_modifier_ = visible_by_modifier; + update(); +} + +void BarIpcClient::update() { + bool visible = visible_by_modifier_; + if (bar_config_.mode == "invisible") { + visible = false; + } else if (bar_config_.mode != "hide" || bar_config_.hidden_state != "hide") { + visible = true; + } + bar_.setMode(visible ? bar_config_.mode : Bar::MODE_INVISIBLE); +} + +} // namespace waybar::modules::sway diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index 186fa4b..73a64c3 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -99,7 +99,8 @@ auto Language::update() -> void { fmt::arg("short", layout_.short_name), fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name), - fmt::arg("variant", layout_.variant))); + fmt::arg("variant", layout_.variant), + fmt::arg("flag", layout_.country_flag()))); label_.set_markup(display_layout); if (tooltipEnabled()) { if (tooltip_format_ != "") { @@ -107,7 +108,8 @@ auto Language::update() -> void { fmt::arg("short", layout_.short_name), fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name), - fmt::arg("variant", layout_.variant))); + fmt::arg("variant", layout_.variant), + fmt::arg("flag", layout_.country_flag()))); label_.set_tooltip_markup(tooltip_display_layout); } else { label_.set_tooltip_markup(display_layout); @@ -212,4 +214,15 @@ Language::XKBContext::~XKBContext() { rxkb_context_unref(context_); delete layout_; } + +std::string Language::Layout::country_flag() const { + if (short_name.size() != 2) return ""; + unsigned char result[] = "\xf0\x9f\x87\x00\xf0\x9f\x87\x00"; + result[3] = short_name[0] + 0x45; + result[7] = short_name[1] + 0x45; + // Check if both emojis are in A-Z symbol bounds + if (result[3] < 0xa6 || result[3] > 0xbf) return ""; + if (result[7] < 0xa6 || result[7] > 0xbf) return ""; + return std::string{reinterpret_cast(result)}; +} } // namespace waybar::modules::sway diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 43dcf33..074d145 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -98,6 +99,7 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) { Json::Value v; v["name"] = p_w_name; v["target_output"] = bar_.output->name; + v["num"] = convertWorkspaceNameToNum(p_w_name); workspaces_.emplace_back(std::move(v)); break; } @@ -107,57 +109,59 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) { Json::Value v; v["name"] = p_w_name; v["target_output"] = ""; + v["num"] = convertWorkspaceNameToNum(p_w_name); workspaces_.emplace_back(std::move(v)); } } } - // config option to sort numeric workspace names before others - bool config_numeric_first = config_["numeric-first"].asBool(); - + // sway has a defined ordering of workspaces that should be preserved in + // the representation displayed by waybar to ensure that commands such + // as "workspace prev" or "workspace next" make sense when looking at + // the workspace representation in the bar. + // Due to waybar's own feature of persistent workspaces unknown to sway, + // custom sorting logic is necessary to make these workspaces appear + // naturally in the list of workspaces without messing up sway's + // sorting. For this purpose, a custom numbering property is created + // that preserves the order provided by sway while inserting numbered + // persistent workspaces at their natural positions. + // + // All of this code assumes that sway provides numbered workspaces first + // and other workspaces are sorted by their creation time. + // + // In a first pass, the maximum "num" value is computed to enqueue + // unnumbered workspaces behind numbered ones when computing the sort + // attribute. + int max_num = -1; + for (auto & workspace : workspaces_) { + max_num = std::max(workspace["num"].asInt(), max_num); + } + for (auto & workspace : workspaces_) { + auto workspace_num = workspace["num"].asInt(); + if (workspace_num > -1) { + workspace["sort"] = workspace_num; + } else { + workspace["sort"] = ++max_num; + } + } std::sort(workspaces_.begin(), workspaces_.end(), - [config_numeric_first](const Json::Value &lhs, const Json::Value &rhs) { - // the "num" property (integer type): - // The workspace number or -1 for workspaces that do - // not start with a number. - // We could rely on sway providing this property: - // - // auto l = lhs["num"].asInt(); - // auto r = rhs["num"].asInt(); - // - // We cannot rely on the "num" property as provided by sway - // via IPC, because persistent workspace might not exist in - // sway's view. However, we need this property also for - // not-yet created persistent workspace. As such, we simply - // duplicate sway's logic of assigning the "num" property - // into waybar (see convertWorkspaceNameToNum). This way the - // sorting should work out even when we include workspaces - // that do not currently exist. + [](const Json::Value &lhs, const Json::Value &rhs) { auto lname = lhs["name"].asString(); auto rname = rhs["name"].asString(); - int l = convertWorkspaceNameToNum(lname); - int r = convertWorkspaceNameToNum(rname); + int l = lhs["sort"].asInt(); + int r = rhs["sort"].asInt(); if (l == r) { - // in case both integers are the same, lexicographical - // sort. This also covers the case when both don't have a - // number (i.e., l == r == -1). + // In case both integers are the same, lexicographical + // sort. The code above already ensure that this will only + // happend in case of explicitly numbered workspaces. return lname < rname; } - // one of the workspaces doesn't begin with a number, so - // num is -1. - if (l < 0 || r < 0) { - if (config_numeric_first) { - return r < 0; - } - return l < 0; - } - - // both workspaces have a "num" so let's just compare those return l < r; }); + } dp.emit(); } catch (const std::exception &e) { diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 932a95e..ddc360b 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -11,7 +11,9 @@ #include #include #include +#include +#include #include #include @@ -86,8 +88,7 @@ static Glib::RefPtr load_icon_from_file(std::string icon_path, int } } -/* Method 1 - get the correct icon name from the desktop file */ -static std::string get_from_desktop_app_info(const std::string &app_id) +static Glib::RefPtr get_app_info_by_name(const std::string& app_id) { static std::vector prefixes = search_prefix(); @@ -103,33 +104,29 @@ static std::string get_from_desktop_app_info(const std::string &app_id) ".desktop" }; - Glib::RefPtr app_info; + for (auto& prefix : prefixes) { + for (auto& folder : app_folders) { + for (auto& suffix : suffixes) { + auto app_info_ = Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix); + if (!app_info_) { + continue; + } - for (auto& prefix : prefixes) - for (auto& folder : app_folders) - for (auto& suffix : suffixes) - if (!app_info) - app_info = Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix); + return app_info_; + } + } + } - if (app_info && app_info->get_icon()) - return app_info->get_icon()->to_string(); - - return ""; + return {}; } -/* Method 2 - use the app_id and check whether there is an icon with this name in the icon theme */ -static std::string get_from_icon_theme(const Glib::RefPtr& icon_theme, - const std::string &app_id) +Glib::RefPtr get_desktop_app_info(const std::string &app_id) { - if (icon_theme->lookup_icon(app_id, 24)) - return app_id; + auto app_info = get_app_info_by_name(app_id); + if (app_info) { + return app_info; + } - return ""; -} - -/* Method 3 - as last resort perform a search for most appropriate desktop info file */ -static std::string get_from_desktop_app_info_search(const std::string &app_id) -{ std::string desktop_file = ""; gchar*** desktop_list = g_desktop_app_info_search(app_id.c_str()); @@ -151,65 +148,84 @@ static std::string get_from_desktop_app_info_search(const std::string &app_id) } g_free(desktop_list); - return get_from_desktop_app_info(desktop_file); + return get_app_info_by_name(desktop_file); } -static bool image_load_icon(Gtk::Image& image, const Glib::RefPtr& icon_theme, - const std::string &app_id_list, int size) -{ +void Task::set_app_info_from_app_id_list(const std::string& app_id_list) { std::string app_id; std::istringstream stream(app_id_list); - bool found = false; /* Wayfire sends a list of app-id's in space separated format, other compositors * send a single app-id, but in any case this works fine */ while (stream >> app_id) { + app_info_ = get_desktop_app_info(app_id); + if (app_info_) { + return; + } + + auto lower_app_id = app_id; + std::transform(lower_app_id.begin(), lower_app_id.end(), lower_app_id.begin(), + [](char c){ return std::tolower(c); }); + app_info_ = get_desktop_app_info(lower_app_id); + if (app_info_) { + return; + } + size_t start = 0, end = app_id.size(); start = app_id.rfind(".", end); std::string app_name = app_id.substr(start+1, app_id.size()); + app_info_ = get_desktop_app_info(app_name); + if (app_info_) { + return; + } - auto lower_app_id = app_id; - std::transform(lower_app_id.begin(), lower_app_id.end(), lower_app_id.begin(), - [](char c){ return std::tolower(c); }); + start = app_id.find("-"); + app_name = app_id.substr(0, start); + app_info_ = get_desktop_app_info(app_name); + } +} - std::string icon_name = get_from_icon_theme(icon_theme, app_id); +static std::string get_icon_name_from_icon_theme(const Glib::RefPtr& icon_theme, + const std::string &app_id) +{ + if (icon_theme->lookup_icon(app_id, 24)) + return app_id; - if (icon_name.empty()) - icon_name = get_from_icon_theme(icon_theme, lower_app_id); - if (icon_name.empty()) - icon_name = get_from_icon_theme(icon_theme, app_name); - if (icon_name.empty()) - icon_name = get_from_desktop_app_info(app_id); - if (icon_name.empty()) - icon_name = get_from_desktop_app_info(lower_app_id); - if (icon_name.empty()) - icon_name = get_from_desktop_app_info(app_name); - if (icon_name.empty()) - icon_name = get_from_desktop_app_info_search(app_id); + return ""; +} - if (icon_name.empty()) - icon_name = "unknown"; +bool Task::image_load_icon(Gtk::Image& image, const Glib::RefPtr& icon_theme, Glib::RefPtr app_info, int size) +{ + std::string ret_icon_name = "unknown"; + if (app_info) { + std::string icon_name = get_icon_name_from_icon_theme(icon_theme, app_info->get_startup_wm_class()); + if (!icon_name.empty()) { + ret_icon_name = icon_name; + } else { + if (app_info->get_icon()) { + ret_icon_name = app_info->get_icon()->to_string(); + } + } + } - Glib::RefPtr pixbuf; + Glib::RefPtr pixbuf; - try { - pixbuf = icon_theme->load_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); - } catch(...) { - if (Glib::file_test(icon_name, Glib::FILE_TEST_EXISTS)) - pixbuf = load_icon_from_file(icon_name, size); - else - pixbuf = {}; - } + try { + pixbuf = icon_theme->load_icon(ret_icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + } catch(...) { + if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS)) + pixbuf = load_icon_from_file(ret_icon_name, size); + else + pixbuf = {}; + } - if (pixbuf) { - image.set(pixbuf); - found = true; - break; - } - } + if (pixbuf) { + image.set(pixbuf); + return true; + } - return found; + return false; } /* Task class implementation */ @@ -276,8 +292,7 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, struct zwlr_foreign_toplevel_handle_v1 *tl_handle, struct wl_seat *seat) : bar_{bar}, config_{config}, tbar_{tbar}, handle_{tl_handle}, seat_{seat}, id_{global_id++}, - content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, - button_visible_{false}, ignored_{false} + content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0} { zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); @@ -290,13 +305,15 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, content_.show(); button_.add(content_); - with_icon_ = false; format_before_.clear(); format_after_.clear(); if (config_["format"].isString()) { /* The user defined a format string, use it */ auto format = config_["format"].asString(); + if (format.find("{name}") != std::string::npos) { + with_name_ = true; + } auto icon_pos = format.find("{icon}"); if (icon_pos == 0) { @@ -377,13 +394,12 @@ std::string Task::state_string(bool shortened) const void Task::handle_title(const char *title) { title_ = title; + hide_if_ignored(); } -void Task::handle_app_id(const char *app_id) +void Task::hide_if_ignored() { - app_id_ = app_id; - - if (tbar_->ignore_list().count(app_id)) { + if (tbar_->ignore_list().count(app_id_) || tbar_->ignore_list().count(title_)) { ignored_ = true; if (button_visible_) { auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); @@ -397,14 +413,35 @@ void Task::handle_app_id(const char *app_id) handle_output_enter(output); } } +} - if (!with_icon_) +void Task::handle_app_id(const char *app_id) +{ + app_id_ = app_id; + hide_if_ignored(); + + auto ids_replace_map = tbar_->app_ids_replace_map(); + if (ids_replace_map.count(app_id_)) { + auto replaced_id = ids_replace_map[app_id_]; + spdlog::debug(fmt::format("Task ({}) [{}] app_id was replaced with {}", id_, app_id_, replaced_id)); + app_id_ = replaced_id; + } + + if (!with_icon_ && !with_name_) { return; + } + + set_app_info_from_app_id_list(app_id_); + name_ = app_info_ ? app_info_->get_display_name() : app_id; + + if (!with_icon_) { + return; + } int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; bool found = false; for (auto& icon_theme : tbar_->icon_themes()) { - if (image_load_icon(icon_, icon_theme, app_id_, icon_size)) { + if (image_load_icon(icon_, icon_theme, app_info_, icon_size)) { found = true; break; } @@ -418,13 +455,13 @@ void Task::handle_app_id(const char *app_id) void Task::handle_output_enter(struct wl_output *output) { - spdlog::debug("{} entered output {}", repr(), (void*)output); - if (ignored_) { spdlog::debug("{} is ignored", repr()); return; } + spdlog::debug("{} entered output {}", repr(), (void*)output); + if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) { /* The task entered the output of the current bar make the button visible */ tbar_->add_button(button_); @@ -560,14 +597,17 @@ void Task::update() { bool markup = config_["markup"].isBool() ? config_["markup"].asBool() : false; std::string title = title_; + std::string name = name_; std::string app_id = app_id_; if (markup) { title = Glib::Markup::escape_text(title); + name = Glib::Markup::escape_text(name); app_id = Glib::Markup::escape_text(app_id); } if (!format_before_.empty()) { auto txt = fmt::format(format_before_, fmt::arg("title", title), + fmt::arg("name", name), fmt::arg("app_id", app_id), fmt::arg("state", state_string()), fmt::arg("short_state", state_string(true)) @@ -581,6 +621,7 @@ void Task::update() if (!format_after_.empty()) { auto txt = fmt::format(format_after_, fmt::arg("title", title), + fmt::arg("name", name), fmt::arg("app_id", app_id), fmt::arg("state", state_string()), fmt::arg("short_state", state_string(true)) @@ -595,6 +636,7 @@ void Task::update() if (!format_tooltip_.empty()) { auto txt = fmt::format(format_tooltip_, fmt::arg("title", title), + fmt::arg("name", name), fmt::arg("app_id", app_id), fmt::arg("state", state_string()), fmt::arg("short_state", state_string(true)) @@ -722,6 +764,15 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu } } + // Load app_id remappings + if (config_["app_ids-mapping"].isObject()) { + const Json::Value& mapping = config_["app_ids-mapping"]; + const std::vector app_ids = config_["app_ids-mapping"].getMemberNames(); + for (auto& app_id : app_ids) { + app_ids_replace_map_.emplace(app_id, mapping[app_id].asString()); + } + } + icon_themes_.push_back(Gtk::IconTheme::get_default()); } @@ -853,10 +904,10 @@ bool Taskbar::all_outputs() const return config_["all-outputs"].isBool() && config_["all-outputs"].asBool(); } -std::vector> Taskbar::icon_themes() const -{ - return icon_themes_; -} -const std::unordered_set &Taskbar::ignore_list() const { return ignore_list_; } +const std::vector>& Taskbar::icon_themes() const { return icon_themes_; } + +const std::unordered_set& Taskbar::ignore_list() const { return ignore_list_; } + +const std::map& Taskbar::app_ids_replace_map() const { return app_ids_replace_map_; } } /* namespace waybar::modules::wlr */ diff --git a/src/modules/wlr/workspace_manager.cpp b/src/modules/wlr/workspace_manager.cpp new file mode 100644 index 0000000..bacef35 --- /dev/null +++ b/src/modules/wlr/workspace_manager.cpp @@ -0,0 +1,472 @@ +#include "modules/wlr/workspace_manager.hpp" + +#include +#include +#include + +#include +#include +#include + +#include "gtkmm/widget.h" +#include "modules/wlr/workspace_manager_binding.hpp" + +namespace waybar::modules::wlr { + +uint32_t WorkspaceGroup::workspace_global_id = 0; +uint32_t WorkspaceManager::group_global_id = 0; +std::map Workspace::icons_map_; + +WorkspaceManager::WorkspaceManager(const std::string &id, const waybar::Bar &bar, + const Json::Value &config) + : waybar::AModule(config, "workspaces", id, false, false), + bar_(bar), + box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) { + auto config_sort_by_name = config_["sort-by-name"]; + if (config_sort_by_name.isBool()) { + sort_by_name_ = config_sort_by_name.asBool(); + } + + auto config_sort_by_coordinates = config_["sort-by-coordinates"]; + if (config_sort_by_coordinates.isBool()) { + sort_by_coordinates_ = config_sort_by_coordinates.asBool(); + } + + auto config_all_outputs = config_["all-outputs"]; + if (config_all_outputs.isBool()) { + all_outputs_ = config_all_outputs.asBool(); + } + + auto config_active_only = config_["active-only"]; + if (config_active_only.isBool()) { + active_only_ = config_active_only.asBool(); + creation_delayed_ = active_only_; + } + + box_.set_name("workspaces"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + + add_registry_listener(this); + if (!workspace_manager_) { + return; + } +} + +auto WorkspaceManager::workspace_comparator() const + -> std::function &, std::unique_ptr &)> { + return [=](std::unique_ptr &lhs, std::unique_ptr &rhs) { + auto is_name_less = lhs->get_name() < rhs->get_name(); + auto is_name_eq = lhs->get_name() == rhs->get_name(); + auto is_coords_less = lhs->get_coords() < rhs->get_coords(); + if (sort_by_name_) { + if (sort_by_coordinates_) { + return is_name_eq ? is_coords_less : is_name_less; + } else { + return is_name_less; + } + } + + if (sort_by_coordinates_) { + return is_coords_less; + } + + return lhs->id() < rhs->id(); + }; +} + +auto WorkspaceManager::sort_workspaces() -> void { + std::vector>> all_workspaces; + for (auto &group : groups_) { + auto &group_workspaces = group->workspaces(); + all_workspaces.reserve(all_workspaces.size() + + std::distance(group_workspaces.begin(), group_workspaces.end())); + if (!active_only()) { + all_workspaces.insert(all_workspaces.end(), group_workspaces.begin(), group_workspaces.end()); + continue; + } + + for (auto &workspace : group_workspaces) { + if (!workspace->is_active()) { + continue; + } + + all_workspaces.push_back(workspace); + } + } + + std::sort(all_workspaces.begin(), all_workspaces.end(), workspace_comparator()); + for (size_t i = 0; i < all_workspaces.size(); ++i) { + box_.reorder_child(all_workspaces[i].get()->get_button_ref(), i); + } +} + +auto WorkspaceManager::register_manager(wl_registry *registry, uint32_t name, uint32_t version) + -> void { + if (workspace_manager_) { + spdlog::warn("Register workspace manager again although already registered!"); + return; + } + if (version != 1) { + spdlog::warn("Using different workspace manager protocol version: {}", version); + } + workspace_manager_ = workspace_manager_bind(registry, name, version, this); +} + +auto WorkspaceManager::handle_workspace_group_create( + zext_workspace_group_handle_v1 *workspace_group_handle) -> void { + auto new_id = ++group_global_id; + groups_.push_back( + std::make_unique(bar_, box_, config_, *this, workspace_group_handle, new_id)); + spdlog::debug("Workspace group {} created", new_id); +} + +auto WorkspaceManager::handle_finished() -> void { + zext_workspace_manager_v1_destroy(workspace_manager_); + workspace_manager_ = nullptr; +} + +auto WorkspaceManager::handle_done() -> void { + for (auto &group : groups_) { + group->handle_done(); + } + dp.emit(); +} + +auto WorkspaceManager::update() -> void { + for (auto &group : groups_) { + group->update(); + } + if (creation_delayed()) { + creation_delayed_ = false; + sort_workspaces(); + } + AModule::update(); +} + +WorkspaceManager::~WorkspaceManager() { + if (!workspace_manager_) { + return; + } + + zext_workspace_manager_v1_destroy(workspace_manager_); + workspace_manager_ = nullptr; +} + +auto WorkspaceManager::remove_workspace_group(uint32_t id) -> void { + auto it = std::find_if(groups_.begin(), + groups_.end(), + [id](const std::unique_ptr &g) { return g->id() == id; }); + + if (it == groups_.end()) { + spdlog::warn("Can't find group with id {}", id); + return; + } + + groups_.erase(it); +} +auto WorkspaceManager::commit() -> void { zext_workspace_manager_v1_commit(workspace_manager_); } + +WorkspaceGroup::WorkspaceGroup(const Bar &bar, Gtk::Box &box, const Json::Value &config, + WorkspaceManager &manager, + zext_workspace_group_handle_v1 *workspace_group_handle, uint32_t id) + : bar_(bar), + box_(box), + config_(config), + workspace_manager_(manager), + workspace_group_handle_(workspace_group_handle), + id_(id) { + add_workspace_group_listener(workspace_group_handle, this); +} + +auto WorkspaceGroup::active_only() const -> bool { return workspace_manager_.active_only(); } +auto WorkspaceGroup::creation_delayed() const -> bool { + return workspace_manager_.creation_delayed(); +} + +auto WorkspaceGroup::add_button(Gtk::Button &button) -> void { + box_.pack_start(button, false, false); +} + +WorkspaceGroup::~WorkspaceGroup() { + if (!workspace_group_handle_) { + return; + } + + zext_workspace_group_handle_v1_destroy(workspace_group_handle_); + workspace_group_handle_ = nullptr; +} + +auto WorkspaceGroup::handle_workspace_create(zext_workspace_handle_v1 *workspace) -> void { + auto new_id = ++workspace_global_id; + workspaces_.push_back(std::make_unique(bar_, config_, *this, workspace, new_id)); + spdlog::debug("Workspace {} created", new_id); +} + +auto WorkspaceGroup::handle_remove() -> void { + zext_workspace_group_handle_v1_destroy(workspace_group_handle_); + workspace_group_handle_ = nullptr; + workspace_manager_.remove_workspace_group(id_); +} + +auto WorkspaceGroup::handle_output_enter(wl_output *output) -> void { + spdlog::debug("Output {} assigned to {} group", (void *)output, id_); + output_ = output; + + if (!is_visible() || workspace_manager_.creation_delayed()) { + return; + } + + for (auto &workspace : workspaces_) { + add_button(workspace->get_button_ref()); + } +} + +auto WorkspaceGroup::is_visible() const -> bool { + return output_ != nullptr && + (workspace_manager_.all_outputs() || + output_ == gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj())); +} + +auto WorkspaceGroup::handle_output_leave() -> void { + spdlog::debug("Output {} remove from {} group", (void *)output_, id_); + output_ = nullptr; + + if (output_ != gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj())) { + return; + } + + for (auto &workspace : workspaces_) { + remove_button(workspace->get_button_ref()); + } +} + +auto WorkspaceGroup::update() -> void { + for (auto &workspace : workspaces_) { + if (workspace_manager_.creation_delayed()) { + add_button(workspace->get_button_ref()); + if (is_visible() && (workspace->is_active() || workspace->is_urgent())) { + workspace->show(); + } + } + + workspace->update(); + } +} + +auto WorkspaceGroup::remove_workspace(uint32_t id) -> void { + auto it = std::find_if(workspaces_.begin(), + workspaces_.end(), + [id](const std::unique_ptr &w) { return w->id() == id; }); + + if (it == workspaces_.end()) { + spdlog::warn("Can't find workspace with id {}", id); + return; + } + + workspaces_.erase(it); +} + +auto WorkspaceGroup::handle_done() -> void { + need_to_sort = false; + if (!is_visible()) { + return; + } + + for (auto &workspace : workspaces_) { + workspace->handle_done(); + } + + if (creation_delayed()) { + return; + } + + if (!workspace_manager_.all_outputs()) { + sort_workspaces(); + } else { + workspace_manager_.sort_workspaces(); + } +} + +auto WorkspaceGroup::commit() -> void { workspace_manager_.commit(); } + +auto WorkspaceGroup::sort_workspaces() -> void { + std::sort(workspaces_.begin(), workspaces_.end(), workspace_manager_.workspace_comparator()); + for (size_t i = 0; i < workspaces_.size(); ++i) { + box_.reorder_child(workspaces_[i]->get_button_ref(), i); + } +} + +auto WorkspaceGroup::remove_button(Gtk::Button &button) -> void { box_.remove(button); } + +Workspace::Workspace(const Bar &bar, const Json::Value &config, WorkspaceGroup &workspace_group, + zext_workspace_handle_v1 *workspace, uint32_t id) + : bar_(bar), + config_(config), + workspace_group_(workspace_group), + workspace_handle_(workspace), + id_(id) { + add_workspace_listener(workspace, this); + + auto config_format = config["format"]; + + format_ = config_format.isString() ? config_format.asString() : "{name}"; + with_icon_ = format_.find("{icon}") != std::string::npos; + + if (with_icon_ && icons_map_.empty()) { + auto format_icons = config["format-icons"]; + for (auto &name : format_icons.getMemberNames()) { + icons_map_.emplace(name, format_icons[name].asString()); + } + } + + /* Handle click events if configured */ + if (config_["on-click"].isString() || config_["on-click-middle"].isString() || + config_["on-click-right"].isString()) { + button_.add_events(Gdk::BUTTON_PRESS_MASK); + button_.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handle_clicked), + false); + } + + button_.set_relief(Gtk::RELIEF_NONE); + content_.set_center_widget(label_); + button_.add(content_); + + if (!workspace_group.is_visible()) { + return; + } + + workspace_group.add_button(button_); + button_.show_all(); +} + +Workspace::~Workspace() { + workspace_group_.remove_button(button_); + if (!workspace_handle_) { + return; + } + + zext_workspace_handle_v1_destroy(workspace_handle_); + workspace_handle_ = nullptr; +} + +auto Workspace::update() -> void { + label_.set_markup(fmt::format( + format_, fmt::arg("name", name_), fmt::arg("icon", with_icon_ ? get_icon() : ""))); +} + +auto Workspace::handle_state(const std::vector &state) -> void { + state_ = 0; + for (auto state_entry : state) { + switch (state_entry) { + case ZEXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE: + state_ |= (uint32_t)State::ACTIVE; + break; + case ZEXT_WORKSPACE_HANDLE_V1_STATE_URGENT: + state_ |= (uint32_t)State::URGENT; + break; + case ZEXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN: + state_ |= (uint32_t)State::HIDDEN; + break; + } + } +} + +auto Workspace::handle_remove() -> void { + zext_workspace_handle_v1_destroy(workspace_handle_); + workspace_handle_ = nullptr; + workspace_group_.remove_workspace(id_); +} + +auto add_or_remove_class(Glib::RefPtr context, bool condition, + const std::string &class_name) { + if (condition) { + context->add_class(class_name); + } else { + context->remove_class(class_name); + } +} + +auto Workspace::handle_done() -> void { + spdlog::debug("Workspace {} changed to state {}", id_, state_); + auto style_context = button_.get_style_context(); + add_or_remove_class(style_context, is_active(), "active"); + add_or_remove_class(style_context, is_urgent(), "urgent"); + add_or_remove_class(style_context, is_hidden(), "hidden"); + + if (workspace_group_.creation_delayed()) { + return; + } + + if (workspace_group_.active_only() && (is_active() || is_urgent())) { + button_.show_all(); + } else if (workspace_group_.active_only() && !(is_active() || is_urgent())) { + button_.hide(); + } +} + +auto Workspace::get_icon() -> std::string { + if (is_active()) { + auto active_icon_it = icons_map_.find("active"); + if (active_icon_it != icons_map_.end()) { + return active_icon_it->second; + } + } + + auto named_icon_it = icons_map_.find(name_); + if (named_icon_it != icons_map_.end()) { + return named_icon_it->second; + } + + auto default_icon_it = icons_map_.find("default"); + if (default_icon_it != icons_map_.end()) { + return default_icon_it->second; + } + + return name_; +} + +auto Workspace::handle_clicked(GdkEventButton *bt) -> bool { + std::string action; + if (config_["on-click"].isString() && bt->button == 1) { + action = config_["on-click"].asString(); + } else if (config_["on-click-middle"].isString() && bt->button == 2) { + action = config_["on-click-middle"].asString(); + } else if (config_["on-click-right"].isString() && bt->button == 3) { + action = config_["on-click-right"].asString(); + } + + if (action.empty()) + return true; + else if (action == "activate") { + zext_workspace_handle_v1_activate(workspace_handle_); + } else if (action == "close") { + zext_workspace_handle_v1_remove(workspace_handle_); + } else { + spdlog::warn("Unknown action {}", action); + } + + workspace_group_.commit(); + + return true; +} + +auto Workspace::show() -> void { button_.show_all(); } +auto Workspace::hide() -> void { button_.hide(); } + +auto Workspace::handle_name(const std::string &name) -> void { + if (name_ != name) { + workspace_group_.set_need_to_sort(); + } + name_ = name; +} + +auto Workspace::handle_coordinates(const std::vector &coordinates) -> void { + if (coordinates_ != coordinates) { + workspace_group_.set_need_to_sort(); + } + coordinates_ = coordinates; +} +} // namespace waybar::modules::wlr diff --git a/src/modules/wlr/workspace_manager_binding.cpp b/src/modules/wlr/workspace_manager_binding.cpp new file mode 100644 index 0000000..30c6041 --- /dev/null +++ b/src/modules/wlr/workspace_manager_binding.cpp @@ -0,0 +1,135 @@ +#include "modules/wlr/workspace_manager_binding.hpp" + +#include +#include + +#include "client.hpp" +#include "modules/wlr/workspace_manager.hpp" + +namespace waybar::modules::wlr { + +static void handle_global(void *data, wl_registry *registry, uint32_t name, const char *interface, + uint32_t version) { + if (std::strcmp(interface, zext_workspace_manager_v1_interface.name) == 0) { + static_cast(data)->register_manager(registry, name, version); + } +} + +static void handle_global_remove(void *data, wl_registry *registry, uint32_t name) { + /* Nothing to do here */ +} + +static const wl_registry_listener registry_listener_impl = {.global = handle_global, + .global_remove = handle_global_remove}; + +void add_registry_listener(void *data) { + wl_display * display = Client::inst()->wl_display; + wl_registry *registry = wl_display_get_registry(display); + + wl_registry_add_listener(registry, ®istry_listener_impl, data); + wl_display_roundtrip(display); + wl_display_roundtrip(display); +} + +static void workspace_manager_handle_workspace_group( + void *data, zext_workspace_manager_v1 *_, zext_workspace_group_handle_v1 *workspace_group) { + static_cast(data)->handle_workspace_group_create(workspace_group); +} + +static void workspace_manager_handle_done(void *data, zext_workspace_manager_v1 *_) { + static_cast(data)->handle_done(); +} + +static void workspace_manager_handle_finished(void *data, zext_workspace_manager_v1 *_) { + static_cast(data)->handle_finished(); +} + +static const zext_workspace_manager_v1_listener workspace_manager_impl = { + .workspace_group = workspace_manager_handle_workspace_group, + .done = workspace_manager_handle_done, + .finished = workspace_manager_handle_finished, +}; + +zext_workspace_manager_v1 *workspace_manager_bind(wl_registry *registry, uint32_t name, + uint32_t version, void *data) { + auto *workspace_manager = static_cast( + wl_registry_bind(registry, name, &zext_workspace_manager_v1_interface, version)); + + if (workspace_manager) + zext_workspace_manager_v1_add_listener(workspace_manager, &workspace_manager_impl, data); + else + spdlog::error("Failed to register manager"); + + return workspace_manager; +} + +static void workspace_group_handle_output_enter(void *data, zext_workspace_group_handle_v1 *_, + wl_output *output) { + static_cast(data)->handle_output_enter(output); +} + +static void workspace_group_handle_output_leave(void *data, zext_workspace_group_handle_v1 *_, + wl_output *output) { + static_cast(data)->handle_output_leave(); +} + +static void workspace_group_handle_workspace(void *data, zext_workspace_group_handle_v1 *_, + zext_workspace_handle_v1 *workspace) { + static_cast(data)->handle_workspace_create(workspace); +} + +static void workspace_group_handle_remove(void *data, zext_workspace_group_handle_v1 *_) { + static_cast(data)->handle_remove(); +} + +static const zext_workspace_group_handle_v1_listener workspace_group_impl = { + .output_enter = workspace_group_handle_output_enter, + .output_leave = workspace_group_handle_output_leave, + .workspace = workspace_group_handle_workspace, + .remove = workspace_group_handle_remove}; + +void add_workspace_group_listener(zext_workspace_group_handle_v1 *workspace_group_handle, + void * data) { + zext_workspace_group_handle_v1_add_listener(workspace_group_handle, &workspace_group_impl, data); +} + +void workspace_handle_name(void *data, struct zext_workspace_handle_v1 *_, const char *name) { + static_cast(data)->handle_name(name); +} + +void workspace_handle_coordinates(void *data, struct zext_workspace_handle_v1 *_, + struct wl_array *coordinates) { + std::vector coords_vec; + auto coords = static_cast(coordinates->data); + for (size_t i = 0; i < coordinates->size / sizeof(uint32_t); ++i) { + coords_vec.push_back(coords[i]); + } + + static_cast(data)->handle_coordinates(coords_vec); +} + +void workspace_handle_state(void *data, struct zext_workspace_handle_v1 *workspace_handle, + struct wl_array *state) { + std::vector state_vec; + auto states = static_cast(state->data); + for (size_t i = 0; i < state->size / sizeof(uint32_t); ++i) { + state_vec.push_back(states[i]); + } + + static_cast(data)->handle_state(state_vec); +} + +void workspace_handle_remove(void *data, struct zext_workspace_handle_v1 *_) { + static_cast(data)->handle_remove(); +} + +static const zext_workspace_handle_v1_listener workspace_impl = { + .name = workspace_handle_name, + .coordinates = workspace_handle_coordinates, + .state = workspace_handle_state, + .remove = workspace_handle_remove}; + +void add_workspace_listener(zext_workspace_handle_v1 *workspace_handle, void *data) { + zext_workspace_handle_v1_add_listener(workspace_handle, &workspace_impl, data); +} +} // namespace waybar::modules::wlr diff --git a/test/GlibTestsFixture.hpp b/test/GlibTestsFixture.hpp new file mode 100644 index 0000000..a21c8e0 --- /dev/null +++ b/test/GlibTestsFixture.hpp @@ -0,0 +1,24 @@ +#pragma once +#include +/** + * Minimal Glib application to be used for tests that require Glib main loop + */ +class GlibTestsFixture : public sigc::trackable { + public: + GlibTestsFixture() : main_loop_{Glib::MainLoop::create()} {} + + void setTimeout(int timeout) { + Glib::signal_timeout().connect_once([]() { throw std::runtime_error("Test timed out"); }, + timeout); + } + + void run(std::function fn) { + Glib::signal_idle().connect_once(fn); + main_loop_->run(); + } + + void quit() { main_loop_->quit(); } + + protected: + Glib::RefPtr main_loop_; +}; diff --git a/test/SafeSignal.cpp b/test/SafeSignal.cpp new file mode 100644 index 0000000..2c67317 --- /dev/null +++ b/test/SafeSignal.cpp @@ -0,0 +1,145 @@ +#define CATCH_CONFIG_RUNNER +#include "util/SafeSignal.hpp" + +#include + +#include +#include +#include + +#include "GlibTestsFixture.hpp" + +using namespace waybar; + +template +using remove_cvref_t = typename std::remove_cv::type>::type; + +/** + * Basic sanity test for SafeSignal: + * check that type deduction works, events are delivered and the order is right + * Running this with -fsanitize=thread should not fail + */ +TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal basic functionality", "[signal][thread][util]") { + const int NUM_EVENTS = 100; + int count = 0; + int last_value = 0; + + SafeSignal test_signal; + + const auto main_tid = std::this_thread::get_id(); + std::thread producer; + + // timeout the test in 500ms + setTimeout(500); + + test_signal.connect([&](auto val, auto str) { + static_assert(std::is_same::value); + static_assert(std::is_same::value); + // check that we're in the same thread as the main loop + REQUIRE(std::this_thread::get_id() == main_tid); + // check event order + REQUIRE(val == last_value + 1); + + last_value = val; + if (++count >= NUM_EVENTS) { + this->quit(); + }; + }); + + run([&]() { + // check that events from the same thread are delivered and processed synchronously + test_signal.emit(1, "test"); + REQUIRE(count == 1); + + // start another thread and generate events + producer = std::thread([&]() { + for (auto i = 2; i <= NUM_EVENTS; ++i) { + test_signal.emit(i, "test"); + } + }); + }); + producer.join(); + REQUIRE(count == NUM_EVENTS); +} + +template +struct TestObject { + T value; + unsigned copied = 0; + unsigned moved = 0; + + TestObject(const T& v) : value(v){}; + ~TestObject() = default; + + TestObject(const TestObject& other) + : value(other.value), copied(other.copied + 1), moved(other.moved) {} + + TestObject(TestObject&& other) noexcept + : value(std::move(other.value)), + copied(std::exchange(other.copied, 0)), + moved(std::exchange(other.moved, 0) + 1) {} + + TestObject& operator=(const TestObject& other) { + value = other.value; + copied = other.copied + 1; + moved = other.moved; + return *this; + } + + TestObject& operator=(TestObject&& other) noexcept { + value = std::move(other.value); + copied = std::exchange(other.copied, 0); + moved = std::exchange(other.moved, 0) + 1; + return *this; + } + + bool operator==(T other) const { return value == other; } + operator T() const { return value; } +}; + +/* + * Check the number of copies/moves performed on the object passed through SafeSignal + */ +TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal copy/move counter", "[signal][thread][util]") { + const int NUM_EVENTS = 3; + int count = 0; + + SafeSignal> test_signal; + + std::thread producer; + + // timeout the test in 500ms + setTimeout(500); + + test_signal.connect([&](auto& val) { + static_assert(std::is_same, remove_cvref_t>::value); + + /* explicit move in the producer thread */ + REQUIRE(val.moved <= 1); + /* copy within the SafeSignal queuing code */ + REQUIRE(val.copied <= 1); + + if (++count >= NUM_EVENTS) { + this->quit(); + }; + }); + + run([&]() { + test_signal.emit(1); + REQUIRE(count == 1); + producer = std::thread([&]() { + for (auto i = 2; i <= NUM_EVENTS; ++i) { + TestObject t{i}; + // check that signal.emit accepts moved objects + test_signal.emit(std::move(t)); + } + }); + }); + producer.join(); + REQUIRE(count == NUM_EVENTS); +} + +int main(int argc, char* argv[]) { + Glib::init(); + return Catch::Session().run(argc, argv); +} diff --git a/test/meson.build b/test/meson.build index 85b9771..bbef21e 100644 --- a/test/meson.build +++ b/test/meson.build @@ -2,6 +2,7 @@ test_inc = include_directories('../include') test_dep = [ catch2, fmt, + gtkmm, jsoncpp, spdlog, ] @@ -14,8 +15,21 @@ config_test = executable( include_directories: test_inc, ) +safesignal_test = executable( + 'safesignal_test', + 'SafeSignal.cpp', + dependencies: test_dep, + include_directories: test_inc, +) + test( 'Configuration test', config_test, workdir: meson.source_root(), ) + +test( + 'SafeSignal test', + safesignal_test, + workdir: meson.source_root(), +)