diff --git a/.clang-tidy b/.clang-tidy index b7a33451d..3d4cf2604 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -15,15 +15,15 @@ Checks: > -readability-redundant-member-init, -readability-redundant-string-init, -readability-identifier-length -CheckOptions: - - { key: readability-identifier-naming.NamespaceCase, value: lower_case } - - { key: readability-identifier-naming.ClassCase, value: CamelCase } - - { key: readability-identifier-naming.StructCase, value: CamelCase } - - { key: readability-identifier-naming.FunctionCase, value: camelBack } - - { key: readability-identifier-naming.VariableCase, value: camelBack } - - { key: readability-identifier-naming.PrivateMemberCase, value: camelBack } - - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } - - { key: readability-identifier-naming.EnumCase, value: CamelCase } - - { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE } - - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } - - { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE } +# CheckOptions: +# - { key: readability-identifier-naming.NamespaceCase, value: lower_case } +# - { key: readability-identifier-naming.ClassCase, value: CamelCase } +# - { key: readability-identifier-naming.StructCase, value: CamelCase } +# - { key: readability-identifier-naming.FunctionCase, value: camelBack } +# - { key: readability-identifier-naming.VariableCase, value: camelBack } +# - { key: readability-identifier-naming.PrivateMemberCase, value: camelBack } +# - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } +# - { key: readability-identifier-naming.EnumCase, value: CamelCase } +# - { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE } +# - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } +# - { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE } diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml.bak similarity index 100% rename from .github/workflows/clang-tidy.yml rename to .github/workflows/clang-tidy.yml.bak diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..d9fc5d3e2 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,32 @@ +name: Build and Push Docker Image + +on: + schedule: + # run every night at midnight + - cron: '0 0 * * *' + +jobs: + build-and-push: + runs-on: ubuntu-latest + strategy: + fail-fast: false # don't fail the other jobs if one of the images fails to build + matrix: + os: [ 'alpine', 'archlinux', 'debian', 'fedora', 'gentoo', 'opensuse' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfiles/${{ matrix.os }} + push: true + tags: alexays/waybar:${{ matrix.os }} diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index f0b8f69c7..7effb4840 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -8,26 +8,29 @@ concurrency: jobs: clang: - # Run actions in a FreeBSD VM on the macos-12 runner + # Run actions in a FreeBSD VM on the ubuntu runner # https://github.com/actions/runner/issues/385 - for FreeBSD runner support - # https://github.com/actions/virtual-environments/issues/4060 - for lack of VirtualBox on MacOS 11 runners - runs-on: macos-12 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Test in FreeBSD VM - uses: cross-platform-actions/action@v0.21.1 + uses: cross-platform-actions/action@v0.23.0 timeout-minutes: 180 + env: + CPPFLAGS: '-isystem/usr/local/include' + LDFLAGS: '-L/usr/local/lib' with: operating_system: freebsd version: "13.2" - environment_variables: CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib + environment_variables: CPPFLAGS LDFLAGS + sync_files: runner-to-vm run: | sudo sed -i '' 's/quarterly/latest/' /etc/pkg/FreeBSD.conf sudo pkg install -y git # subprojects/date sudo pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \ - pkgconf pulseaudio scdoc sndio spdlog wayland-protocols upower \ + pkgconf pipewire pulseaudio scdoc sndio spdlog wayland-protocols upower \ libinotify - meson build -Dman-pages=enabled + meson setup build -Dman-pages=enabled ninja -C build meson test -C build --no-rebuild --print-errorlogs --suite waybar diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index dc6b7edef..c36f68e2d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -9,6 +9,7 @@ concurrency: jobs: build: strategy: + fail-fast: false matrix: distro: - alpine @@ -26,7 +27,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: configure - run: meson -Dman-pages=enabled -Dcpp_std=${{matrix.cpp_std}} build + run: meson setup -Dman-pages=enabled -Dcpp_std=${{matrix.cpp_std}} build - name: build run: ninja -C build - name: test diff --git a/.github/workflows/nix-tests.yml b/.github/workflows/nix-tests.yml new file mode 100644 index 000000000..8859ecb5d --- /dev/null +++ b/.github/workflows/nix-tests.yml @@ -0,0 +1,17 @@ +name: "Nix-Tests" +on: + pull_request: + push: +jobs: + nix-flake-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - run: nix flake show + - run: nix flake check --print-build-logs + - run: nix build --print-build-logs diff --git a/.github/workflows/nix-update-flake-lock.yml b/.github/workflows/nix-update-flake-lock.yml new file mode 100644 index 000000000..2b65c329d --- /dev/null +++ b/.github/workflows/nix-update-flake-lock.yml @@ -0,0 +1,21 @@ +name: update-flake-lock +on: + workflow_dispatch: # allows manual triggering + schedule: + - cron: '0 0 1 * *' # Run monthly + push: + paths: + - 'flake.nix' +jobs: + lockfile: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - name: Update flake.lock + uses: DeterminateSystems/update-flake-lock@v21 diff --git a/.gitignore b/.gitignore index 68bc0dc41..b486237ea 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ packagecache # Nix result result-* + +.ccls-cache diff --git a/Dockerfiles/archlinux b/Dockerfiles/archlinux index 73f190674..d4274a465 100644 --- a/Dockerfiles/archlinux +++ b/Dockerfiles/archlinux @@ -3,5 +3,5 @@ FROM archlinux:base-devel RUN pacman -Syu --noconfirm && \ - pacman -S --noconfirm git meson base-devel libinput wayland wayland-protocols pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient gobject-introspection libxkbcommon playerctl iniparser fftw && \ + pacman -S --noconfirm git meson base-devel libinput wayland wayland-protocols glib2-devel pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient gobject-introspection libxkbcommon playerctl iniparser fftw && \ sed -Ei 's/#(en_(US|GB)\.UTF)/\1/' /etc/locale.gen && locale-gen diff --git a/Dockerfiles/debian b/Dockerfiles/debian index 0745935ec..f479062d6 100644 --- a/Dockerfiles/debian +++ b/Dockerfiles/debian @@ -34,7 +34,7 @@ RUN apt update && \ libudev-dev \ libupower-glib-dev \ libwayland-dev \ - libwireplumber-0.4-dev \ + libwireplumber-0.5-dev \ libxkbcommon-dev \ libxkbregistry-dev \ locales \ diff --git a/Dockerfiles/fedora b/Dockerfiles/fedora index 5892159c7..9dc0337bd 100644 --- a/Dockerfiles/fedora +++ b/Dockerfiles/fedora @@ -29,6 +29,6 @@ RUN dnf install -y @c-development \ 'pkgconfig(wayland-client)' \ 'pkgconfig(wayland-cursor)' \ 'pkgconfig(wayland-protocols)' \ - 'pkgconfig(wireplumber-0.4)' \ + 'pkgconfig(wireplumber-0.5)' \ 'pkgconfig(xkbregistry)' && \ dnf clean all -y diff --git a/Dockerfiles/opensuse b/Dockerfiles/opensuse index bdb42fbfb..6ac3e058a 100644 --- a/Dockerfiles/opensuse +++ b/Dockerfiles/opensuse @@ -6,4 +6,4 @@ RUN zypper -n up && \ zypper addrepo https://download.opensuse.org/repositories/X11:Wayland/openSUSE_Tumbleweed/X11:Wayland.repo | echo 'a' && \ zypper -n refresh && \ zypper -n install -t pattern devel_C_C++ && \ - zypper -n install git meson clang libinput10 libinput-devel pugixml-devel libwayland-client0 libwayland-cursor0 wayland-protocols-devel wayland-devel Mesa-libEGL-devel Mesa-libGLESv2-devel libgbm-devel libxkbcommon-devel libudev-devel libpixman-1-0-devel gtkmm3-devel jsoncpp-devel libxkbregistry-devel scdoc playerctl-devel + zypper -n install git meson clang libinput10 libinput-devel pugixml-devel libwayland-client0 libwayland-cursor0 wayland-protocols-devel wayland-devel Mesa-libEGL-devel Mesa-libGLESv2-devel libgbm-devel libxkbcommon-devel libudev-devel libpixman-1-0-devel gtkmm3-devel jsoncpp-devel libxkbregistry-devel scdoc playerctl-devel python3-packaging diff --git a/Makefile b/Makefile index b1dbfc6e6..3bb11199e 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,11 @@ default: build build: - meson build + meson setup build ninja -C build build-debug: - meson build --buildtype=debug + meson setup build --buildtype=debug ninja -C build install: build diff --git a/README.md b/README.md index 07b11152f..5781ea3a4 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,12 @@ - Sway (Workspaces, Binding mode, Focused window name) - River (Mapping mode, Tags, Focused window name) - Hyprland (Window Icons, Workspaces, Focused window name) -- DWL (Tags) [requires dwl ipc patch](https://github.com/djpohly/dwl/wiki/ipc) +- DWL (Tags, Focused window name) [requires dwl ipc patch](https://github.com/djpohly/dwl/wiki/ipc) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time - Battery - UPower +- Power profiles daemon - Network - Bluetooth - Pulseaudio @@ -36,7 +37,7 @@ Waybar is available from a number of Linux distributions: -[![Packaging status](https://repology.org/badge/vertical-allrepos/waybar.svg)](https://repology.org/project/waybar/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/waybar.svg?columns=3&header=Waybar%20Downstream%20Packaging)](https://repology.org/project/waybar/versions) An Ubuntu PPA with more recent versions is available [here](https://launchpad.net/~nschloe/+archive/ubuntu/waybar). diff --git a/flake.lock b/flake.lock index 25f126441..3f0deffe9 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1704538339, - "narHash": "sha256-1734d3mQuux9ySvwf6axRWZRBhtcZA9Q8eftD6EZg6U=", + "lastModified": 1719506693, + "narHash": "sha256-C8e9S7RzshSdHB7L+v9I51af1gDM5unhJ2xO1ywxNH8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "46ae0210ce163b3cba6c7da08840c1d63de9c701", + "rev": "b2852eb9365c6de48ffb0dc2c9562591f652242a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ebaeb81f1..571c49348 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,12 @@ "x86_64-linux" "aarch64-linux" ] - (system: func (import nixpkgs { inherit system; })); + (system: func (import nixpkgs { + inherit system; + overlays = with self.overlays; [ + waybar + ]; + })); mkDate = longDate: (lib.concatStringsSep "-" [ (builtins.substring 0 4 longDate) @@ -46,22 +51,25 @@ }; }); - overlays.default = final: prev: { - waybar = final.callPackage ./nix/default.nix { - # take the first "version: '...'" from meson.build - version = - (builtins.head (builtins.split "'" - (builtins.elemAt - (builtins.split " version: '" (builtins.readFile ./meson.build)) - 2))) - + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); + overlays = { + default = self.overlays.waybar; + waybar = final: prev: { + waybar = final.callPackage ./nix/default.nix { + waybar = prev.waybar; + # take the first "version: '...'" from meson.build + version = + (builtins.head (builtins.split "'" + (builtins.elemAt + (builtins.split " version: '" (builtins.readFile ./meson.build)) + 2))) + + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); + }; }; }; - packages = genSystems (pkgs: - let packages = self.overlays.default pkgs pkgs; - in packages // { - default = packages.waybar; - }); + packages = genSystems (pkgs: { + default = self.packages.${pkgs.stdenv.hostPlatform.system}.waybar; + inherit (pkgs) waybar; + }); }; } diff --git a/include/ALabel.hpp b/include/ALabel.hpp index 888c65a80..a1aae9da9 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -27,6 +27,10 @@ class ALabel : public AModule { bool handleToggle(GdkEventButton *const &e) override; virtual std::string getState(uint8_t value, bool lesser = false); + + std::map submenus_; + std::map menuActionsMap_; + static void handleGtkMenuEvent(GtkMenuItem *menuitem, gpointer data); }; } // namespace waybar diff --git a/include/AModule.hpp b/include/AModule.hpp index c15efb006..facb3130f 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -13,9 +14,9 @@ class AModule : public IModule { public: static constexpr const char *MODULE_CLASS = "module"; - virtual ~AModule(); + ~AModule() override; auto update() -> void override; - virtual auto refresh(int) -> void{}; + virtual auto refresh(int shouldRefresh) -> void{}; operator Gtk::Widget &() override; auto doAction(const std::string &name) -> void override; @@ -31,19 +32,25 @@ class AModule : public IModule { enum SCROLL_DIR { NONE, UP, DOWN, LEFT, RIGHT }; SCROLL_DIR getScrollDir(GdkEventScroll *e); - bool tooltipEnabled(); + bool tooltipEnabled() const; const std::string name_; const Json::Value &config_; Gtk::EventBox event_box_; + virtual void setCursor(Gdk::CursorType const &c); + virtual bool handleToggle(GdkEventButton *const &ev); + virtual bool handleMouseEnter(GdkEventCrossing *const &ev); + virtual bool handleMouseLeave(GdkEventCrossing *const &ev); virtual bool handleScroll(GdkEventScroll *); virtual bool handleRelease(GdkEventButton *const &ev); + GObject *menu_; private: bool handleUserEvent(GdkEventButton *const &ev); const bool isTooltip; + bool hasUserEvents_; std::vector pid_; gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; diff --git a/include/bar.hpp b/include/bar.hpp index 6dc3c03df..6900da479 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include "AModule.hpp" @@ -41,7 +42,7 @@ struct bar_margins { }; struct bar_mode { - bar_layer layer; + std::optional layer; bool exclusive; bool passthrough; bool visible; diff --git a/include/group.hpp b/include/group.hpp index 67cf43855..564d2eb52 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -11,15 +11,13 @@ namespace waybar { class Group : public AModule { public: - Group(const std::string&, const std::string&, const Json::Value&, bool); + Group(const std::string &, const std::string &, const Json::Value &, bool); virtual ~Group() = default; auto update() -> void override; - operator Gtk::Widget&() override; + operator Gtk::Widget &() override; - virtual Gtk::Box& getBox(); - void addWidget(Gtk::Widget& widget); - - bool handleMouseHover(GdkEventCrossing* const& e); + virtual Gtk::Box &getBox(); + void addWidget(Gtk::Widget &widget); protected: Gtk::Box box; @@ -28,8 +26,8 @@ class Group : public AModule { bool is_first_widget = true; bool is_drawer = false; std::string add_class_to_drawer_children; - - void addHoverHandlerTo(Gtk::Widget& widget); + bool handleMouseEnter(GdkEventCrossing *const &ev) override; + bool handleMouseLeave(GdkEventCrossing *const &ev) override; }; } // namespace waybar diff --git a/include/modules/battery.hpp b/include/modules/battery.hpp index 7955e598f..8e1a2ad2b 100644 --- a/include/modules/battery.hpp +++ b/include/modules/battery.hpp @@ -32,7 +32,7 @@ class Battery : public ALabel { void refreshBatteries(); void worker(); const std::string getAdapterStatus(uint8_t capacity) const; - const std::tuple getInfos(); + std::tuple getInfos(); const std::string formatTimeRemaining(float hoursRemaining); void setBarClass(std::string&); diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index 89658dcf9..b89383a04 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -49,6 +49,9 @@ class Bluetooth : public ALabel { auto update() -> void override; private: + static auto onObjectAdded(GDBusObjectManager*, GDBusObject*, gpointer) -> void; + static auto onObjectRemoved(GDBusObjectManager*, GDBusObject*, gpointer) -> void; + static auto onInterfaceAddedOrRemoved(GDBusObjectManager*, GDBusObject*, GDBusInterface*, gpointer) -> void; static auto onInterfaceProxyPropertiesChanged(GDBusObjectManagerClient*, GDBusObjectProxy*, diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index 8b597c4e6..c212ec8b9 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -21,10 +21,12 @@ class Clock final : public ALabel { auto doAction(const std::string&) -> void override; private: - const std::locale locale_; + const std::locale m_locale_; // tooltip - const std::string tlpFmt_; - std::string tlpText_{""}; // tooltip text to print + const std::string m_tlpFmt_; + std::string m_tlpText_{""}; // tooltip text to print + const Glib::RefPtr m_tooltip_; // tooltip as a separate Gtk::Label + bool query_tlp_cb(int, int, bool, const Glib::RefPtr& tooltip); // Calendar const bool cldInTooltip_; // calendar in tooltip /* @@ -41,6 +43,7 @@ class Clock final : public ALabel { 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 @@ -51,6 +54,9 @@ class Clock final : public ALabel { auto get_calendar(const year_month_day& today, const year_month_day& ymd, const time_zone* tz) -> const std::string; + // get local time zone + auto local_zone() -> const time_zone*; + // time zoned time in tooltip const bool tzInTooltip_; // if need to print time zones text std::vector tzList_; // time zones list @@ -69,6 +75,7 @@ class Clock final : public ALabel { void cldModeSwitch(); void cldShift_up(); void cldShift_down(); + void cldShift_reset(); void tz_up(); void tz_down(); // Module Action Map @@ -76,6 +83,7 @@ class Clock final : public ALabel { {"mode", &waybar::modules::Clock::cldModeSwitch}, {"shift_up", &waybar::modules::Clock::cldShift_up}, {"shift_down", &waybar::modules::Clock::cldShift_down}, + {"shift_reset", &waybar::modules::Clock::cldShift_reset}, {"tz_up", &waybar::modules::Clock::tz_up}, {"tz_down", &waybar::modules::Clock::tz_down}}; }; diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index 2c7ba8a80..6c17c6e45 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -35,6 +35,7 @@ class Custom : public ALabel { std::string id_; std::string alt_; std::string tooltip_; + const bool tooltip_format_enabled_; std::vector class_; int percentage_; FILE* fp_; diff --git a/include/modules/dwl/window.hpp b/include/modules/dwl/window.hpp new file mode 100644 index 000000000..435863999 --- /dev/null +++ b/include/modules/dwl/window.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "dwl-ipc-unstable-v2-client-protocol.h" +#include "util/json.hpp" + +namespace waybar::modules::dwl { + +class Window : public AAppIconLabel, public sigc::trackable { + public: + Window(const std::string &, const waybar::Bar &, const Json::Value &); + ~Window(); + + void handle_layout(const uint32_t layout); + void handle_title(const char *title); + void handle_appid(const char *ppid); + void handle_layout_symbol(const char *layout_symbol); + void handle_frame(); + + struct zdwl_ipc_manager_v2 *status_manager_; + + private: + const Bar &bar_; + + std::string title_; + std::string appid_; + std::string layout_symbol_; + uint32_t layout_; + + struct zdwl_ipc_output_v2 *output_status_; +}; + +} // namespace waybar::modules::dwl diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index d197df3aa..11e73d8f6 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -20,19 +21,23 @@ class IPC { public: IPC() { startIPC(); } - void registerForIPC(const std::string&, EventHandler*); - void unregisterForIPC(EventHandler*); + void registerForIPC(const std::string& ev, EventHandler* ev_handler); + void unregisterForIPC(EventHandler* handler); static std::string getSocket1Reply(const std::string& rq); Json::Value getSocket1JsonReply(const std::string& rq); + static std::filesystem::path getSocketFolder(const char* instanceSig); + + protected: + static std::filesystem::path socketFolder_; private: void startIPC(); void parseIPC(const std::string&); - std::mutex m_callbackMutex; - util::JsonParser m_parser; - std::list> m_callbacks; + std::mutex callbackMutex_; + util::JsonParser parser_; + std::list> callbacks_; }; inline std::unique_ptr gIPC; diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index eb220609a..47a4d69c3 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -30,7 +30,7 @@ class Language : public waybar::ALabel, public EventHandler { std::string short_description; }; - auto getLayout(const std::string&) -> Layout; + static auto getLayout(const std::string&) -> Layout; std::mutex mutex_; const Bar& bar_; diff --git a/include/modules/hyprland/submap.hpp b/include/modules/hyprland/submap.hpp index 4ff232fff..ce980f36f 100644 --- a/include/modules/hyprland/submap.hpp +++ b/include/modules/hyprland/submap.hpp @@ -14,17 +14,20 @@ namespace waybar::modules::hyprland { class Submap : public waybar::ALabel, public EventHandler { public: Submap(const std::string&, const waybar::Bar&, const Json::Value&); - virtual ~Submap(); + ~Submap() override; auto update() -> void override; private: - void onEvent(const std::string&) override; + auto parseConfig(const Json::Value&) -> void; + void onEvent(const std::string& ev) override; std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; std::string submap_; + bool always_on_ = false; + std::string default_submap_ = "Default"; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index 4cc16ffb0..f2c266bd2 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -25,7 +25,7 @@ class Window : public waybar::AAppIconLabel, public EventHandler { std::string last_window; std::string last_window_title; - static auto parse(const Json::Value&) -> Workspace; + static auto parse(const Json::Value& value) -> Workspace; }; struct WindowData { @@ -41,23 +41,25 @@ class Window : public waybar::AAppIconLabel, public EventHandler { static auto parse(const Json::Value&) -> WindowData; }; - auto getActiveWorkspace(const std::string&) -> Workspace; - void onEvent(const std::string&) override; + static auto getActiveWorkspace(const std::string&) -> Workspace; + static auto getActiveWorkspace() -> Workspace; + void onEvent(const std::string& ev) override; void queryActiveWorkspace(); void setClass(const std::string&, bool enable); - bool separate_outputs; + bool separateOutputs_; std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; - WindowData window_data_; + WindowData windowData_; Workspace workspace_; - std::string solo_class_; - std::string last_solo_class_; + std::string soloClass_; + std::string lastSoloClass_; bool solo_; - bool all_floating_; + bool allFloating_; bool swallowing_; bool fullscreen_; + bool focused_; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/windowcreationpayload.hpp b/include/modules/hyprland/windowcreationpayload.hpp new file mode 100644 index 000000000..45229ed42 --- /dev/null +++ b/include/modules/hyprland/windowcreationpayload.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "util/enum.hpp" +#include "util/regex_collection.hpp" + +using WindowAddress = std::string; + +namespace waybar::modules::hyprland { + +class Workspaces; + +class WindowCreationPayload { + public: + WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + std::string window_repr); + WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + std::string window_class, std::string window_title); + 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); + + std::string getWorkspaceName() const { return m_workspaceName; } + WindowAddress getAddress() const { return m_windowAddress; } + + void moveToWorksace(std::string& new_workspace_name); + + private: + void clearAddr(); + void clearWorkspaceName(); + + using Repr = std::string; + using ClassAndTitle = std::pair; + std::variant m_window; + + WindowAddress m_windowAddress; + std::string m_workspaceName; + + int m_timeSpentUncreated = 0; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp new file mode 100644 index 000000000..f1fea4e8c --- /dev/null +++ b/include/modules/hyprland/workspace.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "modules/hyprland/windowcreationpayload.hpp" +#include "util/enum.hpp" +#include "util/regex_collection.hpp" + +using WindowAddress = std::string; + +namespace waybar::modules::hyprland { + +class Workspaces; +class Workspace { + public: + explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, + const Json::Value& clients_data = Json::Value::nullRef); + std::string& selectIcon(std::map& icons_map); + Gtk::Button& button() { return m_button; }; + + int id() const { return m_id; }; + std::string name() const { return m_name; }; + std::string output() const { return m_output; }; + bool isActive() const { return m_isActive; }; + bool isSpecial() const { return m_isSpecial; }; + bool isPersistent() const { return m_isPersistentRule || m_isPersistentConfig; }; + 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; + void setActive(bool value = true) { m_isActive = value; }; + void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; + void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; + void setUrgent(bool value = true) { m_isUrgent = value; }; + void setVisible(bool value = true) { m_isVisible = value; }; + 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); + void initializeWindowMap(const Json::Value& clients_data); + + bool onWindowOpened(WindowCreationPayload const& create_window_paylod); + std::optional closeWindow(WindowAddress const& addr); + + void update(const std::string& format, const std::string& icon); + + private: + Workspaces& m_workspaceManager; + + int m_id; + std::string m_name; + std::string m_output; + uint m_windows; + bool m_isActive = false; + bool m_isSpecial = false; + bool m_isPersistentRule = false; // represents the persistent state in hyprland + bool m_isPersistentConfig = false; // represents the persistent state in the Waybar config + bool m_isUrgent = false; + bool m_isVisible = false; + + std::map m_windowMap; + + Gtk::Button m_button; + Gtk::Box m_content; + Gtk::Label m_label; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 8d46b1a11..f5c20f69c 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -4,19 +4,18 @@ #include #include -#include #include #include #include -#include #include #include -#include #include #include "AModule.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" +#include "modules/hyprland/windowcreationpayload.hpp" +#include "modules/hyprland/workspace.hpp" #include "util/enum.hpp" #include "util/regex_collection.hpp" @@ -26,92 +25,6 @@ namespace waybar::modules::hyprland { class Workspaces; -class WindowCreationPayload { - public: - WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_repr); - WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_class, std::string window_title); - 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); - - std::string getWorkspaceName() const { return m_workspaceName; } - WindowAddress getAddress() const { return m_windowAddress; } - - void moveToWorksace(std::string& new_workspace_name); - - private: - void clearAddr(); - void clearWorkspaceName(); - - using Repr = std::string; - using ClassAndTitle = std::pair; - std::variant m_window; - - WindowAddress m_windowAddress; - std::string m_workspaceName; - - int m_timeSpentUncreated = 0; -}; - -class Workspace { - public: - explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, - const Json::Value& clients_data = Json::Value::nullRef); - std::string& selectIcon(std::map& icons_map); - Gtk::Button& button() { return m_button; }; - - int id() const { return m_id; }; - std::string name() const { return m_name; }; - std::string output() const { return m_output; }; - bool isActive() const { return m_isActive; }; - bool isSpecial() const { return m_isSpecial; }; - bool isPersistent() const { return m_isPersistent; }; - bool isVisible() const { return m_isVisible; }; - bool isEmpty() const { return m_windows == 0; }; - bool isUrgent() const { return m_isUrgent; }; - - bool handleClicked(GdkEventButton* bt) const; - void setActive(bool value = true) { m_isActive = value; }; - void setPersistent(bool value = true) { m_isPersistent = value; }; - void setUrgent(bool value = true) { m_isUrgent = value; }; - void setVisible(bool value = true) { m_isVisible = value; }; - void setWindows(uint value) { m_windows = value; }; - void setName(std::string const& value) { m_name = value; }; - bool containsWindow(WindowAddress const& addr) const { return m_windowMap.contains(addr); } - void insertWindow(WindowCreationPayload create_window_paylod); - std::string removeWindow(WindowAddress const& addr); - void initializeWindowMap(const Json::Value& clients_data); - - bool onWindowOpened(WindowCreationPayload const& create_window_paylod); - std::optional closeWindow(WindowAddress const& addr); - - void update(const std::string& format, const std::string& icon); - - private: - Workspaces& m_workspaceManager; - - int m_id; - std::string m_name; - std::string m_output; - uint m_windows; - bool m_isActive = false; - bool m_isSpecial = false; - bool m_isPersistent = false; - bool m_isUrgent = false; - bool m_isVisible = false; - - std::map m_windowMap; - - Gtk::Button m_button; - Gtk::Box m_content; - Gtk::Label m_label; -}; - class Workspaces : public AModule, public EventHandler { public: Workspaces(const std::string&, const waybar::Bar&, const Json::Value&); @@ -122,6 +35,8 @@ class Workspaces : public AModule, public EventHandler { auto allOutputs() const -> bool { return m_allOutputs; } auto showSpecial() const -> bool { return m_showSpecial; } auto activeOnly() const -> bool { return m_activeOnly; } + auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } + auto moveToMonitor() const -> bool { return m_moveToMonitor; } auto getBarOutput() const -> std::string { return m_bar.output->name; } @@ -135,11 +50,24 @@ class Workspaces : public AModule, public EventHandler { void onEvent(const std::string& e) override; void updateWindowCount(); void sortWorkspaces(); - void createWorkspace(Json::Value const& workspaceData, - Json::Value const& clientsData = Json::Value::nullRef); + void createWorkspace(Json::Value const& workspace_data, + Json::Value const& clients_data = Json::Value::nullRef); + + static Json::Value createMonitorWorkspaceData(std::string const& name, + std::string const& monitor); void removeWorkspace(std::string const& name); void setUrgentWorkspace(std::string const& windowaddress); + + // Config void parseConfig(const Json::Value& config); + auto populateIconsMap(const Json::Value& formatIcons) -> void; + static auto populateBoolConfig(const Json::Value& config, const std::string& key, bool& member) + -> void; + auto populateSortByConfig(const Json::Value& config) -> void; + auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; + auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; + auto populateWindowRewriteConfig(const Json::Value& config) -> void; + void registerIpc(); // workspace events @@ -165,7 +93,13 @@ class Workspaces : public AModule, public EventHandler { int windowRewritePriorityFunction(std::string const& window_rule); + // Update methods void doUpdate(); + void removeWorkspacesToRemove(); + void createWorkspacesToCreate(); + static std::vector getVisibleWorkspaces(); + void updateWorkspaceStates(); + bool updateWindowsToCreate(); void extendOrphans(int workspaceId, Json::Value const& clientsJson); void registerOrphanWindow(WindowCreationPayload create_window_payload); @@ -178,6 +112,8 @@ class Workspaces : public AModule, public EventHandler { bool m_allOutputs = false; bool m_showSpecial = false; bool m_activeOnly = false; + bool m_specialVisibleOnly = false; + bool m_moveToMonitor = false; Json::Value m_persistentWorkspaceConfig; // Map for windows stored in workspaces not present in the current bar. diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 1276e3c3d..2c9071b4e 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -148,6 +148,7 @@ class Stopped : public State { class Disconnected : public State { Context* const ctx_; sigc::connection timer_connection_; + int last_interval_; public: Disconnected(Context* const ctx) : ctx_{ctx} {} @@ -162,7 +163,7 @@ class Disconnected : public State { Disconnected(Disconnected const&) = delete; Disconnected& operator=(Disconnected const&) = delete; - void arm_timer(int interval) noexcept; + bool arm_timer(int interval) noexcept; void disarm_timer() noexcept; bool on_timer(); diff --git a/include/modules/power_profiles_daemon.hpp b/include/modules/power_profiles_daemon.hpp new file mode 100644 index 000000000..a2bd38587 --- /dev/null +++ b/include/modules/power_profiles_daemon.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "ALabel.hpp" +#include "giomm/dbusproxy.h" + +namespace waybar::modules { + +struct Profile { + std::string name; + std::string driver; + + Profile(std::string n, std::string d) : name(std::move(n)), driver(std::move(d)) {} +}; + +class PowerProfilesDaemon : public ALabel { + public: + PowerProfilesDaemon(const std::string &, const Json::Value &); + auto update() -> void override; + void profileChangedCb(const Gio::DBus::Proxy::MapChangedProperties &, + const std::vector &); + void busConnectedCb(Glib::RefPtr &r); + void getAllPropsCb(Glib::RefPtr &r); + void setPropCb(Glib::RefPtr &r); + void populateInitState(); + bool handleToggle(GdkEventButton *const &e) override; + + private: + // True if we're connected to the dbug interface. False if we're + // not. + bool connected_; + // Look for a profile name in the list of available profiles and + // switch activeProfile_ to it. + void switchToProfile(std::string const &); + // Used to toggle/display the profiles + std::vector availableProfiles_; + // Points to the active profile in the profiles list + std::vector::iterator activeProfile_; + // Current CSS class applied to the label + std::string currentStyle_; + // Format string + std::string tooltipFormat_; + // DBus Proxy used to track the current active profile + Glib::RefPtr powerProfilesProxy_; +}; + +} // namespace waybar::modules diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index b8e767686..d7656d312 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -1,10 +1,7 @@ #pragma once -#include -#include #include -#include "ALabel.hpp" #include "gtkmm/box.h" #include "modules/privacy/privacy_item.hpp" #include "util/pipewire/pipewire_backend.hpp" diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp index a0e3038b5..836bd994c 100644 --- a/include/modules/privacy/privacy_item.hpp +++ b/include/modules/privacy/privacy_item.hpp @@ -2,9 +2,6 @@ #include -#include -#include -#include #include #include "gtkmm/box.h" diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 1043157cd..ebc08d45f 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -76,6 +76,8 @@ class Item : public sigc::trackable { void makeMenu(); bool handleClick(GdkEventButton* const& /*ev*/); bool handleScroll(GdkEventScroll* const&); + bool handleMouseEnter(GdkEventCrossing* const&); + bool handleMouseLeave(GdkEventCrossing* const&); // smooth scrolling threshold gdouble scroll_threshold_ = 0; diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index 3e9519f54..ea58c4f09 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -56,6 +56,7 @@ class Language : public ALabel, public sigc::trackable { Layout layout_; std::string tooltip_format_ = ""; std::map layouts_map_; + bool hide_single_; bool is_variant_displayed; std::byte displayed_short_flag = static_cast(DispayedShortFlag::None); diff --git a/include/modules/upower.hpp b/include/modules/upower.hpp new file mode 100644 index 000000000..60a276dbf --- /dev/null +++ b/include/modules/upower.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include + +#include + +#include "AIconLabel.hpp" + +namespace waybar::modules { + +class UPower final : public AIconLabel { + public: + UPower(const std::string &, const Json::Value &); + virtual ~UPower(); + auto update() -> void override; + + private: + const std::string NO_BATTERY{"battery-missing-symbolic"}; + + // Config + bool showIcon_{true}; + bool hideIfEmpty_{true}; + int iconSize_{20}; + int tooltip_spacing_{4}; + int tooltip_padding_{4}; + Gtk::Box contentBox_; // tooltip box + std::string tooltipFormat_; + + // UPower device info + struct upDevice_output { + UpDevice *upDevice{NULL}; + double percentage{0.0}; + double temperature{0.0}; + guint64 time_full{0u}; + guint64 time_empty{0u}; + gchar *icon_name{(char *)'\0'}; + bool upDeviceValid{false}; + UpDeviceState state; + UpDeviceKind kind; + char *nativePath{(char *)'\0'}; + char *model{(char *)'\0'}; + }; + + // Technical variables + std::string nativePath_; + std::string model_; + std::string lastStatus_; + Glib::ustring label_markup_; + std::mutex mutex_; + Glib::RefPtr gtkTheme_; + bool sleeping_; + + // Technical functions + void addDevice(UpDevice *); + void removeDevice(const gchar *); + void removeDevices(); + void resetDevices(); + void setDisplayDevice(); + const Glib::ustring getText(const upDevice_output &upDevice_, const std::string &format); + bool queryTooltipCb(int, int, bool, const Glib::RefPtr &); + + // DBUS variables + guint watcherID_; + Glib::RefPtr conn_; + guint subscrID_{0u}; + + // UPower variables + UpClient *upClient_; + upDevice_output upDevice_; // Device to display + typedef std::unordered_map Devices; + Devices devices_; + bool upRunning_{true}; + + // DBus callbacks + void getConn_cb(Glib::RefPtr &result); + void onAppear(const Glib::RefPtr &, const Glib::ustring &, + const Glib::ustring &); + void onVanished(const Glib::RefPtr &, const Glib::ustring &); + void prepareForSleep_cb(const Glib::RefPtr &connection, + const Glib::ustring &sender_name, const Glib::ustring &object_path, + const Glib::ustring &interface_name, const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters); + + // UPower callbacks + static void deviceAdded_cb(UpClient *client, UpDevice *device, gpointer data); + static void deviceRemoved_cb(UpClient *client, const gchar *objectPath, gpointer data); + static void deviceNotify_cb(UpDevice *device, GParamSpec *pspec, gpointer user_data); + // UPower secondary functions + void getUpDeviceInfo(upDevice_output &upDevice_); +}; + +} // namespace waybar::modules diff --git a/include/modules/upower/upower.hpp b/include/modules/upower/upower.hpp deleted file mode 100644 index d763259b6..000000000 --- a/include/modules/upower/upower.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include - -#include "ALabel.hpp" -#include "glibconfig.h" -#include "gtkmm/box.h" -#include "gtkmm/image.h" -#include "gtkmm/label.h" -#include "modules/upower/upower_tooltip.hpp" - -namespace waybar::modules::upower { - -class UPower : public AModule { - public: - UPower(const std::string &, const Json::Value &); - virtual ~UPower(); - auto update() -> void override; - - private: - typedef std::unordered_map Devices; - - const std::string DEFAULT_FORMAT = "{percentage}"; - const std::string DEFAULT_FORMAT_ALT = "{percentage} {time}"; - - static void deviceAdded_cb(UpClient *client, UpDevice *device, gpointer data); - static void deviceRemoved_cb(UpClient *client, const gchar *objectPath, gpointer data); - static void deviceNotify_cb(UpDevice *device, GParamSpec *pspec, gpointer user_data); - static void prepareForSleep_cb(GDBusConnection *system_bus, const gchar *sender_name, - const gchar *object_path, const gchar *interface_name, - const gchar *signal_name, GVariant *parameters, - gpointer user_data); - static void upowerAppear(GDBusConnection *conn, const gchar *name, const gchar *name_owner, - gpointer data); - static void upowerDisappear(GDBusConnection *connection, const gchar *name, gpointer user_data); - - void removeDevice(const gchar *objectPath); - void addDevice(UpDevice *device); - void setDisplayDevice(); - void resetDevices(); - void removeDevices(); - bool show_tooltip_callback(int, int, bool, const Glib::RefPtr &tooltip); - bool handleToggle(GdkEventButton *const &) override; - std::string timeToString(gint64 time); - - const std::string getDeviceStatus(UpDeviceState &state); - - Gtk::Box box_; - Gtk::Image icon_; - Gtk::Label label_; - - // Config - bool hideIfEmpty = true; - bool tooltip_enabled = true; - uint tooltip_spacing = 4; - uint tooltip_padding = 4; - uint iconSize = 20; - std::string format = DEFAULT_FORMAT; - std::string format_alt = DEFAULT_FORMAT_ALT; - - Devices devices; - std::mutex m_Mutex; - UpClient *client; - UpDevice *displayDevice = nullptr; - guint login1_id; - GDBusConnection *login1_connection; - std::unique_ptr upower_tooltip; - std::string lastStatus; - bool showAltText; - bool showIcon = true; - bool upowerRunning; - guint upowerWatcher_id; - std::string nativePath_; -}; - -} // namespace waybar::modules::upower diff --git a/include/modules/upower/upower_tooltip.hpp b/include/modules/upower/upower_tooltip.hpp deleted file mode 100644 index bc99abede..000000000 --- a/include/modules/upower/upower_tooltip.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include - -#include -#include - -#include "gtkmm/box.h" -#include "gtkmm/label.h" -#include "gtkmm/window.h" - -namespace waybar::modules::upower { - -class UPowerTooltip : public Gtk::Window { - private: - typedef std::unordered_map Devices; - - const std::string getDeviceIcon(UpDeviceKind& kind); - - std::unique_ptr contentBox; - - uint iconSize; - uint tooltipSpacing; - uint tooltipPadding; - - public: - UPowerTooltip(uint iconSize, uint tooltipSpacing, uint tooltipPadding); - virtual ~UPowerTooltip(); - - uint updateTooltip(Devices& devices); -}; - -} // namespace waybar::modules::upower diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index 9bbf4d464..6255b95fd 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -17,12 +17,15 @@ class Wireplumber : public ALabel { auto update() -> void override; private: - void loadRequiredApiModules(); + void asyncLoadRequiredApiModules(); void prepare(); void activatePlugins(); static void updateVolume(waybar::modules::Wireplumber* self, uint32_t id); static void updateNodeName(waybar::modules::Wireplumber* self, uint32_t id); static void onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); + static void onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, + waybar::modules::Wireplumber* self); + static void onMixerApiLoaded(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); static void onObjectManagerInstalled(waybar::modules::Wireplumber* self); static void onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id); static void onDefaultNodesApiChanged(waybar::modules::Wireplumber* self); diff --git a/include/util/backlight_backend.hpp b/include/util/backlight_backend.hpp index 8dcb8958f..eb42d3ccf 100644 --- a/include/util/backlight_backend.hpp +++ b/include/util/backlight_backend.hpp @@ -20,7 +20,7 @@ std::scoped_lock lock((backend).udev_thread_mutex_); \ __devices = (backend).devices_; \ } \ - auto varname = (backend).best_device(__devices.cbegin(), __devices.cend(), preferred_device); + auto varname = (backend).best_device(__devices, preferred_device); namespace waybar::util { @@ -56,27 +56,21 @@ class BacklightBackend { void set_previous_best_device(const BacklightDevice *device); - void set_brightness(std::string preferred_device, ChangeType change_type, double step); + void set_brightness(const std::string &preferred_device, ChangeType change_type, double step); - void set_scaled_brightness(std::string preferred_device, int brightness); - int get_scaled_brightness(std::string preferred_device); - - template - static void upsert_device(ForwardIt first, ForwardIt last, Inserter inserter, udev_device *dev); - - template - static void enumerate_devices(ForwardIt first, ForwardIt last, Inserter inserter, udev *udev); + void set_scaled_brightness(const std::string &preferred_device, int brightness); + int get_scaled_brightness(const std::string &preferred_device); bool is_login_proxy_initialized() const { return static_cast(login_proxy_); } - template - static const BacklightDevice *best_device(ForwardIt first, ForwardIt last, std::string_view); + static const BacklightDevice *best_device(const std::vector &devices, + std::string_view); std::vector devices_; std::mutex udev_thread_mutex_; private: - void set_brightness_internal(std::string device_name, int brightness, int max_brightness); + void set_brightness_internal(const std::string &device_name, int brightness, int max_brightness); std::function on_updated_cb_; std::chrono::milliseconds polling_interval_; @@ -90,4 +84,4 @@ class BacklightBackend { static constexpr int EPOLL_MAX_EVENTS = 16; }; -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util diff --git a/include/util/pipewire/pipewire_backend.hpp b/include/util/pipewire/pipewire_backend.hpp index 4e23b2825..90fb2bb22 100644 --- a/include/util/pipewire/pipewire_backend.hpp +++ b/include/util/pipewire/pipewire_backend.hpp @@ -2,6 +2,8 @@ #include +#include + #include "util/backend_common.hpp" #include "util/pipewire/privacy_node_info.hpp" @@ -13,7 +15,8 @@ class PipewireBackend { pw_context* context_; pw_core* core_; - spa_hook registry_listener; + pw_registry* registry_; + spa_hook registryListener_; /* Hack to keep constructor inaccessible but still public. * This is required to be able to use std::make_shared. @@ -21,20 +24,22 @@ class PipewireBackend { * pointer because the destructor will manually free memory, and this could be * a problem with C++20's copy and move semantics. */ - struct private_constructor_tag {}; + struct PrivateConstructorTag {}; public: - std::mutex mutex_; - - pw_registry* registry; - sigc::signal privacy_nodes_changed_signal_event; std::unordered_map privacy_nodes; + std::mutex mutex_; static std::shared_ptr getInstance(); - PipewireBackend(private_constructor_tag tag); + // Handlers for PipeWire events + void handleRegistryEventGlobal(uint32_t id, uint32_t permissions, const char* type, + uint32_t version, const struct spa_dict* props); + void handleRegistryEventGlobalRemove(uint32_t id); + + PipewireBackend(PrivateConstructorTag tag); ~PipewireBackend(); }; } // namespace waybar::util::PipewireBackend diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp index 3b7f446d3..7b8df0181 100644 --- a/include/util/pipewire/privacy_node_info.hpp +++ b/include/util/pipewire/privacy_node_info.hpp @@ -34,29 +34,12 @@ class PrivacyNodeInfo { void *data; - std::string get_name() { - const std::vector names{&application_name, &node_name}; - std::string name = "Unknown Application"; - for (auto &name_ : names) { - if (name_ != nullptr && name_->length() > 0) { - name = *name_; - name[0] = toupper(name[0]); - break; - } - } - return name; - } - - std::string get_icon_name() { - const std::vector names{&application_icon_name, &pipewire_access_portal_app_id, - &application_name, &node_name}; - const std::string name = "application-x-executable-symbolic"; - for (auto &name_ : names) { - if (name_ != nullptr && name_->length() > 0 && DefaultGtkIconThemeWrapper::has_icon(*name_)) { - return *name_; - } - } - return name; - } + std::string getName(); + std::string getIconName(); + + // Handlers for PipeWire events + void handleProxyEventDestroy(); + void handleNodeEventInfo(const struct pw_node_info *info); }; + } // namespace waybar::util::PipewireBackend diff --git a/include/util/regex_collection.hpp b/include/util/regex_collection.hpp index 5ea2882e0..30d26d4a8 100644 --- a/include/util/regex_collection.hpp +++ b/include/util/regex_collection.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace waybar::util { @@ -17,7 +18,7 @@ struct Rule { // See https://en.cppreference.com/w/cpp/compiler_support/20 "Parenthesized initialization of // aggregates" Rule(std::regex rule, std::string repr, int priority) - : rule(rule), repr(repr), priority(priority) {} + : rule(std::move(rule)), repr(std::move(repr)), priority(priority) {} }; int default_priority_function(std::string& key); @@ -36,16 +37,17 @@ class RegexCollection { std::map regex_cache; std::string default_repr; - std::string& find_match(std::string& value, bool& matched_any); + std::string find_match(std::string& value, bool& matched_any); public: RegexCollection() = default; - RegexCollection(const Json::Value& map, std::string default_repr = "", - std::function priority_function = default_priority_function); + RegexCollection( + const Json::Value& map, std::string default_repr = "", + const std::function& priority_function = default_priority_function); ~RegexCollection() = default; std::string& get(std::string& value, bool& matched_any); std::string& get(std::string& value); }; -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util diff --git a/man/waybar-backlight.5.scd b/man/waybar-backlight.5.scd index 7db18a202..1f674fc00 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -30,7 +30,11 @@ The *backlight* module displays the current backlight level. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *rotate*: ++ typeof: integer ++ @@ -77,6 +81,19 @@ The *backlight* module displays the current backlight level. default: 1.0 ++ The speed at which to change the brightness when scrolling. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # EXAMPLE: ``` diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 52a6a2d1b..4fe9650a7 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -61,7 +61,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *rotate*: ++ typeof: integer++ @@ -105,6 +109,19 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: false ++ Option to enable battery compatibility if not detected. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{capacity}*: Capacity in percentage @@ -115,6 +132,10 @@ The *battery* module displays the current capacity and state (eg. charging) of y *{time}*: Estimate of time until full or empty. Note that this is based on the power draw at the last refresh time, not an average. +*{cycles}*: Amount of charge cycles the highest-capacity battery has seen. *(Linux only)* + +*{health}*: The percentage of the highest-capacity battery's original maximum charge it can still hold. + # TIME FORMAT The *battery* module allows you to define how time should be formatted via *format-time*. diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index 1fdd984ba..1783dab32 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -66,7 +66,11 @@ Addressed by *bluetooth* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -125,6 +129,19 @@ Addressed by *bluetooth* typeof: string ++ This format is used to define how each connected device should be displayed within the *device_enumerate* format replacement in the tooltip menu. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{status}*: Status of the bluetooth device. diff --git a/man/waybar-cava.5.scd b/man/waybar-cava.5.scd index cf75441b3..2a7e8f678 100644 --- a/man/waybar-cava.5.scd +++ b/man/waybar-cava.5.scd @@ -120,6 +120,18 @@ libcava lives in: :[ string :[ /dev/stdout :[ It's impossible to set it. Waybar sets it to = /dev/stdout for internal needs +|[ *menu* +:[ string +:[ +:[ Action that popups the menu. +|[ *menu-file* +:[ string +:[ +:[ Location of the menu descriptor file. There need to be an element of type GtkMenu with id *menu* +|[ *menu-actions* +:[ array +:[ +:[ The actions corresponding to the buttons of the menu. Configuration can be provided as: - The only cava configuration file which is provided through *cava_config*. The rest configuration can be skipped diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index e8ef7bed9..40aedd152 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -84,6 +84,18 @@ $XDG_CONFIG_HOME/waybar/config ++ :[ string :[ same as format :[ Tooltip on hover +|[ *menu* +:[ string +:[ +:[ Action that popups the menu. +|[ *menu-file* +:[ string +:[ +:[ Location of the menu descriptor file. There need to be an element of type GtkMenu with id *menu* +|[ *menu-actions* +:[ array +:[ +:[ The actions corresponding to the buttons of the menu. View all valid format options in *strftime(3)* or have a look https://en.cppreference.com/w/cpp/chrono/duration/formatter diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index 484795689..fcbd12653 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -35,7 +35,11 @@ The *cpu* module displays the current CPU utilization. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *rotate*: ++ typeof: integer ++ diff --git a/man/waybar-custom.5.scd b/man/waybar-custom.5.scd index 4c2190311..4cf3c33dc 100644 --- a/man/waybar-custom.5.scd +++ b/man/waybar-custom.5.scd @@ -21,6 +21,10 @@ Addressed by *custom/* The path to a script, which determines if the script in *exec* should be executed. ++ *exec* will be executed if the exit code of *exec-if* equals 0. +*hide-empty-text*: ++ + typeof: bool ++ + Disables the module when output is empty, but format might contain additional static content. + *exec-on-event*: ++ typeof: bool ++ default: true ++ @@ -72,7 +76,11 @@ Addressed by *custom/* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -117,6 +125,19 @@ Addressed by *custom/* default: false ++ Option to enable escaping of script output. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # RETURN-TYPE When *return-type* is set to *json*, Waybar expects the *exec*-script to output its data in JSON format. diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index d466bddfc..df9ca4e5a 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -45,7 +45,11 @@ Addressed by *disk* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -89,6 +93,19 @@ Addressed by *disk* typeof: string ++ Use with specific_free, specific_used, and specific_total to force calculation to always be in a certain unit. Accepts kB, kiB, MB, Mib, GB, GiB, TB, TiB. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{percentage_used}*: Percentage of disk in use. diff --git a/man/waybar-dwl-window.5.scd b/man/waybar-dwl-window.5.scd new file mode 100644 index 000000000..c2f5b93eb --- /dev/null +++ b/man/waybar-dwl-window.5.scd @@ -0,0 +1,118 @@ +waybar-dwl-window(5) + +# NAME + +waybar - dwl window module + +# DESCRIPTION + +The *window* module displays the title of the currently focused window in DWL + +# CONFIGURATION + +Addressed by *dwl/window* + +*format*: ++ + typeof: string ++ + default: {title} ++ + The format, how information should be displayed. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should accept. + +*align*: ++ + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*rewrite*: ++ + typeof: object ++ + Rules to rewrite the module format output. 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. + +# FORMAT REPLACEMENTS + +*{title}*: The title of the focused window. + +*{app_id}*: The app_id of the focused window. + +*{layout}*: The layout 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 format output is left unchanged. + +Invalid expressions (e.g., mismatched parentheses) are skipped. + +# EXAMPLES + +``` +"dwl/window": { + "format": "{}", + "max-length": 50, + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } +} +``` diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index dba7dbcac..33b28ae4e 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -25,6 +25,19 @@ Addressed by *hyprland/language* typeof: string ++ Specifies which keyboard to use from hyprctl devices output. Using the option that begins with "at-translated-set..." is recommended. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS diff --git a/man/waybar-hyprland-submap.5.scd b/man/waybar-hyprland-submap.5.scd index 0dc0b11af..64398e614 100644 --- a/man/waybar-hyprland-submap.5.scd +++ b/man/waybar-hyprland-submap.5.scd @@ -31,7 +31,11 @@ Addressed by *hyprland/submap* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -66,6 +70,29 @@ Addressed by *hyprland/submap* default: true ++ Option to disable tooltip on hover. +*always-on*: ++ + typeof: bool ++ + default: false ++ + Option to display the widget even when there's no active submap. + +*default-submap* ++ + typeof: string ++ + default: Default ++ + Option to set the submap name to display when not in an active submap. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # EXAMPLES diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 12c1fe391..686f8aa75 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -42,6 +42,11 @@ Addressed by *hyprland/workspaces* default: false ++ If set to true, special workspaces will be shown. +*special-visible-only*: ++ + typeof: bool ++ + default: false ++ + If this and show-special are to true, special workspaces will be shown only if visible. + *all-outputs*: ++ typeof: bool ++ default: false ++ @@ -52,6 +57,13 @@ Addressed by *hyprland/workspaces* default: false ++ If set to true, only the active workspace will be shown. +*move-to-monitor*: ++ + typeof: bool ++ + default: false ++ + If set to true, open the workspace on the current monitor when clicking on a workspace button. + Otherwise, the workspace will open on the monitor where it was previously assigned. + Analog to using `focusworkspaceoncurrentmonitor` dispatcher instead of `workspace` in Hyprland. + *ignore-workspaces*: ++ typeof: array ++ default: [] ++ @@ -135,6 +147,7 @@ Additional to workspace name matching, the following *format-icons* can be set. "class title<.*github.*>": "", // Windows whose class is "firefox" and title contains "github". Note that "class" always comes first. "foot": "", // Windows that contain "foot" in either class or title. For optimization reasons, it will only match against a title if at least one other window explicitly matches against a title. "code": "󰨞", + "title<.* - (.*) - VSCodium>": "codium $1" // captures part of the window title and formats it into output } } ``` @@ -158,3 +171,4 @@ Additional to workspace name matching, the following *format-icons* can be set. - *#workspaces button.persistent* - *#workspaces button.special* - *#workspaces button.urgent* +- *#workspaces button.hosting-monitor* (gets applied if workspace-monitor == waybar-monitor) diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 287def1a3..f7677634c 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -33,7 +33,11 @@ screensaver, also known as "presentation mode". *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -85,6 +89,19 @@ screensaver, also known as "presentation mode". typeof: string ++ This format is used when the inhibit is deactivated. +*menu*: ++ + typeof: string ++ + Action that popups the menu. Cannot be "on-click". + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{status}*: status (*activated* or *deactivated*) diff --git a/man/waybar-inhibitor.5.scd b/man/waybar-inhibitor.5.scd index 1233eb7d7..679a5c4b0 100644 --- a/man/waybar-inhibitor.5.scd +++ b/man/waybar-inhibitor.5.scd @@ -37,7 +37,11 @@ See *systemd-inhibit*(1) for more information. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -72,6 +76,19 @@ See *systemd-inhibit*(1) for more information. default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. Cannot be "on-click". + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{status}*: status (*activated* or *deactivated*) diff --git a/man/waybar-jack.5.scd b/man/waybar-jack.5.scd index 3af71b617..573b36c27 100644 --- a/man/waybar-jack.5.scd +++ b/man/waybar-jack.5.scd @@ -63,7 +63,11 @@ Addressed by *jack* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -81,6 +85,19 @@ Addressed by *jack* typeof: string ++ Command to execute when the module is updated. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{load}*: The current CPU load estimated by JACK. diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index 55c74b0bd..7738c576d 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -45,7 +45,11 @@ Addressed by *memory* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -80,6 +84,19 @@ Addressed by *memory* default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{percentage}*: Percentage of memory in use. diff --git a/man/waybar-menu.5.scd b/man/waybar-menu.5.scd new file mode 100644 index 000000000..19790ed47 --- /dev/null +++ b/man/waybar-menu.5.scd @@ -0,0 +1,153 @@ +waybar-menu(5) + +# NAME + +waybar - menu property + +# OVERVIEW + + +Some modules support a 'menu', which allows to have a popup menu whan a defined +click is done over the module. + +# PROPERTIES + +A module that implements a 'menu' needs 3 properties defined in its config : + +*menu*: ++ + typeof: string ++ + Action that popups the menu. The possibles actions are : + +[- *Option* +:- *Description* +|[ *on-click* +:< When you left-click on the module +|[ *on-click-release* +:< When you release left button on the module +|[ *on-double-click* +:< When you double left click on the module +|[ *on-triple-click* +:< When you triple left click on the module +|[ *on-click-middle* +:< When you middle click on the module using mousewheel +|[ *on-click-middle-release* +:< When you release mousewheel button on the module +|[ *on-double-click-middle* +:< When you double middle click on the module +|[ *on-triple-click-middle* +:< When you triple middle click on the module +|[ *on-click-right* +:< When you right click on the module using +|[ *on-click-right-release* +:< When you release right button on the module +|[ *on-double-click-right* +:< When you double right click on the module +|[ *on-triple-click-right* +:< When you triple middle click on the module +|[ *on-click-backward* +:< When you click on the module using mouse backward button +|[ *on-click-backward-release* +:< When you release mouse backward button on the module +|[ *on-double-click-backward* +:< When you double click on the module using mouse backward button +|[ *on-triple-click-backward* +:< When you triple click on the module using mouse backawrd button +|[ *on-click-forward* +:< When you click on the module using mouse forward button +|[ *on-click-forward-release* +:< When you release mouse forward button on the module +|[ *on-double-click-forward* +:< When you double click on the module using mouse forward button +|[ *on-triple-click-forward* +:< When you triple click on the module using mouse forward button + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu*. + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. The identifiers of + each actions needs to exists as an id in the 'menu-file' for it to be linked + properly. + +# MENU-FILE + +The menu-file is an `.xml` file representing a GtkBuilder. Documentation for it +can be found here : https://docs.gtk.org/gtk4/class.Builder.html + +Here, it needs to have an element of type GtkMenu with id "menu". Eeach actions +in *menu-actions* are linked to elements in the *menu-file* file by the id of +the elements. + +# EXAMPLE + +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", + }, +}, +``` + +~/.config/waybar/power_menu.xml : +``` + + + + + + Suspend + + + + + Hibernate + + + + + Shutdown + + + + + + + + Reboot + + + + +``` + +# STYLING MENUS + +- *menu* + Style for the menu + +- *menuitem* + Style for items in the menu + +# EXAMPLE: + +``` +menu { + border-radius: 15px; + background: #161320; + color: #B5E8E0; +} +menuitem { + border-radius: 15px; +} +``` diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index ffef0fefd..2f1bdf208 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -103,7 +103,11 @@ Addressed by *mpd* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -158,6 +162,19 @@ Addressed by *mpd* default: {} ++ Icon to show depending on the "single" option (*{ "on": "...", "off": "..." }*) +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS ## WHEN PLAYING/PAUSED diff --git a/man/waybar-mpris.5.scd b/man/waybar-mpris.5.scd index 117b816c4..455fcb177 100644 --- a/man/waybar-mpris.5.scd +++ b/man/waybar-mpris.5.scd @@ -119,8 +119,11 @@ The *mpris* module displays currently playing media via libplayerctl. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. ++ - If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -139,11 +142,11 @@ The *mpris* module displays currently playing media via libplayerctl. *player-icons*: ++ typeof: map[string]string ++ - Allows setting _{player-icon}_ based on player-name property. + Allows setting _{player_icon}_ based on player-name property. *status-icons*: ++ typeof: map[string]string ++ - Allows setting _{status-icon}_ based on player status (playing, paused, stopped). + Allows setting _{status_icon}_ based on player status (playing, paused, stopped). # FORMAT REPLACEMENTS diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index 08c86d3d6..cc0b470b1 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -70,7 +70,11 @@ Addressed by *network* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -125,6 +129,19 @@ Addressed by *network* typeof: string ++ This format is used when the displayed interface is disabled. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{ifname}*: Name of the network interface. diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd new file mode 100644 index 000000000..82fad13bd --- /dev/null +++ b/man/waybar-power-profiles-daemon.5.scd @@ -0,0 +1,72 @@ +waybar-power-profiles-daemon(5) + +# NAME + +waybar - power-profiles-daemon module + +# DESCRIPTION + +The *power-profiles-daemon* module displays the active power-profiles-daemon profile and cycle through the available profiles on click. + +# FILES + +$XDG_CONFIG_HOME/waybar/config + +# CONFIGURATION + + +[- *Option* +:- *Typeof* +:- *Default* +:= *Description* +|[ *format* +:[ string +:[ "{icon}" +:[ Message displayed on the bar. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. +|[ *tooltip-format* +:[ string +:[ "Power profile: {profile}\\nDriver: {driver}" +:[ Messaged displayed in the module tooltip. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. +|[ *tooltip* +:[ bool +:[ true +:[ Display the tooltip. +|[ *format-icons* +:[ object +:[ See default value in the example below. +:[ Icons used to represent the various power-profile. *Note*: the default configuration uses the font-awesome icons. You may want to override it if you don't have this font installed on your system. + + +# CONFIGURATION EXAMPLES + +Compact display (default config): + +``` +"power-profiles-daemon": { + "format": "{icon}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "", + "performance": "", + "balanced": "", + "power-saver": "" + } +} +``` + +Display the full profile name: + +``` +"power-profiles-daemon": { + "format": "{icon} {profile}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "", + "performance": "", + "balanced": "", + "power-saver": "" + } +} +``` diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index fc1da1c4a..cf07fed18 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -31,7 +31,7 @@ The volume can be controlled by dragging the slider across the bar or clicking o ``` "modules-right": [ - "pulseaudio-slider", + "pulseaudio/slider", ], "pulseaudio/slider": { "min": 0, diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index e04245ee3..232e84a00 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -56,7 +56,11 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *scroll-step*: ++ typeof: float ++ @@ -109,6 +113,19 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c typeof: array ++ Sinks in this list will not be shown as active sink by Waybar. Entries should be the sink's description field. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{desc}*: Pulseaudio port's description, for bluetooth it'll be the device name. @@ -138,6 +155,8 @@ If they are found in the current PulseAudio port name, the corresponding icons w - *hifi* - *phone* +Additionally, suffixing a device name or port with *-muted* will cause the icon +to be selected when the corresponding audio device is muted. This applies to *default* as well. # EXAMPLES @@ -148,10 +167,12 @@ If they are found in the current PulseAudio port name, the corresponding icons w "format-muted": "", "format-icons": { "alsa_output.pci-0000_00_1f.3.analog-stereo": "", + "alsa_output.pci-0000_00_1f.3.analog-stereo-muted": "", "headphones": "", "handsfree": "", "headset": "", "phone": "", + "phone-muted": "", "portable": "", "car": "", "default": ["", ""] diff --git a/man/waybar-river-layout.5.scd b/man/waybar-river-layout.5.scd index f6f682d01..4fb23085b 100644 --- a/man/waybar-river-layout.5.scd +++ b/man/waybar-river-layout.5.scd @@ -33,7 +33,11 @@ Addressed by *river/layout* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -47,6 +51,19 @@ Addressed by *river/layout* typeof: string ++ Command to execute when you right-click on the module. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # EXAMPLE ``` diff --git a/man/waybar-river-mode.5.scd b/man/waybar-river-mode.5.scd index aea6e205e..5769a9a2c 100644 --- a/man/waybar-river-mode.5.scd +++ b/man/waybar-river-mode.5.scd @@ -31,7 +31,11 @@ Addressed by *river/mode* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -61,6 +65,19 @@ Addressed by *river/mode* typeof: double ++ Threshold to be used when scrolling. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # EXAMPLES ``` diff --git a/man/waybar-river-window.5.scd b/man/waybar-river-window.5.scd index 9c202b2aa..7e661f438 100644 --- a/man/waybar-river-window.5.scd +++ b/man/waybar-river-window.5.scd @@ -31,7 +31,11 @@ Addressed by *river/window* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -45,6 +49,19 @@ Addressed by *river/window* typeof: string ++ Command to execute when you right-click on the module. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # EXAMPLES ``` diff --git a/man/waybar-sndio.5.scd b/man/waybar-sndio.5.scd index 1bb0484a7..f8d1615d4 100644 --- a/man/waybar-sndio.5.scd +++ b/man/waybar-sndio.5.scd @@ -32,7 +32,11 @@ cursor is over the module, and clicking on the module toggles mute. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *scroll-step*: ++ typeof: int ++ @@ -70,6 +74,19 @@ cursor is over the module, and clicking on the module toggles mute. typeof: double ++ Threshold to be used when scrolling. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{volume}*: Volume in percentage. diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in new file mode 100644 index 000000000..0af393ef4 --- /dev/null +++ b/man/waybar-styles.5.scd.in @@ -0,0 +1,44 @@ +waybar-styles(5) + +# NAME + +waybar-styles - using stylesheets for waybar + +# DESCRIPTION + +Waybar uses Cascading Style Sheets (CSS) to configure its appearance. + +It uses the first file found in this search order: + +- *$XDG_CONFIG_HOME/waybar/style.css* +- *~/.config/waybar/style.css* +- *~/waybar/style.css* +- */etc/xdg/waybar/style.css* +- *@sysconfdir@/xdg/waybar/style.css* + +# EXAMPLE + +An example user-controlled stylesheet that just changes the color of the clock to be green on black, while keeping the rest of the system config the same would be: + +``` +@import url("file:///etc/xdg/waybar/style.css") + +#clock { + background: #000000; + color: #00ff00; +} +``` + +## Hover-effect + +You can apply special styling to any module for when the cursor hovers it. + +``` +#clock:hover { + background-color: #ffffff; +} +``` + +# SEE ALSO + +- *waybar(5)* diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd index c257ed75e..a1fc5d08a 100644 --- a/man/waybar-sway-language.5.scd +++ b/man/waybar-sway-language.5.scd @@ -17,6 +17,11 @@ Addressed by *sway/language* default: {} ++ The format, how layout should be displayed. +*hide-single-layout*: ++ + typeof: bool ++ + default: false ++ + Defines visibility of the module if a single layout is configured + *tooltip-format*: ++ typeof: string ++ default: {} ++ @@ -27,6 +32,19 @@ Addressed by *sway/language* default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{short}*: Short name of layout (e.g. "us"). Equals to {}. diff --git a/man/waybar-sway-mode.5.scd b/man/waybar-sway-mode.5.scd index 87e70adfd..1fcf3cf82 100644 --- a/man/waybar-sway-mode.5.scd +++ b/man/waybar-sway-mode.5.scd @@ -31,7 +31,11 @@ Addressed by *sway/mode* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -66,6 +70,19 @@ Addressed by *sway/mode* default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # EXAMPLES ``` diff --git a/man/waybar-sway-scratchpad.5.scd b/man/waybar-sway-scratchpad.5.scd index 64c43db6b..5ae104bcf 100644 --- a/man/waybar-sway-scratchpad.5.scd +++ b/man/waybar-sway-scratchpad.5.scd @@ -36,6 +36,19 @@ Addressed by *sway/scratchpad* default: {app}: {title} ++ The format, how information in the tooltip should be displayed. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{icon}*: Icon, as defined in *format-icons*. diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index 9b793f32c..037e6b55c 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -31,7 +31,11 @@ Addressed by *sway/window* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 3343b8d5a..a65a999ba 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -171,6 +171,7 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge "window-rewrite": { "class": "", "class": "k", + "title<.* - (.*) - VSCodium>": "codium $1" // captures part of the window title and formats it into output } } ``` @@ -182,5 +183,6 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge - *#workspaces button.focused* - *#workspaces button.urgent* - *#workspaces button.persistent* +- *#workspaces button.empty* - *#workspaces button.current_output* - *#workspaces button#sway-workspace-${name}* diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd index ac92c533e..ada3ab8b5 100644 --- a/man/waybar-systemd-failed-units.5.scd +++ b/man/waybar-systemd-failed-units.5.scd @@ -36,6 +36,19 @@ Addressed by *systemd-failed-units* default: *true* ++ Option to hide this module when there is no failing units. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{nr_failed_system}*: Number of failed units from systemwide (PID=1) systemd. diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 1d6e7d2eb..eab4cbb3b 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -25,6 +25,7 @@ Addressed by *temperature* *hwmon-path-abs*: ++ typeof: string ++ The path of the hwmon-directory of the device, e.g. */sys/devices/pci0000:00/0000:00:18.3/hwmon*. (Note that the subdirectory *hwmon/hwmon#*, where *#* is a number is not part of the path!) Has to be used together with *input-filename*. + This can also be an array of strings, for which, it just works like *hwmon-path*. *input-filename*: ++ typeof: string ++ @@ -71,7 +72,11 @@ Addressed by *temperature* *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -106,6 +111,19 @@ Addressed by *temperature* default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{temperatureC}*: Temperature in Celsius. diff --git a/man/waybar-upower.5.scd b/man/waybar-upower.5.scd index 5e2a8eb85..303ee65e1 100644 --- a/man/waybar-upower.5.scd +++ b/man/waybar-upower.5.scd @@ -17,6 +17,12 @@ compatible devices in the tooltip. The battery to monitor. Refer to the https://upower.freedesktop.org/docs/UpDevice.html#UpDevice--native-path ++ Can be obtained using `upower --dump` +*model*: ++ + typeof: string ++ + default: ++ + The battery to monitor, based on the model. (this option is ignored if *native-path* is given). ++ + Can be obtained using `upower --dump` + *icon-size*: ++ typeof: integer ++ default: 20 ++ @@ -62,6 +68,19 @@ compatible devices in the tooltip. default: true ++ Option to disable battery icon. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{percentage}*: The battery capacity in percentage diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index 5424deb6e..770ff0d5c 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -47,7 +47,11 @@ The *wireplumber* module displays the current volume reported by WirePlumber. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *scroll-step*: ++ typeof: float ++ @@ -83,6 +87,19 @@ The *wireplumber* module displays the current volume reported by WirePlumber. default: 100 ++ The maximum volume that can be set, in percentage. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{volume}*: Volume in percentage. diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 628bbf610..53613e4ab 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -16,9 +16,11 @@ Valid locations for this file are: - */etc/xdg/waybar/* - *@sysconfdir@/xdg/waybar/* -A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config +A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config.jsonc Also, a minimal example configuration can be found at the bottom of this man page. +The visual display elements for waybar use a CSS stylesheet, see *waybar-styles(5)* for details. + # BAR CONFIGURATION *layer* ++ @@ -310,6 +312,7 @@ A group may hide all but one element, showing them only on mouse hover. In order - *waybar-custom(5)* - *waybar-disk(5)* - *waybar-dwl-tags(5)* +- *waybar-dwl-window(5)* - *waybar-gamemode(5)* - *waybar-hyprland-language(5)* - *waybar-hyprland-submap(5)* @@ -346,3 +349,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)" diff --git a/meson.build b/meson.build index 46ff9926e..a154a51b7 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'waybar', 'cpp', 'c', - version: '0.9.24', + version: '0.10.3', license: 'MIT', meson_version: '>= 0.59.0', default_options : [ @@ -92,7 +92,7 @@ libevdev = dependency('libevdev', required: get_option('libevdev')) libmpdclient = dependency('libmpdclient', required: get_option('mpd')) xkbregistry = dependency('xkbregistry') libjack = dependency('jack', required: get_option('jack')) -libwireplumber = dependency('wireplumber-0.4', required: get_option('wireplumber')) +libwireplumber = dependency('wireplumber-0.5', required: get_option('wireplumber')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() @@ -192,6 +192,7 @@ man_files = files( 'man/waybar-idle-inhibitor.5.scd', 'man/waybar-image.5.scd', 'man/waybar-states.5.scd', + 'man/waybar-menu.5.scd', 'man/waybar-temperature.5.scd', ) @@ -212,6 +213,7 @@ if is_linux 'src/modules/cpu_usage/linux.cpp', 'src/modules/memory/common.cpp', 'src/modules/memory/linux.cpp', + 'src/modules/power_profiles_daemon.cpp', 'src/modules/systemd_failed_units.cpp', ) man_files += files( @@ -221,6 +223,7 @@ if is_linux 'man/waybar-cpu.5.scd', 'man/waybar-memory.5.scd', 'man/waybar-systemd-failed-units.5.scd', + 'man/waybar-power-profiles-daemon.5.scd', ) elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd add_project_arguments('-DHAVE_CPU_BSD', language: 'cpp') @@ -291,7 +294,9 @@ endif if true add_project_arguments('-DHAVE_DWL', language: 'cpp') src_files += files('src/modules/dwl/tags.cpp') + src_files += files('src/modules/dwl/window.cpp') man_files += files('man/waybar-dwl-tags.5.scd') + man_files += files('man/waybar-dwl-window.5.scd') endif if true @@ -301,7 +306,9 @@ if true 'src/modules/hyprland/language.cpp', 'src/modules/hyprland/submap.cpp', 'src/modules/hyprland/window.cpp', + 'src/modules/hyprland/workspace.cpp', 'src/modules/hyprland/workspaces.cpp', + 'src/modules/hyprland/windowcreationpayload.cpp', ) man_files += files( 'man/waybar-hyprland-language.5.scd', @@ -331,10 +338,7 @@ endif if (upower_glib.found() and not get_option('logind').disabled()) add_project_arguments('-DHAVE_UPOWER', language: 'cpp') - src_files += files( - 'src/modules/upower/upower.cpp', - 'src/modules/upower/upower_tooltip.cpp', - ) + src_files += files('src/modules/upower.cpp') man_files += files('man/waybar-upower.5.scd') endif @@ -344,7 +348,8 @@ if pipewire.found() src_files += files( 'src/modules/privacy/privacy.cpp', 'src/modules/privacy/privacy_item.cpp', - 'src/util/pipewire_backend.cpp', + 'src/util/pipewire/pipewire_backend.cpp', + 'src/util/pipewire/privacy_node_info.cpp', ) man_files += files('man/waybar-privacy.5.scd') endif @@ -462,7 +467,7 @@ if get_option('experimental') endif cava = dependency('cava', - version : '>=0.10.1', + version : '>=0.10.2', required: get_option('cava'), fallback : ['cava', 'cava_dep'], not_found_message: 'cava is not found. Building waybar without cava') @@ -518,8 +523,8 @@ executable( ) install_data( - './resources/config', - './resources/style.css', + 'resources/config.jsonc', + 'resources/style.css', install_dir: sysconfdir / 'xdg/waybar' ) @@ -534,6 +539,14 @@ if scdoc.found() } ) + man_files += configure_file( + input: 'man/waybar-styles.5.scd.in', + output: 'waybar-styles.5.scd', + configuration: { + 'sysconfdir': prefix / sysconfdir + } + ) + fs = import('fs') mandir = get_option('mandir') foreach file : man_files @@ -577,4 +590,3 @@ if clangtidy.found() '-p', meson.project_build_root() ] + src_files) endif - diff --git a/nix/default.nix b/nix/default.nix index bf8f2f216..9ce39a9b5 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -5,12 +5,12 @@ }: let libcava = rec { - version = "0.10.1"; + version = "0.10.2"; src = pkgs.fetchFromGitHub { owner = "LukashonakV"; repo = "cava"; rev = version; - hash = "sha256-iIYKvpOWafPJB5XhDOSIW9Mb4I3A4pcgIIPQdQYEqUw="; + hash = "sha256-jU7RQV2txruu/nUUl0TzjK4nai7G38J1rcTjO7UXumY="; }; }; in @@ -25,6 +25,10 @@ in mesonFlags = lib.remove "-Dgtk-layer-shell=enabled" oldAttrs.mesonFlags; + buildInputs = (builtins.filter (p: p.pname != "wireplumber") oldAttrs.buildInputs) ++ [ + pkgs.wireplumber + ]; + postUnpack = '' pushd "$sourceRoot" cp -R --no-preserve=mode,ownership ${libcava.src} subprojects/cava-${libcava.version} @@ -32,4 +36,4 @@ in popd ''; } -)) \ No newline at end of file +)) diff --git a/resources/config b/resources/config.jsonc similarity index 81% rename from resources/config rename to resources/config.jsonc index adf03a1f3..7e0771f51 100644 --- a/resources/config +++ b/resources/config.jsonc @@ -1,3 +1,4 @@ +// -*- mode: jsonc -*- { // "layer": "top", // Waybar at top layer // "position": "bottom", // Waybar position (top|bottom|left|right) @@ -5,9 +6,33 @@ // "width": 1280, // Waybar width "spacing": 4, // Gaps between modules (4px) // Choose the order of the modules - "modules-left": ["sway/workspaces", "sway/mode", "sway/scratchpad", "custom/media"], - "modules-center": ["sway/window"], - "modules-right": ["mpd", "idle_inhibitor", "pulseaudio", "network", "cpu", "memory", "temperature", "backlight", "keyboard-state", "sway/language", "battery", "battery#bat2", "clock", "tray"], + "modules-left": [ + "sway/workspaces", + "sway/mode", + "sway/scratchpad", + "custom/media" + ], + "modules-center": [ + "sway/window" + ], + "modules-right": [ + "mpd", + "idle_inhibitor", + "pulseaudio", + "network", + "power-profiles-daemon", + "cpu", + "memory", + "temperature", + "backlight", + "keyboard-state", + "sway/language", + "battery", + "battery#bat2", + "clock", + "tray", + "custom/power" + ], // Modules configuration // "sway/workspaces": { // "disable-scroll": true, @@ -49,7 +74,7 @@ "format-disconnected": "Disconnected ", "format-stopped": "{consumeIcon}{randomIcon}{repeatIcon}{singleIcon}Stopped ", "unknown-tag": "N/A", - "interval": 2, + "interval": 5, "consume-icons": { "on": " " }, @@ -124,6 +149,17 @@ "battery#bat2": { "bat": "BAT2" }, + "power-profiles-daemon": { + "format": "{icon}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "", + "performance": "", + "balanced": "", + "power-saver": "" + } + }, "network": { // "interface": "wlp2*", // (Optional) To force the use of this interface "format-wifi": "{essid} ({signalStrength}%) ", @@ -163,6 +199,17 @@ "escape": true, "exec": "$HOME/.config/waybar/mediaplayer.py 2> /dev/null" // Script in resources folder // "exec": "$HOME/.config/waybar/mediaplayer.py --player spotify 2> /dev/null" // Filter player based on name + }, + "custom/power": { + "format" : "⏻ ", + "tooltip": false, + "menu": "on-click", + "menu-file": "$HOME/.config/waybar/power_menu.xml", // Menu file in resources folder + "menu-actions": { + "shutdown": "shutdown", + "reboot": "reboot", + "suspend": "systemctl suspend", + "hibernate": "systemctl hibernate" + } } } - diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 4aea4171b..d1bb72b4d 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -113,6 +113,7 @@ def on_metadata_changed(self, player, metadata, _=None): player_name = player.props.player_name artist = player.get_artist() title = player.get_title() + title = title.replace("&", "&") track_info = "" if player_name == "spotify" and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]: @@ -136,6 +137,10 @@ def on_metadata_changed(self, player, metadata, _=None): def on_player_appeared(self, _, player): logger.info(f"Player has appeared: {player.name}") + if player.name in self.excluded_player: + logger.debug( + "New player appeared, but it's in exclude player list, skipping") + return if player is not None and (self.selected_player is None or player.name == self.selected_player): self.init_player(player) else: diff --git a/resources/custom_modules/power_menu.xml b/resources/custom_modules/power_menu.xml new file mode 100644 index 000000000..aa2a42cae --- /dev/null +++ b/resources/custom_modules/power_menu.xml @@ -0,0 +1,28 @@ + + + + + + Suspend + + + + + Hibernate + + + + + Shutdown + + + + + + + + Reboot + + + + diff --git a/resources/style.css b/resources/style.css index 3d44829f3..7e830285f 100644 --- a/resources/style.css +++ b/resources/style.css @@ -48,6 +48,11 @@ button:hover { box-shadow: inset 0 -3px #ffffff; } +/* you can set a style on hover for any module like this */ +#pulseaudio:hover { + background-color: #a37800; +} + #workspaces button { padding: 0 5px; background-color: transparent; @@ -69,7 +74,7 @@ button:hover { #mode { background-color: #64727D; - border-bottom: 3px solid #ffffff; + box-shadow: inset 0 -3px #ffffff; } #clock, @@ -87,6 +92,7 @@ button:hover { #mode, #idle_inhibitor, #scratchpad, +#power-profiles-daemon, #mpd { padding: 0 10px; color: #ffffff; @@ -139,6 +145,25 @@ button:hover { animation-direction: alternate; } +#power-profiles-daemon { + padding-right: 15px; +} + +#power-profiles-daemon.performance { + background-color: #f53c3c; + color: #ffffff; +} + +#power-profiles-daemon.balanced { + background-color: #2980b9; + color: #ffffff; +} + +#power-profiles-daemon.power-saver { + background-color: #2ecc71; + color: #000000; +} + label:focus { background-color: #000000; } diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index a238143b5..fda5f9fd1 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -24,18 +24,61 @@ AAppIconLabel::AAppIconLabel(const Json::Value& config, const std::string& name, image_.set_pixel_size(app_icon_size_); } +std::string toLowerCase(const std::string& input) { + std::string result = input; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +std::optional getFileBySuffix(const std::string& dir, const std::string& suffix, + bool check_lower_case) { + if (!std::filesystem::exists(dir)) { + return {}; + } + for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + if (filename.size() < suffix.size()) { + continue; + } + if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) || + (check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(), + toLowerCase(suffix)) == 0)) { + return entry.path().string(); + } + } + } + + return {}; +} + +std::optional getFileBySuffix(const std::string& dir, const std::string& suffix) { + return getFileBySuffix(dir, suffix, false); +} + std::optional getDesktopFilePath(const std::string& app_identifier, const std::string& alternative_app_identifier) { + if (app_identifier.empty()) { + return {}; + } + const auto data_dirs = Glib::get_system_data_dirs(); for (const auto& data_dir : data_dirs) { - const auto data_app_dir = data_dir + "applications/"; - auto desktop_file_path = data_app_dir + app_identifier + ".desktop"; - if (std::filesystem::exists(desktop_file_path)) { + const auto data_app_dir = data_dir + "/applications/"; + auto desktop_file_suffix = app_identifier + ".desktop"; + // searching for file by suffix catches cases like terminal emulator "foot" where class is + // "footclient" and desktop file is named "org.codeberg.dnkl.footclient.desktop" + auto desktop_file_path = getFileBySuffix(data_app_dir, desktop_file_suffix, true); + // "true" argument allows checking for lowercase - this catches cases where class name is + // "LibreWolf" and desktop file is named "librewolf.desktop" + if (desktop_file_path.has_value()) { return desktop_file_path; } if (!alternative_app_identifier.empty()) { - desktop_file_path = data_app_dir + alternative_app_identifier + ".desktop"; - if (std::filesystem::exists(desktop_file_path)) { + desktop_file_suffix = alternative_app_identifier + ".desktop"; + desktop_file_path = getFileBySuffix(data_app_dir, desktop_file_suffix, true); + if (desktop_file_path.has_value()) { return desktop_file_path; } } @@ -53,21 +96,14 @@ std::optional getIconName(const std::string& app_identifier, return app_identifier; } - const auto app_identifier_desktop = app_identifier + "-desktop"; + auto app_identifier_desktop = app_identifier + "-desktop"; if (DefaultGtkIconThemeWrapper::has_icon(app_identifier_desktop)) { return app_identifier_desktop; } - const auto to_lower = [](const std::string& str) { - auto str_cpy = str; - std::transform(str_cpy.begin(), str_cpy.end(), str_cpy.begin(), - [](unsigned char c) { return std::tolower(c); }); - return str; - }; - - const auto first_space = app_identifier.find_first_of(' '); + auto first_space = app_identifier.find_first_of(' '); if (first_space != std::string::npos) { - const auto first_word = to_lower(app_identifier.substr(0, first_space)); + auto first_word = toLowerCase(app_identifier.substr(0, first_space)); if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { return first_word; } @@ -75,7 +111,7 @@ std::optional getIconName(const std::string& app_identifier, const auto first_dash = app_identifier.find_first_of('-'); if (first_dash != std::string::npos) { - const auto first_word = to_lower(app_identifier.substr(0, first_dash)); + auto first_word = toLowerCase(app_identifier.substr(0, first_dash)); if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { return first_word; } diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index a7e2380a5..d7ee666e6 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -9,10 +9,23 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const bool enable_click, bool enable_scroll) : ALabel(config, name, id, format, interval, ellipsize, enable_click, enable_scroll) { event_box_.remove(); + label_.unset_name(); + label_.get_style_context()->remove_class(MODULE_CLASS); + box_.get_style_context()->add_class(MODULE_CLASS); + if (!id.empty()) { + label_.get_style_context()->remove_class(id); + box_.get_style_context()->add_class(id); + } + box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); - box_.set_spacing(8); + 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_); + event_box_.add(box_); } diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 7840819d9..da2991a35 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -2,6 +2,8 @@ #include +#include +#include #include namespace waybar { @@ -9,7 +11,9 @@ namespace waybar { ALabel::ALabel(const Json::Value& config, const std::string& name, const std::string& id, const std::string& format, uint16_t interval, bool ellipsize, bool enable_click, bool enable_scroll) - : AModule(config, name, id, config["format-alt"].isString() || enable_click, enable_scroll), + : AModule(config, name, id, + config["format-alt"].isString() || config["menu"].isString() || enable_click, + enable_scroll), format_(config_["format"].isString() ? config_["format"].asString() : format), interval_(config_["interval"] == "once" ? std::chrono::seconds::max() @@ -50,6 +54,58 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st label_.set_xalign(align); } } + + // If a GTKMenu is requested in the config + if (config_["menu"].isString()) { + // Create the GTKMenu widget + try { + // Check that the file exists + std::string menuFile = config_["menu-file"].asString(); + // Read the menu descriptor file + std::ifstream file(menuFile); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + menuFile); + } + std::stringstream fileContent; + fileContent << file.rdbuf(); + GtkBuilder* builder = gtk_builder_new(); + + // Make the GtkBuilder and check for errors in his parsing + if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) { + throw std::runtime_error("Error found in the file " + menuFile); + } + + menu_ = gtk_builder_get_object(builder, "menu"); + if (menu_ == nullptr) { + throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); + } + submenus_ = std::map(); + menuActionsMap_ = std::map(); + + // Linking actions to the GTKMenu based on + for (Json::Value::const_iterator it = config_["menu-actions"].begin(); + it != config_["menu-actions"].end(); ++it) { + std::string key = it.key().asString(); + submenus_[key] = GTK_MENU_ITEM(gtk_builder_get_object(builder, key.c_str())); + menuActionsMap_[key] = it->asString(); + g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), + (gpointer)menuActionsMap_[key].c_str()); + } + } catch (std::runtime_error& e) { + spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what()); + } + } + + if (config_["justify"].isString()) { + auto justify_str = config_["justify"].asString(); + if (justify_str == "left") { + label_.set_justify(Gtk::Justification::JUSTIFY_LEFT); + } else if (justify_str == "right") { + label_.set_justify(Gtk::Justification::JUSTIFY_RIGHT); + } else if (justify_str == "center") { + label_.set_justify(Gtk::Justification::JUSTIFY_CENTER); + } + } } auto ALabel::update() -> void { AModule::update(); } @@ -65,7 +121,7 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ } if (format_icons.isArray()) { auto size = format_icons.size(); - if (size) { + if (size != 0U) { auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); format_icons = format_icons[idx]; } @@ -91,7 +147,7 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& } if (format_icons.isArray()) { auto size = format_icons.size(); - if (size) { + if (size != 0U) { auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); format_icons = format_icons[idx]; } @@ -114,6 +170,10 @@ bool waybar::ALabel::handleToggle(GdkEventButton* const& e) { return AModule::handleToggle(e); } +void ALabel::handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data) { + waybar::util::command::res res = waybar::util::command::exec((char*)data, "GtkMenu"); +} + std::string ALabel::getState(uint8_t value, bool lesser) { if (!config_["states"].isObject()) { return ""; diff --git a/src/AModule.cpp b/src/AModule.cpp index 9a9f13866..c40e3a56b 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -8,37 +8,44 @@ namespace waybar { AModule::AModule(const Json::Value& config, const std::string& name, const std::string& id, bool enable_click, bool enable_scroll) - : name_(std::move(name)), - config_(std::move(config)), + : name_(name), + config_(config), isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, distance_scrolled_y_(0.0), distance_scrolled_x_(0.0) { // Configure module action Map const Json::Value actions{config_["actions"]}; + for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) { if (it.key().isString() && it->isString()) - if (eventActionMap_.count(it.key().asString()) == 0) { + if (!eventActionMap_.contains(it.key().asString())) { eventActionMap_.insert({it.key().asString(), it->asString()}); enable_click = true; enable_scroll = true; } else - spdlog::warn("Dublicate action is ignored: {0}", it.key().asString()); + spdlog::warn("Duplicate action is ignored: {0}", it.key().asString()); else spdlog::warn("Wrong actions section configuration. See config by index: {}", it.index()); } + event_box_.signal_enter_notify_event().connect(sigc::mem_fun(*this, &AModule::handleMouseEnter)); + event_box_.signal_leave_notify_event().connect(sigc::mem_fun(*this, &AModule::handleMouseLeave)); + // configure events' user commands - // hasUserEvent is true if any element from eventMap_ is satisfying the condition in the lambda - bool hasUserEvent = + // hasUserEvents is true if any element from eventMap_ is satisfying the condition in the lambda + bool hasUserEvents = std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) { // True if there is any non-release type event return eventEntry.first.second != GdkEventType::GDK_BUTTON_RELEASE && config[eventEntry.second].isString(); }) != eventMap_.cend(); - if (enable_click || hasUserEvent) { + if (enable_click || hasUserEvents) { + hasUserEvents_ = true; event_box_.add_events(Gdk::BUTTON_PRESS_MASK); event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &AModule::handleToggle)); + } else { + hasUserEvents_ = false; } bool hasReleaseEvent = @@ -51,7 +58,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: event_box_.add_events(Gdk::BUTTON_RELEASE_MASK); event_box_.signal_button_release_event().connect(sigc::mem_fun(*this, &AModule::handleRelease)); } - if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() || enable_scroll) { + if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() || + config_["on-scroll-left"].isString() || config_["on-scroll-right"].isString() || + enable_scroll) { event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &AModule::handleScroll)); } @@ -81,6 +90,34 @@ auto AModule::doAction(const std::string& name) -> void { } } +void AModule::setCursor(Gdk::CursorType const& c) { + auto cursor = Gdk::Cursor::create(c); + auto gdk_window = event_box_.get_window(); + gdk_window->set_cursor(cursor); +} + +bool AModule::handleMouseEnter(GdkEventCrossing* const& e) { + if (auto* module = event_box_.get_child(); module != nullptr) { + module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + } + + if (hasUserEvents_) { + setCursor(Gdk::HAND2); + } + return false; +} + +bool AModule::handleMouseLeave(GdkEventCrossing* const& e) { + if (auto* module = event_box_.get_child(); module != nullptr) { + module->unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + } + + if (hasUserEvents_) { + setCursor(Gdk::ARROW); + } + return false; +} + bool AModule::handleToggle(GdkEventButton* const& e) { return handleUserEvent(e); } bool AModule::handleRelease(GdkEventButton* const& e) { return handleUserEvent(e); } @@ -89,12 +126,23 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { std::string format{}; const std::map, std::string>::const_iterator& rec{ eventMap_.find(std::pair(e->button, e->type))}; + if (rec != eventMap_.cend()) { // First call module actions this->AModule::doAction(rec->second); format = rec->second; } + + // Check that a menu has been configured + if (config_["menu"].isString()) { + // Check if the event is the one specified for the "menu" option + if (rec->second == config_["menu"].asString()) { + // Popup the menu + gtk_widget_show_all(GTK_WIDGET(menu_)); + gtk_menu_popup_at_pointer(GTK_MENU(menu_), reinterpret_cast(e)); + } + } // Second call user scripts if (!format.empty()) { if (config_[format].isString()) @@ -116,7 +164,7 @@ AModule::SCROLL_DIR AModule::getScrollDir(GdkEventScroll* e) { // ignore reverse-scrolling if event comes from a mouse wheel GdkDevice* device = gdk_event_get_source_device((GdkEvent*)e); - if (device != NULL && gdk_device_get_source(device) == GDK_SOURCE_MOUSE) { + if (device != nullptr && gdk_device_get_source(device) == GDK_SOURCE_MOUSE) { reverse = reverse_mouse; } @@ -179,6 +227,10 @@ bool AModule::handleScroll(GdkEventScroll* e) { eventName = "on-scroll-up"; else if (dir == SCROLL_DIR::DOWN) eventName = "on-scroll-down"; + else if (dir == SCROLL_DIR::LEFT) + eventName = "on-scroll-left"; + else if (dir == SCROLL_DIR::RIGHT) + eventName = "on-scroll-right"; // First call module actions this->AModule::doAction(eventName); @@ -190,7 +242,7 @@ bool AModule::handleScroll(GdkEventScroll* e) { return true; } -bool AModule::tooltipEnabled() { return isTooltip; } +bool AModule::tooltipEnabled() const { return isTooltip; } AModule::operator Gtk::Widget&() { return event_box_; } diff --git a/src/bar.cpp b/src/bar.cpp index 31afcd439..8c75c2c20 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -43,7 +43,7 @@ const Bar::bar_mode_map Bar::PRESET_MODES = { // .visible = true}}, {"invisible", {// - .layer = bar_layer::BOTTOM, + .layer = std::nullopt, .exclusive = false, .passthrough = true, .visible = false}}, @@ -59,7 +59,7 @@ const std::string Bar::MODE_INVISIBLE = "invisible"; const std::string_view DEFAULT_BAR_ID = "bar-0"; /* Deserializer for enum bar_layer */ -void from_json(const Json::Value& j, bar_layer& l) { +void from_json(const Json::Value& j, std::optional& l) { if (j == "bottom") { l = bar_layer::BOTTOM; } else if (j == "top") { @@ -72,16 +72,16 @@ void from_json(const Json::Value& j, bar_layer& l) { /* Deserializer for struct bar_mode */ void from_json(const Json::Value& j, bar_mode& m) { if (j.isObject()) { - if (auto v = j["layer"]; v.isString()) { + if (const auto& v = j["layer"]; v.isString()) { from_json(v, m.layer); } - if (auto v = j["exclusive"]; v.isBool()) { + if (const auto& v = j["exclusive"]; v.isBool()) { m.exclusive = v.asBool(); } - if (auto v = j["passthrough"]; v.isBool()) { + if (const auto& v = j["passthrough"]; v.isBool()) { m.passthrough = v.asBool(); } - if (auto v = j["visible"]; v.isBool()) { + if (const auto& v = j["visible"]; v.isBool()) { m.visible = v.asBool(); } } @@ -118,7 +118,7 @@ Glib::ustring to_string(Gtk::PositionType pos) { * Assumes that all the values in the object are deserializable to the same type. */ template ::value>> + typename = std::enable_if_t>> void from_json(const Json::Value& j, std::map& m) { if (j.isObject()) { for (auto it = j.begin(); it != j.end(); ++it) { @@ -316,13 +316,13 @@ void waybar::Bar::setMode(const std::string& mode) { void waybar::Bar::setMode(const struct bar_mode& mode) { auto* gtk_window = window.gobj(); - auto layer = GTK_LAYER_SHELL_LAYER_BOTTOM; - if (mode.layer == bar_layer::TOP) { - layer = GTK_LAYER_SHELL_LAYER_TOP; + if (mode.layer == bar_layer::BOTTOM) { + gtk_layer_set_layer(gtk_window, GTK_LAYER_SHELL_LAYER_BOTTOM); + } else if (mode.layer == bar_layer::TOP) { + gtk_layer_set_layer(gtk_window, GTK_LAYER_SHELL_LAYER_TOP); } else if (mode.layer == bar_layer::OVERLAY) { - layer = GTK_LAYER_SHELL_LAYER_OVERLAY; + gtk_layer_set_layer(gtk_window, GTK_LAYER_SHELL_LAYER_OVERLAY); } - gtk_layer_set_layer(gtk_window, layer); if (mode.exclusive) { gtk_layer_auto_exclusive_zone_enable(gtk_window); @@ -393,19 +393,18 @@ void waybar::Bar::setPosition(Gtk::PositionType position) { } } -void waybar::Bar::onMap(GdkEventAny*) { +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 = window.get_window()->gobj(); surface = gdk_wayland_window_get_wl_surface(gdk_window); configureGlobalOffset(gdk_window_get_width(gdk_window), gdk_window_get_height(gdk_window)); setPassThrough(passthrough_); } -void waybar::Bar::setVisible(bool value) { - visible = value; +void waybar::Bar::setVisible(bool visible) { if (auto mode = config.get("mode", {}); mode.isString()) { setMode(visible ? config["mode"].asString() : MODE_INVISIBLE); } else { @@ -449,7 +448,17 @@ void waybar::Bar::setupAltFormatKeyForModuleList(const char* module_list_name) { Json::Value& modules = config[module_list_name]; for (const Json::Value& module_name : modules) { if (module_name.isString()) { - setupAltFormatKeyForModule(module_name.asString()); + auto ref = module_name.asString(); + if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) { + Json::Value& group_modules = config[ref]["modules"]; + for (const Json::Value& module_name : group_modules) { + if (module_name.isString()) { + setupAltFormatKeyForModule(module_name.asString()); + } + } + } else { + setupAltFormatKeyForModule(ref); + } } } } @@ -463,7 +472,7 @@ void waybar::Bar::handleSignal(int signal) { void waybar::Bar::getModules(const Factory& factory, const std::string& pos, waybar::Group* group = nullptr) { - auto module_list = group ? config[pos]["modules"] : config[pos]; + auto module_list = group != nullptr ? config[pos]["modules"] : config[pos]; if (module_list.isArray()) { for (const auto& name : module_list) { try { @@ -475,10 +484,10 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, auto id_name = ref.substr(6, hash_pos - 6); auto class_name = hash_pos != std::string::npos ? ref.substr(hash_pos + 1) : ""; - auto vertical = (group ? group->getBox().get_orientation() : box_.get_orientation()) == - Gtk::ORIENTATION_VERTICAL; + 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_module = new waybar::Group(id_name, class_name, config[ref], vertical); getModules(factory, ref, group_module); module = group_module; } else { @@ -487,7 +496,7 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, std::shared_ptr module_sp(module); modules_all_.emplace_back(module_sp); - if (group) { + if (group != nullptr) { group->addWidget(*module); } else { if (pos == "modules-left") { diff --git a/src/client.cpp b/src/client.cpp index 7c59dd5ee..5768b1b03 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "gtkmm/icontheme.h" #include "idle-inhibit-unstable-v1-client-protocol.h" @@ -11,13 +12,13 @@ #include "util/format.hpp" waybar::Client *waybar::Client::inst() { - static auto c = new Client(); + static auto *c = new Client(); return c; } void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { - auto client = static_cast(data); + auto *client = static_cast(data); if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) { client->xdg_output_manager = static_cast(wl_registry_bind( @@ -42,7 +43,7 @@ void waybar::Client::handleOutput(struct waybar_output &output) { .description = &handleOutputDescription, }; // owned by output->monitor; no need to destroy - auto wl_output = gdk_wayland_monitor_get_wl_output(output.monitor->gobj()); + auto *wl_output = gdk_wayland_monitor_get_wl_output(output.monitor->gobj()); output.xdg_output.reset(zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, wl_output)); zxdg_output_v1_add_listener(output.xdg_output.get(), &xdgOutputListener, &output); } @@ -61,7 +62,7 @@ std::vector waybar::Client::getOutputConfigs(struct waybar_output & } void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_output*/) { - auto client = waybar::Client::inst(); + auto *client = waybar::Client::inst(); try { auto &output = client->getOutput(data); /** @@ -85,24 +86,24 @@ void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_ } } } catch (const std::exception &e) { - std::cerr << e.what() << std::endl; + std::cerr << e.what() << '\n'; } } void waybar::Client::handleOutputName(void *data, struct zxdg_output_v1 * /*xdg_output*/, const char *name) { - auto client = waybar::Client::inst(); + auto *client = waybar::Client::inst(); try { auto &output = client->getOutput(data); output.name = name; } catch (const std::exception &e) { - std::cerr << e.what() << std::endl; + std::cerr << e.what() << '\n'; } } void waybar::Client::handleOutputDescription(void *data, struct zxdg_output_v1 * /*xdg_output*/, const char *description) { - auto client = waybar::Client::inst(); + auto *client = waybar::Client::inst(); try { auto &output = client->getOutput(data); const char *open_paren = strrchr(description, '('); @@ -111,13 +112,13 @@ void waybar::Client::handleOutputDescription(void *data, struct zxdg_output_v1 * size_t identifier_length = open_paren - description; output.identifier = std::string(description, identifier_length - 1); } catch (const std::exception &e) { - std::cerr << e.what() << std::endl; + std::cerr << e.what() << '\n'; } } void waybar::Client::handleMonitorAdded(Glib::RefPtr monitor) { auto &output = outputs_.emplace_back(); - output.monitor = monitor; + output.monitor = std::move(monitor); handleOutput(output); } @@ -154,15 +155,15 @@ const std::string waybar::Client::getStyle(const std::string &style, std::vector search_files; switch (appearance.value_or(portal->getAppearance())) { case waybar::Appearance::LIGHT: - search_files.push_back("style-light.css"); + search_files.emplace_back("style-light.css"); break; case waybar::Appearance::DARK: - search_files.push_back("style-dark.css"); + search_files.emplace_back("style-dark.css"); break; case waybar::Appearance::UNKNOWN: break; } - search_files.push_back("style.css"); + search_files.emplace_back("style.css"); css_file = Config::findConfigPath(search_files); } else { css_file = style; @@ -196,7 +197,7 @@ void waybar::Client::bindInterfaces() { wl_registry_add_listener(registry, ®istry_listener, this); wl_display_roundtrip(wl_display); - if (!gtk_layer_is_supported()) { + if (gtk_layer_is_supported() == 0) { throw std::runtime_error("The Wayland compositor does not support wlr-layer-shell protocol"); } @@ -233,11 +234,11 @@ int waybar::Client::main(int argc, char *argv[]) { return 1; } if (show_help) { - std::cout << cli << std::endl; + std::cout << cli << '\n'; return 0; } if (show_version) { - std::cout << "Waybar v" << VERSION << std::endl; + std::cout << "Waybar v" << VERSION << '\n'; return 0; } if (!log_level.empty()) { diff --git a/src/config.cpp b/src/config.cpp index 45f5ee38b..b78af56c3 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -21,10 +21,10 @@ const std::vector Config::CONFIG_DIRS = { const char *Config::CONFIG_PATH_ENV = "WAYBAR_CONFIG_DIR"; -std::optional tryExpandPath(const std::string base, const std::string filename) { +std::optional tryExpandPath(const std::string &base, const std::string &filename) { fs::path path; - if (filename != "") { + if (!filename.empty()) { path = fs::path(base) / fs::path(filename); } else { path = fs::path(base); @@ -129,9 +129,9 @@ bool isValidOutput(const Json::Value &config, const std::string &name, if (config_output.substr(0, 1) == "!") { if (config_output.substr(1) == name || config_output.substr(1) == identifier) { return false; - } else { - continue; } + + continue; } if (config_output == name || config_output == identifier) { return true; @@ -142,7 +142,9 @@ bool isValidOutput(const Json::Value &config, const std::string &name, } } return false; - } else if (config["output"].isString()) { + } + + if (config["output"].isString()) { auto config_output = config["output"].asString(); if (!config_output.empty()) { if (config_output.substr(0, 1) == "!") { @@ -162,6 +164,7 @@ void Config::load(const std::string &config) { } config_file_ = file.value(); spdlog::info("Using configuration file {}", config_file_); + config_ = Json::Value(); setupConfig(config_, config_file_, 0); } diff --git a/src/factory.cpp b/src/factory.cpp index 6b709f339..ca10ef956 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -28,6 +28,7 @@ #endif #ifdef HAVE_DWL #include "modules/dwl/tags.hpp" +#include "modules/dwl/window.hpp" #endif #ifdef HAVE_HYPRLAND #include "modules/hyprland/language.hpp" @@ -69,7 +70,7 @@ #include "modules/gamemode.hpp" #endif #ifdef HAVE_UPOWER -#include "modules/upower/upower.hpp" +#include "modules/upower.hpp" #endif #ifdef HAVE_PIPEWIRE #include "modules/privacy/privacy.hpp" @@ -86,6 +87,7 @@ #endif #if defined(__linux__) #include "modules/bluetooth.hpp" +#include "modules/power_profiles_daemon.hpp" #endif #ifdef HAVE_LOGIND_INHIBITOR #include "modules/inhibitor.hpp" @@ -128,7 +130,7 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, #endif #ifdef HAVE_UPOWER if (ref == "upower") { - return new waybar::modules::upower::UPower(id, config_[name]); + return new waybar::modules::UPower(id, config_[name]); } #endif #ifdef HAVE_PIPEWIRE @@ -186,6 +188,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "dwl/tags") { return new waybar::modules::dwl::Tags(id, bar_, config_[name]); } + if (ref == "dwl/window") { + return new waybar::modules::dwl::Window(id, bar_, config_[name]); + } #endif #ifdef HAVE_HYPRLAND if (ref == "hyprland/window") { @@ -282,6 +287,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "bluetooth") { return new waybar::modules::Bluetooth(id, config_[name]); } + if (ref == "power-profiles-daemon") { + return new waybar::modules::PowerProfilesDaemon(id, config_[name]); + } #endif #ifdef HAVE_LOGIND_INHIBITOR if (ref == "inhibitor") { diff --git a/src/group.cpp b/src/group.cpp index 262cae656..c77f2d311 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -4,7 +4,7 @@ #include -#include "gdkmm/device.h" +#include "gtkmm/enums.h" #include "gtkmm/widget.h" namespace waybar { @@ -19,9 +19,9 @@ const Gtk::RevealerTransitionType getPreferredTransitionType(bool is_vertical) { if (is_vertical) { return Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_UP; - } else { - return Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_LEFT; } + + return Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_LEFT; } Group::Group(const std::string& name, const std::string& id, const Json::Value& config, @@ -78,30 +78,21 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& } else { box.pack_start(revealer); } - - addHoverHandlerTo(revealer); } -} -bool Group::handleMouseHover(GdkEventCrossing* const& e) { - switch (e->type) { - case GDK_ENTER_NOTIFY: - revealer.set_reveal_child(true); - break; - case GDK_LEAVE_NOTIFY: - revealer.set_reveal_child(false); - break; - default: - break; - } + event_box_.add(box); +} - return true; +bool Group::handleMouseEnter(GdkEventCrossing* const& e) { + box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + revealer.set_reveal_child(true); + return false; } -void Group::addHoverHandlerTo(Gtk::Widget& widget) { - widget.add_events(Gdk::EventMask::ENTER_NOTIFY_MASK | Gdk::EventMask::LEAVE_NOTIFY_MASK); - widget.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Group::handleMouseHover)); - widget.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Group::handleMouseHover)); +bool Group::handleMouseLeave(GdkEventCrossing* const& e) { + box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + revealer.set_reveal_child(false); + return false; } auto Group::update() -> void { @@ -113,17 +104,13 @@ Gtk::Box& Group::getBox() { return is_drawer ? (is_first_widget ? box : revealer void Group::addWidget(Gtk::Widget& widget) { getBox().pack_start(widget, false, false); - if (is_drawer) { - // Necessary because of GTK's hitbox detection - addHoverHandlerTo(widget); - if (!is_first_widget) { - widget.get_style_context()->add_class(add_class_to_drawer_children); - } + if (is_drawer && !is_first_widget) { + widget.get_style_context()->add_class(add_class_to_drawer_children); } is_first_widget = false; } -Group::operator Gtk::Widget&() { return box; } +Group::operator Gtk::Widget&() { return event_box_; } } // namespace waybar diff --git a/src/main.cpp b/src/main.cpp index ff446ffce..679c66d65 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,7 +13,8 @@ std::list reap; volatile bool reload; void* signalThread(void* args) { - int err, signum; + int err; + int signum; sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGCHLD); @@ -46,7 +47,7 @@ void* signalThread(void* args) { } } -void startSignalThread(void) { +void startSignalThread() { int err; sigset_t mask; sigemptyset(&mask); @@ -71,7 +72,7 @@ void startSignalThread(void) { int main(int argc, char* argv[]) { try { - auto client = waybar::Client::inst(); + auto* client = waybar::Client::inst(); std::signal(SIGUSR1, [](int /*signal*/) { for (auto& bar : waybar::Client::inst()->bars) { diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 9003db6e9..d87cc6129 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -181,7 +181,8 @@ static bool status_gt(const std::string& a, const std::string& b) { return false; } -const std::tuple waybar::modules::Battery::getInfos() { +std::tuple +waybar::modules::Battery::getInfos() { std::lock_guard guard(battery_list_mutex_); try { @@ -234,7 +235,7 @@ const std::tuple waybar::modules::Battery::g } // spdlog::info("{} {} {} {}", capacity,time,status,rate); - return {capacity, time / 60.0, status, rate}; + return {capacity, time / 60.0, status, rate, 0, 0.0F}; #elif defined(__linux__) uint32_t total_power = 0; // μW @@ -252,6 +253,10 @@ const std::tuple waybar::modules::Battery::g uint32_t time_to_full_now = 0; bool time_to_full_now_exists = false; + uint32_t largestDesignCapacity = 0; + uint16_t mainBatCycleCount = 0; + float mainBatHealthPercent = 0.0F; + std::string status = "Unknown"; for (auto const& item : batteries_) { auto bat = item.first; @@ -267,13 +272,6 @@ const std::tuple waybar::modules::Battery::g // Some battery will report current and charge in μA/μAh. // Scale these by the voltage to get μW/μWh. - uint32_t capacity = 0; - bool capacity_exists = false; - if (fs::exists(bat / "capacity")) { - capacity_exists = true; - std::ifstream(bat / "capacity") >> capacity; - } - uint32_t current_now = 0; bool current_now_exists = false; if (fs::exists(bat / "current_now")) { @@ -353,6 +351,43 @@ const std::tuple waybar::modules::Battery::g std::ifstream(bat / "energy_full_design") >> energy_full_design; } + uint16_t cycleCount = 0; + if (fs::exists(bat / "cycle_count")) { + std::ifstream(bat / "cycle_count") >> cycleCount; + } + if (charge_full_design >= largestDesignCapacity) { + largestDesignCapacity = charge_full_design; + + if (cycleCount > mainBatCycleCount) { + mainBatCycleCount = cycleCount; + } + + if (charge_full_exists && charge_full_design_exists) { + float batHealthPercent = ((float)charge_full / charge_full_design) * 100; + if (mainBatHealthPercent == 0.0F || batHealthPercent < mainBatHealthPercent) { + mainBatHealthPercent = batHealthPercent; + } + } else if (energy_full_exists && energy_full_design_exists) { + float batHealthPercent = ((float)energy_full / energy_full_design) * 100; + if (mainBatHealthPercent == 0.0F || batHealthPercent < mainBatHealthPercent) { + mainBatHealthPercent = batHealthPercent; + } + } + } + + uint32_t capacity = 0; + bool capacity_exists = false; + if (charge_now_exists && charge_full_exists && charge_full != 0) { + capacity_exists = true; + capacity = 100 * (uint64_t)charge_now / (uint64_t)charge_full; + } else if (energy_now_exists && energy_full_exists && energy_full != 0) { + capacity_exists = true; + capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; + } else if (fs::exists(bat / "capacity")) { + capacity_exists = true; + std::ifstream(bat / "capacity") >> capacity; + } + if (!voltage_now_exists) { if (power_now_exists && current_now_exists && current_now != 0) { voltage_now_exists = true; @@ -393,13 +428,7 @@ const std::tuple waybar::modules::Battery::g } if (!capacity_exists) { - if (charge_now_exists && charge_full_exists && charge_full != 0) { - capacity_exists = true; - capacity = 100 * (uint64_t)charge_now / (uint64_t)charge_full; - } else if (energy_now_exists && energy_full_exists && energy_full != 0) { - capacity_exists = true; - capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; - } else if (charge_now_exists && energy_full_exists && voltage_now_exists) { + if (charge_now_exists && energy_full_exists && voltage_now_exists) { if (!charge_full_exists && voltage_now != 0) { charge_full_exists = true; charge_full = 1000000 * (uint64_t)energy_full / (uint64_t)voltage_now; @@ -573,11 +602,12 @@ const std::tuple waybar::modules::Battery::g // still charging but not yet done if (cap == 100 && status == "Charging") status = "Full"; - return {cap, time_remaining, status, total_power / 1e6}; + return { + cap, time_remaining, status, total_power / 1e6, mainBatCycleCount, mainBatHealthPercent}; #endif } catch (const std::exception& e) { spdlog::error("Battery: {}", e.what()); - return {0, 0, "Unknown", 0}; + return {0, 0, "Unknown", 0, 0, 0.0f}; } } @@ -633,7 +663,7 @@ auto waybar::modules::Battery::update() -> void { return; } #endif - auto [capacity, time_remaining, status, power] = getInfos(); + auto [capacity, time_remaining, status, power, cycles, health] = getInfos(); if (status == "Unknown") { status = getAdapterStatus(capacity); } @@ -663,10 +693,11 @@ auto waybar::modules::Battery::update() -> void { } else if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text(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))); + label_.set_tooltip_text( + 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), + fmt::arg("health", fmt::format("{:.3}", health)))); } if (!old_status_.empty()) { label_.get_style_context()->remove_class(old_status_); @@ -687,7 +718,8 @@ auto waybar::modules::Battery::update() -> void { auto icons = std::vector{status + "-" + state, status, state}; label_.set_markup(fmt::format( fmt::runtime(format), fmt::arg("capacity", capacity), fmt::arg("power", power), - fmt::arg("icon", getIcon(capacity, icons)), fmt::arg("time", time_remaining_formatted))); + fmt::arg("icon", getIcon(capacity, icons)), fmt::arg("time", time_remaining_formatted), + fmt::arg("cycles", cycles), fmt::arg("health", fmt::format("{:.3}", health)))); } // Call parent update ALabel::update(); diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 80e4731b1..06475a2e5 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -98,30 +98,30 @@ waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& std::back_inserter(device_preference_), [](auto x) { return x.asString(); }); } - // NOTE: assumption made that the controller that is selected stays unchanged - // for duration of the module if (cur_controller_ = findCurController(); !cur_controller_) { if (config_["controller-alias"].isString()) { - spdlog::error("findCurController() failed: no bluetooth controller found with alias '{}'", - config_["controller-alias"].asString()); + spdlog::warn("no bluetooth controller found with alias '{}'", + config_["controller-alias"].asString()); } else { - spdlog::error("findCurController() failed: no bluetooth controller found"); + spdlog::warn("no bluetooth controller found"); } update(); } else { - // These calls only make sense if a controller could be found + // This call only make sense if a controller could be found findConnectedDevices(cur_controller_->path, connected_devices_); - g_signal_connect(manager_.get(), "interface-proxy-properties-changed", - G_CALLBACK(onInterfaceProxyPropertiesChanged), this); - g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), - this); - g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), - this); + } + + g_signal_connect(manager_.get(), "object-added", G_CALLBACK(onObjectAdded), this); + g_signal_connect(manager_.get(), "object-removed", G_CALLBACK(onObjectRemoved), this); + g_signal_connect(manager_.get(), "interface-proxy-properties-changed", + G_CALLBACK(onInterfaceProxyPropertiesChanged), this); + g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), this); + g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), + this); #ifdef WANT_RFKILL - rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); + rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); #endif - } dp.emit(); } @@ -282,6 +282,46 @@ auto waybar::modules::Bluetooth::update() -> void { ALabel::update(); } +auto waybar::modules::Bluetooth::onObjectAdded(GDBusObjectManager* manager, GDBusObject* object, + gpointer user_data) -> void { + ControllerInfo info; + Bluetooth* bt = static_cast(user_data); + + if (!bt->cur_controller_.has_value() && bt->getControllerProperties(object, info) && + (!bt->config_["controller-alias"].isString() || + bt->config_["controller-alias"].asString() == info.alias)) { + bt->cur_controller_ = std::move(info); + bt->dp.emit(); + } +} + +auto waybar::modules::Bluetooth::onObjectRemoved(GDBusObjectManager* manager, GDBusObject* object, + gpointer user_data) -> void { + Bluetooth* bt = static_cast(user_data); + GDBusProxy* proxy_controller; + + if (!bt->cur_controller_.has_value()) { + return; + } + + proxy_controller = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Adapter1")); + + if (proxy_controller != NULL) { + std::string object_path = g_dbus_object_get_object_path(object); + + if (object_path == bt->cur_controller_->path) { + bt->cur_controller_ = bt->findCurController(); + if (bt->cur_controller_.has_value()) { + bt->connected_devices_.clear(); + bt->findConnectedDevices(bt->cur_controller_->path, bt->connected_devices_); + } + bt->dp.emit(); + } + + g_object_unref(proxy_controller); + } +} + // NOTE: only for when the org.bluez.Battery1 interface is added/removed after/before a device is // connected/disconnected auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager* manager, @@ -292,11 +332,13 @@ auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager* m std::string object_path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(interface)); if (interface_name == "org.bluez.Battery1") { Bluetooth* bt = static_cast(user_data); - auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(), - [object_path](auto d) { return d.path == object_path; }); - if (device != bt->connected_devices_.end()) { - device->battery_percentage = bt->getDeviceBatteryPercentage(object); - bt->dp.emit(); + if (bt->cur_controller_.has_value()) { + auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(), + [object_path](auto d) { return d.path == object_path; }); + if (device != bt->connected_devices_.end()) { + device->battery_percentage = bt->getDeviceBatteryPercentage(object); + bt->dp.emit(); + } } } } @@ -309,6 +351,11 @@ auto waybar::modules::Bluetooth::onInterfaceProxyPropertiesChanged( std::string object_path = g_dbus_object_get_object_path(G_DBUS_OBJECT(object_proxy)); Bluetooth* bt = static_cast(user_data); + + if (!bt->cur_controller_.has_value()) { + return; + } + if (interface_name == "org.bluez.Adapter1") { if (object_path == bt->cur_controller_->path) { bt->getControllerProperties(G_DBUS_OBJECT(object_proxy), *bt->cur_controller_); diff --git a/src/modules/cava.cpp b/src/modules/cava.cpp index 072275462..431ce5f15 100644 --- a/src/modules/cava.cpp +++ b/src/modules/cava.cpp @@ -8,13 +8,7 @@ waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) char cfgPath[PATH_MAX]; cfgPath[0] = '\0'; - if (config_["cava_config"].isString()) { - std::string strPath{config_["cava_config"].asString()}; - const std::string fnd{"XDG_CONFIG_HOME"}; - const std::string::size_type npos{strPath.find("$" + fnd)}; - if (npos != std::string::npos) strPath.replace(npos, fnd.length() + 1, getenv(fnd.c_str())); - strcpy(cfgPath, strPath.data()); - } + if (config_["cava_config"].isString()) strcpy(cfgPath, config_["cava_config"].asString().data()); // Load cava config error_.length = 0; diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index b54a360f6..fe2c4c8fb 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -1,5 +1,6 @@ #include "modules/clock.hpp" +#include #include #include @@ -11,20 +12,22 @@ #ifdef HAVE_LANGINFO_1STDAY #include -#include + +#include #endif namespace fmt_lib = waybar::util::date::format; waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true), - locale_{std::locale(config_["locale"].isString() ? config_["locale"].asString() : "")}, - tlpFmt_{(config_["tooltip-format"].isString()) ? config_["tooltip-format"].asString() : ""}, - cldInTooltip_{tlpFmt_.find("{" + kCldPlaceholder + "}") != std::string::npos}, - tzInTooltip_{tlpFmt_.find("{" + kTZPlaceholder + "}") != std::string::npos}, + m_locale_{std::locale(config_["locale"].isString() ? config_["locale"].asString() : "")}, + m_tlpFmt_{(config_["tooltip-format"].isString()) ? config_["tooltip-format"].asString() : ""}, + m_tooltip_{new Gtk::Label()}, + cldInTooltip_{m_tlpFmt_.find("{" + kCldPlaceholder + "}") != std::string::npos}, + tzInTooltip_{m_tlpFmt_.find("{" + kTZPlaceholder + "}") != std::string::npos}, tzCurrIdx_{0}, - ordInTooltip_{tlpFmt_.find("{" + kOrdPlaceholder + "}") != std::string::npos} { - tlpText_ = tlpFmt_; + ordInTooltip_{m_tlpFmt_.find("{" + kOrdPlaceholder + "}") != std::string::npos} { + m_tlpText_ = m_tlpFmt_; if (config_["timezones"].isArray() && !config_["timezones"].empty()) { for (const auto& zone_name : config_["timezones"]) { @@ -87,7 +90,7 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) fmtMap_.insert({3, config_[kCldPlaceholder]["format"]["today"].asString()}); cldBaseDay_ = year_month_day{ - floor(zoned_time{current_zone(), system_clock::now()}.get_local_time())} + floor(zoned_time{local_zone(), system_clock::now()}.get_local_time())} .day(); } else fmtMap_.insert({3, "{}"}); @@ -115,6 +118,7 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) } else cldMonCols_ = 1; if (config_[kCldPlaceholder]["on-scroll"].isInt()) { + cldShift_ = config_[kCldPlaceholder]["on-scroll"].asInt(); event_box_.add_events(Gdk::LEAVE_NOTIFY_MASK); event_box_.signal_leave_notify_event().connect([this](GdkEventCrossing*) { cldCurrShift_ = months{0}; @@ -123,17 +127,28 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) } } + if (tooltipEnabled()) { + label_.set_has_tooltip(true); + label_.signal_query_tooltip().connect(sigc::mem_fun(*this, &Clock::query_tlp_cb)); + } + thread_ = [this] { dp.emit(); thread_.sleep_for(interval_ - system_clock::now().time_since_epoch() % interval_); }; } +bool waybar::modules::Clock::query_tlp_cb(int, int, bool, + const Glib::RefPtr& tooltip) { + tooltip->set_custom(*m_tooltip_.get()); + return true; +} + auto waybar::modules::Clock::update() -> void { - const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : current_zone(); + const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : local_zone(); const zoned_time now{tz, floor(system_clock::now())}; - label_.set_markup(fmt_lib::vformat(locale_, format_, fmt_lib::make_format_args(now))); + label_.set_markup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now))); if (tooltipEnabled()) { const year_month_day today{floor(now.get_local_time())}; @@ -146,16 +161,19 @@ auto waybar::modules::Clock::update() -> void { if (ordInTooltip_) ordText_ = get_ordinal_date(shiftedDay); if (tzInTooltip_ || cldInTooltip_ || ordInTooltip_) { // std::vformat doesn't support named arguments. - tlpText_ = std::regex_replace(tlpFmt_, std::regex("\\{" + kTZPlaceholder + "\\}"), tzText_); - tlpText_ = - std::regex_replace(tlpText_, std::regex("\\{" + kCldPlaceholder + "\\}"), cldText_); - tlpText_ = - std::regex_replace(tlpText_, std::regex("\\{" + kOrdPlaceholder + "\\}"), ordText_); + m_tlpText_ = + std::regex_replace(m_tlpFmt_, std::regex("\\{" + kTZPlaceholder + "\\}"), tzText_); + m_tlpText_ = + std::regex_replace(m_tlpText_, std::regex("\\{" + kCldPlaceholder + "\\}"), cldText_); + m_tlpText_ = + std::regex_replace(m_tlpText_, std::regex("\\{" + kOrdPlaceholder + "\\}"), ordText_); + } else { + m_tlpText_ = m_tlpFmt_; } - tlpText_ = fmt_lib::vformat(locale_, tlpText_, fmt_lib::make_format_args(shiftedNow)); - - label_.set_tooltip_markup(tlpText_); + m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(shiftedNow)); + m_tooltip_->set_markup(m_tlpText_); + label_.trigger_tooltip_query(); } ALabel::update(); @@ -167,9 +185,9 @@ auto waybar::modules::Clock::getTZtext(sys_seconds now) -> std::string { std::stringstream os; 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] : current_zone(); + const auto* tz = tzList_[tz_idx] != nullptr ? tzList_[tz_idx] : local_zone(); auto zt{zoned_time{tz, now}}; - os << fmt_lib::vformat(locale_, format_, fmt_lib::make_format_args(zt)) << '\n'; + os << fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(zt)) << '\n'; } return os.str(); @@ -187,13 +205,13 @@ auto cldGetWeekForLine(const year_month& ym, const weekday& firstdow, const unsi } auto getCalendarLine(const year_month_day& currDate, const year_month ym, const unsigned line, - const weekday& firstdow, const std::locale* const locale_) -> std::string { + const weekday& firstdow, const std::locale* const m_locale_) -> std::string { std::ostringstream os; switch (line) { // Print month and year title case 0: { - os << date::format(*locale_, "{:L%B %Y}", ym); + os << date::format(*m_locale_, "{:L%B %Y}", ym); break; } // Print weekday names title @@ -203,7 +221,7 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const Glib::ustring::size_type wdLen{0}; int clen{0}; do { - wdStr = date::format(*locale_, "{:L%a}", wd); + wdStr = date::format(*m_locale_, "{:L%a}", wd); clen = ustring_clen(wdStr); wdLen = wdStr.length(); while (clen > 2) { @@ -226,7 +244,7 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const os << std::string((wd - firstdow).count() * 3, ' '); if (currDate != ym / d) - os << date::format(*locale_, "{:L%e}", d); + os << date::format(*m_locale_, "{:L%e}", d); else os << "{today}"; @@ -234,7 +252,7 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const ++d; if (currDate != ym / d) - os << date::format(*locale_, " {:L%e}", d); + os << date::format(*m_locale_, " {:L%e}", d); else os << " {today}"; } @@ -249,13 +267,13 @@ auto getCalendarLine(const year_month_day& currDate, const year_month ym, const auto wd{firstdow}; if (currDate != ym / d) - os << date::format(*locale_, "{:L%e}", d); + os << date::format(*m_locale_, "{:L%e}", d); else os << "{today}"; while (++wd != firstdow && ++d <= dlast) { if (currDate != ym / d) - os << date::format(*locale_, " {:L%e}", d); + os << date::format(*m_locale_, " {:L%e}", d); else os << " {today}"; } @@ -325,7 +343,7 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea if (line > 1) { if (line < ml[(unsigned)ymTmp.month() - 1u]) { os << fmt_lib::vformat( - locale_, fmtMap_[4], + m_locale_, fmtMap_[4], fmt_lib::make_format_args( (line == 2) ? static_cast( @@ -341,7 +359,7 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea 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, &locale_)); + getCalendarLine(today, ymTmp, line, firstdow, &m_locale_)); // Week numbers on the right if (cldWPos_ == WS::RIGHT && line > 0) { @@ -349,7 +367,7 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea if (line < ml[(unsigned)ymTmp.month() - 1u]) os << ' ' << fmt_lib::vformat( - locale_, fmtMap_[4], + m_locale_, fmtMap_[4], fmt_lib::make_format_args( (line == 2) ? static_cast( zoned_seconds{tz, local_days{ymTmp / 1}}) @@ -365,7 +383,7 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea // Apply user's formats if (line < 2) tmp << fmt_lib::vformat( - locale_, fmtMap_[line], + m_locale_, fmtMap_[line], fmt_lib::make_format_args(static_cast(os.str()))); else tmp << os.str(); @@ -377,10 +395,10 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea } os << std::regex_replace( - fmt_lib::vformat(locale_, fmtMap_[2], + fmt_lib::vformat(m_locale_, fmtMap_[2], fmt_lib::make_format_args(static_cast(tmp.str()))), std::regex("\\{today\\}"), - fmt_lib::vformat(locale_, fmtMap_[3], + fmt_lib::vformat(m_locale_, fmtMap_[3], fmt_lib::make_format_args( static_cast(date::format("{:L%e}", d))))); @@ -392,6 +410,18 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea return os.str(); } +auto waybar::modules::Clock::local_zone() -> const time_zone* { + const char* tz_name = getenv("TZ"); + if (tz_name) { + try { + return locate_zone(tz_name); + } catch (const std::runtime_error& e) { + spdlog::warn("Timezone: {0}. {1}", tz_name, e.what()); + } + } + return current_zone(); +} + // Actions handler auto waybar::modules::Clock::doAction(const std::string& name) -> void { if (actionMap_[name]) { @@ -405,11 +435,12 @@ void waybar::modules::Clock::cldModeSwitch() { cldMode_ = (cldMode_ == CldMode::YEAR) ? CldMode::MONTH : CldMode::YEAR; } void waybar::modules::Clock::cldShift_up() { - cldCurrShift_ += (months)((cldMode_ == CldMode::YEAR) ? 12 : 1); + cldCurrShift_ += (months)((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_; } void waybar::modules::Clock::cldShift_down() { - cldCurrShift_ -= (months)((cldMode_ == CldMode::YEAR) ? 12 : 1); + cldCurrShift_ -= (months)((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_; } +void waybar::modules::Clock::cldShift_reset() { cldCurrShift_ = (months)0; } void waybar::modules::Clock::tz_up() { const auto tzSize{tzList_.size()}; if (tzSize == 1) return; @@ -434,7 +465,7 @@ using deleting_unique_ptr = std::unique_ptr>; auto waybar::modules::Clock::first_day_of_week() -> weekday { #ifdef HAVE_LANGINFO_1STDAY deleting_unique_ptr::type, freelocale> posix_locale{ - newlocale(LC_ALL, locale_.name().c_str(), nullptr)}; + newlocale(LC_ALL, m_locale_.name().c_str(), nullptr)}; if (posix_locale) { const auto i{(int)((std::intptr_t)nl_langinfo_l(_NL_TIME_WEEK_1STDAY, posix_locale.get()))}; const weekday wd{year_month_day{year(i / 10000) / month(i / 100 % 100) / day(i % 100)}}; @@ -468,4 +499,4 @@ auto waybar::modules::Clock::get_ordinal_date(const year_month_day& today) -> st res << "th"; } return res.str(); -} \ No newline at end of file +} diff --git a/src/modules/cpu_frequency/bsd.cpp b/src/modules/cpu_frequency/bsd.cpp index 31165fa57..743fb2881 100644 --- a/src/modules/cpu_frequency/bsd.cpp +++ b/src/modules/cpu_frequency/bsd.cpp @@ -1,5 +1,4 @@ #include - #include #include "modules/cpu_frequency.hpp" diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 5e5d70193..20d8d9348 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -10,6 +10,7 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, name_(name), output_name_(output_name), id_(id), + tooltip_format_enabled_{config_["tooltip-format"].isString()}, percentage_(0), fp_(nullptr), pid_(-1) { @@ -161,21 +162,21 @@ auto waybar::modules::Custom::update() -> void { auto str = fmt::format(fmt::runtime(format_), text_, fmt::arg("alt", alt_), fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); - if (str.empty()) { + if ((config_["hide-empty-text"].asBool() && text_.empty()) || str.empty()) { event_box_.hide(); } else { label_.set_markup(str); if (tooltipEnabled()) { - if (text_ == tooltip_) { - if (label_.get_tooltip_markup() != str) { - label_.set_tooltip_markup(str); - } - } else if (config_["tooltip-format"].isString()) { + if (tooltip_format_enabled_) { auto tooltip = config_["tooltip-format"].asString(); tooltip = fmt::format(fmt::runtime(tooltip), text_, fmt::arg("alt", alt_), fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); label_.set_tooltip_markup(tooltip); + } else if (text_ == tooltip_) { + if (label_.get_tooltip_markup() != str) { + label_.set_tooltip_markup(str); + } } else { if (label_.get_tooltip_markup() != tooltip_) { label_.set_tooltip_markup(tooltip_); @@ -214,13 +215,19 @@ void waybar::modules::Custom::parseOutputRaw() { if (i == 0) { if (config_["escape"].isBool() && config_["escape"].asBool()) { text_ = Glib::Markup::escape_text(validated_line); + tooltip_ = Glib::Markup::escape_text(validated_line); } else { text_ = validated_line; + tooltip_ = validated_line; } tooltip_ = validated_line; class_.clear(); } else if (i == 1) { - tooltip_ = validated_line; + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(validated_line); + } else { + tooltip_ = validated_line; + } } else if (i == 2) { class_.push_back(validated_line); } else { @@ -246,7 +253,11 @@ void waybar::modules::Custom::parseOutputJson() { } else { alt_ = parsed["alt"].asString(); } - tooltip_ = parsed["tooltip"].asString(); + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString()); + } else { + tooltip_ = parsed["tooltip"].asString(); + } if (parsed["class"].isString()) { class_.push_back(parsed["class"].asString()); } else if (parsed["class"].isArray()) { diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp index f36ece1d0..085b82246 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -21,11 +21,11 @@ wl_array tags, layouts; static uint num_tags = 0; -void toggle_visibility(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { +static void toggle_visibility(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { // Intentionally empty } -void active(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t active) { +static void active(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t active) { // Intentionally empty } @@ -37,15 +37,15 @@ static void set_tag(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t tag : num_tags & ~(1 << tag); } -void set_layout_symbol(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *layout) { +static void set_layout_symbol(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *layout) { // Intentionally empty } -void title(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *title) { +static void title(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *title) { // Intentionally empty } -void dwl_frame(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { +static void dwl_frame(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { // Intentionally empty } @@ -97,6 +97,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con output_status_{nullptr} { struct wl_display *display = Client::inst()->wl_display; struct wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener_impl, this); wl_display_roundtrip(display); @@ -155,6 +156,9 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con } Tags::~Tags() { + if (output_status_) { + zdwl_ipc_output_v2_destroy(output_status_); + } if (status_manager_) { zdwl_ipc_manager_v2_destroy(status_manager_); } diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp new file mode 100644 index 000000000..870d87e4e --- /dev/null +++ b/src/modules/dwl/window.cpp @@ -0,0 +1,120 @@ +#include "modules/dwl/window.hpp" + +#include +#include +#include +#include +#include +#include + +#include "client.hpp" +#include "dwl-ipc-unstable-v2-client-protocol.h" +#include "util/rewrite_string.hpp" + +namespace waybar::modules::dwl { + +static void toggle_visibility(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { + // Intentionally empty +} + +static void active(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t active) { + // Intentionally empty +} + +static void set_tag(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t tag, uint32_t state, + uint32_t clients, uint32_t focused) { + // Intentionally empty +} + +static void set_layout_symbol(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *layout) { + static_cast(data)->handle_layout_symbol(layout); +} + +static void title(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *title) { + static_cast(data)->handle_title(title); +} + +static void dwl_frame(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { + static_cast(data)->handle_frame(); +} + +static void set_layout(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t layout) { + static_cast(data)->handle_layout(layout); +} + +static void appid(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *appid) { + static_cast(data)->handle_appid(appid); +}; + +static const zdwl_ipc_output_v2_listener output_status_listener_impl{ + .toggle_visibility = toggle_visibility, + .active = active, + .tag = set_tag, + .layout = set_layout, + .title = title, + .appid = appid, + .layout_symbol = set_layout_symbol, + .frame = dwl_frame, +}; + +static void handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) { + if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) { + static_cast(data)->status_manager_ = static_cast( + (zdwl_ipc_manager_v2 *)wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1)); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { + /* Ignore event */ +} + +static const wl_registry_listener registry_listener_impl = {.global = handle_global, + .global_remove = handle_global_remove}; + +Window::Window(const std::string &id, const Bar &bar, const Json::Value &config) + : AAppIconLabel(config, "window", id, "{}", 0, true), bar_(bar) { + struct wl_display *display = Client::inst()->wl_display; + struct wl_registry *registry = wl_display_get_registry(display); + + wl_registry_add_listener(registry, ®istry_listener_impl, this); + wl_display_roundtrip(display); + + if (status_manager_ == nullptr) { + spdlog::error("dwl_status_manager_v2 not advertised"); + return; + } + + struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + output_status_ = zdwl_ipc_manager_v2_get_output(status_manager_, output); + zdwl_ipc_output_v2_add_listener(output_status_, &output_status_listener_impl, this); + zdwl_ipc_manager_v2_destroy(status_manager_); +} + +Window::~Window() { + if (output_status_ != nullptr) { + zdwl_ipc_output_v2_destroy(output_status_); + } +} + +void Window::handle_title(const char *title) { title_ = title; } + +void Window::handle_appid(const char *appid) { appid_ = appid; } + +void Window::handle_layout_symbol(const char *layout_symbol) { layout_symbol_ = layout_symbol; } + +void Window::handle_layout(const uint32_t layout) { layout_ = layout; } + +void Window::handle_frame() { + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", title_), + fmt::arg("layout", layout_symbol_), fmt::arg("app_id", appid_)), + config_["rewrite"])); + updateAppIconName(appid_, ""); + updateAppIcon(); + if (tooltipEnabled()) { + label_.set_tooltip_text(title_); + } +} + +} // namespace waybar::modules::dwl diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 3c8313fc7..8ec6eddac 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -9,11 +9,38 @@ #include #include +#include #include #include namespace waybar::modules::hyprland { +std::filesystem::path IPC::socketFolder_; + +std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { + // socket path, specified by EventManager of Hyprland + if (!socketFolder_.empty()) { + return socketFolder_; + } + + const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR"); + std::filesystem::path xdgRuntimeDir; + // Only set path if env variable is set + if (xdgRuntimeDirEnv != nullptr) { + xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv); + } + + if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) { + socketFolder_ = xdgRuntimeDir / "hypr"; + } else { + spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr"); + socketFolder_ = std::filesystem::path("/tmp") / "hypr"; + } + + socketFolder_ = socketFolder_ / instanceSig; + return socketFolder_; +} + void IPC::startIPC() { // will start IPC and relay events to parseIPC @@ -40,9 +67,7 @@ void IPC::startIPC() { addr.sun_family = AF_UNIX; - // socket path, specified by EventManager of Hyprland - std::string socketPath = "/tmp/hypr/" + std::string(his) + "/.socket2.sock"; - + auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock"; strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); addr.sun_path[sizeof(addr.sun_path) - 1] = 0; @@ -54,22 +79,29 @@ void IPC::startIPC() { return; } - auto file = fdopen(socketfd, "r"); + auto* file = fdopen(socketfd, "r"); while (true) { - char buffer[1024]; // Hyprland socket2 events are max 1024 bytes + std::array buffer; // Hyprland socket2 events are max 1024 bytes - auto recievedCharPtr = fgets(buffer, 1024, file); + auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file); - if (!recievedCharPtr) { + if (receivedCharPtr == nullptr) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } - std::string messageRecieved(buffer); - messageRecieved = messageRecieved.substr(0, messageRecieved.find_first_of('\n')); - spdlog::debug("hyprland IPC received {}", messageRecieved); - parseIPC(messageRecieved); + std::string messageReceived(buffer.data()); + messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n')); + spdlog::debug("hyprland IPC received {}", messageReceived); + + try { + parseIPC(messageReceived); + } catch (std::exception& e) { + spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what()); + } catch (...) { + throw; + } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } @@ -78,9 +110,9 @@ void IPC::startIPC() { void IPC::parseIPC(const std::string& ev) { std::string request = ev.substr(0, ev.find_first_of('>')); - std::unique_lock lock(m_callbackMutex); + std::unique_lock lock(callbackMutex_); - for (auto& [eventname, handler] : m_callbacks) { + for (auto& [eventname, handler] : callbacks_) { if (eventname == request) { handler->onEvent(ev); } @@ -88,25 +120,25 @@ void IPC::parseIPC(const std::string& ev) { } void IPC::registerForIPC(const std::string& ev, EventHandler* ev_handler) { - if (!ev_handler) { + if (ev_handler == nullptr) { return; } - std::unique_lock lock(m_callbackMutex); - m_callbacks.emplace_back(ev, ev_handler); + std::unique_lock lock(callbackMutex_); + callbacks_.emplace_back(ev, ev_handler); } void IPC::unregisterForIPC(EventHandler* ev_handler) { - if (!ev_handler) { + if (ev_handler == nullptr) { return; } - std::unique_lock lock(m_callbackMutex); + std::unique_lock lock(callbackMutex_); - for (auto it = m_callbacks.begin(); it != m_callbacks.end();) { + for (auto it = callbacks_.begin(); it != callbacks_.end();) { auto& [eventname, handler] = *it; if (handler == ev_handler) { - m_callbacks.erase(it++); + callbacks_.erase(it++); } else { ++it; } @@ -135,19 +167,17 @@ std::string IPC::getSocket1Reply(const std::string& rq) { } // get the instance signature - auto instanceSig = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + auto* instanceSig = getenv("HYPRLAND_INSTANCE_SIGNATURE"); - if (!instanceSig) { + if (instanceSig == nullptr) { spdlog::error("Hyprland IPC: HYPRLAND_INSTANCE_SIGNATURE was not set! (Is Hyprland running?)"); return ""; } - std::string instanceSigStr = std::string(instanceSig); - sockaddr_un serverAddress = {0}; serverAddress.sun_family = AF_UNIX; - std::string socketPath = "/tmp/hypr/" + instanceSigStr + "/.socket.sock"; + std::string socketPath = IPC::getSocketFolder(instanceSig) / ".socket.sock"; // Use snprintf to copy the socketPath string into serverAddress.sun_path if (snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()) < @@ -169,18 +199,18 @@ std::string IPC::getSocket1Reply(const std::string& rq) { return ""; } - char buffer[8192] = {0}; + std::array buffer = {0}; std::string response; do { - sizeWritten = read(serverSocket, buffer, 8192); + sizeWritten = read(serverSocket, buffer.data(), 8192); if (sizeWritten < 0) { spdlog::error("Hyprland IPC: Couldn't read (5)"); close(serverSocket); return ""; } - response.append(buffer, sizeWritten); + response.append(buffer.data(), sizeWritten); } while (sizeWritten > 0); close(serverSocket); @@ -188,7 +218,13 @@ std::string IPC::getSocket1Reply(const std::string& rq) { } Json::Value IPC::getSocket1JsonReply(const std::string& rq) { - return m_parser.parse(getSocket1Reply("j/" + rq)); + std::string reply = getSocket1Reply("j/" + rq); + + if (reply.empty()) { + return {}; + } + + return parser_.parse(reply); } } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 5339ee9e0..d86393af5 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -13,7 +13,7 @@ Language::Language(const std::string& id, const Bar& bar, const Json::Value& con : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { modulesReady = true; - if (!gIPC.get()) { + if (!gIPC) { gIPC = std::make_unique(); } @@ -36,6 +36,11 @@ Language::~Language() { auto Language::update() -> void { std::lock_guard lg(mutex_); + spdlog::debug("hyprland language update with full name {}", layout_.full_name); + spdlog::debug("hyprland language update with short name {}", layout_.short_name); + spdlog::debug("hyprland language update with short description {}", layout_.short_description); + spdlog::debug("hyprland language update with variant {}", layout_.variant); + std::string layoutName = std::string{}; if (config_.isMember("format-" + layout_.short_description + "-" + layout_.variant)) { const auto propName = "format-" + layout_.short_description + "-" + layout_.variant; @@ -50,6 +55,8 @@ auto Language::update() -> void { fmt::arg("variant", layout_.variant))); } + spdlog::debug("hyprland language formatted layout name {}", layoutName); + if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); @@ -63,7 +70,7 @@ 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_first_of(',') + 1); + auto layoutName = ev.substr(ev.find_last_of(',') + 1); if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) return; // ignore @@ -102,11 +109,11 @@ void Language::initLanguage() { } auto Language::getLayout(const std::string& fullName) -> Layout { - const auto CONTEXT = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); - rxkb_context_parse_default_ruleset(CONTEXT); + auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); + rxkb_context_parse_default_ruleset(context); - rxkb_layout* layout = rxkb_layout_first(CONTEXT); - while (layout) { + rxkb_layout* layout = rxkb_layout_first(context); + while (layout != nullptr) { std::string nameOfLayout = rxkb_layout_get_description(layout); if (nameOfLayout != fullName) { @@ -115,21 +122,20 @@ auto Language::getLayout(const std::string& fullName) -> Layout { } auto name = std::string(rxkb_layout_get_name(layout)); - auto variant_ = rxkb_layout_get_variant(layout); - std::string variant = variant_ == nullptr ? "" : std::string(variant_); + const auto* variantPtr = rxkb_layout_get_variant(layout); + std::string variant = variantPtr == nullptr ? "" : std::string(variantPtr); - auto short_description_ = rxkb_layout_get_brief(layout); - std::string short_description = - short_description_ == nullptr ? "" : std::string(short_description_); + const auto* descriptionPtr = rxkb_layout_get_brief(layout); + std::string description = descriptionPtr == nullptr ? "" : std::string(descriptionPtr); - Layout info = Layout{nameOfLayout, name, variant, short_description}; + Layout info = Layout{nameOfLayout, name, variant, description}; - rxkb_context_unref(CONTEXT); + rxkb_context_unref(context); return info; } - rxkb_context_unref(CONTEXT); + rxkb_context_unref(context); spdlog::debug("hyprland language didn't find matching layout"); diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp index 9f2a98297..96677d127 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -10,13 +10,22 @@ Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "submap", id, "{}", 0, true), bar_(bar) { modulesReady = true; - if (!gIPC.get()) { + parseConfig(config); + + if (!gIPC) { gIPC = std::make_unique(); } label_.hide(); ALabel::update(); + // Displays widget immediately if always_on_ assuming default submap + // Needs an actual way to retrive current submap on startup + if (always_on_) { + submap_ = default_submap_; + label_.get_style_context()->add_class(submap_); + } + // register for hyprland ipc gIPC->registerForIPC("submap", this); dp.emit(); @@ -28,6 +37,18 @@ Submap::~Submap() { std::lock_guard lg(mutex_); } +auto Submap::parseConfig(const Json::Value& config) -> void { + auto const& alwaysOn = config["always-on"]; + if (alwaysOn.isBool()) { + always_on_ = alwaysOn.asBool(); + } + + auto const& defaultSubmap = config["default-submap"]; + if (defaultSubmap.isString()) { + default_submap_ = defaultSubmap.asString(); + } +} + auto Submap::update() -> void { std::lock_guard lg(mutex_); @@ -60,6 +81,10 @@ void Submap::onEvent(const std::string& ev) { submap_ = submapName; + if (submap_.empty() && always_on_) { + submap_ = default_submap_; + } + label_.get_style_context()->add_class(submap_); spdlog::debug("hyprland submap onevent with {}", submap_); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index fca8b9ab5..905c57e62 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -17,9 +17,9 @@ namespace waybar::modules::hyprland { Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) { modulesReady = true; - separate_outputs = config["separate-outputs"].asBool(); + separateOutputs_ = config["separate-outputs"].asBool(); - if (!gIPC.get()) { + if (!gIPC) { gIPC = std::make_unique(); } @@ -45,41 +45,47 @@ auto Window::update() -> void { // fix ampersands std::lock_guard lg(mutex_); - std::string window_name = waybar::util::sanitize_string(workspace_.last_window_title); - std::string window_address = workspace_.last_window; + std::string windowName = waybar::util::sanitize_string(workspace_.last_window_title); + std::string windowAddress = workspace_.last_window; - window_data_.title = window_name; + windowData_.title = windowName; if (!format_.empty()) { label_.show(); label_.set_markup(waybar::util::rewriteString( - fmt::format(fmt::runtime(format_), fmt::arg("title", window_name), - fmt::arg("initialTitle", window_data_.initial_title), - fmt::arg("class", window_data_.class_name), - fmt::arg("initialClass", window_data_.initial_class_name)), + fmt::format(fmt::runtime(format_), fmt::arg("title", windowName), + fmt::arg("initialTitle", windowData_.initial_title), + fmt::arg("class", windowData_.class_name), + fmt::arg("initialClass", windowData_.initial_class_name)), config_["rewrite"])); } else { label_.hide(); } + if (focused_) { + image_.show(); + } else { + image_.hide(); + } + setClass("empty", workspace_.windows == 0); setClass("solo", solo_); - setClass("floating", all_floating_); + setClass("floating", allFloating_); setClass("swallowing", swallowing_); setClass("fullscreen", fullscreen_); - if (!last_solo_class_.empty() && solo_class_ != last_solo_class_) { - if (bar_.window.get_style_context()->has_class(last_solo_class_)) { - bar_.window.get_style_context()->remove_class(last_solo_class_); - spdlog::trace("Removing solo class: {}", last_solo_class_); + if (!lastSoloClass_.empty() && soloClass_ != lastSoloClass_) { + if (bar_.window.get_style_context()->has_class(lastSoloClass_)) { + bar_.window.get_style_context()->remove_class(lastSoloClass_); + spdlog::trace("Removing solo class: {}", lastSoloClass_); } } - if (!solo_class_.empty() && solo_class_ != last_solo_class_) { - bar_.window.get_style_context()->add_class(solo_class_); - spdlog::trace("Adding solo class: {}", solo_class_); + if (!soloClass_.empty() && soloClass_ != lastSoloClass_) { + bar_.window.get_style_context()->add_class(soloClass_); + spdlog::trace("Adding solo class: {}", soloClass_); } - last_solo_class_ = solo_class_; + lastSoloClass_ = soloClass_; AAppIconLabel::update(); } @@ -109,8 +115,12 @@ auto Window::getActiveWorkspace(const std::string& monitorName = "") -> Workspac } auto Window::Workspace::parse(const Json::Value& value) -> Window::Workspace { - return Workspace{value["id"].asInt(), value["windows"].asInt(), value["lastwindow"].asString(), - value["lastwindowtitle"].asString()}; + return Workspace{ + value["id"].asInt(), + value["windows"].asInt(), + value["lastwindow"].asString(), + value["lastwindowtitle"].asString(), + }; } auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData { @@ -123,42 +133,45 @@ auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData { void Window::queryActiveWorkspace() { std::lock_guard lg(mutex_); - if (separate_outputs) { + if (separateOutputs_) { workspace_ = getActiveWorkspace(this->bar_.output->name); } else { workspace_ = getActiveWorkspace(); } + focused_ = true; if (workspace_.windows > 0) { const auto clients = gIPC->getSocket1JsonReply("clients"); assert(clients.isArray()); - auto active_window = std::find_if(clients.begin(), clients.end(), [&](Json::Value window) { + auto activeWindow = std::find_if(clients.begin(), clients.end(), [&](Json::Value window) { return window["address"] == workspace_.last_window; }); - if (active_window == std::end(clients)) { + + if (activeWindow == std::end(clients)) { + focused_ = false; return; } - window_data_ = WindowData::parse(*active_window); - updateAppIconName(window_data_.class_name, window_data_.initial_class_name); - std::vector workspace_windows; - std::copy_if(clients.begin(), clients.end(), std::back_inserter(workspace_windows), + windowData_ = WindowData::parse(*activeWindow); + updateAppIconName(windowData_.class_name, windowData_.initial_class_name); + std::vector workspaceWindows; + std::copy_if(clients.begin(), clients.end(), std::back_inserter(workspaceWindows), [&](Json::Value window) { return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); }); swallowing_ = - std::any_of(workspace_windows.begin(), workspace_windows.end(), [&](Json::Value window) { + std::any_of(workspaceWindows.begin(), workspaceWindows.end(), [&](Json::Value window) { return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; }); - std::vector visible_windows; - std::copy_if(workspace_windows.begin(), workspace_windows.end(), - std::back_inserter(visible_windows), + std::vector visibleWindows; + std::copy_if(workspaceWindows.begin(), workspaceWindows.end(), + std::back_inserter(visibleWindows), [&](Json::Value window) { return !window["hidden"].asBool(); }); - solo_ = 1 == std::count_if(visible_windows.begin(), visible_windows.end(), + solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(), [&](Json::Value window) { return !window["floating"].asBool(); }); - all_floating_ = std::all_of(visible_windows.begin(), visible_windows.end(), - [&](Json::Value window) { return window["floating"].asBool(); }); - fullscreen_ = window_data_.fullscreen; + allFloating_ = std::all_of(visibleWindows.begin(), visibleWindows.end(), + [&](Json::Value window) { return window["floating"].asBool(); }); + fullscreen_ = windowData_.fullscreen; // Fullscreen windows look like they are solo if (fullscreen_) { @@ -166,23 +179,24 @@ void Window::queryActiveWorkspace() { } // Grouped windows have a tab bar and therefore don't look fullscreen or solo - if (window_data_.grouped) { + if (windowData_.grouped) { fullscreen_ = false; solo_ = false; } if (solo_) { - solo_class_ = window_data_.class_name; + soloClass_ = windowData_.class_name; } else { - solo_class_ = ""; + soloClass_ = ""; } } else { - window_data_ = WindowData{}; - all_floating_ = false; + focused_ = false; + windowData_ = WindowData{}; + allFloating_ = false; swallowing_ = false; fullscreen_ = false; solo_ = false; - solo_class_ = ""; + soloClass_ = ""; } } diff --git a/src/modules/hyprland/windowcreationpayload.cpp b/src/modules/hyprland/windowcreationpayload.cpp new file mode 100644 index 000000000..df7fe784e --- /dev/null +++ b/src/modules/hyprland/windowcreationpayload.cpp @@ -0,0 +1,108 @@ +#include "modules/hyprland/windowcreationpayload.hpp" + +#include +#include + +#include +#include +#include + +#include "modules/hyprland/workspaces.hpp" + +namespace waybar::modules::hyprland { + +WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data) + : m_window(std::make_pair(client_data["class"].asString(), client_data["title"].asString())), + m_windowAddress(client_data["address"].asString()), + m_workspaceName(client_data["workspace"]["name"].asString()) { + clearAddr(); + clearWorkspaceName(); +} + +WindowCreationPayload::WindowCreationPayload(std::string workspace_name, + WindowAddress window_address, std::string window_repr) + : m_window(std::move(window_repr)), + m_windowAddress(std::move(window_address)), + m_workspaceName(std::move(workspace_name)) { + clearAddr(); + clearWorkspaceName(); +} + +WindowCreationPayload::WindowCreationPayload(std::string workspace_name, + WindowAddress window_address, std::string window_class, + std::string window_title) + : 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)) { + clearAddr(); + clearWorkspaceName(); +} + +void WindowCreationPayload::clearAddr() { + // substr(2, ...) is necessary because Hyprland's JSON follows this format: + // 0x{ADDR} + // While Hyprland's IPC follows this format: + // {ADDR} + static const std::string ADDR_PREFIX = "0x"; + static const int ADDR_PREFIX_LEN = ADDR_PREFIX.length(); + + if (m_windowAddress.starts_with(ADDR_PREFIX)) { + m_windowAddress = + m_windowAddress.substr(ADDR_PREFIX_LEN, m_windowAddress.length() - ADDR_PREFIX_LEN); + } +} + +void WindowCreationPayload::clearWorkspaceName() { + // The workspace name may optionally feature "special:" at the beginning. + // If so, we need to remove it because the workspace is saved WITHOUT the + // special qualifier. The reasoning is that not all of Hyprland's IPC events + // use this qualifier, so it's better to be consistent about our uses. + + static const std::string SPECIAL_QUALIFIER_PREFIX = "special:"; + static const int SPECIAL_QUALIFIER_PREFIX_LEN = SPECIAL_QUALIFIER_PREFIX.length(); + + if (m_workspaceName.starts_with(SPECIAL_QUALIFIER_PREFIX)) { + m_workspaceName = m_workspaceName.substr( + SPECIAL_QUALIFIER_PREFIX_LEN, m_workspaceName.length() - SPECIAL_QUALIFIER_PREFIX_LEN); + } + + std::size_t spaceFound = m_workspaceName.find(' '); + if (spaceFound != std::string::npos) { + m_workspaceName.erase(m_workspaceName.begin() + spaceFound, m_workspaceName.end()); + } +} + +bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) { + if (std::holds_alternative(m_window)) { + return std::get(m_window).empty(); + } + if (std::holds_alternative(m_window)) { + auto [window_class, window_title] = std::get(m_window); + return (window_class.empty() && + (!workspace_manager.windowRewriteConfigUsesTitle() || window_title.empty())); + } + // Unreachable + spdlog::error("WorkspaceWindow::isEmpty: Unreachable"); + throw std::runtime_error("WorkspaceWindow::isEmpty: Unreachable"); +} + +int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; } + +void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { + m_workspaceName = new_workspace_name; +} + +std::string 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); + } + // Unreachable + spdlog::error("WorkspaceWindow::repr: Unreachable"); + throw std::runtime_error("WorkspaceWindow::repr: Unreachable"); +} + +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp new file mode 100644 index 000000000..eac21b7ed --- /dev/null +++ b/src/modules/hyprland/workspace.cpp @@ -0,0 +1,215 @@ +#include +#include + +#include +#include +#include + +#include "modules/hyprland/workspaces.hpp" + +namespace waybar::modules::hyprland { + +Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_manager, + const Json::Value &clients_data) + : m_workspaceManager(workspace_manager), + m_id(workspace_data["id"].asInt()), + m_name(workspace_data["name"].asString()), + m_output(workspace_data["monitor"].asString()), // TODO:allow using monitor desc + m_windows(workspace_data["windows"].asInt()), + m_isActive(true), + m_isPersistentRule(workspace_data["persistent-rule"].asBool()), + m_isPersistentConfig(workspace_data["persistent-config"].asBool()) { + if (m_name.starts_with("name:")) { + m_name = m_name.substr(5); + } else if (m_name.starts_with("special")) { + m_name = m_id == -99 ? m_name : m_name.substr(8); + m_isSpecial = true; + } + + m_button.add_events(Gdk::BUTTON_PRESS_MASK); + m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), + false); + + m_button.set_relief(Gtk::RELIEF_NONE); + m_content.set_center_widget(m_label); + m_button.add(m_content); + + initializeWindowMap(clients_data); +} + +void addOrRemoveClass(const Glib::RefPtr &context, bool condition, + const std::string &class_name) { + if (condition) { + context->add_class(class_name); + } else { + context->remove_class(class_name); + } +} + +std::optional Workspace::closeWindow(WindowAddress const &addr) { + if (m_windowMap.contains(addr)) { + return removeWindow(addr); + } + return std::nullopt; +} + +bool Workspace::handleClicked(GdkEventButton *bt) const { + if (bt->type == GDK_BUTTON_PRESS) { + try { + if (id() > 0) { // normal + if (m_workspaceManager.moveToMonitor()) { + gIPC->getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); + } else { + gIPC->getSocket1Reply("dispatch workspace " + std::to_string(id())); + } + } else if (!isSpecial()) { // named (this includes persistent) + if (m_workspaceManager.moveToMonitor()) { + gIPC->getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); + } else { + gIPC->getSocket1Reply("dispatch workspace name:" + name()); + } + } else if (id() != -99) { // named special + gIPC->getSocket1Reply("dispatch togglespecialworkspace " + name()); + } else { // special + gIPC->getSocket1Reply("dispatch togglespecialworkspace"); + } + return true; + } catch (const std::exception &e) { + spdlog::error("Failed to dispatch workspace: {}", e.what()); + } + } + return false; +} + +void Workspace::initializeWindowMap(const Json::Value &clients_data) { + m_windowMap.clear(); + for (auto client : clients_data) { + if (client["workspace"]["id"].asInt() == id()) { + insertWindow({client}); + } + } +} + +void Workspace::insertWindow(WindowCreationPayload create_window_paylod) { + if (!create_window_paylod.isEmpty(m_workspaceManager)) { + m_windowMap[create_window_paylod.getAddress()] = create_window_paylod.repr(m_workspaceManager); + } +}; + +bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_paylod) { + if (create_window_paylod.getWorkspaceName() == name()) { + insertWindow(create_window_paylod); + 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()) { + auto urgentIconIt = icons_map.find("urgent"); + if (urgentIconIt != icons_map.end()) { + return urgentIconIt->second; + } + } + + if (isActive()) { + auto activeIconIt = icons_map.find("active"); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; + } + } + + if (isSpecial()) { + auto specialIconIt = icons_map.find("special"); + if (specialIconIt != icons_map.end()) { + return specialIconIt->second; + } + } + + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; + } + + if (isVisible()) { + auto visibleIconIt = icons_map.find("visible"); + if (visibleIconIt != icons_map.end()) { + return visibleIconIt->second; + } + } + + if (isEmpty()) { + auto emptyIconIt = icons_map.find("empty"); + if (emptyIconIt != icons_map.end()) { + return emptyIconIt->second; + } + } + + if (isPersistent()) { + auto persistentIconIt = icons_map.find("persistent"); + if (persistentIconIt != icons_map.end()) { + return persistentIconIt->second; + } + } + + auto defaultIconIt = icons_map.find("default"); + if (defaultIconIt != icons_map.end()) { + return defaultIconIt->second; + } + + return m_name; +} + +void Workspace::update(const std::string &format, const std::string &icon) { + // clang-format off + if (this->m_workspaceManager.activeOnly() && \ + !this->isActive() && \ + !this->isPersistent() && \ + !this->isVisible() && \ + !this->isSpecial()) { + // clang-format on + // if activeOnly is true, hide if not active, persistent, visible or special + m_button.hide(); + return; + } + if (this->m_workspaceManager.specialVisibleOnly() && this->isSpecial() && !this->isVisible()) { + m_button.hide(); + return; + } + m_button.show(); + + auto styleContext = m_button.get_style_context(); + addOrRemoveClass(styleContext, isActive(), "active"); + addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isEmpty(), "empty"); + addOrRemoveClass(styleContext, isPersistent(), "persistent"); + addOrRemoveClass(styleContext, isUrgent(), "urgent"); + addOrRemoveClass(styleContext, isVisible(), "visible"); + addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); + + std::string windows; + auto windowSeparator = m_workspaceManager.getWindowSeparator(); + + bool isNotFirst = false; + + for (auto &[_pid, window_repr] : m_windowMap) { + if (isNotFirst) { + windows.append(windowSeparator); + } + 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))); +} + +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 3e3931211..047703cc9 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -7,32 +7,11 @@ #include #include #include -#include #include "util/regex_collection.hpp" namespace waybar::modules::hyprland { -int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) { - // Rules that match against title are prioritized - // Rules that don't specify if they're matching against either title or class are deprioritized - bool const hasTitle = window_rule.find("title") != std::string::npos; - bool const hasClass = window_rule.find("class") != std::string::npos; - - if (hasTitle && hasClass) { - m_anyWindowRewriteRuleUsesTitle = true; - return 3; - } - if (hasTitle) { - m_anyWindowRewriteRuleUsesTitle = true; - return 2; - } - if (hasClass) { - return 1; - } - return 0; -} - Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) : AModule(config, "workspaces", id, false, false), m_bar(bar), m_box(bar.orientation, 0) { modulesReady = true; @@ -54,120 +33,87 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value registerIpc(); } -auto Workspaces::parseConfig(const Json::Value &config) -> void { - const Json::Value &configFormat = config["format"]; +Workspaces::~Workspaces() { + gIPC->unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lg(m_mutex); +} - m_format = configFormat.isString() ? configFormat.asString() : "{name}"; - m_withIcon = m_format.find("{icon}") != std::string::npos; +void Workspaces::init() { + m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); - if (m_withIcon && m_iconsMap.empty()) { - Json::Value formatIcons = config["format-icons"]; - for (std::string &name : formatIcons.getMemberNames()) { - m_iconsMap.emplace(name, formatIcons[name].asString()); - } - m_iconsMap.emplace("", ""); - } + initializeWorkspaces(); + dp.emit(); +} - auto configAllOutputs = config_["all-outputs"]; - if (configAllOutputs.isBool()) { - m_allOutputs = configAllOutputs.asBool(); +Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name, + std::string const &monitor) { + spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor); + Json::Value workspaceData; + try { + // numbered persistent workspaces get the name as ID + workspaceData["id"] = name == "special" ? -99 : std::stoi(name); + } catch (const std::exception &e) { + // named persistent workspaces start with ID=0 + workspaceData["id"] = 0; } + workspaceData["name"] = name; + workspaceData["monitor"] = monitor; + workspaceData["windows"] = 0; + return workspaceData; +} - auto configShowSpecial = config_["show-special"]; - if (configShowSpecial.isBool()) { - m_showSpecial = configShowSpecial.asBool(); - } +void Workspaces::createWorkspace(Json::Value const &workspace_data, + Json::Value const &clients_data) { + auto workspaceName = workspace_data["name"].asString(); + spdlog::debug("Creating workspace {}", workspaceName); - auto configActiveOnly = config_["active-only"]; - if (configActiveOnly.isBool()) { - m_activeOnly = configActiveOnly.asBool(); - } + // avoid recreating existing workspaces + auto workspace = std::find_if( + m_workspaces.begin(), m_workspaces.end(), + [workspaceName](std::unique_ptr const &w) { + return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || + workspaceName == w->name(); + }); - auto configSortBy = config_["sort-by"]; - if (configSortBy.isString()) { - auto sortByStr = configSortBy.asString(); - try { - m_sortBy = m_enumParser.parseStringToEnum(sortByStr, m_sortMap); - } catch (const std::invalid_argument &e) { - // Handle the case where the string is not a valid enum representation. - m_sortBy = SortMethod::DEFAULT; - g_warning("Invalid string representation for sort-by. Falling back to default sort method."); - } - } + if (workspace != m_workspaces.end()) { + // don't recreate workspace, but update persistency if necessary + const auto keys = workspace_data.getMemberNames(); - Json::Value ignoreWorkspaces = config["ignore-workspaces"]; - if (ignoreWorkspaces.isArray()) { - for (Json::Value &workspaceRegex : ignoreWorkspaces) { - if (workspaceRegex.isString()) { - std::string ruleString = workspaceRegex.asString(); - try { - const std::regex rule{ruleString, std::regex_constants::icase}; - m_ignoreWorkspaces.emplace_back(rule); - } catch (const std::regex_error &e) { - spdlog::error("Invalid rule {}: {}", ruleString, e.what()); - } - } else { - spdlog::error("Not a string: '{}'", workspaceRegex); - } + const auto *k = "persistent-rule"; + if (std::find(keys.begin(), keys.end(), k) != keys.end()) { + spdlog::debug("Set dynamic persistency of workspace {} to: {}", workspaceName, + workspace_data[k].asBool() ? "true" : "false"); + (*workspace)->setPersistentRule(workspace_data[k].asBool()); } - } - - 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()) { - m_persistentWorkspaceConfig = config_["persistent-workspaces"].isObject() - ? config_["persistent-workspaces"] - : config_["persistent_workspaces"]; - } - const Json::Value &formatWindowSeparator = config["format-window-separator"]; - m_formatWindowSeparator = - formatWindowSeparator.isString() ? formatWindowSeparator.asString() : " "; + k = "persistent-config"; + if (std::find(keys.begin(), keys.end(), k) != keys.end()) { + spdlog::debug("Set config persistency of workspace {} to: {}", workspaceName, + workspace_data[k].asBool() ? "true" : "false"); + (*workspace)->setPersistentConfig(workspace_data[k].asBool()); + } - const Json::Value &windowRewrite = config["window-rewrite"]; - if (!windowRewrite.isObject()) { - spdlog::debug("window-rewrite is not defined or is not an object, using default rules."); return; } - const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"]; - std::string windowRewriteDefault = - windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; - - m_windowRewriteRules = util::RegexCollection( - windowRewrite, windowRewriteDefault, - [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); + // create new workspace + m_workspaces.emplace_back(std::make_unique(workspace_data, *this, clients_data)); + Gtk::Button &newWorkspaceButton = m_workspaces.back()->button(); + m_box.pack_start(newWorkspaceButton, false, false); + sortWorkspaces(); + newWorkspaceButton.show_all(); } -void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) { - if (!create_window_payload.isEmpty(*this)) { - m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this); +void Workspaces::createWorkspacesToCreate() { + for (const auto &[workspaceData, clientsData] : m_workspacesToCreate) { + createWorkspace(workspaceData, clientsData); } -} - -auto Workspaces::registerIpc() -> void { - gIPC->registerForIPC("workspace", this); - gIPC->registerForIPC("activespecial", this); - gIPC->registerForIPC("createworkspace", this); - gIPC->registerForIPC("destroyworkspace", this); - gIPC->registerForIPC("focusedmon", this); - gIPC->registerForIPC("moveworkspace", this); - gIPC->registerForIPC("renameworkspace", this); - gIPC->registerForIPC("openwindow", this); - gIPC->registerForIPC("closewindow", this); - gIPC->registerForIPC("movewindow", this); - gIPC->registerForIPC("urgent", this); - gIPC->registerForIPC("configreloaded", this); - - if (windowRewriteConfigUsesTitle()) { - spdlog::info( - "Registering for Hyprland's 'windowtitle' events because a user-defined window " - "rewrite rule uses the 'title' field."); - gIPC->registerForIPC("windowtitle", this); + if (!m_workspacesToCreate.empty()) { + updateWindowCount(); + sortWorkspaces(); } + m_workspacesToCreate.clear(); } /** @@ -179,90 +125,87 @@ auto Workspaces::registerIpc() -> void { void Workspaces::doUpdate() { std::unique_lock lock(m_mutex); - // remove workspaces that wait to be removed - for (auto &elem : m_workspacesToRemove) { - removeWorkspace(elem); + removeWorkspacesToRemove(); + createWorkspacesToCreate(); + updateWorkspaceStates(); + updateWindowCount(); + sortWorkspaces(); + + bool anyWindowCreated = updateWindowsToCreate(); + + if (anyWindowCreated) { + dp.emit(); } - m_workspacesToRemove.clear(); +} - // add workspaces that wait to be created - for (auto &[workspaceData, clientsData] : m_workspacesToCreate) { - createWorkspace(workspaceData, clientsData); +void Workspaces::extendOrphans(int workspaceId, Json::Value const &clientsJson) { + spdlog::trace("Extending orphans with workspace {}", workspaceId); + for (const auto &client : clientsJson) { + if (client["workspace"]["id"].asInt() == workspaceId) { + registerOrphanWindow({client}); + } } - m_workspacesToCreate.clear(); +} - // get all active workspaces - spdlog::trace("Getting active workspaces"); - auto monitors = gIPC->getSocket1JsonReply("monitors"); +std::string Workspaces::getRewrite(std::string window_class, std::string window_title) { + std::string windowReprKey; + if (windowRewriteConfigUsesTitle()) { + windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title); + } else { + windowReprKey = fmt::format("class<{}>", window_class); + } + auto const rewriteRule = m_windowRewriteRules.get(windowReprKey); + return fmt::format(fmt::runtime(rewriteRule), fmt::arg("class", window_class), + fmt::arg("title", window_title)); +} + +std::vector Workspaces::getVisibleWorkspaces() { std::vector visibleWorkspaces; - for (Json::Value &monitor : monitors) { + auto monitors = gIPC->getSocket1JsonReply("monitors"); + for (const auto &monitor : monitors) { auto ws = monitor["activeWorkspace"]; - if (ws.isObject() && (ws["name"].isString())) { + if (ws.isObject() && ws["name"].isString()) { visibleWorkspaces.push_back(ws["name"].asString()); } auto sws = monitor["specialWorkspace"]; auto name = sws["name"].asString(); - if (sws.isObject() && (sws["name"].isString()) && !name.empty()) { + if (sws.isObject() && sws["name"].isString() && !name.empty()) { visibleWorkspaces.push_back(!name.starts_with("special:") ? name : name.substr(8)); } } + return visibleWorkspaces; +} - spdlog::trace("Updating workspace states"); - for (auto &workspace : m_workspaces) { - // active - workspace->setActive(workspace->name() == m_activeWorkspaceName || - workspace->name() == m_activeSpecialWorkspaceName); - // disable urgency if workspace is active - if (workspace->name() == m_activeWorkspaceName && workspace->isUrgent()) { - workspace->setUrgent(false); - } - - // visible - workspace->setVisible(std::find(visibleWorkspaces.begin(), visibleWorkspaces.end(), - workspace->name()) != visibleWorkspaces.end()); +void Workspaces::initializeWorkspaces() { + spdlog::debug("Initializing workspaces"); - // set workspace icon - std::string &workspaceIcon = m_iconsMap[""]; - if (m_withIcon) { - workspaceIcon = workspace->selectIcon(m_iconsMap); - } - workspace->update(m_format, workspaceIcon); + // if the workspace rules changed since last initialization, make sure we reset everything: + for (auto &workspace : m_workspaces) { + m_workspacesToRemove.push_back(workspace->name()); } - spdlog::trace("Updating window count"); - bool anyWindowCreated = false; - std::vector notCreated; + // get all current workspaces + auto const workspacesJson = gIPC->getSocket1JsonReply("workspaces"); + auto const clientsJson = gIPC->getSocket1JsonReply("clients"); - for (auto &windowPayload : m_windowsToCreate) { - bool created = false; - for (auto &workspace : m_workspaces) { - if (workspace->onWindowOpened(windowPayload)) { - created = true; - anyWindowCreated = true; - break; - } - } - if (!created) { - static auto const WINDOW_CREATION_TIMEOUT = 2; - if (windowPayload.incrementTimeSpentUncreated() < WINDOW_CREATION_TIMEOUT) { - notCreated.push_back(windowPayload); - } else { - registerOrphanWindow(windowPayload); - } + for (Json::Value workspaceJson : workspacesJson) { + std::string workspaceName = workspaceJson["name"].asString(); + if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && + (!workspaceName.starts_with("special") || showSpecial()) && + !isWorkspaceIgnored(workspaceName)) { + m_workspacesToCreate.emplace_back(workspaceJson, clientsJson); + } else { + extendOrphans(workspaceJson["id"].asInt(), clientsJson); } } - if (anyWindowCreated) { - dp.emit(); + spdlog::debug("Initializing persistent workspaces"); + if (m_persistentWorkspaceConfig.isObject()) { + // a persistent workspace config is defined, so use that instead of workspace rules + loadPersistentWorkspacesFromConfig(clientsJson); } - - m_windowsToCreate.clear(); - m_windowsToCreate = notCreated; -} - -auto Workspaces::update() -> void { - doUpdate(); - AModule::update(); + // load Hyprland's workspace rules + loadPersistentWorkspacesFromWorkspaceRules(clientsJson); } bool isDoubleSpecial(std::string const &workspace_name) { @@ -284,6 +227,90 @@ bool Workspaces::isWorkspaceIgnored(std::string const &name) { return false; } +void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJson) { + spdlog::info("Loading persistent workspaces from Waybar config"); + const std::vector keys = m_persistentWorkspaceConfig.getMemberNames(); + std::vector persistentWorkspacesToCreate; + + const std::string currentMonitor = m_bar.output->name; + const bool monitorInConfig = std::find(keys.begin(), keys.end(), currentMonitor) != keys.end(); + for (const std::string &key : keys) { + // only add if either: + // 1. key is the current monitor name + // 2. key is "*" and this monitor is not already defined in the config + bool canCreate = key == currentMonitor || (key == "*" && !monitorInConfig); + const Json::Value &value = m_persistentWorkspaceConfig[key]; + spdlog::trace("Parsing persistent workspace config: {} => {}", key, value.toStyledString()); + + if (value.isInt()) { + // value is a number => create that many workspaces for this monitor + if (canCreate) { + int amount = value.asInt(); + spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, currentMonitor); + for (int i = 0; i < amount; i++) { + persistentWorkspacesToCreate.emplace_back(std::to_string(m_monitorId * amount + i + 1)); + } + } + } else if (value.isArray() && !value.empty()) { + // 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())); + } + } + } else { + // key is the workspace and value is array of monitors to create on + for (const Json::Value &monitor : value) { + if (monitor.isString() && monitor.asString() == currentMonitor) { + persistentWorkspacesToCreate.emplace_back(currentMonitor); + break; + } + } + } + } else { + // this workspace should be displayed on all monitors + persistentWorkspacesToCreate.emplace_back(key); + } + } + + for (auto const &workspace : persistentWorkspacesToCreate) { + auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); + workspaceData["persistent-config"] = true; + m_workspacesToCreate.emplace_back(workspaceData, clientsJson); + } +} + +void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &clientsJson) { + spdlog::info("Loading persistent workspaces from Hyprland workspace rules"); + + auto const workspaceRules = gIPC->getSocket1JsonReply("workspacerules"); + for (Json::Value const &rule : workspaceRules) { + if (!rule["workspaceString"].isString()) { + spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule); + continue; + } + if (!rule["persistent"].asBool()) { + continue; + } + auto const &workspace = rule["workspaceString"].asString(); + auto const &monitor = rule["monitor"].asString(); + // create this workspace persistently if: + // 1. the allOutputs config option is enabled + // 2. the rule's monitor is the current monitor + // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor + if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { + // => persistent workspace should be shown on this monitor + auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); + workspaceData["persistent-rule"] = true; + m_workspacesToCreate.emplace_back(workspaceData, clientsJson); + } else { + m_workspacesToRemove.emplace_back(workspace); + } + } +} + void Workspaces::onEvent(const std::string &ev) { std::lock_guard lock(m_mutex); std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); @@ -299,7 +326,7 @@ void Workspaces::onEvent(const std::string &ev) { onWorkspaceCreated(payload); } else if (eventName == "focusedmon") { onMonitorFocused(payload); - } else if (eventName == "moveworkspace" && !allOutputs()) { + } else if (eventName == "moveworkspace") { onWorkspaceMoved(payload); } else if (eventName == "openwindow") { onWindowOpened(payload); @@ -349,7 +376,7 @@ void Workspaces::onWorkspaceCreated(std::string const &workspaceName, (showSpecial() || !name.starts_with("special")) && !isDoubleSpecial(workspaceName)) { for (Json::Value const &rule : workspaceRules) { if (rule["workspaceString"].asString() == workspaceName) { - workspaceJson["persistent"] = rule["persistent"].asBool(); + workspaceJson["persistent-rule"] = rule["persistent"].asBool(); break; } } @@ -368,6 +395,12 @@ void Workspaces::onWorkspaceCreated(std::string const &workspaceName, void Workspaces::onWorkspaceMoved(std::string const &payload) { spdlog::debug("Workspace moved: {}", payload); + + // Update active workspace + m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); + + if (allOutputs()) return; + std::string workspaceName = payload.substr(0, payload.find(',')); std::string monitorName = payload.substr(payload.find(',') + 1); @@ -528,356 +561,167 @@ void Workspaces::onConfigReloaded() { init(); } -void Workspaces::updateWindowCount() { - const Json::Value workspacesJson = gIPC->getSocket1JsonReply("workspaces"); - for (auto &workspace : m_workspaces) { - auto workspaceJson = - std::find_if(workspacesJson.begin(), workspacesJson.end(), [&](Json::Value const &x) { - return x["name"].asString() == workspace->name() || - (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); - }); - uint32_t count = 0; - if (workspaceJson != workspacesJson.end()) { - try { - count = (*workspaceJson)["windows"].asUInt(); - } catch (const std::exception &e) { - spdlog::error("Failed to update window count: {}", e.what()); - } - } - workspace->setWindows(count); - } -} - -void Workspace::initializeWindowMap(const Json::Value &clients_data) { - m_windowMap.clear(); - for (auto client : clients_data) { - if (client["workspace"]["id"].asInt() == id()) { - insertWindow({client}); - } - } -} - -void Workspace::insertWindow(WindowCreationPayload create_window_paylod) { - if (!create_window_paylod.isEmpty(m_workspaceManager)) { - m_windowMap[create_window_paylod.getAddress()] = create_window_paylod.repr(m_workspaceManager); - } -}; - -std::string Workspace::removeWindow(WindowAddress const &addr) { - std::string windowRepr = m_windowMap[addr]; - m_windowMap.erase(addr); - return windowRepr; -} - -bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_paylod) { - if (create_window_paylod.getWorkspaceName() == name()) { - insertWindow(create_window_paylod); - return true; - } - return false; -} - -std::optional Workspace::closeWindow(WindowAddress const &addr) { - if (m_windowMap.contains(addr)) { - return removeWindow(addr); - } - return std::nullopt; -} - -void Workspaces::createWorkspace(Json::Value const &workspace_data, - Json::Value const &clients_data) { - auto workspaceName = workspace_data["name"].asString(); - spdlog::debug("Creating workspace {}, persistent: {}", workspaceName, - workspace_data["persistent"].asBool() ? "true" : "false"); - - // avoid recreating existing workspaces - auto workspace = std::find_if( - m_workspaces.begin(), m_workspaces.end(), - [workspaceName](std::unique_ptr const &w) { - 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 - (*workspace)->setPersistent(workspace_data["persistent"].asBool()); - return; - } - - // create new workspace - m_workspaces.emplace_back(std::make_unique(workspace_data, *this, clients_data)); - Gtk::Button &newWorkspaceButton = m_workspaces.back()->button(); - m_box.pack_start(newWorkspaceButton, false, false); - sortWorkspaces(); - newWorkspaceButton.show_all(); -} - -void Workspaces::removeWorkspace(std::string const &name) { - spdlog::debug("Removing workspace {}", name); - auto workspace = - std::find_if(m_workspaces.begin(), m_workspaces.end(), [&](std::unique_ptr &x) { - return (name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name(); - }); - - if (workspace == m_workspaces.end()) { - // happens when a workspace on another monitor is destroyed - return; - } - - if ((*workspace)->isPersistent()) { - spdlog::trace("Not removing persistent workspace {}", name); - return; - } - - m_box.remove(workspace->get()->button()); - m_workspaces.erase(workspace); -} - -Json::Value createPersistentWorkspaceData(std::string const &name, std::string const &monitor) { - spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor); - Json::Value workspaceData; - try { - // numbered persistent workspaces get the name as ID - workspaceData["id"] = name == "special" ? -99 : std::stoi(name); - } catch (const std::exception &e) { - // named persistent workspaces start with ID=0 - workspaceData["id"] = 0; - } - workspaceData["name"] = name; - workspaceData["monitor"] = monitor; - workspaceData["windows"] = 0; - workspaceData["persistent"] = true; - return workspaceData; -} - -void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJson) { - spdlog::info("Loading persistent workspaces from Waybar config"); - const std::vector keys = m_persistentWorkspaceConfig.getMemberNames(); - std::vector persistentWorkspacesToCreate; - - const std::string currentMonitor = m_bar.output->name; - const bool monitorInConfig = std::find(keys.begin(), keys.end(), currentMonitor) != keys.end(); - for (const std::string &key : keys) { - // only add if either: - // 1. key is the current monitor name - // 2. key is "*" and this monitor is not already defined in the config - bool canCreate = key == currentMonitor || (key == "*" && !monitorInConfig); - const Json::Value &value = m_persistentWorkspaceConfig[key]; - spdlog::trace("Parsing persistent workspace config: {} => {}", key, value.toStyledString()); - - if (value.isInt()) { - // value is a number => create that many workspaces for this monitor - if (canCreate) { - int amount = value.asInt(); - spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, currentMonitor); - for (int i = 0; i < amount; i++) { - persistentWorkspacesToCreate.emplace_back(std::to_string(m_monitorId * amount + i + 1)); - } - } - } else if (value.isArray() && !value.empty()) { - // 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())); - } - } - } else { - // key is the workspace and value is array of monitors to create on - for (const Json::Value &monitor : value) { - if (monitor.isString() && monitor.asString() == currentMonitor) { - persistentWorkspacesToCreate.emplace_back(currentMonitor); - break; - } - } - } - } else { - // this workspace should be displayed on all monitors - persistentWorkspacesToCreate.emplace_back(key); - } - } - - for (auto const &workspace : persistentWorkspacesToCreate) { - auto const workspaceData = createPersistentWorkspaceData(workspace, m_bar.output->name); - m_workspacesToCreate.emplace_back(workspaceData, clientsJson); - } -} - -void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &clientsJson) { - spdlog::info("Loading persistent workspaces from Hyprland workspace rules"); - - auto const workspaceRules = gIPC->getSocket1JsonReply("workspacerules"); - for (Json::Value const &rule : workspaceRules) { - if (!rule["workspaceString"].isString()) { - spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule); - continue; - } - if (!rule["persistent"].asBool()) { - continue; - } - auto const &workspace = rule["workspaceString"].asString(); - auto const &monitor = rule["monitor"].asString(); - // create this workspace persistently if: - // 1. the allOutputs config option is enabled - // 2. the rule's monitor is the current monitor - // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor - if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { - // => persistent workspace should be shown on this monitor - auto workspaceData = createPersistentWorkspaceData(workspace, m_bar.output->name); - m_workspacesToCreate.emplace_back(workspaceData, clientsJson); - } else { - m_workspacesToRemove.emplace_back(workspace); - } - } -} - -void Workspaces::setCurrentMonitorId() { - // get monitor ID from name (used by persistent workspaces) - m_monitorId = 0; - auto monitors = gIPC->getSocket1JsonReply("monitors"); - auto currentMonitor = std::find_if( - monitors.begin(), monitors.end(), - [this](const Json::Value &m) { return m["name"].asString() == m_bar.output->name; }); - if (currentMonitor == monitors.end()) { - spdlog::error("Monitor '{}' does not have an ID? Using 0", m_bar.output->name); - } else { - m_monitorId = (*currentMonitor)["id"].asInt(); - spdlog::trace("Current monitor ID: {}", m_monitorId); - } -} - -void Workspaces::initializeWorkspaces() { - spdlog::debug("Initializing workspaces"); +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; - // if the workspace rules changed since last initialization, make sure we reset everything: - for (auto &workspace : m_workspaces) { - m_workspacesToRemove.push_back(workspace->name()); + if (m_withIcon && m_iconsMap.empty()) { + populateIconsMap(config["format-icons"]); } - // get all current workspaces - auto const workspacesJson = gIPC->getSocket1JsonReply("workspaces"); - auto const clientsJson = gIPC->getSocket1JsonReply("clients"); + populateBoolConfig(config, "all-outputs", m_allOutputs); + populateBoolConfig(config, "show-special", m_showSpecial); + populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly); + populateBoolConfig(config, "active-only", m_activeOnly); + populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); - for (Json::Value workspaceJson : workspacesJson) { - std::string workspaceName = workspaceJson["name"].asString(); - if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && - (!workspaceName.starts_with("special") || showSpecial()) && - !isWorkspaceIgnored(workspaceName)) { - m_workspacesToCreate.emplace_back(workspaceJson, clientsJson); - } else { - extendOrphans(workspaceJson["id"].asInt(), clientsJson); - } + m_persistentWorkspaceConfig = config.get("persistent-workspaces", Json::Value()); + populateSortByConfig(config); + populateIgnoreWorkspacesConfig(config); + populateFormatWindowSeparatorConfig(config); + populateWindowRewriteConfig(config); +} + +auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void { + for (const auto &name : formatIcons.getMemberNames()) { + m_iconsMap.emplace(name, formatIcons[name].asString()); } + m_iconsMap.emplace("", ""); +} - spdlog::debug("Initializing persistent workspaces"); - if (m_persistentWorkspaceConfig.isObject()) { - // a persistent workspace config is defined, so use that instead of workspace rules - loadPersistentWorkspacesFromConfig(clientsJson); - } else { - // no persistent workspaces config defined, use Hyprland's workspace rules - loadPersistentWorkspacesFromWorkspaceRules(clientsJson); +auto Workspaces::populateBoolConfig(const Json::Value &config, const std::string &key, bool &member) + -> void { + const auto &configValue = config[key]; + if (configValue.isBool()) { + member = configValue.asBool(); } } -void Workspaces::extendOrphans(int workspaceId, Json::Value const &clientsJson) { - spdlog::trace("Extending orphans with workspace {}", workspaceId); - for (const auto &client : clientsJson) { - if (client["workspace"]["id"].asInt() == workspaceId) { - registerOrphanWindow({client}); +auto Workspaces::populateSortByConfig(const Json::Value &config) -> void { + const auto &configSortBy = config["sort-by"]; + if (configSortBy.isString()) { + auto sortByStr = configSortBy.asString(); + try { + m_sortBy = m_enumParser.parseStringToEnum(sortByStr, m_sortMap); + } catch (const std::invalid_argument &e) { + m_sortBy = SortMethod::DEFAULT; + spdlog::warn( + "Invalid string representation for sort-by. Falling back to default sort method."); } } } -void Workspaces::init() { - m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); - - initializeWorkspaces(); - updateWindowCount(); - sortWorkspaces(); - dp.emit(); +auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value &config) -> void { + auto ignoreWorkspaces = config["ignore-workspaces"]; + if (ignoreWorkspaces.isArray()) { + for (const auto &workspaceRegex : ignoreWorkspaces) { + if (workspaceRegex.isString()) { + std::string ruleString = workspaceRegex.asString(); + try { + const std::regex rule{ruleString, std::regex_constants::icase}; + m_ignoreWorkspaces.emplace_back(rule); + } catch (const std::regex_error &e) { + spdlog::error("Invalid rule {}: {}", ruleString, e.what()); + } + } else { + spdlog::error("Not a string: '{}'", workspaceRegex); + } + } + } } -Workspaces::~Workspaces() { - gIPC->unregisterForIPC(this); - // wait for possible event handler to finish - std::lock_guard lg(m_mutex); +auto Workspaces::populateFormatWindowSeparatorConfig(const Json::Value &config) -> void { + const auto &formatWindowSeparator = config["format-window-separator"]; + m_formatWindowSeparator = + formatWindowSeparator.isString() ? formatWindowSeparator.asString() : " "; } -Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_manager, - const Json::Value &clients_data) - : m_workspaceManager(workspace_manager), - m_id(workspace_data["id"].asInt()), - m_name(workspace_data["name"].asString()), - m_output(workspace_data["monitor"].asString()), // TODO:allow using monitor desc - m_windows(workspace_data["windows"].asInt()), - m_isActive(true), - m_isPersistent(workspace_data["persistent"].asBool()) { - if (m_name.starts_with("name:")) { - m_name = m_name.substr(5); - } else if (m_name.starts_with("special")) { - m_name = m_id == -99 ? m_name : m_name.substr(8); - m_isSpecial = true; +auto Workspaces::populateWindowRewriteConfig(const Json::Value &config) -> void { + const auto &windowRewrite = config["window-rewrite"]; + if (!windowRewrite.isObject()) { + spdlog::debug("window-rewrite is not defined or is not an object, using default rules."); + return; } - m_button.add_events(Gdk::BUTTON_PRESS_MASK); - m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), - false); - - m_button.set_relief(Gtk::RELIEF_NONE); - m_content.set_center_widget(m_label); - m_button.add(m_content); + const auto &windowRewriteDefaultConfig = config["window-rewrite-default"]; + std::string windowRewriteDefault = + windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; - initializeWindowMap(clients_data); + m_windowRewriteRules = util::RegexCollection( + windowRewrite, windowRewriteDefault, + [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); } -void addOrRemoveClass(const Glib::RefPtr &context, bool condition, - const std::string &class_name) { - if (condition) { - context->add_class(class_name); - } else { - context->remove_class(class_name); +void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) { + if (!create_window_payload.isEmpty(*this)) { + m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this); } } -void Workspace::update(const std::string &format, const std::string &icon) { - // clang-format off - if (this->m_workspaceManager.activeOnly() && \ - !this->isActive() && \ - !this->isPersistent() && \ - !this->isVisible() && \ - !this->isSpecial()) { - // clang-format on - // if activeOnly is true, hide if not active, persistent, visible or special - m_button.hide(); - return; +auto Workspaces::registerIpc() -> void { + gIPC->registerForIPC("workspace", this); + gIPC->registerForIPC("activespecial", this); + gIPC->registerForIPC("createworkspace", this); + gIPC->registerForIPC("destroyworkspace", this); + gIPC->registerForIPC("focusedmon", this); + gIPC->registerForIPC("moveworkspace", this); + gIPC->registerForIPC("renameworkspace", this); + gIPC->registerForIPC("openwindow", this); + gIPC->registerForIPC("closewindow", this); + gIPC->registerForIPC("movewindow", this); + gIPC->registerForIPC("urgent", this); + gIPC->registerForIPC("configreloaded", this); + + if (windowRewriteConfigUsesTitle()) { + spdlog::info( + "Registering for Hyprland's 'windowtitle' events because a user-defined window " + "rewrite rule uses the 'title' field."); + gIPC->registerForIPC("windowtitle", this); } - m_button.show(); +} - auto styleContext = m_button.get_style_context(); - addOrRemoveClass(styleContext, isActive(), "active"); - addOrRemoveClass(styleContext, isSpecial(), "special"); - addOrRemoveClass(styleContext, isEmpty(), "empty"); - addOrRemoveClass(styleContext, isPersistent(), "persistent"); - addOrRemoveClass(styleContext, isUrgent(), "urgent"); - addOrRemoveClass(styleContext, isVisible(), "visible"); +void Workspaces::removeWorkspacesToRemove() { + for (const auto &workspaceName : m_workspacesToRemove) { + removeWorkspace(workspaceName); + } + m_workspacesToRemove.clear(); +} - std::string windows; - auto windowSeparator = m_workspaceManager.getWindowSeparator(); +void Workspaces::removeWorkspace(std::string const &name) { + spdlog::debug("Removing workspace {}", name); + auto workspace = + std::find_if(m_workspaces.begin(), m_workspaces.end(), [&](std::unique_ptr &x) { + return (name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name(); + }); - bool isNotFirst = false; + if (workspace == m_workspaces.end()) { + // happens when a workspace on another monitor is destroyed + return; + } - for (auto &[_pid, window_repr] : m_windowMap) { - if (isNotFirst) { - windows.append(windowSeparator); - } - isNotFirst = true; - windows.append(window_repr); + if ((*workspace)->isPersistentConfig()) { + spdlog::trace("Not removing config persistent workspace {}", name); + return; } - 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))); + m_box.remove(workspace->get()->button()); + m_workspaces.erase(workspace); +} + +void Workspaces::setCurrentMonitorId() { + // get monitor ID from name (used by persistent workspaces) + m_monitorId = 0; + auto monitors = gIPC->getSocket1JsonReply("monitors"); + auto currentMonitor = std::find_if( + monitors.begin(), monitors.end(), + [this](const Json::Value &m) { return m["name"].asString() == m_bar.output->name; }); + if (currentMonitor == monitors.end()) { + spdlog::error("Monitor '{}' does not have an ID? Using 0", m_bar.output->name); + } else { + m_monitorId = (*currentMonitor)["id"].asInt(); + spdlog::trace("Current monitor ID: {}", m_monitorId); + } } void Workspaces::sortWorkspaces() { @@ -943,83 +787,6 @@ void Workspaces::sortWorkspaces() { } } -std::string &Workspace::selectIcon(std::map &icons_map) { - spdlog::trace("Selecting icon for workspace {}", name()); - if (isUrgent()) { - auto urgentIconIt = icons_map.find("urgent"); - if (urgentIconIt != icons_map.end()) { - return urgentIconIt->second; - } - } - - if (isActive()) { - auto activeIconIt = icons_map.find("active"); - if (activeIconIt != icons_map.end()) { - return activeIconIt->second; - } - } - - if (isSpecial()) { - auto specialIconIt = icons_map.find("special"); - if (specialIconIt != icons_map.end()) { - return specialIconIt->second; - } - } - - auto namedIconIt = icons_map.find(name()); - if (namedIconIt != icons_map.end()) { - return namedIconIt->second; - } - - if (isVisible()) { - auto visibleIconIt = icons_map.find("visible"); - if (visibleIconIt != icons_map.end()) { - return visibleIconIt->second; - } - } - - if (isEmpty()) { - auto emptyIconIt = icons_map.find("empty"); - if (emptyIconIt != icons_map.end()) { - return emptyIconIt->second; - } - } - - if (isPersistent()) { - auto persistentIconIt = icons_map.find("persistent"); - if (persistentIconIt != icons_map.end()) { - return persistentIconIt->second; - } - } - - auto defaultIconIt = icons_map.find("default"); - if (defaultIconIt != icons_map.end()) { - return defaultIconIt->second; - } - - return m_name; -} - -bool Workspace::handleClicked(GdkEventButton *bt) const { - if (bt->type == GDK_BUTTON_PRESS) { - try { - if (id() > 0) { // normal - gIPC->getSocket1Reply("dispatch workspace " + std::to_string(id())); - } else if (!isSpecial()) { // named (this includes persistent) - gIPC->getSocket1Reply("dispatch workspace name:" + name()); - } else if (id() != -99) { // named special - gIPC->getSocket1Reply("dispatch togglespecialworkspace " + name()); - } else { // special - gIPC->getSocket1Reply("dispatch togglespecialworkspace"); - } - return true; - } catch (const std::exception &e) { - spdlog::error("Failed to dispatch workspace: {}", e.what()); - } - } - return false; -} - void Workspaces::setUrgentWorkspace(std::string const &windowaddress) { const Json::Value clientsJson = gIPC->getSocket1JsonReply("clients"); int workspaceId = -1; @@ -1039,110 +806,103 @@ void Workspaces::setUrgentWorkspace(std::string const &windowaddress) { } } -std::string Workspaces::getRewrite(std::string window_class, std::string window_title) { - std::string windowReprKey; - if (windowRewriteConfigUsesTitle()) { - windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title); - } else { - windowReprKey = fmt::format("class<{}>", window_class); - } - auto const rewriteRule = m_windowRewriteRules.get(windowReprKey); - return fmt::format(fmt::runtime(rewriteRule), fmt::arg("class", window_class), - fmt::arg("title", window_title)); -} - -WindowCreationPayload::WindowCreationPayload(std::string workspace_name, - WindowAddress window_address, std::string window_repr) - : m_window(std::move(window_repr)), - m_windowAddress(std::move(window_address)), - m_workspaceName(std::move(workspace_name)) { - clearAddr(); - clearWorkspaceName(); -} - -WindowCreationPayload::WindowCreationPayload(std::string workspace_name, - WindowAddress window_address, std::string window_class, - std::string window_title) - : 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)) { - clearAddr(); - clearWorkspaceName(); -} - -WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data) - : m_window(std::make_pair(client_data["class"].asString(), client_data["title"].asString())), - m_windowAddress(client_data["address"].asString()), - m_workspaceName(client_data["workspace"]["name"].asString()) { - clearAddr(); - clearWorkspaceName(); +auto Workspaces::update() -> void { + doUpdate(); + AModule::update(); } -std::string 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); +void Workspaces::updateWindowCount() { + const Json::Value workspacesJson = gIPC->getSocket1JsonReply("workspaces"); + for (auto &workspace : m_workspaces) { + auto workspaceJson = + std::find_if(workspacesJson.begin(), workspacesJson.end(), [&](Json::Value const &x) { + return x["name"].asString() == workspace->name() || + (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); + }); + uint32_t count = 0; + if (workspaceJson != workspacesJson.end()) { + try { + count = (*workspaceJson)["windows"].asUInt(); + } catch (const std::exception &e) { + spdlog::error("Failed to update window count: {}", e.what()); + } + } + workspace->setWindows(count); } - // Unreachable - spdlog::error("WorkspaceWindow::repr: Unreachable"); - throw std::runtime_error("WorkspaceWindow::repr: Unreachable"); } -bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) { - if (std::holds_alternative(m_window)) { - return std::get(m_window).empty(); - } - if (std::holds_alternative(m_window)) { - auto [window_class, window_title] = std::get(m_window); - return (window_class.empty() && - (!workspace_manager.windowRewriteConfigUsesTitle() || window_title.empty())); +bool Workspaces::updateWindowsToCreate() { + bool anyWindowCreated = false; + std::vector notCreated; + for (auto &windowPayload : m_windowsToCreate) { + bool created = false; + for (auto &workspace : m_workspaces) { + if (workspace->onWindowOpened(windowPayload)) { + created = true; + anyWindowCreated = true; + break; + } + } + if (!created) { + static auto const WINDOW_CREATION_TIMEOUT = 2; + if (windowPayload.incrementTimeSpentUncreated() < WINDOW_CREATION_TIMEOUT) { + notCreated.push_back(windowPayload); + } else { + registerOrphanWindow(windowPayload); + } + } } - // Unreachable - spdlog::error("WorkspaceWindow::isEmpty: Unreachable"); - throw std::runtime_error("WorkspaceWindow::isEmpty: Unreachable"); + m_windowsToCreate.clear(); + m_windowsToCreate = notCreated; + return anyWindowCreated; } -int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; } - -void WindowCreationPayload::clearAddr() { - // substr(2, ...) is necessary because Hyprland's JSON follows this format: - // 0x{ADDR} - // While Hyprland's IPC follows this format: - // {ADDR} - static const std::string ADDR_PREFIX = "0x"; - static const int ADDR_PREFIX_LEN = ADDR_PREFIX.length(); - - if (m_windowAddress.starts_with(ADDR_PREFIX)) { - m_windowAddress = - m_windowAddress.substr(ADDR_PREFIX_LEN, m_windowAddress.length() - ADDR_PREFIX_LEN); +void Workspaces::updateWorkspaceStates() { + const std::vector visibleWorkspaces = getVisibleWorkspaces(); + auto updatedWorkspaces = gIPC->getSocket1JsonReply("workspaces"); + for (auto &workspace : m_workspaces) { + workspace->setActive(workspace->name() == m_activeWorkspaceName || + workspace->name() == m_activeSpecialWorkspaceName); + if (workspace->name() == m_activeWorkspaceName && workspace->isUrgent()) { + workspace->setUrgent(false); + } + workspace->setVisible(std::find(visibleWorkspaces.begin(), visibleWorkspaces.end(), + workspace->name()) != visibleWorkspaces.end()); + std::string &workspaceIcon = m_iconsMap[""]; + if (m_withIcon) { + workspaceIcon = workspace->selectIcon(m_iconsMap); + } + auto updatedWorkspace = std::find_if( + updatedWorkspaces.begin(), updatedWorkspaces.end(), [&workspace](const auto &w) { + auto wNameRaw = w["name"].asString(); + auto wName = wNameRaw.starts_with("special:") ? wNameRaw.substr(8) : wNameRaw; + return wName == workspace->name(); + }); + if (updatedWorkspace != updatedWorkspaces.end()) { + workspace->setOutput((*updatedWorkspace)["monitor"].asString()); + } + workspace->update(m_format, workspaceIcon); } } -void WindowCreationPayload::clearWorkspaceName() { - // The workspace name may optionally feature "special:" at the beginning. - // If so, we need to remove it because the workspace is saved WITHOUT the - // special qualifier. The reasoning is that not all of Hyprland's IPC events - // use this qualifier, so it's better to be consistent about our uses. - - static const std::string SPECIAL_QUALIFIER_PREFIX = "special:"; - static const int SPECIAL_QUALIFIER_PREFIX_LEN = SPECIAL_QUALIFIER_PREFIX.length(); +int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) { + // Rules that match against title are prioritized + // Rules that don't specify if they're matching against either title or class are deprioritized + bool const hasTitle = window_rule.find("title") != std::string::npos; + bool const hasClass = window_rule.find("class") != std::string::npos; - if (m_workspaceName.starts_with(SPECIAL_QUALIFIER_PREFIX)) { - m_workspaceName = m_workspaceName.substr( - SPECIAL_QUALIFIER_PREFIX_LEN, m_workspaceName.length() - SPECIAL_QUALIFIER_PREFIX_LEN); + if (hasTitle && hasClass) { + m_anyWindowRewriteRuleUsesTitle = true; + return 3; } - - std::size_t spaceFound = m_workspaceName.find(' '); - if (spaceFound != std::string::npos) { - m_workspaceName.erase(m_workspaceName.begin() + spaceFound, m_workspaceName.end()); + if (hasTitle) { + m_anyWindowRewriteRuleUsesTitle = true; + return 2; } -} - -void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { - m_workspaceName = new_workspace_name; + if (hasClass) { + return 1; + } + return 0; } } // namespace waybar::modules::hyprland diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 73062c766..192e6c1ad 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -4,6 +4,7 @@ #include #include +#include #include using namespace waybar::util; @@ -52,10 +53,10 @@ auto waybar::modules::MPD::update() -> void { void waybar::modules::MPD::queryMPD() { if (connection_ != nullptr) { - spdlog::debug("{}: fetching state information", module_name_); + spdlog::trace("{}: fetching state information", module_name_); try { fetchState(); - spdlog::debug("{}: fetch complete", module_name_); + spdlog::trace("{}: fetch complete", module_name_); } catch (std::exception const& e) { spdlog::error("{}: {}", module_name_, e.what()); state_ = MPD_STATE_UNKNOWN; @@ -254,6 +255,21 @@ std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool act } } +static bool isServerUnavailable(const std::error_code& ec) { + if (ec.category() == std::system_category()) { + switch (ec.value()) { + case ECONNREFUSED: + case ECONNRESET: + case ENETDOWN: + case ENETUNREACH: + case EHOSTDOWN: + case ENOENT: + return true; + } + } + return false; +} + void waybar::modules::MPD::tryConnect() { if (connection_ != nullptr) { return; @@ -281,6 +297,11 @@ void waybar::modules::MPD::tryConnect() { } checkErrors(connection_.get()); } + } catch (std::system_error& e) { + /* Tone down logs if it's likely that the mpd server is not running */ + auto level = isServerUnavailable(e.code()) ? spdlog::level::debug : spdlog::level::err; + spdlog::log(level, "{}: Failed to connect to MPD: {}", module_name_, e.what()); + connection_.reset(); } catch (std::runtime_error& e) { spdlog::error("{}: Failed to connect to MPD: {}", module_name_, e.what()); connection_.reset(); @@ -298,6 +319,12 @@ void waybar::modules::MPD::checkErrors(mpd_connection* conn) { connection_.reset(); state_ = MPD_STATE_UNKNOWN; throw std::runtime_error("Connection to MPD closed"); + case MPD_ERROR_SYSTEM: + if (auto ec = mpd_connection_get_system_error(conn); ec != 0) { + mpd_connection_clear_error(conn); + throw std::system_error(ec, std::system_category()); + } + G_GNUC_FALLTHROUGH; default: if (conn) { auto error_message = mpd_connection_get_error_message(conn); diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index aa1a18f8e..3d7c8561e 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -119,7 +119,7 @@ bool Idle::on_io(Glib::IOCondition const&) { void Playing::entry() noexcept { sigc::slot timer_slot = sigc::mem_fun(*this, &Playing::on_timer); - timer_connection_ = Glib::signal_timeout().connect(timer_slot, /* milliseconds */ 1'000); + timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1); spdlog::debug("mpd: Playing: enabled 1 second periodic timer."); } @@ -327,14 +327,20 @@ void Stopped::pause() { void Stopped::update() noexcept { ctx_->do_update(); } -void Disconnected::arm_timer(int interval) noexcept { +bool Disconnected::arm_timer(int interval) noexcept { + // check if it's necessary to modify the timer + if (timer_connection_ && last_interval_ == interval) { + return true; + } // unregister timer, if present disarm_timer(); // register timer + last_interval_ = interval; sigc::slot timer_slot = sigc::mem_fun(*this, &Disconnected::on_timer); - timer_connection_ = Glib::signal_timeout().connect(timer_slot, interval); - spdlog::debug("mpd: Disconnected: enabled interval timer."); + timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, interval); + spdlog::debug("mpd: Disconnected: enabled {}s interval timer.", interval); + return false; } void Disconnected::disarm_timer() noexcept { @@ -347,7 +353,7 @@ void Disconnected::disarm_timer() noexcept { void Disconnected::entry() noexcept { ctx_->emit(); - arm_timer(1'000); + arm_timer(1 /* second */); } void Disconnected::exit() noexcept { disarm_timer(); } @@ -376,9 +382,7 @@ bool Disconnected::on_timer() { spdlog::warn("mpd: Disconnected: error: {}", e.what()); } - arm_timer(ctx_->interval() * 1'000); - - return false; + return arm_timer(ctx_->interval()); } void Disconnected::update() noexcept { ctx_->do_update(); } diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index eea9a82b7..ed383b0ce 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -96,9 +96,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } if (config_["dynamic-order"].isArray()) { dynamic_order_.clear(); - for (auto it = config_["dynamic-order"].begin(); it != config_["dynamic-order"].end(); ++it) { - if (it->isString()) { - dynamic_order_.push_back(it->asString()); + for (const auto& item : config_["dynamic-order"]) { + if (item.isString()) { + dynamic_order_.push_back(item.asString()); } } } @@ -110,10 +110,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) player_ = config_["player"].asString(); } if (config_["ignored-players"].isArray()) { - for (auto it = config_["ignored-players"].begin(); it != config_["ignored-players"].end(); - ++it) { - if (it->isString()) { - ignored_players_.push_back(it->asString()); + for (const auto& item : config_["ignored-players"]) { + if (item.isString()) { + ignored_players_.push_back(item.asString()); } } } @@ -146,8 +145,8 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) throw std::runtime_error(fmt::format("unable to list players: {}", error->message)); } - for (auto p = players; p != NULL; p = p->next) { - auto pn = static_cast(p->data); + for (auto* p = players; p != nullptr; p = p->next) { + auto* pn = static_cast(p->data); if (strcmp(pn->name, player_.c_str()) == 0) { player = playerctl_player_new_from_name(pn, &error); break; @@ -180,17 +179,14 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } Mpris::~Mpris() { - if (manager != NULL) g_object_unref(manager); - if (player != NULL) g_object_unref(player); + if (manager != nullptr) g_object_unref(manager); + if (player != nullptr) g_object_unref(player); } auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) -> std::string { if (icons.isObject()) { - if (icons[key].isString()) { - return icons[key].asString(); - } else if (icons["default"].isString()) { - return icons["default"].asString(); - } + if (icons[key].isString()) return icons[key].asString(); + if (icons["default"].isString()) return icons["default"].asString(); } return ""; } @@ -205,7 +201,7 @@ size_t utf8_truncate(std::string& str, size_t width = std::string::npos) { size_t total_width = 0; - for (gchar *data = str.data(), *end = data + str.size(); data;) { + for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) { gunichar c = g_utf8_get_char_validated(data, end - data); if (c == -1U || c == -2U) { // invalid unicode, treat string as ascii @@ -269,7 +265,7 @@ auto Mpris::getLengthStr(const PlayerInfo& info, bool truncated) -> std::string auto length = info.length.value(); return (truncated && length.substr(0, 3) == "00:") ? length.substr(3) : length; } - return std::string(); + return {}; } auto Mpris::getPositionStr(const PlayerInfo& info, bool truncated) -> std::string { @@ -277,7 +273,7 @@ auto Mpris::getPositionStr(const PlayerInfo& info, bool truncated) -> std::strin auto position = info.position.value(); return (truncated && position.substr(0, 3) == "00:") ? position.substr(3) : position; } - return std::string(); + return {}; } auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> std::string { @@ -319,33 +315,33 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> size_t totalLen = 0; - for (auto it = dynamic_prio_.begin(); it != dynamic_prio_.end(); ++it) { - if (*it == "artist") { + for (const auto& item : dynamic_prio_) { + if (item == "artist") { if (totalLen + artistLen > dynamicLen) { showArtist = false; } else if (showArtist) { totalLen += artistLen; } - } else if (*it == "album") { + } else if (item == "album") { if (totalLen + albumLen > dynamicLen) { showAlbum = false; } else if (showAlbum) { totalLen += albumLen; } - } else if (*it == "title") { + } else if (item == "title") { if (totalLen + titleLen > dynamicLen) { showTitle = false; } else if (showTitle) { totalLen += titleLen; } - } else if (*it == "length") { + } else if (item == "length") { if (totalLen + lengthLen > dynamicLen) { showLength = false; } else if (showLength) { totalLen += lengthLen; posLen = std::max((size_t)2, posLen) - 2; } - } else if (*it == "position") { + } else if (item == "position") { if (totalLen + posLen > dynamicLen) { showPos = false; } else if (showPos) { @@ -406,7 +402,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: name-appeared callback: {}", player_name->name); @@ -415,7 +411,7 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye return; } - mpris->player = playerctl_player_new_from_name(player_name, NULL); + mpris->player = playerctl_player_new_from_name(player_name, nullptr); g_object_connect(mpris->player, "signal::play", G_CALLBACK(onPlayerPlay), mpris, "signal::pause", G_CALLBACK(onPlayerPause), mpris, "signal::stop", G_CALLBACK(onPlayerStop), mpris, "signal::stop", G_CALLBACK(onPlayerStop), mpris, "signal::metadata", @@ -426,19 +422,20 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye auto Mpris::onPlayerNameVanished(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-vanished callback: {}", player_name->name); if (std::string(player_name->name) == mpris->player_) { mpris->player = nullptr; + mpris->event_box_.set_visible(false); mpris->dp.emit(); } } auto Mpris::onPlayerPlay(PlayerctlPlayer* player, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-play callback"); @@ -447,7 +444,7 @@ auto Mpris::onPlayerPlay(PlayerctlPlayer* player, gpointer data) -> void { } auto Mpris::onPlayerPause(PlayerctlPlayer* player, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-pause callback"); @@ -456,7 +453,7 @@ auto Mpris::onPlayerPause(PlayerctlPlayer* player, gpointer data) -> void { } auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-stop callback"); @@ -468,7 +465,7 @@ auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void { } auto Mpris::onPlayerMetadata(PlayerctlPlayer* player, GVariant* metadata, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-metadata callback"); @@ -523,30 +520,30 @@ auto Mpris::getPlayerInfo() -> std::optional { .length = std::nullopt, }; - if (auto artist_ = playerctl_player_get_artist(player, &error)) { + if (auto* artist_ = playerctl_player_get_artist(player, &error)) { spdlog::debug("mpris[{}]: artist = {}", info.name, artist_); info.artist = artist_; g_free(artist_); } if (error) goto errorexit; - if (auto album_ = playerctl_player_get_album(player, &error)) { + if (auto* album_ = playerctl_player_get_album(player, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); info.album = album_; g_free(album_); } if (error) goto errorexit; - if (auto title_ = playerctl_player_get_title(player, &error)) { + if (auto* title_ = playerctl_player_get_title(player, &error)) { spdlog::debug("mpris[{}]: title = {}", info.name, title_); info.title = title_; g_free(title_); } if (error) goto errorexit; - if (auto length_ = playerctl_player_print_metadata_prop(player, "mpris:length", &error)) { + if (auto* length_ = playerctl_player_print_metadata_prop(player, "mpris:length", &error)) { spdlog::debug("mpris[{}]: mpris:length = {}", info.name, length_); - std::chrono::microseconds len = std::chrono::microseconds(std::strtol(length_, nullptr, 10)); + auto len = std::chrono::microseconds(std::strtol(length_, nullptr, 10)); auto len_h = std::chrono::duration_cast(len); auto len_m = std::chrono::duration_cast(len - len_h); auto len_s = std::chrono::duration_cast(len - len_h - len_m); @@ -563,7 +560,7 @@ auto Mpris::getPlayerInfo() -> std::optional { error = nullptr; } else { spdlog::debug("mpris[{}]: position = {}", info.name, position_); - std::chrono::microseconds len = std::chrono::microseconds(position_); + auto len = std::chrono::microseconds(position_); auto len_h = std::chrono::duration_cast(len); auto len_m = std::chrono::duration_cast(len - len_h); auto len_s = std::chrono::duration_cast(len - len_h - len_m); diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp new file mode 100644 index 000000000..eaa470232 --- /dev/null +++ b/src/modules/power_profiles_daemon.cpp @@ -0,0 +1,213 @@ +#include "modules/power_profiles_daemon.hpp" + +#include +#include +#include +#include + +namespace waybar::modules { + +PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Value& config) + : ALabel(config, "power-profiles-daemon", id, "{icon}", 0, false, true), connected_(false) { + if (config_["tooltip-format"].isString()) { + tooltipFormat_ = config_["tooltip-format"].asString(); + } else { + tooltipFormat_ = "Power profile: {profile}\nDriver: {driver}"; + } + // Fasten your seatbelt, we're up for quite a ride. The rest of the + // init is performed asynchronously. There's 2 callbacks involved. + // Here's the overall idea: + // 1. Async connect to the system bus. + // 2. In the system bus connect callback, try to call + // org.freedesktop.DBus.Properties.GetAll to see if + // power-profiles-daemon is able to respond. + // 3. In the GetAll callback, connect the activeProfile monitoring + // callback, consider the init to be successful. Meaning start + // drawing the module. + // + // There's sadly no other way around that, we have to try to call a + // 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 + // 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. + // + // Revisit this in 2026, systems should be updated by then. + + Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::BUS_TYPE_SYSTEM, "net.hadess.PowerProfiles", + "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", + sigc::mem_fun(*this, &PowerProfilesDaemon::busConnectedCb)); + // Schedule update to set the initial visibility + dp.emit(); +} + +void PowerProfilesDaemon::busConnectedCb(Glib::RefPtr& r) { + try { + powerProfilesProxy_ = Gio::DBus::Proxy::create_for_bus_finish(r); + using GetAllProfilesVar = Glib::Variant>; + auto callArgs = GetAllProfilesVar::create(std::make_tuple("net.hadess.PowerProfiles")); + powerProfilesProxy_->call("org.freedesktop.DBus.Properties.GetAll", + sigc::mem_fun(*this, &PowerProfilesDaemon::getAllPropsCb), callArgs); + // Connect active profile callback + } catch (const std::exception& e) { + spdlog::error("Failed to create the power profiles daemon DBus proxy: {}", e.what()); + } catch (const Glib::Error& e) { + spdlog::error("Failed to create the power profiles daemon DBus proxy: {}", + std::string(e.what())); + } +} + +// Callback for the GetAll call. +// +// We're abusing this call to make sure power-profiles-daemon is +// available on the host. We're not really using +void PowerProfilesDaemon::getAllPropsCb(Glib::RefPtr& r) { + try { + auto _ = powerProfilesProxy_->call_finish(r); + // Power-profiles-daemon responded something, we can assume it's + // available, we can safely attach the activeProfile monitoring + // now. + connected_ = true; + powerProfilesProxy_->signal_properties_changed().connect( + sigc::mem_fun(*this, &PowerProfilesDaemon::profileChangedCb)); + populateInitState(); + } catch (const std::exception& err) { + spdlog::error("Failed to query power-profiles-daemon via dbus: {}", err.what()); + } catch (const Glib::Error& err) { + spdlog::error("Failed to query power-profiles-daemon via dbus: {}", std::string(err.what())); + } +} + +void PowerProfilesDaemon::populateInitState() { + // Retrieve current active profile + Glib::Variant profileStr; + powerProfilesProxy_->get_cached_property(profileStr, "ActiveProfile"); + + // Retrieve profiles list, it's aa{sv}. + using ProfilesType = std::vector>>; + Glib::Variant profilesVariant; + powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles"); + for (auto& variantDict : profilesVariant.get()) { + Glib::ustring name; + Glib::ustring driver; + if (auto p = variantDict.find("Profile"); p != variantDict.end()) { + name = p->second.get(); + } + if (auto d = variantDict.find("Driver"); d != variantDict.end()) { + driver = d->second.get(); + } + if (!name.empty()) { + availableProfiles_.emplace_back(std::move(name), std::move(driver)); + } else { + spdlog::error( + "Power profiles daemon: power-profiles-daemon sent us an empty power profile name. " + "Something is wrong."); + } + } + + // Find the index of the current activated mode (to toggle) + std::string str = profileStr.get(); + switchToProfile(str); +} + +void PowerProfilesDaemon::profileChangedCb( + const Gio::DBus::Proxy::MapChangedProperties& changedProperties, + const std::vector& invalidatedProperties) { + // We're likely connected if this callback gets triggered. + // But better be safe than sorry. + if (connected_) { + if (auto activeProfileVariant = changedProperties.find("ActiveProfile"); + activeProfileVariant != changedProperties.end()) { + std::string activeProfile = + Glib::VariantBase::cast_dynamic>(activeProfileVariant->second) + .get(); + switchToProfile(activeProfile); + } + } +} + +// Look for the profile str in our internal profiles list. Using a +// vector to store the profiles ain't the smartest move +// complexity-wise, but it makes toggling between the mode easy. This +// vector is 3 elements max, we'll be fine :P +void PowerProfilesDaemon::switchToProfile(std::string const& str) { + auto pred = [str](Profile const& p) { return p.name == str; }; + this->activeProfile_ = std::find_if(availableProfiles_.begin(), availableProfiles_.end(), pred); + if (activeProfile_ == availableProfiles_.end()) { + spdlog::error( + "Power profile daemon: can't find the active profile {} in the available profiles list", + str); + } + dp.emit(); +} + +auto PowerProfilesDaemon::update() -> void { + if (connected_ && activeProfile_ != availableProfiles_.end()) { + auto profile = (*activeProfile_); + // Set label + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("profile", profile.name)); + store.push_back(fmt::arg("driver", profile.driver)); + store.push_back(fmt::arg("icon", getIcon(0, profile.name))); + label_.set_markup(fmt::vformat(format_, store)); + if (tooltipEnabled()) { + label_.set_tooltip_text(fmt::vformat(tooltipFormat_, store)); + } + + // Set CSS class + if (!currentStyle_.empty()) { + label_.get_style_context()->remove_class(currentStyle_); + } + label_.get_style_context()->add_class(profile.name); + currentStyle_ = profile.name; + event_box_.set_visible(true); + } else { + event_box_.set_visible(false); + } + + ALabel::update(); +} + +bool PowerProfilesDaemon::handleToggle(GdkEventButton* const& e) { + if (e->type == GdkEventType::GDK_BUTTON_PRESS && connected_) { + if (e->button == 1) /* left click */ { + activeProfile_++; + if (activeProfile_ == availableProfiles_.end()) { + activeProfile_ = availableProfiles_.begin(); + } + } else { + if (activeProfile_ == availableProfiles_.begin()) { + activeProfile_ = availableProfiles_.end(); + } + activeProfile_--; + } + + using VarStr = Glib::Variant; + using SetPowerProfileVar = Glib::Variant>; + VarStr activeProfileVariant = VarStr::create(activeProfile_->name); + auto callArgs = SetPowerProfileVar::create( + std::make_tuple("net.hadess.PowerProfiles", "ActiveProfile", activeProfileVariant)); + powerProfilesProxy_->call("org.freedesktop.DBus.Properties.Set", + sigc::mem_fun(*this, &PowerProfilesDaemon::setPropCb), callArgs); + } + return true; +} + +void PowerProfilesDaemon::setPropCb(Glib::RefPtr& r) { + try { + auto _ = powerProfilesProxy_->call_finish(r); + dp.emit(); + } catch (const std::exception& e) { + spdlog::error("Failed to set the the active power profile: {}", e.what()); + } catch (const Glib::Error& e) { + spdlog::error("Failed to set the active power profile: {}", std::string(e.what())); + } +} + +} // namespace waybar::modules diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 64a1572b3..97996c336 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -1,16 +1,11 @@ #include "modules/privacy/privacy.hpp" -#include #include -#include #include -#include -#include #include #include "AModule.hpp" -#include "gtkmm/image.h" #include "modules/privacy/privacy_item.hpp" namespace waybar::modules::privacy { @@ -50,32 +45,30 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, const std::st // Initialize each privacy module Json::Value modules = config_["modules"]; // Add Screenshare and Mic usage as default modules if none are specified - if (!modules.isArray() || modules.size() == 0) { + if (!modules.isArray() || modules.empty()) { modules = Json::Value(Json::arrayValue); - for (auto& type : {"screenshare", "audio-in"}) { + for (const auto& type : {"screenshare", "audio-in"}) { Json::Value obj = Json::Value(Json::objectValue); obj["type"] = type; modules.append(obj); } } - for (uint i = 0; i < modules.size(); i++) { - const Json::Value& module_config = modules[i]; - if (!module_config.isObject() || !module_config["type"].isString()) continue; - const std::string type = module_config["type"].asString(); - if (type == "screenshare") { - auto item = - Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_VIDEO_INPUT, - &nodes_screenshare, pos, iconSize, transition_duration); - box_.add(*item); - } else if (type == "audio-in") { - auto item = - Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_AUDIO_INPUT, - &nodes_audio_in, pos, iconSize, transition_duration); - box_.add(*item); - } else if (type == "audio-out") { - auto item = - Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_AUDIO_OUTPUT, - &nodes_audio_out, pos, iconSize, transition_duration); + + std::map > typeMap = { + {"screenshare", {&nodes_screenshare, PRIVACY_NODE_TYPE_VIDEO_INPUT}}, + {"audio-in", {&nodes_audio_in, PRIVACY_NODE_TYPE_AUDIO_INPUT}}, + {"audio-out", {&nodes_audio_out, PRIVACY_NODE_TYPE_AUDIO_OUTPUT}}, + }; + + for (const auto& module : modules) { + if (!module.isObject() || !module["type"].isString()) continue; + const std::string type = module["type"].asString(); + + auto iter = typeMap.find(type); + if (iter != typeMap.end()) { + auto& [nodePtr, nodeType] = iter->second; + auto* item = Gtk::make_managed(module, nodeType, nodePtr, pos, iconSize, + transition_duration); box_.add(*item); } } @@ -120,24 +113,35 @@ void Privacy::onPrivacyNodesChanged() { } auto Privacy::update() -> void { - mutex_.lock(); - bool screenshare, audio_in, audio_out; + // set in modules or not + bool setScreenshare = false; + bool setAudioIn = false; + bool setAudioOut = false; + // used or not + bool useScreenshare = false; + bool useAudioIn = false; + bool useAudioOut = false; + + mutex_.lock(); for (Gtk::Widget* widget : box_.get_children()) { - PrivacyItem* module = dynamic_cast(widget); - if (!module) continue; + auto* module = dynamic_cast(widget); + if (module == nullptr) continue; switch (module->privacy_type) { case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: - screenshare = !nodes_screenshare.empty(); - module->set_in_use(screenshare); + setScreenshare = true; + useScreenshare = !nodes_screenshare.empty(); + module->set_in_use(useScreenshare); break; case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: - audio_in = !nodes_audio_in.empty(); - module->set_in_use(audio_in); + setAudioIn = true; + useAudioIn = !nodes_audio_in.empty(); + module->set_in_use(useAudioIn); break; case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: - audio_out = !nodes_audio_out.empty(); - module->set_in_use(audio_out); + setAudioOut = true; + useAudioOut = !nodes_audio_out.empty(); + module->set_in_use(useAudioOut); break; case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: break; @@ -146,25 +150,28 @@ auto Privacy::update() -> void { mutex_.unlock(); // Hide the whole widget if none are in use - bool is_visible = screenshare || audio_in || audio_out; - if (is_visible != event_box_.get_visible()) { + bool isVisible = (setScreenshare && useScreenshare) || (setAudioIn && useAudioIn) || + (setAudioOut && useAudioOut); + + if (isVisible != event_box_.get_visible()) { // Disconnect any previous connection so that it doesn't get activated in // the future, hiding the module when it should be visible visibility_conn.disconnect(); - if (is_visible) { + if (isVisible) { event_box_.set_visible(true); } else { // Hides the widget when all of the privacy_item revealers animations // have finished animating visibility_conn = Glib::signal_timeout().connect( sigc::track_obj( - [this] { + [this, setScreenshare, setAudioOut, setAudioIn]() { mutex_.lock(); - bool screenshare = !nodes_screenshare.empty(); - bool audio_in = !nodes_audio_in.empty(); - bool audio_out = !nodes_audio_out.empty(); + bool visible = false; + visible |= setScreenshare && !nodes_screenshare.empty(); + visible |= setAudioIn && !nodes_audio_in.empty(); + visible |= setAudioOut && !nodes_audio_out.empty(); mutex_.unlock(); - event_box_.set_visible(screenshare || audio_in || audio_out); + event_box_.set_visible(visible); return false; }, *this), diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index a0a2da573..a38b95a4a 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -1,23 +1,11 @@ #include "modules/privacy/privacy_item.hpp" -#include -#include -#include - -#include -#include #include -#include -#include "AModule.hpp" #include "glibmm/main.h" -#include "glibmm/priorities.h" -#include "gtkmm/enums.h" #include "gtkmm/label.h" #include "gtkmm/revealer.h" #include "gtkmm/tooltip.h" -#include "sigc++/adaptors/bind.h" -#include "util/gtk_icon.hpp" #include "util/pipewire/privacy_node_info.hpp" namespace waybar::modules::privacy { @@ -98,7 +86,7 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac void PrivacyItem::update_tooltip() { // Removes all old nodes - for (auto child : tooltip_window.get_children()) { + for (auto *child : tooltip_window.get_children()) { delete child; } @@ -108,12 +96,12 @@ void PrivacyItem::update_tooltip() { // Set device icon Gtk::Image *node_icon = new Gtk::Image(); node_icon->set_pixel_size(tooltipIconSize); - node_icon->set_from_icon_name(node->get_icon_name(), Gtk::ICON_SIZE_INVALID); + node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID); box->add(*node_icon); // Set model - Gtk::Label *node_name = new Gtk::Label(node->get_name()); - box->add(*node_name); + auto *nodeName = new Gtk::Label(node->getName()); + box->add(*nodeName); tooltip_window.add(*box); } diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index d7dc80d36..3efd9d232 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -42,15 +42,27 @@ static const std::array ports = { }; const std::vector waybar::modules::Pulseaudio::getPulseIcon() const { - std::vector res = {backend->getCurrentSinkName(), backend->getDefaultSourceName()}; + std::vector res; + auto sink_muted = backend->getSinkMuted(); + if (sink_muted) { + res.emplace_back(backend->getCurrentSinkName() + "-muted"); + } + res.push_back(backend->getCurrentSinkName()); + res.push_back(backend->getDefaultSourceName()); std::string nameLC = backend->getSinkPortName() + backend->getFormFactor(); std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower); for (auto const &port : ports) { if (nameLC.find(port) != std::string::npos) { + if (sink_muted) { + res.emplace_back(port + "-muted"); + } res.push_back(port); - return res; + break; } } + if (sink_muted) { + res.emplace_back("default-muted"); + } return res; } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index c3de2357f..b5c0dd85f 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -8,6 +8,7 @@ #include #include +#include "gdk/gdk.h" #include "util/format.hpp" #include "util/gtk_icon.hpp" @@ -57,6 +58,8 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf event_box.add_events(Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); event_box.signal_button_press_event().connect(sigc::mem_fun(*this, &Item::handleClick)); event_box.signal_scroll_event().connect(sigc::mem_fun(*this, &Item::handleScroll)); + event_box.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseEnter)); + event_box.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseLeave)); // initial visibility event_box.show_all(); event_box.set_visible(show_passive_); @@ -69,6 +72,16 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf cancellable_, interface); } +bool Item::handleMouseEnter(GdkEventCrossing* const& e) { + event_box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + return false; +} + +bool Item::handleMouseLeave(GdkEventCrossing* const& e) { + event_box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + return false; +} + void Item::onConfigure(GdkEventConfigure* ev) { this->updateImage(); } void Item::proxyReady(Glib::RefPtr& result) { diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index a5860bd09..a005df17c 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -19,6 +19,7 @@ const std::string Language::XKB_ACTIVE_LAYOUT_NAME_KEY = "xkb_active_layout_name Language::Language(const std::string& id, const Json::Value& config) : ALabel(config, "language", id, "{}", 0, true) { + 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); @@ -95,6 +96,10 @@ void Language::onEvent(const struct Ipc::ipc_response& res) { auto Language::update() -> void { std::lock_guard lock(mutex_); + if (hide_single_ && layouts_map_.size() <= 1) { + event_box_.hide(); + return; + } auto display_layout = trim(fmt::format( fmt::runtime(format_), fmt::arg("short", layout_.short_name), fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name), diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 1bc9f382c..2adde69ca 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -107,11 +107,16 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) { auto payload = parser_.parse(res.payload); workspaces_.clear(); std::vector outputs; + bool alloutputs = config_["all-outputs"].asBool(); std::copy_if(payload["nodes"].begin(), payload["nodes"].end(), std::back_inserter(outputs), - [&](const auto &workspace) { - return !config_["all-outputs"].asBool() - ? workspace["name"].asString() == bar_.output->name - : true; + [&](const auto &output) { + if (alloutputs && output["name"].asString() != "__i3") { + return true; + } + if (output["name"].asString() == bar_.output->name) { + return true; + } + return false; }); for (auto &output : outputs) { @@ -136,12 +141,12 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) { for (const std::string &p_w_name : p_workspaces_names) { const Json::Value &p_w = p_workspaces[p_w_name]; - auto it = - std::find_if(payload.begin(), payload.end(), [&p_w_name](const Json::Value &node) { - return node["name"].asString() == p_w_name; - }); + auto it = std::find_if(workspaces_.begin(), workspaces_.end(), + [&p_w_name](const Json::Value &node) { + return node["name"].asString() == p_w_name; + }); - if (it != payload.end()) { + if (it != workspaces_.end()) { continue; // already displayed by some bar } @@ -253,11 +258,14 @@ bool Workspaces::hasFlag(const Json::Value &node, const std::string &flag) { [&](auto const &e) { return hasFlag(e, flag); })) { return true; } + if (std::any_of(node["floating_nodes"].begin(), node["floating_nodes"].end(), + [&](auto const &e) { return hasFlag(e, flag); })) { + return true; + } return false; } void Workspaces::updateWindows(const Json::Value &node, std::string &windows) { - auto format = config_["window-format"].asString(); if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") && node["name"].isString()) { std::string title = g_markup_escape_text(node["name"].asString().c_str(), -1); @@ -290,12 +298,13 @@ auto Workspaces::update() -> void { if (needReorder) { box_.reorder_child(button, it - workspaces_.begin()); } + bool noNodes = (*it)["nodes"].empty() && (*it)["floating_nodes"].empty(); if (hasFlag((*it), "focused")) { button.get_style_context()->add_class("focused"); } else { button.get_style_context()->remove_class("focused"); } - if (hasFlag((*it), "visible")) { + if (hasFlag((*it), "visible") || ((*it)["output"].isString() && noNodes)) { button.get_style_context()->add_class("visible"); } else { button.get_style_context()->remove_class("visible"); @@ -305,11 +314,16 @@ auto Workspaces::update() -> void { } else { button.get_style_context()->remove_class("urgent"); } - if (hasFlag((*it), "target_output")) { + if ((*it)["target_output"].isString()) { button.get_style_context()->add_class("persistent"); } else { button.get_style_context()->remove_class("persistent"); } + if (noNodes) { + button.get_style_context()->add_class("empty"); + } else { + button.get_style_context()->remove_class("empty"); + } if ((*it)["output"].isString()) { if (((*it)["output"].asString()) == bar_.output->name) { button.get_style_context()->add_class("current_output"); @@ -392,7 +406,7 @@ std::string Workspaces::getIcon(const std::string &name, const Json::Value &node } } if (key == "focused" || key == "urgent") { - if (config_["format-icons"][key].isString() && node[key].asBool()) { + if (config_["format-icons"][key].isString() && hasFlag(node, key)) { return config_["format-icons"][key].asString(); } } else if (config_["format-icons"]["persistent"].isString() && @@ -420,9 +434,16 @@ bool Workspaces::handleScroll(GdkEventScroll *e) { } std::string name; { + bool alloutputs = config_["all-outputs"].asBool(); std::lock_guard lock(mutex_); - auto it = std::find_if(workspaces_.begin(), workspaces_.end(), - [](const auto &workspace) { return workspace["focused"].asBool(); }); + auto it = + std::find_if(workspaces_.begin(), workspaces_.end(), [alloutputs](const auto &workspace) { + if (alloutputs) { + return hasFlag(workspace, "focused"); + } + bool noNodes = workspace["nodes"].empty() && workspace["floating_nodes"].empty(); + return hasFlag(workspace, "visible") || (workspace["output"].isString() && noNodes); + }); if (it == workspaces_.end()) { return true; } @@ -480,7 +501,14 @@ std::string Workspaces::trimWorkspaceName(std::string name) { void Workspaces::onButtonReady(const Json::Value &node, Gtk::Button &button) { if (config_["current-only"].asBool()) { - if (node["focused"].asBool()) { + // 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. + bool focused = node["focused"].asBool() || + std::any_of(node["nodes"].begin(), node["nodes"].end(), + [](const auto &child) { return child["focused"].asBool(); }); + + if (focused) { button.show(); } else { button.hide(); diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index accab969c..889109826 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -1,6 +1,7 @@ #include "modules/temperature.hpp" #include +#include #if defined(__FreeBSD__) #include @@ -9,39 +10,53 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Value& config) : ALabel(config, "temperature", id, "{temperatureC}°C", 10) { #if defined(__FreeBSD__) -// try to read sysctl? +// FreeBSD uses sysctlbyname instead of read from a file #else - auto& hwmon_path = config_["hwmon-path"]; - if (hwmon_path.isString()) { - file_path_ = hwmon_path.asString(); - } else if (hwmon_path.isArray()) { - // if hwmon_path is an array, loop to find first valid item - for (auto& item : hwmon_path) { - auto path = item.asString(); - if (std::filesystem::exists(path)) { - file_path_ = path; - break; - } - } - } else if (config_["hwmon-path-abs"].isString() && config_["input-filename"].isString()) { - for (const auto& hwmon : - std::filesystem::directory_iterator(config_["hwmon-path-abs"].asString())) { - if (hwmon.path().filename().string().starts_with("hwmon")) { - file_path_ = hwmon.path().string() + "/" + config_["input-filename"].asString(); - break; - } - } + auto traverseAsArray = [](const Json::Value& value, auto&& check_set_path) { + if (value.isString()) + check_set_path(value.asString()); + else if (value.isArray()) + for (const auto& item : value) + if (check_set_path(item.asString())) break; + }; + + // if hwmon_path is an array, loop to find first valid item + traverseAsArray(config_["hwmon-path"], [this](const std::string& path) { + if (!std::filesystem::exists(path)) return false; + file_path_ = path; + return true; + }); + + if (file_path_.empty() && config_["input-filename"].isString()) { + // fallback to hwmon_paths-abs + traverseAsArray(config_["hwmon-path-abs"], [this](const std::string& path) { + if (!std::filesystem::is_directory(path)) return false; + return std::ranges::any_of( + std::filesystem::directory_iterator(path), [this](const auto& hwmon) { + if (!hwmon.path().filename().string().starts_with("hwmon")) return false; + file_path_ = hwmon.path().string() + "/" + config_["input-filename"].asString(); + return true; + }); + }); } if (file_path_.empty()) { auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); } + + // check if file_path_ can be used to retrive the temperature std::ifstream temp(file_path_); if (!temp.is_open()) { throw std::runtime_error("Can't open " + file_path_); } + if (!temp.good()) { + temp.close(); + throw std::runtime_error("Can't read from " + file_path_); + } + temp.close(); #endif + thread_ = [this] { dp.emit(); thread_.sleep_for(interval_); @@ -93,11 +108,11 @@ float waybar::modules::Temperature::getTemperature() { size_t size = sizeof temp; auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; - auto sysctl_thermal = fmt::format("hw.acpi.thermal.tz{}.temperature", zone); - if (sysctlbyname("hw.acpi.thermal.tz0.temperature", &temp, &size, NULL, 0) != 0) { - throw std::runtime_error( - "sysctl hw.acpi.thermal.tz0.temperature or dev.cpu.0.temperature failed"); + if (sysctlbyname(fmt::format("hw.acpi.thermal.tz{}.temperature", zone).c_str(), &temp, &size, + NULL, 0) != 0) { + throw std::runtime_error(fmt::format( + "sysctl hw.acpi.thermal.tz{}.temperature or dev.cpu.{}.temperature failed", zone, zone)); } auto temperature_c = ((float)temp - 2732) / 10; return temperature_c; @@ -110,6 +125,9 @@ float waybar::modules::Temperature::getTemperature() { std::string line; if (temp.good()) { getline(temp, line); + } else { + temp.close(); + throw std::runtime_error("Can't read from " + file_path_); } temp.close(); auto temperature_c = std::strtol(line.c_str(), nullptr, 10) / 1000.0; diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp new file mode 100644 index 000000000..552495f85 --- /dev/null +++ b/src/modules/upower.cpp @@ -0,0 +1,496 @@ +#include "modules/upower.hpp" + +#include +#include +#include + +namespace waybar::modules { + +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 + : Gtk::ORIENTATION_HORIZONTAL); + // Get current theme + gtkTheme_ = Gtk::IconTheme::get_default(); + + // Icon Size + if (config_["icon-size"].isInt()) { + iconSize_ = config_["icon-size"].asInt(); + } + image_.set_pixel_size(iconSize_); + + // Show icon only when "show-icon" isn't set to false + if (config_["show-icon"].isBool()) showIcon_ = config_["show-icon"].asBool(); + if (!showIcon_) box_.remove(image_); + // Device user wants + if (config_["native-path"].isString()) nativePath_ = config_["native-path"].asString(); + // Device model user wants + if (config_["model"].isString()) model_ = config_["model"].asString(); + + // Hide If Empty + if (config_["hide-if-empty"].isBool()) hideIfEmpty_ = config_["hide-if-empty"].asBool(); + + // Tooltip Spacing + if (config_["tooltip-spacing"].isInt()) tooltip_spacing_ = config_["tooltip-spacing"].asInt(); + + // Tooltip Padding + if (config_["tooltip-padding"].isInt()) { + tooltip_padding_ = config_["tooltip-padding"].asInt(); + contentBox_.set_margin_top(tooltip_padding_); + contentBox_.set_margin_bottom(tooltip_padding_); + contentBox_.set_margin_left(tooltip_padding_); + contentBox_.set_margin_right(tooltip_padding_); + } + + // Tooltip Format + if (config_["tooltip-format"].isString()) tooltipFormat_ = config_["tooltip-format"].asString(); + + // Start watching DBUS + watcherID_ = Gio::DBus::watch_name( + Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.UPower", + sigc::mem_fun(*this, &UPower::onAppear), sigc::mem_fun(*this, &UPower::onVanished), + Gio::DBus::BusNameWatcherFlags::BUS_NAME_WATCHER_FLAGS_AUTO_START); + // Get DBus async connect + Gio::DBus::Connection::get(Gio::DBus::BusType::BUS_TYPE_SYSTEM, + sigc::mem_fun(*this, &UPower::getConn_cb)); + + // Make UPower client + GError **gErr = NULL; + upClient_ = up_client_new_full(NULL, gErr); + if (upClient_ == NULL) + spdlog::error("Upower. UPower client connection error. {}", (*gErr)->message); + + // Subscribe UPower events + g_signal_connect(upClient_, "device-added", G_CALLBACK(deviceAdded_cb), this); + 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); + + resetDevices(); + setDisplayDevice(); + // Update the widget + dp.emit(); +} + +UPower::~UPower() { + if (upDevice_.upDevice != NULL) g_object_unref(upDevice_.upDevice); + if (upClient_ != NULL) g_object_unref(upClient_); + if (subscrID_ > 0u) { + conn_->signal_unsubscribe(subscrID_); + subscrID_ = 0u; + } + Gio::DBus::unwatch_name(watcherID_); + watcherID_ = 0u; + removeDevices(); +} + +static const std::string getDeviceStatus(UpDeviceState &state) { + switch (state) { + case UP_DEVICE_STATE_CHARGING: + case UP_DEVICE_STATE_PENDING_CHARGE: + return "charging"; + case UP_DEVICE_STATE_DISCHARGING: + case UP_DEVICE_STATE_PENDING_DISCHARGE: + return "discharging"; + case UP_DEVICE_STATE_FULLY_CHARGED: + return "full"; + case UP_DEVICE_STATE_EMPTY: + return "empty"; + default: + return "unknown-status"; + } +} + +static const std::string getDeviceIcon(UpDeviceKind &kind) { + switch (kind) { + case UP_DEVICE_KIND_LINE_POWER: + return "ac-adapter-symbolic"; + case UP_DEVICE_KIND_BATTERY: + return "battery-symbolic"; + case UP_DEVICE_KIND_UPS: + return "uninterruptible-power-supply-symbolic"; + case UP_DEVICE_KIND_MONITOR: + return "video-display-symbolic"; + case UP_DEVICE_KIND_MOUSE: + return "input-mouse-symbolic"; + case UP_DEVICE_KIND_KEYBOARD: + return "input-keyboard-symbolic"; + case UP_DEVICE_KIND_PDA: + return "pda-symbolic"; + case UP_DEVICE_KIND_PHONE: + return "phone-symbolic"; + case UP_DEVICE_KIND_MEDIA_PLAYER: + return "multimedia-player-symbolic"; + case UP_DEVICE_KIND_TABLET: + return "computer-apple-ipad-symbolic"; + case UP_DEVICE_KIND_COMPUTER: + return "computer-symbolic"; + case UP_DEVICE_KIND_GAMING_INPUT: + return "input-gaming-symbolic"; + case UP_DEVICE_KIND_PEN: + return "input-tablet-symbolic"; + case UP_DEVICE_KIND_TOUCHPAD: + return "input-touchpad-symbolic"; + case UP_DEVICE_KIND_MODEM: + return "modem-symbolic"; + case UP_DEVICE_KIND_NETWORK: + return "network-wired-symbolic"; + case UP_DEVICE_KIND_HEADSET: + return "audio-headset-symbolic"; + case UP_DEVICE_KIND_HEADPHONES: + return "audio-headphones-symbolic"; + case UP_DEVICE_KIND_OTHER_AUDIO: + case UP_DEVICE_KIND_SPEAKERS: + return "audio-speakers-symbolic"; + case UP_DEVICE_KIND_VIDEO: + return "camera-web-symbolic"; + case UP_DEVICE_KIND_PRINTER: + return "printer-symbolic"; + case UP_DEVICE_KIND_SCANNER: + return "scanner-symbolic"; + case UP_DEVICE_KIND_CAMERA: + return "camera-photo-symbolic"; + case UP_DEVICE_KIND_BLUETOOTH_GENERIC: + return "bluetooth-active-symbolic"; + case UP_DEVICE_KIND_TOY: + case UP_DEVICE_KIND_REMOTE_CONTROL: + case UP_DEVICE_KIND_WEARABLE: + case UP_DEVICE_KIND_LAST: + default: + return "battery-symbolic"; + } +} + +static std::string secondsToString(const std::chrono::seconds sec) { + const auto ds{std::chrono::duration_cast(sec)}; + const auto hrs{std::chrono::duration_cast(sec - ds)}; + const auto min{std::chrono::duration_cast(sec - ds - hrs)}; + std::string_view strRet{(ds.count() > 0) ? "{D}d {H}h {M}min" + : (hrs.count() > 0) ? "{H}h {M}min" + : (min.count() > 0) ? "{M}min" + : ""}; + spdlog::debug( + "UPower::secondsToString(). seconds: \"{0}\", minutes: \"{1}\", hours: \"{2}\", \ +days: \"{3}\", strRet: \"{4}\"", + sec.count(), min.count(), hrs.count(), ds.count(), strRet); + return fmt::format(fmt::runtime(strRet), fmt::arg("D", ds.count()), fmt::arg("H", hrs.count()), + fmt::arg("M", min.count())); +} + +auto UPower::update() -> void { + std::lock_guard guard{mutex_}; + // Don't update widget if the UPower service isn't running + if (!upRunning_ || sleeping_) { + if (hideIfEmpty_) box_.hide(); + return; + } + + getUpDeviceInfo(upDevice_); + + if (upDevice_.upDevice == NULL && hideIfEmpty_) { + box_.hide(); + return; + } + /* Every Device which is handled by Upower and which is not + * UP_DEVICE_KIND_UNKNOWN (0) or UP_DEVICE_KIND_LINE_POWER (1) is a Battery + */ + const bool upDeviceValid{upDevice_.kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN && + upDevice_.kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER}; + // Get CSS status + const auto status{getDeviceStatus(upDevice_.state)}; + // Remove last status if it exists + if (!lastStatus_.empty() && box_.get_style_context()->has_class(lastStatus_)) + box_.get_style_context()->remove_class(lastStatus_); + if (!box_.get_style_context()->has_class(status)) box_.get_style_context()->add_class(status); + lastStatus_ = status; + + if (devices_.size() == 0 && !upDeviceValid && hideIfEmpty_) { + box_.hide(); + // Call parent update + AModule::update(); + return; + } + + label_.set_markup(getText(upDevice_, format_)); + // Set icon + if (upDevice_.icon_name == NULL || !gtkTheme_->has_icon(upDevice_.icon_name)) + upDevice_.icon_name = (char *)NO_BATTERY.c_str(); + image_.set_from_icon_name(upDevice_.icon_name, Gtk::ICON_SIZE_INVALID); + + box_.show(); + + // Call parent update + ALabel::update(); +} + +void UPower::getConn_cb(Glib::RefPtr &result) { + try { + conn_ = Gio::DBus::Connection::get_finish(result); + // Subscribe DBUs events + subscrID_ = conn_->signal_subscribe(sigc::mem_fun(*this, &UPower::prepareForSleep_cb), + "org.freedesktop.login1", "org.freedesktop.login1.Manager", + "PrepareForSleep", "/org/freedesktop/login1"); + + } catch (const Glib::Error &e) { + spdlog::error("Upower. DBus connection error. {}", e.what().c_str()); + } +} + +void UPower::onAppear(const Glib::RefPtr &conn, const Glib::ustring &name, + const Glib::ustring &name_owner) { + upRunning_ = true; +} + +void UPower::onVanished(const Glib::RefPtr &conn, + const Glib::ustring &name) { + upRunning_ = false; +} + +void UPower::prepareForSleep_cb(const Glib::RefPtr &connection, + const Glib::ustring &sender_name, const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters) { + if (parameters.is_of_type(Glib::VariantType("(b)"))) { + Glib::Variant sleeping; + parameters.get_child(sleeping, 0); + if (!sleeping.get()) { + resetDevices(); + setDisplayDevice(); + sleeping_ = false; + // Update the widget + dp.emit(); + } else + sleeping_ = true; + } +} + +void UPower::deviceAdded_cb(UpClient *client, UpDevice *device, gpointer data) { + UPower *up{static_cast(data)}; + up->addDevice(device); + up->setDisplayDevice(); + // Update the widget + up->dp.emit(); +} + +void UPower::deviceRemoved_cb(UpClient *client, const gchar *objectPath, gpointer data) { + UPower *up{static_cast(data)}; + up->removeDevice(objectPath); + up->setDisplayDevice(); + // Update the widget + up->dp.emit(); +} + +void UPower::deviceNotify_cb(UpDevice *device, GParamSpec *pspec, gpointer data) { + UPower *up{static_cast(data)}; + // Update the widget + up->dp.emit(); +} + +void UPower::addDevice(UpDevice *device) { + std::lock_guard guard{mutex_}; + + if (G_IS_OBJECT(device)) { + const gchar *objectPath{up_device_get_object_path(device)}; + + // Due to the device getting cleared after this event is fired, we + // create a new object pointing to its objectPath + device = up_device_new(); + upDevice_output upDevice{.upDevice = device}; + gboolean ret{up_device_set_object_path_sync(device, objectPath, NULL, NULL)}; + if (!ret) { + g_object_unref(G_OBJECT(device)); + return; + } + + if (devices_.find(objectPath) != devices_.cend()) { + auto upDevice{devices_[objectPath]}; + if (G_IS_OBJECT(upDevice.upDevice)) g_object_unref(upDevice.upDevice); + devices_.erase(objectPath); + } + + g_signal_connect(device, "notify", G_CALLBACK(deviceNotify_cb), this); + devices_.emplace(Devices::value_type(objectPath, upDevice)); + } +} + +void UPower::removeDevice(const gchar *objectPath) { + std::lock_guard guard{mutex_}; + if (devices_.find(objectPath) != devices_.cend()) { + auto upDevice{devices_[objectPath]}; + if (G_IS_OBJECT(upDevice.upDevice)) g_object_unref(upDevice.upDevice); + devices_.erase(objectPath); + } +} + +void UPower::removeDevices() { + std::lock_guard guard{mutex_}; + if (!devices_.empty()) { + auto it{devices_.cbegin()}; + while (it != devices_.cend()) { + if (G_IS_OBJECT(it->second.upDevice)) g_object_unref(it->second.upDevice); + devices_.erase(it++); + } + } +} + +// Removes all devices and adds the current devices +void UPower::resetDevices() { + // Remove all devices + removeDevices(); + + // Adds all devices + GPtrArray *newDevices = up_client_get_devices2(upClient_); + if (newDevices != NULL) + for (guint i{0}; i < newDevices->len; ++i) { + UpDevice *device{(UpDevice *)g_ptr_array_index(newDevices, i)}; + if (device && G_IS_OBJECT(device)) addDevice(device); + } +} + +void UPower::setDisplayDevice() { + std::lock_guard guard{mutex_}; + + if (nativePath_.empty() && model_.empty()) { + // Unref current upDevice + if (upDevice_.upDevice != NULL) g_object_unref(upDevice_.upDevice); + + upDevice_.upDevice = up_client_get_display_device(upClient_); + getUpDeviceInfo(upDevice_); + } else { + g_ptr_array_foreach( + up_client_get_devices2(upClient_), + [](gpointer data, gpointer user_data) { + upDevice_output upDevice; + auto thisPtr{static_cast(user_data)}; + upDevice.upDevice = static_cast(data); + thisPtr->getUpDeviceInfo(upDevice); + upDevice_output displayDevice{NULL}; + if (!thisPtr->nativePath_.empty()) { + if (upDevice.nativePath == nullptr) return; + if (0 == std::strcmp(upDevice.nativePath, thisPtr->nativePath_.c_str())) { + displayDevice = upDevice; + } + } else { + if (upDevice.model == nullptr) return; + if (0 == std::strcmp(upDevice.model, thisPtr->model_.c_str())) { + displayDevice = upDevice; + } + } + // Unref current upDevice + if (displayDevice.upDevice != NULL) g_object_unref(thisPtr->upDevice_.upDevice); + // Reassign new upDevice + thisPtr->upDevice_ = displayDevice; + }, + this); + } + + if (upDevice_.upDevice != NULL) + g_signal_connect(upDevice_.upDevice, "notify", G_CALLBACK(deviceNotify_cb), this); +} + +void UPower::getUpDeviceInfo(upDevice_output &upDevice_) { + if (upDevice_.upDevice != NULL && G_IS_OBJECT(upDevice_.upDevice)) { + g_object_get(upDevice_.upDevice, "kind", &upDevice_.kind, "state", &upDevice_.state, + "percentage", &upDevice_.percentage, "icon-name", &upDevice_.icon_name, + "time-to-empty", &upDevice_.time_empty, "time-to-full", &upDevice_.time_full, + "temperature", &upDevice_.temperature, "native-path", &upDevice_.nativePath, + "model", &upDevice_.model, NULL); + spdlog::debug( + "UPower. getUpDeviceInfo. kind: \"{0}\". state: \"{1}\". percentage: \"{2}\". \ +icon_name: \"{3}\". time-to-empty: \"{4}\". time-to-full: \"{5}\". temperature: \"{6}\". \ +native_path: \"{7}\". model: \"{8}\"", + fmt::format_int(upDevice_.kind).str(), fmt::format_int(upDevice_.state).str(), + upDevice_.percentage, upDevice_.icon_name, upDevice_.time_empty, upDevice_.time_full, + upDevice_.temperature, upDevice_.nativePath, upDevice_.model); + } +} + +const Glib::ustring UPower::getText(const upDevice_output &upDevice_, const std::string &format) { + Glib::ustring ret{""}; + if (upDevice_.upDevice != NULL) { + std::string timeStr{""}; + switch (upDevice_.state) { + case UP_DEVICE_STATE_CHARGING: + case UP_DEVICE_STATE_PENDING_CHARGE: + timeStr = secondsToString(std::chrono::seconds(upDevice_.time_full)); + break; + case UP_DEVICE_STATE_DISCHARGING: + case UP_DEVICE_STATE_PENDING_DISCHARGE: + timeStr = secondsToString(std::chrono::seconds(upDevice_.time_empty)); + break; + default: + break; + } + + ret = fmt::format( + fmt::runtime(format), + fmt::arg("percentage", std::to_string((int)std::round(upDevice_.percentage)) + '%'), + fmt::arg("time", timeStr), + fmt::arg("temperature", fmt::format("{:-.2g}C", upDevice_.temperature)), + fmt::arg("model", upDevice_.model), fmt::arg("native-path", upDevice_.nativePath)); + } + + return ret; +} + +bool UPower::queryTooltipCb(int x, int y, bool keyboard_tooltip, + const Glib::RefPtr &tooltip) { + std::lock_guard guard{mutex_}; + + // Clear content box + contentBox_.forall([this](Gtk::Widget &wg) { contentBox_.remove(wg); }); + + // Fill content box with the content + for (auto pairDev : devices_) { + // Get device info + getUpDeviceInfo(pairDev.second); + + if (pairDev.second.kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN && + pairDev.second.kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER) { + // Make box record + Gtk::Box *boxRec{new Gtk::Box{box_.get_orientation(), tooltip_spacing_}}; + contentBox_.add(*boxRec); + Gtk::Box *boxDev{new Gtk::Box{box_.get_orientation()}}; + Gtk::Box *boxUsr{new Gtk::Box{box_.get_orientation()}}; + boxRec->add(*boxDev); + boxRec->add(*boxUsr); + // Construct device box + // Set icon from kind + std::string iconNameDev{getDeviceIcon(pairDev.second.kind)}; + if (!gtkTheme_->has_icon(iconNameDev)) iconNameDev = (char *)NO_BATTERY.c_str(); + Gtk::Image *iconDev{new Gtk::Image{}}; + iconDev->set_from_icon_name(iconNameDev, Gtk::ICON_SIZE_INVALID); + iconDev->set_pixel_size(iconSize_); + boxDev->add(*iconDev); + // Set label from model + Gtk::Label *labelDev{new Gtk::Label{pairDev.second.model}}; + boxDev->add(*labelDev); + // Construct user box + // Set icon from icon state + if (pairDev.second.icon_name == NULL || !gtkTheme_->has_icon(pairDev.second.icon_name)) + pairDev.second.icon_name = (char *)NO_BATTERY.c_str(); + Gtk::Image *iconTooltip{new Gtk::Image{}}; + iconTooltip->set_from_icon_name(pairDev.second.icon_name, Gtk::ICON_SIZE_INVALID); + iconTooltip->set_pixel_size(iconSize_); + boxUsr->add(*iconTooltip); + // Set markup text + Gtk::Label *labelTooltip{new Gtk::Label{}}; + labelTooltip->set_markup(getText(pairDev.second, tooltipFormat_)); + boxUsr->add(*labelTooltip); + } + } + tooltip->set_custom(contentBox_); + contentBox_.show_all(); + + return true; +} + +} // namespace waybar::modules diff --git a/src/modules/upower/upower.cpp b/src/modules/upower/upower.cpp deleted file mode 100644 index 3554d43ba..000000000 --- a/src/modules/upower/upower.cpp +++ /dev/null @@ -1,397 +0,0 @@ -#include "modules/upower/upower.hpp" - -#include - -#include -#include - -#include "gtkmm/tooltip.h" -#include "util/gtk_icon.hpp" - -namespace waybar::modules::upower { -UPower::UPower(const std::string& id, const Json::Value& config) - : AModule(config, "upower", id), - box_(Gtk::ORIENTATION_HORIZONTAL, 0), - icon_(), - label_(), - devices(), - m_Mutex(), - client(), - showAltText(false) { - // Show icon only when "show-icon" isn't set to false - if (config_["show-icon"].isBool()) { - showIcon = config_["show-icon"].asBool(); - } - - if (showIcon) { - box_.pack_start(icon_); - } - - box_.pack_start(label_); - box_.set_name(name_); - event_box_.add(box_); - - // Device user wants - if (config_["native-path"].isString()) nativePath_ = config_["native-path"].asString(); - // Icon Size - if (config_["icon-size"].isUInt()) { - iconSize = config_["icon-size"].asUInt(); - } - icon_.set_pixel_size(iconSize); - - // Hide If Empty - if (config_["hide-if-empty"].isBool()) { - hideIfEmpty = config_["hide-if-empty"].asBool(); - } - - // Format - if (config_["format"].isString()) { - format = config_["format"].asString(); - } - - // Format Alt - if (config_["format-alt"].isString()) { - format_alt = config_["format-alt"].asString(); - } - - // Tooltip Spacing - if (config_["tooltip-spacing"].isUInt()) { - tooltip_spacing = config_["tooltip-spacing"].asUInt(); - } - - // Tooltip Padding - if (config_["tooltip-padding"].isUInt()) { - tooltip_padding = config_["tooltip-padding"].asUInt(); - } - - // Tooltip - if (config_["tooltip"].isBool()) { - tooltip_enabled = config_["tooltip"].asBool(); - } - box_.set_has_tooltip(tooltip_enabled); - if (tooltip_enabled) { - // Sets the window to use when showing the tooltip - upower_tooltip = std::make_unique(iconSize, tooltip_spacing, tooltip_padding); - box_.set_tooltip_window(*upower_tooltip); - box_.signal_query_tooltip().connect(sigc::mem_fun(*this, &UPower::show_tooltip_callback)); - } - - upowerWatcher_id = g_bus_watch_name(G_BUS_TYPE_SYSTEM, "org.freedesktop.UPower", - G_BUS_NAME_WATCHER_FLAGS_AUTO_START, upowerAppear, - upowerDisappear, this, NULL); - - client = up_client_new_full(NULL, NULL); - if (client == NULL) { - throw std::runtime_error("Unable to create UPower client!"); - } - - // Connect to Login1 PrepareForSleep signal - login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL); - if (!login1_connection) { - throw std::runtime_error("Unable to connect to the SYSTEM Bus!..."); - } else { - login1_id = g_dbus_connection_signal_subscribe( - login1_connection, "org.freedesktop.login1", "org.freedesktop.login1.Manager", - "PrepareForSleep", "/org/freedesktop/login1", NULL, G_DBUS_SIGNAL_FLAGS_NONE, - prepareForSleep_cb, this, NULL); - } - - event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &UPower::handleToggle)); - - g_signal_connect(client, "device-added", G_CALLBACK(deviceAdded_cb), this); - g_signal_connect(client, "device-removed", G_CALLBACK(deviceRemoved_cb), this); - - resetDevices(); - setDisplayDevice(); -} - -UPower::~UPower() { - if (displayDevice != NULL) g_object_unref(displayDevice); - if (client != NULL) g_object_unref(client); - if (login1_id > 0) { - g_dbus_connection_signal_unsubscribe(login1_connection, login1_id); - login1_id = 0; - } - g_bus_unwatch_name(upowerWatcher_id); - removeDevices(); -} - -void UPower::deviceAdded_cb(UpClient* client, UpDevice* device, gpointer data) { - UPower* up = static_cast(data); - up->addDevice(device); - up->setDisplayDevice(); - // Update the widget - up->dp.emit(); -} -void UPower::deviceRemoved_cb(UpClient* client, const gchar* objectPath, gpointer data) { - UPower* up = static_cast(data); - up->removeDevice(objectPath); - up->setDisplayDevice(); - // Update the widget - up->dp.emit(); -} -void UPower::deviceNotify_cb(UpDevice* device, GParamSpec* pspec, gpointer data) { - UPower* up = static_cast(data); - // Update the widget - up->dp.emit(); -} -void UPower::prepareForSleep_cb(GDBusConnection* system_bus, const gchar* sender_name, - const gchar* object_path, const gchar* interface_name, - const gchar* signal_name, GVariant* parameters, gpointer data) { - if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)"))) { - gboolean sleeping; - g_variant_get(parameters, "(b)", &sleeping); - - if (!sleeping) { - UPower* up = static_cast(data); - up->resetDevices(); - up->setDisplayDevice(); - } - } -} -void UPower::upowerAppear(GDBusConnection* conn, const gchar* name, const gchar* name_owner, - gpointer data) { - UPower* up = static_cast(data); - up->upowerRunning = true; - up->event_box_.set_visible(true); -} -void UPower::upowerDisappear(GDBusConnection* conn, const gchar* name, gpointer data) { - UPower* up = static_cast(data); - up->upowerRunning = false; - up->event_box_.set_visible(false); -} - -void UPower::removeDevice(const gchar* objectPath) { - std::lock_guard guard(m_Mutex); - if (devices.find(objectPath) != devices.end()) { - UpDevice* device = devices[objectPath]; - if (G_IS_OBJECT(device)) { - g_object_unref(device); - } - devices.erase(objectPath); - } -} - -void UPower::addDevice(UpDevice* device) { - if (G_IS_OBJECT(device)) { - const gchar* objectPath = up_device_get_object_path(device); - - // Due to the device getting cleared after this event is fired, we - // create a new object pointing to its objectPath - gboolean ret; - device = up_device_new(); - ret = up_device_set_object_path_sync(device, objectPath, NULL, NULL); - if (!ret) { - g_object_unref(G_OBJECT(device)); - return; - } - - std::lock_guard guard(m_Mutex); - - if (devices.find(objectPath) != devices.end()) { - UpDevice* device = devices[objectPath]; - if (G_IS_OBJECT(device)) { - g_object_unref(device); - } - devices.erase(objectPath); - } - - g_signal_connect(device, "notify", G_CALLBACK(deviceNotify_cb), this); - devices.emplace(Devices::value_type(objectPath, device)); - } -} - -void UPower::setDisplayDevice() { - std::lock_guard guard(m_Mutex); - - if (nativePath_.empty()) - displayDevice = up_client_get_display_device(client); - else { - g_ptr_array_foreach( - up_client_get_devices2(client), - [](gpointer data, gpointer user_data) { - UpDevice* device{static_cast(data)}; - UPower* thisPtr{static_cast(user_data)}; - gchar* nativePath; - if (!thisPtr->displayDevice) { - g_object_get(device, "native-path", &nativePath, NULL); - if (!std::strcmp(nativePath, thisPtr->nativePath_.c_str())) - thisPtr->displayDevice = device; - } - }, - this); - } - - if (displayDevice) g_signal_connect(displayDevice, "notify", G_CALLBACK(deviceNotify_cb), this); -} - -void UPower::removeDevices() { - std::lock_guard guard(m_Mutex); - if (!devices.empty()) { - auto it = devices.cbegin(); - while (it != devices.cend()) { - if (G_IS_OBJECT(it->second)) { - g_object_unref(it->second); - } - devices.erase(it++); - } - } -} - -/** Removes all devices and adds the current devices */ -void UPower::resetDevices() { - // Removes all devices - removeDevices(); - - // Adds all devices - GPtrArray* newDevices = up_client_get_devices2(client); - for (guint i = 0; i < newDevices->len; i++) { - UpDevice* device = (UpDevice*)g_ptr_array_index(newDevices, i); - if (device && G_IS_OBJECT(device)) addDevice(device); - } - - // Update the widget - dp.emit(); -} - -bool UPower::show_tooltip_callback(int, int, bool, const Glib::RefPtr& tooltip) { - return true; -} - -const std::string UPower::getDeviceStatus(UpDeviceState& state) { - switch (state) { - case UP_DEVICE_STATE_CHARGING: - case UP_DEVICE_STATE_PENDING_CHARGE: - return "charging"; - case UP_DEVICE_STATE_DISCHARGING: - case UP_DEVICE_STATE_PENDING_DISCHARGE: - return "discharging"; - case UP_DEVICE_STATE_FULLY_CHARGED: - return "full"; - case UP_DEVICE_STATE_EMPTY: - return "empty"; - default: - return "unknown-status"; - } -} - -bool UPower::handleToggle(GdkEventButton* const& event) { - std::lock_guard guard(m_Mutex); - showAltText = !showAltText; - return AModule::handleToggle(event); -} - -std::string UPower::timeToString(gint64 time) { - if (time == 0) return ""; - float hours = (float)time / 3600; - float hours_fixed = static_cast(static_cast(hours * 10)) / 10; - float minutes = static_cast(static_cast(hours * 60 * 10)) / 10; - if (hours_fixed >= 1) { - return fmt::format("{H} h", fmt::arg("H", hours_fixed)); - } else { - return fmt::format("{M} min", fmt::arg("M", minutes)); - } -} - -auto UPower::update() -> void { - std::lock_guard guard(m_Mutex); - - // Don't update widget if the UPower service isn't running - if (!upowerRunning) { - if (hideIfEmpty) { - event_box_.set_visible(false); - } - return; - } - - UpDeviceKind kind; - UpDeviceState state; - double percentage; - gint64 time_empty; - gint64 time_full; - gchar* icon_name{(char*)'\0'}; - std::string percentString{""}; - std::string time_format{""}; - - bool displayDeviceValid{false}; - - if (displayDevice) { - g_object_get(displayDevice, "kind", &kind, "state", &state, "percentage", &percentage, - "icon-name", &icon_name, "time-to-empty", &time_empty, "time-to-full", &time_full, - NULL); - /* Every Device which is handled by Upower and which is not - * UP_DEVICE_KIND_UNKNOWN (0) or UP_DEVICE_KIND_LINE_POWER (1) is a Battery - */ - displayDeviceValid = (kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN && - kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER); - } - - // CSS status class - const std::string status = getDeviceStatus(state); - // Remove last status if it exists - if (!lastStatus.empty() && box_.get_style_context()->has_class(lastStatus)) { - box_.get_style_context()->remove_class(lastStatus); - } - // Add the new status class to the Box - if (!box_.get_style_context()->has_class(status)) { - box_.get_style_context()->add_class(status); - } - lastStatus = status; - - if (devices.size() == 0 && !displayDeviceValid && hideIfEmpty) { - event_box_.set_visible(false); - // Call parent update - AModule::update(); - return; - } - - event_box_.set_visible(true); - - if (displayDeviceValid) { - // Tooltip - if (tooltip_enabled) { - uint tooltipCount = upower_tooltip->updateTooltip(devices); - // Disable the tooltip if there aren't any devices in the tooltip - box_.set_has_tooltip(!devices.empty() && tooltipCount > 0); - } - - // Set percentage - percentString = std::to_string(int(percentage + 0.5)) + "%"; - - // Label format - switch (state) { - case UP_DEVICE_STATE_CHARGING: - case UP_DEVICE_STATE_PENDING_CHARGE: - time_format = timeToString(time_full); - break; - case UP_DEVICE_STATE_DISCHARGING: - case UP_DEVICE_STATE_PENDING_DISCHARGE: - time_format = timeToString(time_empty); - break; - default: - break; - } - } - std::string label_format = - fmt::format(fmt::runtime(showAltText ? format_alt : format), - fmt::arg("percentage", percentString), fmt::arg("time", time_format)); - // Only set the label text if it doesn't only contain spaces - bool onlySpaces = true; - for (auto& character : label_format) { - if (character == ' ') continue; - onlySpaces = false; - break; - } - label_.set_markup(onlySpaces ? "" : label_format); - - // Set icon - if (icon_name == NULL || !DefaultGtkIconThemeWrapper::has_icon(icon_name)) { - icon_name = (char*)"battery-missing-symbolic"; - } - icon_.set_from_icon_name(icon_name, Gtk::ICON_SIZE_INVALID); - - // Call parent update - AModule::update(); -} - -} // namespace waybar::modules::upower diff --git a/src/modules/upower/upower_tooltip.cpp b/src/modules/upower/upower_tooltip.cpp deleted file mode 100644 index 1a653f858..000000000 --- a/src/modules/upower/upower_tooltip.cpp +++ /dev/null @@ -1,160 +0,0 @@ -#include "modules/upower/upower_tooltip.hpp" - -#include "gtkmm/box.h" -#include "gtkmm/enums.h" -#include "gtkmm/image.h" -#include "gtkmm/label.h" -#include "util/gtk_icon.hpp" - -namespace waybar::modules::upower { -UPowerTooltip::UPowerTooltip(uint iconSize_, uint tooltipSpacing_, uint tooltipPadding_) - : Gtk::Window(), - contentBox(std::make_unique(Gtk::ORIENTATION_VERTICAL)), - iconSize(iconSize_), - tooltipSpacing(tooltipSpacing_), - tooltipPadding(tooltipPadding_) { - // Sets the Tooltip Padding - contentBox->set_margin_top(tooltipPadding); - contentBox->set_margin_bottom(tooltipPadding); - contentBox->set_margin_left(tooltipPadding); - contentBox->set_margin_right(tooltipPadding); - - add(*contentBox); - contentBox->show(); -} - -UPowerTooltip::~UPowerTooltip() {} - -uint UPowerTooltip::updateTooltip(Devices& devices) { - // Removes all old devices - for (auto child : contentBox->get_children()) { - delete child; - } - - uint deviceCount = 0; - // Adds all valid devices - for (auto pair : devices) { - UpDevice* device = pair.second; - std::string objectPath = pair.first; - - if (!G_IS_OBJECT(device)) continue; - - Gtk::Box* box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, tooltipSpacing); - - UpDeviceKind kind; - double percentage; - gchar* native_path; - gchar* model; - gchar* icon_name; - - g_object_get(device, "kind", &kind, "percentage", &percentage, "native-path", &native_path, - "model", &model, "icon-name", &icon_name, NULL); - - // Skip Line_Power and BAT0 devices - if (kind == UP_DEVICE_KIND_LINE_POWER || native_path == NULL || strlen(native_path) == 0 || - strcmp(native_path, "BAT0") == 0) - continue; - - Gtk::Box* modelBox = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); - box->add(*modelBox); - // Set device icon - std::string deviceIconName = getDeviceIcon(kind); - Gtk::Image* deviceIcon = new Gtk::Image(); - deviceIcon->set_pixel_size(iconSize); - if (!DefaultGtkIconThemeWrapper::has_icon(deviceIconName)) { - deviceIconName = "battery-missing-symbolic"; - } - deviceIcon->set_from_icon_name(deviceIconName, Gtk::ICON_SIZE_INVALID); - modelBox->add(*deviceIcon); - - // Set model - if (model == NULL) model = (gchar*)""; - Gtk::Label* modelLabel = new Gtk::Label(model); - modelBox->add(*modelLabel); - - Gtk::Box* chargeBox = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); - box->add(*chargeBox); - - // Set icon - Gtk::Image* icon = new Gtk::Image(); - icon->set_pixel_size(iconSize); - if (icon_name == NULL || !DefaultGtkIconThemeWrapper::has_icon(icon_name)) { - icon_name = (char*)"battery-missing-symbolic"; - } - icon->set_from_icon_name(icon_name, Gtk::ICON_SIZE_INVALID); - chargeBox->add(*icon); - - // Set percentage - std::string percentString = std::to_string(int(percentage + 0.5)) + "%"; - Gtk::Label* percentLabel = new Gtk::Label(percentString); - chargeBox->add(*percentLabel); - - contentBox->add(*box); - - deviceCount++; - } - - contentBox->show_all(); - return deviceCount; -} - -const std::string UPowerTooltip::getDeviceIcon(UpDeviceKind& kind) { - switch (kind) { - case UP_DEVICE_KIND_LINE_POWER: - return "ac-adapter-symbolic"; - case UP_DEVICE_KIND_BATTERY: - return "battery"; - case UP_DEVICE_KIND_UPS: - return "uninterruptible-power-supply-symbolic"; - case UP_DEVICE_KIND_MONITOR: - return "video-display-symbolic"; - case UP_DEVICE_KIND_MOUSE: - return "input-mouse-symbolic"; - case UP_DEVICE_KIND_KEYBOARD: - return "input-keyboard-symbolic"; - case UP_DEVICE_KIND_PDA: - return "pda-symbolic"; - case UP_DEVICE_KIND_PHONE: - return "phone-symbolic"; - case UP_DEVICE_KIND_MEDIA_PLAYER: - return "multimedia-player-symbolic"; - case UP_DEVICE_KIND_TABLET: - return "computer-apple-ipad-symbolic"; - case UP_DEVICE_KIND_COMPUTER: - return "computer-symbolic"; - case UP_DEVICE_KIND_GAMING_INPUT: - return "input-gaming-symbolic"; - case UP_DEVICE_KIND_PEN: - return "input-tablet-symbolic"; - case UP_DEVICE_KIND_TOUCHPAD: - return "input-touchpad-symbolic"; - case UP_DEVICE_KIND_MODEM: - return "modem-symbolic"; - case UP_DEVICE_KIND_NETWORK: - return "network-wired-symbolic"; - case UP_DEVICE_KIND_HEADSET: - return "audio-headset-symbolic"; - case UP_DEVICE_KIND_HEADPHONES: - return "audio-headphones-symbolic"; - case UP_DEVICE_KIND_OTHER_AUDIO: - case UP_DEVICE_KIND_SPEAKERS: - return "audio-speakers-symbolic"; - case UP_DEVICE_KIND_VIDEO: - return "camera-web-symbolic"; - case UP_DEVICE_KIND_PRINTER: - return "printer-symbolic"; - case UP_DEVICE_KIND_SCANNER: - return "scanner-symbolic"; - case UP_DEVICE_KIND_CAMERA: - return "camera-photo-symbolic"; - case UP_DEVICE_KIND_BLUETOOTH_GENERIC: - return "bluetooth-active-symbolic"; - case UP_DEVICE_KIND_TOY: - case UP_DEVICE_KIND_REMOTE_CONTROL: - case UP_DEVICE_KIND_WEARABLE: - case UP_DEVICE_KIND_LAST: - default: - return "battery-symbolic"; - } -} -} // namespace waybar::modules::upower diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 51bb708d1..bd019b623 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -18,31 +18,24 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val min_step_(0.0), node_id_(0) { wp_init(WP_INIT_PIPEWIRE); - wp_core_ = wp_core_new(NULL, NULL); + wp_core_ = wp_core_new(nullptr, nullptr, nullptr); apis_ = g_ptr_array_new_with_free_func(g_object_unref); om_ = wp_object_manager_new(); prepare(); - loadRequiredApiModules(); + spdlog::debug("[{}]: connecting to pipewire...", name_); - spdlog::debug("[{}]: connecting to pipewire...", this->name_); - - if (!wp_core_connect(wp_core_)) { - spdlog::error("[{}]: Could not connect to PipeWire", this->name_); + if (wp_core_connect(wp_core_) == 0) { + spdlog::error("[{}]: Could not connect to PipeWire", name_); throw std::runtime_error("Could not connect to PipeWire\n"); } - spdlog::debug("[{}]: connected!", this->name_); + spdlog::debug("[{}]: connected!", name_); g_signal_connect_swapped(om_, "installed", (GCallback)onObjectManagerInstalled, this); - activatePlugins(); - - dp.emit(); - - event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); - event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Wireplumber::handleScroll)); + asyncLoadRequiredApiModules(); } waybar::modules::Wireplumber::~Wireplumber() { @@ -63,32 +56,36 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* return; } - auto proxy = static_cast(wp_object_manager_lookup( - self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, NULL)); + 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) { + if (proxy == nullptr) { auto err = fmt::format("Object '{}' not found\n", id); spdlog::error("[{}]: {}", self->name_, err); throw std::runtime_error(err); } g_autoptr(WpProperties) properties = - WP_IS_PIPEWIRE_OBJECT(proxy) ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy)) - : wp_properties_new_empty(); - g_autoptr(WpProperties) global_p = wp_global_proxy_get_global_properties(WP_GLOBAL_PROXY(proxy)); + 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, global_p); - wp_properties_set(properties, "object.id", NULL); - auto nick = wp_properties_get(properties, "node.nick"); - auto description = wp_properties_get(properties, "node.description"); - - self->node_name_ = nick ? nick : description ? description : "Unknown node name"; + 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->node_name_ = nick != nullptr ? nick + : description != nullptr ? description + : "Unknown node name"; spdlog::debug("[{}]: Updating node name to: {}", self->name_, self->node_name_); } void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self, uint32_t id) { spdlog::debug("[{}]: updating volume", self->name_); - GVariant* variant = NULL; + GVariant* variant = nullptr; if (!isValidNodeId(id)) { spdlog::error("[{}]: '{}' is not a valid node ID. Ignoring volume update.", self->name_, id); @@ -97,7 +94,7 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se g_signal_emit_by_name(self->mixer_api_, "get-volume", id, &variant); - if (!variant) { + if (variant == nullptr) { auto err = fmt::format("Node {} does not support volume\n", id); spdlog::error("[{}]: {}", self->name_, err); throw std::runtime_error(err); @@ -115,9 +112,9 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* spdlog::debug("[{}]: (onMixerChanged) - id: {}", self->name_, 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, NULL)); + self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, nullptr)); - if (!node) { + if (node == nullptr) { spdlog::warn("[{}]: (onMixerChanged) - Object with id {} not found", self->name_, id); return; } @@ -140,49 +137,49 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wireplumber* self) { spdlog::debug("[{}]: (onDefaultNodesApiChanged)", self->name_); - uint32_t default_node_id; - g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &default_node_id); + uint32_t defaultNodeId; + g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &defaultNodeId); - if (!isValidNodeId(default_node_id)) { + if (!isValidNodeId(defaultNodeId)) { spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node change.", self->name_, - default_node_id); + defaultNodeId); return; } g_autoptr(WpNode) node = static_cast( wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", - "=u", default_node_id, NULL)); + "=u", defaultNodeId, nullptr)); - if (!node) { + if (node == nullptr) { spdlog::warn("[{}]: (onDefaultNodesApiChanged) - Object with id {} not found", self->name_, - default_node_id); + defaultNodeId); return; } - const gchar* default_node_name = + 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_, default_node_name, default_node_id); + self->name_, defaultNodeName, defaultNodeId); - if (g_strcmp0(self->default_node_name_, default_node_name) == 0) { + if (g_strcmp0(self->default_node_name_, defaultNodeName) == 0) { spdlog::debug( "[{}]: (onDefaultNodesApiChanged) - Default node has not changed. Node(name: {}, id: {}). " "Ignoring.", - self->name_, self->default_node_name_, default_node_id); + self->name_, self->default_node_name_, defaultNodeId); return; } spdlog::debug( "[{}]: (onDefaultNodesApiChanged) - Default node changed to -> Node(name: {}, id: {})", - self->name_, default_node_name, default_node_id); + self->name_, defaultNodeName, defaultNodeId); g_free(self->default_node_name_); - self->default_node_name_ = g_strdup(default_node_name); - self->node_id_ = default_node_id; - updateVolume(self, default_node_id); - updateNodeName(self, default_node_id); + 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) { @@ -190,14 +187,14 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir self->def_nodes_api_ = wp_plugin_find(self->wp_core_, "default-nodes-api"); - if (!self->def_nodes_api_) { + if (self->def_nodes_api_ == nullptr) { spdlog::error("[{}]: default nodes api is not loaded.", self->name_); throw std::runtime_error("Default nodes API is not loaded\n"); } self->mixer_api_ = wp_plugin_find(self->wp_core_, "mixer-api"); - if (!self->mixer_api_) { + if (self->mixer_api_ == nullptr) { spdlog::error("[{}]: mixer api is not loaded.", self->name_); throw std::runtime_error("Mixer api is not loaded\n"); } @@ -206,7 +203,7 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir &self->default_node_name_); g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &self->node_id_); - if (self->default_node_name_) { + if (self->default_node_name_ != nullptr) { spdlog::debug("[{}]: (onObjectManagerInstalled) - default configured node name: {} and id: {}", self->name_, self->default_node_name_, self->node_id_); } @@ -221,11 +218,11 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self) { - auto plugin_name = wp_plugin_get_name(WP_PLUGIN(p)); - spdlog::debug("[{}]: onPluginActivated: {}", self->name_, plugin_name); - g_autoptr(GError) error = NULL; + const auto* pluginName = wp_plugin_get_name(WP_PLUGIN(p)); + spdlog::debug("[{}]: onPluginActivated: {}", self->name_, pluginName); + g_autoptr(GError) error = nullptr; - if (!wp_object_activate_finish(p, res, &error)) { + if (wp_object_activate_finish(p, res, &error) == 0) { spdlog::error("[{}]: error activating plugin: {}", self->name_, error->message); throw std::runtime_error(error->message); } @@ -240,7 +237,7 @@ void waybar::modules::Wireplumber::activatePlugins() { for (uint16_t i = 0; i < apis_->len; i++) { WpPlugin* plugin = static_cast(g_ptr_array_index(apis_, i)); pending_plugins_++; - wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, NULL, + wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, nullptr, (GAsyncReadyCallback)onPluginActivated, this); } } @@ -248,34 +245,67 @@ void waybar::modules::Wireplumber::activatePlugins() { void waybar::modules::Wireplumber::prepare() { spdlog::debug("[{}]: preparing object manager", name_); wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", - "=s", "Audio/Sink", NULL); + "=s", "Audio/Sink", nullptr); } -void waybar::modules::Wireplumber::loadRequiredApiModules() { - spdlog::debug("[{}]: loading required modules", name_); - g_autoptr(GError) error = NULL; +void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, + waybar::modules::Wireplumber* self) { + gboolean success = FALSE; + g_autoptr(GError) error = nullptr; - if (!wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", NULL, - &error)) { + spdlog::debug("[{}]: callback loading default node api module", self->name_); + + success = wp_core_load_component_finish(self->wp_core_, res, &error); + + if (success == FALSE) { + spdlog::error("[{}]: default nodes API load failed", self->name_); throw std::runtime_error(error->message); } + spdlog::debug("[{}]: loaded default nodes api", self->name_); + g_ptr_array_add(self->apis_, wp_plugin_find(self->wp_core_, "default-nodes-api")); + + spdlog::debug("[{}]: loading mixer api module", self->name_); + wp_core_load_component(self->wp_core_, "libwireplumber-module-mixer-api", "module", nullptr, + "mixer-api", nullptr, (GAsyncReadyCallback)onMixerApiLoaded, self); +} - if (!wp_core_load_component(wp_core_, "libwireplumber-module-mixer-api", "module", NULL, - &error)) { +void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* res, + waybar::modules::Wireplumber* self) { + gboolean success = FALSE; + g_autoptr(GError) error = nullptr; + + success = wp_core_load_component_finish(self->wp_core_, res, nullptr); + + if (success == FALSE) { + spdlog::error("[{}]: mixer API load failed", self->name_); throw std::runtime_error(error->message); } - g_ptr_array_add(apis_, wp_plugin_find(wp_core_, "default-nodes-api")); - g_ptr_array_add(apis_, ({ - WpPlugin* p = wp_plugin_find(wp_core_, "mixer-api"); - g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, NULL); + spdlog::debug("[{}]: loaded mixer API", self->name_); + g_ptr_array_add(self->apis_, ({ + WpPlugin* p = wp_plugin_find(self->wp_core_, "mixer-api"); + g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, nullptr); p; })); + + self->activatePlugins(); + + self->dp.emit(); + + self->event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + self->event_box_.signal_scroll_event().connect(sigc::mem_fun(*self, &Wireplumber::handleScroll)); +} + +void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { + spdlog::debug("[{}]: loading default nodes api module", name_); + wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", nullptr, + "default-nodes-api", nullptr, (GAsyncReadyCallback)onDefaultNodesApiLoaded, + this); } auto waybar::modules::Wireplumber::update() -> void { auto format = format_; - std::string tooltip_format; + std::string tooltipFormat; if (muted_) { format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; @@ -292,12 +322,12 @@ auto waybar::modules::Wireplumber::update() -> void { getState(vol); if (tooltipEnabled()) { - if (tooltip_format.empty() && config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); + if (tooltipFormat.empty() && config_["tooltip-format"].isString()) { + tooltipFormat = config_["tooltip-format"].asString(); } - if (!tooltip_format.empty()) { - label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format), + 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)))); } else { @@ -317,31 +347,31 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { if (dir == SCROLL_DIR::NONE) { return true; } - double max_volume = 1; + double maxVolume = 1; double step = 1.0 / 100.0; if (config_["scroll-step"].isDouble()) { step = config_["scroll-step"].asDouble() / 100.0; } if (config_["max-volume"].isDouble()) { - max_volume = config_["max-volume"].asDouble() / 100.0; + maxVolume = config_["max-volume"].asDouble() / 100.0; } if (step < min_step_) step = min_step_; - double new_vol = volume_; + double newVol = volume_; if (dir == SCROLL_DIR::UP) { - if (volume_ < max_volume) { - new_vol = volume_ + step; - if (new_vol > max_volume) new_vol = max_volume; + if (volume_ < maxVolume) { + newVol = volume_ + step; + if (newVol > maxVolume) newVol = maxVolume; } } else if (dir == SCROLL_DIR::DOWN) { if (volume_ > 0) { - new_vol = volume_ - step; - if (new_vol < 0) new_vol = 0; + newVol = volume_ - step; + if (newVol < 0) newVol = 0; } } - if (new_vol != volume_) { - GVariant* variant = g_variant_new_double(new_vol); + if (newVol != volume_) { + GVariant* variant = g_variant_new_double(newVol); gboolean ret; g_signal_emit_by_name(mixer_api_, "set-volume", node_id_, variant, &ret); } diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 2709584b3..e6c8e536c 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -30,6 +30,9 @@ namespace waybar::modules::wlr { 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/"); @@ -47,9 +50,6 @@ static std::vector search_prefix() { } while (end != std::string::npos); } - std::string home_dir = std::getenv("HOME"); - prefixes.push_back(home_dir + "/.local/share/"); - for (auto &p : prefixes) spdlog::debug("Using 'desktop' search path prefix: {}", p); return prefixes; @@ -334,9 +334,7 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, } button.add_events(Gdk::BUTTON_PRESS_MASK); - button.signal_button_press_event().connect(sigc::mem_fun(*this, &Task::handle_clicked), false); - button.signal_button_release_event().connect(sigc::mem_fun(*this, &Task::handle_button_release), - false); + button.signal_button_release_event().connect(sigc::mem_fun(*this, &Task::handle_clicked), false); button.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Task::handle_motion_notify), false); @@ -573,12 +571,8 @@ bool Task::handle_clicked(GdkEventButton *bt) { else spdlog::warn("Unknown action {}", action); - return true; -} - -bool Task::handle_button_release(GdkEventButton *bt) { drag_start_button = -1; - return false; + return true; } bool Task::handle_motion_notify(GdkEventMotion *mn) { @@ -794,6 +788,10 @@ 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()); + } } Taskbar::~Taskbar() { @@ -900,7 +898,7 @@ void Taskbar::move_button(Gtk::Button &bt, int pos) { box_.reorder_child(bt, pos void Taskbar::remove_button(Gtk::Button &bt) { box_.remove(bt); - if (tasks_.empty()) { + if (box_.get_children().empty()) { box_.get_style_context()->add_class("empty"); } } diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index f4dd72c4a..e634784bb 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace waybar::util { @@ -15,13 +16,11 @@ AudioBackend::AudioBackend(std::function on_updated_cb, private_construc : mainloop_(nullptr), mainloop_api_(nullptr), context_(nullptr), - sink_idx_(0), volume_(0), muted_(false), - source_idx_(0), source_volume_(0), source_muted_(false), - on_updated_cb_(on_updated_cb) { + on_updated_cb_(std::move(on_updated_cb)) { mainloop_ = pa_threaded_mainloop_new(); if (mainloop_ == nullptr) { throw std::runtime_error("pa_mainloop_new() failed."); @@ -66,7 +65,7 @@ void AudioBackend::connectContext() { } void AudioBackend::contextStateCb(pa_context *c, void *data) { - auto backend = static_cast(data); + auto *backend = static_cast(data); switch (pa_context_get_state(c)) { case PA_CONTEXT_TERMINATED: backend->mainloop_api_->quit(backend->mainloop_api_, 0); @@ -127,7 +126,7 @@ void AudioBackend::subscribeCb(pa_context *context, pa_subscription_event_type_t * Called in response to a volume change request */ void AudioBackend::volumeModifyCb(pa_context *c, int success, void *data) { - auto backend = static_cast(data); + auto *backend = static_cast(data); if (success != 0) { pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data); } @@ -140,7 +139,7 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i void *data) { if (i == nullptr) return; - auto backend = static_cast(data); + auto *backend = static_cast(data); if (!backend->ignored_sinks_.empty()) { for (const auto &ignored_sink : backend->ignored_sinks_) { @@ -151,11 +150,7 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i } if (backend->current_sink_name_ == i->name) { - if (i->state != PA_SINK_RUNNING) { - backend->current_sink_running_ = false; - } else { - backend->current_sink_running_ = true; - } + backend->current_sink_running_ = i->state == PA_SINK_RUNNING; } if (!backend->current_sink_running_ && i->state == PA_SINK_RUNNING) { @@ -173,7 +168,7 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i backend->desc_ = i->description; backend->monitor_ = i->monitor_source_name; backend->port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; - if (auto ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) { + if (const auto *ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) { backend->form_factor_ = ff; } else { backend->form_factor_ = ""; @@ -187,7 +182,7 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i */ void AudioBackend::sourceInfoCb(pa_context * /*context*/, const pa_source_info *i, int /*eol*/, void *data) { - auto backend = static_cast(data); + auto *backend = static_cast(data); if (i != nullptr && backend->default_source_name_ == i->name) { auto source_volume = static_cast(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM}; backend->source_volume_ = std::round(source_volume * 100.0F); @@ -204,7 +199,7 @@ void AudioBackend::sourceInfoCb(pa_context * /*context*/, const pa_source_info * * used to find the default PulseAudio sink. */ void AudioBackend::serverInfoCb(pa_context *context, const pa_server_info *i, void *data) { - auto backend = static_cast(data); + auto *backend = static_cast(data); backend->current_sink_name_ = i->default_sink_name; backend->default_source_name_ = i->default_source_name; @@ -253,22 +248,26 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma void AudioBackend::toggleSinkMute() { muted_ = !muted_; - pa_context_set_sink_mute_by_index(context_, sink_idx_, muted_, nullptr, nullptr); + pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, + nullptr); } void AudioBackend::toggleSinkMute(bool mute) { muted_ = mute; - pa_context_set_sink_mute_by_index(context_, sink_idx_, muted_, nullptr, nullptr); + pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, + nullptr); } void AudioBackend::toggleSourceMute() { source_muted_ = !muted_; - pa_context_set_source_mute_by_index(context_, source_idx_, source_muted_, nullptr, nullptr); + pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), + nullptr, nullptr); } void AudioBackend::toggleSourceMute(bool mute) { source_muted_ = mute; - pa_context_set_source_mute_by_index(context_, source_idx_, source_muted_, nullptr, nullptr); + pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), + nullptr, nullptr); } bool AudioBackend::isBluetooth() { @@ -287,4 +286,4 @@ void AudioBackend::setIgnoredSinks(const Json::Value &config) { } } -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp index 1512103cb..bb102cd93 100644 --- a/src/util/backlight_backend.cpp +++ b/src/util/backlight_backend.cpp @@ -4,7 +4,9 @@ #include #include +#include #include +#include namespace { class FileDescriptor { @@ -73,8 +75,56 @@ void check_nn(const void *ptr, const char *message = "ptr was null") { namespace waybar::util { +static void upsert_device(std::vector &devices, udev_device *dev) { + const char *name = udev_device_get_sysname(dev); + check_nn(name); + + const char *actual_brightness_attr = + strncmp(name, "amdgpu_bl", 9) == 0 || strcmp(name, "apple-panel-bl") == 0 + ? "brightness" + : "actual_brightness"; + + const char *actual = udev_device_get_sysattr_value(dev, actual_brightness_attr); + const char *max = udev_device_get_sysattr_value(dev, "max_brightness"); + const char *power = udev_device_get_sysattr_value(dev, "bl_power"); + + auto found = std::find_if(devices.begin(), devices.end(), [name](const BacklightDevice &device) { + return device.name() == name; + }); + if (found != devices.end()) { + if (actual != nullptr) { + found->set_actual(std::stoi(actual)); + } + if (max != nullptr) { + found->set_max(std::stoi(max)); + } + if (power != nullptr) { + found->set_powered(std::stoi(power) == 0); + } + } else { + const int actual_int = actual == nullptr ? 0 : std::stoi(actual); + const int max_int = max == nullptr ? 0 : std::stoi(max); + const bool power_bool = power == nullptr ? true : std::stoi(power) == 0; + devices.emplace_back(name, actual_int, max_int, power_bool); + } +} + +static void enumerate_devices(std::vector &devices, udev *udev) { + std::unique_ptr enumerate{udev_enumerate_new(udev)}; + udev_enumerate_add_match_subsystem(enumerate.get(), "backlight"); + udev_enumerate_scan_devices(enumerate.get()); + udev_list_entry *enum_devices = udev_enumerate_get_list_entry(enumerate.get()); + udev_list_entry *dev_list_entry; + udev_list_entry_foreach(dev_list_entry, enum_devices) { + const char *path = udev_list_entry_get_name(dev_list_entry); + std::unique_ptr dev{udev_device_new_from_syspath(udev, path)}; + check_nn(dev.get(), "dev new failed"); + upsert_device(devices, dev.get()); + } +} + BacklightDevice::BacklightDevice(std::string name, int actual, int max, bool powered) - : name_(name), actual_(actual), max_(max), powered_(powered) {} + : name_(std::move(name)), actual_(actual), max_(max), powered_(powered) {} std::string BacklightDevice::name() const { return name_; } @@ -92,11 +142,10 @@ void BacklightDevice::set_powered(bool powered) { powered_ = powered; } BacklightBackend::BacklightBackend(std::chrono::milliseconds interval, std::function on_updated_cb) - : on_updated_cb_(on_updated_cb), polling_interval_(interval), previous_best_({}) { + : on_updated_cb_(std::move(on_updated_cb)), polling_interval_(interval), previous_best_({}) { std::unique_ptr udev_check{udev_new()}; check_nn(udev_check.get(), "Udev check new failed"); - enumerate_devices(devices_.begin(), devices_.end(), std::back_inserter(devices_), - udev_check.get()); + enumerate_devices(devices_, udev_check.get()); if (devices_.empty()) { throw std::runtime_error("No backlight found"); } @@ -145,12 +194,12 @@ BacklightBackend::BacklightBackend(std::chrono::milliseconds interval, check_eq(event.data.fd, udev_fd, "unexpected udev fd"); std::unique_ptr dev{udev_monitor_receive_device(mon.get())}; check_nn(dev.get(), "epoll dev was null"); - upsert_device(devices.begin(), devices.end(), std::back_inserter(devices), dev.get()); + upsert_device(devices, dev.get()); } // Refresh state if timed out if (event_count == 0) { - enumerate_devices(devices.begin(), devices.end(), std::back_inserter(devices), udev.get()); + enumerate_devices(devices, udev.get()); } { std::scoped_lock lock(udev_thread_mutex_); @@ -161,19 +210,20 @@ BacklightBackend::BacklightBackend(std::chrono::milliseconds interval, }; } -template -const BacklightDevice *BacklightBackend::best_device(ForwardIt first, ForwardIt last, +const BacklightDevice *BacklightBackend::best_device(const std::vector &devices, std::string_view preferred_device) { const auto found = std::find_if( - first, last, [preferred_device](const auto &dev) { return dev.name() == preferred_device; }); - if (found != last) { + devices.begin(), devices.end(), + [preferred_device](const BacklightDevice &dev) { return dev.name() == preferred_device; }); + if (found != devices.end()) { return &(*found); } const auto max = std::max_element( - first, last, [](const auto &l, const auto &r) { return l.get_max() < r.get_max(); }); + devices.begin(), devices.end(), + [](const BacklightDevice &l, const BacklightDevice &r) { return l.get_max() < r.get_max(); }); - return max == last ? nullptr : &(*max); + return max == devices.end() ? nullptr : &(*max); } const BacklightDevice *BacklightBackend::get_previous_best_device() { @@ -188,24 +238,24 @@ void BacklightBackend::set_previous_best_device(const BacklightDevice *device) { } } -void BacklightBackend::set_scaled_brightness(std::string preferred_device, int brightness) { +void BacklightBackend::set_scaled_brightness(const std::string &preferred_device, int brightness) { GET_BEST_DEVICE(best, (*this), preferred_device); if (best != nullptr) { const auto max = best->get_max(); - const auto abs_val = static_cast(round(brightness * max / 100.0f)); + const auto abs_val = static_cast(std::round(brightness * max / 100.0F)); set_brightness_internal(best->name(), abs_val, best->get_max()); } } -void BacklightBackend::set_brightness(std::string preferred_device, ChangeType change_type, +void BacklightBackend::set_brightness(const std::string &preferred_device, ChangeType change_type, double step) { GET_BEST_DEVICE(best, (*this), preferred_device); if (best != nullptr) { const auto max = best->get_max(); - const auto abs_step = static_cast(round(step * max / 100.0f)); + const auto abs_step = static_cast(round(step * max / 100.0F)); const int new_brightness = change_type == ChangeType::Increase ? best->get_actual() + abs_step : best->get_actual() - abs_step; @@ -213,7 +263,7 @@ void BacklightBackend::set_brightness(std::string preferred_device, ChangeType c } } -void BacklightBackend::set_brightness_internal(std::string device_name, int brightness, +void BacklightBackend::set_brightness_internal(const std::string &device_name, int brightness, int max_brightness) { brightness = std::clamp(brightness, 0, max_brightness); @@ -223,7 +273,7 @@ void BacklightBackend::set_brightness_internal(std::string device_name, int brig login_proxy_->call_sync("SetBrightness", call_args); } -int BacklightBackend::get_scaled_brightness(std::string preferred_device) { +int BacklightBackend::get_scaled_brightness(const std::string &preferred_device) { GET_BEST_DEVICE(best, (*this), preferred_device); if (best != nullptr) { @@ -233,56 +283,4 @@ int BacklightBackend::get_scaled_brightness(std::string preferred_device) { return 0; } -template -void BacklightBackend::upsert_device(ForwardIt first, ForwardIt last, Inserter inserter, - udev_device *dev) { - const char *name = udev_device_get_sysname(dev); - check_nn(name); - - const char *actual_brightness_attr = - strncmp(name, "amdgpu_bl", 9) == 0 || strcmp(name, "apple-panel-bl") == 0 - ? "brightness" - : "actual_brightness"; - - const char *actual = udev_device_get_sysattr_value(dev, actual_brightness_attr); - const char *max = udev_device_get_sysattr_value(dev, "max_brightness"); - const char *power = udev_device_get_sysattr_value(dev, "bl_power"); - - auto found = - std::find_if(first, last, [name](const auto &device) { return device.name() == name; }); - if (found != last) { - if (actual != nullptr) { - found->set_actual(std::stoi(actual)); - } - if (max != nullptr) { - found->set_max(std::stoi(max)); - } - if (power != nullptr) { - found->set_powered(std::stoi(power) == 0); - } - } else { - const int actual_int = actual == nullptr ? 0 : std::stoi(actual); - const int max_int = max == nullptr ? 0 : std::stoi(max); - const bool power_bool = power == nullptr ? true : std::stoi(power) == 0; - *inserter = BacklightDevice{name, actual_int, max_int, power_bool}; - ++inserter; - } -} - -template -void BacklightBackend::enumerate_devices(ForwardIt first, ForwardIt last, Inserter inserter, - udev *udev) { - std::unique_ptr enumerate{udev_enumerate_new(udev)}; - udev_enumerate_add_match_subsystem(enumerate.get(), "backlight"); - udev_enumerate_scan_devices(enumerate.get()); - udev_list_entry *enum_devices = udev_enumerate_get_list_entry(enumerate.get()); - udev_list_entry *dev_list_entry; - udev_list_entry_foreach(dev_list_entry, enum_devices) { - const char *path = udev_list_entry_get_name(dev_list_entry); - std::unique_ptr dev{udev_device_new_from_syspath(udev, path)}; - check_nn(dev.get(), "dev new failed"); - upsert_device(first, last, inserter, dev.get()); - } -} - -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util diff --git a/src/util/pipewire/pipewire_backend.cpp b/src/util/pipewire/pipewire_backend.cpp new file mode 100644 index 000000000..5bb7c19a1 --- /dev/null +++ b/src/util/pipewire/pipewire_backend.cpp @@ -0,0 +1,152 @@ +#include "util/pipewire/pipewire_backend.hpp" + +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +static void getNodeInfo(void *data_, const struct pw_node_info *info) { + auto *pNodeInfo = static_cast(data_); + pNodeInfo->handleNodeEventInfo(info); + + static_cast(pNodeInfo->data)->privacy_nodes_changed_signal_event.emit(); +} + +static const struct pw_node_events NODE_EVENTS = { + .version = PW_VERSION_NODE_EVENTS, + .info = getNodeInfo, +}; + +static void proxyDestroy(void *data) { + static_cast(data)->handleProxyEventDestroy(); +} + +static const struct pw_proxy_events PROXY_EVENTS = { + .version = PW_VERSION_PROXY_EVENTS, + .destroy = proxyDestroy, +}; + +static void registryEventGlobal(void *_data, uint32_t id, uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) { + static_cast(_data)->handleRegistryEventGlobal(id, permissions, type, version, + props); +} + +static void registryEventGlobalRemove(void *_data, uint32_t id) { + static_cast(_data)->handleRegistryEventGlobalRemove(id); +} + +static const struct pw_registry_events REGISTRY_EVENTS = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = registryEventGlobal, + .global_remove = registryEventGlobalRemove, +}; + +PipewireBackend::PipewireBackend(PrivateConstructorTag tag) + : mainloop_(nullptr), context_(nullptr), core_(nullptr) { + pw_init(nullptr, nullptr); + mainloop_ = pw_thread_loop_new("waybar", nullptr); + if (mainloop_ == nullptr) { + throw std::runtime_error("pw_thread_loop_new() failed."); + } + + pw_thread_loop_lock(mainloop_); + + context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0); + if (context_ == nullptr) { + pw_thread_loop_unlock(mainloop_); + throw std::runtime_error("pa_context_new() failed."); + } + core_ = pw_context_connect(context_, nullptr, 0); + if (core_ == nullptr) { + pw_thread_loop_unlock(mainloop_); + throw std::runtime_error("pw_context_connect() failed"); + } + registry_ = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0); + + spa_zero(registryListener_); + pw_registry_add_listener(registry_, ®istryListener_, ®ISTRY_EVENTS, this); + if (pw_thread_loop_start(mainloop_) < 0) { + pw_thread_loop_unlock(mainloop_); + throw std::runtime_error("pw_thread_loop_start() failed."); + } + pw_thread_loop_unlock(mainloop_); +} + +PipewireBackend::~PipewireBackend() { + if (mainloop_ != nullptr) { + pw_thread_loop_lock(mainloop_); + } + + if (registry_ != nullptr) { + pw_proxy_destroy((struct pw_proxy *)registry_); + } + + spa_zero(registryListener_); + + if (core_ != nullptr) { + pw_core_disconnect(core_); + } + + if (context_ != nullptr) { + pw_context_destroy(context_); + } + + if (mainloop_ != nullptr) { + pw_thread_loop_unlock(mainloop_); + pw_thread_loop_stop(mainloop_); + pw_thread_loop_destroy(mainloop_); + } +} + +std::shared_ptr PipewireBackend::getInstance() { + PrivateConstructorTag tag; + return std::make_shared(tag); +} + +void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) { + if (props == nullptr || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; + + const char *lookupStr = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (lookupStr == nullptr) return; + std::string mediaClass = lookupStr; + enum PrivacyNodeType mediaType = PRIVACY_NODE_TYPE_NONE; + if (mediaClass == "Stream/Input/Video") { + mediaType = PRIVACY_NODE_TYPE_VIDEO_INPUT; + } else if (mediaClass == "Stream/Input/Audio") { + mediaType = PRIVACY_NODE_TYPE_AUDIO_INPUT; + } else if (mediaClass == "Stream/Output/Audio") { + mediaType = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; + } else { + return; + } + + auto *proxy = (pw_proxy *)pw_registry_bind(registry_, id, type, version, sizeof(PrivacyNodeInfo)); + + if (proxy == nullptr) return; + + auto *pNodeInfo = (PrivacyNodeInfo *)pw_proxy_get_user_data(proxy); + pNodeInfo->id = id; + pNodeInfo->data = this; + pNodeInfo->type = mediaType; + pNodeInfo->media_class = mediaClass; + + pw_proxy_add_listener(proxy, &pNodeInfo->proxy_listener, &PROXY_EVENTS, pNodeInfo); + + pw_proxy_add_object_listener(proxy, &pNodeInfo->object_listener, &NODE_EVENTS, pNodeInfo); + + privacy_nodes.insert_or_assign(id, pNodeInfo); +} + +void PipewireBackend::handleRegistryEventGlobalRemove(uint32_t id) { + mutex_.lock(); + auto iter = privacy_nodes.find(id); + if (iter != privacy_nodes.end()) { + privacy_nodes.erase(id); + } + mutex_.unlock(); + + privacy_nodes_changed_signal_event.emit(); +} + +} // namespace waybar::util::PipewireBackend diff --git a/src/util/pipewire/privacy_node_info.cpp b/src/util/pipewire/privacy_node_info.cpp new file mode 100644 index 000000000..739dc528f --- /dev/null +++ b/src/util/pipewire/privacy_node_info.cpp @@ -0,0 +1,56 @@ +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +std::string PrivacyNodeInfo::getName() { + const std::vector names{&application_name, &node_name}; + std::string name = "Unknown Application"; + for (const auto &item : names) { + if (item != nullptr && !item->empty()) { + name = *item; + name[0] = toupper(name[0]); + break; + } + } + return name; +} + +std::string PrivacyNodeInfo::getIconName() { + const std::vector names{&application_icon_name, &pipewire_access_portal_app_id, + &application_name, &node_name}; + std::string name = "application-x-executable-symbolic"; + for (const auto &item : names) { + if (item != nullptr && !item->empty() && DefaultGtkIconThemeWrapper::has_icon(*item)) { + return *item; + } + } + return name; +} + +void PrivacyNodeInfo::handleProxyEventDestroy() { + spa_hook_remove(&proxy_listener); + spa_hook_remove(&object_listener); +} + +void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info *info) { + state = info->state; + + const struct spa_dict_item *item; + spa_dict_for_each(item, info->props) { + if (strcmp(item->key, PW_KEY_CLIENT_ID) == 0) { + client_id = strtoul(item->value, nullptr, 10); + } else if (strcmp(item->key, PW_KEY_MEDIA_NAME) == 0) { + media_name = item->value; + } else if (strcmp(item->key, PW_KEY_NODE_NAME) == 0) { + node_name = item->value; + } else if (strcmp(item->key, PW_KEY_APP_NAME) == 0) { + application_name = item->value; + } else if (strcmp(item->key, "pipewire.access.portal.app_id") == 0) { + pipewire_access_portal_app_id = item->value; + } else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) { + application_icon_name = item->value; + } + } +} + +} // namespace waybar::util::PipewireBackend diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp deleted file mode 100644 index 5fe3ba62f..000000000 --- a/src/util/pipewire_backend.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "util/pipewire/pipewire_backend.hpp" - -#include "util/pipewire/privacy_node_info.hpp" - -namespace waybar::util::PipewireBackend { - -static void get_node_info(void *data_, const struct pw_node_info *info) { - PrivacyNodeInfo *p_node_info = static_cast(data_); - PipewireBackend *backend = (PipewireBackend *)p_node_info->data; - - p_node_info->state = info->state; - - const struct spa_dict_item *item; - spa_dict_for_each(item, info->props) { - if (strcmp(item->key, PW_KEY_CLIENT_ID) == 0) { - p_node_info->client_id = strtoul(item->value, NULL, 10); - } else if (strcmp(item->key, PW_KEY_MEDIA_NAME) == 0) { - p_node_info->media_name = item->value; - } else if (strcmp(item->key, PW_KEY_NODE_NAME) == 0) { - p_node_info->node_name = item->value; - } else if (strcmp(item->key, PW_KEY_APP_NAME) == 0) { - p_node_info->application_name = item->value; - } else if (strcmp(item->key, "pipewire.access.portal.app_id") == 0) { - p_node_info->pipewire_access_portal_app_id = item->value; - } else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) { - p_node_info->application_icon_name = item->value; - } - } - - backend->privacy_nodes_changed_signal_event.emit(); -} - -static const struct pw_node_events node_events = { - .version = PW_VERSION_NODE_EVENTS, - .info = get_node_info, -}; - -static void proxy_destroy(void *data) { - PrivacyNodeInfo *node = (PrivacyNodeInfo *)data; - - spa_hook_remove(&node->proxy_listener); - spa_hook_remove(&node->object_listener); -} - -static const struct pw_proxy_events proxy_events = { - .version = PW_VERSION_PROXY_EVENTS, - .destroy = proxy_destroy, -}; - -static void registry_event_global(void *_data, uint32_t id, uint32_t permissions, const char *type, - uint32_t version, const struct spa_dict *props) { - if (!props || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; - - const char *lookup_str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); - if (!lookup_str) return; - std::string media_class = lookup_str; - enum PrivacyNodeType media_type = PRIVACY_NODE_TYPE_NONE; - if (media_class == "Stream/Input/Video") { - media_type = PRIVACY_NODE_TYPE_VIDEO_INPUT; - } else if (media_class == "Stream/Input/Audio") { - media_type = PRIVACY_NODE_TYPE_AUDIO_INPUT; - } else if (media_class == "Stream/Output/Audio") { - media_type = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; - } else { - return; - } - - PipewireBackend *backend = static_cast(_data); - struct pw_proxy *proxy = - (pw_proxy *)pw_registry_bind(backend->registry, id, type, version, sizeof(PrivacyNodeInfo)); - - if (!proxy) return; - - PrivacyNodeInfo *p_node_info = (PrivacyNodeInfo *)pw_proxy_get_user_data(proxy); - p_node_info->id = id; - p_node_info->data = backend; - p_node_info->type = media_type; - p_node_info->media_class = media_class; - - pw_proxy_add_listener(proxy, &p_node_info->proxy_listener, &proxy_events, p_node_info); - - pw_proxy_add_object_listener(proxy, &p_node_info->object_listener, &node_events, p_node_info); - - backend->privacy_nodes.insert_or_assign(id, p_node_info); -} - -static void registry_event_global_remove(void *_data, uint32_t id) { - auto backend = static_cast(_data); - - backend->mutex_.lock(); - auto iter = backend->privacy_nodes.find(id); - if (iter != backend->privacy_nodes.end()) { - backend->privacy_nodes.erase(id); - } - backend->mutex_.unlock(); - - backend->privacy_nodes_changed_signal_event.emit(); -} - -static const struct pw_registry_events registry_events = { - .version = PW_VERSION_REGISTRY_EVENTS, - .global = registry_event_global, - .global_remove = registry_event_global_remove, -}; - -PipewireBackend::PipewireBackend(private_constructor_tag tag) - : mainloop_(nullptr), context_(nullptr), core_(nullptr) { - pw_init(nullptr, nullptr); - mainloop_ = pw_thread_loop_new("waybar", nullptr); - if (mainloop_ == nullptr) { - throw std::runtime_error("pw_thread_loop_new() failed."); - } - context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0); - if (context_ == nullptr) { - throw std::runtime_error("pa_context_new() failed."); - } - core_ = pw_context_connect(context_, nullptr, 0); - if (core_ == nullptr) { - throw std::runtime_error("pw_context_connect() failed"); - } - registry = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0); - - spa_zero(registry_listener); - pw_registry_add_listener(registry, ®istry_listener, ®istry_events, this); - if (pw_thread_loop_start(mainloop_) < 0) { - throw std::runtime_error("pw_thread_loop_start() failed."); - } -} - -PipewireBackend::~PipewireBackend() { - if (registry != nullptr) { - pw_proxy_destroy((struct pw_proxy *)registry); - } - - spa_zero(registry_listener); - - if (core_ != nullptr) { - pw_core_disconnect(core_); - } - - if (context_ != nullptr) { - pw_context_destroy(context_); - } - - if (mainloop_ != nullptr) { - pw_thread_loop_stop(mainloop_); - pw_thread_loop_destroy(mainloop_); - } -} - -std::shared_ptr PipewireBackend::getInstance() { - private_constructor_tag tag; - return std::make_shared(tag); -} -} // namespace waybar::util::PipewireBackend diff --git a/src/util/portal.cpp b/src/util/portal.cpp index 50c646c54..5874871b9 100644 --- a/src/util/portal.cpp +++ b/src/util/portal.cpp @@ -85,7 +85,9 @@ void waybar::Portal::on_signal(const Glib::ustring& sender_name, const Glib::ust if (signal_name != "SettingChanged" || parameters.get_n_children() != 3) { return; } - Glib::VariantBase nspcv, keyv, valuev; + Glib::VariantBase nspcv; + Glib::VariantBase keyv; + Glib::VariantBase valuev; parameters.get_child(nspcv, 0); parameters.get_child(keyv, 1); parameters.get_child(valuev, 2); diff --git a/src/util/prepare_for_sleep.cpp b/src/util/prepare_for_sleep.cpp index 661285a21..3adcdf672 100644 --- a/src/util/prepare_for_sleep.cpp +++ b/src/util/prepare_for_sleep.cpp @@ -7,14 +7,14 @@ namespace { class PrepareForSleep { private: PrepareForSleep() { - login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL); - if (!login1_connection) { + login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr); + if (login1_connection == nullptr) { spdlog::warn("Unable to connect to the SYSTEM Bus!..."); } else { login1_id = g_dbus_connection_signal_subscribe( login1_connection, "org.freedesktop.login1", "org.freedesktop.login1.Manager", - "PrepareForSleep", "/org/freedesktop/login1", NULL, G_DBUS_SIGNAL_FLAGS_NONE, - prepareForSleep_cb, this, NULL); + "PrepareForSleep", "/org/freedesktop/login1", nullptr, G_DBUS_SIGNAL_FLAGS_NONE, + prepareForSleep_cb, this, nullptr); } } @@ -22,11 +22,11 @@ class PrepareForSleep { const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { - if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)"))) { + if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)")) != 0) { gboolean sleeping; g_variant_get(parameters, "(b)", &sleeping); - PrepareForSleep *self = static_cast(user_data); + auto *self = static_cast(user_data); self->signal.emit(sleeping); } } diff --git a/src/util/regex_collection.cpp b/src/util/regex_collection.cpp index 704d65455..929e67cd6 100644 --- a/src/util/regex_collection.cpp +++ b/src/util/regex_collection.cpp @@ -3,13 +3,15 @@ #include #include +#include + namespace waybar::util { int default_priority_function(std::string& key) { return 0; } RegexCollection::RegexCollection(const Json::Value& map, std::string default_repr, - std::function priority_function) - : default_repr(default_repr) { + const std::function& priority_function) + : default_repr(std::move(default_repr)) { if (!map.isObject()) { spdlog::warn("Mapping is not an object"); return; @@ -31,11 +33,12 @@ RegexCollection::RegexCollection(const Json::Value& map, std::string default_rep std::sort(rules.begin(), rules.end(), [](Rule& a, Rule& b) { return a.priority > b.priority; }); } -std::string& RegexCollection::find_match(std::string& value, bool& matched_any) { +std::string RegexCollection::find_match(std::string& value, bool& matched_any) { for (auto& rule : rules) { - if (std::regex_search(value, rule.rule)) { + std::smatch match; + if (std::regex_search(value, match, rule.rule)) { matched_any = true; - return rule.repr; + return match.format(rule.repr.data()); } } diff --git a/src/util/sanitize_str.cpp b/src/util/sanitize_str.cpp index 131b9f28e..ae9a9e37e 100644 --- a/src/util/sanitize_str.cpp +++ b/src/util/sanitize_str.cpp @@ -10,9 +10,8 @@ std::string sanitize_string(std::string str) { const std::pair replacement_table[] = { {'&', "&"}, {'<', "<"}, {'>', ">"}, {'"', """}, {'\'', "'"}}; size_t startpoint; - for (size_t i = 0; i < (sizeof(replacement_table) / sizeof(replacement_table[0])); ++i) { + for (const auto& pair : replacement_table) { startpoint = 0; - std::pair pair = replacement_table[i]; while ((startpoint = str.find(pair.first, startpoint)) != std::string::npos) { str.replace(startpoint, 1, pair.second); startpoint += pair.second.length(); diff --git a/src/util/ustring_clen.cpp b/src/util/ustring_clen.cpp index 374df0d62..a8b9c9af6 100644 --- a/src/util/ustring_clen.cpp +++ b/src/util/ustring_clen.cpp @@ -2,8 +2,8 @@ int ustring_clen(const Glib::ustring &str) { int total = 0; - for (auto i = str.begin(); i != str.end(); ++i) { - total += g_unichar_iswide(*i) + 1; + for (unsigned int i : str) { + total += g_unichar_iswide(i) + 1; } return total; -} \ No newline at end of file +} diff --git a/subprojects/cava.wrap b/subprojects/cava.wrap index 19383d119..275ba114b 100644 --- a/subprojects/cava.wrap +++ b/subprojects/cava.wrap @@ -1,7 +1,7 @@ [wrap-file] -directory = cava-0.10.1 -source_url = https://github.com/LukashonakV/cava/archive/0.10.1.tar.gz -source_filename = cava-0.10.1.tar.gz -source_hash = ae8c7339908d6febeac5ab8df4576c03c9fdbca6c8e8975daf9ce68b57038bb5 +directory = cava-0.10.2 +source_url = https://github.com/LukashonakV/cava/archive/0.10.2.tar.gz +source_filename = cava-0.10.2.tar.gz +source_hash = dff78c4787c9843583086408a0a6e5bde7a5dee1fa17ae526847366846cb19c3 [provide] cava = cava_dep diff --git a/subprojects/spdlog.wrap b/subprojects/spdlog.wrap index 69ef566fa..08004c901 100644 --- a/subprojects/spdlog.wrap +++ b/subprojects/spdlog.wrap @@ -1,12 +1,13 @@ [wrap-file] -directory = spdlog-1.11.0 -source_url = https://github.com/gabime/spdlog/archive/v1.11.0.tar.gz -source_filename = v1.11.0.tar.gz -source_hash = ca5cae8d6cac15dae0ec63b21d6ad3530070650f68076f3a4a862ca293a858bb -patch_filename = spdlog_1.11.0-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.11.0-2/get_patch -patch_hash = db1364fe89502ac67f245a6c8c51290a52afd74a51eed26fa9ecb5b3443df57a -wrapdb_version = 1.11.0-2 +directory = spdlog-1.12.0 +source_url = https://github.com/gabime/spdlog/archive/refs/tags/v1.12.0.tar.gz +source_filename = spdlog-1.12.0.tar.gz +source_hash = 4dccf2d10f410c1e2feaff89966bfc49a1abb29ef6f08246335b110e001e09a9 +patch_filename = spdlog_1.12.0-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.12.0-2/get_patch +patch_hash = 9596972d1eb2e0a69cea4a53273ca7bbbcb9b2fa872cd734864fc7232dc2d573 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/spdlog_1.12.0-2/spdlog-1.12.0.tar.gz +wrapdb_version = 1.12.0-2 [provide] spdlog = spdlog_dep diff --git a/test/config.cpp b/test/config.cpp index ad3df0651..c60519ce2 100644 --- a/test/config.cpp +++ b/test/config.cpp @@ -117,3 +117,42 @@ TEST_CASE("Load multiple bar config with include", "[config]") { REQUIRE(data.size() == 4); REQUIRE(data[0]["output"].asString() == "OUT-0"); } + +TEST_CASE("Load Hyprland Workspaces bar config", "[config]") { + waybar::Config conf; + conf.load("test/config/hyprland-workspaces.json"); + + auto& data = conf.getConfig(); + auto hyprland = data[0]["hyprland/workspaces"]; + auto hyprland_window_rewrite = data[0]["hyprland/workspaces"]["window-rewrite"]; + auto hyprland_format_icons = data[0]["hyprland/workspaces"]["format-icons"]; + auto hyprland_persistent_workspaces = data[0]["hyprland/workspaces"]["persistent-workspaces"]; + + REQUIRE(data.isArray()); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 20); + REQUIRE(data[0]["layer"].asString() == "bottom"); + REQUIRE(data[0]["output"].isArray()); + REQUIRE(data[0]["output"][0].asString() == "HDMI-0"); + REQUIRE(data[0]["output"][1].asString() == "DP-0"); + + REQUIRE(hyprland["active-only"].asBool() == true); + REQUIRE(hyprland["all-outputs"].asBool() == false); + REQUIRE(hyprland["move-to-monitor"].asBool() == true); + REQUIRE(hyprland["format"].asString() == "{icon} {windows}"); + REQUIRE(hyprland["format-window-separator"].asString() == " "); + REQUIRE(hyprland["on-scroll-down"].asString() == "hyprctl dispatch workspace e-1"); + REQUIRE(hyprland["on-scroll-up"].asString() == "hyprctl dispatch workspace e+1"); + REQUIRE(hyprland["show-special"].asBool() == true); + REQUIRE(hyprland["window-rewrite-default"].asString() == ""); + REQUIRE(hyprland["window-rewrite-separator"].asString() == " "); + REQUIRE(hyprland_format_icons["1"].asString() == "󰎤"); + REQUIRE(hyprland_format_icons["2"].asString() == "󰎧"); + REQUIRE(hyprland_format_icons["3"].asString() == "󰎪"); + REQUIRE(hyprland_format_icons["default"].asString() == ""); + REQUIRE(hyprland_format_icons["empty"].asString() == "󱓼"); + REQUIRE(hyprland_format_icons["urgent"].asString() == "󱨇"); + REQUIRE(hyprland_persistent_workspaces["1"].asString() == "HDMI-0"); + REQUIRE(hyprland_window_rewrite["title"].asString() == ""); + REQUIRE(hyprland["sort-by"].asString() == "number"); +} diff --git a/test/config/hyprland-workspaces.json b/test/config/hyprland-workspaces.json new file mode 100644 index 000000000..dd733897f --- /dev/null +++ b/test/config/hyprland-workspaces.json @@ -0,0 +1,37 @@ +[ + { + "height": 20, + "layer": "bottom", + "output": [ + "HDMI-0", + "DP-0" + ], + "hyprland/workspaces": { + "active-only": true, + "all-outputs": false, + "show-special": true, + "move-to-monitor": true, + "format": "{icon} {windows}", + "format-window-separator": " ", + "format-icons": { + "1": "󰎤", + "2": "󰎧", + "3": "󰎪", + "default": "", + "empty": "󱓼", + "urgent": "󱨇" + }, + "persistent-workspaces": { + "1": "HDMI-0" + }, + "on-scroll-down": "hyprctl dispatch workspace e-1", + "on-scroll-up": "hyprctl dispatch workspace e+1", + "window-rewrite": { + "title": "" + }, + "window-rewrite-default": "", + "window-rewrite-separator": " ", + "sort-by": "number" + } + } +] diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp new file mode 100644 index 000000000..dcae05090 --- /dev/null +++ b/test/hyprland/backend.cpp @@ -0,0 +1,61 @@ +#if __has_include() +#include +#else +#include +#endif + +#include "fixtures/IPCTestFixture.hpp" + +namespace fs = std::filesystem; +namespace hyprland = waybar::modules::hyprland; + +TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirExists", "[getSocketFolder]") { + // Test case: XDG_RUNTIME_DIR exists and contains "hypr" directory + // Arrange + tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + fs::path expectedPath = tempDir / "hypr" / instanceSig; + fs::create_directories(tempDir / "hypr" / instanceSig); + setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); + + // Act + fs::path actualPath = getSocketFolder(instanceSig); + + // Assert expected result + REQUIRE(actualPath == expectedPath); +} + +TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirDoesNotExist", "[getSocketFolder]") { + // Test case: XDG_RUNTIME_DIR does not exist + // Arrange + unsetenv("XDG_RUNTIME_DIR"); + fs::path expectedPath = fs::path("/tmp") / "hypr" / instanceSig; + + // Act + fs::path actualPath = getSocketFolder(instanceSig); + + // Assert expected result + REQUIRE(actualPath == expectedPath); +} + +TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirExistsNoHyprDir", "[getSocketFolder]") { + // Test case: XDG_RUNTIME_DIR exists but does not contain "hypr" directory + // Arrange + fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + fs::create_directories(tempDir); + setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); + fs::path expectedPath = fs::path("/tmp") / "hypr" / instanceSig; + + // Act + fs::path actualPath = getSocketFolder(instanceSig); + + // Assert expected result + REQUIRE(actualPath == expectedPath); +} + +TEST_CASE_METHOD(IPCMock, "getSocket1JsonReply handles empty response", "[getSocket1JsonReply]") { + std::string request = "test_request"; + + Json::Value jsonResponse = getSocket1JsonReply(request); + + REQUIRE(jsonResponse.isNull()); +} diff --git a/test/hyprland/fixtures/IPCTestFixture.hpp b/test/hyprland/fixtures/IPCTestFixture.hpp new file mode 100644 index 000000000..f6fa335f7 --- /dev/null +++ b/test/hyprland/fixtures/IPCTestFixture.hpp @@ -0,0 +1,22 @@ +#include "modules/hyprland/backend.hpp" + +namespace fs = std::filesystem; +namespace hyprland = waybar::modules::hyprland; + +class IPCTestFixture : public hyprland::IPC { + public: + IPCTestFixture() : IPC() { IPC::socketFolder_ = ""; } + ~IPCTestFixture() { fs::remove_all(tempDir); } + + protected: + const char* instanceSig = "instance_sig"; + fs::path tempDir = fs::temp_directory_path() / "hypr_test"; + + private: +}; + +class IPCMock : public IPCTestFixture { + public: + // Mock getSocket1Reply to return an empty string + static std::string getSocket1Reply(const std::string& rq) { return ""; } +}; diff --git a/test/hyprland/meson.build b/test/hyprland/meson.build new file mode 100644 index 000000000..533022fc8 --- /dev/null +++ b/test/hyprland/meson.build @@ -0,0 +1,28 @@ +test_inc = include_directories('../../include') + +test_dep = [ + catch2, + fmt, + gtkmm, + jsoncpp, + spdlog, +] + +test_src = files( + '../main.cpp', + 'backend.cpp', + '../../src/modules/hyprland/backend.cpp' +) + +hyprland_test = executable( + 'hyprland_test', + test_src, + dependencies: test_dep, + include_directories: test_inc, +) + +test( + 'hyprland', + hyprland_test, + workdir: meson.project_source_root(), +) diff --git a/test/meson.build b/test/meson.build index 7c9226712..ea430c504 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,4 +1,5 @@ test_inc = include_directories('../include') + test_dep = [ catch2, fmt, @@ -6,21 +7,13 @@ test_dep = [ jsoncpp, spdlog, ] + test_src = files( 'main.cpp', - 'JsonParser.cpp', - 'SafeSignal.cpp', 'config.cpp', - 'css_reload_helper.cpp', '../src/config.cpp', - '../src/util/css_reload_helper.cpp', ) -if tz_dep.found() - test_dep += tz_dep - test_src += files('date.cpp') -endif - waybar_test = executable( 'waybar_test', test_src, @@ -33,3 +26,6 @@ test( waybar_test, workdir: meson.project_source_root(), ) + +subdir('utils') +subdir('hyprland') diff --git a/test/JsonParser.cpp b/test/utils/JsonParser.cpp similarity index 100% rename from test/JsonParser.cpp rename to test/utils/JsonParser.cpp diff --git a/test/SafeSignal.cpp b/test/utils/SafeSignal.cpp similarity index 98% rename from test/SafeSignal.cpp rename to test/utils/SafeSignal.cpp index f496d7ab2..341e8e2ea 100644 --- a/test/SafeSignal.cpp +++ b/test/utils/SafeSignal.cpp @@ -10,7 +10,7 @@ #include #include -#include "GlibTestsFixture.hpp" +#include "fixtures/GlibTestsFixture.hpp" using namespace waybar; diff --git a/test/css_reload_helper.cpp b/test/utils/css_reload_helper.cpp similarity index 100% rename from test/css_reload_helper.cpp rename to test/utils/css_reload_helper.cpp diff --git a/test/date.cpp b/test/utils/date.cpp similarity index 100% rename from test/date.cpp rename to test/utils/date.cpp diff --git a/test/GlibTestsFixture.hpp b/test/utils/fixtures/GlibTestsFixture.hpp similarity index 100% rename from test/GlibTestsFixture.hpp rename to test/utils/fixtures/GlibTestsFixture.hpp diff --git a/test/utils/meson.build b/test/utils/meson.build new file mode 100644 index 000000000..b7b3665a8 --- /dev/null +++ b/test/utils/meson.build @@ -0,0 +1,36 @@ +test_inc = include_directories('../../include') + +test_dep = [ + catch2, + fmt, + gtkmm, + jsoncpp, + spdlog, +] +test_src = files( + '../main.cpp', + '../config.cpp', + '../../src/config.cpp', + 'JsonParser.cpp', + 'SafeSignal.cpp', + 'css_reload_helper.cpp', + '../../src/util/css_reload_helper.cpp', +) + +if tz_dep.found() + test_dep += tz_dep + test_src += files('date.cpp') +endif + +utils_test = executable( + 'utils_test', + test_src, + dependencies: test_dep, + include_directories: test_inc, +) + +test( + 'utils', + utils_test, + workdir: meson.project_source_root(), +)