diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 66c465ba..0e7e2944 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,14 +1,15 @@ name: Build and Push Docker Image on: + workflow_dispatch: schedule: - # run every night at midnight - - cron: '0 0 * * *' + # run monthly + - cron: '0 0 1 * *' jobs: build-and-push: runs-on: ubuntu-latest - if: github.repository == 'Alexays/Waybar' + if: github.event_name != 'schedule' || github.repository == 'Alexays/Waybar' strategy: fail-fast: false # don't fail the other jobs if one of the images fails to build matrix: diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index ca0dcbc8..b9114c31 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -14,14 +14,14 @@ jobs: steps: - uses: actions/checkout@v3 - name: Test in FreeBSD VM - uses: cross-platform-actions/action@v0.25.0 + uses: cross-platform-actions/action@v0.28.0 timeout-minutes: 180 env: CPPFLAGS: '-isystem/usr/local/include' LDFLAGS: '-L/usr/local/lib' with: operating_system: freebsd - version: "14.1" + version: "14.2" environment_variables: CPPFLAGS LDFLAGS sync_files: runner-to-vm run: | diff --git a/Dockerfiles/debian b/Dockerfiles/debian index f479062d..c2584ccf 100644 --- a/Dockerfiles/debian +++ b/Dockerfiles/debian @@ -1,6 +1,6 @@ # vim: ft=Dockerfile -FROM debian:sid +FROM debian:sid-slim RUN apt update && \ apt install --no-install-recommends --no-install-suggests -y \ diff --git a/Dockerfiles/gentoo b/Dockerfiles/gentoo index f2ec0dc9..f7023825 100644 --- a/Dockerfiles/gentoo +++ b/Dockerfiles/gentoo @@ -6,6 +6,6 @@ RUN export FEATURES="-ipc-sandbox -network-sandbox -pid-sandbox -sandbox -usersa emerge --sync && \ eselect news read --quiet new 1>/dev/null 2>&1 && \ emerge --verbose --update --deep --with-bdeps=y --backtrack=30 --newuse @world && \ - USE="wayland gtk3 gtk -doc X pulseaudio minimal" emerge dev-vcs/git dev-libs/wayland dev-libs/wayland-protocols =dev-cpp/gtkmm-3.24.6 x11-libs/libxkbcommon \ + USE="wayland gtk3 gtk -doc X pulseaudio minimal" emerge dev-vcs/git dev-libs/wayland dev-libs/wayland-protocols dev-cpp/gtkmm:3.0 x11-libs/libxkbcommon \ x11-libs/gtk+:3 dev-libs/libdbusmenu dev-libs/libnl sys-power/upower media-libs/libpulse dev-libs/libevdev media-libs/libmpdclient \ media-sound/sndio gui-libs/gtk-layer-shell app-text/scdoc media-sound/playerctl dev-libs/iniparser sci-libs/fftw diff --git a/Makefile b/Makefile index 3bb11199..12af8e0e 100644 --- a/Makefile +++ b/Makefile @@ -20,8 +20,12 @@ debug-run: build-debug ./build/waybar --log-level debug test: - meson test -C build --no-rebuild --verbose --suite waybar + meson test -C build --verbose --suite waybar .PHONY: test +test-detailed: + meson test -C build --verbose --print-errorlogs --test-args='--reporter console -s' +.PHONY: test-detailed + clean: rm -rf build diff --git a/README.md b/README.md index 5266e916..4edf2253 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,36 @@ sudo apt install \ libxkbregistry-dev ``` +On Arch, you can use this command: + +``` +pacman -S \ + gtkmm3 \ + jsoncpp \ + libsigc++ \ + fmt \ + wayland \ + chrono-date \ + spdlog \ + gtk3 \ + gobject-introspection \ + libgirepository \ + libpulse \ + libnl \ + libappindicator-gtk3 \ + libdbusmenu-gtk3 \ + libmpdclient \ + sndio \ + libevdev \ + libxkbcommon \ + upower \ + meson \ + cmake \ + scdoc \ + wayland-protocols \ + glib2-devel +``` + Contributions welcome!
Have fun :)
diff --git a/asan.supp b/asan.supp new file mode 100644 index 00000000..3633b361 --- /dev/null +++ b/asan.supp @@ -0,0 +1,5 @@ +# Suppress common address sanitizer issues in dependencies, these are often non-fixable or not an issue. +# Use it like this (when in repo root): ASAN_OPTIONS="suppressions=./asan.supp" ./build/waybar +leak:libpangoft2-1.0.so.0 +leak:libgtk-3.so.0 +leak:libfontconfig.so.1 \ No newline at end of file diff --git a/flake.lock b/flake.lock index 480f004f..9b8db656 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "owner": "edolstra", "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1745391562, - "narHash": "sha256-sPwcCYuiEopaafePqlG826tBhctuJsLx/mhKKM5Fmjo=", + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8a2f738d9d1f1d986b5a4cd2fd2061a7127237d7", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7c7a2281..b2f96731 100644 --- a/flake.nix +++ b/flake.nix @@ -65,7 +65,7 @@ nativeBuildInputs = pkgs.waybar.nativeBuildInputs ++ (with pkgs; [ - nixfmt-rfc-style + nixfmt clang-tools gdb ]); @@ -75,28 +75,26 @@ formatter = genSystems ( pkgs: pkgs.treefmt.withConfig { - settings = [ - { - formatter = { - clang-format = { - options = [ "-i" ]; - command = lib.getExe' pkgs.clang-tools "clang-format"; - excludes = [ ]; - includes = [ - "*.c" - "*.cpp" - "*.h" - "*.hpp" - ]; - }; - nixfmt = { - command = lib.getExe pkgs.nixfmt-rfc-style; - includes = [ "*.nix" ]; - }; + settings = { + formatter = { + clang-format = { + options = [ "-i" ]; + command = lib.getExe' pkgs.clang-tools "clang-format"; + excludes = [ ]; + includes = [ + "*.c" + "*.cpp" + "*.h" + "*.hpp" + ]; }; - tree-root-file = ".git/index"; - } - ]; + nixfmt = { + command = lib.getExe pkgs.nixfmt; + includes = [ "*.nix" ]; + }; + }; + tree-root-file = ".git/index"; + }; } ); diff --git a/include/ALabel.hpp b/include/ALabel.hpp index a1aae9da..92fc2e0f 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -21,7 +21,7 @@ class ALabel : public AModule { protected: Gtk::Label label_; std::string format_; - const std::chrono::seconds interval_; + const std::chrono::milliseconds interval_; bool alt_ = false; std::string default_format_; diff --git a/include/AModule.hpp b/include/AModule.hpp index 2ffc429c..2fcbfc23 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -36,6 +36,7 @@ class AModule : public IModule { SCROLL_DIR getScrollDir(GdkEventScroll *e); bool tooltipEnabled() const; + std::vector pid_children_; const std::string name_; const Json::Value &config_; Gtk::EventBox event_box_; @@ -54,7 +55,6 @@ class AModule : public IModule { const bool isTooltip; const bool isExpand; bool hasUserEvents_; - std::vector pid_; gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; std::map eventActionMap_; diff --git a/include/bar.hpp b/include/bar.hpp index 9b407abf..9953b1f5 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -14,6 +14,7 @@ #include "AModule.hpp" #include "group.hpp" +#include "util/kill_signal.hpp" #include "xdg-output-unstable-v1-client-protocol.h" namespace waybar { @@ -68,7 +69,11 @@ class Bar : public sigc::trackable { void setMode(const std::string &mode); void setVisible(bool value); void toggle(); + void show(); + void hide(); void handleSignal(int); + util::KillSignalAction getOnSigusr1Action(); + util::KillSignalAction getOnSigusr2Action(); struct waybar_output *output; Json::Value config; @@ -118,6 +123,9 @@ class Bar : public sigc::trackable { std::unique_ptr _ipc_client; #endif std::vector> modules_all_; + + waybar::util::KillSignalAction onSigusr1 = util::SIGNALACTION_DEFAULT_SIGUSR1; + waybar::util::KillSignalAction onSigusr2 = util::SIGNALACTION_DEFAULT_SIGUSR2; }; } // namespace waybar diff --git a/include/config.hpp b/include/config.hpp index 5256bb46..bf653c2a 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -35,6 +35,8 @@ class Config { 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_); + static std::vector findIncludePath( + const std::string &name, const std::vector &dirs = CONFIG_DIRS); std::string config_file_; diff --git a/include/modules/battery.hpp b/include/modules/battery.hpp index 8e1a2ad2..c593e59b 100644 --- a/include/modules/battery.hpp +++ b/include/modules/battery.hpp @@ -35,6 +35,7 @@ class Battery : public ALabel { std::tuple getInfos(); const std::string formatTimeRemaining(float hoursRemaining); void setBarClass(std::string&); + void processEvents(std::string& state, std::string& status, uint8_t capacity); int global_watch; std::map batteries_; @@ -43,6 +44,7 @@ class Battery : public ALabel { int global_watch_fd_; std::mutex battery_list_mutex_; std::string old_status_; + std::string last_event_; bool warnFirstTime_{true}; const Bar& bar_; diff --git a/include/modules/cava.hpp b/include/modules/cava.hpp deleted file mode 100644 index 1a88c7b7..00000000 --- a/include/modules/cava.hpp +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include "ALabel.hpp" -#include "util/sleeper_thread.hpp" - -namespace cava { -extern "C" { -// Need sdl_glsl output feature to be enabled on libcava -#ifndef SDL_GLSL -#define SDL_GLSL -#endif - -#include - -#ifdef SDL_GLSL -#undef SDL_GLSL -#endif -} -} // namespace cava - -namespace waybar::modules { -using namespace std::literals::chrono_literals; - -class Cava final : public ALabel { - public: - Cava(const std::string&, const Json::Value&); - virtual ~Cava(); - auto update() -> void override; - auto doAction(const std::string& name) -> void override; - - private: - util::SleeperThread thread_; - util::SleeperThread thread_fetch_input_; - - struct cava::error_s error_{}; // cava errors - struct cava::config_params prm_{}; // cava parameters - struct cava::audio_raw audio_raw_{}; // cava handled raw audio data(is based on audio_data) - struct cava::audio_data audio_data_{}; // cava audio data - struct cava::cava_plan* plan_; //{new cava_plan{}}; - // Cava API to read audio source - cava::ptr input_source_; - // Delay to handle audio source - std::chrono::milliseconds frame_time_milsec_{1s}; - // Text to display - std::string text_{""}; - int rePaint_{1}; - std::chrono::seconds fetch_input_delay_{4}; - std::chrono::seconds suspend_silence_delay_{0}; - bool silence_{false}; - bool hide_on_silence_{false}; - std::string format_silent_{""}; - int sleep_counter_{0}; - // Cava method - void pause_resume(); - // ModuleActionMap - static inline std::map actionMap_{ - {"mode", &waybar::modules::Cava::pause_resume}}; -}; -} // namespace waybar::modules diff --git a/include/modules/cava/cava.hpp b/include/modules/cava/cava.hpp new file mode 100644 index 00000000..6b13c4bd --- /dev/null +++ b/include/modules/cava/cava.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "ALabel.hpp" +#include "cava_backend.hpp" + +namespace waybar::modules::cava { + +class Cava final : public ALabel, public sigc::trackable { + public: + Cava(const std::string&, const Json::Value&); + ~Cava() = default; + auto onUpdate(const std::string& input) -> void; + auto onSilence() -> void; + auto doAction(const std::string& name) -> void override; + + private: + std::shared_ptr backend_; + // Text to display + std::string label_text_{""}; + bool hide_on_silence_{false}; + std::string format_silent_{""}; + int ascii_range_{0}; + bool silence_{false}; + // Cava method + void pause_resume(); + // ModuleActionMap + static inline std::map + actionMap_{{"mode", &waybar::modules::cava::Cava::pause_resume}}; +}; +} // namespace waybar::modules::cava diff --git a/include/modules/cava/cava_backend.hpp b/include/modules/cava/cava_backend.hpp new file mode 100644 index 00000000..d8a2ce06 --- /dev/null +++ b/include/modules/cava/cava_backend.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#include "util/sleeper_thread.hpp" + +namespace cava { +extern "C" { +// Need sdl_glsl output feature to be enabled on libcava +#ifndef SDL_GLSL +#define SDL_GLSL +#endif + +#include + +#ifdef SDL_GLSL +#undef SDL_GLSL +#endif +} +} // namespace cava + +namespace waybar::modules::cava { +using namespace std::literals::chrono_literals; + +class CavaBackend final { + public: + static std::shared_ptr inst(const Json::Value& config); + + virtual ~CavaBackend(); + // Methods + int getAsciiRange(); + void doPauseResume(); + void Update(); + // Signal accessor + using type_signal_update = sigc::signal; + type_signal_update signal_update(); + using type_signal_silence = sigc::signal; + type_signal_silence signal_silence(); + + private: + CavaBackend(const Json::Value& config); + util::SleeperThread thread_; + util::SleeperThread read_thread_; + // Cava API to read audio source + ::cava::ptr input_source_; + + struct ::cava::error_s error_{}; // cava errors + struct ::cava::config_params prm_{}; // cava parameters + struct ::cava::audio_raw audio_raw_{}; // cava handled raw audio data(is based on audio_data) + struct ::cava::audio_data audio_data_{}; // cava audio data + struct ::cava::cava_plan* plan_; //{new cava_plan{}}; + + std::chrono::seconds fetch_input_delay_{4}; + // Delay to handle audio source + std::chrono::milliseconds frame_time_milsec_{1s}; + + int re_paint_{0}; + bool silence_{false}; + bool silence_prev_{false}; + std::chrono::seconds suspend_silence_delay_{0}; + int sleep_counter_{0}; + std::string output_{}; + // Methods + void invoke(); + void execute(); + bool isSilence(); + void doUpdate(bool force = false); + + // Signal + type_signal_update m_signal_update_; + type_signal_silence m_signal_silence_; +}; +} // namespace waybar::modules::cava diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index 40b4f80e..c3548063 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -38,39 +38,41 @@ class Clock final : public ALabel { 5 - tooltip-format */ std::map fmtMap_; - uint cldMonCols_{3}; // calendar count month columns - int cldWnLen_{3}; // calendar week number length - const int cldMonColLen_{20}; // calendar month column length - WS cldWPos_{WS::HIDDEN}; // calendar week side to print - months cldCurrShift_{0}; // calendar months shift - int cldShift_{1}; // calendar months shift factor - year_month_day cldYearShift_; // calendar Year mode. Cached ymd - std::string cldYearCached_; // calendar Year mode. Cached calendar - year_month cldMonShift_; // calendar Month mode. Cached ym - std::string cldMonCached_; // calendar Month mode. Cached calendar - day cldBaseDay_{0}; // calendar Cached day. Is used when today is changing(midnight) + uint cldMonCols_{3}; // calendar count month columns + int cldWnLen_{3}; // calendar week number length + const int cldMonColLen_{20}; // calendar month column length + WS cldWPos_{WS::HIDDEN}; // calendar week side to print + date::months cldCurrShift_{0}; // calendar months shift + int cldShift_{1}; // calendar months shift factor + date::year_month_day cldYearShift_; // calendar Year mode. Cached ymd + std::string cldYearCached_; // calendar Year mode. Cached calendar + date::year_month cldMonShift_; // calendar Month mode. Cached ym + std::string cldMonCached_; // calendar Month mode. Cached calendar + date::day cldBaseDay_{0}; // calendar Cached day. Is used when today is changing(midnight) std::string cldText_{""}; // calendar text to print + bool iso8601Calendar_{false}; // whether the calendar is in ISO8601 CldMode cldMode_{CldMode::MONTH}; - auto get_calendar(const year_month_day& today, const year_month_day& ymd, const time_zone* tz) - -> const std::string; + auto get_calendar(const date::year_month_day& today, const date::year_month_day& ymd, + const date::time_zone* tz) -> const std::string; // get local time zone - auto local_zone() -> const time_zone*; + auto local_zone() -> const date::time_zone*; // time zoned time in tooltip - const bool tzInTooltip_; // if need to print time zones text - std::vector tzList_; // time zones list - int tzCurrIdx_; // current time zone index for tzList_ - std::string tzText_{""}; // time zones text to print + const bool tzInTooltip_; // if need to print time zones text + std::vector tzList_; // time zones list + int tzCurrIdx_; // current time zone index for tzList_ + std::string tzText_{""}; // time zones text to print + std::string tzTooltipFormat_{""}; // optional timezone tooltip format util::SleeperThread thread_; // ordinal date in tooltip const bool ordInTooltip_; std::string ordText_{""}; - auto get_ordinal_date(const year_month_day& today) -> std::string; + auto get_ordinal_date(const date::year_month_day& today) -> std::string; - auto getTZtext(sys_seconds now) -> std::string; - auto first_day_of_week() -> weekday; + auto getTZtext(date::sys_seconds now) -> std::string; + auto first_day_of_week() -> date::weekday; // Module actions void cldModeSwitch(); void cldShift_up(); diff --git a/include/modules/ext/workspace_manager.hpp b/include/modules/ext/workspace_manager.hpp new file mode 100644 index 00000000..686c7fc7 --- /dev/null +++ b/include/modules/ext/workspace_manager.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "ext-workspace-v1-client-protocol.h" + +namespace waybar::modules::ext { + +class WorkspaceGroup; +class Workspace; + +class WorkspaceManager final : public AModule { + public: + WorkspaceManager(const std::string &id, const waybar::Bar &bar, const Json::Value &config); + ~WorkspaceManager() override; + void register_manager(wl_registry *registry, uint32_t name, uint32_t version); + void remove_workspace_group(uint32_t id); + void remove_workspace(uint32_t id); + void set_needs_sorting() { needs_sorting_ = true; } + + // wl events + void handle_workspace_group(ext_workspace_group_handle_v1 *handle); + void handle_workspace(ext_workspace_handle_v1 *handle); + void handle_done(); + void handle_finished(); + + // wl requests + void commit() const; + + private: + void update() override; + bool has_button(const Gtk::Button *button); + void sort_workspaces(); + void clear_buttons(); + void update_buttons(); + + static uint32_t group_global_id; + static uint32_t workspace_global_id; + uint32_t workspace_name = 0; + + bool sort_by_id_ = false; + bool sort_by_name_ = true; + bool sort_by_coordinates_ = false; + bool all_outputs_ = false; + + const waybar::Bar &bar_; + Gtk::Box box_; + + ext_workspace_manager_v1 *ext_manager_ = nullptr; + std::vector> groups_; + std::vector> workspaces_; + + bool needs_sorting_ = false; +}; + +class WorkspaceGroup { + public: + WorkspaceGroup(WorkspaceManager &manager, ext_workspace_group_handle_v1 *handle, uint32_t id); + ~WorkspaceGroup(); + + u_int32_t id() const { return id_; } + bool has_output(const wl_output *output); + bool has_workspace(const ext_workspace_handle_v1 *workspace); + + // wl events + void handle_capabilities(uint32_t capabilities); + void handle_output_enter(wl_output *output); + void handle_output_leave(wl_output *output); + void handle_workspace_enter(ext_workspace_handle_v1 *handle); + void handle_workspace_leave(ext_workspace_handle_v1 *handle); + void handle_removed(); + + private: + WorkspaceManager &workspaces_manager_; + ext_workspace_group_handle_v1 *ext_handle_; + uint32_t id_; + std::vector outputs_; + std::vector workspaces_; +}; + +class Workspace { + public: + Workspace(const Json::Value &config, WorkspaceManager &manager, ext_workspace_handle_v1 *handle, + uint32_t id, const std::string &name); + ~Workspace(); + + ext_workspace_handle_v1 *handle() const { return ext_handle_; } + u_int32_t id() const { return id_; } + std::string &workspace_id() { return workspace_id_; } + std::string &name() { return name_; } + std::vector &coordinates() { return coordinates_; } + Gtk::Button &button() { return button_; } + void update(); + + // wl events + void handle_id(const std::string &id); + void handle_name(const std::string &name); + void handle_coordinates(const std::vector &coordinates); + void handle_state(uint32_t state); + void handle_capabilities(uint32_t capabilities); + void handle_removed(); + + // gdk events + bool handle_clicked(const GdkEventButton *button) const; + + private: + bool has_state(uint32_t state) const { return (state_ & state) == state; } + std::string icon(); + + WorkspaceManager &workspace_manager_; + ext_workspace_handle_v1 *ext_handle_ = nullptr; + uint32_t id_; + uint32_t state_ = 0; + std::string workspace_id_; + std::string name_; + std::vector coordinates_; + + bool active_only_ = false; + bool ignore_hidden_ = true; + std::string format_; + bool with_icon_ = false; + static std::map icon_map_; + std::string on_click_action_; + std::string on_click_middle_action_; + std::string on_click_right_action_; + + Gtk::Button button_; + Gtk::Box content_; + Gtk::Label label_; +}; + +} // namespace waybar::modules::ext diff --git a/include/modules/ext/workspace_manager_binding.hpp b/include/modules/ext/workspace_manager_binding.hpp new file mode 100644 index 00000000..b41f207c --- /dev/null +++ b/include/modules/ext/workspace_manager_binding.hpp @@ -0,0 +1,10 @@ +#include "ext-workspace-v1-client-protocol.h" + +namespace waybar::modules::ext { +void add_registry_listener(void *data); +void add_workspace_listener(ext_workspace_handle_v1 *workspace_handle, void *data); +void add_workspace_group_listener(ext_workspace_group_handle_v1 *workspace_group_handle, + void *data); +ext_workspace_manager_v1 *workspace_manager_bind(wl_registry *registry, uint32_t name, + uint32_t version, void *data); +} // namespace waybar::modules::ext diff --git a/include/modules/gps.hpp b/include/modules/gps.hpp new file mode 100644 index 00000000..80df12ba --- /dev/null +++ b/include/modules/gps.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#ifdef WANT_RFKILL +#include "util/rfkill.hpp" +#endif + +#include + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class Gps : public ALabel { + public: + Gps(const std::string&, const Json::Value&); + virtual ~Gps(); + auto update() -> void override; + + private: +#ifdef WANT_RFKILL + util::Rfkill rfkill_; +#endif + const std::string getFixModeName() const; + const std::string getFixModeString() const; + + const std::string getFixStatusString() const; + + util::SleeperThread thread_, gps_thread_; + gps_data_t gps_data_; + std::string state_; + + bool hideDisconnected = true; + bool hideNoFix = false; +}; + +} // namespace waybar::modules diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index cfd0b258..2e0ef657 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -17,9 +17,13 @@ class EventHandler { virtual ~EventHandler() = default; }; +/// If you want to use the Hyprland IPC, simply use IPC::inst() to get the singleton instance. +/// Do not create multiple instances. class IPC { + protected: + IPC(); // use IPC::inst() instead. + public: - IPC(); ~IPC(); static IPC& inst(); @@ -42,8 +46,7 @@ class IPC { util::JsonParser parser_; std::list> callbacks_; int socketfd_; // the hyprland socket file descriptor - bool running_ = true; + pid_t socketOwnerPid_; + bool running_ = true; // the ipcThread will stop running when this is false }; - -inline bool modulesReady = false; }; // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/windowcount.hpp b/include/modules/hyprland/windowcount.hpp new file mode 100644 index 00000000..7c86fb3e --- /dev/null +++ b/include/modules/hyprland/windowcount.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" + +namespace waybar::modules::hyprland { + +class WindowCount : public waybar::AAppIconLabel, public EventHandler { + public: + WindowCount(const std::string&, const waybar::Bar&, const Json::Value&); + ~WindowCount() override; + + auto update() -> void override; + + private: + struct Workspace { + int id; + int windows; + bool hasfullscreen; + static auto parse(const Json::Value& value) -> Workspace; + }; + + auto getActiveWorkspace(const std::string&) -> Workspace; + auto getActiveWorkspace() -> Workspace; + void onEvent(const std::string& ev) override; + void queryActiveWorkspace(); + void setClass(const std::string&, bool enable); + + bool separateOutputs_; + std::mutex mutex_; + const Bar& bar_; + Workspace workspace_; + IPC& m_ipc; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/windowcreationpayload.hpp b/include/modules/hyprland/windowcreationpayload.hpp index 45229ed4..226a7c41 100644 --- a/include/modules/hyprland/windowcreationpayload.hpp +++ b/include/modules/hyprland/windowcreationpayload.hpp @@ -26,34 +26,48 @@ namespace waybar::modules::hyprland { class Workspaces; +struct WindowRepr { + std::string address; + std::string window_class; + std::string window_title; + std::string repr_rewrite; + bool isActive = false; + + public: + bool empty() const { return address.empty(); } + void setActive(bool value) { isActive = value; } +}; + class WindowCreationPayload { public: WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_repr); + WindowRepr window_repr); WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_class, std::string window_title); + std::string window_class, std::string window_title, bool is_active); WindowCreationPayload(Json::Value const& client_data); int incrementTimeSpentUncreated(); bool isEmpty(Workspaces& workspace_manager); bool reprIsReady() const { return std::holds_alternative(m_window); } - std::string repr(Workspaces& workspace_manager); + WindowRepr repr(Workspaces& workspace_manager); + void setActive(bool value) { m_isActive = value; } std::string getWorkspaceName() const { return m_workspaceName; } WindowAddress getAddress() const { return m_windowAddress; } - void moveToWorksace(std::string& new_workspace_name); + void moveToWorkspace(std::string& new_workspace_name); private: void clearAddr(); void clearWorkspaceName(); - using Repr = std::string; + using Repr = WindowRepr; using ClassAndTitle = std::pair; std::variant m_window; WindowAddress m_windowAddress; std::string m_workspaceName; + bool m_isActive = false; int m_timeSpentUncreated = 0; }; diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 3dedba4c..1d80d331 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -42,7 +42,6 @@ class Workspace { bool isPersistentConfig() const { return m_isPersistentConfig; }; bool isPersistentRule() const { return m_isPersistentRule; }; bool isVisible() const { return m_isVisible; }; - bool isEmpty() const { return m_windows == 0; }; bool isUrgent() const { return m_isUrgent; }; bool handleClicked(GdkEventButton* bt) const; @@ -54,15 +53,18 @@ class Workspace { void setWindows(uint value) { m_windows = value; }; void setName(std::string const& value) { m_name = value; }; void setOutput(std::string const& value) { m_output = value; }; - bool containsWindow(WindowAddress const& addr) const { return m_windowMap.contains(addr); } - void insertWindow(WindowCreationPayload create_window_paylod); - std::string removeWindow(WindowAddress const& addr); + bool containsWindow(WindowAddress const& addr) const { + return std::ranges::any_of(m_windowMap, + [&addr](const auto& window) { return window.address == addr; }); + }; + void insertWindow(WindowCreationPayload create_window_payload); void initializeWindowMap(const Json::Value& clients_data); + void setActiveWindow(WindowAddress const& addr); - bool onWindowOpened(WindowCreationPayload const& create_window_paylod); - std::optional closeWindow(WindowAddress const& addr); + bool onWindowOpened(WindowCreationPayload const& create_window_payload); + std::optional closeWindow(WindowAddress const& addr); - void update(const std::string& format, const std::string& icon); + void update(const std::string& workspace_icon); private: Workspaces& m_workspaceManager; @@ -78,11 +80,17 @@ class Workspace { bool m_isUrgent = false; bool m_isVisible = false; - std::map m_windowMap; + std::vector m_windowMap; Gtk::Button m_button; Gtk::Box m_content; - Gtk::Label m_label; + Gtk::Label m_labelBefore; + Gtk::Label m_labelAfter; + + bool isEmpty() const; + void updateTaskbar(const std::string& workspace_icon); + bool handleClick(const GdkEventButton* event_button, WindowAddress const& addr) const; + bool shouldSkipWindow(const WindowRepr& window_repr) const; IPC& m_ipc; }; diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 6b33baea..a5d94bbf 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -18,6 +19,7 @@ #include "modules/hyprland/windowcreationpayload.hpp" #include "modules/hyprland/workspace.hpp" #include "util/enum.hpp" +#include "util/icon_loader.hpp" #include "util/regex_collection.hpp" using WindowAddress = std::string; @@ -37,19 +39,36 @@ class Workspaces : public AModule, public EventHandler { auto showSpecial() const -> bool { return m_showSpecial; } auto activeOnly() const -> bool { return m_activeOnly; } auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } + auto persistentOnly() const -> bool { return m_persistentOnly; } auto moveToMonitor() const -> bool { return m_moveToMonitor; } + auto enableTaskbar() const -> bool { return m_enableTaskbar; } + auto taskbarWithIcon() const -> bool { return m_taskbarWithIcon; } auto getBarOutput() const -> std::string { return m_bar.output->name; } + auto formatBefore() const -> std::string { return m_formatBefore; } + auto formatAfter() const -> std::string { return m_formatAfter; } + auto taskbarFormatBefore() const -> std::string { return m_taskbarFormatBefore; } + auto taskbarFormatAfter() const -> std::string { return m_taskbarFormatAfter; } + auto taskbarIconSize() const -> int { return m_taskbarIconSize; } + auto taskbarOrientation() const -> Gtk::Orientation { return m_taskbarOrientation; } + auto taskbarReverseDirection() const -> bool { return m_taskbarReverseDirection; } + auto onClickWindow() const -> std::string { return m_onClickWindow; } + auto getIgnoredWindows() const -> std::vector { return m_ignoreWindows; } + + enum class ActiveWindowPosition { NONE, FIRST, LAST }; + auto activeWindowPosition() const -> ActiveWindowPosition { return m_activeWindowPosition; } std::string getRewrite(std::string window_class, std::string window_title); std::string& getWindowSeparator() { return m_formatWindowSeparator; } bool isWorkspaceIgnored(std::string const& workspace_name); bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } + const IconLoader& iconLoader() const { return m_iconLoader; } private: void onEvent(const std::string& e) override; void updateWindowCount(); + void sortSpecialCentered(); void sortWorkspaces(); void createWorkspace(Json::Value const& workspace_data, Json::Value const& clients_data = Json::Value::nullRef); @@ -68,6 +87,7 @@ class Workspaces : public AModule, public EventHandler { auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; auto populateWindowRewriteConfig(const Json::Value& config) -> void; + auto populateWorkspaceTaskbarConfig(const Json::Value& config) -> void; void registerIpc(); @@ -90,6 +110,7 @@ class Workspaces : public AModule, public EventHandler { void onWindowMoved(std::string const& payload); void onWindowTitleEvent(std::string const& payload); + void onActiveWindowChanged(WindowAddress const& payload); void onConfigReloaded(); @@ -122,23 +143,26 @@ class Workspaces : public AModule, public EventHandler { bool m_showSpecial = false; bool m_activeOnly = false; bool m_specialVisibleOnly = false; + bool m_persistentOnly = false; bool m_moveToMonitor = false; Json::Value m_persistentWorkspaceConfig; // Map for windows stored in workspaces not present in the current bar. // This happens when the user has multiple monitors (hence, multiple bars) - // and doesn't share windows accross bars (a.k.a `all-outputs` = false) - std::map m_orphanWindowMap; + // and doesn't share windows across bars (a.k.a `all-outputs` = false) + std::map> m_orphanWindowMap; - enum class SortMethod { ID, NAME, NUMBER, DEFAULT }; + enum class SortMethod { ID, NAME, NUMBER, SPECIAL_CENTERED, DEFAULT }; util::EnumParser m_enumParser; SortMethod m_sortBy = SortMethod::DEFAULT; std::map m_sortMap = {{"ID", SortMethod::ID}, {"NAME", SortMethod::NAME}, {"NUMBER", SortMethod::NUMBER}, + {"SPECIAL-CENTERED", SortMethod::SPECIAL_CENTERED}, {"DEFAULT", SortMethod::DEFAULT}}; - std::string m_format; + std::string m_formatBefore; + std::string m_formatAfter; std::map m_iconsMap; util::RegexCollection m_windowRewriteRules; @@ -154,7 +178,28 @@ class Workspaces : public AModule, public EventHandler { std::vector m_workspacesToRemove; std::vector m_windowsToCreate; + IconLoader m_iconLoader; + bool m_enableTaskbar = false; + bool m_updateActiveWindow = false; + bool m_taskbarWithIcon = false; + bool m_taskbarWithTitle = false; + std::string m_taskbarFormatBefore; + std::string m_taskbarFormatAfter; + int m_taskbarIconSize = 16; + Gtk::Orientation m_taskbarOrientation = Gtk::ORIENTATION_HORIZONTAL; + bool m_taskbarReverseDirection = false; + util::EnumParser m_activeWindowEnumParser; + ActiveWindowPosition m_activeWindowPosition = ActiveWindowPosition::NONE; + std::map m_activeWindowPositionMap = { + {"NONE", ActiveWindowPosition::NONE}, + {"FIRST", ActiveWindowPosition::FIRST}, + {"LAST", ActiveWindowPosition::LAST}, + }; + std::string m_onClickWindow; + std::string m_currentActiveWindowAddress; + std::vector m_ignoreWorkspaces; + std::vector m_ignoreWindows; std::mutex m_mutex; const Bar& m_bar; diff --git a/include/modules/image.hpp b/include/modules/image.hpp index 7c0d014f..510dad94 100644 --- a/include/modules/image.hpp +++ b/include/modules/image.hpp @@ -31,7 +31,7 @@ class Image : public AModule { std::string path_; std::string tooltip_; int size_; - int interval_; + std::chrono::milliseconds interval_; util::command::res output_; util::SleeperThread thread_; diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 5fd0c180..4abe26bd 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -9,6 +9,7 @@ #include #include +#include #include "ALabel.hpp" #include "util/sleeper_thread.hpp" @@ -27,8 +28,8 @@ class Network : public ALabel { auto update() -> void override; private: - static const uint8_t MAX_RETRY = 5; - static const uint8_t EPOLL_MAX = 200; + static const uint8_t MAX_RETRY{5}; + static const uint8_t EPOLL_MAX{200}; static int handleEvents(struct nl_msg*, void*); static int handleEventsDone(struct nl_msg*, void*); @@ -44,44 +45,44 @@ class Network : public ALabel { void parseFreq(struct nlattr**); void parseBssid(struct nlattr**); bool associatedOrJoined(struct nlattr**); - bool checkInterface(std::string name); + bool matchInterface(const std::string& ifname, const std::vector& altnames, + std::string& matched) const; auto getInfo() -> void; const std::string getNetworkState() const; void clearIface(); - bool wildcardMatch(const std::string& pattern, const std::string& text) const; std::optional> readBandwidthUsage(); - int ifid_; - ip_addr_pref addr_pref_; - struct sockaddr_nl nladdr_ = {0}; - struct nl_sock* sock_ = nullptr; - struct nl_sock* ev_sock_ = nullptr; - int efd_; - int ev_fd_; - int nl80211_id_; + int ifid_{-1}; + ip_addr_pref addr_pref_{ip_addr_pref::IPV4}; + struct sockaddr_nl nladdr_{0}; + struct nl_sock* sock_{nullptr}; + struct nl_sock* ev_sock_{nullptr}; + int efd_{-1}; + int ev_fd_{-1}; + int nl80211_id_{-1}; std::mutex mutex_; - bool want_route_dump_; - bool want_link_dump_; - bool want_addr_dump_; - bool dump_in_progress_; - bool is_p2p_; + bool want_route_dump_{false}; + bool want_link_dump_{false}; + bool want_addr_dump_{false}; + bool dump_in_progress_{false}; + bool is_p2p_{false}; - unsigned long long bandwidth_down_total_; - unsigned long long bandwidth_up_total_; + unsigned long long bandwidth_down_total_{0}; + unsigned long long bandwidth_up_total_{0}; std::string state_; std::string essid_; std::string bssid_; - bool carrier_; + bool carrier_{false}; std::string ifname_; std::string ipaddr_; std::string ipaddr6_; std::string gwaddr_; std::string netmask_; std::string netmask6_; - int cidr_; - int cidr6_; + int cidr_{0}; + int cidr6_{0}; int32_t signal_strength_dbm_; uint8_t signal_strength_; std::string signal_strength_app_; @@ -90,9 +91,9 @@ class Network : public ALabel { util::SleeperThread thread_; util::SleeperThread thread_timer_; #ifdef WANT_RFKILL - util::Rfkill rfkill_; + util::Rfkill rfkill_{RFKILL_TYPE_WLAN}; #endif - float frequency_; + float frequency_{0}; }; } // namespace waybar::modules diff --git a/include/modules/niri/language.hpp b/include/modules/niri/language.hpp index 42b90ac4..77f0a624 100644 --- a/include/modules/niri/language.hpp +++ b/include/modules/niri/language.hpp @@ -33,6 +33,7 @@ class Language : public ALabel, public EventHandler { std::vector layouts_; unsigned current_idx_; + std::string last_short_name_; }; } // namespace waybar::modules::niri diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index 6179098c..cb6a34da 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -31,6 +31,8 @@ class Privacy : public AModule { uint iconSpacing = 4; uint iconSize = 20; uint transition_duration = 250; + std::set> ignore; + bool ignore_monitor = true; std::shared_ptr backend = nullptr; }; diff --git a/include/modules/river/layout.hpp b/include/modules/river/layout.hpp index dc78ee20..89f0c7d9 100644 --- a/include/modules/river/layout.hpp +++ b/include/modules/river/layout.hpp @@ -24,6 +24,7 @@ class Layout : public waybar::ALabel { private: const waybar::Bar &bar_; + std::string name_; struct wl_output *output_; // stores the output this module belongs to struct wl_output *focused_output_; // stores the currently focused output struct zriver_output_status_v1 *output_status_; diff --git a/include/modules/river/tags.hpp b/include/modules/river/tags.hpp index fb3eefaa..fd867346 100644 --- a/include/modules/river/tags.hpp +++ b/include/modules/river/tags.hpp @@ -21,6 +21,7 @@ class Tags : public waybar::AModule { void handle_view_tags(struct wl_array *tags); void handle_urgent_tags(uint32_t tags); + void handle_show(); void handle_primary_clicked(uint32_t tag); bool handle_button_press(GdkEventButton *event_button, uint32_t tag); diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index c5e86d37..503ab637 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -26,7 +26,7 @@ struct ToolTip { class Item : public sigc::trackable { public: Item(const std::string&, const std::string&, const Json::Value&, const Bar&); - ~Item() = default; + ~Item(); std::string bus_name; std::string object_path; diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 6cda35d7..5f12d7f2 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -21,6 +21,7 @@ class Tray : public AModule { void onRemove(std::unique_ptr& item); static inline std::size_t nb_hosts_ = 0; + bool show_passive_ = false; Gtk::Box box_; SNI::Watcher::singleton watcher_; SNI::Host host_; diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index ea58c4f0..91aa181d 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -21,7 +21,7 @@ class Language : public ALabel, public sigc::trackable { auto update() -> void override; private: - enum class DispayedShortFlag { None = 0, ShortName = 1, ShortDescription = 1 << 1 }; + enum class DisplayedShortFlag { None = 0, ShortName = 1, ShortDescription = 1 << 1 }; struct Layout { std::string full_name; @@ -58,7 +58,7 @@ class Language : public ALabel, public sigc::trackable { std::map layouts_map_; bool hide_single_; bool is_variant_displayed; - std::byte displayed_short_flag = static_cast(DispayedShortFlag::None); + std::byte displayed_short_flag = static_cast(DisplayedShortFlag::None); util::JsonParser parser_; std::mutex mutex_; diff --git a/include/modules/sway/window.hpp b/include/modules/sway/window.hpp index 427c2e81..60cd2679 100644 --- a/include/modules/sway/window.hpp +++ b/include/modules/sway/window.hpp @@ -19,10 +19,11 @@ class Window : public AAppIconLabel, public sigc::trackable { auto update() -> void override; private: - void setClass(std::string classname, bool enable); + void setClass(const std::string& classname, bool enable); void onEvent(const struct Ipc::ipc_response&); void onCmd(const struct Ipc::ipc_response&); - std::tuple + std::tuple getFocusedNode(const Json::Value& nodes, std::string& output); void getTree(); @@ -35,6 +36,7 @@ class Window : public AAppIconLabel, public sigc::trackable { std::string old_app_id_; std::size_t app_nb_; std::string shell_; + std::string marks_; int floating_count_; util::JsonParser parser_; std::mutex mutex_; diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index 58c173ec..d8a9e18a 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -48,7 +48,7 @@ class Workspaces : public AModule, public sigc::trackable { std::vector high_priority_named_; std::vector workspaces_order_; Gtk::Box box_; - std::string m_formatWindowSeperator; + std::string m_formatWindowSeparator; util::RegexCollection m_windowRewriteRules; util::JsonParser parser_; std::unordered_map buttons_; diff --git a/include/modules/systemd_failed_units.hpp b/include/modules/systemd_failed_units.hpp index 9c3fbcee..48b0074e 100644 --- a/include/modules/systemd_failed_units.hpp +++ b/include/modules/systemd_failed_units.hpp @@ -19,12 +19,15 @@ class SystemdFailedUnits : public ALabel { std::string format_ok; bool update_pending; - uint32_t nr_failed_system, nr_failed_user; + std::string system_state, user_state, overall_state; + uint32_t nr_failed_system, nr_failed_user, nr_failed; std::string last_status; Glib::RefPtr system_proxy, user_proxy; void notify_cb(const Glib::ustring &sender_name, const Glib::ustring &signal_name, const Glib::VariantContainerBase &arguments); + void RequestFailedUnits(); + void RequestSystemState(); void updateData(); }; diff --git a/include/modules/wayfire/backend.hpp b/include/modules/wayfire/backend.hpp new file mode 100644 index 00000000..9d55c820 --- /dev/null +++ b/include/modules/wayfire/backend.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace waybar::modules::wayfire { + +using EventHandler = std::function; + +struct State { + /* + ┌───────────┐ ┌───────────┐ + │ output #1 │ │ output #2 │ + └─────┬─────┘ └─────┬─────┘ + └─┐ └─────┐─ ─ ─ ─ ─ ─ ─ ─ ┐ + ┌───────┴───────┐ ┌───────┴──────┐ ┌───────┴───────┐ + │ wset #1 │ │ wset #2 │ │ wset #3 │ + │┌────────────┐ │ │┌────────────┐│ │┌────────────┐ │ + ││ workspaces │ │ ││ workspaces ││ ││ workspaces │ │ + │└─┬──────────┘ │ │└────────────┘│ │└─┬──────────┘ │ + │ │ ┌─────────┐│ └──────────────┘ │ │ ┌─────────┐│ + │ ├─┤ view #1 ││ │ └─┤ view #3 ││ + │ │ └─────────┘│ │ └─────────┘│ + │ │ ┌─────────┐│ └───────────────┘ + │ └─┤ view #2 ││ + │ └─────────┘│ + └───────────────┘ + */ + + struct Output { + size_t id; + size_t w, h; + size_t wset_idx; + }; + + struct Workspace { + size_t num_views; + size_t num_sticky_views; + }; + + struct Wset { + std::optional> output; + std::vector wss; + size_t ws_w, ws_h, ws_x, ws_y; + size_t focused_view_id; + + auto ws_idx() const { return ws_w * ws_y + ws_x; } + auto count_ws(const Json::Value& pos) -> Workspace&; + auto locate_ws(const Json::Value& geo) -> Workspace&; + auto locate_ws(const Json::Value& geo) const -> const Workspace&; + }; + + std::unordered_map outputs; + std::unordered_map wsets; + std::unordered_map views; + std::string focused_output_name; + size_t maybe_empty_focus_wset_idx = {}; + size_t vswitch_sticky_view_id = {}; + bool new_output_detected = {}; + bool vswitching = {}; + + auto update_view(const Json::Value& view) -> void; +}; + +struct Sock { + int fd; + + Sock(int fd) : fd{fd} {} + ~Sock() { close(fd); } + Sock(const Sock&) = delete; + auto operator=(const Sock&) = delete; + Sock(Sock&& rhs) noexcept { + fd = rhs.fd; + rhs.fd = -1; + } + auto& operator=(Sock&& rhs) noexcept { + fd = rhs.fd; + rhs.fd = -1; + return *this; + } +}; + +class IPC { + static std::weak_ptr instance; + Json::CharReaderBuilder reader_builder; + Json::StreamWriterBuilder writer_builder; + std::list>> handlers; + std::mutex handlers_mutex; + State state; + std::mutex state_mutex; + + IPC() { start(); } + + static auto connect() -> Sock; + auto receive(Sock& sock) -> Json::Value; + auto start() -> void; + auto root_event_handler(const std::string& event, const Json::Value& data) -> void; + auto update_state_handler(const std::string& event, const Json::Value& data) -> void; + + public: + static auto get_instance() -> std::shared_ptr; + auto send(const std::string& method, Json::Value&& data) -> Json::Value; + auto register_handler(const std::string& event, const EventHandler& handler) -> void; + auto unregister_handler(EventHandler& handler) -> void; + + auto lock_state() -> std::lock_guard { return std::lock_guard{state_mutex}; } + auto& get_outputs() const { return state.outputs; } + auto& get_wsets() const { return state.wsets; } + auto& get_views() const { return state.views; } + auto& get_focused_output_name() const { return state.focused_output_name; } +}; + +} // namespace waybar::modules::wayfire diff --git a/include/modules/wayfire/window.hpp b/include/modules/wayfire/window.hpp new file mode 100644 index 00000000..3e8cb291 --- /dev/null +++ b/include/modules/wayfire/window.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "modules/wayfire/backend.hpp" + +namespace waybar::modules::wayfire { + +class Window : public AAppIconLabel { + std::shared_ptr ipc; + EventHandler handler; + + const Bar& bar_; + std::string old_app_id_; + + public: + Window(const std::string& id, const Bar& bar, const Json::Value& config); + ~Window() override; + + auto update() -> void override; + auto update_icon_label() -> void; +}; + +} // namespace waybar::modules::wayfire diff --git a/include/modules/wayfire/workspaces.hpp b/include/modules/wayfire/workspaces.hpp new file mode 100644 index 00000000..ab7cac44 --- /dev/null +++ b/include/modules/wayfire/workspaces.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/wayfire/backend.hpp" + +namespace waybar::modules::wayfire { + +class Workspaces : public AModule { + std::shared_ptr ipc; + EventHandler handler; + + const Bar& bar_; + Gtk::Box box_; + std::vector buttons_; + + auto handleScroll(GdkEventScroll* e) -> bool override; + auto update() -> void override; + auto update_box() -> void; + + public: + Workspaces(const std::string& id, const Bar& bar, const Json::Value& config); + ~Workspaces() override; +}; + +} // namespace waybar::modules::wayfire diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index e0033e8a..0565704f 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -22,6 +22,8 @@ class Wireplumber : public ALabel { void activatePlugins(); static void updateVolume(waybar::modules::Wireplumber* self, uint32_t id); static void updateNodeName(waybar::modules::Wireplumber* self, uint32_t id); + static void updateSourceVolume(waybar::modules::Wireplumber* self, uint32_t id); + static void updateSourceName(waybar::modules::Wireplumber* self, uint32_t id); // NEW static void onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); static void onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); @@ -46,7 +48,12 @@ class Wireplumber : public ALabel { double min_step_; uint32_t node_id_{0}; std::string node_name_; + std::string source_name_; gchar* type_; + uint32_t source_node_id_; + bool source_muted_; + double source_volume_; + gchar* default_source_name_; }; } // namespace waybar::modules diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 07110dde..8dc4dadd 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -19,6 +19,7 @@ #include "bar.hpp" #include "client.hpp" #include "giomm/desktopappinfo.h" +#include "util/icon_loader.hpp" #include "util/json.hpp" #include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" @@ -89,9 +90,6 @@ class Task { std::string state_string(bool = false) const; void set_minimize_hint(); void on_button_size_allocated(Gtk::Allocation &alloc); - 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: @@ -153,7 +151,7 @@ class Taskbar : public waybar::AModule { Gtk::Box box_; std::vector tasks_; - std::vector> icon_themes_; + IconLoader icon_loader_; std::unordered_set ignore_list_; std::map app_ids_replace_map_; @@ -178,7 +176,7 @@ class Taskbar : public waybar::AModule { bool show_output(struct wl_output *) const; bool all_outputs() const; - const std::vector> &icon_themes() const; + const IconLoader &icon_loader() const; const std::unordered_set &ignore_list() const; const std::map &app_ids_replace_map() const; }; diff --git a/include/modules/wlr/workspace_manager.hpp b/include/modules/wlr/workspace_manager.hpp deleted file mode 100644 index f7cc759e..00000000 --- a/include/modules/wlr/workspace_manager.hpp +++ /dev/null @@ -1,172 +0,0 @@ -#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, std::string name); - ~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); } - auto is_empty() const -> bool { return state_ & static_cast(State::EMPTY); } - auto is_persistent() const -> bool { return persistent_; } - // 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 make_persistent() -> void; - auto handle_duplicate() -> 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), - EMPTY = (1 << 3), - }; - - 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; - bool persistent_ = 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 persistent_workspaces() -> std::vector & { return persistent_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; - auto fill_persistent_workspaces() -> void; - auto create_persistent_workspaces() -> 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; - std::vector persistent_workspaces_; - bool persistent_created_ = 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 sort_by_number_ = false; - 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 deleted file mode 100644 index cc242c94..00000000 --- a/include/modules/wlr/workspace_manager_binding.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#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); -} // namespace waybar::modules::wlr diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp index 458bc57e..340f74ee 100644 --- a/include/util/SafeSignal.hpp +++ b/include/util/SafeSignal.hpp @@ -11,6 +11,11 @@ #include #include +#ifdef __OpenBSD__ +#define SIGRTMIN SIGUSR1 - 1 +#define SIGRTMAX SIGUSR1 + 1 +#endif + namespace waybar { /** diff --git a/include/util/date.hpp b/include/util/date.hpp index a467cc56..d8653faf 100644 --- a/include/util/date.hpp +++ b/include/util/date.hpp @@ -15,7 +15,7 @@ namespace date { #if HAVE_CHRONO_TIMEZONES using namespace std::chrono; -using namespace std; +using std::format; #else using system_clock = std::chrono::system_clock; @@ -73,5 +73,3 @@ struct fmt::formatter> { } }; #endif - -using namespace date; diff --git a/include/util/gtk_icon.hpp b/include/util/gtk_icon.hpp index 44555f65..06f15abe 100644 --- a/include/util/gtk_icon.hpp +++ b/include/util/gtk_icon.hpp @@ -10,5 +10,7 @@ class DefaultGtkIconThemeWrapper { public: static bool has_icon(const std::string&); - static Glib::RefPtr load_icon(const char*, int, Gtk::IconLookupFlags); + static Glib::RefPtr load_icon( + const char*, int, Gtk::IconLookupFlags, + Glib::RefPtr style = Glib::RefPtr()); }; diff --git a/include/util/icon_loader.hpp b/include/util/icon_loader.hpp new file mode 100644 index 00000000..664e510c --- /dev/null +++ b/include/util/icon_loader.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "util/gtk_icon.hpp" + +class IconLoader { + private: + std::vector> custom_icon_themes_; + Glib::RefPtr default_icon_theme_ = Gtk::IconTheme::get_default(); + static std::vector search_prefix(); + static Glib::RefPtr get_app_info_by_name(const std::string &app_id); + static Glib::RefPtr get_desktop_app_info(const std::string &app_id); + static Glib::RefPtr load_icon_from_file(std::string const &icon_path, int size); + static std::string get_icon_name_from_icon_theme(const Glib::RefPtr &icon_theme, + const std::string &app_id); + static bool image_load_icon(Gtk::Image &image, const Glib::RefPtr &icon_theme, + Glib::RefPtr app_info, int size); + + public: + void add_custom_icon_theme(const std::string &theme_name); + bool image_load_icon(Gtk::Image &image, Glib::RefPtr app_info, + int size) const; + static Glib::RefPtr get_app_info_from_app_id_list( + const std::string &app_id_list); +}; diff --git a/include/util/kill_signal.hpp b/include/util/kill_signal.hpp new file mode 100644 index 00000000..26d156e9 --- /dev/null +++ b/include/util/kill_signal.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + +namespace waybar::util { + +enum class KillSignalAction : std::uint8_t { + TOGGLE, + RELOAD, + SHOW, + HIDE, + NOOP, +}; +const std::map userKillSignalActions = { + {"TOGGLE", KillSignalAction::TOGGLE}, + {"RELOAD", KillSignalAction::RELOAD}, + {"SHOW", KillSignalAction::SHOW}, + {"HIDE", KillSignalAction::HIDE}, + {"NOOP", KillSignalAction::NOOP}}; + +const KillSignalAction SIGNALACTION_DEFAULT_SIGUSR1 = KillSignalAction::TOGGLE; +const KillSignalAction SIGNALACTION_DEFAULT_SIGUSR2 = KillSignalAction::RELOAD; + +}; // namespace waybar::util diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp index 7b8df018..54da7d16 100644 --- a/include/util/pipewire/privacy_node_info.hpp +++ b/include/util/pipewire/privacy_node_info.hpp @@ -25,6 +25,7 @@ class PrivacyNodeInfo { std::string media_name; std::string node_name; std::string application_name; + bool is_monitor = false; std::string pipewire_access_portal_app_id; std::string application_icon_name; diff --git a/include/util/portal.hpp b/include/util/portal.hpp index 23619169..bff74b11 100644 --- a/include/util/portal.hpp +++ b/include/util/portal.hpp @@ -6,14 +6,12 @@ namespace waybar { -using namespace Gio; - enum class Appearance { UNKNOWN = 0, DARK = 1, LIGHT = 2, }; -class Portal : private DBus::Proxy { +class Portal : private Gio::DBus::Proxy { public: Portal(); void refreshAppearance(); diff --git a/include/util/prepare_for_sleep.h b/include/util/prepare_for_sleep.h index 68db8d8e..82f3b627 100644 --- a/include/util/prepare_for_sleep.h +++ b/include/util/prepare_for_sleep.h @@ -4,6 +4,6 @@ namespace waybar::util { -// Get a signal emited with value true when entering sleep, and false when exiting +// Get a signal emitted with value true when entering sleep, and false when exiting SafeSignal& prepare_for_sleep(); } // namespace waybar::util diff --git a/include/util/rfkill.hpp b/include/util/rfkill.hpp index f620db53..47ab729b 100644 --- a/include/util/rfkill.hpp +++ b/include/util/rfkill.hpp @@ -5,6 +5,8 @@ #include #include +#include + namespace waybar::util { class Rfkill : public sigc::trackable { @@ -17,7 +19,7 @@ class Rfkill : public sigc::trackable { private: enum rfkill_type rfkill_type_; - bool state_ = false; + std::atomic_bool state_ = false; int fd_ = -1; bool on_event(Glib::IOCondition cond); diff --git a/include/util/string.hpp b/include/util/string.hpp index d06557c1..cb8a6892 100644 --- a/include/util/string.hpp +++ b/include/util/string.hpp @@ -23,3 +23,26 @@ inline std::string capitalize(const std::string& str) { [](unsigned char c) { return std::toupper(c); }); return result; } + +inline std::string toLower(const std::string& str) { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +inline std::vector split(std::string_view s, std::string_view delimiter, + int max_splits = -1) { + std::vector result; + size_t pos = 0; + size_t next_pos = 0; + while ((next_pos = s.find(delimiter, pos)) != std::string::npos) { + result.push_back(std::string(s.substr(pos, next_pos - pos))); + pos = next_pos + delimiter.size(); + if (max_splits > 0 && result.size() == static_cast(max_splits)) { + break; + } + } + result.push_back(std::string(s.substr(pos))); + return result; +} diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index a3c21a20..2819aa68 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -116,7 +116,7 @@ The *battery* module displays the current capacity and state (eg. charging) of y *menu-file*: ++ typeof: string ++ Location of the menu descriptor file. There need to be an element of type - GtkMenu with id *menu* + GtkMenu with id *menu*. *menu-actions*: ++ typeof: array ++ @@ -127,6 +127,10 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: false ++ Enables this module to consume all left over space dynamically. +*events*: ++ + typeof: object ++ + Specifies commands to be executed on specific battery states. See *EVENTS* section below. + # FORMAT REPLACEMENTS *{capacity}*: Capacity in percentage @@ -166,6 +170,19 @@ The *battery* module allows one to define custom formats based on up to two fact - The state can be addressed as a CSS class in the *style.css*. The name of the CSS class is the ** of the state. Each class gets activated when the current capacity is equal to or below the configured **. - Also each state can have its own *format*. Those can be configured via *format-*. Or if you want to differentiate a bit more even as *format--*. For more information see *custom-formats*. +# EVENTS + +Every entry in the *events* object consists of a ** (typeof: *string*) and a ** (typeof: *string*). ++ +** can be in one of the following formats: + +- *on--* +- *on--* + +Where: + +- ** is either *charging* or *discharging*, +- ** is the name of one of the states specified in the *states* object, +- ** is a battery level value (between *0-100*). # EXAMPLES @@ -178,6 +195,11 @@ The *battery* module allows one to define custom formats based on up to two fact "warning": 30, "critical": 15 }, + "events": { + "on-discharging-warning": "notify-send -u normal 'Low Battery'", + "on-discharging-critical": "notify-send -u critical 'Very Low Battery'", + "on-charging-100": "notify-send -u normal 'Battery Full!'" + }, "format": "{capacity}% {icon}", "format-icons": ["", "", "", "", ""], "max-length": 25 diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 50a5fc07..b4b5d5b7 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -39,6 +39,12 @@ $XDG_CONFIG_HOME/waybar/config ++ :[ A list of timezones (as in *timezone*) to use for time display, changed using the scroll wheel. Do not specify *timezone* option when *timezones* is specified. "" represents the system's local timezone +|[ *timezone-tooltip-format* +:[ string +:[ +:[ Format to use for displaying timezones in the tooltip. When set, this allows showing + timezone information (like timezone abbreviations) in the tooltip while keeping the + main display clean. Uses the same format options as *format* |[ *locale* :[ string :[ @@ -126,6 +132,12 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ 1 :[ Value to scroll months/years forward/backward. Can be negative. Is configured under *on-scroll* option +|[ *iso8601* +:[ bool +:[ false +:[ When enabled, the calendar follows the ISO 8601 standard: weeks begin on + Monday, and the first week of the year is numbered 1. The default week format is + '{:%V}'. 3. Addressed by *clock: calendar: format* [- *Option* @@ -229,6 +241,25 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe } ``` +4. Show timezone in tooltip only + +``` +"clock": { + "interval": 60, + "format": "{:%H:%M}", + "timezone-tooltip-format": "{:%H:%M %Z}", + "timezones": [ + "", + "America/Chicago", + "America/Los_Angeles", + "Europe/Paris", + "UTC" + ], + "tooltip": true, + "tooltip-format": "{tz_list}" +} +``` + # STYLE - *#clock* diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index 287bf123..40682372 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -11,9 +11,10 @@ The *cpu* module displays the current CPU utilization. # CONFIGURATION *interval*: ++ - typeof: integer ++ + typeof: integer or float ++ default: 10 ++ - The interval in which the information gets polled. + The interval in which the information gets polled. ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. *format*: ++ typeof: string ++ diff --git a/man/waybar-custom.5.scd b/man/waybar-custom.5.scd index 309fc184..37b4c42c 100644 --- a/man/waybar-custom.5.scd +++ b/man/waybar-custom.5.scd @@ -35,15 +35,17 @@ Addressed by *custom/* See *return-type* *interval*: ++ - typeof: integer ++ + typeof: integer or float ++ The interval (in seconds) in which the information gets polled. ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ Use *once* if you want to execute the module only on startup. ++ You can update it manually with a signal. If no *interval* or *signal* is defined, it is assumed that the out script loops itself. ++ If a *signal* is defined then the script will run once on startup and will only update with a signal. *restart-interval*: ++ - typeof: integer ++ + typeof: integer or float ++ The restart interval (in seconds). ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ Can't be used with the *interval* option, so only with continuous scripts. ++ Once the script exits, it'll be re-executed after the *restart-interval*. diff --git a/man/waybar-wlr-workspaces.5.scd b/man/waybar-ext-workspaces.5.scd similarity index 69% rename from man/waybar-wlr-workspaces.5.scd rename to man/waybar-ext-workspaces.5.scd index 62d3f636..3fdae606 100644 --- a/man/waybar-wlr-workspaces.5.scd +++ b/man/waybar-ext-workspaces.5.scd @@ -10,7 +10,7 @@ The *workspaces* module displays the currently used workspaces in wayland compos # CONFIGURATION -Addressed by *wlr/workspaces* +Addressed by *ext/workspaces* *format*: ++ typeof: string ++ @@ -24,18 +24,18 @@ Addressed by *wlr/workspaces* *sort-by-name*: ++ typeof: bool ++ default: true ++ - Should workspaces be sorted by name. + Should workspaces be sorted by name. Workspace names will be sorted numerically when all names are numbers. *sort-by-coordinates*: ++ typeof: bool ++ - default: true ++ + default: false ++ 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. -*sort-by-number*: ++ +*sort-by-id*: ++ typeof: bool ++ default: false ++ - If set to true, workspace names will be sorted numerically. Takes precedence over any other sort-by option. + Should workspaces be sorted by ID. Workspace ID will be sorted numerically when all ID are numbers. Takes precedence over any other sort-by option. *all-outputs*: ++ typeof: bool ++ @@ -47,9 +47,16 @@ Addressed by *wlr/workspaces* default: false ++ If set to true only active or urgent workspaces will be shown. +*ignore-hidden*: ++ + typeof: bool ++ + default: true ++ + If set to false hidden workspaces will be shown. + # FORMAT REPLACEMENTS -*{name}*: Name of workspace assigned by compositor +*{name}*: Name of workspace assigned by compositor. + +*{id}*: ID of workspace assigned by compositor. *{icon}*: Icon, as defined in *format-icons*. @@ -57,6 +64,8 @@ Addressed by *wlr/workspaces* *activate*: Switch to workspace. +*deactivate*: Deactivate the workspace. + *close*: Close the workspace. # ICONS @@ -69,18 +78,18 @@ In addition to workspace name matching, the following *format-icons* can be set. # EXAMPLES ``` -"wlr/workspaces": { +"ext/workspaces": { "format": "{name}: {icon}", + "on-click": "activate", "format-icons": { - "1": "", - "2": "", - "3": "", - "4": "", - "5": "", + "Workspace 1": "", + "Workspace 2": "", + "Workspace 3": "", + "Workspace 4": "", "active": "", "default": "" }, - "sort-by-number": true + "sort-by-id": true } ``` diff --git a/man/waybar-gps.5.scd b/man/waybar-gps.5.scd new file mode 100644 index 00000000..e7473601 --- /dev/null +++ b/man/waybar-gps.5.scd @@ -0,0 +1,111 @@ +waybar-gps(5) "waybar-gps" "User Manual" + +# NAME + +waybar - gps module + +# DESCRIPTION + +*gps* module for gpsd. + + +# FILES + +$XDG_CONFIG_HOME/waybar/config ++ + Per user configuration file + +# ADDITIONAL FILES + +libgps lives in: + +. /usr/lib/libgps.so or /usr/lib64/libgps.so +. /usr/lib/pkgconfig/libgps.pc or /usr/lib64/pkgconfig/libgps.pc +. /usr/include/gps + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: {glyph} ++ + The text format. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: Games running: {glyph} ++ + The text format of the tooltip. + +*interval*: ++ + typeof: integer ++ + default: 5 ++ + The interval in which the GPS information gets polled (e.g. current speed). + Significant updates (e.g. the current fix mode) are updated immediately. + +*hide-disconnected*: ++ + typeof: bool ++ + default: true ++ + Defines if the module should be hidden if there is no GPS receiver. + +*hide-no-fix*: ++ + typeof: bool ++ + default: false ++ + Defines if the module should be hidden if there is no GPS fix. + +# FORMAT REPLACEMENTS + +*{mode}*: Fix mode + +*{status}*: Technology used for GPS fix. Not all GPS receivers report this. + +*{latitude}*: Latitude, decimal degrees. Can be NaN. + +*{latitude_error}*: Latitude uncertainty, meters. Can be NaN. + +*{longitude}*: Longitude, decimal degrees. Can be NaN. + +*{longitude_error}*: Longitude uncertainty, meters. Can be NaN. + +*{altitude_hae}*: Altitude, height above ellipsoid, meters. Can be NaN. + +*{altitude_msl}*: Longitude, MSL, meters. Can be NaN. + +*{altitude_error}*: Altitude uncertainty, meters. Can be NaN. + +*{speed}*: Speed over ground, meters/sec. Can be NaN. + +*{speed_error}*: Speed uncertainty, meters/sec. Can be NaN. + +*{climb}*: Vertical speed, meters/sec. Can be NaN. + +*{climb_error}*: Vertical speed uncertainty, meters/sec. Can be NaN. + +*{satellites_visible}*: Number of satellites visible from the GPS receiver. + +*{satellites_used}*: Number of satellites used for the GPS fix. + +# EXAMPLES + +``` +"gps": { + "format": "{mode}", + "format-disabled": "", // an empty format will hide the module + "format-no-fix": "No fix", + "format-fix-3d": "{status}", + "tooltip-format": "{mode}", + "tooltip-format-no-fix": "{satellites_visible} satellites visible", + "tooltip-format-fix-2d": "{satellites_used}/{satellites_visible} satellites used", + "tooltip-format-fix-3d": "Altitude: {altitude_hae}m", + "hide-disconnected": false +} +``` +# STYLE + +- *#gps* +- *#gps.disabled* Applied when GPS is disabled. +- *#gps.fix-none* Applied when GPS is present, but there is no fix. +- *#gps.fix-2d* Applied when there is a 2D fix. +- *#gps.fix-3d* Applied when there is a 3D fix. diff --git a/man/waybar-hyprland-windowcount.5.scd b/man/waybar-hyprland-windowcount.5.scd new file mode 100644 index 00000000..6a4f87d1 --- /dev/null +++ b/man/waybar-hyprland-windowcount.5.scd @@ -0,0 +1,46 @@ +waybar-hyprland-windowcount(5) + +# NAME + +waybar - hyprland window count module + +# DESCRIPTION + +The *windowcount* module displays the number of windows in the current Hyprland workspace. + +# CONFIGURATION + +Addressed by *hyprland/windowcount* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format for how information should be displayed. On {} the current workspace window count is displayed. + +*format-empty*: ++ + typeof: string ++ + Override the format when the workspace contains no windows window + +*format-windowed*: ++ + typeof: string ++ + Override the format when the workspace contains no fullscreen windows + +*format-fullscreen*: ++ + typeof: string ++ + Override the format when the workspace contains a fullscreen window + +*separate-outputs*: ++ + typeof: bool ++ + default: true ++ + Show the active workspace window count of the monitor the bar belongs to, instead of the focused workspace. + +# STYLE + +- *#windowcount* + +The following classes are applied to the entire Waybar rather than just the +windowcount widget: + +- *window#waybar.empty* When no windows are in the workspace +- *window#waybar.fullscreen* When there is a fullscreen window in the workspace; + useful with Hyprland's *fullscreen, 1* mode diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 18c39898..1d04157b 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -26,17 +26,77 @@ Addressed by *hyprland/workspaces* Regex rules to map window class to an icon or preferred method of representation for a workspace's window. Keys are the rules, while the values are the methods of representation. Values may use the placeholders {class} and {title} to use the window's original class and/or title respectively. Rules may specify `class<...>`, `title<...>`, or both in order to fine-tune the matching. - You may assign an empty value to a rule to have it ignored from generating any representation in workspaces. + You may assign an empty value to a rule to have it ignored from generating any representation in workspaces. ++ +This setting is ignored if *workspace-taskbar.enable* is set to true. -*window-rewrite-default*: +*window-rewrite-default*: ++ typeof: string ++ default: "?" ++ - The default method of representation for a workspace's window. This will be used for windows whose classes do not match any of the rules in *window-rewrite*. + The default method of representation for a workspace's window. This will be used for windows whose classes do not match any of the rules in *window-rewrite*. ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. *format-window-separator*: ++ typeof: string ++ default: " " ++ - The separator to be used between windows in a workspace. + The separator to be used between windows in a workspace. ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + +*workspace-taskbar*: ++ + typeof: object ++ + Contains settings for the workspace taskbar, an alternative mode for the workspaces module which displays the window icons as images instead of text. + + *enable*: ++ + typeof: bool ++ + default: false ++ + Enables the workspace taskbar mode. + + *update-active-window*: ++ + typeof: bool ++ + default: false ++ + If true, the active/focused window will have an 'active' class. Could cause higher CPU usage due to more frequent redraws. + + *reverse-direction*: ++ + typeof: bool ++ + default: false ++ + If true, the taskbar windows will be added in reverse order (right to left if orientation is horizontal, bottom to top if vertical). + + *active-window-position*: ++ + typeof: "none" | "first" | "last" ++ + default: "none" ++ + If set to "first", the active window will be moved at the beginning of the taskbar. If set to "last", it will be moved at the end. It will only work if *update-active-window* is set to true. + + *format*: ++ + typeof: string ++ + default: {icon} ++ + Format to use for each window in the workspace taskbar. Available placeholders are {icon} and {title}. + + *icon-size*: ++ + typeof: int ++ + default: 16 ++ + Size of the icons in the workspace taskbar. + + *icon-theme*: ++ + typeof: string | array ++ + default: [] ++ + Icon theme to use for the workspace taskbar. If an array is provided, the first theme that is found for a given icon will be used. If no theme is found (or the array is empty), the default icon theme is used. + + *orientation*: ++ + typeof: "horizontal" | "vertical" ++ + default: horizontal ++ + Direction in which the workspace taskbar is displayed. + + *ignore-list*: ++ + typeof: array ++ + default: [] ++ + Regex patterns to match against window class or window title. If a window's class OR title matches any of the patterns, it will not be shown. + + *on-click-window*: ++ + typeof: string ++ + default: "" ++ + Command to run when a window is clicked. Available placeholders are: ++ + - {address} Hyprland address of the clicked window. ++ + - {button} Pressed button number, see https://api.gtkd.org/gdk.c.types.GdkEventButton.button.html. ++ + See https://github.com/Alexays/Waybar/wiki/Module:-Hyprland#workspace-taskbars-example for a full example. *show-special*: ++ typeof: bool ++ @@ -48,6 +108,11 @@ Addressed by *hyprland/workspaces* default: false ++ If this and show-special are to true, special workspaces will be shown only if visible. +*persistent-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only persistent workspaces will be shown on bar. + *all-outputs*: ++ typeof: bool ++ default: false ++ @@ -76,6 +141,7 @@ Addressed by *hyprland/workspaces* If set to number, workspaces will sort by number. If set to name, workspaces will sort by name. If set to id, workspaces will sort by id. + If set to special-centered, workspaces will sort by default with special workspaces in the center. If none of those, workspaces will sort with default behavior. *expand*: ++ @@ -178,3 +244,6 @@ Additional to workspace name matching, the following *format-icons* can be set. - *#workspaces button.special* - *#workspaces button.urgent* - *#workspaces button.hosting-monitor* (gets applied if workspace-monitor == waybar-monitor) +- *#workspaces .workspace-label* +- *#workspaces .taskbar-window* (each window in the taskbar, only if 'workspace-taskbar.enable' is true) +- *#workspaces .taskbar-window.active* (applied to the focused window, only if 'workspace-taskbar.update-active-window' is true) diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 6d5a2170..405c8fc5 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -125,3 +125,9 @@ screensaver, also known as "presentation mode". "timeout": 30.5 } ``` + +# STYLE + +- *#idle_inhibitor* +- *#idle_inhibitor.activated* +- *#idle_inhibitor.deactivated* diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index a2dcc938..8c991265 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -24,8 +24,9 @@ The *image* module displays an image from a path. The width/height to render the image. *interval*: ++ - typeof: integer ++ + typeof: integer or float ++ The interval (in seconds) to re-render the image. ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ This is useful if the contents of *path* changes. ++ If no *interval* is defined, the image will only be rendered once. diff --git a/man/waybar-jack.5.scd b/man/waybar-jack.5.scd index 85ce7180..98adc8db 100644 --- a/man/waybar-jack.5.scd +++ b/man/waybar-jack.5.scd @@ -45,7 +45,7 @@ Addressed by *jack* The format of information displayed in the tooltip. *interval*: ++ - typeof: integer ++ + typeof: integer or float ++ default: 1 ++ The interval in which the information gets polled. diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index cc42d5a3..567c2c72 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -120,6 +120,8 @@ Addressed by *memory* *{swapAvail}*: Amount of available swap in GiB. +*{swapState}*: Signals if swap is activated or not + # EXAMPLES ``` diff --git a/man/waybar-menu.5.scd b/man/waybar-menu.5.scd index 19790ed4..798917f0 100644 --- a/man/waybar-menu.5.scd +++ b/man/waybar-menu.5.scd @@ -7,7 +7,7 @@ waybar - menu property # OVERVIEW -Some modules support a 'menu', which allows to have a popup menu whan a defined +Some modules support a 'menu', which allows to have a popup menu when a defined click is done over the module. # PROPERTIES @@ -87,15 +87,15 @@ Module config : ``` "custom/power": { "format" : "⏻ ", - "tooltip": false, - "menu": "on-click", - "menu-file": "~/.config/waybar/power_menu.xml", - "menu-actions": { - "shutdown": "shutdown", - "reboot": "reboot", - "suspend": "systemctl suspend", - "hibernate": "systemctl hibernate", - }, + "tooltip": false, + "menu": "on-click", + "menu-file": "~/.config/waybar/power_menu.xml", + "menu-actions": { + "shutdown": "shutdown", + "reboot": "reboot", + "suspend": "systemctl suspend", + "hibernate": "systemctl hibernate", + }, }, ``` @@ -104,28 +104,28 @@ Module config : - - - Suspend - - - - - Hibernate - - + + + Suspend + + + + + Hibernate + + - - Shutdown - + + Shutdown + - - Reboot - + + Reboot + diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index 84abc2e8..d310efd4 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -204,6 +204,10 @@ Addressed by *mpd* *{queueLength}*: The length of the current queue. +*{uri}*: The URI of the song relative to the MPD music directory. + +*{filename}* The last part of the URI. + *{stateIcon}*: The icon corresponding to the playing or paused status of the player (see *state-icons* option) *{consumeIcon}*: The icon corresponding the "consume" option (see *consume-icons* option) diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index 15f15395..9e146ac0 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -16,6 +16,11 @@ Addressed by *network* typeof: string ++ Use the defined interface instead of auto-detection. Accepts wildcard. +*rfkill*: ++ + typeof: bool ++ + default: true ++ + If enabled, the *disabled* format will be used when rfkill is blocking wlan interfaces. + *interval*: ++ typeof: integer ++ default: 60 ++ @@ -49,7 +54,7 @@ Addressed by *network* *format-disabled*: ++ typeof: string ++ - This format is used when the displayed interface is disabled. + This format is used when rfkill is blocking wlan interfaces. *format-icons*: ++ typeof: array/object ++ @@ -127,7 +132,7 @@ Addressed by *network* *tooltip-format-disabled*: ++ typeof: string ++ - This format is used when the displayed interface is disabled. + This format is used when rfkill is blocking wlan interfaces. *menu*: ++ typeof: string ++ @@ -157,7 +162,7 @@ Addressed by *network* *{netmask}*: The subnetmask corresponding to the IP(V4). -*{netmask6}*: The subnetmask corresponding to the IP(V6). +*{netmask6}*: The subnetmask corresponding to the IP(V6). *{cidr}*: The subnetmask corresponding to the IP(V4) in CIDR notation. @@ -171,7 +176,7 @@ Addressed by *network* *{signaldBm}*: Signal strength of the wireless network in dBm. -*{frequency}*: Frequency of the wireless network in MHz. +*{frequency}*: Frequency of the wireless network in GHz. *{bandwidthUpBits}*: Instant up speed in bits/seconds. diff --git a/man/waybar-niri-language.5.scd b/man/waybar-niri-language.5.scd index 44876fd9..78cf14f8 100644 --- a/man/waybar-niri-language.5.scd +++ b/man/waybar-niri-language.5.scd @@ -52,8 +52,8 @@ Addressed by *niri/language* ``` "niri/language": { - "format": "Lang: {long}" - "format-en": "AMERICA, HELL YEAH!" + "format": "Lang: {long}", + "format-en": "AMERICA, HELL YEAH!", "format-tr": "As bayrakları" } ``` @@ -61,3 +61,12 @@ Addressed by *niri/language* # STYLE - *#language* + +Additionally, a CSS class matching the current layout's short name is added to the widget. This +allows per-language styling, for example: + +``` +#language.us { color: #00ff00; } +#language.de { color: #ff0000; } +#language.fr { color: #0000ff; } +``` diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 0c0249ca..4be85eb3 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -70,6 +70,8 @@ Additional to workspace name matching, the following *format-icons* can be set. - *default*: Will be shown, when no string matches are found. - *focused*: Will be shown, when workspace is focused. - *active*: Will be shown, when workspace is active on its output. +- *urgent*: Will be shown, when workspace has urgent windows. +- *empty*: Will be shown, when workspace is empty. # EXAMPLES @@ -95,6 +97,7 @@ Additional to workspace name matching, the following *format-icons* can be set. - *#workspaces button* - *#workspaces button.focused*: The single focused workspace. - *#workspaces button.active*: The workspace is active (visible) on its output. +- *#workspaces button.urgent*: The workspace has one or more urgent windows. - *#workspaces button.empty*: The workspace is empty. - *#workspaces button.current_output*: The workspace is from the same output as the bar that it is displayed on. diff --git a/man/waybar-privacy.5.scd b/man/waybar-privacy.5.scd index 946fe136..1c4ef59d 100644 --- a/man/waybar-privacy.5.scd +++ b/man/waybar-privacy.5.scd @@ -37,6 +37,17 @@ the screen or playing audio. default: false ++ Enables this module to consume all left over space dynamically. +*ignore-monitor* ++ + typeof: bool ++ + default: true ++ + Ignore streams with *stream.monitor* property. + +*ignore* ++ + typeof: array of objects ++ + default: [] ++ + Additional streams to be ignored. See *IGNORE CONFIGURATION* for++ + more information. + # MODULES CONFIGURATION *type*: ++ @@ -54,6 +65,14 @@ the screen or playing audio. default: 24 ++ The size of each icon in the tooltip. +# IGNORE CONFIGURATION + +*type*: ++ + typeof: string + +*name*: ++ + typeof: string + # EXAMPLES ``` @@ -77,6 +96,17 @@ the screen or playing audio. "tooltip": true, "tooltip-icon-size": 24 } + ], + "ignore-monitor": true, + "ignore": [ + { + "type": "audio-in", + "name": "cava" + }, + { + "type": "screenshare", + "name": "obs" + } ] }, ``` diff --git a/man/waybar-river-layout.5.scd b/man/waybar-river-layout.5.scd index 78e03634..a259b9ca 100644 --- a/man/waybar-river-layout.5.scd +++ b/man/waybar-river-layout.5.scd @@ -83,6 +83,7 @@ Addressed by *river/layout* - *#layout* - *#layout.focused* Applied when the output this module's bar belongs to is focused. +- *#layout.* Applied when the output this module's bar belongs uses this layout. # SEE ALSO diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index a7eb4f05..6fa9170b 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -89,6 +89,11 @@ Addressed by *sway/window* default: false ++ If the workspace itself is focused and the workspace contains nodes or floating_nodes, show the workspace name. If not set, text remains empty but styles according to nodes in the workspace are still applied. +*show-hidden-marks*: ++ + typeof: bool ++ + default: false ++ + For the *{marks}* format replacement, include hidden marks that start with an underscore. + *rewrite*: ++ typeof: object ++ Rules to rewrite the module format output. See *rewrite rules*. @@ -117,6 +122,8 @@ Addressed by *sway/window* *{shell}*: The shell of the focused window. It's 'xwayland' when the window is running through xwayland, otherwise, it's 'xdg-shell'. +*{marks}*: Marks of the window. + # REWRITE RULES *rewrite* is an object where keys are regular expressions and values are diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 1c4360e9..25b59eb9 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -31,6 +31,11 @@ Addressed by *sway/workspaces* default: false ++ If set to false, you can scroll to cycle through workspaces. If set to true this behaviour is disabled. +*reverse-scroll*: ++ + typeof: bool ++ + default: false ++ + If set to false, scrolling up will switch to the previous workspace and scrolling down will switch to the next workspace. If set to true, the behavior will be reversed. + *disable-click*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd index 92e74e9d..8d7c980a 100644 --- a/man/waybar-systemd-failed-units.5.scd +++ b/man/waybar-systemd-failed-units.5.scd @@ -62,6 +62,12 @@ Addressed by *systemd-failed-units* *{nr_failed}*: Number of total failed units. +*{systemd_state}:* State of the systemd system session + +*{user_state}:* State of the systemd user session + +*{overall_state}:* Overall state of the systemd and user session. ("Ok" or "Degraded") + # EXAMPLES ``` diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 923d643d..1e19ec82 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -40,7 +40,7 @@ Addressed by *temperature* The threshold before it is considered critical (Celsius). *interval*: ++ - typeof: integer ++ + typeof: integer or float ++ default: 10 ++ The interval in which the information gets polled. @@ -160,4 +160,5 @@ Addressed by *temperature* # STYLE - *#temperature* +- *#temperature.warning* - *#temperature.critical* diff --git a/man/waybar-wayfire-window.5.scd b/man/waybar-wayfire-window.5.scd new file mode 100644 index 00000000..290b0c65 --- /dev/null +++ b/man/waybar-wayfire-window.5.scd @@ -0,0 +1,82 @@ +waybar-wayfire-window(5) + +# NAME + +waybar - wayfire window module + +# DESCRIPTION + +The *window* module displays the title of the currently focused window in wayfire. + +# CONFIGURATION + +Addressed by *wayfire/window* + +*format*: ++ + typeof: string ++ + default: {title} ++ + The format, how information should be displayed. On {} the current window title is displayed. + +*rewrite*: ++ + typeof: object ++ + Rules to rewrite window title. See *rewrite rules*. + +*icon*: ++ + typeof: bool ++ + default: false ++ + Option to hide the application icon. + +*icon-size*: ++ + typeof: integer ++ + default: 24 ++ + Option to change the size of the application icon. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +See the output of "wayfire msg windows" for examples + +*{title}*: The current title of the focused window. + +*{app_id}*: The current app ID of the focused window. + +# REWRITE RULES + +*rewrite* is an object where keys are regular expressions and values are +rewrite rules if the expression matches. Rules may contain references to +captures of the expression. + +Regular expression and replacement follow ECMA-script rules. + +If no expression matches, the title is left unchanged. + +Invalid expressions (e.g., mismatched parentheses) are skipped. + +# EXAMPLES + +``` +"wayfire/window": { + "format": "{}", + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } +} +``` + +# STYLE + +- *#window* +- *window#waybar.empty #window* When no windows are on the workspace + +The following classes are applied to the entire Waybar rather than just the +window widget: + +- *window#waybar.empty* When no windows are in the workspace +- *window#waybar.solo* When only one window is on the workspace +- *window#waybar.* Where *app-id* is the app ID of the only window on + the workspace diff --git a/man/waybar-wayfire-workspaces.5.scd b/man/waybar-wayfire-workspaces.5.scd new file mode 100644 index 00000000..53a179e8 --- /dev/null +++ b/man/waybar-wayfire-workspaces.5.scd @@ -0,0 +1,86 @@ +waybar-wayfire-workspaces(5) + +# NAME + +waybar - wayfire workspaces module + +# DESCRIPTION + +The *workspaces* module displays the currently used workspaces in wayfire. + +# CONFIGURATION + +Addressed by *wayfire/workspaces* + +*format*: ++ + typeof: string ++ + default: {value} ++ + The format, how information should be displayed. + +*format-icons*: ++ + typeof: array ++ + Based on the workspace name, index and state, the corresponding icon gets selected. See *icons*. + +*disable-click*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can click to change workspace. If set to true this behaviour is disabled. + +*disable-markup*: ++ + typeof: bool ++ + default: false ++ + If set to true, button label will escape pango markup. + +*current-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only the active or focused workspace will be shown. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +*{icon}*: Icon, as defined in *format-icons*. + +*{index}*: Index of the workspace on its output. + +*{output}*: Output where the workspace is located. + +# ICONS + +Additional to workspace name matching, the following *format-icons* can be set. + +- *default*: Will be shown, when no string matches are found. +- *focused*: Will be shown, when workspace is focused. + +# EXAMPLES + +``` +"wayfire/workspaces": { + "format": "{icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "focused": "", + "default": "" + } +} +``` + +# Style + +- *#workspaces button* +- *#workspaces button.focused*: The single focused workspace. +- *#workspaces button.empty*: The workspace is empty. +- *#workspaces button.current_output*: The workspace is from the same output as + the bar that it is displayed on. diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 5bb62724..1b799275 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -86,7 +86,7 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( *no-center* ++ typeof: bool ++ default: false ++ - Option to disable the center modules fully usefull together with expand-\*. + Option to disable the center modules fully useful together with expand-\*. *spacing* ++ typeof: integer ++ @@ -151,6 +151,20 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( default: *false* ++ Option to enable reloading the css style if a modification is detected on the style sheet file or any imported css files. +*on-sigusr1* ++ + typeof: string ++ + default: *toggle* ++ + Action that is performed when receiving SIGUSR1 kill signal. ++ + Possible values: *show*, *hide*, *toggle*, *reload*, *noop*. ++ + Default value: *toggle*. + +*on-sigusr2* ++ + typeof: string ++ + default: *reload* ++ + Action that is performed when receiving SIGUSR2 kill signal. ++ + Possible values: *show*, *hide*, *toggle*, *reload*, *noop*. ++ + Default value: *reload*. + # MODULE FORMAT You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). @@ -206,14 +220,36 @@ A minimal *config* file could look like this: Waybar accepts the following signals: *SIGUSR1* - Toggles the bar visibility (hides if shown, shows if hidden) + By default toggles the bar visibility (hides if shown, shows if hidden) *SIGUSR2* - Reloads (resets) the bar + By default reloads (resets) the bar *SIGINT* Quits the bar For example, to toggle the bar programmatically, you can invoke `killall -SIGUSR1 waybar`. +## User signal configuration + +Config parameters *on-sigusr1* and *on-sigusr2* change what happens when bars receive +*SIGUSR1* and *SIGUSR2* signals. + +This means that commands `killall -SIGUSR1 waybar` and `killall -SIGUSR2 waybar` +can perform user-configured action. + +It also means that if an external script has the PID of the bar then it can +perform more complex `show`/`hide`/`reload` logic for each instance of Waybar. +One can find the PID e.g. by doing `pgrep -a waybar` which could then match +by config name or other parameters. + +## Kill parameter meanings + +*show* Switches state to visible (per bar). +*hide* Switches state to hidden (per bar). +*toggle* Switches state between visible and hidden (per bar). +*reload* Reloads all waybars of current waybar process (basically equivalent to +restarting with updated config which sets initial visibility values). +*noop* Does nothing when the kill signal is received. + # MULTI OUTPUT CONFIGURATION ## Limit a configuration to some outputs @@ -272,6 +308,17 @@ When positioning Waybar on the left or right side of the screen, sometimes it's Valid options for the "rotate" property are: 0, 90, 180, and 270. +## Swapping icon and label + +If a module displays both a label and an icon, it might be desirable to swap them (for instance, for panels on the left or right of the screen, or for user adopting a right-to-left script). This can be achieved with the "swap-icon-label" property, taking a boolean. Example: +``` +{ + "sway/window": { + "swap-icon-label": true + } +} +``` + ## Grouping modules Module groups allow stacking modules in any direction. By default, when the bar is positioned on the top or bottom of the screen, modules in a group are stacked vertically. Likewise, when positioned on the left or right, modules in a group are stacked horizontally. This can be changed with the "orientation" property. @@ -390,4 +437,4 @@ A group may hide all but one element, showing them only on mouse hover. In order # SEE ALSO *sway-output(5)* -*waybar-styles(5)" +*waybar-styles(5)* diff --git a/meson.build b/meson.build index 7f9854d5..822c566b 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'waybar', 'cpp', 'c', - version: '0.12.0', + version: '0.14.0', license: 'MIT', meson_version: '>= 0.59.0', default_options : [ @@ -69,7 +69,7 @@ is_openbsd = host_machine.system() == 'openbsd' thread_dep = dependency('threads') fmt = dependency('fmt', version : ['>=8.1.1'], fallback : ['fmt', 'fmt_dep']) -spdlog = dependency('spdlog', version : ['>=1.10.0'], fallback : ['spdlog', 'spdlog_dep'], default_options : ['external_fmt=enabled', 'std_format=disabled', 'tests=disabled']) +spdlog = dependency('spdlog', version : ['>=1.15.2'], fallback : ['spdlog', 'spdlog_dep'], default_options : ['std_format=disabled', 'tests=disabled']) wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols') @@ -93,6 +93,7 @@ libmpdclient = dependency('libmpdclient', required: get_option('mpd')) xkbregistry = dependency('xkbregistry') libjack = dependency('jack', required: get_option('jack')) libwireplumber = dependency('wireplumber-0.5', required: get_option('wireplumber')) +libgps = dependency('libgps', required: get_option('gps')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() @@ -182,6 +183,7 @@ src_files = files( 'src/util/sanitize_str.cpp', 'src/util/rewrite_string.cpp', 'src/util/gtk_icon.cpp', + 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', 'src/util/css_reload_helper.cpp' ) @@ -275,6 +277,17 @@ if true man_files += files('man/waybar-wlr-taskbar.5.scd') endif +if wayland_protos.version().version_compare('>=1.39') + add_project_arguments('-DHAVE_EXT_WORKSPACES', language: 'cpp') + src_files += files( + 'src/modules/ext/workspace_manager.cpp', + 'src/modules/ext/workspace_manager_binding.cpp', + ) + man_files += files( + 'man/waybar-ext-workspaces.5.scd', + ) +endif + if true add_project_arguments('-DHAVE_RIVER', language: 'cpp') src_files += files( @@ -306,6 +319,7 @@ if true 'src/modules/hyprland/language.cpp', 'src/modules/hyprland/submap.cpp', 'src/modules/hyprland/window.cpp', + 'src/modules/hyprland/windowcount.cpp', 'src/modules/hyprland/workspace.cpp', 'src/modules/hyprland/workspaces.cpp', 'src/modules/hyprland/windowcreationpayload.cpp', @@ -333,6 +347,15 @@ if get_option('niri') ) endif +if true + add_project_arguments('-DHAVE_WAYFIRE', language: 'cpp') + src_files += files( + 'src/modules/wayfire/backend.cpp', + 'src/modules/wayfire/window.cpp', + 'src/modules/wayfire/workspaces.cpp', + ) +endif + if get_option('login-proxy') add_project_arguments('-DHAVE_LOGIN_PROXY', language: 'cpp') endif @@ -474,17 +497,6 @@ else man_files += files('man/waybar-clock.5.scd') endif -if get_option('experimental') - add_project_arguments('-DHAVE_WLR_WORKSPACES', language: 'cpp') - src_files += files( - 'src/modules/wlr/workspace_manager.cpp', - 'src/modules/wlr/workspace_manager_binding.cpp', - ) - man_files += files( - 'man/waybar-wlr-workspaces.5.scd', - ) -endif - cava = dependency('cava', version : '>=0.10.4', required: get_option('cava'), @@ -493,10 +505,16 @@ cava = dependency('cava', if cava.found() add_project_arguments('-DHAVE_LIBCAVA', language: 'cpp') - src_files += files('src/modules/cava.cpp') + src_files += files('src/modules/cava/cava.cpp', 'src/modules/cava/cava_backend.cpp') man_files += files('man/waybar-cava.5.scd') endif +if libgps.found() + add_project_arguments('-DHAVE_LIBGPS', language: 'cpp') + src_files += files('src/modules/gps.cpp') + man_files += files('man/waybar-gps.5.scd') +endif + subdir('protocol') app_resources = [] @@ -535,7 +553,8 @@ executable( libsndio, tz_dep, xkbregistry, - cava + cava, + libgps ], include_directories: inc_dirs, install: true, diff --git a/meson_options.txt b/meson_options.txt index d83fe01f..c0f63f28 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -21,3 +21,4 @@ option('wireplumber', type: 'feature', value: 'auto', description: 'Enable suppo option('cava', type: 'feature', value: 'auto', description: 'Enable support for Cava') option('niri', type: 'boolean', description: 'Enable support for niri') option('login-proxy', type: 'boolean', description: 'Enable interfacing with dbus login interface') +option('gps', type: 'feature', value: 'auto', description: 'Enable support for gps') diff --git a/nix/default.nix b/nix/default.nix index 4cfd75c0..a96d0b3f 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -30,9 +30,12 @@ in # nixpkgs checks version, no need when building locally nativeInstallCheckInputs = [ ]; - buildInputs = (builtins.filter (p: p.pname != "wireplumber") oldAttrs.buildInputs) ++ [ - pkgs.wireplumber - ]; + buildInputs = + (builtins.filter (p: p.pname != "wireplumber" && p.pname != "gps") oldAttrs.buildInputs) + ++ [ + pkgs.wireplumber + pkgs.gpsd + ]; postUnpack = '' pushd "$sourceRoot" diff --git a/protocol/ext-workspace-unstable-v1.xml b/protocol/ext-workspace-unstable-v1.xml deleted file mode 100644 index 24410b62..00000000 --- a/protocol/ext-workspace-unstable-v1.xml +++ /dev/null @@ -1,306 +0,0 @@ - - - - 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 cd9a77b1..b16113b2 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -26,12 +26,17 @@ client_protocols = [ [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], [wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-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'], ['dwl-ipc-unstable-v2.xml'], ] +if wayland_protos.version().version_compare('>=1.39') + client_protocols += [ + [wl_protocol_dir, 'staging/ext-workspace/ext-workspace-v1.xml'] + ] +endif + client_protos_src = [] client_protos_headers = [] diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index d1bb72b4..524d4d2a 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -112,6 +112,7 @@ class PlayerManager: logger.debug(f"Metadata changed for player {player.props.player_name}") player_name = player.props.player_name artist = player.get_artist() + artist = artist.replace("&", "&") title = player.get_title() title = title.replace("&", "&") diff --git a/resources/style.css b/resources/style.css index 7e830285..bcfcb5c8 100644 --- a/resources/style.css +++ b/resources/style.css @@ -63,7 +63,7 @@ button:hover { background: rgba(0, 0, 0, 0.2); } -#workspaces button.focused { +#workspaces button.focused, #workspaces button.active { background-color: #64727D; box-shadow: inset 0 -3px #ffffff; } diff --git a/resources/waybar.service.in b/resources/waybar.service.in index 81ac6779..18bac54c 100644 --- a/resources/waybar.service.in +++ b/resources/waybar.service.in @@ -1,5 +1,5 @@ [Unit] -Description=Highly customizable Wayland bar for Sway and Wlroots based compositors. +Description=Highly customizable Wayland bar for Sway and Wlroots based compositors Documentation=https://github.com/Alexays/Waybar/wiki/ PartOf=graphical-session.target After=graphical-session.target diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index 3f47eff1..a309a6e0 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -63,7 +63,8 @@ std::optional getDesktopFilePath(const std::string& app_identifier, return {}; } - const auto data_dirs = Glib::get_system_data_dirs(); + auto data_dirs = Glib::get_system_data_dirs(); + data_dirs.insert(data_dirs.begin(), Glib::get_user_data_dir()); for (const auto& data_dir : data_dirs) { const auto data_app_dir = data_dir + "/applications/"; auto desktop_file_suffix = app_identifier + ".desktop"; diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index d7ee666e..a20c22e9 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -1,6 +1,7 @@ #include "AIconLabel.hpp" #include +#include namespace waybar { @@ -17,14 +18,39 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const box_.get_style_context()->add_class(id); } - box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); + int rot = 0; + + if (config_["rotate"].isUInt()) { + rot = config["rotate"].asUInt() % 360; + if ((rot % 90) != 00) rot = 0; + rot /= 90; + } + + if ((rot % 2) == 0) + box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); + else + box_.set_orientation(Gtk::Orientation::ORIENTATION_VERTICAL); box_.set_name(name); int spacing = config_["icon-spacing"].isInt() ? config_["icon-spacing"].asInt() : 8; box_.set_spacing(spacing); - box_.add(image_); - box_.add(label_); + bool swap_icon_label = false; + if (config_["swap-icon-label"].isNull()) { + } else if (config_["swap-icon-label"].isBool()) { + swap_icon_label = config_["swap-icon-label"].asBool(); + } else { + spdlog::warn("'swap-icon-label' must be a bool, found '{}'. Using default value (false).", + config_["swap-icon-label"].asString()); + } + + if ((rot == 0 || rot == 3) ^ swap_icon_label) { + box_.add(image_); + box_.add(label_); + } else { + box_.add(label_); + box_.add(image_); + } event_box_.add(box_); } diff --git a/src/ALabel.cpp b/src/ALabel.cpp index c218e402..4e6d3349 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -17,10 +17,17 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st config["format-alt"].isString() || config["menu"].isString() || enable_click, enable_scroll), format_(config_["format"].isString() ? config_["format"].asString() : format), + + // Leave the default option outside of the std::max(1L, ...), because the zero value + // (default) is used in modules/custom.cpp to make the difference between + // two types of custom scripts. Fixes #4521. interval_(config_["interval"] == "once" - ? std::chrono::seconds::max() - : std::chrono::seconds( - config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)), + ? std::chrono::milliseconds::max() + : std::chrono::milliseconds( + (config_["interval"].isNumeric() + ? std::max(1L, // Minimum 1ms due to millisecond precision + static_cast(config_["interval"].asDouble()) * 1000) + : 1000 * (long)interval))), default_format_(format_) { label_.set_name(name); if (!id.empty()) { @@ -200,7 +207,7 @@ std::string ALabel::getState(uint8_t value, bool lesser) { } } // Sort states - std::sort(states.begin(), states.end(), [&lesser](auto& a, auto& b) { + std::ranges::sort(states.begin(), states.end(), [&lesser](auto& a, auto& b) { return lesser ? a.second < b.second : a.second > b.second; }); std::string valid_state; diff --git a/src/AModule.cpp b/src/AModule.cpp index 5abb779a..c6fdff3e 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -83,7 +83,7 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } AModule::~AModule() { - for (const auto& pid : pid_) { + for (const auto& pid : pid_children_) { if (pid != -1) { killpg(pid, SIGTERM); } @@ -93,15 +93,15 @@ AModule::~AModule() { auto AModule::update() -> void { // Run user-provided update handler if configured if (config_["on-update"].isString()) { - pid_.push_back(util::command::forkExec(config_["on-update"].asString())); + pid_children_.push_back(util::command::forkExec(config_["on-update"].asString())); } } // Get mapping between event name and module action name -// Then call overrided doAction in order to call appropriate module action +// Then call overridden doAction in order to call appropriate module action auto AModule::doAction(const std::string& name) -> void { if (!name.empty()) { const std::map::const_iterator& recA{eventActionMap_.find(name)}; - // Call overrided action if derrived class has implemented it + // Call overridden action if derived class has implemented it if (recA != eventActionMap_.cend() && name != recA->second) this->doAction(recA->second); } } @@ -172,6 +172,10 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { // Popup the menu gtk_widget_show_all(GTK_WIDGET(menu_)); gtk_menu_popup_at_pointer(GTK_MENU(menu_), reinterpret_cast(e)); + // Manually reset prelight to make sure the module doesn't stay in a hover state + if (auto* module = event_box_.get_child(); module != nullptr) { + module->unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + } } } // Second call user scripts @@ -182,7 +186,7 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { format.clear(); } if (!format.empty()) { - pid_.push_back(util::command::forkExec(format)); + pid_children_.push_back(util::command::forkExec(format)); } dp.emit(); return true; @@ -267,7 +271,7 @@ bool AModule::handleScroll(GdkEventScroll* e) { this->AModule::doAction(eventName); // Second call user scripts if (config_[eventName].isString()) - pid_.push_back(util::command::forkExec(config_[eventName].asString())); + pid_children_.push_back(util::command::forkExec(config_[eventName].asString())); dp.emit(); return true; diff --git a/src/bar.cpp b/src/bar.cpp index b7737d36..70029a2a 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -8,6 +8,8 @@ #include "client.hpp" #include "factory.hpp" #include "group.hpp" +#include "util/enum.hpp" +#include "util/kill_signal.hpp" #ifdef HAVE_SWAY #include "modules/sway/bar.hpp" @@ -277,6 +279,32 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) } #endif + waybar::util::EnumParser m_signalActionEnumParser; + const auto& configSigusr1 = config["on-sigusr1"]; + if (configSigusr1.isString()) { + auto strSigusr1 = configSigusr1.asString(); + try { + onSigusr1 = + m_signalActionEnumParser.parseStringToEnum(strSigusr1, util::userKillSignalActions); + } catch (const std::invalid_argument& e) { + onSigusr1 = util::SIGNALACTION_DEFAULT_SIGUSR1; + spdlog::warn( + "Invalid string representation for on-sigusr1. Falling back to default mode (toggle)."); + } + } + const auto& configSigusr2 = config["on-sigusr2"]; + if (configSigusr2.isString()) { + auto strSigusr2 = configSigusr2.asString(); + try { + onSigusr2 = + m_signalActionEnumParser.parseStringToEnum(strSigusr2, util::userKillSignalActions); + } catch (const std::invalid_argument& e) { + onSigusr2 = util::SIGNALACTION_DEFAULT_SIGUSR2; + spdlog::warn( + "Invalid string representation for on-sigusr2. Falling back to default mode (reload)."); + } + } + setupWidgets(); window.show_all(); @@ -405,7 +433,18 @@ void waybar::Bar::onMap(GdkEventAny* /*unused*/) { /* * Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor). */ - auto* gdk_window = window.get_window()->gobj(); + auto gdk_window_ref = window.get_window(); + if (!gdk_window_ref) { + spdlog::warn("Failed to get GDK window during onMap, deferring surface initialization"); + return; + } + + auto* gdk_window = gdk_window_ref->gobj(); + if (!gdk_window) { + spdlog::warn("GDK window object is null during onMap, deferring surface initialization"); + return; + } + surface = gdk_wayland_window_get_wl_surface(gdk_window); configureGlobalOffset(gdk_window_get_width(gdk_window), gdk_window_get_height(gdk_window)); @@ -422,6 +461,8 @@ void waybar::Bar::setVisible(bool value) { } void waybar::Bar::toggle() { setVisible(!visible); } +void waybar::Bar::show() { setVisible(true); } +void waybar::Bar::hide() { setVisible(false); } // Converting string to button code rn as to avoid doing it later void waybar::Bar::setupAltFormatKeyForModule(const std::string& module_name) { @@ -479,6 +520,9 @@ void waybar::Bar::handleSignal(int signal) { } } +waybar::util::KillSignalAction waybar::Bar::getOnSigusr1Action() { return this->onSigusr1; } +waybar::util::KillSignalAction waybar::Bar::getOnSigusr2Action() { return this->onSigusr2; } + void waybar::Bar::getModules(const Factory& factory, const std::string& pos, waybar::Group* group = nullptr) { auto module_list = group != nullptr ? config[pos]["modules"] : config[pos]; @@ -496,7 +540,11 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, auto vertical = (group != nullptr ? group->getBox().get_orientation() : box_.get_orientation()) == Gtk::ORIENTATION_VERTICAL; - auto* group_module = new waybar::Group(id_name, class_name, config[ref], vertical); + auto group_config = config[ref]; + if (group_config["modules"].isNull()) { + spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); + } + auto* group_module = new waybar::Group(id_name, class_name, group_config, vertical); getModules(factory, ref, group_module); module = group_module; } else { @@ -545,7 +593,6 @@ auto waybar::Bar::setupWidgets() -> void { if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) { box_.set_center_widget(center_); } else { - spdlog::error("No fixed center_"); box_.pack_start(center_, true, expand_center); } } @@ -569,13 +616,13 @@ auto waybar::Bar::setupWidgets() -> void { if (!no_center) { for (auto const& module : modules_center_) { - center_.pack_start(*module, false, false); + center_.pack_start(*module, module->expandEnabled(), module->expandEnabled()); } } std::reverse(modules_right_.begin(), modules_right_.end()); for (auto const& module : modules_right_) { - right_.pack_end(*module, false, false); + right_.pack_end(*module, module->expandEnabled(), module->expandEnabled()); } } diff --git a/src/client.cpp b/src/client.cpp index 63a9276a..946780db 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -86,7 +86,7 @@ void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_ } } } catch (const std::exception &e) { - std::cerr << e.what() << '\n'; + spdlog::warn("caught exception in zxdg_output_v1_listener::done: {}", e.what()); } } @@ -97,7 +97,7 @@ void waybar::Client::handleOutputName(void *data, struct zxdg_output_v1 * /*xdg_ auto &output = client->getOutput(data); output.name = name; } catch (const std::exception &e) { - std::cerr << e.what() << '\n'; + spdlog::warn("caught exception in zxdg_output_v1_listener::name: {}", e.what()); } } @@ -106,13 +106,13 @@ void waybar::Client::handleOutputDescription(void *data, struct zxdg_output_v1 * auto *client = waybar::Client::inst(); try { auto &output = client->getOutput(data); - const char *open_paren = strrchr(description, '('); // Description format: "identifier (name)" - size_t identifier_length = open_paren - description; - output.identifier = std::string(description, identifier_length - 1); + auto s = std::string(description); + auto pos = s.find(" ("); + output.identifier = pos != std::string::npos ? s.substr(0, pos) : s; } catch (const std::exception &e) { - std::cerr << e.what() << '\n'; + spdlog::warn("caught exception in zxdg_output_v1_listener::description: {}", e.what()); } } @@ -151,15 +151,19 @@ void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr mon const std::string waybar::Client::getStyle(const std::string &style, std::optional appearance = std::nullopt) { + auto gtk_settings = Gtk::Settings::get_default(); std::optional css_file; + if (style.empty()) { std::vector search_files; switch (appearance.value_or(portal->getAppearance())) { case waybar::Appearance::LIGHT: search_files.emplace_back("style-light.css"); + gtk_settings->property_gtk_application_prefer_dark_theme() = false; break; case waybar::Appearance::DARK: search_files.emplace_back("style-dark.css"); + gtk_settings->property_gtk_application_prefer_dark_theme() = true; break; case waybar::Appearance::UNKNOWN: break; @@ -169,9 +173,11 @@ const std::string waybar::Client::getStyle(const std::string &style, } else { css_file = style; } + if (!css_file) { throw std::runtime_error("Missing required resource files"); } + spdlog::info("Using CSS file {}", css_file.value()); return css_file.value(); }; diff --git a/src/config.cpp b/src/config.cpp index 7096ba89..145056dd 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -2,7 +2,11 @@ #include #include +#ifndef __OpenBSD__ #include +#else +#include +#endif #include #include @@ -34,6 +38,7 @@ std::vector Config::tryExpandPath(const std::string &base, spdlog::debug("Try expanding: {}", path.string()); std::vector results; +#ifndef __OpenBSD__ wordexp_t p; if (wordexp(path.c_str(), &p, 0) == 0) { for (size_t i = 0; i < p.we_wordc; i++) { @@ -44,6 +49,18 @@ std::vector Config::tryExpandPath(const std::string &base, } wordfree(&p); } +#else + glob_t p; + if (glob(path.c_str(), 0, NULL, &p) == 0) { + for (size_t i = 0; i < p.gl_pathc; i++) { + if (access(p.gl_pathv[i], F_OK) == 0) { + results.emplace_back(p.gl_pathv[i]); + spdlog::debug("Found config file: {}", p.gl_pathv[i]); + } + } + globfree(&p); + } +#endif return results; } @@ -89,19 +106,49 @@ void Config::setupConfig(Json::Value &dst, const std::string &config_file, int d mergeConfig(dst, tmp_config); } +std::vector Config::findIncludePath(const std::string &name, + const std::vector &dirs) { + auto match1 = tryExpandPath(name, ""); + if (!match1.empty()) { + return match1; + } + if (const char *dir = std::getenv(Config::CONFIG_PATH_ENV)) { + if (auto res = tryExpandPath(dir, name); !res.empty()) { + return res; + } + } + for (const auto &dir : dirs) { + if (auto res = tryExpandPath(dir, name); !res.empty()) { + return res; + } + } + + return {}; +} + 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()); - for (const auto &match : tryExpandPath(include.asString(), "")) { - setupConfig(config, match, depth + 1); + auto matches = findIncludePath(include.asString()); + if (!matches.empty()) { + for (const auto &match : matches) { + setupConfig(config, match, depth + 1); + } + } else { + spdlog::warn("Unable to find resource file: {}", include.asString()); } } } else if (includes.isString()) { spdlog::info("Including resource file: {}", includes.asString()); - for (const auto &match : tryExpandPath(includes.asString(), "")) { - setupConfig(config, match, depth + 1); + auto matches = findIncludePath(includes.asString()); + if (!matches.empty()) { + for (const auto &match : matches) { + setupConfig(config, match, depth + 1); + } + } else { + spdlog::warn("Unable to find resource file: {}", includes.asString()); } } } diff --git a/src/factory.cpp b/src/factory.cpp index 1483397d..7828ce75 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -17,8 +17,8 @@ #ifdef HAVE_WLR_TASKBAR #include "modules/wlr/taskbar.hpp" #endif -#ifdef HAVE_WLR_WORKSPACES -#include "modules/wlr/workspace_manager.hpp" +#ifdef HAVE_EXT_WORKSPACES +#include "modules/ext/workspace_manager.hpp" #endif #ifdef HAVE_RIVER #include "modules/river/layout.hpp" @@ -34,6 +34,7 @@ #include "modules/hyprland/language.hpp" #include "modules/hyprland/submap.hpp" #include "modules/hyprland/window.hpp" +#include "modules/hyprland/windowcount.hpp" #include "modules/hyprland/workspaces.hpp" #endif #ifdef HAVE_NIRI @@ -41,6 +42,10 @@ #include "modules/niri/window.hpp" #include "modules/niri/workspaces.hpp" #endif +#ifdef HAVE_WAYFIRE +#include "modules/wayfire/window.hpp" +#include "modules/wayfire/workspaces.hpp" +#endif #if defined(__FreeBSD__) || defined(__linux__) #include "modules/battery.hpp" #endif @@ -104,11 +109,14 @@ #include "modules/wireplumber.hpp" #endif #ifdef HAVE_LIBCAVA -#include "modules/cava.hpp" +#include "modules/cava/cava.hpp" #endif #ifdef HAVE_SYSTEMD_MONITOR #include "modules/systemd_failed_units.hpp" #endif +#ifdef HAVE_LIBGPS +#include "modules/gps.hpp" +#endif #include "modules/cffi.hpp" #include "modules/custom.hpp" #include "modules/image.hpp" @@ -170,9 +178,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]); } #endif -#ifdef HAVE_WLR_WORKSPACES - if (ref == "wlr/workspaces") { - return new waybar::modules::wlr::WorkspaceManager(id, bar_, config_[name]); +#ifdef HAVE_EXT_WORKSPACES + if (ref == "ext/workspaces") { + return new waybar::modules::ext::WorkspaceManager(id, bar_, config_[name]); } #endif #ifdef HAVE_RIVER @@ -201,6 +209,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "hyprland/window") { return new waybar::modules::hyprland::Window(id, bar_, config_[name]); } + if (ref == "hyprland/windowcount") { + return new waybar::modules::hyprland::WindowCount(id, bar_, config_[name]); + } if (ref == "hyprland/language") { return new waybar::modules::hyprland::Language(id, bar_, config_[name]); } @@ -221,6 +232,14 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "niri/workspaces") { return new waybar::modules::niri::Workspaces(id, bar_, config_[name]); } +#endif +#ifdef HAVE_WAYFIRE + if (ref == "wayfire/window") { + return new waybar::modules::wayfire::Window(id, bar_, config_[name]); + } + if (ref == "wayfire/workspaces") { + return new waybar::modules::wayfire::Workspaces(id, bar_, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); @@ -324,13 +343,18 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, #endif #ifdef HAVE_LIBCAVA if (ref == "cava") { - return new waybar::modules::Cava(id, config_[name]); + return new waybar::modules::cava::Cava(id, config_[name]); } #endif #ifdef HAVE_SYSTEMD_MONITOR if (ref == "systemd-failed-units") { return new waybar::modules::SystemdFailedUnits(id, config_[name]); } +#endif +#ifdef HAVE_LIBGPS + if (ref == "gps") { + return new waybar::modules::Gps(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/main.cpp b/src/main.cpp index 045b2cd4..021f8e1a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -6,67 +7,150 @@ #include #include +#include "bar.hpp" #include "client.hpp" +#include "util/SafeSignal.hpp" std::mutex reap_mtx; std::list reap; -volatile bool reload; -void* signalThread(void* args) { - int err; - int signum; - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGCHLD); +static int signal_pipe_write_fd; + +// Write a single signal to `signal_pipe_write_fd`. +// This function is set as a signal handler, so it must be async-signal-safe. +static void writeSignalToPipe(int signum) { + ssize_t amt = write(signal_pipe_write_fd, &signum, sizeof(int)); + + // There's not much we can safely do inside of a signal handler. + // Let's just ignore any errors. + (void)amt; +} + +// This initializes `signal_pipe_write_fd`, and sets up signal handlers. +// +// This function will run forever, emitting every `SIGUSR1`, `SIGUSR2`, +// `SIGINT`, `SIGCHLD`, and `SIGRTMIN + 1`...`SIGRTMAX` signal received +// to `signal_handler`. +static void catchSignals(waybar::SafeSignal& signal_handler) { + int fd[2]; + pipe(fd); + + int signal_pipe_read_fd = fd[0]; + signal_pipe_write_fd = fd[1]; + + // This pipe should be able to buffer ~thousands of signals. If it fills up, + // we'll drop signals instead of blocking. + + // We can't allow the write end to block because we'll be writing to it in a + // signal handler, which could interrupt the loop that's reading from it and + // deadlock. + + fcntl(signal_pipe_write_fd, F_SETFL, O_NONBLOCK); + + std::signal(SIGUSR1, writeSignalToPipe); + std::signal(SIGUSR2, writeSignalToPipe); + std::signal(SIGINT, writeSignalToPipe); + std::signal(SIGCHLD, writeSignalToPipe); + + for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { + std::signal(sig, writeSignalToPipe); + } while (true) { - err = sigwait(&mask, &signum); - if (err != 0) { - spdlog::error("sigwait failed: {}", strerror(errno)); + int signum; + ssize_t amt = read(signal_pipe_read_fd, &signum, sizeof(int)); + if (amt < 0) { + spdlog::error("read from signal pipe failed with error {}, closing thread", strerror(errno)); + break; + } + + if (amt != sizeof(int)) { continue; } - switch (signum) { - case SIGCHLD: - spdlog::debug("Received SIGCHLD in signalThread"); - if (!reap.empty()) { - reap_mtx.lock(); - for (auto it = reap.begin(); it != reap.end(); ++it) { - if (waitpid(*it, nullptr, WNOHANG) == *it) { - spdlog::debug("Reaped child with PID: {}", *it); - it = reap.erase(it); - } - } - reap_mtx.unlock(); - } - break; - default: - spdlog::debug("Received signal with number {}, but not handling", signum); - break; - } + signal_handler.emit(signum); } } -void startSignalThread() { - int err; - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGCHLD); +waybar::util::KillSignalAction getActionForBar(waybar::Bar* bar, int signal) { + switch (signal) { + case SIGUSR1: + return bar->getOnSigusr1Action(); + case SIGUSR2: + return bar->getOnSigusr2Action(); + default: + return waybar::util::KillSignalAction::NOOP; + } +} - // Block SIGCHLD so it can be handled by the signal thread - // Any threads created by this one (the main thread) should not - // modify their signal mask to unblock SIGCHLD - err = pthread_sigmask(SIG_BLOCK, &mask, nullptr); - if (err != 0) { - spdlog::error("pthread_sigmask failed in startSignalThread: {}", strerror(err)); - exit(1); +void handleUserSignal(int signal, bool& reload) { + int i = 0; + for (auto& bar : waybar::Client::inst()->bars) { + switch (getActionForBar(bar.get(), signal)) { + case waybar::util::KillSignalAction::HIDE: + spdlog::debug("Visibility 'hide' for bar ", i); + bar->hide(); + break; + case waybar::util::KillSignalAction::SHOW: + spdlog::debug("Visibility 'show' for bar ", i); + bar->show(); + break; + case waybar::util::KillSignalAction::TOGGLE: + spdlog::debug("Visibility 'toggle' for bar ", i); + bar->toggle(); + break; + case waybar::util::KillSignalAction::RELOAD: + spdlog::info("Reloading..."); + reload = true; + waybar::Client::inst()->reset(); + return; + case waybar::util::KillSignalAction::NOOP: + break; + } + i++; + } +} + +// Must be called on the main thread. +// +// If this signal should restart or close the bar, this function will write +// `true` or `false`, respectively, into `reload`. +static void handleSignalMainThread(int signum, bool& reload) { + if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) { + for (auto& bar : waybar::Client::inst()->bars) { + bar->handleSignal(signum); + } + return; } - pthread_t thread_id; - err = pthread_create(&thread_id, nullptr, signalThread, nullptr); - if (err != 0) { - spdlog::error("pthread_create failed in startSignalThread: {}", strerror(err)); - exit(1); + switch (signum) { + case SIGUSR1: + handleUserSignal(SIGUSR1, reload); + break; + case SIGUSR2: + handleUserSignal(SIGUSR2, reload); + break; + case SIGINT: + spdlog::info("Quitting."); + reload = false; + waybar::Client::inst()->reset(); + break; + case SIGCHLD: + spdlog::debug("Received SIGCHLD in signalThread"); + if (!reap.empty()) { + reap_mtx.lock(); + for (auto it = reap.begin(); it != reap.end(); ++it) { + if (waitpid(*it, nullptr, WNOHANG) == *it) { + spdlog::debug("Reaped child with PID: {}", *it); + it = reap.erase(it); + } + } + reap_mtx.unlock(); + } + break; + default: + spdlog::debug("Received signal with number {}, but not handling", signum); + break; } } @@ -74,32 +158,16 @@ int main(int argc, char* argv[]) { try { auto* client = waybar::Client::inst(); - std::signal(SIGUSR1, [](int /*signal*/) { - for (auto& bar : waybar::Client::inst()->bars) { - bar->toggle(); - } - }); + bool reload; - std::signal(SIGUSR2, [](int /*signal*/) { - spdlog::info("Reloading..."); - reload = true; - waybar::Client::inst()->reset(); - }); + waybar::SafeSignal posix_signal_received; + posix_signal_received.connect([&](int signum) { handleSignalMainThread(signum, reload); }); - std::signal(SIGINT, [](int /*signal*/) { - spdlog::info("Quitting."); - reload = false; - waybar::Client::inst()->reset(); - }); + std::thread signal_thread([&]() { catchSignals(posix_signal_received); }); - for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { - std::signal(sig, [](int sig) { - for (auto& bar : waybar::Client::inst()->bars) { - bar->handleSignal(sig); - } - }); - } - startSignalThread(); + // Every `std::thread` must be joined or detached. + // This thread should run forever, so detach it. + signal_thread.detach(); auto ret = 0; do { diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index ff58951c..3c50f1d1 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -38,10 +38,20 @@ auto waybar::modules::Backlight::update() -> void { event_box_.show(); const uint8_t percent = best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max()); - std::string desc = fmt::format(fmt::runtime(format_), fmt::arg("percent", percent), + + // Get the state and apply state-specific format if available + auto state = getState(percent); + std::string current_format = format_; + if (!state.empty()) { + std::string state_format_name = "format-" + state; + if (config_[state_format_name].isString()) { + current_format = config_[state_format_name].asString(); + } + } + + std::string desc = fmt::format(fmt::runtime(current_format), fmt::arg("percent", percent), fmt::arg("icon", getIcon(percent))); label_.set_markup(desc); - getState(percent); if (tooltipEnabled()) { std::string tooltip_format; if (config_["tooltip-format"].isString()) { diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 44481448..138432d0 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -1,14 +1,16 @@ #include "modules/battery.hpp" #include +#include + +#include "util/command.hpp" #if defined(__FreeBSD__) #include #endif #include -#include waybar::modules::Battery::Battery(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "battery", id, "{capacity}%", 60), bar_(bar) { + : ALabel(config, "battery", id, "{capacity}%", 60), last_event_(""), bar_(bar) { #if defined(__linux__) battery_watch_fd_ = inotify_init1(IN_CLOEXEC); if (battery_watch_fd_ == -1) { @@ -26,6 +28,7 @@ waybar::modules::Battery::Battery(const std::string& id, const Bar& bar, const J throw std::runtime_error("Could not watch for battery plug/unplug"); } #endif + spdlog::debug("battery: worker interval is {}", interval_.count()); worker(); } @@ -677,18 +680,22 @@ auto waybar::modules::Battery::update() -> void { } auto status_pretty = status; // Transform to lowercase and replace space with dash - std::transform(status.begin(), status.end(), status.begin(), - [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); + std::ranges::transform(status.begin(), status.end(), status.begin(), + [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); auto format = format_; auto state = getState(capacity, true); + processEvents(state, status, capacity); setBarClass(state); auto time_remaining_formatted = formatTimeRemaining(time_remaining); if (tooltipEnabled()) { std::string tooltip_text_default; std::string tooltip_format = "{timeTo}"; if (time_remaining != 0) { - std::string time_to = std::string("Time to ") + ((time_remaining > 0) ? "empty" : "full"); - tooltip_text_default = time_to + ": " + time_remaining_formatted; + if (time_remaining > 0) { + tooltip_text_default = std::string("Empty in ") + time_remaining_formatted; + } else { + tooltip_text_default = std::string("Full in ") + time_remaining_formatted; + } } else { tooltip_text_default = status_pretty; } @@ -701,7 +708,7 @@ auto waybar::modules::Battery::update() -> void { } else if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text( + label_.set_tooltip_markup( fmt::format(fmt::runtime(tooltip_format), fmt::arg("timeTo", tooltip_text_default), fmt::arg("power", power), fmt::arg("capacity", capacity), fmt::arg("time", time_remaining_formatted), fmt::arg("cycles", cycles), @@ -767,3 +774,25 @@ void waybar::modules::Battery::setBarClass(std::string& state) { bar_.window.get_style_context()->add_class(new_class); } } + +void waybar::modules::Battery::processEvents(std::string& state, std::string& status, + uint8_t capacity) { + // There are no events specified, skip + auto events = config_["events"]; + if (!events.isObject() || events.empty()) { + return; + } + std::string event_name = fmt::format("on-{}-{}", status == "discharging" ? status : "charging", + state.empty() ? std::to_string(capacity) : state); + if (last_event_ != event_name) { + spdlog::debug("battery: triggering event {}", event_name); + if (events[event_name].isString()) { + std::string exec = events[event_name].asString(); + // Execute the command if it is not empty + if (!exec.empty()) { + util::command::exec(exec, ""); + } + } + last_event_ = event_name; + } +} diff --git a/src/modules/cava.cpp b/src/modules/cava.cpp deleted file mode 100644 index 405a351a..00000000 --- a/src/modules/cava.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include "modules/cava.hpp" - -#include - -waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) - : ALabel(config, "cava", id, "{}", 60, false, false, false) { - // Load waybar module config - char cfgPath[PATH_MAX]; - cfgPath[0] = '\0'; - - if (config_["cava_config"].isString()) strcpy(cfgPath, config_["cava_config"].asString().data()); - // Load cava config - error_.length = 0; - - if (!load_config(cfgPath, &prm_, false, &error_)) { - spdlog::error("Error loading config. {0}", error_.message); - exit(EXIT_FAILURE); - } - - // Override cava parameters by the user config - prm_.inAtty = 0; - prm_.output = cava::output_method::OUTPUT_RAW; - strcpy(prm_.data_format, "ascii"); - strcpy(prm_.raw_target, "/dev/stdout"); - prm_.ascii_range = config_["format-icons"].size() - 1; - - prm_.bar_width = 2; - prm_.bar_spacing = 0; - prm_.bar_height = 32; - prm_.bar_width = 1; - prm_.orientation = cava::ORIENT_TOP; - prm_.xaxis = cava::xaxis_scale::NONE; - prm_.mono_opt = cava::AVERAGE; - prm_.autobars = 0; - prm_.gravity = 0; - prm_.integral = 1; - - if (config_["framerate"].isInt()) prm_.framerate = config_["framerate"].asInt(); - if (config_["autosens"].isInt()) prm_.autosens = config_["autosens"].asInt(); - if (config_["sensitivity"].isInt()) prm_.sens = config_["sensitivity"].asInt(); - if (config_["bars"].isInt()) prm_.fixedbars = config_["bars"].asInt(); - if (config_["lower_cutoff_freq"].isNumeric()) - prm_.lower_cut_off = config_["lower_cutoff_freq"].asLargestInt(); - if (config_["higher_cutoff_freq"].isNumeric()) - prm_.upper_cut_off = config_["higher_cutoff_freq"].asLargestInt(); - if (config_["sleep_timer"].isInt()) prm_.sleep_timer = config_["sleep_timer"].asInt(); - if (config_["method"].isString()) - prm_.input = cava::input_method_by_name(config_["method"].asString().c_str()); - if (config_["source"].isString()) prm_.audio_source = config_["source"].asString().data(); - if (config_["sample_rate"].isNumeric()) prm_.samplerate = config_["sample_rate"].asLargestInt(); - if (config_["sample_bits"].isInt()) prm_.samplebits = config_["sample_bits"].asInt(); - if (config_["stereo"].isBool()) prm_.stereo = config_["stereo"].asBool(); - if (config_["reverse"].isBool()) prm_.reverse = config_["reverse"].asBool(); - if (config_["bar_delimiter"].isInt()) prm_.bar_delim = config_["bar_delimiter"].asInt(); - if (config_["monstercat"].isBool()) prm_.monstercat = config_["monstercat"].asBool(); - if (config_["waves"].isBool()) prm_.waves = config_["waves"].asBool(); - if (config_["noise_reduction"].isDouble()) - prm_.noise_reduction = config_["noise_reduction"].asDouble(); - if (config_["input_delay"].isInt()) - fetch_input_delay_ = std::chrono::seconds(config_["input_delay"].asInt()); - if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool(); - if (config_["format_silent"].isString()) format_silent_ = config_["format_silent"].asString(); - // Make cava parameters configuration - plan_ = new cava::cava_plan{}; - - audio_raw_.height = prm_.ascii_range; - audio_data_.format = -1; - audio_data_.source = new char[1 + strlen(prm_.audio_source)]; - audio_data_.source[0] = '\0'; - strcpy(audio_data_.source, prm_.audio_source); - - audio_data_.rate = 0; - audio_data_.samples_counter = 0; - audio_data_.channels = 2; - audio_data_.IEEE_FLOAT = 0; - - audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels; - audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8; - - audio_data_.cava_in = new double[audio_data_.cava_buffer_size]{0.0}; - - audio_data_.terminate = 0; - audio_data_.suspendFlag = false; - input_source_ = get_input(&audio_data_, &prm_); - - if (!input_source_) { - spdlog::error("cava API didn't provide input audio source method"); - exit(EXIT_FAILURE); - } - // Calculate delay for Update() thread - frame_time_milsec_ = std::chrono::milliseconds((int)(1e3 / prm_.framerate)); - - // Init cava plan, audio_raw structure - audio_raw_init(&audio_data_, &audio_raw_, &prm_, plan_); - if (!plan_) spdlog::error("cava plan is not provided"); - audio_raw_.previous_frame[0] = -1; // For first Update() call need to rePaint text message - // Read audio source trough cava API. Cava orginizes this process via infinity loop - thread_fetch_input_ = [this] { - thread_fetch_input_.sleep_for(fetch_input_delay_); - input_source_(&audio_data_); - }; - - thread_ = [this] { - dp.emit(); - thread_.sleep_for(frame_time_milsec_); - }; -} - -waybar::modules::Cava::~Cava() { - thread_fetch_input_.stop(); - thread_.stop(); - delete plan_; - plan_ = nullptr; -} - -void upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { - if (delta == std::chrono::seconds{0}) { - delta += std::chrono::seconds{1}; - delay += delta; - } -} - -void downThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { - if (delta > std::chrono::seconds{0}) { - delay -= delta; - delta -= std::chrono::seconds{1}; - } -} - -auto waybar::modules::Cava::update() -> void { - if (audio_data_.suspendFlag) return; - silence_ = true; - - for (int i{0}; i < audio_data_.input_buffer_size; ++i) { - if (audio_data_.cava_in[i]) { - silence_ = false; - sleep_counter_ = 0; - break; - } - } - - if (silence_ && prm_.sleep_timer != 0) { - if (sleep_counter_ <= - (int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { - ++sleep_counter_; - silence_ = false; - } - } - - if (!silence_ || prm_.sleep_timer == 0) { - downThreadDelay(frame_time_milsec_, suspend_silence_delay_); - // Process: execute cava - pthread_mutex_lock(&audio_data_.lock); - cava::cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out, - plan_); - if (audio_data_.samples_counter > 0) audio_data_.samples_counter = 0; - pthread_mutex_unlock(&audio_data_.lock); - - // Do transformation under raw data - audio_raw_fetch(&audio_raw_, &prm_, &rePaint_, plan_); - - if (rePaint_ == 1) { - text_.clear(); - - for (int i{0}; i < audio_raw_.number_of_bars; ++i) { - audio_raw_.previous_frame[i] = audio_raw_.bars[i]; - text_.append( - getIcon((audio_raw_.bars[i] > prm_.ascii_range) ? prm_.ascii_range : audio_raw_.bars[i], - "", prm_.ascii_range + 1)); - if (prm_.bar_delim != 0) text_.push_back(prm_.bar_delim); - } - - label_.set_markup(text_); - label_.show(); - ALabel::update(); - label_.get_style_context()->add_class("updated"); - } - - label_.get_style_context()->remove_class("silent"); - } else { - upThreadDelay(frame_time_milsec_, suspend_silence_delay_); - if (hide_on_silence_) - label_.hide(); - else if (config_["format_silent"].isString()) - label_.set_markup(format_silent_); - - label_.get_style_context()->add_class("silent"); - label_.get_style_context()->remove_class("updated"); - } -} - -auto waybar::modules::Cava::doAction(const std::string& name) -> void { - if ((actionMap_[name])) { - (this->*actionMap_[name])(); - } else - spdlog::error("Cava. Unsupported action \"{0}\"", name); -} - -// Cava actions -void waybar::modules::Cava::pause_resume() { - pthread_mutex_lock(&audio_data_.lock); - if (audio_data_.suspendFlag) { - audio_data_.suspendFlag = false; - pthread_cond_broadcast(&audio_data_.resumeCond); - downThreadDelay(frame_time_milsec_, suspend_silence_delay_); - } else { - audio_data_.suspendFlag = true; - upThreadDelay(frame_time_milsec_, suspend_silence_delay_); - } - pthread_mutex_unlock(&audio_data_.lock); -} diff --git a/src/modules/cava/cava.cpp b/src/modules/cava/cava.cpp new file mode 100644 index 00000000..a2a74606 --- /dev/null +++ b/src/modules/cava/cava.cpp @@ -0,0 +1,51 @@ +#include "modules/cava/cava.hpp" + +#include + +waybar::modules::cava::Cava::Cava(const std::string& id, const Json::Value& config) + : ALabel(config, "cava", id, "{}", 60, false, false, false), + backend_{waybar::modules::cava::CavaBackend::inst(config)} { + if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool(); + if (config_["format_silent"].isString()) format_silent_ = config_["format_silent"].asString(); + + ascii_range_ = backend_->getAsciiRange(); + backend_->signal_update().connect(sigc::mem_fun(*this, &Cava::onUpdate)); + backend_->signal_silence().connect(sigc::mem_fun(*this, &Cava::onSilence)); + backend_->Update(); +} + +auto waybar::modules::cava::Cava::doAction(const std::string& name) -> void { + if ((actionMap_[name])) { + (this->*actionMap_[name])(); + } else + spdlog::error("Cava. Unsupported action \"{0}\"", name); +} + +// Cava actions +void waybar::modules::cava::Cava::pause_resume() { backend_->doPauseResume(); } +auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void { + if (silence_) { + label_.get_style_context()->remove_class("silent"); + label_.get_style_context()->add_class("updated"); + } + label_text_.clear(); + for (auto& ch : input) + label_text_.append(getIcon((ch > ascii_range_) ? ascii_range_ : ch, "", ascii_range_ + 1)); + + label_.set_markup(label_text_); + label_.show(); + ALabel::update(); + silence_ = false; +} +auto waybar::modules::cava::Cava::onSilence() -> void { + if (!silence_) { + label_.get_style_context()->remove_class("updated"); + + if (hide_on_silence_) + label_.hide(); + else if (config_["format_silent"].isString()) + label_.set_markup(format_silent_); + silence_ = true; + label_.get_style_context()->add_class("silent"); + } +} diff --git a/src/modules/cava/cava_backend.cpp b/src/modules/cava/cava_backend.cpp new file mode 100644 index 00000000..ec32261c --- /dev/null +++ b/src/modules/cava/cava_backend.cpp @@ -0,0 +1,223 @@ +#include "modules/cava/cava_backend.hpp" + +#include + +std::shared_ptr waybar::modules::cava::CavaBackend::inst( + const Json::Value& config) { + static auto* backend = new CavaBackend(config); + static std::shared_ptr backend_ptr{backend}; + return backend_ptr; +} + +waybar::modules::cava::CavaBackend::CavaBackend(const Json::Value& config) { + // Load waybar module config + char cfgPath[PATH_MAX]; + cfgPath[0] = '\0'; + + if (config["cava_config"].isString()) strcpy(cfgPath, config["cava_config"].asString().data()); + // Load cava config + error_.length = 0; + + if (!load_config(cfgPath, &prm_, false, &error_)) { + spdlog::error("cava backend. Error loading config. {0}", error_.message); + exit(EXIT_FAILURE); + } + + // Override cava parameters by the user config + prm_.inAtty = 0; + prm_.output = ::cava::output_method::OUTPUT_RAW; + strcpy(prm_.data_format, "ascii"); + strcpy(prm_.raw_target, "/dev/stdout"); + prm_.ascii_range = config["format-icons"].size() - 1; + + prm_.bar_width = 2; + prm_.bar_spacing = 0; + prm_.bar_height = 32; + prm_.bar_width = 1; + prm_.orientation = ::cava::ORIENT_TOP; + prm_.xaxis = ::cava::xaxis_scale::NONE; + prm_.mono_opt = ::cava::AVERAGE; + prm_.autobars = 0; + prm_.gravity = 0; + prm_.integral = 1; + + if (config["framerate"].isInt()) prm_.framerate = config["framerate"].asInt(); + // Calculate delay for Update() thread + frame_time_milsec_ = std::chrono::milliseconds((int)(1e3 / prm_.framerate)); + if (config["autosens"].isInt()) prm_.autosens = config["autosens"].asInt(); + if (config["sensitivity"].isInt()) prm_.sens = config["sensitivity"].asInt(); + if (config["bars"].isInt()) prm_.fixedbars = config["bars"].asInt(); + if (config["lower_cutoff_freq"].isNumeric()) + prm_.lower_cut_off = config["lower_cutoff_freq"].asLargestInt(); + if (config["higher_cutoff_freq"].isNumeric()) + prm_.upper_cut_off = config["higher_cutoff_freq"].asLargestInt(); + if (config["sleep_timer"].isInt()) prm_.sleep_timer = config["sleep_timer"].asInt(); + if (config["method"].isString()) + prm_.input = ::cava::input_method_by_name(config["method"].asString().c_str()); + if (config["source"].isString()) prm_.audio_source = config["source"].asString().data(); + if (config["sample_rate"].isNumeric()) prm_.samplerate = config["sample_rate"].asLargestInt(); + if (config["sample_bits"].isInt()) prm_.samplebits = config["sample_bits"].asInt(); + if (config["stereo"].isBool()) prm_.stereo = config["stereo"].asBool(); + if (config["reverse"].isBool()) prm_.reverse = config["reverse"].asBool(); + if (config["bar_delimiter"].isInt()) prm_.bar_delim = config["bar_delimiter"].asInt(); + if (config["monstercat"].isBool()) prm_.monstercat = config["monstercat"].asBool(); + if (config["waves"].isBool()) prm_.waves = config["waves"].asBool(); + if (config["noise_reduction"].isDouble()) + prm_.noise_reduction = config["noise_reduction"].asDouble(); + if (config["input_delay"].isInt()) + fetch_input_delay_ = std::chrono::seconds(config["input_delay"].asInt()); + + // Make cava parameters configuration + plan_ = new ::cava::cava_plan{}; + + audio_raw_.height = prm_.ascii_range; + audio_data_.format = -1; + audio_data_.source = new char[1 + strlen(prm_.audio_source)]; + audio_data_.source[0] = '\0'; + strcpy(audio_data_.source, prm_.audio_source); + + audio_data_.rate = 0; + audio_data_.samples_counter = 0; + audio_data_.channels = 2; + audio_data_.IEEE_FLOAT = 0; + + audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels; + audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8; + + audio_data_.cava_in = new double[audio_data_.cava_buffer_size]{0.0}; + + audio_data_.terminate = 0; + audio_data_.suspendFlag = false; + input_source_ = get_input(&audio_data_, &prm_); + + if (!input_source_) { + spdlog::error("cava backend API didn't provide input audio source method"); + exit(EXIT_FAILURE); + } + + // Init cava plan, audio_raw structure + audio_raw_init(&audio_data_, &audio_raw_, &prm_, plan_); + if (!plan_) spdlog::error("cava backend plan is not provided"); + audio_raw_.previous_frame[0] = -1; // For first Update() call need to rePaint text message + // Read audio source trough cava API. Cava orginizes this process via infinity loop + read_thread_ = [this] { + try { + input_source_(&audio_data_); + } catch (const std::runtime_error& e) { + spdlog::warn("Cava backend. Read source error: {0}", e.what()); + } + read_thread_.sleep_for(fetch_input_delay_); + }; + + thread_ = [this] { + doUpdate(); + thread_.sleep_for(frame_time_milsec_); + }; +} + +waybar::modules::cava::CavaBackend::~CavaBackend() { + thread_.stop(); + read_thread_.stop(); + delete plan_; + plan_ = nullptr; +} + +static void upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { + if (delta == std::chrono::seconds{0}) { + delta += std::chrono::seconds{1}; + delay += delta; + } +} + +static void downThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { + if (delta > std::chrono::seconds{0}) { + delay -= delta; + delta -= std::chrono::seconds{1}; + } +} + +bool waybar::modules::cava::CavaBackend::isSilence() { + for (int i{0}; i < audio_data_.input_buffer_size; ++i) { + if (audio_data_.cava_in[i]) { + return false; + } + } + + return true; +} + +int waybar::modules::cava::CavaBackend::getAsciiRange() { return prm_.ascii_range; } + +// Process: execute cava +void waybar::modules::cava::CavaBackend::invoke() { + pthread_mutex_lock(&audio_data_.lock); + ::cava::cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out, + plan_); + if (audio_data_.samples_counter > 0) audio_data_.samples_counter = 0; + pthread_mutex_unlock(&audio_data_.lock); +} + +// Do transformation under raw data +void waybar::modules::cava::CavaBackend::execute() { + invoke(); + audio_raw_fetch(&audio_raw_, &prm_, &re_paint_, plan_); + + if (re_paint_ == 1) { + output_.clear(); + for (int i{0}; i < audio_raw_.number_of_bars; ++i) { + audio_raw_.previous_frame[i] = audio_raw_.bars[i]; + output_.push_back(audio_raw_.bars[i]); + if (prm_.bar_delim != 0) output_.push_back(prm_.bar_delim); + } + } +} + +void waybar::modules::cava::CavaBackend::doPauseResume() { + pthread_mutex_lock(&audio_data_.lock); + if (audio_data_.suspendFlag) { + audio_data_.suspendFlag = false; + pthread_cond_broadcast(&audio_data_.resumeCond); + downThreadDelay(frame_time_milsec_, suspend_silence_delay_); + } else { + audio_data_.suspendFlag = true; + upThreadDelay(frame_time_milsec_, suspend_silence_delay_); + } + pthread_mutex_unlock(&audio_data_.lock); +} + +waybar::modules::cava::CavaBackend::type_signal_update +waybar::modules::cava::CavaBackend::signal_update() { + return m_signal_update_; +} + +waybar::modules::cava::CavaBackend::type_signal_silence +waybar::modules::cava::CavaBackend::signal_silence() { + return m_signal_silence_; +} + +void waybar::modules::cava::CavaBackend::Update() { doUpdate(true); } + +void waybar::modules::cava::CavaBackend::doUpdate(bool force) { + if (audio_data_.suspendFlag && !force) return; + + silence_ = isSilence(); + if (!silence_) sleep_counter_ = 0; + + if (silence_ && prm_.sleep_timer != 0) { + if (sleep_counter_ <= + (int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { + ++sleep_counter_; + silence_ = false; + } + } + + if (!silence_ || prm_.sleep_timer == 0) { + downThreadDelay(frame_time_milsec_, suspend_silence_delay_); + execute(); + if (re_paint_ == 1 || force) m_signal_update_.emit(output_); + } else { + upThreadDelay(frame_time_milsec_, suspend_silence_delay_); + if (silence_ != silence_prev_ || force) m_signal_silence_.emit(); + } + silence_prev_ = silence_; +} diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 269fa765..62706944 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -1,5 +1,6 @@ #include "modules/clock.hpp" +#include #include #include @@ -16,6 +17,7 @@ #include #endif +using namespace date; namespace fmt_lib = waybar::util::date::format; waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) @@ -25,8 +27,12 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) m_tooltip_{new Gtk::Label()}, cldInTooltip_{m_tlpFmt_.find("{" + kCldPlaceholder + "}") != std::string::npos}, cldYearShift_{January / 1 / 1900}, + cldMonShift_{year(1900) / January}, tzInTooltip_{m_tlpFmt_.find("{" + kTZPlaceholder + "}") != std::string::npos}, tzCurrIdx_{0}, + tzTooltipFormat_{config_["timezone-tooltip-format"].isString() + ? config_["timezone-tooltip-format"].asString() + : ""}, ordInTooltip_{m_tlpFmt_.find("{" + kOrdPlaceholder + "}") != std::string::npos} { m_tlpText_ = m_tlpFmt_; @@ -60,8 +66,8 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) if (cldInTooltip_) { if (config_[kCldPlaceholder]["mode"].isString()) { const std::string cfgMode{config_[kCldPlaceholder]["mode"].asString()}; - const std::map monthModes{{"month", CldMode::MONTH}, - {"year", CldMode::YEAR}}; + const std::map monthModes{{"month", CldMode::MONTH}, + {"year", CldMode::YEAR}}; if (monthModes.find(cfgMode) != monthModes.end()) cldMode_ = monthModes.at(cfgMode); else @@ -70,6 +76,11 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) "using instead", cfgMode); } + + if (config_[kCldPlaceholder]["iso8601"].isBool()) { + iso8601Calendar_ = config_[kCldPlaceholder]["iso8601"].asBool(); + } + if (config_[kCldPlaceholder]["weeks-pos"].isString()) { if (config_[kCldPlaceholder]["weeks-pos"].asString() == "left") cldWPos_ = WS::LEFT; if (config_[kCldPlaceholder]["weeks-pos"].asString() == "right") cldWPos_ = WS::RIGHT; @@ -89,23 +100,25 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) fmtMap_.insert({2, "{}"}); if (config_[kCldPlaceholder]["format"]["today"].isString()) { fmtMap_.insert({3, config_[kCldPlaceholder]["format"]["today"].asString()}); - cldBaseDay_ = - year_month_day{ - floor(zoned_time{local_zone(), system_clock::now()}.get_local_time())} - .day(); + auto local_time = zoned_time{local_zone(), system_clock::now()}.get_local_time(); + cldBaseDay_ = year_month_day{floor(local_time)}.day(); } else fmtMap_.insert({3, "{}"}); if (config_[kCldPlaceholder]["format"]["weeks"].isString() && cldWPos_ != WS::HIDDEN) { + const auto defaultFmt = + iso8601Calendar_ ? "{:%V}" : ((first_day_of_week() == Monday) ? "{:%W}" : "{:%U}"); fmtMap_.insert({4, std::regex_replace(config_[kCldPlaceholder]["format"]["weeks"].asString(), - std::regex("\\{\\}"), - (first_day_of_week() == Monday) ? "{:%W}" : "{:%U}")}); + std::regex("\\{\\}"), defaultFmt)}); Glib::ustring tmp{std::regex_replace(fmtMap_[4], std::regex("]+>|\\{.*\\}"), "")}; cldWnLen_ += tmp.size(); } else { - if (cldWPos_ != WS::HIDDEN) - fmtMap_.insert({4, (first_day_of_week() == Monday) ? "{:%W}" : "{:%U}"}); - else + if (cldWPos_ != WS::HIDDEN) { + const auto defaultFmt = + iso8601Calendar_ ? "{:%V}" : ((first_day_of_week() == Monday) ? "{:%W}" : "{:%U}"); + fmtMap_.insert({4, defaultFmt}); + } else { cldWnLen_ = 0; + } } if (config_[kCldPlaceholder]["mode-mon-col"].isInt()) { cldMonCols_ = config_[kCldPlaceholder]["mode-mon-col"].asInt(); @@ -185,11 +198,26 @@ auto waybar::modules::Clock::getTZtext(sys_seconds now) -> std::string { if (tzList_.size() == 1) return ""; std::stringstream os; + bool first = true; for (size_t tz_idx{0}; tz_idx < tzList_.size(); ++tz_idx) { - if (static_cast(tz_idx) == tzCurrIdx_) continue; - const auto* tz = tzList_[tz_idx] != nullptr ? tzList_[tz_idx] : local_zone(); + // Skip local timezone (nullptr) - never show it in tooltip + if (tzList_[tz_idx] == nullptr) continue; + + // Skip current timezone unless timezone-tooltip-format is specified + if (static_cast(tz_idx) == tzCurrIdx_ && tzTooltipFormat_.empty()) continue; + + const auto* tz = tzList_[tz_idx]; auto zt{zoned_time{tz, now}}; - os << fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(zt)) << '\n'; + + // Add newline before each entry except the first + if (!first) { + os << '\n'; + } + first = false; + + // Use timezone-tooltip-format if specified, otherwise use format_ + const std::string& fmt = tzTooltipFormat_.empty() ? format_ : tzTooltipFormat_; + os << fmt_lib::vformat(m_locale_, fmt, fmt_lib::make_format_args(zt)); } return os.str(); @@ -201,9 +229,11 @@ const unsigned cldRowsInMonth(const year_month& ym, const weekday& firstdow) { auto cldGetWeekForLine(const year_month& ym, const weekday& firstdow, const unsigned line) -> const year_month_weekday { - unsigned index{line - 2}; - if (weekday{ym / 1} == firstdow) ++index; - return ym / firstdow[index]; + const unsigned idx = line - 2; + const std::chrono::weekday_indexed indexed_first_day_of_week = + weekday{ym / 1} == firstdow ? firstdow[idx + 1] : firstdow[idx]; + + return ym / indexed_first_day_of_week; } auto getCalendarLine(const year_month_day& currDate, const year_month ym, const unsigned line, @@ -262,7 +292,7 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const } // Print non-first week default: { - auto ymdTmp{cldGetWeekForLine(ym, firstdow, line)}; + const auto ymdTmp{cldGetWeekForLine(ym, firstdow, line)}; if (ymdTmp.ok()) { auto d{year_month_day{ymdTmp}.day()}; const auto dlast{(ym / last).day()}; @@ -348,20 +378,34 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea m_locale_, fmtMap_[4], fmt_lib::make_format_args( (line == 2) - ? static_cast( + ? static_cast( zoned_seconds{tz, local_days{ymTmp / 1}}) - : static_cast(zoned_seconds{ + : static_cast(zoned_seconds{ tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}}))) << ' '; - } else + } else { os << pads; + } } } - os << Glib::ustring::format((cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, - std::setfill(L' '), - std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ : 0)), - getCalendarLine(today, ymTmp, line, firstdow, &m_locale_)); + // Count wide characters to avoid extra padding + size_t wideCharCount = 0; + std::string calendarLine = getCalendarLine(today, ymTmp, line, firstdow, &m_locale_); + if (line < 2) { + for (gchar *data = calendarLine.data(), *end = data + calendarLine.size(); + data != nullptr;) { + gunichar c = g_utf8_get_char_validated(data, end - data); + if (g_unichar_iswide(c)) { + wideCharCount++; + } + data = g_utf8_find_next_char(data, end); + } + } + os << Glib::ustring::format( + (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(L' '), + std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), + calendarLine); // Week numbers on the right if (cldWPos_ == WS::RIGHT && line > 0) { @@ -371,9 +415,9 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea << fmt_lib::vformat( m_locale_, fmtMap_[4], fmt_lib::make_format_args( - (line == 2) ? static_cast( + (line == 2) ? static_cast( zoned_seconds{tz, local_days{ymTmp / 1}}) - : static_cast( + : static_cast( zoned_seconds{tz, local_days{cldGetWeekForLine( ymTmp, firstdow, line)}}))); else @@ -465,6 +509,9 @@ using deleting_unique_ptr = std::unique_ptr>; // Computations done similarly to Linux cal utility. auto waybar::modules::Clock::first_day_of_week() -> weekday { + if (iso8601Calendar_) { + return Monday; + } #ifdef HAVE_LANGINFO_1STDAY deleting_unique_ptr::type, freelocale> posix_locale{ newlocale(LC_ALL, m_locale_.name().c_str(), nullptr)}; diff --git a/src/modules/cpu_frequency/bsd.cpp b/src/modules/cpu_frequency/bsd.cpp index 743fb288..eb5ae98a 100644 --- a/src/modules/cpu_frequency/bsd.cpp +++ b/src/modules/cpu_frequency/bsd.cpp @@ -5,11 +5,12 @@ std::vector waybar::modules::CpuFrequency::parseCpuFrequencies() { std::vector frequencies; - char buffer[256]; size_t len; int32_t freq; - uint32_t i = 0; +#ifndef __OpenBSD__ + char buffer[256]; + uint32_t i = 0; while (true) { len = 4; snprintf(buffer, 256, "dev.cpu.%u.freq", i); @@ -17,6 +18,12 @@ std::vector waybar::modules::CpuFrequency::parseCpuFrequencies() { frequencies.push_back(freq); ++i; } +#else + int getMhz[] = {CTL_HW, HW_CPUSPEED}; + len = sizeof(freq); + sysctl(getMhz, 2, &freq, &len, NULL, 0); + frequencies.push_back((float)freq); +#endif if (frequencies.empty()) { spdlog::warn("cpu/bsd: parseCpuFrequencies failed, not found in sysctl"); diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index e023aaf6..d75633e9 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -14,6 +14,9 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, percentage_(0), fp_(nullptr), pid_(-1) { + if (config.isNull()) { + spdlog::warn("There is no configuration for 'custom/{}', element will be hidden", name); + } dp.emit(); if (!config_["signal"].empty() && config_["interval"].empty() && config_["restart-interval"].empty()) { @@ -35,6 +38,13 @@ waybar::modules::Custom::~Custom() { void waybar::modules::Custom::delayWorker() { thread_ = [this] { + for (int i : this->pid_children_) { + int status; + waitpid(i, &status, 0); + } + + this->pid_children_.clear(); + bool can_update = true; if (config_["exec-if"].isString()) { output_ = util::command::execNoRead(config_["exec-if"].asString()); @@ -62,7 +72,7 @@ void waybar::modules::Custom::continuousWorker() { } thread_ = [this, cmd] { char* buff = nullptr; - waybar::util::ScopeGuard buff_deleter([buff]() { + waybar::util::ScopeGuard buff_deleter([&buff]() { if (buff) { free(buff); } @@ -79,9 +89,11 @@ void waybar::modules::Custom::continuousWorker() { dp.emit(); spdlog::error("{} stopped unexpectedly, is it endless?", name_); } - if (config_["restart-interval"].isUInt()) { + if (config_["restart-interval"].isNumeric()) { pid_ = -1; - thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt())); + thread_.sleep_for(std::chrono::milliseconds( + std::max(1L, // Minimum 1ms due to millisecond precision + static_cast(config_["restart-interval"].asDouble() * 1000)))); fp_ = util::command::open(cmd, pid_, output_name_); if (!fp_) { throw std::runtime_error("Unable to open " + cmd); diff --git a/src/modules/ext/workspace_manager.cpp b/src/modules/ext/workspace_manager.cpp new file mode 100644 index 00000000..64c4be23 --- /dev/null +++ b/src/modules/ext/workspace_manager.cpp @@ -0,0 +1,504 @@ +#include "modules/ext/workspace_manager.hpp" + +#include +#include +#include + +#include +#include +#include + +#include "client.hpp" +#include "gtkmm/widget.h" +#include "modules/ext/workspace_manager_binding.hpp" + +namespace waybar::modules::ext { + +// WorkspaceManager + +uint32_t WorkspaceManager::group_global_id = 0; +uint32_t WorkspaceManager::workspace_global_id = 0; +std::map Workspace::icon_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.orientation, 0) { + add_registry_listener(this); + + // parse configuration + + const auto config_sort_by_number = config_["sort-by-number"]; + if (config_sort_by_number.isBool()) { + spdlog::warn("[ext/workspaces]: Prefer sort-by-id instead of sort-by-number"); + sort_by_id_ = config_sort_by_number.asBool(); + } + + const auto config_sort_by_id = config_["sort-by-id"]; + if (config_sort_by_id.isBool()) { + sort_by_id_ = config_sort_by_id.asBool(); + } + + const auto config_sort_by_name = config_["sort-by-name"]; + if (config_sort_by_name.isBool()) { + sort_by_name_ = config_sort_by_name.asBool(); + } + + const auto config_sort_by_coordinates = config_["sort-by-coordinates"]; + if (config_sort_by_coordinates.isBool()) { + sort_by_coordinates_ = config_sort_by_coordinates.asBool(); + } + + const auto config_all_outputs = config_["all-outputs"]; + if (config_all_outputs.isBool()) { + all_outputs_ = config_all_outputs.asBool(); + } + + // setup UI + + box_.set_name("workspaces"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + box_.get_style_context()->add_class(MODULE_CLASS); + event_box_.add(box_); + + spdlog::debug("[ext/workspaces]: Workspace manager created"); +} + +WorkspaceManager::~WorkspaceManager() { + workspaces_.clear(); + groups_.clear(); + + if (ext_manager_ != nullptr) { + auto *display = Client::inst()->wl_display; + // Send `stop` request and wait for one roundtrip. + ext_workspace_manager_v1_stop(ext_manager_); + wl_display_roundtrip(display); + } + + if (ext_manager_ != nullptr) { + spdlog::warn("[ext/workspaces]: Destroying workspace manager before .finished event"); + ext_workspace_manager_v1_destroy(ext_manager_); + } + + spdlog::debug("[ext/workspaces]: Workspace manager destroyed"); +} + +void WorkspaceManager::register_manager(wl_registry *registry, uint32_t name, uint32_t version) { + if (ext_manager_ != nullptr) { + spdlog::warn("[ext/workspaces]: Register workspace manager again although already registered!"); + return; + } + if (version != 1) { + spdlog::warn("[ext/workspaces]: Using different workspace manager protocol version: {}", + version); + } + + ext_manager_ = workspace_manager_bind(registry, name, version, this); +} + +void WorkspaceManager::remove_workspace_group(uint32_t id) { + const auto it = + std::find_if(groups_.begin(), groups_.end(), [id](const auto &g) { return g->id() == id; }); + + if (it == groups_.end()) { + spdlog::warn("[ext/workspaces]: Can't find workspace group with id {}", id); + return; + } + + groups_.erase(it); +} + +void WorkspaceManager::remove_workspace(uint32_t id) { + const auto it = std::find_if(workspaces_.begin(), workspaces_.end(), + [id](const auto &w) { return w->id() == id; }); + + if (it == workspaces_.end()) { + spdlog::warn("[ext/workspaces]: Can't find workspace with id {}", id); + return; + } + + workspaces_.erase(it); +} + +void WorkspaceManager::handle_workspace_group(ext_workspace_group_handle_v1 *handle) { + const auto new_id = ++group_global_id; + groups_.push_back(std::make_unique(*this, handle, new_id)); + spdlog::debug("[ext/workspaces]: Workspace group {} created", new_id); +} + +void WorkspaceManager::handle_workspace(ext_workspace_handle_v1 *handle) { + const auto new_id = ++workspace_global_id; + const auto new_name = std::to_string(++workspace_name); + workspaces_.push_back(std::make_unique(config_, *this, handle, new_id, new_name)); + set_needs_sorting(); + spdlog::debug("[ext/workspaces]: Workspace {} created", new_id); +} + +void WorkspaceManager::handle_done() { dp.emit(); } + +void WorkspaceManager::handle_finished() { + spdlog::debug("[ext/workspaces]: Finishing workspace manager"); + ext_workspace_manager_v1_destroy(ext_manager_); + ext_manager_ = nullptr; +} + +void WorkspaceManager::commit() const { ext_workspace_manager_v1_commit(ext_manager_); } + +void WorkspaceManager::update() { + spdlog::debug("[ext/workspaces]: Updating state"); + + if (needs_sorting_) { + clear_buttons(); + sort_workspaces(); + needs_sorting_ = false; + } + + update_buttons(); + AModule::update(); +} + +bool WorkspaceManager::has_button(const Gtk::Button *button) { + const auto buttons = box_.get_children(); + return std::find(buttons.begin(), buttons.end(), button) != buttons.end(); +} + +void WorkspaceManager::sort_workspaces() { + // determine if workspace ID's and names can be sort numerically or literally + + auto is_numeric = [](const std::string &s) { + return !s.empty() && std::all_of(s.begin(), s.end(), ::isdigit); + }; + + auto sort_by_workspace_id_numerically = + std::all_of(workspaces_.begin(), workspaces_.end(), + [&](const auto &w) { return is_numeric(w->workspace_id()); }); + + auto sort_by_name_numerically = std::all_of(workspaces_.begin(), workspaces_.end(), + [&](const auto &w) { return is_numeric(w->name()); }); + + // sort based on configuration setting with sort-by-id as fallback + + std::sort(workspaces_.begin(), workspaces_.end(), [&](const auto &w1, const auto &w2) { + if (sort_by_id_ || (!sort_by_name_ && !sort_by_coordinates_)) { + if (w1->workspace_id() == w2->workspace_id()) { + return w1->id() < w2->id(); + } + if (sort_by_workspace_id_numerically) { + // the idea is that phonetic compare can be applied just to numbers + // with same number of digits + return w1->workspace_id().size() < w2->workspace_id().size() || + (w1->workspace_id().size() == w2->workspace_id().size() && + w1->workspace_id() < w2->workspace_id()); + } + return w1->workspace_id() < w2->workspace_id(); + } + + if (sort_by_name_) { + if (w1->name() == w2->name()) { + return w1->id() < w2->id(); + } + if (sort_by_name_numerically) { + // see above about numeric sorting + return w1->name().size() < w2->name().size() || + (w1->name().size() == w2->name().size() && w1->name() < w2->name()); + } + return w1->name() < w2->name(); + } + + if (sort_by_coordinates_) { + if (w1->coordinates() == w2->coordinates()) { + return w1->id() < w2->id(); + } + return w1->coordinates() < w2->coordinates(); + } + + return w1->id() < w2->id(); + }); +} + +void WorkspaceManager::clear_buttons() { + for (const auto &workspace : workspaces_) { + if (has_button(&workspace->button())) { + box_.remove(workspace->button()); + } + } +} + +void WorkspaceManager::update_buttons() { + const auto *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + + // go through all workspace + + for (const auto &workspace : workspaces_) { + const bool workspace_on_any_group_for_output = + std::any_of(groups_.begin(), groups_.end(), [&](const auto &group) { + const bool group_on_output = group->has_output(output) || all_outputs_; + const bool workspace_on_group = group->has_workspace(workspace->handle()); + return group_on_output && workspace_on_group; + }); + const bool bar_contains_button = has_button(&workspace->button()); + + // add or remove buttons if needed, update button state + + if (workspace_on_any_group_for_output) { + if (!bar_contains_button) { + // add button to bar + box_.pack_start(workspace->button(), false, false); + workspace->button().show_all(); + } + workspace->update(); + } else { + if (bar_contains_button) { + // remove button from bar + box_.remove(workspace->button()); + } + } + } +} + +// WorkspaceGroup + +WorkspaceGroup::WorkspaceGroup(WorkspaceManager &manager, ext_workspace_group_handle_v1 *handle, + uint32_t id) + : workspaces_manager_(manager), ext_handle_(handle), id_(id) { + add_workspace_group_listener(ext_handle_, this); +} + +WorkspaceGroup::~WorkspaceGroup() { + if (ext_handle_ != nullptr) { + ext_workspace_group_handle_v1_destroy(ext_handle_); + } + spdlog::debug("[ext/workspaces]: Workspace group {} destroyed", id_); +} + +bool WorkspaceGroup::has_output(const wl_output *output) { + return std::find(outputs_.begin(), outputs_.end(), output) != outputs_.end(); +} + +bool WorkspaceGroup::has_workspace(const ext_workspace_handle_v1 *workspace) { + return std::find(workspaces_.begin(), workspaces_.end(), workspace) != workspaces_.end(); +} + +void WorkspaceGroup::handle_capabilities(uint32_t capabilities) { + spdlog::debug("[ext/workspaces]: Capabilities for workspace group {}:", id_); + if ((capabilities & EXT_WORKSPACE_GROUP_HANDLE_V1_GROUP_CAPABILITIES_CREATE_WORKSPACE) == + capabilities) { + spdlog::debug("[ext/workspaces]: - create-workspace"); + } +} + +void WorkspaceGroup::handle_output_enter(wl_output *output) { outputs_.push_back(output); } + +void WorkspaceGroup::handle_output_leave(wl_output *output) { + const auto it = std::find(outputs_.begin(), outputs_.end(), output); + if (it != outputs_.end()) { + outputs_.erase(it); + } +} + +void WorkspaceGroup::handle_workspace_enter(ext_workspace_handle_v1 *handle) { + workspaces_.push_back(handle); +} + +void WorkspaceGroup::handle_workspace_leave(ext_workspace_handle_v1 *handle) { + const auto it = std::find(workspaces_.begin(), workspaces_.end(), handle); + if (it != workspaces_.end()) { + workspaces_.erase(it); + } +} + +void WorkspaceGroup::handle_removed() { + spdlog::debug("[ext/workspaces]: Removing workspace group {}", id_); + workspaces_manager_.remove_workspace_group(id_); +} + +// Workspace + +Workspace::Workspace(const Json::Value &config, WorkspaceManager &manager, + ext_workspace_handle_v1 *handle, uint32_t id, const std::string &name) + : workspace_manager_(manager), ext_handle_(handle), id_(id), workspace_id_(name), name_(name) { + add_workspace_listener(ext_handle_, this); + + // parse configuration + + const auto &config_active_only = config["active-only"]; + if (config_active_only.isBool()) { + active_only_ = config_active_only.asBool(); + } + + const auto &config_ignore_hidden = config["ignore-hidden"]; + if (config_ignore_hidden.isBool()) { + ignore_hidden_ = config_ignore_hidden.asBool(); + } + + const auto &config_format = config["format"]; + format_ = config_format.isString() ? config_format.asString() : "{name}"; + with_icon_ = format_.find("{icon}") != std::string::npos; + + if (with_icon_ && icon_map_.empty()) { + const auto &format_icons = config["format-icons"]; + for (auto &n : format_icons.getMemberNames()) { + icon_map_.emplace(n, format_icons[n].asString()); + } + } + + const bool config_on_click = config["on-click"].isString(); + if (config_on_click) { + on_click_action_ = config["on-click"].asString(); + } + const bool config_on_click_middle = config["on-click-middle"].isString(); + if (config_on_click_middle) { + on_click_middle_action_ = config["on-click-middle"].asString(); + } + const bool config_on_click_right = config["on-click-right"].isString(); + if (config_on_click_right) { + on_click_right_action_ = config["on-click-right"].asString(); + } + + // setup UI + + if (config_on_click || config_on_click_middle || config_on_click_right) { + 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_); +} + +Workspace::~Workspace() { + if (ext_handle_ != nullptr) { + ext_workspace_handle_v1_destroy(ext_handle_); + } + spdlog::debug("[ext/workspaces]: Workspace {} destroyed", id_); +} + +void Workspace::update() { + const auto style_context = button_.get_style_context(); + + // update style and visibility + + if (!has_state(EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE)) { + style_context->remove_class("active"); + } + if (!has_state(EXT_WORKSPACE_HANDLE_V1_STATE_URGENT)) { + style_context->remove_class("urgent"); + } + if (!has_state(EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN)) { + style_context->remove_class("hidden"); + } + + if (has_state(EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE)) { + button_.set_visible(true); + style_context->add_class("active"); + } + if (has_state(EXT_WORKSPACE_HANDLE_V1_STATE_URGENT)) { + button_.set_visible(true); + style_context->add_class("urgent"); + } + if (has_state(EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN)) { + button_.set_visible(!active_only_ && !ignore_hidden_); + style_context->add_class("hidden"); + } + if (state_ == 0) { + button_.set_visible(!active_only_); + } + + // update label + label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("name", name_), + fmt::arg("id", workspace_id_), + fmt::arg("icon", with_icon_ ? icon() : ""))); +} + +void Workspace::handle_id(const std::string &id) { + spdlog::debug("[ext/workspaces]: ID for workspace {}: {}", id_, id); + workspace_id_ = id; + workspace_manager_.set_needs_sorting(); +} + +void Workspace::handle_name(const std::string &name) { + spdlog::debug("[ext/workspaces]: Name for workspace {}: {}", id_, name); + name_ = name; + workspace_manager_.set_needs_sorting(); +} + +void Workspace::handle_coordinates(const std::vector &coordinates) { + coordinates_ = coordinates; + workspace_manager_.set_needs_sorting(); +} + +void Workspace::handle_state(uint32_t state) { state_ = state; } + +void Workspace::handle_capabilities(uint32_t capabilities) { + spdlog::debug("[ext/workspaces]: Capabilities for workspace {}:", id_); + if ((capabilities & EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ACTIVATE) == capabilities) { + spdlog::debug("[ext/workspaces]: - activate"); + } + if ((capabilities & EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_DEACTIVATE) == capabilities) { + spdlog::debug("[ext/workspaces]: - deactivate"); + } + if ((capabilities & EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_REMOVE) == capabilities) { + spdlog::debug("[ext/workspaces]: - remove"); + } + if ((capabilities & EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ASSIGN) == capabilities) { + spdlog::debug("[ext/workspaces]: - assign"); + } +} + +void Workspace::handle_removed() { + spdlog::debug("[ext/workspaces]: Removing workspace {}", id_); + workspace_manager_.remove_workspace(id_); +} + +bool Workspace::handle_clicked(const GdkEventButton *button) const { + std::string action; + if (button->button == GDK_BUTTON_PRIMARY) { + action = on_click_action_; + } else if (button->button == GDK_BUTTON_MIDDLE) { + action = on_click_middle_action_; + } else if (button->button == GDK_BUTTON_SECONDARY) { + action = on_click_right_action_; + } + + if (action.empty()) { + return true; + } + + if (action == "activate") { + ext_workspace_handle_v1_activate(ext_handle_); + } else if (action == "deactivate") { + ext_workspace_handle_v1_deactivate(ext_handle_); + } else if (action == "close") { + ext_workspace_handle_v1_remove(ext_handle_); + } else { + spdlog::warn("[ext/workspaces]: Unknown action {}", action); + } + workspace_manager_.commit(); + return true; +} + +std::string Workspace::icon() { + if (has_state(EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE)) { + const auto active_icon_it = icon_map_.find("active"); + if (active_icon_it != icon_map_.end()) { + return active_icon_it->second; + } + } + + const auto named_icon_it = icon_map_.find(name_); + if (named_icon_it != icon_map_.end()) { + return named_icon_it->second; + } + + const auto default_icon_it = icon_map_.find("default"); + if (default_icon_it != icon_map_.end()) { + return default_icon_it->second; + } + + return name_; +} + +} // namespace waybar::modules::ext diff --git a/src/modules/ext/workspace_manager_binding.cpp b/src/modules/ext/workspace_manager_binding.cpp new file mode 100644 index 00000000..2d9c6b48 --- /dev/null +++ b/src/modules/ext/workspace_manager_binding.cpp @@ -0,0 +1,159 @@ +#include "modules/ext/workspace_manager_binding.hpp" + +#include + +#include + +#include "client.hpp" +#include "modules/ext/workspace_manager.hpp" + +namespace waybar::modules::ext { + +static void handle_global(void *data, wl_registry *registry, uint32_t name, const char *interface, + uint32_t version) { + if (std::strcmp(interface, ext_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); +} + +static void workspace_manager_handle_workspace_group( + void *data, ext_workspace_manager_v1 *_, ext_workspace_group_handle_v1 *workspace_group) { + static_cast(data)->handle_workspace_group(workspace_group); +} + +static void workspace_manager_handle_workspace(void *data, ext_workspace_manager_v1 *_, + ext_workspace_handle_v1 *workspace) { + static_cast(data)->handle_workspace(workspace); +} + +static void workspace_manager_handle_done(void *data, ext_workspace_manager_v1 *_) { + static_cast(data)->handle_done(); +} + +static void workspace_manager_handle_finished(void *data, ext_workspace_manager_v1 *_) { + static_cast(data)->handle_finished(); +} + +static const ext_workspace_manager_v1_listener workspace_manager_impl = { + .workspace_group = workspace_manager_handle_workspace_group, + .workspace = workspace_manager_handle_workspace, + .done = workspace_manager_handle_done, + .finished = workspace_manager_handle_finished, +}; + +ext_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, &ext_workspace_manager_v1_interface, version)); + + if (workspace_manager) + ext_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_capabilities(void *data, ext_workspace_group_handle_v1 *_, + uint32_t capabilities) { + static_cast(data)->handle_capabilities(capabilities); +} + +static void workspace_group_handle_output_enter(void *data, ext_workspace_group_handle_v1 *_, + wl_output *output) { + static_cast(data)->handle_output_enter(output); +} + +static void workspace_group_handle_output_leave(void *data, ext_workspace_group_handle_v1 *_, + wl_output *output) { + static_cast(data)->handle_output_leave(output); +} + +static void workspace_group_handle_workspace_enter(void *data, ext_workspace_group_handle_v1 *_, + ext_workspace_handle_v1 *workspace) { + static_cast(data)->handle_workspace_enter(workspace); +} + +static void workspace_group_handle_workspace_leave(void *data, ext_workspace_group_handle_v1 *_, + ext_workspace_handle_v1 *workspace) { + static_cast(data)->handle_workspace_leave(workspace); +} + +static void workspace_group_handle_removed(void *data, ext_workspace_group_handle_v1 *_) { + static_cast(data)->handle_removed(); +} + +static const ext_workspace_group_handle_v1_listener workspace_group_impl = { + .capabilities = workspace_group_handle_capabilities, + .output_enter = workspace_group_handle_output_enter, + .output_leave = workspace_group_handle_output_leave, + .workspace_enter = workspace_group_handle_workspace_enter, + .workspace_leave = workspace_group_handle_workspace_leave, + .removed = workspace_group_handle_removed}; + +void add_workspace_group_listener(ext_workspace_group_handle_v1 *workspace_group_handle, + void *data) { + ext_workspace_group_handle_v1_add_listener(workspace_group_handle, &workspace_group_impl, data); +} + +void workspace_handle_name(void *data, struct ext_workspace_handle_v1 *_, const char *name) { + static_cast(data)->handle_name(name); +} + +void workspace_handle_id(void *data, struct ext_workspace_handle_v1 *_, const char *id) { + static_cast(data)->handle_id(id); +} + +void workspace_handle_coordinates(void *data, struct ext_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 ext_workspace_handle_v1 *workspace_handle, + uint32_t state) { + static_cast(data)->handle_state(state); +} + +static void workspace_handle_capabilities(void *data, + struct ext_workspace_handle_v1 *workspace_handle, + uint32_t capabilities) { + static_cast(data)->handle_capabilities(capabilities); +} + +void workspace_handle_removed(void *data, struct ext_workspace_handle_v1 *workspace_handle) { + static_cast(data)->handle_removed(); +} + +static const ext_workspace_handle_v1_listener workspace_impl = { + .id = workspace_handle_id, + .name = workspace_handle_name, + .coordinates = workspace_handle_coordinates, + .state = workspace_handle_state, + .capabilities = workspace_handle_capabilities, + .removed = workspace_handle_removed}; + +void add_workspace_listener(ext_workspace_handle_v1 *workspace_handle, void *data) { + ext_workspace_handle_v1_add_listener(workspace_handle, &workspace_impl, data); +} +} // namespace waybar::modules::ext diff --git a/src/modules/gamemode.cpp b/src/modules/gamemode.cpp index 811f13ca..72ef9503 100644 --- a/src/modules/gamemode.cpp +++ b/src/modules/gamemode.cpp @@ -53,7 +53,6 @@ Gamemode::Gamemode(const std::string& id, const Json::Value& config) if (config_["icon-spacing"].isUInt()) { iconSpacing = config_["icon-spacing"].asUInt(); } - box_.set_spacing(iconSpacing); // Whether to use icon or not if (config_["use-icon"].isBool()) { @@ -64,7 +63,6 @@ Gamemode::Gamemode(const std::string& id, const Json::Value& config) if (config_["icon-size"].isUInt()) { iconSize = config_["icon-size"].asUInt(); } - icon_.set_pixel_size(iconSize); // Format if (config_["format"].isString()) { @@ -228,6 +226,11 @@ auto Gamemode::update() -> void { iconName = DEFAULT_ICON_NAME; } icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + box_.set_spacing(iconSpacing); + icon_.set_pixel_size(iconSize); + } else { + box_.set_spacing(0); + icon_.set_pixel_size(0); } // Call parent update diff --git a/src/modules/gps.cpp b/src/modules/gps.cpp new file mode 100644 index 00000000..a7bab659 --- /dev/null +++ b/src/modules/gps.cpp @@ -0,0 +1,216 @@ +#include "modules/gps.hpp" + +#include +#include + +#include +#include + +// 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 + +namespace { +using namespace waybar::util; +constexpr const char* DEFAULT_FORMAT = "{mode}"; +} // namespace + +waybar::modules::Gps::Gps(const std::string& id, const Json::Value& config) + : ALabel(config, "gps", id, "{}", 5) +#ifdef WANT_RFKILL + , + rfkill_{RFKILL_TYPE_GPS} +#endif +{ + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; + + if (0 != gps_open("localhost", "2947", &gps_data_)) { + throw std::runtime_error("Can't open gpsd socket"); + } + + if (config_["hide-disconnected"].isBool()) { + hideDisconnected = config_["hide-disconnected"].asBool(); + } + + if (config_["hide-no-fix"].isBool()) { + hideNoFix = config_["hide-no-fix"].asBool(); + } + + gps_thread_ = [this] { + dp.emit(); + gps_stream(&gps_data_, WATCH_ENABLE, NULL); + int last_gps_mode = 0; + + while (gps_waiting(&gps_data_, 5000000)) { + if (gps_read(&gps_data_, NULL, 0) == -1) { + throw std::runtime_error("Can't read data from gpsd."); + } + + if (MODE_SET != (MODE_SET & gps_data_.set)) { + // did not even get mode, nothing to see here + continue; + } + + if (gps_data_.fix.mode != last_gps_mode) { + // significant update + dp.emit(); + } + last_gps_mode = gps_data_.fix.mode; + } + }; + +#ifdef WANT_RFKILL + rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Gps::update))); +#endif +} + +const std::string waybar::modules::Gps::getFixModeName() const { + switch (gps_data_.fix.mode) { + case MODE_NO_FIX: + return "fix-none"; + case MODE_2D: + return "fix-2d"; + case MODE_3D: + return "fix-3d"; + default: +#ifdef WANT_RFKILL + if (rfkill_.getState()) return "disabled"; +#endif + return "disconnected"; + } +} + +const std::string waybar::modules::Gps::getFixModeString() const { + switch (gps_data_.fix.mode) { + case MODE_NO_FIX: + return "No fix"; + case MODE_2D: + return "2D Fix"; + case MODE_3D: + return "3D Fix"; + default: + return "Disconnected"; + } +} + +const std::string waybar::modules::Gps::getFixStatusString() const { + switch (gps_data_.fix.status) { + case STATUS_GPS: + return "GPS"; + case STATUS_DGPS: + return "DGPS"; + case STATUS_RTK_FIX: + return "RTK Fixed"; + case STATUS_RTK_FLT: + return "RTK Float"; + case STATUS_DR: + return "Dead Reckoning"; + case STATUS_GNSSDR: + return "GNSS + Dead Reckoning"; + case STATUS_TIME: + return "Time Only"; + case STATUS_PPS_FIX: + return "PPS Fix"; + default: + +#ifdef WANT_RFKILL + if (rfkill_.getState()) return "Disabled"; +#endif + + return "Unknown"; + } +} + +auto waybar::modules::Gps::update() -> void { + sleep(0); // Wait for gps status change + + if ((gps_data_.fix.mode == MODE_NOT_SEEN && hideDisconnected) || + (gps_data_.fix.mode == MODE_NO_FIX && hideNoFix)) { + event_box_.set_visible(false); + return; + } + + // Show the module + if (!event_box_.get_visible()) event_box_.set_visible(true); + + std::string tooltip_format; + + if (!alt_) { + auto state = getFixModeName(); + if (!state_.empty() && label_.get_style_context()->has_class(state_)) { + label_.get_style_context()->remove_class(state_); + } + if (config_["format-" + state].isString()) { + default_format_ = config_["format-" + state].asString(); + } else if (config_["format"].isString()) { + default_format_ = config_["format"].asString(); + } else { + default_format_ = DEFAULT_FORMAT; + } + if (config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } + if (!label_.get_style_context()->has_class(state)) { + label_.get_style_context()->add_class(state); + } + format_ = default_format_; + state_ = state; + } + + auto format = format_; + + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("mode", getFixModeString())); + store.push_back(fmt::arg("status", getFixStatusString())); + + store.push_back(fmt::arg("latitude", gps_data_.fix.latitude)); + store.push_back(fmt::arg("latitude_error", gps_data_.fix.epy)); + + store.push_back(fmt::arg("longitude", gps_data_.fix.longitude)); + store.push_back(fmt::arg("longitude_error", gps_data_.fix.epx)); + + store.push_back(fmt::arg("altitude_hae", gps_data_.fix.altHAE)); + store.push_back(fmt::arg("altitude_msl", gps_data_.fix.altMSL)); + store.push_back(fmt::arg("altitude_error", gps_data_.fix.epv)); + + store.push_back(fmt::arg("speed", gps_data_.fix.speed)); + store.push_back(fmt::arg("speed_error", gps_data_.fix.eps)); + + store.push_back(fmt::arg("climb", gps_data_.fix.climb)); + store.push_back(fmt::arg("climb_error", gps_data_.fix.epc)); + + store.push_back(fmt::arg("satellites_used", gps_data_.satellites_used)); + store.push_back(fmt::arg("satellites_visible", gps_data_.satellites_visible)); + + auto text = fmt::vformat(format, store); + + if (tooltipEnabled()) { + if (tooltip_format.empty() && config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + auto tooltip_text = fmt::vformat(tooltip_format, store); + if (label_.get_tooltip_text() != tooltip_text) { + label_.set_tooltip_markup(tooltip_text); + } + } else if (label_.get_tooltip_text() != text) { + label_.set_tooltip_markup(text); + } + } + label_.set_markup(text); + // Call parent update + ALabel::update(); +} + +waybar::modules::Gps::~Gps() { + gps_stream(&gps_data_, WATCH_DISABLE, NULL); + gps_close(&gps_data_); +} diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 2bd3b509..7060d304 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -46,9 +46,14 @@ std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { IPC::IPC() { // will start IPC and relay events to parseIPC ipcThread_ = std::thread([this]() { socketListener(); }); + socketOwnerPid_ = getpid(); } IPC::~IPC() { + // Do no stop Hyprland IPC if a child process (with successful fork() but + // failed exec()) exits. + if (getpid() != socketOwnerPid_) return; + running_ = false; spdlog::info("Hyprland IPC stopping..."); if (socketfd_ != -1) { @@ -78,8 +83,6 @@ void IPC::socketListener() { return; } - if (!modulesReady) return; - spdlog::info("Hyprland IPC starting"); struct sockaddr_un addr; diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index da56e578..25f6789d 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -11,8 +11,6 @@ namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "language", id, "{}", 0, true), bar_(bar), m_ipc(IPC::inst()) { - modulesReady = true; - // get the active layout when open initLanguage(); @@ -66,7 +64,18 @@ auto Language::update() -> void { void Language::onEvent(const std::string& ev) { std::lock_guard lg(mutex_); std::string kbName(begin(ev) + ev.find_last_of('>') + 1, begin(ev) + ev.find_first_of(',')); - auto layoutName = ev.substr(ev.find_last_of(',') + 1); + + // Last comma before variants parenthesis, eg: + // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., + // with dead keys) + std::string beforeParenthesis; + auto parenthesisPos = ev.find_last_of('('); + if (parenthesisPos == std::string::npos) { + beforeParenthesis = ev; + } else { + beforeParenthesis = std::string(begin(ev), begin(ev) + parenthesisPos); + } + auto layoutName = ev.substr(beforeParenthesis.find_last_of(',') + 1); if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) return; // ignore diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp index 97c4bb62..eb9b1de8 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -2,21 +2,17 @@ #include -#include "util/sanitize_str.hpp" - namespace waybar::modules::hyprland { Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "submap", id, "{}", 0, true), bar_(bar), m_ipc(IPC::inst()) { - modulesReady = true; - parseConfig(config); label_.hide(); ALabel::update(); // Displays widget immediately if always_on_ assuming default submap - // Needs an actual way to retrive current submap on startup + // Needs an actual way to retrieve current submap on startup if (always_on_) { submap_ = default_submap_; label_.get_style_context()->add_class(submap_); @@ -68,8 +64,7 @@ void Submap::onEvent(const std::string& ev) { return; } - auto submapName = ev.substr(ev.find_last_of('>') + 1); - submapName = waybar::util::sanitize_string(submapName); + auto submapName = ev.substr(ev.find_first_of('>') + 2); if (!submap_.empty()) { label_.get_style_context()->remove_class(submap_); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 815fbad8..23437c5f 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -21,7 +21,6 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), m_ipc(IPC::inst()) { std::unique_lock windowIpcUniqueLock(windowIpcSmtx); - modulesReady = true; separateOutputs_ = config["separate-outputs"].asBool(); // register for hyprland ipc diff --git a/src/modules/hyprland/windowcount.cpp b/src/modules/hyprland/windowcount.cpp new file mode 100644 index 00000000..b2721aea --- /dev/null +++ b/src/modules/hyprland/windowcount.cpp @@ -0,0 +1,143 @@ +#include "modules/hyprland/windowcount.hpp" + +#include +#include +#include +#include + +#include +#include + +#include "modules/hyprland/backend.hpp" + +namespace waybar::modules::hyprland { + +WindowCount::WindowCount(const std::string& id, const Bar& bar, const Json::Value& config) + : AAppIconLabel(config, "windowcount", id, "{count}", 0, true), bar_(bar), m_ipc(IPC::inst()) { + separateOutputs_ = + config.isMember("separate-outputs") ? config["separate-outputs"].asBool() : true; + + queryActiveWorkspace(); + update(); + dp.emit(); + + // register for hyprland ipc + m_ipc.registerForIPC("fullscreen", this); + m_ipc.registerForIPC("workspace", this); + m_ipc.registerForIPC("focusedmon", this); + m_ipc.registerForIPC("openwindow", this); + m_ipc.registerForIPC("closewindow", this); + m_ipc.registerForIPC("movewindow", this); +} + +WindowCount::~WindowCount() { + m_ipc.unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lg(mutex_); +} + +auto WindowCount::update() -> void { + std::lock_guard lg(mutex_); + + std::string format = config_["format"].asString(); + std::string formatEmpty = config_["format-empty"].asString(); + std::string formatWindowed = config_["format-windowed"].asString(); + std::string formatFullscreen = config_["format-fullscreen"].asString(); + + setClass("empty", workspace_.windows == 0); + setClass("fullscreen", workspace_.hasfullscreen); + + if (workspace_.windows == 0 && !formatEmpty.empty()) { + label_.set_markup(fmt::format(fmt::runtime(formatEmpty), workspace_.windows)); + } else if (!workspace_.hasfullscreen && !formatWindowed.empty()) { + label_.set_markup(fmt::format(fmt::runtime(formatWindowed), workspace_.windows)); + } else if (workspace_.hasfullscreen && !formatFullscreen.empty()) { + label_.set_markup(fmt::format(fmt::runtime(formatFullscreen), workspace_.windows)); + } else if (!format.empty()) { + label_.set_markup(fmt::format(fmt::runtime(format), workspace_.windows)); + } else { + label_.set_text(fmt::format("{}", workspace_.windows)); + } + + label_.show(); + AAppIconLabel::update(); +} + +auto WindowCount::getActiveWorkspace() -> Workspace { + const auto workspace = m_ipc.getSocket1JsonReply("activeworkspace"); + + if (workspace.isObject()) { + return Workspace::parse(workspace); + } + + return {}; +} + +auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspace { + const auto monitors = m_ipc.getSocket1JsonReply("monitors"); + if (monitors.isArray()) { + auto monitor = std::ranges::find_if( + monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + if (monitor == std::end(monitors)) { + spdlog::warn("Monitor not found: {}", monitorName); + return Workspace{ + .id = -1, + .windows = 0, + .hasfullscreen = false, + }; + } + const int id = (*monitor)["activeWorkspace"]["id"].asInt(); + + const auto workspaces = m_ipc.getSocket1JsonReply("workspaces"); + if (workspaces.isArray()) { + auto workspace = std::ranges::find_if( + workspaces, [&](Json::Value workspace) { return workspace["id"] == id; }); + if (workspace == std::end(workspaces)) { + spdlog::warn("No workspace with id {}", id); + return Workspace{ + .id = -1, + .windows = 0, + .hasfullscreen = false, + }; + } + return Workspace::parse(*workspace); + }; + }; + + return {}; +} + +auto WindowCount::Workspace::parse(const Json::Value& value) -> WindowCount::Workspace { + return Workspace{ + .id = value["id"].asInt(), + .windows = value["windows"].asInt(), + .hasfullscreen = value["hasfullscreen"].asBool(), + }; +} + +void WindowCount::queryActiveWorkspace() { + std::lock_guard lg(mutex_); + + if (separateOutputs_) { + workspace_ = getActiveWorkspace(this->bar_.output->name); + } else { + workspace_ = getActiveWorkspace(); + } +} + +void WindowCount::onEvent(const std::string& ev) { + queryActiveWorkspace(); + dp.emit(); +} + +void WindowCount::setClass(const std::string& classname, bool enable) { + if (enable) { + if (!bar_.window.get_style_context()->has_class(classname)) { + bar_.window.get_style_context()->add_class(classname); + } + } else { + bar_.window.get_style_context()->remove_class(classname); + } +} + +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/windowcreationpayload.cpp b/src/modules/hyprland/windowcreationpayload.cpp index df7fe784..055b0f0c 100644 --- a/src/modules/hyprland/windowcreationpayload.cpp +++ b/src/modules/hyprland/windowcreationpayload.cpp @@ -20,7 +20,7 @@ WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data) } WindowCreationPayload::WindowCreationPayload(std::string workspace_name, - WindowAddress window_address, std::string window_repr) + WindowAddress window_address, WindowRepr window_repr) : m_window(std::move(window_repr)), m_windowAddress(std::move(window_address)), m_workspaceName(std::move(workspace_name)) { @@ -30,10 +30,11 @@ WindowCreationPayload::WindowCreationPayload(std::string workspace_name, WindowCreationPayload::WindowCreationPayload(std::string workspace_name, WindowAddress window_address, std::string window_class, - std::string window_title) + std::string window_title, bool is_active) : m_window(std::make_pair(std::move(window_class), std::move(window_title))), m_windowAddress(std::move(window_address)), - m_workspaceName(std::move(workspace_name)) { + m_workspaceName(std::move(workspace_name)), + m_isActive(is_active) { clearAddr(); clearWorkspaceName(); } @@ -88,17 +89,18 @@ bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) { int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; } -void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { +void WindowCreationPayload::moveToWorkspace(std::string &new_workspace_name) { m_workspaceName = new_workspace_name; } -std::string WindowCreationPayload::repr(Workspaces &workspace_manager) { +WindowRepr WindowCreationPayload::repr(Workspaces &workspace_manager) { if (std::holds_alternative(m_window)) { return std::get(m_window); } if (std::holds_alternative(m_window)) { - auto [window_class, window_title] = std::get(m_window); - return workspace_manager.getRewrite(window_class, window_title); + auto const &[window_class, window_title] = std::get(m_window); + return {m_windowAddress, window_class, window_title, + workspace_manager.getRewrite(window_class, window_title), m_isActive}; } // Unreachable spdlog::error("WorkspaceWindow::repr: Unreachable"); diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 4655096f..ae66bc84 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -6,6 +6,8 @@ #include #include "modules/hyprland/workspaces.hpp" +#include "util/command.hpp" +#include "util/icon_loader.hpp" namespace waybar::modules::hyprland { @@ -32,7 +34,12 @@ Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_ma false); m_button.set_relief(Gtk::RELIEF_NONE); - m_content.set_center_widget(m_label); + if (m_workspaceManager.enableTaskbar()) { + m_content.set_orientation(m_workspaceManager.taskbarOrientation()); + m_content.pack_start(m_labelBefore, false, false); + } else { + m_content.set_center_widget(m_labelBefore); + } m_button.add(m_content); initializeWindowMap(clients_data); @@ -47,9 +54,14 @@ void addOrRemoveClass(const Glib::RefPtr &context, bool condi } } -std::optional Workspace::closeWindow(WindowAddress const &addr) { - if (m_windowMap.contains(addr)) { - return removeWindow(addr); +std::optional Workspace::closeWindow(WindowAddress const &addr) { + auto it = std::ranges::find_if(m_windowMap, + [&addr](const auto &window) { return window.address == addr; }); + // If the vector contains the address, remove it and return the window representation + if (it != m_windowMap.end()) { + WindowRepr windowRepr = *it; + m_windowMap.erase(it); + return windowRepr; } return std::nullopt; } @@ -91,30 +103,55 @@ void Workspace::initializeWindowMap(const Json::Value &clients_data) { } } -void Workspace::insertWindow(WindowCreationPayload create_window_paylod) { - if (!create_window_paylod.isEmpty(m_workspaceManager)) { - auto repr = create_window_paylod.repr(m_workspaceManager); +void Workspace::setActiveWindow(WindowAddress const &addr) { + std::optional activeIdx; + for (size_t i = 0; i < m_windowMap.size(); ++i) { + auto &window = m_windowMap[i]; + bool isActive = (window.address == addr); + window.setActive(isActive); + if (isActive) { + activeIdx = i; + } + } - if (!repr.empty()) { - m_windowMap[create_window_paylod.getAddress()] = repr; + auto activeWindowPos = m_workspaceManager.activeWindowPosition(); + if (activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE) { + auto window = std::move(m_windowMap[*activeIdx]); + m_windowMap.erase(m_windowMap.begin() + *activeIdx); + if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) { + m_windowMap.insert(m_windowMap.begin(), std::move(window)); + } else if (activeWindowPos == Workspaces::ActiveWindowPosition::LAST) { + m_windowMap.emplace_back(std::move(window)); + } + } +} + +void Workspace::insertWindow(WindowCreationPayload create_window_payload) { + if (!create_window_payload.isEmpty(m_workspaceManager)) { + auto repr = create_window_payload.repr(m_workspaceManager); + + if (!repr.empty() || m_workspaceManager.enableTaskbar()) { + auto addr = create_window_payload.getAddress(); + auto it = std::ranges::find_if( + m_windowMap, [&addr](const auto &window) { return window.address == addr; }); + // If the vector contains the address, update the window representation, otherwise insert it + if (it != m_windowMap.end()) { + *it = repr; + } else { + m_windowMap.emplace_back(repr); + } } } }; -bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_paylod) { - if (create_window_paylod.getWorkspaceName() == name()) { - insertWindow(create_window_paylod); +bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_payload) { + if (create_window_payload.getWorkspaceName() == name()) { + insertWindow(create_window_payload); return true; } return false; } -std::string Workspace::removeWindow(WindowAddress const &addr) { - std::string windowRepr = m_windowMap[addr]; - m_windowMap.erase(addr); - return windowRepr; -} - std::string &Workspace::selectIcon(std::map &icons_map) { spdlog::trace("Selecting icon for workspace {}", name()); if (isUrgent()) { @@ -172,7 +209,11 @@ std::string &Workspace::selectIcon(std::map &icons_map return m_name; } -void Workspace::update(const std::string &format, const std::string &icon) { +void Workspace::update(const std::string &workspace_icon) { + if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) { + m_button.hide(); + return; + } // clang-format off if (this->m_workspaceManager.activeOnly() && \ !this->isActive() && \ @@ -200,21 +241,144 @@ void Workspace::update(const std::string &format, const std::string &icon) { addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); std::string windows; - auto windowSeparator = m_workspaceManager.getWindowSeparator(); + // Optimization: The {windows} substitution string is only possible if the taskbar is disabled, no + // need to compute this if enableTaskbar() is true + if (!m_workspaceManager.enableTaskbar()) { + auto windowSeparator = m_workspaceManager.getWindowSeparator(); - bool isNotFirst = false; + bool isNotFirst = false; - for (auto &[_pid, window_repr] : m_windowMap) { - if (isNotFirst) { - windows.append(windowSeparator); + for (const auto &window_repr : m_windowMap) { + if (isNotFirst) { + windows.append(windowSeparator); + } + isNotFirst = true; + windows.append(window_repr.repr_rewrite); } - isNotFirst = true; - windows.append(window_repr); } - m_label.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), - fmt::arg("name", name()), fmt::arg("icon", icon), - fmt::arg("windows", windows))); + auto formatBefore = m_workspaceManager.formatBefore(); + m_labelBefore.set_markup(fmt::format(fmt::runtime(formatBefore), fmt::arg("id", id()), + fmt::arg("name", name()), fmt::arg("icon", workspace_icon), + fmt::arg("windows", windows))); + m_labelBefore.get_style_context()->add_class("workspace-label"); + + if (m_workspaceManager.enableTaskbar()) { + updateTaskbar(workspace_icon); + } +} + +bool Workspace::isEmpty() const { + auto ignore_list = m_workspaceManager.getIgnoredWindows(); + if (ignore_list.empty()) { + return m_windows == 0; + } + // If there are windows but they are all ignored, consider the workspace empty + return std::all_of( + m_windowMap.begin(), m_windowMap.end(), + [this, &ignore_list](const auto &window_repr) { return shouldSkipWindow(window_repr); }); +} + +void Workspace::updateTaskbar(const std::string &workspace_icon) { + for (auto child : m_content.get_children()) { + if (child != &m_labelBefore) { + m_content.remove(*child); + } + } + + bool isFirst = true; + auto processWindow = [&](const WindowRepr &window_repr) { + if (shouldSkipWindow(window_repr)) { + return; // skip + } + if (isFirst) { + isFirst = false; + } else if (m_workspaceManager.getWindowSeparator() != "") { + auto windowSeparator = Gtk::make_managed(m_workspaceManager.getWindowSeparator()); + m_content.pack_start(*windowSeparator, false, false); + windowSeparator->show(); + } + + auto window_box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); + window_box->set_tooltip_text(window_repr.window_title); + window_box->get_style_context()->add_class("taskbar-window"); + if (window_repr.isActive) { + window_box->get_style_context()->add_class("active"); + } + auto event_box = Gtk::manage(new Gtk::EventBox()); + event_box->add(*window_box); + if (m_workspaceManager.onClickWindow() != "") { + event_box->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address)); + } + + auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), + fmt::arg("title", window_repr.window_title)); + if (!text_before.empty()) { + auto window_label_before = Gtk::make_managed(text_before); + window_box->pack_start(*window_label_before, true, true); + } + + if (m_workspaceManager.taskbarWithIcon()) { + auto app_info_ = IconLoader::get_app_info_from_app_id_list(window_repr.window_class); + int icon_size = m_workspaceManager.taskbarIconSize(); + auto window_icon = Gtk::make_managed(); + m_workspaceManager.iconLoader().image_load_icon(*window_icon, app_info_, icon_size); + window_box->pack_start(*window_icon, false, false); + } + + auto text_after = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatAfter()), + fmt::arg("title", window_repr.window_title)); + if (!text_after.empty()) { + auto window_label_after = Gtk::make_managed(text_after); + window_box->pack_start(*window_label_after, true, true); + } + + m_content.pack_start(*event_box, true, false); + event_box->show_all(); + }; + + if (m_workspaceManager.taskbarReverseDirection()) { + for (auto it = m_windowMap.rbegin(); it != m_windowMap.rend(); ++it) { + processWindow(*it); + } + } else { + for (const auto &window_repr : m_windowMap) { + processWindow(window_repr); + } + } + + auto formatAfter = m_workspaceManager.formatAfter(); + if (!formatAfter.empty()) { + m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()), + fmt::arg("name", name()), + fmt::arg("icon", workspace_icon))); + m_content.pack_end(m_labelAfter, false, false); + m_labelAfter.show(); + } +} + +bool Workspace::handleClick(const GdkEventButton *event_button, WindowAddress const &addr) const { + if (event_button->type == GDK_BUTTON_PRESS) { + std::string command = std::regex_replace(m_workspaceManager.onClickWindow(), + std::regex("\\{address\\}"), "0x" + addr); + command = std::regex_replace(command, std::regex("\\{button\\}"), + std::to_string(event_button->button)); + auto res = util::command::execNoRead(command); + if (res.exit_code != 0) { + spdlog::error("Failed to execute {}: {}", command, res.out); + } + } + return true; +} + +bool Workspace::shouldSkipWindow(const WindowRepr &window_repr) const { + auto ignore_list = m_workspaceManager.getIgnoredWindows(); + auto it = std::ranges::find_if(ignore_list, [&window_repr](const auto &ignoreItem) { + return std::regex_match(window_repr.window_class, ignoreItem) || + std::regex_match(window_repr.window_title, ignoreItem); + }); + return it != ignore_list.end(); } } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 0e225935..8360137f 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -10,6 +10,7 @@ #include #include "util/regex_collection.hpp" +#include "util/string.hpp" namespace waybar::modules::hyprland { @@ -18,7 +19,6 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value m_bar(bar), m_box(bar.orientation, 0), m_ipc(IPC::inst()) { - modulesReady = true; parseConfig(config); m_box.set_name("workspaces"); @@ -65,14 +65,17 @@ Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name, void Workspaces::createWorkspace(Json::Value const &workspace_data, Json::Value const &clients_data) { auto workspaceName = workspace_data["name"].asString(); + auto workspaceId = workspace_data["id"].asInt(); spdlog::debug("Creating workspace {}", workspaceName); // avoid recreating existing workspaces - auto workspace = - std::ranges::find_if(m_workspaces, [workspaceName](std::unique_ptr const &w) { - return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || - workspaceName == w->name(); - }); + auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr const &w) { + if (workspaceId > 0) { + return w->id() == workspaceId; + } + return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || + workspaceName == w->name(); + }); if (workspace != m_workspaces.end()) { // don't recreate workspace, but update persistency if necessary @@ -253,10 +256,8 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJs // value is an array => create defined workspaces for this monitor if (canCreate) { for (const Json::Value &workspace : value) { - if (workspace.isInt()) { - spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor); - persistentWorkspacesToCreate.emplace_back(std::to_string(workspace.asInt())); - } + spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor); + persistentWorkspacesToCreate.emplace_back(workspace.asString()); } } else { // key is the workspace and value is array of monitors to create on @@ -292,8 +293,13 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &c if (!rule["persistent"].asBool()) { continue; } - auto const &workspace = rule.isMember("defaultName") ? rule["defaultName"].asString() - : rule["workspaceString"].asString(); + auto workspace = rule.isMember("defaultName") ? rule["defaultName"].asString() + : rule["workspaceString"].asString(); + + // The prefix "name:" cause mismatches with workspace names taken anywhere else. + if (workspace.starts_with("name:")) { + workspace = workspace.substr(5); + } auto const &monitor = rule["monitor"].asString(); // create this workspace persistently if: // 1. the allOutputs config option is enabled @@ -340,6 +346,8 @@ void Workspaces::onEvent(const std::string &ev) { onWorkspaceRenamed(payload); } else if (eventName == "windowtitlev2") { onWindowTitleEvent(payload); + } else if (eventName == "activewindowv2") { + onActiveWindowChanged(payload); } else if (eventName == "configreloaded") { onConfigReloaded(); } @@ -489,12 +497,14 @@ void Workspaces::onWindowOpened(std::string const &payload) { std::string windowTitle = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx); - m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle); + bool isActive = m_currentActiveWindowAddress == windowAddress; + m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle, isActive); } void Workspaces::onWindowClosed(std::string const &addr) { spdlog::trace("Window closed: {}", addr); updateWindowCount(); + m_orphanWindowMap.erase(addr); for (auto &workspace : m_workspaces) { if (workspace->closeWindow(addr)) { break; @@ -507,13 +517,13 @@ void Workspaces::onWindowMoved(std::string const &payload) { updateWindowCount(); auto [windowAddress, _, workspaceName] = splitTriplePayload(payload); - std::string windowRepr; + WindowRepr windowRepr; // If the window was still queued to be created, just change its destination // and exit for (auto &window : m_windowsToCreate) { if (window.getAddress() == windowAddress) { - window.moveToWorksace(workspaceName); + window.moveToWorkspace(workspaceName); return; } } @@ -533,6 +543,7 @@ void Workspaces::onWindowMoved(std::string const &payload) { // ...and then add it to the new workspace if (!windowRepr.empty()) { + m_orphanWindowMap.erase(windowAddress); m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowRepr); } } @@ -558,9 +569,10 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) { (*windowWorkspace)->insertWindow(std::move(wcp)); }; } else { - auto queuedWindow = std::ranges::find_if(m_windowsToCreate, [payload](auto &windowPayload) { - return windowPayload.getAddress() == payload; - }); + auto queuedWindow = + std::ranges::find_if(m_windowsToCreate, [&windowAddress](auto &windowPayload) { + return windowPayload.getAddress() == windowAddress; + }); // If the window was queued, rename it in the queue if (queuedWindow != m_windowsToCreate.end()) { @@ -571,7 +583,7 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) { if (inserter.has_value()) { Json::Value clientsData = m_ipc.getSocket1JsonReply("clients"); - std::string jsonWindowAddress = fmt::format("0x{}", payload); + std::string jsonWindowAddress = fmt::format("0x{}", windowAddress); auto client = std::ranges::find_if(clientsData, [jsonWindowAddress](auto &client) { return client["address"].asString() == jsonWindowAddress; @@ -583,6 +595,21 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) { } } +void Workspaces::onActiveWindowChanged(WindowAddress const &activeWindowAddress) { + spdlog::trace("Active window changed: {}", activeWindowAddress); + m_currentActiveWindowAddress = activeWindowAddress; + + for (auto &[address, window] : m_orphanWindowMap) { + window.setActive(address == activeWindowAddress); + } + for (auto const &workspace : m_workspaces) { + workspace->setActiveWindow(activeWindowAddress); + } + for (auto &window : m_windowsToCreate) { + window.setActive(window.getAddress() == activeWindowAddress); + } +} + void Workspaces::onConfigReloaded() { spdlog::info("Hyprland config reloaded, reinitializing hyprland/workspaces module..."); init(); @@ -590,8 +617,9 @@ void Workspaces::onConfigReloaded() { auto Workspaces::parseConfig(const Json::Value &config) -> void { const auto &configFormat = config["format"]; - m_format = configFormat.isString() ? configFormat.asString() : "{name}"; - m_withIcon = m_format.find("{icon}") != std::string::npos; + m_formatBefore = configFormat.isString() ? configFormat.asString() : "{name}"; + m_withIcon = m_formatBefore.find("{icon}") != std::string::npos; + auto withWindows = m_formatBefore.find("{windows}") != std::string::npos; if (m_withIcon && m_iconsMap.empty()) { populateIconsMap(config["format-icons"]); @@ -600,6 +628,7 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { populateBoolConfig(config, "all-outputs", m_allOutputs); populateBoolConfig(config, "show-special", m_showSpecial); populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly); + populateBoolConfig(config, "persistent-only", m_persistentOnly); populateBoolConfig(config, "active-only", m_activeOnly); populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); @@ -608,6 +637,15 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { populateIgnoreWorkspacesConfig(config); populateFormatWindowSeparatorConfig(config); populateWindowRewriteConfig(config); + + if (withWindows) { + populateWorkspaceTaskbarConfig(config); + } + if (m_enableTaskbar) { + auto parts = split(m_formatBefore, "{windows}", 1); + m_formatBefore = parts[0]; + m_formatAfter = parts.size() > 1 ? parts[1] : ""; + } } auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void { @@ -680,6 +718,77 @@ auto Workspaces::populateWindowRewriteConfig(const Json::Value &config) -> void [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); } +auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value &config) -> void { + const auto &workspaceTaskbar = config["workspace-taskbar"]; + if (!workspaceTaskbar.isObject()) { + spdlog::debug("workspace-taskbar is not defined or is not an object, using default rules."); + return; + } + + populateBoolConfig(workspaceTaskbar, "enable", m_enableTaskbar); + populateBoolConfig(workspaceTaskbar, "update-active-window", m_updateActiveWindow); + populateBoolConfig(workspaceTaskbar, "reverse-direction", m_taskbarReverseDirection); + + if (workspaceTaskbar["format"].isString()) { + /* The user defined a format string, use it */ + std::string format = workspaceTaskbar["format"].asString(); + m_taskbarWithTitle = format.find("{title") != std::string::npos; /* {title} or {title.length} */ + auto parts = split(format, "{icon}", 1); + m_taskbarFormatBefore = parts[0]; + if (parts.size() > 1) { + m_taskbarWithIcon = true; + m_taskbarFormatAfter = parts[1]; + } + } else { + /* The default is to only show the icon */ + m_taskbarWithIcon = true; + } + + auto iconTheme = workspaceTaskbar["icon-theme"]; + if (iconTheme.isArray()) { + for (auto &c : iconTheme) { + m_iconLoader.add_custom_icon_theme(c.asString()); + } + } else if (iconTheme.isString()) { + m_iconLoader.add_custom_icon_theme(iconTheme.asString()); + } + + if (workspaceTaskbar["icon-size"].isInt()) { + m_taskbarIconSize = workspaceTaskbar["icon-size"].asInt(); + } + if (workspaceTaskbar["orientation"].isString() && + toLower(workspaceTaskbar["orientation"].asString()) == "vertical") { + m_taskbarOrientation = Gtk::ORIENTATION_VERTICAL; + } + + if (workspaceTaskbar["on-click-window"].isString()) { + m_onClickWindow = workspaceTaskbar["on-click-window"].asString(); + } + + if (workspaceTaskbar["ignore-list"].isArray()) { + for (auto &windowRegex : workspaceTaskbar["ignore-list"]) { + std::string ruleString = windowRegex.asString(); + try { + m_ignoreWindows.emplace_back(ruleString, std::regex_constants::icase); + } catch (const std::regex_error &e) { + spdlog::error("Invalid rule {}: {}", ruleString, e.what()); + } + } + } + + if (workspaceTaskbar["active-window-position"].isString()) { + auto posStr = workspaceTaskbar["active-window-position"].asString(); + try { + m_activeWindowPosition = + m_activeWindowEnumParser.parseStringToEnum(posStr, m_activeWindowPositionMap); + } catch (const std::invalid_argument &e) { + spdlog::warn( + "Invalid string representation for active-window-position. Falling back to 'none'."); + m_activeWindowPosition = ActiveWindowPosition::NONE; + } + } +} + void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) { if (!create_window_payload.isEmpty(*this)) { m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this); @@ -700,12 +809,18 @@ auto Workspaces::registerIpc() -> void { m_ipc.registerForIPC("urgent", this); m_ipc.registerForIPC("configreloaded", this); - if (windowRewriteConfigUsesTitle()) { + if (windowRewriteConfigUsesTitle() || m_taskbarWithTitle) { spdlog::info( "Registering for Hyprland's 'windowtitlev2' events because a user-defined window " "rewrite rule uses the 'title' field."); m_ipc.registerForIPC("windowtitlev2", this); } + if (m_updateActiveWindow) { + spdlog::info( + "Registering for Hyprland's 'activewindowv2' events because 'update-active-window' is set " + "to true."); + m_ipc.registerForIPC("activewindowv2", this); + } } void Workspaces::removeWorkspacesToRemove() { @@ -770,6 +885,40 @@ void Workspaces::setCurrentMonitorId() { } } +void Workspaces::sortSpecialCentered() { + std::vector> specialWorkspaces; + std::vector> hiddenWorkspaces; + std::vector> normalWorkspaces; + + for (auto &workspace : m_workspaces) { + if (workspace->isSpecial()) { + specialWorkspaces.push_back(std::move(workspace)); + } else { + if (workspace->button().is_visible()) { + normalWorkspaces.push_back(std::move(workspace)); + } else { + hiddenWorkspaces.push_back(std::move(workspace)); + } + } + } + m_workspaces.clear(); + + size_t center = normalWorkspaces.size() / 2; + + m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(normalWorkspaces.begin()), + std::make_move_iterator(normalWorkspaces.begin() + center)); + + m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(specialWorkspaces.begin()), + std::make_move_iterator(specialWorkspaces.end())); + + m_workspaces.insert(m_workspaces.end(), + std::make_move_iterator(normalWorkspaces.begin() + center), + std::make_move_iterator(normalWorkspaces.end())); + + m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(hiddenWorkspaces.begin()), + std::make_move_iterator(hiddenWorkspaces.end())); +} + void Workspaces::sortWorkspaces() { std::ranges::sort( // m_workspaces, [&](std::unique_ptr &a, std::unique_ptr &b) { @@ -828,6 +977,9 @@ void Workspaces::sortWorkspaces() { // Return a default value if none of the cases match. return isNameLess; // You can adjust this to your specific needs. }); + if (m_sortBy == SortMethod::SPECIAL_CENTERED) { + this->sortSpecialCentered(); + } for (size_t i = 0; i < m_workspaces.size(); ++i) { m_box.reorder_child(m_workspaces[i]->button(), i); @@ -860,7 +1012,7 @@ auto Workspaces::update() -> void { void Workspaces::updateWindowCount() { const Json::Value workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); - for (auto &workspace : m_workspaces) { + for (auto const &workspace : m_workspaces) { auto workspaceJson = std::ranges::find_if(workspacesJson, [&](Json::Value const &x) { return x["name"].asString() == workspace->name() || (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); @@ -906,9 +1058,17 @@ bool Workspaces::updateWindowsToCreate() { void Workspaces::updateWorkspaceStates() { const std::vector visibleWorkspaces = getVisibleWorkspaces(); auto updatedWorkspaces = m_ipc.getSocket1JsonReply("workspaces"); + + auto currentWorkspace = m_ipc.getSocket1JsonReply("activeworkspace"); + std::string currentWorkspaceName = + currentWorkspace.isMember("name") ? currentWorkspace["name"].asString() : ""; + for (auto &workspace : m_workspaces) { + bool isActiveByName = + !currentWorkspaceName.empty() && workspace->name() == currentWorkspaceName; + workspace->setActive( - workspace->id() == m_activeWorkspaceId || + workspace->id() == m_activeWorkspaceId || isActiveByName || (workspace->isSpecial() && workspace->name() == m_activeSpecialWorkspaceName)); if (workspace->isActive() && workspace->isUrgent()) { workspace->setUrgent(false); @@ -927,7 +1087,7 @@ void Workspaces::updateWorkspaceStates() { if (updatedWorkspace != updatedWorkspaces.end()) { workspace->setOutput((*updatedWorkspace)["monitor"].asString()); } - workspace->update(m_format, workspaceIcon); + workspace->update(workspaceIcon); } } @@ -981,7 +1141,7 @@ std::optional Workspaces::parseWorkspaceId(std::string const &workspaceIdSt try { return workspaceIdStr == "special" ? -99 : std::stoi(workspaceIdStr); } catch (std::exception const &e) { - spdlog::error("Failed to parse workspace ID: {}", e.what()); + spdlog::debug("Workspace \"{}\" is not bound to an id: {}", workspaceIdStr, e.what()); return std::nullopt; } } diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 71e93b94..173aabd3 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -14,14 +14,20 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config) size_ = config["size"].asInt(); - interval_ = config_["interval"].asInt(); + interval_ = config_["interval"] == "once" + ? std::chrono::milliseconds::max() + : std::chrono::milliseconds(std::max( + 1L, // Minimum 1ms due to millisecond precision + static_cast( + (config_["interval"].isNumeric() ? config_["interval"].asDouble() : 0) * + 1000))); if (size_ == 0) { size_ = 16; } - if (interval_ == 0) { - interval_ = INT_MAX; + if (interval_.count() == 0) { + interval_ = std::chrono::milliseconds::max(); } delayWorker(); @@ -30,8 +36,7 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config) void waybar::modules::Image::delayWorker() { thread_ = [this] { dp.emit(); - auto interval = std::chrono::seconds(interval_); - thread_.sleep_for(interval); + thread_.sleep_for(interval_); }; } diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 544d7814..18600cd2 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -60,6 +60,7 @@ auto waybar::modules::Memory::update() -> void { fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("percentage", used_ram_percentage), + fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), fmt::arg("swapAvail", available_swap_gigabytes))); @@ -72,6 +73,7 @@ auto waybar::modules::Memory::update() -> void { fmt::runtime(tooltip_format), used_ram_percentage, fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("percentage", used_ram_percentage), + fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), fmt::arg("swapAvail", available_swap_gigabytes))); diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 192e6c1a..8e6bbb25 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -118,7 +118,7 @@ void waybar::modules::MPD::setLabel() { auto format = format_; Glib::ustring artist, album_artist, album, title; - std::string date, filename; + std::string date, filename, uri; int song_pos = 0, queue_length = 0, volume = 0; std::chrono::seconds elapsedTime, totalTime; @@ -151,6 +151,7 @@ void waybar::modules::MPD::setLabel() { title = sanitize_string(getTag(MPD_TAG_TITLE)); date = sanitize_string(getTag(MPD_TAG_DATE)); filename = sanitize_string(getFilename()); + uri = mpd_song_get_uri(song_.get()); song_pos = mpd_status_get_song_pos(status_.get()) + 1; volume = mpd_status_get_volume(status_.get()); if (volume < 0) { @@ -184,7 +185,7 @@ void waybar::modules::MPD::setLabel() { fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon), - fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename)); + fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri)); if (text.empty()) { label_.hide(); } else { @@ -200,15 +201,15 @@ void waybar::modules::MPD::setLabel() { tooltip_format = config_["tooltip-format"].isString() ? config_["tooltip-format"].asString() : "MPD (connected)"; try { - auto tooltip_text = - fmt::format(fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), - fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), - fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), - fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), - fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon), - fmt::arg("repeatIcon", repeatIcon), fmt::arg("singleIcon", singleIcon)); + auto tooltip_text = fmt::format( + fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()), + fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), + fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), + fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), + fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), + fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon), + fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri)); label_.set_tooltip_text(tooltip_text); } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error (tooltip): {}", e.what()); diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 2492f3b5..661ec88f 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -14,7 +14,6 @@ extern "C" { #include #include - namespace waybar::modules::mpris { const std::string DEFAULT_FORMAT = "{player} ({status}): {dynamic}"; @@ -435,9 +434,11 @@ auto Mpris::onPlayerNameVanished(PlayerctlPlayerManager* manager, PlayerctlPlaye auto* mpris = static_cast(data); if (!mpris) return; - spdlog::debug("mpris: player-vanished callback: {}", player_name->name); + spdlog::debug("mpris: name-vanished callback: {}", player_name->name); - if (std::string(player_name->name) == mpris->player_) { + if (mpris->player_ == "playerctld") { + mpris->dp.emit(); + } else if (mpris->player_ == player_name->name) { mpris->player = nullptr; mpris->event_box_.set_visible(false); mpris->dp.emit(); @@ -508,7 +509,10 @@ auto Mpris::getPlayerInfo() -> std::optional { // > get the list of players [..] in order of activity // https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249 players = g_list_first(players); - if (players) player_name = static_cast(players->data)->name; + if (players) + player_name = static_cast(players->data)->name; + else + return std::nullopt; // no players found, hide the widget } if (std::any_of(ignored_players_.begin(), ignored_players_.end(), @@ -601,38 +605,45 @@ errorexit: } bool Mpris::handleToggle(GdkEventButton* const& e) { + if (!e || e->type != GdkEventType::GDK_BUTTON_PRESS) { + return false; + } + + auto info = getPlayerInfo(); + if (!info) return false; + + struct ButtonAction { + guint button; + const char* config_key; + std::function builtin_action; + }; + GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error) { g_error_free(error); } }); - auto info = getPlayerInfo(); - if (!info) return false; + // Command pattern: encapsulate each button's action + const ButtonAction actions[] = { + {1, "on-click", [&]() { playerctl_player_play_pause(player, &error); }}, + {2, "on-click-middle", [&]() { playerctl_player_previous(player, &error); }}, + {3, "on-click-right", [&]() { playerctl_player_next(player, &error); }}, + {8, "on-click-backward", [&]() { playerctl_player_previous(player, &error); }}, + {9, "on-click-forward", [&]() { playerctl_player_next(player, &error); }}, + }; - if (e->type == GdkEventType::GDK_BUTTON_PRESS) { - switch (e->button) { - case 1: // left-click - if (config_["on-click"].isString()) { - return ALabel::handleToggle(e); - } - playerctl_player_play_pause(player, &error); - break; - case 2: // middle-click - if (config_["on-click-middle"].isString()) { - return ALabel::handleToggle(e); - } - playerctl_player_previous(player, &error); - break; - case 3: // right-click - if (config_["on-click-right"].isString()) { - return ALabel::handleToggle(e); - } - playerctl_player_next(player, &error); - break; + for (const auto& action : actions) { + if (e->button == action.button) { + if (config_[action.config_key].isString()) { + return ALabel::handleToggle(e); + } + action.builtin_action(); + break; } } + if (error) { spdlog::error("mpris[{}]: error running builtin on-click action: {}", (*info).name, error->message); diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 0a77c00e..d0b7970c 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -1,13 +1,18 @@ #include "modules/network.hpp" #include +#include +#include #include #include #include +#include #include #include #include +#include +#include #include "util/format.hpp" #ifdef WANT_RFKILL @@ -78,25 +83,7 @@ waybar::modules::Network::readBandwidthUsage() { } waybar::modules::Network::Network(const std::string &id, const Json::Value &config) - : ALabel(config, "network", id, DEFAULT_FORMAT, 60), - ifid_(-1), - addr_pref_(IPV4), - efd_(-1), - ev_fd_(-1), - want_route_dump_(false), - want_link_dump_(false), - want_addr_dump_(false), - dump_in_progress_(false), - is_p2p_(false), - cidr_(0), - cidr6_(0), - signal_strength_dbm_(0), - signal_strength_(0), -#ifdef WANT_RFKILL - rfkill_{RFKILL_TYPE_WLAN}, -#endif - frequency_(0.0) { - + : ALabel(config, "network", id, DEFAULT_FORMAT, 60) { // Start with some "text" in the module's label_. update() will then // update it. Since the text should be different, update() will be able // to show or hide the event_box_. This is to work around the case where @@ -223,8 +210,8 @@ void waybar::modules::Network::worker() { std::lock_guard lock(mutex_); if (ifid_ > 0) { getInfo(); - dp.emit(); } + dp.emit(); } thread_timer_.sleep_for(interval_); }; @@ -271,13 +258,16 @@ void waybar::modules::Network::worker() { } const std::string waybar::modules::Network::getNetworkState() const { - if (ifid_ == -1) { + if (ifid_ == -1 || !carrier_) { #ifdef WANT_RFKILL - if (rfkill_.getState()) return "disabled"; + bool display_rfkill = true; + if (config_["rfkill"].isBool()) { + display_rfkill = config_["rfkill"].asBool(); + } + if (rfkill_.getState() && display_rfkill) return "disabled"; #endif return "disconnected"; } - if (!carrier_) return "disconnected"; if (ipaddr_.empty() && ipaddr6_.empty()) return "linked"; if (essid_.empty()) return "ethernet"; return "wifi"; @@ -343,18 +333,23 @@ auto waybar::modules::Network::update() -> void { fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), - fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), - fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")), - fmt::arg("bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / interval_.count(), "b/s")), - fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / interval_.count(), "o/s")), - fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / interval_.count(), "o/s")), + fmt::arg("bandwidthDownBits", + pow_format(bandwidth_down * 8ull / (interval_.count() / 1000.0), "b/s")), + fmt::arg("bandwidthUpBits", + pow_format(bandwidth_up * 8ull / (interval_.count() / 1000.0), "b/s")), + fmt::arg( + "bandwidthTotalBits", + pow_format((bandwidth_up + bandwidth_down) * 8ull / (interval_.count() / 1000.0), "b/s")), + fmt::arg("bandwidthDownOctets", + pow_format(bandwidth_down / (interval_.count() / 1000.0), "o/s")), + fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / (interval_.count() / 1000.0), "o/s")), fmt::arg("bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "o/s")), - fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / interval_.count(), "B/s")), - fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / interval_.count(), "B/s")), + pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "o/s")), + fmt::arg("bandwidthDownBytes", + pow_format(bandwidth_down / (interval_.count() / 1000.0), "B/s")), + fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / (interval_.count() / 1000.0), "B/s")), fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "B/s"))); + pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "B/s"))); if (text.compare(label_.get_label()) != 0) { label_.set_markup(text); if (text.empty()) { @@ -401,11 +396,65 @@ auto waybar::modules::Network::update() -> void { ALabel::update(); } -bool waybar::modules::Network::checkInterface(std::string name) { - if (config_["interface"].isString()) { - return config_["interface"].asString() == name || - wildcardMatch(config_["interface"].asString(), name); +// https://gist.github.com/rressi/92af77630faf055934c723ce93ae2495 +static bool wildcardMatch(const std::string &pattern, const std::string &text) { + auto P = int(pattern.size()); + auto T = int(text.size()); + + auto p = 0, fallback_p = -1; + auto t = 0, fallback_t = -1; + + while (t < T) { + // Wildcard match: + if (p < P && pattern[p] == '*') { + fallback_p = p++; // starting point after failures + fallback_t = t; // starting point after failures + } + + // Simple match: + else if (p < P && (pattern[p] == '?' || pattern[p] == text[t])) { + p++; + t++; + } + + // Failure, fall back just after last matched '*': + else if (fallback_p >= 0) { + p = fallback_p + 1; // position just after last matched '*" + t = ++fallback_t; // re-try to match text from here + } + + // There were no '*' before, so we fail here: + else { + return false; + } } + + // Consume all '*' at the end of pattern: + while (p < P && pattern[p] == '*') p++; + + return p == P; +} + +bool waybar::modules::Network::matchInterface(const std::string &ifname, + const std::vector &altnames, + std::string &matched) const { + if (!config_["interface"].isString()) { + return false; + } + + auto config_ifname = config_["interface"].asString(); + if (config_ifname == ifname || wildcardMatch(config_ifname, ifname)) { + matched = ifname; + return true; + } + + for (const auto &altname : altnames) { + if (config_ifname == altname || wildcardMatch(config_ifname, altname)) { + matched = altname; + return true; + } + } + return false; } @@ -420,6 +469,7 @@ void waybar::modules::Network::clearIface() { netmask_.clear(); netmask6_.clear(); carrier_ = false; + is_p2p_ = false; cidr_ = 0; cidr6_ = 0; signal_strength_dbm_ = 0; @@ -439,12 +489,16 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { is_del_event = true; case RTM_NEWLINK: { struct ifinfomsg *ifi = static_cast(NLMSG_DATA(nh)); - ssize_t attrlen = IFLA_PAYLOAD(nh); - struct rtattr *ifla = IFLA_RTA(ifi); - const char *ifname = NULL; - size_t ifname_len = 0; + struct nlattr *attrs[IFLA_MAX + 1]; + std::string ifname; + std::vector altnames; std::optional carrier; + if (nlmsg_parse(nh, sizeof(*ifi), attrs, IFLA_MAX, nullptr) < 0) { + spdlog::error("network: failed to parse netlink attributes"); + return NL_SKIP; + } + if (net->ifid_ != -1 && ifi->ifi_index != net->ifid_) { return NL_OK; } @@ -462,26 +516,33 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { return NL_OK; } - for (; RTA_OK(ifla, attrlen); ifla = RTA_NEXT(ifla, attrlen)) { - switch (ifla->rta_type) { - case IFLA_IFNAME: - ifname = static_cast(RTA_DATA(ifla)); - ifname_len = RTA_PAYLOAD(ifla) - 1; // minus \0 - if (ifi->ifi_flags & IFF_POINTOPOINT && net->checkInterface(ifname)) - net->is_p2p_ = true; - break; - case IFLA_CARRIER: { - carrier = *(char *)RTA_DATA(ifla) == 1; - break; + if (attrs[IFLA_IFNAME] != nullptr) { + const char *ifname_ptr = nla_get_string(attrs[IFLA_IFNAME]); + size_t ifname_len = nla_len(attrs[IFLA_IFNAME]) - 1; // minus \0 + ifname = std::string(ifname_ptr, ifname_len); + } + + if (attrs[IFLA_CARRIER] != nullptr) { + carrier = nla_get_u8(attrs[IFLA_CARRIER]) == 1; + } + + if (attrs[IFLA_PROP_LIST] != nullptr) { + struct nlattr *prop; + int rem; + + nla_for_each_nested(prop, attrs[IFLA_PROP_LIST], rem) { + if (nla_type(prop) == IFLA_ALT_IFNAME) { + const char *altname_ptr = nla_get_string(prop); + size_t altname_len = nla_len(prop) - 1; // minus \0 + altnames.emplace_back(altname_ptr, altname_len); } } } if (!is_del_event && ifi->ifi_index == net->ifid_) { // Update interface information - if (net->ifname_.empty() && ifname != NULL) { - std::string new_ifname(ifname, ifname_len); - net->ifname_ = new_ifname; + if (net->ifname_.empty() && !ifname.empty()) { + net->ifname_ = ifname; } if (carrier.has_value()) { if (net->carrier_ != *carrier) { @@ -502,13 +563,25 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { } } else if (!is_del_event && net->ifid_ == -1) { // Checking if it's an interface we care about. - std::string new_ifname(ifname, ifname_len); - if (net->checkInterface(new_ifname)) { - spdlog::debug("network: selecting new interface {}/{}", new_ifname, ifi->ifi_index); + std::string matched; + if (net->matchInterface(ifname, altnames, matched)) { + if (ifname == matched) { + spdlog::debug("network: selecting new interface {}/{}", ifname, ifi->ifi_index); + } else { + spdlog::debug("network: selecting new interface {}/{} (matched altname {})", ifname, + ifi->ifi_index, matched); + } - net->ifname_ = new_ifname; + net->ifname_ = ifname; net->ifid_ = ifi->ifi_index; - if (ifi->ifi_flags & IFF_POINTOPOINT) net->is_p2p_ = true; + if ((ifi->ifi_flags & IFF_POINTOPOINT) != 0) { + net->is_p2p_ = true; + } + if ((ifi->ifi_flags & IFF_UP) == 0) { + // With some network drivers (e.g. mt7921e), the interface may + // report having a carrier even though interface is down. + carrier = false; + } if (carrier.has_value()) { net->carrier_ = carrier.value(); } @@ -602,7 +675,6 @@ 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: { @@ -613,6 +685,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { int family = rtm->rtm_family; ssize_t attrlen = RTM_PAYLOAD(nh); struct rtattr *attr = RTM_RTA(rtm); + char gateway_addr[INET6_ADDRSTRLEN]; bool has_gateway = false; bool has_destination = false; int temp_idx = -1; @@ -638,7 +711,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { * If someone ever needs to figure out the gateway address as well, * it's here as the attribute payload. */ - inet_ntop(family, RTA_DATA(attr), temp_gw_addr, sizeof(temp_gw_addr)); + inet_ntop(family, RTA_DATA(attr), gateway_addr, sizeof(gateway_addr)); has_gateway = true; break; case RTA_DST: { @@ -683,8 +756,8 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { net->clearIface(); net->ifid_ = temp_idx; net->route_priority = priority; - net->gwaddr_ = temp_gw_addr; - spdlog::debug("network: new default route via {} on if{} metric {}", temp_gw_addr, + net->gwaddr_ = gateway_addr; + spdlog::debug("network: new default route via {} on if{} metric {}", gateway_addr, temp_idx, priority); /* Ask ifname associated with temp_idx as well as carrier status */ @@ -897,43 +970,3 @@ auto waybar::modules::Network::getInfo() -> void { } nl_send_sync(sock_, nl_msg); } - -// https://gist.github.com/rressi/92af77630faf055934c723ce93ae2495 -bool waybar::modules::Network::wildcardMatch(const std::string &pattern, - const std::string &text) const { - auto P = int(pattern.size()); - auto T = int(text.size()); - - auto p = 0, fallback_p = -1; - auto t = 0, fallback_t = -1; - - while (t < T) { - // Wildcard match: - if (p < P && pattern[p] == '*') { - fallback_p = p++; // starting point after failures - fallback_t = t; // starting point after failures - } - - // Simple match: - else if (p < P && (pattern[p] == '?' || pattern[p] == text[t])) { - p++; - t++; - } - - // Failure, fall back just after last matched '*': - else if (fallback_p >= 0) { - p = fallback_p + 1; // position just after last matched '*" - t = ++fallback_t; // re-try to match text from here - } - - // There were no '*' before, so we fail here: - else { - return false; - } - } - - // Consume all '*' at the end of pattern: - while (p < P && pattern[p] == '*') p++; - - return p == P; -} diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 383bf113..fa4dc287 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -147,6 +147,17 @@ void IPC::parseIPC(const std::string &line) { } else { spdlog::error("Active window changed on unknown workspace"); } + } else if (const auto &payload = ev["WorkspaceUrgencyChanged"]) { + const auto id = payload["id"].asUInt64(); + const auto urgent = payload["urgent"].asBool(); + auto it = std::find_if(workspaces_.begin(), workspaces_.end(), + [id](const auto &ws) { return ws["id"].asUInt64() == id; }); + if (it != workspaces_.end()) { + auto &ws = *it; + ws["is_urgent"] = urgent; + } else { + spdlog::error("Urgency changed for unknown workspace"); + } } else if (const auto &payload = ev["KeyboardLayoutsChanged"]) { const auto &layouts = payload["keyboard_layouts"]; const auto &names = layouts["names"]; diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp index 3b55ff24..496e5404 100644 --- a/src/modules/niri/language.cpp +++ b/src/modules/niri/language.cpp @@ -58,6 +58,16 @@ void Language::doUpdate() { spdlog::debug("niri language update with short description {}", layout.short_description); spdlog::debug("niri language update with variant {}", layout.variant); + if (!last_short_name_.empty()) { + label_.get_style_context()->remove_class(last_short_name_); + } + if (!layout.short_name.empty()) { + label_.get_style_context()->add_class(layout.short_name); + last_short_name_ = layout.short_name; + } else { + last_short_name_.clear(); + } + std::string layoutName = std::string{}; if (config_.isMember("format-" + layout.short_description + "-" + layout.variant)) { const auto propName = "format-" + layout.short_description + "-" + layout.variant; diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index d2fcad5d..7dfc0b35 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -20,6 +20,7 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value gIPC->registerForIPC("WorkspacesChanged", this); gIPC->registerForIPC("WorkspaceActivated", this); gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); + gIPC->registerForIPC("WorkspaceUrgencyChanged", this); dp.emit(); } @@ -67,6 +68,11 @@ void Workspaces::doUpdate() { else style_context->remove_class("active"); + if (ws["is_urgent"].asBool()) + style_context->add_class("urgent"); + else + style_context->remove_class("urgent"); + if (ws["output"]) { if (ws["output"].asString() == bar_.output->name) style_context->add_class("current_output"); @@ -166,6 +172,10 @@ std::string Workspaces::getIcon(const std::string &value, const Json::Value &ws) const auto &icons = config_["format-icons"]; if (!icons) return value; + if (ws["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString(); + + if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString(); + if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].asString(); if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index 3ae3ae83..abad763d 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -29,14 +29,14 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu // method on the proxy to see whether or not something's responding // on the other side. - // NOTE: the DBus adresses are under migration. They should be + // NOTE: the DBus addresses are under migration. They should be // changed to org.freedesktop.UPower.PowerProfiles at some point. // // See // https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/releases/0.20 // // The old name is still announced for now. Let's rather use the old - // adresses for compatibility sake. + // addresses for compatibility sake. // // Revisit this in 2026, systems should be updated by then. diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 48bba888..904c8fd9 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -74,6 +74,24 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat } } + for (const auto& ignore_item : config_["ignore"]) { + if (!ignore_item.isObject() || !ignore_item["type"].isString() || + !ignore_item["name"].isString()) + continue; + const std::string type = ignore_item["type"].asString(); + const std::string name = ignore_item["name"].asString(); + + auto iter = typeMap.find(type); + if (iter != typeMap.end()) { + auto& [_, nodeType] = iter->second; + ignore.emplace(nodeType, std::move(name)); + } + } + + if (config_["ignore-monitor"].isBool()) { + ignore_monitor = config_["ignore-monitor"].asBool(); + } + backend = util::PipewireBackend::PipewireBackend::getInstance(); backend->privacy_nodes_changed_signal_event.connect( sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); @@ -88,6 +106,11 @@ void Privacy::onPrivacyNodesChanged() { nodes_screenshare.clear(); for (auto& node : backend->privacy_nodes) { + if (ignore_monitor && node.second->is_monitor) continue; + + auto iter = ignore.find(std::pair(node.second->type, node.second->node_name)); + if (iter != ignore.end()) continue; + switch (node.second->state) { case PW_NODE_STATE_RUNNING: switch (node.second->type) { diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index 54e61b43..6424da9e 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -1,5 +1,7 @@ #include "modules/privacy/privacy_item.hpp" +#include + #include #include "glibmm/main.h" @@ -96,20 +98,22 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac void PrivacyItem::update_tooltip() { // Removes all old nodes for (auto *child : tooltip_window.get_children()) { + tooltip_window.remove(*child); + // despite the remove, still needs a delete to prevent memory leak. Speculating that this might + // work differently in GTK4. delete child; } - for (auto *node : *nodes) { - Gtk::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4); + auto *box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL, 4); // Set device icon - Gtk::Image *node_icon = new Gtk::Image(); + auto *node_icon = Gtk::make_managed(); node_icon->set_pixel_size(tooltipIconSize); node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID); box->add(*node_icon); // Set model - auto *nodeName = new Gtk::Label(node->getName()); + auto *nodeName = Gtk::make_managed(node->getName()); box->add(*nodeName); tooltip_window.add(*box); diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index 255ca571..ceed20dd 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -132,7 +132,7 @@ auto waybar::modules::Pulseaudio::update() -> void { tooltip_format = config_["tooltip-format"].asString(); } if (!tooltip_format.empty()) { - label_.set_tooltip_text(fmt::format( + label_.set_tooltip_markup(fmt::format( fmt::runtime(tooltip_format), fmt::arg("desc", sink_desc), fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc), diff --git a/src/modules/river/layout.cpp b/src/modules/river/layout.cpp index e938400f..308a0901 100644 --- a/src/modules/river/layout.cpp +++ b/src/modules/river/layout.cpp @@ -145,9 +145,15 @@ void Layout::handle_name(const char *name) { if (std::strcmp(name, "") == 0 || format_.empty()) { label_.hide(); // hide empty labels or labels with empty format } else { - label_.show(); + if (!name_.empty()) { + label_.get_style_context()->remove_class(name_); + } + + label_.get_style_context()->add_class(name); label_.set_markup(fmt::format(fmt::runtime(format_), Glib::Markup::escape_text(name).raw())); + label_.show(); } + name_ = name; ALabel::update(); } diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index 359e5a23..33be0e6f 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -150,11 +150,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con button.show(); } - struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output); - zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this); - - zriver_status_manager_v1_destroy(status_manager_); + box_.signal_show().connect(sigc::mem_fun(*this, &Tags::handle_show)); } Tags::~Tags() { @@ -165,6 +161,19 @@ Tags::~Tags() { if (control_) { zriver_control_v1_destroy(control_); } + + if (status_manager_) { + zriver_status_manager_v1_destroy(status_manager_); + } +} + +void Tags::handle_show() { + struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output); + zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this); + + zriver_status_manager_v1_destroy(status_manager_); + status_manager_ = nullptr; } void Tags::handle_primary_clicked(uint32_t tag) { diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 9dc13158..398afac6 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -74,6 +74,13 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf cancellable_, interface); } +Item::~Item() { + if (this->gtk_menu != nullptr) { + this->gtk_menu->popdown(); + this->gtk_menu->detach(); + } +} + bool Item::handleMouseEnter(GdkEventCrossing* const& e) { event_box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); return false; @@ -140,7 +147,25 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { category = get_variant(value); } else if (name == "Id") { id = get_variant(value); - setCustomIcon(id); + + /* + * HACK: Electron apps seem to have the same ID, but tooltip seems correct, so use that as ID + * to pass as the custom icon option. I'm avoiding being disruptive and setting that to the ID + * itself as I've no idea what this would affect. + * The tooltip text is converted to lowercase since that's what (most?) themes expect? + * I still haven't found a way for it to pick from theme automatically, although + * it might be my theme. + */ + if (id == "chrome_status_icon_1") { + Glib::VariantBase value; + this->proxy_->get_cached_property(value, "ToolTip"); + tooltip = get_variant(value); + if (!tooltip.text.empty()) { + setCustomIcon(tooltip.text.lowercase()); + } + } else { + setCustomIcon(id); + } } else if (name == "Title") { title = get_variant(value); if (tooltip.text.empty()) { @@ -203,6 +228,8 @@ void Item::setStatus(const Glib::ustring& value) { } void Item::setCustomIcon(const std::string& id) { + spdlog::debug("SNI tray id: {}", id); + std::string custom_icon = IconManager::instance().getIconForApp(id); if (!custom_icon.empty()) { if (std::filesystem::exists(custom_icon)) { @@ -388,14 +415,17 @@ Glib::RefPtr Item::getIconPixbuf() { Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { icon_theme->rescan_if_needed(); - if (!icon_theme_path.empty() && - icon_theme->lookup_icon(name.c_str(), request_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE)) { - return icon_theme->load_icon(name.c_str(), request_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + if (!icon_theme_path.empty()) { + auto icon_info = icon_theme->lookup_icon(name.c_str(), request_size, + Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + if (icon_info) { + bool is_sym = false; + return icon_info.load_symbolic(event_box.get_style_context(), is_sym); + } } return DefaultGtkIconThemeWrapper::load_icon(name.c_str(), request_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE, + event_box.get_style_context()); } double Item::getScaledIconSize() { @@ -420,6 +450,9 @@ void Item::makeMenu() { gtk_menu->attach_to_widget(event_box); } } + // Manually reset prelight to make sure the tray item doesn't stay in a hover state even though + // the menu is focused + event_box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); } bool Item::handleClick(GdkEventButton* const& ev) { diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index f657c855..34a3c05f 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -2,6 +2,8 @@ #include +#include + #include "modules/sni/icon_manager.hpp" namespace waybar::modules::SNI { @@ -21,6 +23,9 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) if (config_["spacing"].isUInt()) { box_.set_spacing(config_["spacing"].asUInt()); } + if (config["show-passive-items"].isBool()) { + show_passive_ = config["show-passive-items"].asBool(); + } nb_hosts_ += 1; if (config_["icons"].isObject()) { IconManager::instance().setIconsConfig(config_["icons"]); @@ -44,7 +49,15 @@ void Tray::onRemove(std::unique_ptr& item) { auto Tray::update() -> void { // Show tray only when items are available - event_box_.set_visible(!box_.get_children().empty()); + std::vector children = box_.get_children(); + if (show_passive_) { + event_box_.set_visible(!children.empty()); + } else { + event_box_.set_visible(!std::all_of(children.begin(), children.end(), [](Gtk::Widget* child) { + return child->get_style_context()->has_class("passive"); + })); + } + // Call parent update AModule::update(); } diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index a005df17..f4cfa6c3 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -22,10 +22,10 @@ Language::Language(const std::string& id, const Json::Value& config) hide_single_ = config["hide-single-layout"].isBool() && config["hide-single-layout"].asBool(); 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); + displayed_short_flag |= static_cast(DisplayedShortFlag::ShortName); } if (format_.find("{shortDescription}") != std::string::npos) { - displayed_short_flag |= static_cast(DispayedShortFlag::ShortDescription); + displayed_short_flag |= static_cast(DisplayedShortFlag::ShortDescription); } if (config.isMember("tooltip-format")) { tooltip_format_ = config["tooltip-format"].asString(); diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index 25e430a7..68655a76 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -41,8 +41,8 @@ void Window::onCmd(const struct Ipc::ipc_response& res) { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); auto output = payload["output"].isString() ? payload["output"].asString() : ""; - std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_) = - getFocusedNode(payload["nodes"], output); + std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_, + marks_) = getFocusedNode(payload["nodes"], output); updateAppIconName(app_id_, app_class_); dp.emit(); } catch (const std::exception& e) { @@ -96,7 +96,7 @@ auto Window::update() -> void { label_.set_markup(waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_), - fmt::arg("shell", shell_)), + fmt::arg("shell", shell_), fmt::arg("marks", marks_)), config_["rewrite"])); if (tooltipEnabled()) { label_.set_tooltip_text(window_); @@ -108,7 +108,7 @@ auto Window::update() -> void { AAppIconLabel::update(); } -void Window::setClass(std::string classname, bool enable) { +void Window::setClass(const std::string& classname, bool enable) { if (enable) { if (!bar_.window.get_style_context()->has_class(classname)) { bar_.window.get_style_context()->add_class(classname); @@ -169,17 +169,31 @@ std::optional> getSingleChildNode( return {getSingleChildNode(child)}; } -std::tuple getWindowInfo(const Json::Value& node) { +std::tuple getWindowInfo( + const Json::Value& node, bool showHidden) { const auto app_id = node["app_id"].isString() ? node["app_id"].asString() : node["window_properties"]["instance"].asString(); const auto app_class = node["window_properties"]["class"].isString() ? node["window_properties"]["class"].asString() : ""; const auto shell = node["shell"].isString() ? node["shell"].asString() : ""; - return {app_id, app_class, shell}; + std::string marks = ""; + if (node["marks"].isArray()) { + for (const auto& m : node["marks"]) { + if (!m.isString() || (!showHidden && m.asString().at(0) == '_')) { + continue; + } + if (!marks.empty()) { + marks += ','; + } + marks += m.asString(); + } + } + return {app_id, app_class, shell, marks}; } -std::tuple +std::tuple gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Value& config_, const Bar& bar_, Json::Value& parentWorkspace, const Json::Value& immediateParent) { @@ -207,7 +221,8 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu "", "", "", - node["layout"].asString()}; + node["layout"].asString(), + ""}; } parentWorkspace = node; } else if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") && @@ -215,7 +230,8 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu // found node spdlog::trace("actual output {}, output found {}, node (focused) found {}", bar_.output->name, output, node["name"].asString()); - const auto [app_id, app_class, shell] = getWindowInfo(node); + const auto [app_id, app_class, shell, marks] = + getWindowInfo(node, config_["show-hidden-marks"].asBool()); int nb = node.size(); int floating_count = 0; std::string workspace_layout = ""; @@ -232,20 +248,21 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu app_id, app_class, shell, - workspace_layout}; + workspace_layout, + marks}; } // iterate - auto [nb, f, id, name, app_id, app_class, shell, workspace_layout] = + auto [nb, f, id, name, app_id, app_class, shell, workspace_layout, marks] = gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace, node); - auto [nb2, f2, id2, name2, app_id2, app_class2, shell2, workspace_layout2] = + auto [nb2, f2, id2, name2, app_id2, app_class2, shell2, workspace_layout2, marks2] = gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace, node); // if ((id > 0 || ((id2 < 0 || name2.empty()) && id > -1)) && !name.empty()) { if ((id > 0) || (id2 < 0 && id > -1)) { - return {nb, f, id, name, app_id, app_class, shell, workspace_layout}; + return {nb, f, id, name, app_id, app_class, shell, workspace_layout, marks}; } else if (id2 > 0 && !name2.empty()) { - return {nb2, f2, id2, name2, app_id2, app_class, shell2, workspace_layout2}; + return {nb2, f2, id2, name2, app_id2, app_class, shell2, workspace_layout2, marks2}; } } @@ -258,10 +275,12 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu std::string app_id = ""; std::string app_class = ""; std::string workspace_layout = ""; + std::string marks = ""; if (all_leaf_nodes.first == 1) { const auto single_child = getSingleChildNode(immediateParent); if (single_child.has_value()) { - std::tie(app_id, app_class, workspace_layout) = getWindowInfo(single_child.value()); + std::tie(app_id, app_class, workspace_layout, marks) = + getWindowInfo(single_child.value(), config_["show-hidden-marks"].asBool()); } } return {all_leaf_nodes.first, @@ -273,13 +292,15 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu app_id, app_class, workspace_layout, - immediateParent["layout"].asString()}; + immediateParent["layout"].asString(), + marks}; } - return {0, 0, -1, "", "", "", "", ""}; + return {0, 0, -1, "", "", "", "", "", ""}; } -std::tuple +std::tuple Window::getFocusedNode(const Json::Value& nodes, std::string& output) { Json::Value placeholder = Json::Value::null; return gfnWithWorkspace(nodes, output, config_, bar_, placeholder, placeholder); diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index b8ed73d4..86e7349e 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -57,9 +57,9 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); if (config_["format-window-separator"].isString()) { - m_formatWindowSeperator = config_["format-window-separator"].asString(); + m_formatWindowSeparator = config_["format-window-separator"].asString(); } else { - m_formatWindowSeperator = " "; + m_formatWindowSeparator = " "; } const Json::Value &windowRewrite = config["window-rewrite"]; if (windowRewrite.isObject()) { @@ -271,7 +271,7 @@ void Workspaces::updateWindows(const Json::Value &node, std::string &windows) { window = fmt::format(fmt::runtime(window), fmt::arg("name", title), fmt::arg("class", windowClass)); windows.append(window); - windows.append(m_formatWindowSeperator); + windows.append(m_formatWindowSeparator); } } for (const Json::Value &child : node["nodes"]) { @@ -340,7 +340,7 @@ auto Workspaces::update() -> void { fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)), fmt::arg("value", output), fmt::arg("name", trimWorkspaceName(output)), fmt::arg("index", (*it)["num"].asString()), fmt::arg("windows", - windows.substr(0, windows.length() - m_formatWindowSeperator.length())), + windows.substr(0, windows.length() - m_formatWindowSeparator.length())), fmt::arg("output", (*it)["output"].asString())); } if (!config_["disable-markup"].asBool()) { @@ -443,10 +443,11 @@ bool Workspaces::handleScroll(GdkEventScroll *e) { if (it == workspaces_.end()) { return true; } + bool reverse_scroll = config_["reverse-scroll"].isBool() && config_["reverse-scroll"].asBool(); if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) { - name = getCycleWorkspace(it, false); + name = getCycleWorkspace(it, reverse_scroll ? true : false); } else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) { - name = getCycleWorkspace(it, true); + name = getCycleWorkspace(it, reverse_scroll ? false : true); } else { return true; } @@ -494,7 +495,7 @@ std::string Workspaces::trimWorkspaceName(std::string name) { return name; } -bool is_focused_recursive(const Json::Value& node) { +bool is_focused_recursive(const Json::Value &node) { // If a workspace has a focused container then get_tree will say // that the workspace itself isn't focused. Therefore we need to // check if any of its nodes are focused as well. @@ -504,13 +505,13 @@ bool is_focused_recursive(const Json::Value& node) { return true; } - for (const auto& child : node["nodes"]) { + for (const auto &child : node["nodes"]) { if (is_focused_recursive(child)) { return true; } } - for (const auto& child : node["floating_nodes"]) { + for (const auto &child : node["floating_nodes"]) { if (is_focused_recursive(child)) { return true; } diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 56e624cf..90f33be7 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -16,6 +16,7 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& update_pending(false), nr_failed_system(0), nr_failed_user(0), + nr_failed(0), last_status() { if (config["hide-on-ok"].isBool()) { hide_on_ok = config["hide-on-ok"].asBool(); @@ -67,11 +68,38 @@ auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, } } -void SystemdFailedUnits::updateData() { - update_pending = false; +void SystemdFailedUnits::RequestSystemState() { + auto load = [](const char* kind, Glib::RefPtr& proxy) -> std::string { + try { + if (!proxy) return "unknown"; + auto parameters = Glib::VariantContainerBase( + g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState")); + Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); + if (data && data.is_of_type(Glib::VariantType("(v)"))) { + Glib::VariantBase variant; + g_variant_get(data.gobj_copy(), "(v)", &variant); + if (variant && variant.is_of_type(Glib::VARIANT_TYPE_STRING)) { + return g_variant_get_string(variant.gobj_copy(), NULL); + } + } + } catch (Glib::Error& e) { + spdlog::error("Failed to get {} state: {}", kind, e.what().c_str()); + } + return "unknown"; + }; + system_state = load("systemwide", system_proxy); + user_state = load("user", user_proxy); + if (system_state == "running" && user_state == "running") + overall_state = "ok"; + else + overall_state = "degraded"; +} + +void SystemdFailedUnits::RequestFailedUnits() { auto load = [](const char* kind, Glib::RefPtr& proxy) -> uint32_t { try { + if (!proxy) return 0; auto parameters = Glib::VariantContainerBase( g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "NFailedUnits")); Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); @@ -79,9 +107,7 @@ void SystemdFailedUnits::updateData() { Glib::VariantBase variant; g_variant_get(data.gobj_copy(), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_UINT32)) { - uint32_t value = 0; - g_variant_get(variant.gobj_copy(), "u", &value); - return value; + return g_variant_get_uint32(variant.gobj_copy()); } } } catch (Glib::Error& e) { @@ -90,40 +116,46 @@ void SystemdFailedUnits::updateData() { return 0; }; - if (system_proxy) { - nr_failed_system = load("systemwide", system_proxy); - } - if (user_proxy) { - nr_failed_user = load("user", user_proxy); - } + nr_failed_system = load("systemwide", system_proxy); + nr_failed_user = load("user", user_proxy); + nr_failed = nr_failed_system + nr_failed_user; +} + +void SystemdFailedUnits::updateData() { + update_pending = false; + + RequestSystemState(); + if (overall_state == "degraded") RequestFailedUnits(); + dp.emit(); } auto SystemdFailedUnits::update() -> void { - uint32_t nr_failed = nr_failed_system + nr_failed_user; + if (last_status == overall_state) return; // Hide if needed. - if (nr_failed == 0 && hide_on_ok) { + if (overall_state == "ok" && hide_on_ok) { event_box_.set_visible(false); return; } - if (!event_box_.get_visible()) { - event_box_.set_visible(true); - } + + event_box_.set_visible(true); // Set state class. - const std::string status = nr_failed == 0 ? "ok" : "degraded"; if (!last_status.empty() && label_.get_style_context()->has_class(last_status)) { label_.get_style_context()->remove_class(last_status); } - if (!label_.get_style_context()->has_class(status)) { - label_.get_style_context()->add_class(status); + if (!label_.get_style_context()->has_class(overall_state)) { + label_.get_style_context()->add_class(overall_state); } - last_status = status; + + last_status = overall_state; label_.set_markup(fmt::format( fmt::runtime(nr_failed == 0 ? format_ok : format_), fmt::arg("nr_failed", nr_failed), - fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user))); + fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user), + fmt::arg("system_state", system_state), fmt::arg("user_state", user_state), + fmt::arg("overall_state", overall_state))); ALabel::update(); } diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index a3e1c1ee..fa23ef56 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -45,7 +45,7 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); } - // check if file_path_ can be used to retrive the temperature + // check if file_path_ can be used to retrieve the temperature std::ifstream temp(file_path_); if (!temp.is_open()) { throw std::runtime_error("Can't open " + file_path_); @@ -74,12 +74,14 @@ auto waybar::modules::Temperature::update() -> void { if (critical) { format = config_["format-critical"].isString() ? config_["format-critical"].asString() : format; label_.get_style_context()->add_class("critical"); - } else if (warning) { - format = config_["format-warning"].isString() ? config_["format-warning"].asString() : format; - label_.get_style_context()->add_class("warning"); } else { label_.get_style_context()->remove_class("critical"); - label_.get_style_context()->remove_class("warning"); + if (warning) { + format = config_["format-warning"].isString() ? config_["format-warning"].asString() : format; + label_.get_style_context()->add_class("warning"); + } else { + label_.get_style_context()->remove_class("warning"); + } } if (format.empty()) { diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp index 4b832b7e..4844582f 100644 --- a/src/modules/upower.cpp +++ b/src/modules/upower.cpp @@ -10,7 +10,6 @@ UPower::UPower(const std::string &id, const Json::Value &config) : AIconLabel(config, "upower", id, "{percentage}", 0, true, true, true), sleeping_{false} { box_.set_name(name_); box_.set_spacing(0); - box_.set_has_tooltip(AModule::tooltipEnabled()); // Tooltip box contentBox_.set_orientation((box_.get_orientation() == Gtk::ORIENTATION_HORIZONTAL) ? Gtk::ORIENTATION_VERTICAL @@ -70,8 +69,10 @@ UPower::UPower(const std::string &id, const Json::Value &config) g_signal_connect(upClient_, "device-removed", G_CALLBACK(deviceRemoved_cb), this); // Subscribe tooltip query events - box_.set_has_tooltip(); - box_.signal_query_tooltip().connect(sigc::mem_fun(*this, &UPower::queryTooltipCb), false); + box_.set_has_tooltip(AModule::tooltipEnabled()); + if (AModule::tooltipEnabled()) { + box_.signal_query_tooltip().connect(sigc::mem_fun(*this, &UPower::queryTooltipCb), false); + } resetDevices(); setDisplayDevice(); diff --git a/src/modules/user.cpp b/src/modules/user.cpp index 418fc585..3ca5e431 100644 --- a/src/modules/user.cpp +++ b/src/modules/user.cpp @@ -62,7 +62,13 @@ long User::uptime_as_seconds() { #if HAVE_CPU_BSD struct timespec s_info; - if (0 == clock_gettime(CLOCK_UPTIME_PRECISE, &s_info)) { + int flags = 0; +#ifndef __OpenBSD__ + flags = CLOCK_UPTIME_PRECISE; +#else + flags = CLOCK_UPTIME; +#endif + if (0 == clock_gettime(flags, &s_info)) { uptime = s_info.tv_sec; } #endif diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp new file mode 100644 index 00000000..5a9c0c1a --- /dev/null +++ b/src/modules/wayfire/backend.cpp @@ -0,0 +1,445 @@ +#include "modules/wayfire/backend.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace waybar::modules::wayfire { + +std::weak_ptr IPC::instance; + +// C++23: std::byteswap +inline auto byteswap(uint32_t x) -> uint32_t { + return (x & 0xff000000) >> 24 | (x & 0x00ff0000) >> 8 | (x & 0x0000ff00) << 8 | + (x & 0x000000ff) << 24; +} + +auto pack_and_write(Sock& sock, std::string&& buf) -> void { + uint32_t len = buf.size(); + if constexpr (std::endian::native != std::endian::little) len = byteswap(len); + (void)write(sock.fd, &len, 4); + (void)write(sock.fd, buf.data(), buf.size()); +} + +auto read_exact(Sock& sock, size_t n) -> std::string { + auto buf = std::string(n, 0); + for (size_t i = 0; i < n;) i += read(sock.fd, &buf[i], n - i); + return buf; +} + +// https://github.com/WayfireWM/pywayfire/blob/69b7c21/wayfire/ipc.py#L438 +inline auto is_mapped_toplevel_view(const Json::Value& view) -> bool { + return view["mapped"].asBool() && view["role"] != "desktop-environment" && + view["pid"].asInt() != -1; +} + +auto State::Wset::count_ws(const Json::Value& pos) -> Workspace& { + auto x = pos["x"].asInt(); + auto y = pos["y"].asInt(); + return wss.at(ws_w * y + x); +} + +auto State::Wset::locate_ws(const Json::Value& geo) -> Workspace& { + return const_cast(std::as_const(*this).locate_ws(geo)); +} + +auto State::Wset::locate_ws(const Json::Value& geo) const -> const Workspace& { + const auto& out = output.value().get(); + auto [qx, rx] = std::div(geo["x"].asInt(), out.w); + auto [qy, ry] = std::div(geo["y"].asInt(), out.h); + auto x = std::max(0, (int)ws_x + qx - int{rx < 0}); + auto y = std::max(0, (int)ws_y + qy - int{ry < 0}); + return wss.at(ws_w * y + x); +} + +auto State::update_view(const Json::Value& view) -> void { + auto id = view["id"].asUInt(); + + // erase old view information + if (views.contains(id)) { + auto& old_view = views.at(id); + auto& ws = wsets.at(old_view["wset-index"].asUInt()).locate_ws(old_view["geometry"]); + ws.num_views--; + if (old_view["sticky"].asBool()) ws.num_sticky_views--; + views.erase(id); + } + + // insert or assign new view information + if (is_mapped_toplevel_view(view)) { + try { + // view["wset-index"] could be messed up + auto& ws = wsets.at(view["wset-index"].asUInt()).locate_ws(view["geometry"]); + ws.num_views++; + if (view["sticky"].asBool()) ws.num_sticky_views++; + views.emplace(id, view); + } catch (const std::exception&) { + } + } +} + +auto IPC::get_instance() -> std::shared_ptr { + auto p = instance.lock(); + if (!p) instance = p = std::shared_ptr(new IPC); + return p; +} + +auto IPC::connect() -> Sock { + auto* path = std::getenv("WAYFIRE_SOCKET"); + if (path == nullptr) { + throw std::runtime_error{"Wayfire IPC: ipc not available"}; + } + + auto sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock == -1) { + throw std::runtime_error{"Wayfire IPC: socket() failed"}; + } + + auto addr = sockaddr_un{.sun_family = AF_UNIX}; + std::strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + + if (::connect(sock, (const sockaddr*)&addr, sizeof(addr)) == -1) { + close(sock); + throw std::runtime_error{"Wayfire IPC: connect() failed"}; + } + + return {sock}; +} + +auto IPC::receive(Sock& sock) -> Json::Value { + auto len = *reinterpret_cast(read_exact(sock, 4).data()); + if constexpr (std::endian::native != std::endian::little) len = byteswap(len); + auto buf = read_exact(sock, len); + + Json::Value json; + std::string err; + auto* reader = reader_builder.newCharReader(); + if (!reader->parse(&*buf.begin(), &*buf.end(), &json, &err)) { + throw std::runtime_error{"Wayfire IPC: parse json failed: " + err}; + } + return json; +} + +auto IPC::send(const std::string& method, Json::Value&& data) -> Json::Value { + spdlog::debug("Wayfire IPC: send method \"{}\"", method); + auto sock = connect(); + + Json::Value json; + json["method"] = method; + json["data"] = std::move(data); + + pack_and_write(sock, Json::writeString(writer_builder, json)); + auto res = receive(sock); + root_event_handler(method, res); + return res; +} + +auto IPC::start() -> void { + spdlog::info("Wayfire IPC: starting"); + + // init state + send("window-rules/list-outputs", {}); + send("window-rules/list-wsets", {}); + send("window-rules/list-views", {}); + send("window-rules/get-focused-view", {}); + send("window-rules/get-focused-output", {}); + + std::thread([&] { + auto sock = connect(); + + { + Json::Value json; + json["method"] = "window-rules/events/watch"; + + pack_and_write(sock, Json::writeString(writer_builder, json)); + if (receive(sock)["result"] != "ok") { + spdlog::error( + "Wayfire IPC: method \"window-rules/events/watch\"" + " have failed"); + return; + } + } + + while (auto json = receive(sock)) { + auto ev = json["event"].asString(); + spdlog::debug("Wayfire IPC: received event \"{}\"", ev); + root_event_handler(ev, json); + } + }).detach(); +} + +auto IPC::register_handler(const std::string& event, const EventHandler& handler) -> void { + auto _ = std::lock_guard{handlers_mutex}; + handlers.emplace_back(event, handler); +} + +auto IPC::unregister_handler(EventHandler& handler) -> void { + auto _ = std::lock_guard{handlers_mutex}; + handlers.remove_if([&](auto& e) { return &e.second.get() == &handler; }); +} + +auto IPC::root_event_handler(const std::string& event, const Json::Value& data) -> void { + bool new_output_detected; + { + auto _ = lock_state(); + update_state_handler(event, data); + new_output_detected = state.new_output_detected; + state.new_output_detected = false; + } + if (new_output_detected) { + send("window-rules/list-outputs", {}); + send("window-rules/list-wsets", {}); + } + { + auto _ = std::lock_guard{handlers_mutex}; + for (const auto& [_event, handler] : handlers) + if (_event == event) handler(event); + } +} + +auto IPC::update_state_handler(const std::string& event, const Json::Value& data) -> void { + // IPC events + // https://github.com/WayfireWM/wayfire/blob/053b222/plugins/ipc-rules/ipc-events.hpp#L108-L125 + /* + [x] view-mapped + [x] view-unmapped + [-] view-set-output // for detect new output + [ ] view-geometry-changed // -> view-workspace-changed + [x] view-wset-changed + [x] view-focused + [x] view-title-changed + [x] view-app-id-changed + [x] plugin-activation-state-changed + [x] output-gain-focus + + [ ] view-tiled + [ ] view-minimized + [ ] view-fullscreened + [x] view-sticky + [x] view-workspace-changed + [x] output-wset-changed + [x] wset-workspace-changed + */ + + if (event == "view-mapped") { + // data: { event, view } + state.update_view(data["view"]); + return; + } + + if (event == "view-unmapped") { + // data: { event, view } + try { + // data["view"]["wset-index"] could be messed up + state.update_view(data["view"]); + state.maybe_empty_focus_wset_idx = data["view"]["wset-index"].asUInt(); + } catch (const std::exception&) { + } + return; + } + + if (event == "view-set-output") { + // data: { event, output?, view } + // new output event + if (!state.outputs.contains(data["view"]["output-name"].asString())) { + state.new_output_detected = true; + } + return; + } + + if (event == "view-wset-changed") { + // data: { event, old-wset: wset, new-wset: wset, view } + state.maybe_empty_focus_wset_idx = data["old-wset"]["index"].asUInt(); + state.update_view(data["view"]); + return; + } + + if (event == "view-focused") { + // data: { event, view? } + if (const auto& view = data["view"]) { + try { + // view["wset-index"] could be messed up + auto& wset = state.wsets.at(view["wset-index"].asUInt()); + wset.focused_view_id = view["id"].asUInt(); + } catch (const std::exception&) { + } + } else { + // focused to null + if (state.wsets.contains(state.maybe_empty_focus_wset_idx)) + state.wsets.at(state.maybe_empty_focus_wset_idx).focused_view_id = {}; + } + return; + } + + if (event == "view-title-changed" || event == "view-app-id-changed" || event == "view-sticky") { + // data: { event, view } + state.update_view(data["view"]); + return; + } + + if (event == "plugin-activation-state-changed") { + // data: { event, plugin: name, state: bool, output: id, output-data: output } + auto plugin = data["plugin"].asString(); + auto plugin_state = data["state"].asBool(); + + if (plugin == "vswitch") { + state.vswitching = plugin_state; + if (plugin_state) { + state.maybe_empty_focus_wset_idx = data["output-data"]["wset-index"].asUInt(); + } + } + + return; + } + + if (event == "output-gain-focus") { + // data: { event, output } + state.focused_output_name = data["output"]["name"].asString(); + return; + } + + if (event == "view-workspace-changed") { + // data: { event, from: point, to: point, view } + if (state.vswitching) { + if (state.vswitch_sticky_view_id == 0) { + auto& wset = state.wsets.at(data["view"]["wset-index"].asUInt()); + auto& old_ws = wset.locate_ws(state.views.at(data["view"]["id"].asUInt())["geometry"]); + auto& new_ws = wset.count_ws(data["to"]); + old_ws.num_views--; + new_ws.num_views++; + if (data["view"]["sticky"].asBool()) { + old_ws.num_sticky_views--; + new_ws.num_sticky_views++; + } + state.update_view(data["view"]); + state.vswitch_sticky_view_id = data["view"]["id"].asUInt(); + } else { + state.vswitch_sticky_view_id = {}; + } + return; + } + state.update_view(data["view"]); + return; + } + + if (event == "output-wset-changed") { + // data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output } + auto& output = state.outputs.at(data["output-data"]["name"].asString()); + auto wset_idx = data["new-wset-data"]["index"].asUInt(); + state.wsets.at(wset_idx).output = output; + output.wset_idx = wset_idx; + return; + } + + if (event == "wset-workspace-changed") { + // data: { event, previous-workspace: point, new-workspace: point, + // output: id, wset: wset.name, output-data: output, wset-data: wset } + auto wset_idx = data["wset-data"]["index"].asUInt(); + auto& wset = state.wsets.at(wset_idx); + wset.ws_x = data["new-workspace"]["x"].asUInt(); + wset.ws_y = data["new-workspace"]["y"].asUInt(); + + // correct existing views geometry + auto& out = wset.output.value().get(); + auto dx = (int)out.w * ((int)wset.ws_x - data["previous-workspace"]["x"].asInt()); + auto dy = (int)out.h * ((int)wset.ws_y - data["previous-workspace"]["y"].asInt()); + for (auto& [_, view] : state.views) { + if (view["wset-index"].asUInt() == wset_idx && + view["id"].asUInt() != state.vswitch_sticky_view_id) { + view["geometry"]["x"] = view["geometry"]["x"].asInt() - dx; + view["geometry"]["y"] = view["geometry"]["y"].asInt() - dy; + } + } + return; + } + + // IPC responses + // https://github.com/WayfireWM/wayfire/blob/053b222/plugins/ipc-rules/ipc-rules.cpp#L27-L37 + + if (event == "window-rules/list-views") { + // data: [ view ] + state.views.clear(); + for (auto& [_, wset] : state.wsets) std::ranges::fill(wset.wss, State::Workspace{}); + for (const auto& view : data | std::views::filter(is_mapped_toplevel_view)) { + state.update_view(view); + } + return; + } + + if (event == "window-rules/list-outputs") { + // data: [ output ] + state.outputs.clear(); + for (const auto& output_data : data) { + state.outputs.emplace(output_data["name"].asString(), + State::Output{ + .id = output_data["id"].asUInt(), + .w = output_data["geometry"]["width"].asUInt(), + .h = output_data["geometry"]["height"].asUInt(), + .wset_idx = output_data["wset-index"].asUInt(), + }); + } + return; + } + + if (event == "window-rules/list-wsets") { + // data: [ wset ] + std::unordered_map wsets; + for (const auto& wset_data : data) { + auto wset_idx = wset_data["index"].asUInt(); + + auto output_name = wset_data["output-name"].asString(); + auto output = state.outputs.contains(output_name) + ? std::optional{std::ref(state.outputs.at(output_name))} + : std::nullopt; + + const auto& ws_data = wset_data["workspace"]; + auto ws_w = ws_data["grid_width"].asUInt(); + auto ws_h = ws_data["grid_height"].asUInt(); + + wsets.emplace(wset_idx, State::Wset{ + .output = output, + .wss = std::vector(ws_w * ws_h), + .ws_w = ws_w, + .ws_h = ws_h, + .ws_x = ws_data["x"].asUInt(), + .ws_y = ws_data["y"].asUInt(), + }); + + if (state.wsets.contains(wset_idx)) { + auto& old_wset = state.wsets.at(wset_idx); + auto& new_wset = wsets.at(wset_idx); + new_wset.wss = std::move(old_wset.wss); + new_wset.focused_view_id = old_wset.focused_view_id; + } + } + state.wsets = std::move(wsets); + return; + } + + if (event == "window-rules/get-focused-view") { + // data: { ok, info: view? } + if (const auto& view = data["info"]) { + auto& wset = state.wsets.at(view["wset-index"].asUInt()); + wset.focused_view_id = view["id"].asUInt(); + state.update_view(view); + } + return; + } + + if (event == "window-rules/get-focused-output") { + // data: { ok, info: output } + state.focused_output_name = data["info"]["name"].asString(); + return; + } +} + +} // namespace waybar::modules::wayfire diff --git a/src/modules/wayfire/window.cpp b/src/modules/wayfire/window.cpp new file mode 100644 index 00000000..fbcde6ec --- /dev/null +++ b/src/modules/wayfire/window.cpp @@ -0,0 +1,77 @@ +#include "modules/wayfire/window.hpp" + +#include +#include +#include + +#include "util/rewrite_string.hpp" +#include "util/sanitize_str.hpp" + +namespace waybar::modules::wayfire { + +Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) + : AAppIconLabel(config, "window", id, "{title}", 0, true), + ipc{IPC::get_instance()}, + handler{[this](const auto&) { dp.emit(); }}, + bar_{bar} { + ipc->register_handler("view-unmapped", handler); + ipc->register_handler("view-focused", handler); + ipc->register_handler("view-title-changed", handler); + ipc->register_handler("view-app-id-changed", handler); + + ipc->register_handler("window-rules/get-focused-view", handler); + + dp.emit(); +} + +Window::~Window() { ipc->unregister_handler(handler); } + +auto Window::update() -> void { + update_icon_label(); + AAppIconLabel::update(); +} + +auto Window::update_icon_label() -> void { + auto _ = ipc->lock_state(); + + const auto& output = ipc->get_outputs().at(bar_.output->name); + const auto& wset = ipc->get_wsets().at(output.wset_idx); + const auto& views = ipc->get_views(); + auto ctx = bar_.window.get_style_context(); + + if (views.contains(wset.focused_view_id)) { + const auto& view = views.at(wset.focused_view_id); + auto title = view["title"].asString(); + auto app_id = view["app-id"].asString(); + + // update label + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", waybar::util::sanitize_string(title)), + fmt::arg("app_id", waybar::util::sanitize_string(app_id))), + config_["rewrite"])); + + // update window#waybar.solo + if (wset.locate_ws(view["geometry"]).num_views > 1) + ctx->remove_class("solo"); + else + ctx->add_class("solo"); + + // update window#waybar. + ctx->remove_class(old_app_id_); + ctx->add_class(old_app_id_ = app_id); + + // update window#waybar.empty + ctx->remove_class("empty"); + + // + updateAppIconName(app_id, ""); + label_.show(); + } else { + ctx->add_class("empty"); + + updateAppIconName("", ""); + label_.hide(); + } +} + +} // namespace waybar::modules::wayfire diff --git a/src/modules/wayfire/workspaces.cpp b/src/modules/wayfire/workspaces.cpp new file mode 100644 index 00000000..4c4cd6c1 --- /dev/null +++ b/src/modules/wayfire/workspaces.cpp @@ -0,0 +1,183 @@ +#include "modules/wayfire/workspaces.hpp" + +#include +#include +#include + +#include +#include + +#include "modules/wayfire/backend.hpp" + +namespace waybar::modules::wayfire { + +Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config) + : AModule{config, "workspaces", id, false, !config["disable-scroll"].asBool()}, + ipc{IPC::get_instance()}, + handler{[this](const auto&) { dp.emit(); }}, + bar_{bar} { + // init box_ + box_.set_name("workspaces"); + if (!id.empty()) box_.get_style_context()->add_class(id); + box_.get_style_context()->add_class(MODULE_CLASS); + event_box_.add(box_); + + // scroll events + if (!config_["disable-scroll"].asBool()) { + auto& target = config_["enable-bar-scroll"].asBool() ? const_cast(bar_).window + : dynamic_cast(box_); + target.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + target.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); + } + + // listen events + ipc->register_handler("view-mapped", handler); + ipc->register_handler("view-unmapped", handler); + ipc->register_handler("view-wset-changed", handler); + ipc->register_handler("output-gain-focus", handler); + ipc->register_handler("view-sticky", handler); + ipc->register_handler("view-workspace-changed", handler); + ipc->register_handler("output-wset-changed", handler); + ipc->register_handler("wset-workspace-changed", handler); + + ipc->register_handler("window-rules/list-views", handler); + ipc->register_handler("window-rules/list-outputs", handler); + ipc->register_handler("window-rules/list-wsets", handler); + ipc->register_handler("window-rules/get-focused-output", handler); + + // initial render + dp.emit(); +} + +Workspaces::~Workspaces() { ipc->unregister_handler(handler); } + +auto Workspaces::handleScroll(GdkEventScroll* e) -> bool { + // Ignore emulated scroll events on window + if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) return false; + + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) return true; + + int delta; + if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) + delta = 1; + else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) + delta = -1; + else + return true; + + // cycle workspace + Json::Value data; + { + auto _ = ipc->lock_state(); + const auto& output = ipc->get_outputs().at(bar_.output->name); + const auto& wset = ipc->get_wsets().at(output.wset_idx); + auto n = wset.ws_w * wset.ws_h; + auto i = (wset.ws_idx() + delta + n) % n; + data["x"] = Json::Value((uint64_t)i % wset.ws_w); + data["y"] = Json::Value((uint64_t)i / wset.ws_h); + data["output-id"] = Json::Value((uint64_t)output.id); + } + ipc->send("vswitch/set-workspace", std::move(data)); + + return true; +} + +auto Workspaces::update() -> void { + update_box(); + AModule::update(); +} + +auto Workspaces::update_box() -> void { + auto _ = ipc->lock_state(); + + const auto& output_name = bar_.output->name; + const auto& output = ipc->get_outputs().at(output_name); + const auto& wset = ipc->get_wsets().at(output.wset_idx); + + auto output_focused = ipc->get_focused_output_name() == output_name; + auto ws_w = wset.ws_w; + auto ws_h = wset.ws_h; + auto num_wss = ws_w * ws_h; + + // add buttons for new workspaces + for (auto i = buttons_.size(); i < num_wss; i++) { + auto& btn = buttons_.emplace_back(""); + box_.pack_start(btn, false, false, 0); + btn.set_relief(Gtk::RELIEF_NONE); + if (!config_["disable-click"].asBool()) { + btn.signal_pressed().connect([=, this] { + Json::Value data; + data["x"] = Json::Value((uint64_t)i % ws_w); + data["y"] = Json::Value((uint64_t)i / ws_h); + data["output-id"] = Json::Value((uint64_t)output.id); + ipc->send("vswitch/set-workspace", std::move(data)); + }); + } + } + + // remove buttons for removed workspaces + buttons_.resize(num_wss); + + // update buttons + for (size_t i = 0; i < num_wss; i++) { + const auto& ws = wset.wss[i]; + auto& btn = buttons_[i]; + auto ctx = btn.get_style_context(); + auto ws_focused = i == wset.ws_idx(); + auto ws_empty = ws.num_views == 0; + + // update #workspaces button.focused + if (ws_focused) + ctx->add_class("focused"); + else + ctx->remove_class("focused"); + + // update #workspaces button.empty + if (ws_empty) + ctx->add_class("empty"); + else + ctx->remove_class("empty"); + + // update #workspaces button.current_output + if (output_focused) + ctx->add_class("current_output"); + else + ctx->remove_class("current_output"); + + // update label + auto label = std::to_string(i + 1); + if (config_["format"].isString()) { + auto format = config_["format"].asString(); + auto ws_idx = std::to_string(i + 1); + + const auto& icons = config_["format-icons"]; + std::string icon; + if (!icons) + icon = ws_idx; + else if (ws_focused && icons["focused"]) + icon = icons["focused"].asString(); + else if (icons[ws_idx]) + icon = icons[ws_idx].asString(); + else if (icons["default"]) + icon = icons["default"].asString(); + else + icon = ws_idx; + + label = fmt::format(fmt::runtime(format), fmt::arg("icon", icon), fmt::arg("index", ws_idx), + fmt::arg("output", output_name)); + } + if (!config_["disable-markup"].asBool()) + static_cast(btn.get_children()[0])->set_markup(label); + else + btn.set_label(label); + + // + if (config_["current-only"].asBool() && i != wset.ws_idx()) + btn.hide(); + else + btn.show(); + } +} + +} // namespace waybar::modules::wayfire diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 106ca403..a43ad29b 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -14,11 +14,15 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val mixer_api_(nullptr), def_nodes_api_(nullptr), default_node_name_(nullptr), + default_source_name_(nullptr), pending_plugins_(0), muted_(false), + source_muted_(false), volume_(0.0), + source_volume_(0.0), min_step_(0.0), node_id_(0), + source_node_id_(0), type_(nullptr) { waybar::modules::Wireplumber::modules.push_back(this); @@ -55,6 +59,7 @@ waybar::modules::Wireplumber::~Wireplumber() { g_clear_object(&mixer_api_); g_clear_object(&def_nodes_api_); g_free(default_node_name_); + g_free(default_source_name_); g_free(type_); } @@ -94,6 +99,43 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_); } +void waybar::modules::Wireplumber::updateSourceName(waybar::modules::Wireplumber* self, + uint32_t id) { + spdlog::debug("[{}]: updating source name with node.id {}", self->name_, id); + + if (!isValidNodeId(id)) { + spdlog::warn("[{}]: '{}' is not a valid source node ID. Ignoring source name update.", + self->name_, id); + return; + } + + auto* proxy = static_cast(wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY, + WP_CONSTRAINT_TYPE_G_PROPERTY, + "bound-id", "=u", id, nullptr)); + + if (proxy == nullptr) { + auto err = fmt::format("Source object '{}' not found\n", id); + spdlog::error("[{}]: {}", self->name_, err); + return; + } + + g_autoptr(WpProperties) properties = + WP_IS_PIPEWIRE_OBJECT(proxy) != 0 + ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy)) + : wp_properties_new_empty(); + g_autoptr(WpProperties) globalP = wp_global_proxy_get_global_properties(WP_GLOBAL_PROXY(proxy)); + properties = wp_properties_ensure_unique_owner(properties); + wp_properties_add(properties, globalP); + wp_properties_set(properties, "object.id", nullptr); + const auto* nick = wp_properties_get(properties, "node.nick"); + const auto* description = wp_properties_get(properties, "node.description"); + + self->source_name_ = nick != nullptr ? nick + : description != nullptr ? description + : "Unknown source name"; + spdlog::debug("[{}]: Updating source name to: {}", self->name_, self->source_name_); +} + void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self, uint32_t id) { spdlog::debug("[{}]: updating volume", self->name_); GVariant* variant = nullptr; @@ -120,6 +162,31 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se self->dp.emit(); } +void waybar::modules::Wireplumber::updateSourceVolume(waybar::modules::Wireplumber* self, + uint32_t id) { + spdlog::debug("[{}]: updating source volume", self->name_); + GVariant* variant = nullptr; + + if (!isValidNodeId(id)) { + spdlog::error("[{}]: '{}' is not a valid source node ID. Ignoring source volume update.", + self->name_, id); + return; + } + + g_signal_emit_by_name(self->mixer_api_, "get-volume", id, &variant); + + if (variant == nullptr) { + spdlog::debug("[{}]: Source node {} does not support volume", self->name_, id); + return; + } + + g_variant_lookup(variant, "volume", "d", &self->source_volume_); + g_variant_lookup(variant, "mute", "b", &self->source_muted_); + g_clear_pointer(&variant, g_variant_unref); + + self->dp.emit(); +} + void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id) { g_autoptr(WpNode) node = static_cast(wp_object_manager_lookup( self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, nullptr)); @@ -127,9 +194,9 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* if (node == nullptr) { // log a warning only if no other widget is targeting the id. // this reduces log spam when multiple instances of the module are used on different node types. - if (id != self->node_id_) { + if (id != self->node_id_ && id != self->source_node_id_) { for (auto const& module : waybar::modules::Wireplumber::modules) { - if (module->node_id_ == id) { + if (module->node_id_ == id || module->source_node_id_ == id) { return; } } @@ -142,68 +209,73 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* const gchar* name = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name"); - if (self->node_id_ != id) { - spdlog::debug( - "[{}]: (onMixerChanged: {}) - ignoring mixer update for node: id: {}, name: {} as it is " - "not the default node: {} with id: {}", - self->name_, self->type_, id, name, self->default_node_name_, self->node_id_); - return; + if (self->node_id_ == id) { + spdlog::debug("[{}]: (onMixerChanged: {}) - updating sink volume for node: {}", self->name_, + self->type_, name); + updateVolume(self, id); + } else if (self->source_node_id_ == id) { + spdlog::debug("[{}]: (onMixerChanged: {}) - updating source volume for node: {}", self->name_, + self->type_, name); + updateSourceVolume(self, id); } - - spdlog::debug( - "[{}]: (onMixerChanged: {}) - Need to update volume for node with id {} and name {}", - self->name_, self->type_, id, name); - updateVolume(self, id); } void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wireplumber* self) { spdlog::debug("[{}]: (onDefaultNodesApiChanged: {})", self->name_, self->type_); + // Handle sink uint32_t defaultNodeId; g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &defaultNodeId); - if (!isValidNodeId(defaultNodeId)) { - spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring '{}' node change.", self->name_, - defaultNodeId, self->type_); - return; + if (isValidNodeId(defaultNodeId)) { + g_autoptr(WpNode) node = static_cast( + wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", + "=u", defaultNodeId, nullptr)); + + if (node != nullptr) { + const gchar* defaultNodeName = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name"); + + if (g_strcmp0(self->default_node_name_, defaultNodeName) != 0 || + self->node_id_ != defaultNodeId) { + spdlog::debug("[{}]: Default sink changed to -> Node(name: {}, id: {})", self->name_, + defaultNodeName, defaultNodeId); + + g_free(self->default_node_name_); + self->default_node_name_ = g_strdup(defaultNodeName); + self->node_id_ = defaultNodeId; + updateVolume(self, defaultNodeId); + updateNodeName(self, defaultNodeId); + } + } } - g_autoptr(WpNode) node = static_cast( - wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", - "=u", defaultNodeId, nullptr)); + // Handle source + uint32_t defaultSourceId; + g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Source", &defaultSourceId); - if (node == nullptr) { - spdlog::warn("[{}]: (onDefaultNodesApiChanged: {}) - Object with id {} not found", self->name_, - self->type_, defaultNodeId); - return; + if (isValidNodeId(defaultSourceId)) { + g_autoptr(WpNode) sourceNode = static_cast( + wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", + "=u", defaultSourceId, nullptr)); + + if (sourceNode != nullptr) { + const gchar* defaultSourceName = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(sourceNode), "node.name"); + + if (g_strcmp0(self->default_source_name_, defaultSourceName) != 0 || + self->source_node_id_ != defaultSourceId) { + spdlog::debug("[{}]: Default source changed to -> Node(name: {}, id: {})", self->name_, + defaultSourceName, defaultSourceId); + + g_free(self->default_source_name_); + self->default_source_name_ = g_strdup(defaultSourceName); + self->source_node_id_ = defaultSourceId; + updateSourceVolume(self, defaultSourceId); + updateSourceName(self, defaultSourceId); + } + } } - - const gchar* defaultNodeName = - wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name"); - - spdlog::debug( - "[{}]: (onDefaultNodesApiChanged: {}) - got the following default node: Node(name: {}, id: " - "{})", - self->name_, self->type_, defaultNodeName, defaultNodeId); - - if (g_strcmp0(self->default_node_name_, defaultNodeName) == 0 && - self->node_id_ == defaultNodeId) { - spdlog::debug( - "[{}]: (onDefaultNodesApiChanged: {}) - Default node has not changed. Node(name: {}, id: " - "{}). Ignoring.", - self->name_, self->type_, self->default_node_name_, defaultNodeId); - return; - } - - spdlog::debug( - "[{}]: (onDefaultNodesApiChanged: {}) - Default node changed to -> Node(name: {}, id: {})", - self->name_, self->type_, defaultNodeName, defaultNodeId); - - g_free(self->default_node_name_); - self->default_node_name_ = g_strdup(defaultNodeName); - self->node_id_ = defaultNodeId; - updateVolume(self, defaultNodeId); - updateNodeName(self, defaultNodeId); } void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wireplumber* self) { @@ -223,18 +295,31 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir throw std::runtime_error("Mixer api is not loaded\n"); } + // Get default sink g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", self->type_, &self->default_node_name_); g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &self->node_id_); + // Get default source + g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Source", + &self->default_source_name_); + g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Source", + &self->source_node_id_); + if (self->default_node_name_ != nullptr) { spdlog::debug( "[{}]: (onObjectManagerInstalled: {}) - default configured node name: {} and id: {}", self->name_, self->type_, self->default_node_name_, self->node_id_); } + if (self->default_source_name_ != nullptr) { + spdlog::debug("[{}]: default source: {} (id: {})", self->name_, self->default_source_name_, + self->source_node_id_); + } updateVolume(self, self->node_id_); updateNodeName(self, self->node_id_); + updateSourceVolume(self, self->source_node_id_); + updateSourceName(self, self->source_node_id_); g_signal_connect_swapped(self->mixer_api_, "changed", (GCallback)onMixerChanged, self); g_signal_connect_swapped(self->def_nodes_api_, "changed", (GCallback)onDefaultNodesApiChanged, @@ -271,6 +356,8 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { spdlog::debug("[{}]: preparing object manager: '{}'", name_, self->type_); wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", self->type_, nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", + "=s", "Audio/Source", nullptr); } void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, @@ -332,19 +419,57 @@ auto waybar::modules::Wireplumber::update() -> void { auto format = format_; std::string tooltipFormat; + // Handle sink mute state if (muted_) { format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; label_.get_style_context()->add_class("muted"); + label_.get_style_context()->add_class("sink-muted"); } else { label_.get_style_context()->remove_class("muted"); + label_.get_style_context()->remove_class("sink-muted"); + } + + // Handle source mute state + if (source_muted_) { + label_.get_style_context()->add_class("source-muted"); + } else { + label_.get_style_context()->remove_class("source-muted"); } int vol = round(volume_ * 100.0); - std::string markup = fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_), - fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol))); - label_.set_markup(markup); + int source_vol = round(source_volume_ * 100.0); - getState(vol); + // Get the state and apply state-specific format if available + auto state = getState(vol); + if (!state.empty()) { + std::string format_name = muted_ ? "format-muted" : "format"; + std::string state_format_name = format_name + "-" + state; + if (config_[state_format_name].isString()) { + format = config_[state_format_name].asString(); + } + } + + // Prepare source format string (similar to PulseAudio) + std::string format_source = "{volume}%"; + if (source_muted_) { + if (config_["format-source-muted"].isString()) { + format_source = config_["format-source-muted"].asString(); + } + } else { + if (config_["format-source"].isString()) { + format_source = config_["format-source"].asString(); + } + } + + // Format the source string with actual volume + std::string formatted_source = + fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_vol)); + + std::string markup = + fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), + fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), + fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_)); + label_.set_markup(markup); if (tooltipEnabled()) { if (tooltipFormat.empty() && config_["tooltip-format"].isString()) { @@ -352,9 +477,10 @@ auto waybar::modules::Wireplumber::update() -> void { } if (!tooltipFormat.empty()) { - label_.set_tooltip_text(fmt::format(fmt::runtime(tooltipFormat), - fmt::arg("node_name", node_name_), - fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol)))); + label_.set_tooltip_text(fmt::format( + fmt::runtime(tooltipFormat), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), + fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), + fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_))); } else { label_.set_tooltip_text(node_name_); } diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 8e3b2542..c5c522d8 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -26,194 +26,6 @@ namespace waybar::modules::wlr { -/* Icon loading functions */ -static std::vector search_prefix() { - std::vector prefixes = {""}; - - std::string home_dir = std::getenv("HOME"); - prefixes.push_back(home_dir + "/.local/share/"); - - auto xdg_data_dirs = std::getenv("XDG_DATA_DIRS"); - if (!xdg_data_dirs) { - prefixes.emplace_back("/usr/share/"); - prefixes.emplace_back("/usr/local/share/"); - } else { - std::string xdg_data_dirs_str(xdg_data_dirs); - size_t start = 0, end = 0; - - do { - end = xdg_data_dirs_str.find(':', start); - auto p = xdg_data_dirs_str.substr(start, end - start); - prefixes.push_back(trim(p) + "/"); - - start = end == std::string::npos ? end : end + 1; - } while (end != std::string::npos); - } - - for (auto &p : prefixes) spdlog::debug("Using 'desktop' search path prefix: {}", p); - - return prefixes; -} - -static Glib::RefPtr load_icon_from_file(std::string icon_path, int size) { - try { - auto pb = Gdk::Pixbuf::create_from_file(icon_path, size, size); - return pb; - } catch (...) { - return {}; - } -} - -static Glib::RefPtr get_app_info_by_name(const std::string &app_id) { - static std::vector prefixes = search_prefix(); - - std::vector app_folders = {"", "applications/", "applications/kde/", - "applications/org.kde."}; - - std::vector suffixes = {"", ".desktop"}; - - for (auto &prefix : prefixes) { - for (auto &folder : app_folders) { - for (auto &suffix : suffixes) { - auto app_info_ = - Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix); - if (!app_info_) { - continue; - } - - return app_info_; - } - } - } - - return {}; -} - -Glib::RefPtr get_desktop_app_info(const std::string &app_id) { - auto app_info = get_app_info_by_name(app_id); - if (app_info) { - return app_info; - } - - std::string desktop_file = ""; - - gchar ***desktop_list = g_desktop_app_info_search(app_id.c_str()); - if (desktop_list != nullptr && desktop_list[0] != nullptr) { - for (size_t i = 0; desktop_list[0][i]; i++) { - if (desktop_file == "") { - desktop_file = desktop_list[0][i]; - } else { - auto tmp_info = Gio::DesktopAppInfo::create(desktop_list[0][i]); - if (!tmp_info) - // see https://github.com/Alexays/Waybar/issues/1446 - continue; - - auto startup_class = tmp_info->get_startup_wm_class(); - if (startup_class == app_id) { - desktop_file = desktop_list[0][i]; - break; - } - } - } - g_strfreev(desktop_list[0]); - } - g_free(desktop_list); - - return get_app_info_by_name(desktop_file); -} - -void Task::set_app_info_from_app_id_list(const std::string &app_id_list) { - std::string app_id; - std::istringstream stream(app_id_list); - - /* Wayfire sends a list of app-id's in space separated format, other compositors - * send a single app-id, but in any case this works fine */ - while (stream >> app_id) { - app_info_ = get_desktop_app_info(app_id); - if (app_info_) { - return; - } - - auto lower_app_id = app_id; - std::transform(lower_app_id.begin(), lower_app_id.end(), lower_app_id.begin(), - [](char c) { return std::tolower(c); }); - app_info_ = get_desktop_app_info(lower_app_id); - if (app_info_) { - return; - } - - size_t start = 0, end = app_id.size(); - start = app_id.rfind(".", end); - std::string app_name = app_id.substr(start + 1, app_id.size()); - app_info_ = get_desktop_app_info(app_name); - if (app_info_) { - return; - } - - start = app_id.find("-"); - app_name = app_id.substr(0, start); - app_info_ = get_desktop_app_info(app_name); - } -} - -static std::string get_icon_name_from_icon_theme(const Glib::RefPtr &icon_theme, - const std::string &app_id) { - if (icon_theme->lookup_icon(app_id, 24)) return app_id; - - return ""; -} - -bool Task::image_load_icon(Gtk::Image &image, const Glib::RefPtr &icon_theme, - Glib::RefPtr app_info, int size) { - std::string ret_icon_name = "unknown"; - if (app_info) { - std::string icon_name = - get_icon_name_from_icon_theme(icon_theme, app_info->get_startup_wm_class()); - if (!icon_name.empty()) { - ret_icon_name = icon_name; - } else { - if (app_info->get_icon()) { - ret_icon_name = app_info->get_icon()->to_string(); - } - } - } - - Glib::RefPtr pixbuf; - auto scaled_icon_size = size * image.get_scale_factor(); - - try { - pixbuf = icon_theme->load_icon(ret_icon_name, scaled_icon_size, Gtk::ICON_LOOKUP_FORCE_SIZE); - spdlog::debug("{} Loaded icon '{}'", repr(), ret_icon_name); - } catch (...) { - if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS)) { - pixbuf = load_icon_from_file(ret_icon_name, scaled_icon_size); - spdlog::debug("{} Loaded icon from file '{}'", repr(), ret_icon_name); - } else { - try { - pixbuf = DefaultGtkIconThemeWrapper::load_icon( - "image-missing", scaled_icon_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); - spdlog::debug("{} Loaded icon from resource", repr()); - } catch (...) { - pixbuf = {}; - spdlog::debug("{} Unable to load icon.", repr()); - } - } - } - - if (pixbuf) { - if (pixbuf->get_width() != scaled_icon_size) { - int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); - pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); - } - auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image.get_scale_factor(), - image.get_window()); - image.set(surface); - return true; - } - - return false; -} - /* Task class implementation */ uint32_t Task::global_id = 0; @@ -299,16 +111,11 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, with_name_ = true; } - auto icon_pos = format.find("{icon}"); - if (icon_pos == 0) { + auto parts = split(format, "{icon}", 1); + format_before_ = parts[0]; + if (parts.size() > 1) { with_icon_ = true; - format_after_ = format.substr(6); - } else if (icon_pos == std::string::npos) { - format_before_ = format; - } else { - with_icon_ = true; - format_before_ = format.substr(0, icon_pos); - format_after_ = format.substr(icon_pos + 6); + format_after_ = parts[1]; } } else { /* The default is to only show the icon */ @@ -395,7 +202,7 @@ void Task::handle_title(const char *title) { return; } - set_app_info_from_app_id_list(title_); + app_info_ = IconLoader::get_app_info_from_app_id_list(title_); name_ = app_info_ ? app_info_->get_display_name() : title; if (!with_icon_) { @@ -403,15 +210,7 @@ void Task::handle_title(const char *title) { } int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; - bool found = false; - for (auto &icon_theme : tbar_->icon_themes()) { - if (image_load_icon(icon_, icon_theme, app_info_, icon_size)) { - found = true; - break; - } - } - - if (found) + if (tbar_->icon_loader().image_load_icon(icon_, app_info_, icon_size)) icon_.show(); else spdlog::debug("Couldn't find icon for {}", title_); @@ -460,7 +259,7 @@ void Task::handle_app_id(const char *app_id) { return; } - set_app_info_from_app_id_list(app_id_); + app_info_ = IconLoader::get_app_info_from_app_id_list(app_id_); name_ = app_info_ ? app_info_->get_display_name() : app_id; if (!with_icon_) { @@ -468,15 +267,7 @@ void Task::handle_app_id(const char *app_id) { } int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; - bool found = false; - for (auto &icon_theme : tbar_->icon_themes()) { - if (image_load_icon(icon_, icon_theme, app_info_, icon_size)) { - found = true; - break; - } - } - - if (found) + if (tbar_->icon_loader().image_load_icon(icon_, app_info_, icon_size)) icon_.show(); else spdlog::debug("Couldn't find icon for {}", app_id_); @@ -712,6 +503,9 @@ void Task::update() { fmt::format(fmt::runtime(format_tooltip_), fmt::arg("title", title), fmt::arg("name", name), fmt::arg("app_id", app_id), fmt::arg("state", state_string()), fmt::arg("short_state", state_string(true))); + + txt = waybar::util::rewriteString(txt, config_["rewrite"]); + if (markup) button.set_tooltip_markup(txt); else @@ -799,22 +593,10 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu /* Get the configured icon theme if specified */ if (config_["icon-theme"].isArray()) { for (auto &c : config_["icon-theme"]) { - auto it_name = c.asString(); - - auto it = Gtk::IconTheme::create(); - it->set_custom_theme(it_name); - spdlog::debug("Use custom icon theme: {}", it_name); - - icon_themes_.push_back(it); + icon_loader_.add_custom_icon_theme(c.asString()); } } else if (config_["icon-theme"].isString()) { - auto it_name = config_["icon-theme"].asString(); - - auto it = Gtk::IconTheme::create(); - it->set_custom_theme(it_name); - spdlog::debug("Use custom icon theme: {}", it_name); - - icon_themes_.push_back(it); + icon_loader_.add_custom_icon_theme(config_["icon-theme"].asString()); } // Load ignore-list @@ -833,8 +615,6 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu } } - icon_themes_.push_back(Gtk::IconTheme::get_default()); - for (auto &t : tasks_) { t->handle_app_id(t->app_id().c_str()); } @@ -969,9 +749,7 @@ bool Taskbar::all_outputs() const { return config_["all-outputs"].isBool() && config_["all-outputs"].asBool(); } -const std::vector> &Taskbar::icon_themes() const { - return icon_themes_; -} +const IconLoader &Taskbar::icon_loader() const { return icon_loader_; } const std::unordered_set &Taskbar::ignore_list() const { return ignore_list_; } diff --git a/src/modules/wlr/workspace_manager.cpp b/src/modules/wlr/workspace_manager.cpp deleted file mode 100644 index f556a161..00000000 --- a/src/modules/wlr/workspace_manager.cpp +++ /dev/null @@ -1,585 +0,0 @@ -#include "modules/wlr/workspace_manager.hpp" - -#include -#include -#include - -#include -#include -#include -#include - -#include "client.hpp" -#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.orientation, 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_sort_by_number = config_["sort-by-number"]; - if (config_sort_by_number.isBool()) { - sort_by_number_ = config_sort_by_number.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); - } - box_.get_style_context()->add_class(MODULE_CLASS); - event_box_.add(box_); - - add_registry_listener(this); - if (!workspace_manager_) { - return; - } -} - -auto WorkspaceManager::workspace_comparator() const - -> std::function &, std::unique_ptr &)> { - return [=, this](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_number_) { - try { - auto is_number_less = std::stoi(lhs->get_name()) < std::stoi(rhs->get_name()); - return is_number_less; - } catch (const std::invalid_argument &) { - } - } - - 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; - } - - wl_display *display = Client::inst()->wl_display; - - // Send `stop` request and wait for one roundtrip. This is not quite correct as - // the protocol encourages us to wait for the .finished event, but it should work - // with wlroots workspace manager implementation. - zext_workspace_manager_v1_stop(workspace_manager_); - wl_display_roundtrip(display); - - // If the .finished handler is still not executed, destroy the workspace manager here. - if (workspace_manager_) { - spdlog::warn("Foreign toplevel manager destroyed before .finished event"); - 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::fill_persistent_workspaces() -> void { - if (config_["persistent_workspaces"].isObject()) { - spdlog::warn( - "persistent_workspaces is deprecated. Please change config to use persistent-workspaces."); - } - - if ((config_["persistent-workspaces"].isObject() || - config_["persistent_workspaces"].isObject()) && - !workspace_manager_.all_outputs()) { - const Json::Value &p_workspaces = config_["persistent-workspaces"].isObject() - ? config_["persistent-workspaces"] - : config_["persistent_workspaces"]; - const std::vector p_workspaces_names = p_workspaces.getMemberNames(); - - for (const std::string &p_w_name : p_workspaces_names) { - const Json::Value &p_w = p_workspaces[p_w_name]; - if (p_w.isArray() && !p_w.empty()) { - // Adding to target outputs - for (const Json::Value &output : p_w) { - if (output.asString() == bar_.output->name) { - persistent_workspaces_.push_back(p_w_name); - break; - } - } - } else { - // Adding to all outputs - persistent_workspaces_.push_back(p_w_name); - } - } - } -} - -auto WorkspaceGroup::create_persistent_workspaces() -> void { - for (const std::string &p_w_name : persistent_workspaces_) { - auto new_id = ++workspace_global_id; - workspaces_.push_back( - std::make_unique(bar_, config_, *this, nullptr, new_id, p_w_name)); - spdlog::debug("Workspace {} created", new_id); - } -} - -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); - if (!persistent_created_) { - fill_persistent_workspaces(); - create_persistent_workspaces(); - persistent_created_ = true; - } -} - -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, std::string name) - : bar_(bar), - config_(config), - workspace_group_(workspace_group), - workspace_handle_(workspace), - id_(id), - name_(name) { - if (workspace) { - add_workspace_listener(workspace, this); - } else { - state_ = (uint32_t)State::EMPTY; - } - - 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(fmt::runtime(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 { - if (workspace_handle_) { - zext_workspace_handle_v1_destroy(workspace_handle_); - workspace_handle_ = nullptr; - } - if (!persistent_) { - workspace_group_.remove_workspace(id_); - } else { - state_ = (uint32_t)State::EMPTY; - } -} - -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"); - add_or_remove_class(style_context, is_empty(), "persistent"); - - 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; - } - - if (is_empty()) { - auto persistent_icon_it = icons_map_.find("persistent"); - if (persistent_icon_it != icons_map_.end()) { - return persistent_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; - spdlog::debug("Workspace {} added to group {}", name, workspace_group_.id()); - - make_persistent(); - handle_duplicate(); -} - -auto Workspace::make_persistent() -> void { - auto p_workspaces = workspace_group_.persistent_workspaces(); - - if (std::find(p_workspaces.begin(), p_workspaces.end(), name_) != p_workspaces.end()) { - persistent_ = true; - } -} - -auto Workspace::handle_duplicate() -> void { - auto duplicate = - std::find_if(workspace_group_.workspaces().begin(), workspace_group_.workspaces().end(), - [this](const std::unique_ptr &g) { - return g->get_name() == name_ && g->id() != id_; - }); - if (duplicate != workspace_group_.workspaces().end()) { - workspace_group_.remove_workspace(duplicate->get()->id()); - } -} - -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 deleted file mode 100644 index 22e68fbf..00000000 --- a/src/modules/wlr/workspace_manager_binding.cpp +++ /dev/null @@ -1,136 +0,0 @@ -#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/src/util/css_reload_helper.cpp b/src/util/css_reload_helper.cpp index 274bdeed..06381d60 100644 --- a/src/util/css_reload_helper.cpp +++ b/src/util/css_reload_helper.cpp @@ -2,7 +2,13 @@ #include #include +#ifndef __OpenBSD__ #include +#else +#include +#include +#include +#endif #include #include diff --git a/src/util/enum.cpp b/src/util/enum.cpp index dc3eae0c..6b5d5562 100644 --- a/src/util/enum.cpp +++ b/src/util/enum.cpp @@ -41,5 +41,7 @@ EnumType EnumParser::parseStringToEnum(const std::string& str, // Explicit instantiations for specific EnumType types you intend to use // Add explicit instantiations for all relevant EnumType types template struct EnumParser; +template struct EnumParser; +template struct EnumParser; } // namespace waybar::util diff --git a/src/util/gtk_icon.cpp b/src/util/gtk_icon.cpp index 5dd741f9..73f77284 100644 --- a/src/util/gtk_icon.cpp +++ b/src/util/gtk_icon.cpp @@ -15,11 +15,24 @@ bool DefaultGtkIconThemeWrapper::has_icon(const std::string& value) { return Gtk::IconTheme::get_default()->has_icon(value); } -Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon(const char* name, int tmp_size, - Gtk::IconLookupFlags flags) { +Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon( + const char* name, int tmp_size, Gtk::IconLookupFlags flags, + Glib::RefPtr style) { const std::lock_guard lock(default_theme_mutex); auto default_theme = Gtk::IconTheme::get_default(); default_theme->rescan_if_needed(); - return default_theme->load_icon(name, tmp_size, flags); + + auto icon_info = default_theme->lookup_icon(name, tmp_size, flags); + + if (icon_info == nullptr) { + return default_theme->load_icon(name, tmp_size, flags); + } + + if (style.get() == nullptr) { + return icon_info.load_icon(); + } + + bool is_sym = false; + return icon_info.load_symbolic(style, is_sym); } diff --git a/src/util/icon_loader.cpp b/src/util/icon_loader.cpp new file mode 100644 index 00000000..69e25dbc --- /dev/null +++ b/src/util/icon_loader.cpp @@ -0,0 +1,207 @@ +#include "util/icon_loader.hpp" + +#include "util/string.hpp" + +std::vector IconLoader::search_prefix() { + std::vector prefixes = {""}; + + std::string home_dir = std::getenv("HOME"); + prefixes.push_back(home_dir + "/.local/share/"); + + auto xdg_data_dirs = std::getenv("XDG_DATA_DIRS"); + if (!xdg_data_dirs) { + prefixes.emplace_back("/usr/share/"); + prefixes.emplace_back("/usr/local/share/"); + } else { + std::string xdg_data_dirs_str(xdg_data_dirs); + size_t start = 0; + size_t end = 0; + + do { + end = xdg_data_dirs_str.find(':', start); + auto p = xdg_data_dirs_str.substr(start, end - start); + prefixes.push_back(trim(p) + "/"); + + start = end == std::string::npos ? end : end + 1; + } while (end != std::string::npos); + } + + for (auto &p : prefixes) spdlog::debug("Using 'desktop' search path prefix: {}", p); + + return prefixes; +} + +Glib::RefPtr IconLoader::get_app_info_by_name(const std::string &app_id) { + static std::vector prefixes = search_prefix(); + + std::vector app_folders = {"", "applications/", "applications/kde/", + "applications/org.kde."}; + + std::vector suffixes = {"", ".desktop"}; + + for (auto const &prefix : prefixes) { + for (auto const &folder : app_folders) { + for (auto const &suffix : suffixes) { + auto app_info_ = + Gio::DesktopAppInfo::create_from_filename(prefix + folder + app_id + suffix); + if (!app_info_) { + continue; + } + + return app_info_; + } + } + } + + return {}; +} + +Glib::RefPtr IconLoader::get_desktop_app_info(const std::string &app_id) { + auto app_info = get_app_info_by_name(app_id); + if (app_info) { + return app_info; + } + + std::string desktop_file = ""; + + gchar ***desktop_list = g_desktop_app_info_search(app_id.c_str()); + if (desktop_list != nullptr && desktop_list[0] != nullptr) { + for (size_t i = 0; desktop_list[0][i]; i++) { + if (desktop_file == "") { + desktop_file = desktop_list[0][i]; + } else { + auto tmp_info = Gio::DesktopAppInfo::create(desktop_list[0][i]); + if (!tmp_info) + // see https://github.com/Alexays/Waybar/issues/1446 + continue; + + auto startup_class = tmp_info->get_startup_wm_class(); + if (startup_class == app_id) { + desktop_file = desktop_list[0][i]; + break; + } + } + } + g_strfreev(desktop_list[0]); + } + g_free(desktop_list); + + return get_app_info_by_name(desktop_file); +} + +Glib::RefPtr IconLoader::load_icon_from_file(std::string const &icon_path, int size) { + try { + auto pb = Gdk::Pixbuf::create_from_file(icon_path, size, size); + return pb; + } catch (...) { + return {}; + } +} + +std::string IconLoader::get_icon_name_from_icon_theme( + const Glib::RefPtr &icon_theme, const std::string &app_id) { + if (icon_theme->lookup_icon(app_id, 24)) return app_id; + + return ""; +} + +bool IconLoader::image_load_icon(Gtk::Image &image, const Glib::RefPtr &icon_theme, + Glib::RefPtr app_info, int size) { + std::string ret_icon_name = "unknown"; + if (app_info) { + std::string icon_name = + get_icon_name_from_icon_theme(icon_theme, app_info->get_startup_wm_class()); + if (!icon_name.empty()) { + ret_icon_name = icon_name; + } else { + if (app_info->get_icon()) { + ret_icon_name = app_info->get_icon()->to_string(); + } + } + } + + Glib::RefPtr pixbuf; + auto scaled_icon_size = size * image.get_scale_factor(); + + try { + pixbuf = icon_theme->load_icon(ret_icon_name, scaled_icon_size, Gtk::ICON_LOOKUP_FORCE_SIZE); + } catch (...) { + if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS)) { + pixbuf = load_icon_from_file(ret_icon_name, scaled_icon_size); + } else { + try { + pixbuf = DefaultGtkIconThemeWrapper::load_icon( + "image-missing", scaled_icon_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + } catch (...) { + pixbuf = {}; + } + } + } + + if (pixbuf) { + if (pixbuf->get_width() != scaled_icon_size) { + int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); + pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); + } + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image.get_scale_factor(), + image.get_window()); + image.set(surface); + return true; + } + + return false; +} + +void IconLoader::add_custom_icon_theme(const std::string &theme_name) { + auto icon_theme = Gtk::IconTheme::create(); + icon_theme->set_custom_theme(theme_name); + custom_icon_themes_.push_back(icon_theme); + spdlog::debug("Use custom icon theme: {}", theme_name); +} + +bool IconLoader::image_load_icon(Gtk::Image &image, Glib::RefPtr app_info, + int size) const { + for (auto &icon_theme : custom_icon_themes_) { + if (image_load_icon(image, icon_theme, app_info, size)) { + return true; + } + } + return image_load_icon(image, default_icon_theme_, app_info, size); +} + +Glib::RefPtr IconLoader::get_app_info_from_app_id_list( + const std::string &app_id_list) { + std::string app_id; + std::istringstream stream(app_id_list); + Glib::RefPtr app_info_; + + /* Wayfire sends a list of app-id's in space separated format, other compositors + * send a single app-id, but in any case this works fine */ + while (stream >> app_id) { + app_info_ = get_desktop_app_info(app_id); + if (app_info_) { + return app_info_; + } + + auto lower_app_id = app_id; + std::ranges::transform(lower_app_id, lower_app_id.begin(), + [](char c) { return std::tolower(c); }); + app_info_ = get_desktop_app_info(lower_app_id); + if (app_info_) { + return app_info_; + } + + size_t start = 0, end = app_id.size(); + start = app_id.rfind(".", end); + std::string app_name = app_id.substr(start + 1, app_id.size()); + app_info_ = get_desktop_app_info(app_name); + if (app_info_) { + return app_info_; + } + + start = app_id.find("-"); + app_name = app_id.substr(0, start); + app_info_ = get_desktop_app_info(app_name); + } + return app_info_; +} diff --git a/src/util/pipewire/privacy_node_info.cpp b/src/util/pipewire/privacy_node_info.cpp index 739dc528..ec110b86 100644 --- a/src/util/pipewire/privacy_node_info.cpp +++ b/src/util/pipewire/privacy_node_info.cpp @@ -49,6 +49,8 @@ void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info *info) { pipewire_access_portal_app_id = item->value; } else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) { application_icon_name = item->value; + } else if (strcmp(item->key, "stream.monitor") == 0) { + is_monitor = strcmp(item->value, "true") == 0; } } } diff --git a/src/util/portal.cpp b/src/util/portal.cpp index 5874871b..6df2a6b6 100644 --- a/src/util/portal.cpp +++ b/src/util/portal.cpp @@ -17,8 +17,6 @@ static constexpr const char* PORTAL_NAMESPACE = "org.freedesktop.appearance"; static constexpr const char* PORTAL_KEY = "color-scheme"; } // namespace waybar -using namespace Gio; - auto fmt::formatter::format(waybar::Appearance c, format_context& ctx) const { string_view name; switch (c) { @@ -36,8 +34,8 @@ auto fmt::formatter::format(waybar::Appearance c, format_con } waybar::Portal::Portal() - : DBus::Proxy(DBus::Connection::get_sync(DBus::BusType::BUS_TYPE_SESSION), PORTAL_BUS_NAME, - PORTAL_OBJ_PATH, PORTAL_INTERFACE), + : Gio::DBus::Proxy(Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION), + PORTAL_BUS_NAME, PORTAL_OBJ_PATH, PORTAL_INTERFACE), currentMode(Appearance::UNKNOWN) { refreshAppearance(); }; diff --git a/subprojects/spdlog.wrap b/subprojects/spdlog.wrap index af00d5a7..f7a43600 100644 --- a/subprojects/spdlog.wrap +++ b/subprojects/spdlog.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = spdlog-1.14.1 -source_url = https://github.com/gabime/spdlog/archive/refs/tags/v1.14.1.tar.gz -source_filename = spdlog-1.14.1.tar.gz -source_hash = 1586508029a7d0670dfcb2d97575dcdc242d3868a259742b69f100801ab4e16b -patch_filename = spdlog_1.14.1-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.14.1-1/get_patch -patch_hash = ae878e732330ea1048f90d7e117c40c0cd2a6fb8ae5492c7955818ce3aaade6c -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/spdlog_1.14.1-1/spdlog-1.14.1.tar.gz -wrapdb_version = 1.14.1-1 +directory = spdlog-1.15.2 +source_url = https://github.com/gabime/spdlog/archive/refs/tags/v1.15.2.tar.gz +source_filename = spdlog-1.15.2.tar.gz +source_hash = 7a80896357f3e8e920e85e92633b14ba0f229c506e6f978578bdc35ba09e9a5d +patch_filename = spdlog_1.15.2-3_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.15.2-3/get_patch +patch_hash = d5ab078661f571ef5113a8e4bc5c4121e16c044e7772a24b44b1ca8f3ee7c6cb +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/spdlog_1.15.2-3/spdlog-1.15.2.tar.gz +wrapdb_version = 1.15.2-3 [provide] spdlog = spdlog_dep diff --git a/test/config.cpp b/test/config.cpp index c60519ce..638d9663 100644 --- a/test/config.cpp +++ b/test/config.cpp @@ -84,6 +84,33 @@ TEST_CASE("Load simple config with include", "[config]") { } } +TEST_CASE("Load simple config with wildcard include", "[config]") { + waybar::Config conf; + conf.load("test/config/include-wildcard.json"); + + auto& data = conf.getConfig(); + SECTION("validate cpu include file") { REQUIRE(data["cpu"]["format"].asString() == "goo"); } + SECTION("validate memory include file") { REQUIRE(data["memory"]["format"].asString() == "foo"); } +} + +TEST_CASE("Load config using relative paths and wildcards", "[config]") { + waybar::Config conf; + + const char* old_config_path = std::getenv(waybar::Config::CONFIG_PATH_ENV); + setenv(waybar::Config::CONFIG_PATH_ENV, "test/config", 1); + + conf.load("test/config/include-relative-path.json"); + + auto& data = conf.getConfig(); + SECTION("validate cpu include file") { REQUIRE(data["cpu"]["format"].asString() == "goo"); } + SECTION("validate memory include file") { REQUIRE(data["memory"]["format"].asString() == "foo"); } + + if (old_config_path) + setenv(waybar::Config::CONFIG_PATH_ENV, old_config_path, 1); + else + unsetenv(waybar::Config::CONFIG_PATH_ENV); +} + TEST_CASE("Load multiple bar config with include", "[config]") { waybar::Config conf; conf.load("test/config/include-multi.json"); diff --git a/test/config/include-relative-path.json b/test/config/include-relative-path.json new file mode 100644 index 00000000..82214b37 --- /dev/null +++ b/test/config/include-relative-path.json @@ -0,0 +1,5 @@ +{ + "include": ["modules/*.jsonc"], + "position": "top", + "nullOption": null +} diff --git a/test/config/include-wildcard.json b/test/config/include-wildcard.json new file mode 100644 index 00000000..75c9f3a1 --- /dev/null +++ b/test/config/include-wildcard.json @@ -0,0 +1,5 @@ +{ + "include": ["test/config/modules/*.jsonc"], + "position": "top", + "nullOption": null +} diff --git a/test/config/modules/cpu.jsonc b/test/config/modules/cpu.jsonc new file mode 100644 index 00000000..393fd786 --- /dev/null +++ b/test/config/modules/cpu.jsonc @@ -0,0 +1,6 @@ +{ + "cpu": { + "interval": 2, + "format": "goo" + } +} diff --git a/test/config/modules/memory.jsonc b/test/config/modules/memory.jsonc new file mode 100644 index 00000000..6085d43a --- /dev/null +++ b/test/config/modules/memory.jsonc @@ -0,0 +1,6 @@ +{ + "memory": { + "interval": 2, + "format": "foo", + } +} diff --git a/test/utils/date.cpp b/test/utils/date.cpp index d317f98a..576a4799 100644 --- a/test/utils/date.cpp +++ b/test/utils/date.cpp @@ -18,8 +18,10 @@ return #endif +using namespace date; using namespace std::literals::chrono_literals; namespace fmt_lib = waybar::util::date::format; + /* * Check that the date/time formatter with locale and timezone support is working as expected. */ diff --git a/tsan.supp b/tsan.supp new file mode 100644 index 00000000..48a47611 --- /dev/null +++ b/tsan.supp @@ -0,0 +1,7 @@ +# Suppress common thread issues in dependencies, these are often non-fixable or not an issue. +# Use it like this (when in repo root): TSAN_OPTIONS="suppressions=./tsan.supp" ./build/waybar +race:libfontconfig.so +race:libglib-2.0.so +race:libpango-1.0.so +race:libc.so.6 +race:libgio-2.0.so