diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index f02c9b57..03e5d707 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -4,20 +4,24 @@ 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: usesh: true prepare: | export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio sed -i '' 's/quarterly/latest/' /etc/pkg/FreeBSD.conf pkg install -y git # subprojects/date - pkg install -y evdev-proto gtk-layer-shell gtkmm30 jsoncpp libdbusmenu \ - libevdev libfmt libmpdclient libudev-devd meson pkgconf pulseaudio \ - scdoc sndio spdlog + pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ + libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \ + pkgconf pulseaudio scdoc sndio spdlog run: | meson build -Dman-pages=enabled ninja -C build + meson test -C build --no-rebuild --print-errorlogs --suite waybar diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index e550e20b..d4efbf8c 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -23,3 +23,5 @@ jobs: run: meson -Dman-pages=enabled build - name: build run: ninja -C build + - name: test + run: meson test -C build --no-rebuild --print-errorlogs --suite waybar diff --git a/Makefile b/Makefile index d7182c18..94f8ee6e 100644 --- a/Makefile +++ b/Makefile @@ -16,5 +16,8 @@ install: build run: build ./build/waybar +debug-run: build-debug + ./build/waybar --log-level debug + clean: rm -rf build diff --git a/include/client.hpp b/include/client.hpp index e7fa1db0..bd80d0bd 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -3,11 +3,10 @@ #include #include #include -#include #include -#include #include "bar.hpp" +#include "config.hpp" struct zwlr_layer_shell_v1; struct zwp_idle_inhibitor_v1; @@ -29,18 +28,13 @@ class Client { struct zxdg_output_manager_v1 * xdg_output_manager = nullptr; struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; std::vector> bars; + Config config; private: Client() = default; - std::tuple getConfigs(const std::string &config, - const std::string &style) const; - void bindInterfaces(); - const std::string getValidPath(const std::vector &paths) const; + const std::string getStyle(const std::string &style); + void bindInterfaces(); void handleOutput(struct waybar_output &output); - bool isValidOutput(const Json::Value &config, struct waybar_output &output); - auto setupConfig(const std::string &config_file, int depth) -> void; - auto resolveConfigIncludes(Json::Value &config, int depth) -> void; - auto mergeConfig(Json::Value &a_config_, Json::Value &b_config_) -> void; auto setupCss(const std::string &css_file) -> void; struct waybar_output & getOutput(void *); std::vector getOutputConfigs(struct waybar_output &output); @@ -55,7 +49,6 @@ class Client { void handleMonitorRemoved(Glib::RefPtr monitor); void handleDeferredMonitorRemoval(Glib::RefPtr monitor); - Json::Value config_; Glib::RefPtr style_context_; Glib::RefPtr css_provider_; std::list outputs_; diff --git a/include/config.hpp b/include/config.hpp new file mode 100644 index 00000000..82d55995 --- /dev/null +++ b/include/config.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include +#include + +#ifndef SYSCONFDIR +#define SYSCONFDIR "/etc" +#endif + +namespace waybar { + +class Config { + public: + static const std::vector CONFIG_DIRS; + + /* Try to find any of provided names in the supported set of config directories */ + static std::optional findConfigPath( + const std::vector &names, const std::vector &dirs = CONFIG_DIRS); + + Config() = default; + + void load(const std::string &config); + + Json::Value &getConfig() { return config_; } + + std::vector getOutputConfigs(const std::string &name, const std::string &identifier); + + private: + void setupConfig(Json::Value &dst, const std::string &config_file, int depth); + void resolveConfigIncludes(Json::Value &config, int depth); + void mergeConfig(Json::Value &a_config_, Json::Value &b_config_); + + std::string config_file_; + + Json::Value config_; +}; +} // namespace waybar diff --git a/include/factory.hpp b/include/factory.hpp index 4b9f32aa..43dd2cfd 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" diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index b3e2634f..17752e4d 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -37,6 +37,7 @@ class Clock : public ALabel { 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); }; } // namespace waybar::modules diff --git a/include/modules/cpu.hpp b/include/modules/cpu.hpp index 866b5af7..7e32a43f 100644 --- a/include/modules/cpu.hpp +++ b/include/modules/cpu.hpp @@ -19,11 +19,11 @@ class Cpu : public ALabel { auto update() -> void; private: - double getCpuLoad(); - std::tuple getCpuUsage(); - std::tuple getCpuFrequency(); - std::vector> parseCpuinfo(); - std::vector parseCpuFrequencies(); + double getCpuLoad(); + std::tuple, std::string> getCpuUsage(); + std::tuple getCpuFrequency(); + std::vector> parseCpuinfo(); + std::vector parseCpuFrequencies(); std::vector> prev_times_; diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 009ae5a3..c91b598b 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_; @@ -67,6 +68,7 @@ class Network : public ALabel { bool carrier_; std::string ifname_; std::string ipaddr_; + std::string gwaddr_; std::string netmask_; int cidr_; int32_t signal_strength_dbm_; diff --git a/include/modules/river/tags.hpp b/include/modules/river/tags.hpp index 9b75fbd3..c49ec60d 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/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 6680bbf0..71c9fd24 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -84,6 +84,7 @@ class Task 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 */ diff --git a/include/modules/wlr/workspace_manager.hpp b/include/modules/wlr/workspace_manager.hpp new file mode 100644 index 00000000..e4cdb4da --- /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 00000000..0bfe663f --- /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/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 48c2ee19..e8053c91 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-clock.5.scd b/man/waybar-clock.5.scd index 28688ee9..2c901d2d 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -24,7 +24,8 @@ The *clock* module displays the current date and time. *timezone*: ++ typeof: string ++ default: inferred local timezone ++ - The timezone to display the time in, e.g. America/New_York. + The timezone to display the time in, e.g. America/New_York. ++ + This field will be ignored if *timezones* field is set and have at least one value. *timezones*: ++ typeof: list of strings ++ diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index 679ca2ce..2e0d6c71 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -82,7 +82,9 @@ The *cpu* module displays the current cpu utilization. *{load}*: Current cpu load. -*{usage}*: Current cpu usage. +*{usage}*: Current overall cpu usage. + +*{usage*{n}*}*: Current cpu core n usage. Cores are numbered from zero, so first core will be {usage0} and 4th will be {usage3}. *{avg_frequency}*: Current cpu average frequency (based on all cores) in GHz. @@ -90,7 +92,13 @@ The *cpu* module displays the current cpu utilization. *{min_frequency}*: Current cpu min frequency (based on the core with the lowest frequency) in GHz. -# EXAMPLE +*{icon}*: Icon for overall cpu usage. + +*{icon*{n}*}*: Icon for cpu core n usage. Use like {icon0}. + +# EXAMPLES + +Basic configuration: ``` "cpu": { @@ -100,6 +108,16 @@ The *cpu* module displays the current cpu utilization. } ``` +Cpu usage per core rendered as icons: + +``` +"cpu": { + "interval": 1, + "format": "{icon0}{icon1}{icon2}{icon3} {usage:>2}% ", + "format-icons": ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"], +}, +``` + # STYLE - *#cpu* diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index f274881e..f8bdd65d 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -131,6 +131,8 @@ Addressed by *network* *{ipaddr}*: The first IP of the interface. +*{gwaddr}*: The default gateway for the interface + *{netmask}*: The subnetmask corresponding to the IP. *{cidr}*: The subnetmask corresponding to the IP in CIDR notation. diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index 65b90332..7814ee49 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-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index 4ce43da9..40be7be4 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -70,7 +70,7 @@ 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 ++ diff --git a/man/waybar-wlr-workspaces.5.scd b/man/waybar-wlr-workspaces.5.scd new file mode 100644 index 00000000..f0df5e94 --- /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 9dc6925f..66d5b2eb 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -64,6 +64,10 @@ Also a minimal example configuration can be found on the at the bottom of this m typeof: integer ++ Margins value without units. +*spacing* ++ + typeof: integer ++ + Size of gaps in between of the different modules. + *name* ++ typeof: string ++ Optional name added as a CSS class, for styling multiple waybars. @@ -87,8 +91,9 @@ Also a minimal example configuration can be found on the at the bottom of this m *include* ++ typeof: string|array ++ - Paths to additional configuration files. In case of duplicate options, the including file's value takes precedence. Make sure to avoid circular imports. - For a multi-bar config, specify at least an empty object for each bar also in every file being included. + Paths to additional configuration files. + Each file can contain a single object with any of the bar configuration options. In case of duplicate options, the first defined value takes precedence, i.e. including file -> first included file -> etc. Nested includes are permitted, but make sure to avoid circular imports. + For a multi-bar config, the include directive affects only current bar configuration object. # MODULE FORMAT @@ -219,5 +224,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 835b70e3..62ac8e36 100644 --- a/meson.build +++ b/meson.build @@ -149,6 +149,7 @@ src_files = files( 'src/main.cpp', 'src/bar.cpp', 'src/client.cpp', + 'src/config.cpp', 'src/util/ustring_clen.cpp' ) @@ -185,6 +186,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 @@ -333,6 +336,7 @@ 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', ] @@ -359,6 +363,15 @@ if scdoc.found() endforeach endif +catch2 = dependency( + 'catch2', + fallback: ['catch2', 'catch2_dep'], + required: get_option('tests'), +) +if catch2.found() + subdir('test') +endif + clangtidy = find_program('clang-tidy', required: false) if clangtidy.found() diff --git a/meson_options.txt b/meson_options.txt index fefb3dc3..81e44689 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -10,3 +10,4 @@ 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('tests', type: 'feature', value: 'auto', description: 'Enable tests') diff --git a/protocol/ext-workspace-unstable-v1.xml b/protocol/ext-workspace-unstable-v1.xml new file mode 100644 index 00000000..24410b62 --- /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 07d524ae..6e82d63d 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 00000000..b8faa45c --- /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/config b/resources/config index 87f24c04..063393b0 100644 --- a/resources/config +++ b/resources/config @@ -3,6 +3,7 @@ // "position": "bottom", // Waybar position (top|bottom|left|right) "height": 30, // Waybar height (to be removed for auto height) // "width": 1280, // Waybar width + "spacing": 4, // Gaps between modules (4px) // Choose the order of the modules "modules-left": ["sway/workspaces", "sway/mode", "custom/media"], "modules-center": ["sway/window"], @@ -117,7 +118,8 @@ "network": { // "interface": "wlp2*", // (Optional) To force the use of this interface "format-wifi": "{essid} ({signalStrength}%) ", - "format-ethernet": "{ifname}: {ipaddr}/{cidr} ", + "format-ethernet": "{ipaddr}/{cidr} ", + "tooltip-format": "{ifname} via {gwaddr} ", "format-linked": "{ifname} (No IP) ", "format-disconnected": "Disconnected ⚠", "format-alt": "{ifname}: {ipaddr}/{cidr}" diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index cf3df4b6..fa9aa58b 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -79,7 +79,7 @@ def signal_handler(sig, frame): def parse_arguments(): parser = argparse.ArgumentParser() - # Increase verbosity with every occurence of -v + # Increase verbosity with every occurrence of -v parser.add_argument('-v', '--verbose', action='count', default=0) # Define for which player we're listening diff --git a/resources/style.css b/resources/style.css index c0d4d9b9..0235942b 100644 --- a/resources/style.css +++ b/resources/style.css @@ -80,7 +80,6 @@ window#waybar.chromium { #idle_inhibitor, #mpd { padding: 0 10px; - margin: 0 4px; color: #ffffff; } diff --git a/src/ALabel.cpp b/src/ALabel.cpp index dd41a32c..b9b3d1d2 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -67,8 +67,10 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ } if (format_icons.isArray()) { auto size = format_icons.size(); - auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); - format_icons = format_icons[idx]; + if (size) { + auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); + format_icons = format_icons[idx]; + } } if (format_icons.isString()) { return format_icons.asString(); @@ -90,8 +92,10 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& } if (format_icons.isArray()) { auto size = format_icons.size(); - auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); - format_icons = format_icons[idx]; + if (size) { + auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); + format_icons = format_icons[idx]; + } } if (format_icons.isString()) { return format_icons.asString(); diff --git a/src/bar.cpp b/src/bar.cpp index 7d763599..a8b230e1 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -13,10 +13,10 @@ namespace waybar { static constexpr const char* MIN_HEIGHT_MSG = - "Requested height: {} exceeds the minimum height: {} required by the modules"; + "Requested height: {} is less than the minimum height: {} required by the modules"; static constexpr const char* MIN_WIDTH_MSG = - "Requested width: {} exceeds the minimum width: {} required by the modules"; + "Requested width: {} is less than the minimum width: {} required by the modules"; static constexpr const char* BAR_SIZE_MSG = "Bar configured (width: {}, height: {}) for output: {}"; @@ -438,6 +438,13 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) center_.get_style_context()->add_class("modules-center"); right_.get_style_context()->add_class("modules-right"); + if (config["spacing"].isInt()) { + int spacing = config["spacing"].asInt(); + left_.set_spacing(spacing); + center_.set_spacing(spacing); + right_.set_spacing(spacing); + } + uint32_t height = config["height"].isUInt() ? config["height"].asUInt() : 0; uint32_t width = config["width"].isUInt() ? config["width"].asUInt() : 0; diff --git a/src/client.cpp b/src/client.cpp index ff6e7bf2..95f5a295 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -3,12 +3,10 @@ #include #include -#include #include #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" -#include "util/json.hpp" #include "wlr-layer-shell-unstable-v1-client-protocol.h" waybar::Client *waybar::Client::inst() { @@ -16,23 +14,6 @@ waybar::Client *waybar::Client::inst() { return c; } -const std::string waybar::Client::getValidPath(const std::vector &paths) const { - wordexp_t p; - - for (const std::string &path : paths) { - if (wordexp(path.c_str(), &p, 0) == 0) { - if (access(*p.we_wordv, F_OK) == 0) { - std::string result = *p.we_wordv; - wordfree(&p); - return result; - } - wordfree(&p); - } - } - - return std::string(); -} - void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { auto client = static_cast(data); @@ -70,29 +51,6 @@ void waybar::Client::handleOutput(struct waybar_output &output) { zxdg_output_v1_add_listener(output.xdg_output.get(), &xdgOutputListener, &output); } -bool waybar::Client::isValidOutput(const Json::Value &config, struct waybar_output &output) { - if (config["output"].isArray()) { - for (auto const &output_conf : config["output"]) { - if (output_conf.isString() && - (output_conf.asString() == output.name || output_conf.asString() == output.identifier)) { - return true; - } - } - return false; - } else if (config["output"].isString()) { - auto config_output = config["output"].asString(); - if (!config_output.empty()) { - if (config_output.substr(0, 1) == "!") { - return config_output.substr(1) != output.name && - config_output.substr(1) != output.identifier; - } - return config_output == output.name || config_output == output.identifier; - } - } - - return true; -} - struct waybar::waybar_output &waybar::Client::getOutput(void *addr) { auto it = std::find_if( outputs_.begin(), outputs_.end(), [&addr](const auto &output) { return &output == addr; }); @@ -103,17 +61,7 @@ struct waybar::waybar_output &waybar::Client::getOutput(void *addr) { } std::vector waybar::Client::getOutputConfigs(struct waybar_output &output) { - std::vector configs; - if (config_.isArray()) { - for (auto const &config : config_) { - if (config.isObject() && isValidOutput(config, output)) { - configs.push_back(config); - } - } - } else if (isValidOutput(config_, output)) { - configs.push_back(config_); - } - return configs; + return config.getOutputConfigs(output.name, output.identifier); } void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_output*/) { @@ -203,94 +151,14 @@ void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr mon outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; }); } -std::tuple waybar::Client::getConfigs( - const std::string &config, const std::string &style) const { - auto config_file = config.empty() ? getValidPath({ - "$XDG_CONFIG_HOME/waybar/config", - "$XDG_CONFIG_HOME/waybar/config.jsonc", - "$HOME/.config/waybar/config", - "$HOME/.config/waybar/config.jsonc", - "$HOME/waybar/config", - "$HOME/waybar/config.jsonc", - "/etc/xdg/waybar/config", - "/etc/xdg/waybar/config.jsonc", - SYSCONFDIR "/xdg/waybar/config", - "./resources/config", - }) - : config; - auto css_file = style.empty() ? getValidPath({ - "$XDG_CONFIG_HOME/waybar/style.css", - "$HOME/.config/waybar/style.css", - "$HOME/waybar/style.css", - "/etc/xdg/waybar/style.css", - SYSCONFDIR "/xdg/waybar/style.css", - "./resources/style.css", - }) - : style; - if (css_file.empty() || config_file.empty()) { - throw std::runtime_error("Missing required resources files"); +const std::string waybar::Client::getStyle(const std::string &style) { + auto css_file = style.empty() ? Config::findConfigPath({"style.css"}) : style; + if (!css_file) { + throw std::runtime_error("Missing required resource files"); } - spdlog::info("Resources files: {}, {}", config_file, css_file); - return {config_file, css_file}; -} - -auto waybar::Client::setupConfig(const std::string &config_file, int depth) -> void { - if (depth > 100) { - throw std::runtime_error("Aborting due to likely recursive include in config files"); - } - std::ifstream file(config_file); - if (!file.is_open()) { - throw std::runtime_error("Can't open config file"); - } - std::string str((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - util::JsonParser parser; - Json::Value tmp_config_ = parser.parse(str); - if (tmp_config_.isArray()) { - for (auto &config_part : tmp_config_) { - resolveConfigIncludes(config_part, depth); - } - } else { - resolveConfigIncludes(tmp_config_, depth); - } - mergeConfig(config_, tmp_config_); -} - -auto waybar::Client::resolveConfigIncludes(Json::Value &config, int depth) -> void { - Json::Value includes = config["include"]; - if (includes.isArray()) { - for (const auto &include : includes) { - spdlog::info("Including resource file: {}", include.asString()); - setupConfig(getValidPath({include.asString()}), ++depth); - } - } else if (includes.isString()) { - spdlog::info("Including resource file: {}", includes.asString()); - setupConfig(getValidPath({includes.asString()}), ++depth); - } -} - -auto waybar::Client::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) -> void { - if (!a_config_) { - // For the first config - a_config_ = b_config_; - } else if (a_config_.isObject() && b_config_.isObject()) { - for (const auto &key : b_config_.getMemberNames()) { - if (a_config_[key].isObject() && b_config_[key].isObject()) { - mergeConfig(a_config_[key], b_config_[key]); - } else { - a_config_[key] = b_config_[key]; - } - } - } else if (a_config_.isArray() && b_config_.isArray()) { - // This can happen only on the top-level array of a multi-bar config - for (Json::Value::ArrayIndex i = 0; i < b_config_.size(); i++) { - if (a_config_[i].isObject() && b_config_[i].isObject()) { - mergeConfig(a_config_[i], b_config_[i]); - } - } - } else { - spdlog::error("Cannot merge config, conflicting or invalid JSON types"); - } -} + spdlog::info("Using CSS file {}", css_file.value()); + return css_file.value(); +}; auto waybar::Client::setupCss(const std::string &css_file) -> void { css_provider_ = Gtk::CssProvider::create(); @@ -329,14 +197,14 @@ void waybar::Client::bindInterfaces() { int waybar::Client::main(int argc, char *argv[]) { bool show_help = false; bool show_version = false; - std::string config; - std::string style; + 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") | - clara::detail::Opt(config, "config")["-c"]["--config"]("Config path") | - clara::detail::Opt(style, "style")["-s"]["--style"]("Style path") | + clara::detail::Opt(config_opt, "config")["-c"]["--config"]("Config path") | + clara::detail::Opt(style_opt, "style")["-s"]["--style"]("Style path") | clara::detail::Opt( log_level, "trace|debug|info|warning|error|critical|off")["-l"]["--log-level"]("Log level") | @@ -367,8 +235,8 @@ int waybar::Client::main(int argc, char *argv[]) { throw std::runtime_error("Bar need to run under Wayland"); } wl_display = gdk_wayland_display_get_wl_display(gdk_display->gobj()); - auto [config_file, css_file] = getConfigs(config, style); - setupConfig(config_file, 0); + config.load(config_opt); + auto css_file = getStyle(style_opt); setupCss(css_file); bindInterfaces(); gtk_app->hold(); diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 00000000..63149cbd --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,153 @@ +#include "config.hpp" + +#include +#include +#include +#include + +#include +#include + +#include "util/json.hpp" + +namespace waybar { + +const std::vector Config::CONFIG_DIRS = { + "$XDG_CONFIG_HOME/waybar/", + "$HOME/.config/waybar/", + "$HOME/waybar/", + "/etc/xdg/waybar/", + SYSCONFDIR "/xdg/waybar/", + "./resources/", +}; + +std::optional tryExpandPath(const std::string &path) { + wordexp_t p; + if (wordexp(path.c_str(), &p, 0) == 0) { + if (access(*p.we_wordv, F_OK) == 0) { + std::string result = *p.we_wordv; + wordfree(&p); + return result; + } + wordfree(&p); + } + return std::nullopt; +} + +std::optional Config::findConfigPath(const std::vector &names, + const std::vector &dirs) { + std::vector paths; + for (const auto &dir : dirs) { + for (const auto &name : names) { + if (auto res = tryExpandPath(dir + name); res) { + return res; + } + } + } + return std::nullopt; +} + +void Config::setupConfig(Json::Value &dst, const std::string &config_file, int depth) { + if (depth > 100) { + throw std::runtime_error("Aborting due to likely recursive include in config files"); + } + std::ifstream file(config_file); + if (!file.is_open()) { + throw std::runtime_error("Can't open config file"); + } + std::string str((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + util::JsonParser parser; + Json::Value tmp_config = parser.parse(str); + if (tmp_config.isArray()) { + for (auto &config_part : tmp_config) { + resolveConfigIncludes(config_part, depth); + } + } else { + resolveConfigIncludes(tmp_config, depth); + } + mergeConfig(dst, tmp_config); +} + +void Config::resolveConfigIncludes(Json::Value &config, int depth) { + Json::Value includes = config["include"]; + if (includes.isArray()) { + for (const auto &include : includes) { + spdlog::info("Including resource file: {}", include.asString()); + setupConfig(config, tryExpandPath(include.asString()).value_or(""), ++depth); + } + } else if (includes.isString()) { + spdlog::info("Including resource file: {}", includes.asString()); + setupConfig(config, tryExpandPath(includes.asString()).value_or(""), ++depth); + } +} + +void Config::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) { + if (!a_config_) { + // For the first config + a_config_ = b_config_; + } else if (a_config_.isObject() && b_config_.isObject()) { + for (const auto &key : b_config_.getMemberNames()) { + // [] creates key with default value. Use `get` to avoid that. + if (a_config_.get(key, Json::Value::nullSingleton()).isObject() && + b_config_[key].isObject()) { + mergeConfig(a_config_[key], b_config_[key]); + } else if (!a_config_.isMember(key)) { + // do not allow overriding value set by top or previously included config + a_config_[key] = b_config_[key]; + } else { + spdlog::trace("Option {} is already set; ignoring value {}", key, b_config_[key]); + } + } + } else { + spdlog::error("Cannot merge config, conflicting or invalid JSON types"); + } +} +bool isValidOutput(const Json::Value &config, const std::string &name, + const std::string &identifier) { + if (config["output"].isArray()) { + for (auto const &output_conf : config["output"]) { + if (output_conf.isString() && + (output_conf.asString() == name || output_conf.asString() == identifier)) { + return true; + } + } + return false; + } else if (config["output"].isString()) { + auto config_output = config["output"].asString(); + if (!config_output.empty()) { + if (config_output.substr(0, 1) == "!") { + return config_output.substr(1) != name && config_output.substr(1) != identifier; + } + return config_output == name || config_output == identifier; + } + } + + return true; +} + +void Config::load(const std::string &config) { + auto file = config.empty() ? findConfigPath({"config", "config.jsonc"}) : config; + if (!file) { + throw std::runtime_error("Missing required resource files"); + } + config_file_ = file.value(); + spdlog::info("Using configuration file {}", config_file_); + setupConfig(config_, config_file_, 0); +} + +std::vector Config::getOutputConfigs(const std::string &name, + const std::string &identifier) { + std::vector configs; + if (config_.isArray()) { + for (auto const &config : config_) { + if (config.isObject() && isValidOutput(config, name, identifier)) { + configs.push_back(config); + } + } + } else if (isValidOutput(config_, name, identifier)) { + configs.push_back(config_); + } + return configs; +} + +} // namespace waybar diff --git a/src/factory.cpp b/src/factory.cpp index 98363545..a577751a 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -30,6 +30,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "wlr/taskbar") { return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]); } + if (ref == "wlr/workspaces") { + return new waybar::modules::wlr::WorkspaceManager(id, bar_, config_[name]); + } #endif #ifdef HAVE_RIVER if (ref == "river/tags") { diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 3ebc6e7b..fcd668c5 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -173,7 +173,7 @@ auto waybar::modules::Backlight::update() -> void { return; } - const auto percent = best->get_max() == 0 ? 100 : best->get_actual() * 100 / best->get_max(); + const uint8_t percent = best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max()); label_.set_markup(fmt::format( format_, fmt::arg("percent", std::to_string(percent)), fmt::arg("icon", getIcon(percent)))); getState(percent); diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 82c57010..7c94c457 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -15,10 +15,14 @@ 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) { - if (config_["timezone"].isString()) { - spdlog::warn("As using a timezone, some format args may be missing as the date library havn't got a release since 2018."); - time_zone_ = date::locate_zone(config_["timezone"].asString()); - fixed_time_zone_ = true; + if (config_["timezones"].isArray() && !config_["timezones"].empty()) { + time_zone_idx_ = 0; + setTimeZone(config_["timezones"][time_zone_idx_]); + } else { + setTimeZone(config_["timezone"]); + } + if (fixed_time_zone_) { + spdlog::warn("As using a timezone, some format args may be missing as the date library haven't got a release since 2018."); } if (config_["locale"].isString()) { @@ -72,6 +76,17 @@ auto waybar::modules::Clock::update() -> void { 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()) { @@ -92,14 +107,7 @@ bool waybar::modules::Clock::handleScroll(GdkEventScroll *e) { } else { time_zone_idx_ = time_zone_idx_ == 0 ? nr_zones - 1 : time_zone_idx_ - 1; } - auto zone_name = config_["timezones"][time_zone_idx_]; - if (!zone_name.isString() || zone_name.empty()) { - fixed_time_zone_ = false; - } else { - time_zone_ = date::locate_zone(zone_name.asString()); - fixed_time_zone_ = true; - } - + setTimeZone(config_["timezones"][time_zone_idx_]); update(); return true; } diff --git a/src/modules/cpu/common.cpp b/src/modules/cpu/common.cpp index 767cde92..4cf67eba 100644 --- a/src/modules/cpu/common.cpp +++ b/src/modules/cpu/common.cpp @@ -1,5 +1,14 @@ #include "modules/cpu.hpp" +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + waybar::modules::Cpu::Cpu(const std::string& id, const Json::Value& config) : ALabel(config, "cpu", id, "{usage}%", 10) { thread_ = [this] { @@ -17,7 +26,8 @@ auto waybar::modules::Cpu::update() -> void { label_.set_tooltip_text(tooltip); } auto format = format_; - auto state = getState(cpu_usage); + auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; + auto state = getState(total_usage); if (!state.empty() && config_["format-" + state].isString()) { format = config_["format-" + state].asString(); } @@ -27,13 +37,22 @@ auto waybar::modules::Cpu::update() -> void { } else { event_box_.show(); auto icons = std::vector{state}; - label_.set_markup(fmt::format(format, - fmt::arg("load", cpu_load), - fmt::arg("usage", cpu_usage), - fmt::arg("icon", getIcon(cpu_usage, icons)), - fmt::arg("max_frequency", max_frequency), - fmt::arg("min_frequency", min_frequency), - fmt::arg("avg_frequency", avg_frequency))); + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("load", cpu_load)); + store.push_back(fmt::arg("load", cpu_load)); + store.push_back(fmt::arg("usage", total_usage)); + store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); + store.push_back(fmt::arg("max_frequency", max_frequency)); + store.push_back(fmt::arg("min_frequency", min_frequency)); + store.push_back(fmt::arg("avg_frequency", avg_frequency)); + for (size_t i = 1; i < cpu_usage.size(); ++i) { + auto core_i = i - 1; + auto core_format = fmt::format("usage{}", core_i); + store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i])); + auto icon_format = fmt::format("icon{}", core_i); + store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); + } + label_.set_markup(fmt::vformat(format, store)); } // Call parent update @@ -48,14 +67,14 @@ double waybar::modules::Cpu::getCpuLoad() { throw std::runtime_error("Can't get Cpu load"); } -std::tuple waybar::modules::Cpu::getCpuUsage() { +std::tuple, std::string> waybar::modules::Cpu::getCpuUsage() { if (prev_times_.empty()) { prev_times_ = parseCpuinfo(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } std::vector> curr_times = parseCpuinfo(); std::string tooltip; - uint16_t usage = 0; + std::vector usage; for (size_t i = 0; i < curr_times.size(); ++i) { auto [curr_idle, curr_total] = curr_times[i]; auto [prev_idle, prev_total] = prev_times_[i]; @@ -63,11 +82,11 @@ std::tuple waybar::modules::Cpu::getCpuUsage() { const float delta_total = curr_total - prev_total; uint16_t tmp = 100 * (1 - delta_idle / delta_total); if (i == 0) { - usage = tmp; tooltip = fmt::format("Total: {}%", tmp); } else { tooltip = tooltip + fmt::format("\nCore{}: {}%", i - 1, tmp); } + usage.push_back(tmp); } prev_times_ = curr_times; return {usage, tooltip}; diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index e63db475..b4e50533 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/memory/common.cpp b/src/modules/memory/common.cpp index 31219ed6..75e05302 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -15,11 +15,11 @@ auto waybar::modules::Memory::update() -> void { unsigned long memfree; if (meminfo_.count("MemAvailable")) { // New kernels (3.4+) have an accurate available memory field. - memfree = meminfo_["MemAvailable"]; + memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"]; } else { // Old kernel; give a best-effort approximation of available memory. memfree = meminfo_["MemFree"] + meminfo_["Buffers"] + meminfo_["Cached"] + - meminfo_["SReclaimable"] - meminfo_["Shmem"]; + meminfo_["SReclaimable"] - meminfo_["Shmem"] + meminfo_["zfs_size"]; } if (memtotal > 0 && memfree >= 0) { diff --git a/src/modules/memory/linux.cpp b/src/modules/memory/linux.cpp index 75f05fe3..34d55c93 100644 --- a/src/modules/memory/linux.cpp +++ b/src/modules/memory/linux.cpp @@ -1,8 +1,29 @@ #include "modules/memory.hpp" +static unsigned zfsArcSize() { + std::ifstream zfs_arc_stats{"/proc/spl/kstat/zfs/arcstats"}; + + if (zfs_arc_stats.is_open()) { + std::string name; + std::string type; + unsigned long data{0}; + + std::string line; + while (std::getline(zfs_arc_stats, line)) { + std::stringstream(line) >> name >> type >> data; + + if (name == "size") { + return data / 1024; // convert to kB + } + } + } + + return 0; +} + void waybar::modules::Memory::parseMeminfo() { const std::string data_dir_ = "/proc/meminfo"; - std::ifstream info(data_dir_); + std::ifstream info(data_dir_); if (!info.is_open()) { throw std::runtime_error("Can't open " + data_dir_); } @@ -17,4 +38,6 @@ void waybar::modules::Memory::parseMeminfo() { int64_t value = std::stol(line.substr(posDelim + 1)); meminfo_[name] = value; } + + meminfo_["zfs_size"] = zfsArcSize(); } diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 0a7c9706..6d272867 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 7d0f6382..e7b20ab5 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), @@ -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_)) { @@ -348,6 +334,7 @@ auto waybar::modules::Network::update() -> void { 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("icon", getIcon(signal_strength_, state_)), @@ -376,6 +363,7 @@ auto waybar::modules::Network::update() -> void { 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("icon", getIcon(signal_strength_, state_)), @@ -409,6 +397,7 @@ void waybar::modules::Network::clearIface() { ifname_.clear(); essid_.clear(); ipaddr_.clear(); + gwaddr_.clear(); netmask_.clear(); carrier_ = false; cidr_ = 0; @@ -466,7 +455,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { } if (!is_del_event && ifi->ifi_index == net->ifid_) { - // Update inferface information + // Update interface information if (net->ifname_.empty() && ifname != NULL) { std::string new_ifname (ifname, ifname_len); net->ifname_ = new_ifname; @@ -581,6 +570,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { break; } + char temp_gw_addr[INET6_ADDRSTRLEN]; case RTM_DELROUTE: is_del_event = true; case RTM_NEWROUTE: { @@ -595,6 +585,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { int temp_idx = -1; uint32_t priority = 0; + /* Find the message(s) concerting the main routing table, each message * corresponds to a single routing table entry. */ @@ -612,9 +603,10 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { case RTA_GATEWAY: /* The gateway of the route. * - * If someone every needs to figure out the gateway address as well, + * If someone ever needs to figure out the gateway address as well, * it's here as the attribute payload. */ + inet_ntop(net->family_, RTA_DATA(attr), temp_gw_addr, sizeof(temp_gw_addr)); has_gateway = true; break; case RTA_DST: { @@ -655,8 +647,8 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { net->clearIface(); net->ifid_ = temp_idx; net->route_priority = priority; - - spdlog::debug("network: new default route via if{} metric {}", temp_idx, priority); + net->gwaddr_ = temp_gw_addr; + spdlog::debug("network: new default route via {} on if{} metric {}", temp_gw_addr, temp_idx, priority); /* Ask ifname associated with temp_idx as well as carrier status */ struct ifinfomsg ifinfo_hdr = { diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index 2628af2d..87655beb 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/sni/item.cpp b/src/modules/sni/item.cpp index 991ccc4d..b504c8d0 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -62,6 +62,7 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf event_box.signal_button_press_event().connect(sigc::mem_fun(*this, &Item::handleClick)); event_box.signal_scroll_event().connect(sigc::mem_fun(*this, &Item::handleScroll)); // initial visibility + event_box.show_all(); event_box.set_visible(show_passive_); cancellable_ = Gio::Cancellable::create(); @@ -287,7 +288,11 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { if (array != nullptr) { g_free(array); } +#if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 68 + array = static_cast(g_memdup2(data, size)); +#else array = static_cast(g_memdup(data, size)); +#endif lwidth = width; lheight = height; } diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index e73c9eb4..c32c0d6a 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -35,11 +35,8 @@ void Tray::onRemove(std::unique_ptr& item) { } auto Tray::update() -> void { - if (box_.get_children().empty()) { - box_.hide(); - } else { - box_.show_all(); - } + // Show tray only when items are available + box_.set_visible(!box_.get_children().empty()); // Call parent update AModule::update(); } diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index 85dc769e..186fa4bb 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -20,15 +20,15 @@ const std::string Language::XKB_ACTIVE_LAYOUT_NAME_KEY = "xkb_active_layout_name Language::Language(const std::string& id, const Json::Value& config) : ALabel(config, "language", id, "{}", 0, true) { is_variant_displayed = format_.find("{variant}") != std::string::npos; - if (format_.find("{}") != std::string::npos || format_.find("{short}") != std::string::npos) { - displayed_short_flag |= static_cast(DispayedShortFlag::ShortName); - } - if (format_.find("{shortDescription}") != std::string::npos) { - displayed_short_flag |= static_cast(DispayedShortFlag::ShortDescription); - } - if (config.isMember("tooltip-format")) { - tooltip_format_ = config["tooltip-format"].asString(); - } + if (format_.find("{}") != std::string::npos || format_.find("{short}") != std::string::npos) { + displayed_short_flag |= static_cast(DispayedShortFlag::ShortName); + } + if (format_.find("{shortDescription}") != std::string::npos) { + displayed_short_flag |= static_cast(DispayedShortFlag::ShortDescription); + } + if (config.isMember("tooltip-format")) { + tooltip_format_ = config["tooltip-format"].asString(); + } ipc_.subscribe(R"(["input"])"); ipc_.signal_event.connect(sigc::mem_fun(*this, &Language::onEvent)); ipc_.signal_cmd.connect(sigc::mem_fun(*this, &Language::onCmd)); @@ -102,16 +102,16 @@ auto Language::update() -> void { fmt::arg("variant", layout_.variant))); label_.set_markup(display_layout); if (tooltipEnabled()) { - if (tooltip_format_ != "") { - auto tooltip_display_layout = trim(fmt::format(tooltip_format_, - fmt::arg("short", layout_.short_name), - fmt::arg("shortDescription", layout_.short_description), - fmt::arg("long", layout_.full_name), - fmt::arg("variant", layout_.variant))); - label_.set_tooltip_markup(tooltip_display_layout); - } else { - label_.set_tooltip_markup(display_layout); - } + if (tooltip_format_ != "") { + auto tooltip_display_layout = trim(fmt::format(tooltip_format_, + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("long", layout_.full_name), + fmt::arg("variant", layout_.variant))); + label_.set_tooltip_markup(tooltip_display_layout); + } else { + label_.set_tooltip_markup(display_layout); + } } event_box_.show(); @@ -126,7 +126,7 @@ auto Language::set_current_layout(std::string current_layout) -> void { auto Language::init_layouts_map(const std::vector& used_layouts) -> void { std::map> found_by_short_names; - XKBContext xkb_context; + XKBContext xkb_context; auto layout = xkb_context.next_layout(); for (; layout != nullptr; layout = xkb_context.next_layout()) { if (std::find(used_layouts.begin(), used_layouts.end(), layout->full_name) == @@ -161,15 +161,15 @@ auto Language::init_layouts_map(const std::vector& used_layouts) -> if (short_name_to_number_map.count(used_layout->short_name) == 0) { short_name_to_number_map[used_layout->short_name] = 1; } - - if (displayed_short_flag != static_cast(0)) { - int& number = short_name_to_number_map[used_layout->short_name]; - used_layout->short_name = - used_layout->short_name + std::to_string(number); - used_layout->short_description = - used_layout->short_description + std::to_string(number); - ++number; - } + + if (displayed_short_flag != static_cast(0)) { + int& number = short_name_to_number_map[used_layout->short_name]; + used_layout->short_name = + used_layout->short_name + std::to_string(number); + used_layout->short_description = + used_layout->short_description + std::to_string(number); + ++number; + } } } @@ -195,14 +195,14 @@ auto Language::XKBContext::next_layout() -> Layout* { auto variant_ = rxkb_layout_get_variant(xkb_layout_); std::string variant = variant_ == nullptr ? "" : std::string(variant_); auto short_description_ = rxkb_layout_get_brief(xkb_layout_); - std::string short_description; - if (short_description_ != nullptr) { - short_description = std::string(short_description_); - base_layouts_by_name_.emplace(name, xkb_layout_); - } else { - auto base_layout = base_layouts_by_name_[name]; - short_description = base_layout == nullptr ? "" : std::string(rxkb_layout_get_brief(base_layout)); - } + std::string short_description; + if (short_description_ != nullptr) { + short_description = std::string(short_description_); + base_layouts_by_name_.emplace(name, xkb_layout_); + } else { + auto base_layout = base_layouts_by_name_[name]; + short_description = base_layout == nullptr ? "" : std::string(rxkb_layout_get_brief(base_layout)); + } delete layout_; layout_ = new Layout{description, name, variant, short_description}; return layout_; diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index b6779585..fc81b2cf 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -68,15 +68,22 @@ auto Window::update() -> void { int leafNodesInWorkspace(const Json::Value& node) { auto const& nodes = node["nodes"]; - if(nodes.empty()) { + auto const& floating_nodes = node["floating_nodes"]; + if(nodes.empty() && floating_nodes.empty()) { if(node["type"] == "workspace") return 0; else return 1; } int sum = 0; - for(auto const& node : nodes) - sum += leafNodesInWorkspace(node); + if (!nodes.empty()) { + for(auto const& node : nodes) + sum += leafNodesInWorkspace(node); + } + if (!floating_nodes.empty()) { + for(auto const& node : floating_nodes) + sum += leafNodesInWorkspace(node); + } return sum; } diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 06ac3676..ddc360bb 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -292,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); @@ -395,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()); @@ -415,6 +413,12 @@ void Task::handle_app_id(const char *app_id) handle_output_enter(output); } } +} + +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_)) { @@ -451,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_); diff --git a/src/modules/wlr/workspace_manager.cpp b/src/modules/wlr/workspace_manager.cpp new file mode 100644 index 00000000..bacef355 --- /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 00000000..30c60413 --- /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/subprojects/catch2.wrap b/subprojects/catch2.wrap new file mode 100644 index 00000000..c82b310f --- /dev/null +++ b/subprojects/catch2.wrap @@ -0,0 +1,12 @@ +[wrap-file] +directory = Catch2-2.13.7 +source_url = https://github.com/catchorg/Catch2/archive/v2.13.7.zip +source_filename = Catch2-2.13.7.zip +source_hash = 3f3ccd90ad3a8fbb1beeb15e6db440ccdcbebe378dfd125d07a1f9a587a927e9 +patch_filename = catch2_2.13.7-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/catch2_2.13.7-1/get_patch +patch_hash = 2f7369645d747e5bd866317ac1dd4c3d04dc97d3aad4fc6b864bdf75d3b57158 + +[provide] +catch2 = catch2_dep + diff --git a/test/config.cpp b/test/config.cpp new file mode 100644 index 00000000..edd6d6b8 --- /dev/null +++ b/test/config.cpp @@ -0,0 +1,115 @@ +#define CATCH_CONFIG_MAIN +#include "config.hpp" + +#include + +TEST_CASE("Load simple config", "[config]") { + waybar::Config conf; + conf.load("test/config/simple.json"); + + SECTION("validate the config data") { + auto& data = conf.getConfig(); + REQUIRE(data["layer"].asString() == "top"); + REQUIRE(data["height"].asInt() == 30); + } + SECTION("select configs for configured output") { + auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(configs.size() == 1); + } + SECTION("select configs for missing output") { + auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(configs.empty()); + } +} + +TEST_CASE("Load config with multiple bars", "[config]") { + waybar::Config conf; + conf.load("test/config/multi.json"); + + SECTION("select multiple configs #1") { + auto data = conf.getOutputConfigs("DP-0", "Fake DisplayPort output #0"); + REQUIRE(data.size() == 3); + REQUIRE(data[0]["layer"].asString() == "bottom"); + REQUIRE(data[0]["height"].asInt() == 20); + REQUIRE(data[1]["layer"].asString() == "top"); + REQUIRE(data[1]["position"].asString() == "bottom"); + REQUIRE(data[1]["height"].asInt() == 21); + REQUIRE(data[2]["layer"].asString() == "overlay"); + REQUIRE(data[2]["position"].asString() == "right"); + REQUIRE(data[2]["height"].asInt() == 23); + } + SECTION("select multiple configs #2") { + auto data = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(data.size() == 2); + REQUIRE(data[0]["layer"].asString() == "bottom"); + REQUIRE(data[0]["height"].asInt() == 20); + REQUIRE(data[1]["layer"].asString() == "overlay"); + REQUIRE(data[1]["position"].asString() == "right"); + REQUIRE(data[1]["height"].asInt() == 23); + } + SECTION("select single config by output description") { + auto data = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["layer"].asString() == "overlay"); + REQUIRE(data[0]["position"].asString() == "left"); + REQUIRE(data[0]["height"].asInt() == 22); + } +} + +TEST_CASE("Load simple config with include", "[config]") { + waybar::Config conf; + conf.load("test/config/include.json"); + + SECTION("validate the config data") { + auto& data = conf.getConfig(); + // config override behavior: preserve first included value + REQUIRE(data["layer"].asString() == "top"); + REQUIRE(data["height"].asInt() == 30); + // config override behavior: preserve value from the top config + REQUIRE(data["position"].asString() == "top"); + // config override behavior: explicit null is still a value and should be preserved + REQUIRE((data.isMember("nullOption") && data["nullOption"].isNull())); + } + SECTION("select configs for configured output") { + auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + REQUIRE(configs.size() == 1); + } + SECTION("select configs for missing output") { + auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + REQUIRE(configs.empty()); + } +} + +TEST_CASE("Load multiple bar config with include", "[config]") { + waybar::Config conf; + conf.load("test/config/include-multi.json"); + + SECTION("bar config with sole include") { + auto data = conf.getOutputConfigs("OUT-0", "Fake output #0"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 20); + } + + SECTION("bar config with output and include") { + auto data = conf.getOutputConfigs("OUT-1", "Fake output #1"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 21); + } + + SECTION("bar config with output override") { + auto data = conf.getOutputConfigs("OUT-2", "Fake output #2"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 22); + } + + SECTION("multiple levels of include") { + auto data = conf.getOutputConfigs("OUT-3", "Fake output #3"); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 23); + } + + auto& data = conf.getConfig(); + REQUIRE(data.isArray()); + REQUIRE(data.size() == 4); + REQUIRE(data[0]["output"].asString() == "OUT-0"); +} diff --git a/test/config/include-1.json b/test/config/include-1.json new file mode 100644 index 00000000..7c47a882 --- /dev/null +++ b/test/config/include-1.json @@ -0,0 +1,7 @@ +{ + "layer": "top", + "position": "bottom", + "height": 30, + "output": ["HDMI-0", "DP-0"], + "nullOption": "not null" +} diff --git a/test/config/include-2.json b/test/config/include-2.json new file mode 100644 index 00000000..741194f1 --- /dev/null +++ b/test/config/include-2.json @@ -0,0 +1,3 @@ +{ + "layer": "bottom" +} diff --git a/test/config/include-multi-0.json b/test/config/include-multi-0.json new file mode 100644 index 00000000..a4c3fc17 --- /dev/null +++ b/test/config/include-multi-0.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-0", + "height": 20 +} diff --git a/test/config/include-multi-1.json b/test/config/include-multi-1.json new file mode 100644 index 00000000..2b28d6c5 --- /dev/null +++ b/test/config/include-multi-1.json @@ -0,0 +1,3 @@ +{ + "height": 21 +} diff --git a/test/config/include-multi-2.json b/test/config/include-multi-2.json new file mode 100644 index 00000000..f74c2b4e --- /dev/null +++ b/test/config/include-multi-2.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-1", + "height": 22 +} diff --git a/test/config/include-multi-3-0.json b/test/config/include-multi-3-0.json new file mode 100644 index 00000000..11cdd3f9 --- /dev/null +++ b/test/config/include-multi-3-0.json @@ -0,0 +1,3 @@ +{ + "height": 23 +} diff --git a/test/config/include-multi-3.json b/test/config/include-multi-3.json new file mode 100644 index 00000000..309fe15e --- /dev/null +++ b/test/config/include-multi-3.json @@ -0,0 +1,4 @@ +{ + "output": "OUT-3", + "include": "test/config/include-multi-3-0.json" +} diff --git a/test/config/include-multi.json b/test/config/include-multi.json new file mode 100644 index 00000000..e128aba5 --- /dev/null +++ b/test/config/include-multi.json @@ -0,0 +1,16 @@ +[ + { + "include": "test/config/include-multi-0.json" + }, + { + "output": "OUT-1", + "include": "test/config/include-multi-1.json" + }, + { + "output": "OUT-2", + "include": "test/config/include-multi-2.json" + }, + { + "include": "test/config/include-multi-3.json" + } +] diff --git a/test/config/include.json b/test/config/include.json new file mode 100644 index 00000000..c46aaf24 --- /dev/null +++ b/test/config/include.json @@ -0,0 +1,5 @@ +{ + "include": ["test/config/include-1.json", "test/config/include-2.json"], + "position": "top", + "nullOption": null +} diff --git a/test/config/multi.json b/test/config/multi.json new file mode 100644 index 00000000..ed43a39e --- /dev/null +++ b/test/config/multi.json @@ -0,0 +1,25 @@ +[ + { + "layer": "bottom", + "height": 20, + "output": ["HDMI-0", "DP-0"] + }, + { + "position": "bottom", + "layer": "top", + "height": 21, + "output": ["DP-0"] + }, + { + "position": "left", + "layer": "overlay", + "height": 22, + "output": "Fake HDMI output #1" + }, + { + "position": "right", + "layer": "overlay", + "height": 23, + "output": "!HDMI-1" + } +] diff --git a/test/config/simple.json b/test/config/simple.json new file mode 100644 index 00000000..1cb1e3a1 --- /dev/null +++ b/test/config/simple.json @@ -0,0 +1,5 @@ +{ + "layer": "top", + "height": 30, + "output": ["HDMI-0", "DP-0"] +} diff --git a/test/meson.build b/test/meson.build new file mode 100644 index 00000000..85b9771f --- /dev/null +++ b/test/meson.build @@ -0,0 +1,21 @@ +test_inc = include_directories('../include') +test_dep = [ + catch2, + fmt, + jsoncpp, + spdlog, +] + +config_test = executable( + 'config_test', + 'config.cpp', + '../src/config.cpp', + dependencies: test_dep, + include_directories: test_inc, +) + +test( + 'Configuration test', + config_test, + workdir: meson.source_root(), +)