diff --git a/.envrc.sample b/.envrc.sample new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc.sample @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index a6da7ef..550f945 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -9,7 +9,7 @@ jobs: # https://github.com/actions/virtual-environments/issues/4060 - for lack of VirtualBox on MacOS 11 runners runs-on: macos-12 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Test in FreeBSD VM uses: vmactions/freebsd-vm@v0 with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e9f1656..d11d2cc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: DoozyX/clang-format-lint-action@v0.13 with: source: '.' diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 4c77c3a..1c00a5e 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -13,16 +13,20 @@ jobs: - fedora - opensuse - gentoo + cpp_std: [c++17] + include: + - distro: fedora + cpp_std: c++20 runs-on: ubuntu-latest container: image: alexays/waybar:${{ matrix.distro }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: configure - run: meson -Dman-pages=enabled build + run: meson -Dman-pages=enabled -Dcpp_std=${{matrix.cpp_std}} build - name: build run: ninja -C build - name: test - run: meson test -C build --no-rebuild --print-errorlogs --suite waybar + run: make test diff --git a/.gitignore b/.gitignore index 56a2f73..4d7babf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *~ vgcore.* /.vscode +/.idea +/.cache *.swp packagecache /subprojects/**/ @@ -41,3 +43,4 @@ packagecache *.exe *.out *.app +/.direnv/ diff --git a/Dockerfiles/alpine b/Dockerfiles/alpine index 03836aa..d9a3dd5 100644 --- a/Dockerfiles/alpine +++ b/Dockerfiles/alpine @@ -2,4 +2,4 @@ FROM alpine:latest -RUN apk add --no-cache git meson alpine-sdk libinput-dev wayland-dev wayland-protocols mesa-dev libxkbcommon-dev eudev-dev pixman-dev gtkmm3-dev jsoncpp-dev pugixml-dev libnl3-dev pulseaudio-dev libmpdclient-dev sndio-dev scdoc libxkbcommon tzdata +RUN apk add --no-cache git meson alpine-sdk libinput-dev wayland-dev wayland-protocols mesa-dev libxkbcommon-dev eudev-dev pixman-dev gtkmm3-dev jsoncpp-dev pugixml-dev libnl3-dev pulseaudio-dev libmpdclient-dev sndio-dev scdoc libxkbcommon tzdata playerctl-dev diff --git a/Dockerfiles/archlinux b/Dockerfiles/archlinux index 40a1b2e..cab4146 100644 --- a/Dockerfiles/archlinux +++ b/Dockerfiles/archlinux @@ -3,4 +3,5 @@ FROM archlinux:base-devel RUN pacman -Syu --noconfirm && \ - pacman -S git meson base-devel libinput wayland wayland-protocols pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient gobject-introspection --noconfirm libxkbcommon + 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 && \ + sed -Ei 's/#(en_(US|GB)\.UTF)/\1/' /etc/locale.gen && locale-gen diff --git a/Dockerfiles/debian b/Dockerfiles/debian index 026d8fd..578588c 100644 --- a/Dockerfiles/debian +++ b/Dockerfiles/debian @@ -3,5 +3,5 @@ FROM debian:sid RUN apt-get update && \ - apt-get install -y build-essential meson ninja-build git pkg-config libinput10 libpugixml-dev libinput-dev wayland-protocols libwayland-client0 libwayland-cursor0 libwayland-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libxkbcommon-dev libudev-dev libpixman-1-dev libgtkmm-3.0-dev libjsoncpp-dev scdoc libdbusmenu-gtk3-dev libnl-3-dev libnl-genl-3-dev libpulse-dev libmpdclient-dev gobject-introspection libgirepository1.0-dev libxkbcommon-dev libxkbregistry-dev libxkbregistry0 && \ + apt-get install -y build-essential meson ninja-build git pkg-config libinput10 libpugixml-dev libinput-dev wayland-protocols libwayland-client0 libwayland-cursor0 libwayland-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libxkbcommon-dev libudev-dev libpixman-1-dev libgtkmm-3.0-dev libjsoncpp-dev scdoc libdbusmenu-gtk3-dev libnl-3-dev libnl-genl-3-dev libpulse-dev libmpdclient-dev gobject-introspection libgirepository1.0-dev libxkbcommon-dev libxkbregistry-dev libxkbregistry0 libplayerctl-dev && \ apt-get clean diff --git a/Dockerfiles/fedora b/Dockerfiles/fedora index a61dcd3..5892159 100644 --- a/Dockerfiles/fedora +++ b/Dockerfiles/fedora @@ -2,11 +2,33 @@ FROM fedora:latest -RUN dnf install -y @c-development git-core meson scdoc 'pkgconfig(date)' \ - 'pkgconfig(dbusmenu-gtk3-0.4)' 'pkgconfig(fmt)' 'pkgconfig(gdk-pixbuf-2.0)' \ - 'pkgconfig(gio-unix-2.0)' 'pkgconfig(gtk-layer-shell-0)' 'pkgconfig(gtkmm-3.0)' \ - 'pkgconfig(jsoncpp)' 'pkgconfig(libinput)' 'pkgconfig(libmpdclient)' \ - 'pkgconfig(libnl-3.0)' 'pkgconfig(libnl-genl-3.0)' 'pkgconfig(libpulse)' \ - 'pkgconfig(libudev)' 'pkgconfig(pugixml)' 'pkgconfig(sigc++-2.0)' 'pkgconfig(spdlog)' \ - 'pkgconfig(wayland-client)' 'pkgconfig(wayland-cursor)' 'pkgconfig(wayland-protocols)' 'pkgconfig(xkbregistry)' && \ +RUN dnf install -y @c-development \ + git-core glibc-langpack-en meson scdoc \ + 'pkgconfig(catch2)' \ + 'pkgconfig(date)' \ + 'pkgconfig(dbusmenu-gtk3-0.4)' \ + 'pkgconfig(fmt)' \ + 'pkgconfig(gdk-pixbuf-2.0)' \ + 'pkgconfig(gio-unix-2.0)' \ + 'pkgconfig(gtk-layer-shell-0)' \ + 'pkgconfig(gtkmm-3.0)' \ + 'pkgconfig(jack)' \ + 'pkgconfig(jsoncpp)' \ + 'pkgconfig(libevdev)' \ + 'pkgconfig(libinput)' \ + 'pkgconfig(libmpdclient)' \ + 'pkgconfig(libnl-3.0)' \ + 'pkgconfig(libnl-genl-3.0)' \ + 'pkgconfig(libpulse)' \ + 'pkgconfig(libudev)' \ + 'pkgconfig(playerctl)' \ + 'pkgconfig(pugixml)' \ + 'pkgconfig(sigc++-2.0)' \ + 'pkgconfig(spdlog)' \ + 'pkgconfig(upower-glib)' \ + 'pkgconfig(wayland-client)' \ + 'pkgconfig(wayland-cursor)' \ + 'pkgconfig(wayland-protocols)' \ + 'pkgconfig(wireplumber-0.4)' \ + 'pkgconfig(xkbregistry)' && \ dnf clean all -y diff --git a/Dockerfiles/gentoo b/Dockerfiles/gentoo index 536ef63..f2ec0dc 100644 --- a/Dockerfiles/gentoo +++ b/Dockerfiles/gentoo @@ -6,6 +6,6 @@ RUN export FEATURES="-ipc-sandbox -network-sandbox -pid-sandbox -sandbox -usersa emerge --sync && \ eselect news read --quiet new 1>/dev/null 2>&1 && \ emerge --verbose --update --deep --with-bdeps=y --backtrack=30 --newuse @world && \ - USE="wayland gtk3 gtk -doc X" emerge dev-vcs/git dev-libs/wayland dev-libs/wayland-protocols =dev-cpp/gtkmm-3.24.6 x11-libs/libxkbcommon \ + USE="wayland gtk3 gtk -doc X pulseaudio minimal" emerge dev-vcs/git dev-libs/wayland dev-libs/wayland-protocols =dev-cpp/gtkmm-3.24.6 x11-libs/libxkbcommon \ x11-libs/gtk+:3 dev-libs/libdbusmenu dev-libs/libnl sys-power/upower media-libs/libpulse dev-libs/libevdev media-libs/libmpdclient \ - media-sound/sndio gui-libs/gtk-layer-shell app-text/scdoc + media-sound/sndio gui-libs/gtk-layer-shell app-text/scdoc media-sound/playerctl dev-libs/iniparser sci-libs/fftw diff --git a/Dockerfiles/opensuse b/Dockerfiles/opensuse index 49dea27..bdb42fb 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 + 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 diff --git a/Makefile b/Makefile index 94f8ee6..b1dbfc6 100644 --- a/Makefile +++ b/Makefile @@ -19,5 +19,9 @@ run: build debug-run: build-debug ./build/waybar --log-level debug +test: + meson test -C build --no-rebuild --verbose --suite waybar +.PHONY: test + clean: rm -rf build diff --git a/README.md b/README.md index 2b57b13..718ceb4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Waybar [![Licence](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Paypal Donate](https://img.shields.io/badge/Donate-Paypal-2244dd.svg)](https://paypal.me/ARouillard)
![Waybar](https://raw.githubusercontent.com/alexays/waybar/master/preview-2.png) > Highly customizable Wayland bar for Sway and Wlroots based compositors.
-> Available in Arch [community](https://www.archlinux.org/packages/community/x86_64/waybar/) or +> Available in Arch [community](https://www.archlinux.org/packages/extra/x86_64/waybar/) or [AUR](https://aur.archlinux.org/packages/waybar-git/), [Gentoo](https://packages.gentoo.org/packages/gui-apps/waybar), [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/waybar), and [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=waybar).
> *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)* @@ -9,6 +9,7 @@ - Sway (Workspaces, Binding mode, Focused window name) - River (Mapping mode, Tags, Focused window name) - Hyprland (Focused window name) +- DWL (Tags) [requires dwl ipc patch](https://github.com/djpohly/dwl/wiki/ipc) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time - Battery diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..2cccff2 --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +(import + ( + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { src = ./.; } +).defaultNix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..b10c9bf --- /dev/null +++ b/flake.lock @@ -0,0 +1,111 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1676293499, + "narHash": "sha256-uIOTlTxvrXxpKeTvwBI1JGDGtCxMXE3BI0LFwoQMhiQ=", + "owner": "numtide", + "repo": "devshell", + "rev": "71e3022e3ab20bbf1342640547ef5bc14fb43bf4", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1642700792, + "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1676283394, + "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1643381941, + "narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1676300157, + "narHash": "sha256-1HjRzfp6LOLfcj/HJHdVKWAkX9QRAouoh6AjzJiIerU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "545c7a31e5dedea4a6d372712a18e00ce097d462", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-compat": "flake-compat", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..97f4ed5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,93 @@ +{ + description = "Highly customizable Wayland bar for Sway and Wlroots based compositors."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + devshell.url = "github:numtide/devshell"; + flake-utils.url = "github:numtide/flake-utils"; + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + }; + + outputs = { self, flake-utils, devshell, nixpkgs, flake-compat }: + let + inherit (nixpkgs) lib; + genSystems = lib.genAttrs [ + "x86_64-linux" + ]; + + pkgsFor = genSystems (system: + import nixpkgs { + inherit system; + }); + + mkDate = longDate: (lib.concatStringsSep "-" [ + (builtins.substring 0 4 longDate) + (builtins.substring 4 2 longDate) + (builtins.substring 6 2 longDate) + ]); + in + { + overlays.default = _: prev: { + waybar = prev.callPackage ./nix/default.nix { + version = prev.waybar.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); + }; + }; + packages = genSystems + (system: + (self.overlays.default null pkgsFor.${system}) + // { + default = self.packages.${system}.waybar; + }); + } // + flake-utils.lib.eachDefaultSystem (system: { + devShell = + let pkgs = import nixpkgs { + inherit system; + + overlays = [ devshell.overlay ]; + }; + in + pkgs.devshell.mkShell { + imports = [ "${pkgs.devshell.extraModulesDir}/language/c.nix" ]; + commands = [ + { + package = pkgs.devshell.cli; + help = "Per project developer environments"; + } + ]; + devshell.packages = with pkgs; [ + clang-tools + gdb + # from nativeBuildInputs + gnumake + meson + ninja + pkg-config + scdoc + ] ++ (map lib.getDev [ + # from buildInputs + wayland wlroots gtkmm3 libsigcxx jsoncpp spdlog gtk-layer-shell howard-hinnant-date libxkbcommon + # optional dependencies + gobject-introspection glib playerctl python3.pkgs.pygobject3 + libevdev libinput libjack2 libmpdclient playerctl libnl + libpulseaudio sndio sway libdbusmenu-gtk3 udev upower wireplumber + + # from propagated build inputs? + at-spi2-atk atkmm cairo cairomm catch2 fmt_8 fontconfig + gdk-pixbuf glibmm gtk3 harfbuzz pango pangomm wayland-protocols + ]); + env = with pkgs; [ + { name = "CPLUS_INCLUDE_PATH"; prefix = "$DEVSHELL_DIR/include"; } + { name = "PKG_CONFIG_PATH"; prefix = "$DEVSHELL_DIR/lib/pkgconfig"; } + { name = "PKG_CONFIG_PATH"; prefix = "$DEVSHELL_DIR/share/pkgconfig"; } + { name = "PATH"; prefix = "${wayland.bin}/bin"; } + { name = "LIBRARY_PATH"; prefix = "${lib.getLib sndio}/lib"; } + { name = "LIBRARY_PATH"; prefix = "${lib.getLib zlib}/lib"; } + { name = "LIBRARY_PATH"; prefix = "${lib.getLib howard-hinnant-date}/lib"; } + ]; + }; + }); +} diff --git a/include/AIconLabel.hpp b/include/AIconLabel.hpp index aeeba6c..054d031 100644 --- a/include/AIconLabel.hpp +++ b/include/AIconLabel.hpp @@ -13,7 +13,7 @@ class AIconLabel : public ALabel { const std::string &format, uint16_t interval = 0, bool ellipsize = false, bool enable_click = false, bool enable_scroll = false); virtual ~AIconLabel() = default; - virtual auto update() -> void; + auto update() -> void override; protected: Gtk::Image image_; diff --git a/include/ALabel.hpp b/include/ALabel.hpp index 14f8224..888c65a 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -14,7 +14,7 @@ class ALabel : public AModule { uint16_t interval = 0, bool ellipsize = false, bool enable_click = false, bool enable_scroll = false); virtual ~ALabel() = default; - virtual auto update() -> void; + auto update() -> void override; virtual std::string getIcon(uint16_t, const std::string &alt = "", uint16_t max = 0); virtual std::string getIcon(uint16_t, const std::vector &alts, uint16_t max = 0); @@ -25,7 +25,7 @@ class ALabel : public AModule { bool alt_ = false; std::string default_format_; - virtual bool handleToggle(GdkEventButton *const &e); + bool handleToggle(GdkEventButton *const &e) override; virtual std::string getState(uint8_t value, bool lesser = false); }; diff --git a/include/AModule.hpp b/include/AModule.hpp index 357f70e..9b16076 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -11,15 +11,20 @@ namespace waybar { class AModule : public IModule { public: - AModule(const Json::Value &, const std::string &, const std::string &, bool enable_click = false, - bool enable_scroll = false); virtual ~AModule(); - virtual auto update() -> void; - virtual operator Gtk::Widget &(); + auto update() -> void override; + virtual auto refresh(int) -> void{}; + operator Gtk::Widget &() override; + auto doAction(const std::string &name) -> void override; Glib::Dispatcher dp; protected: + // Don't need to make an object directly + // Derived classes are able to use it + AModule(const Json::Value &, const std::string &, const std::string &, bool enable_click = false, + bool enable_scroll = false); + enum SCROLL_DIR { NONE, UP, DOWN, LEFT, RIGHT }; SCROLL_DIR getScrollDir(GdkEventScroll *e); @@ -36,6 +41,7 @@ class AModule : public IModule { std::vector pid_; gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; + std::map eventActionMap_; static const inline std::map, std::string> eventMap_{ {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, {std::make_pair(1, GdkEventType::GDK_2BUTTON_PRESS), "on-double-click"}, diff --git a/include/IModule.hpp b/include/IModule.hpp index 961a461..b76c33e 100644 --- a/include/IModule.hpp +++ b/include/IModule.hpp @@ -9,6 +9,7 @@ class IModule { virtual ~IModule() = default; virtual auto update() -> void = 0; virtual operator Gtk::Widget&() = 0; + virtual auto doAction(const std::string& name) -> void = 0; }; } // namespace waybar diff --git a/include/factory.hpp b/include/factory.hpp index d69930f..90d0ac1 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -1,7 +1,7 @@ #pragma once #include -#ifdef HAVE_LIBDATE +#if defined(HAVE_CHRONO_TIMEZONES) || defined(HAVE_LIBDATE) #include "modules/clock.hpp" #else #include "modules/simpleclock.hpp" @@ -18,14 +18,20 @@ #include "modules/wlr/workspace_manager.hpp" #endif #ifdef HAVE_RIVER +#include "modules/river/layout.hpp" #include "modules/river/mode.hpp" #include "modules/river/tags.hpp" #include "modules/river/window.hpp" #endif +#ifdef HAVE_DWL +#include "modules/dwl/tags.hpp" +#endif #ifdef HAVE_HYPRLAND #include "modules/hyprland/backend.hpp" #include "modules/hyprland/language.hpp" +#include "modules/hyprland/submap.hpp" #include "modules/hyprland/window.hpp" +#include "modules/hyprland/workspaces.hpp" #endif #if defined(__FreeBSD__) || (defined(__linux__) && !defined(NO_FILESYSTEM)) #include "modules/battery.hpp" @@ -41,6 +47,9 @@ #ifdef HAVE_DBUSMENU #include "modules/sni/tray.hpp" #endif +#ifdef HAVE_MPRIS +#include "modules/mpris/mpris.hpp" +#endif #ifdef HAVE_LIBNL #include "modules/network.hpp" #endif @@ -75,6 +84,9 @@ #ifdef HAVE_LIBWIREPLUMBER #include "modules/wireplumber.hpp" #endif +#ifdef HAVE_LIBCAVA +#include "modules/cava.hpp" +#endif #include "bar.hpp" #include "modules/custom.hpp" #include "modules/image.hpp" diff --git a/include/group.hpp b/include/group.hpp index 5e82867..60e31c9 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -12,10 +12,10 @@ namespace waybar { class Group : public AModule { public: - Group(const std::string&, const Bar&, const Json::Value&); + Group(const std::string&, const std::string&, const Json::Value&, bool); ~Group() = default; - auto update() -> void; - operator Gtk::Widget&(); + auto update() -> void override; + operator Gtk::Widget&() override; Gtk::Box box; }; diff --git a/include/modules/backlight.hpp b/include/modules/backlight.hpp index 81e2516..ade4bc7 100644 --- a/include/modules/backlight.hpp +++ b/include/modules/backlight.hpp @@ -6,6 +6,7 @@ #include #include "ALabel.hpp" +#include "giomm/dbusproxy.h" #include "util/json.hpp" #include "util/sleeper_thread.hpp" @@ -39,8 +40,8 @@ class Backlight : public ALabel { public: Backlight(const std::string &, const Json::Value &); - ~Backlight(); - auto update() -> void; + virtual ~Backlight(); + auto update() -> void override; private: template @@ -50,6 +51,8 @@ class Backlight : public ALabel { template static void enumerate_devices(ForwardIt first, ForwardIt last, Inserter inserter, udev *udev); + bool handleScroll(GdkEventScroll *e) override; + const std::string preferred_device_; static constexpr int EPOLL_MAX_EVENTS = 16; @@ -60,5 +63,7 @@ class Backlight : public ALabel { std::vector devices_; // thread must destruct before shared data util::SleeperThread udev_thread_; + + Glib::RefPtr login_proxy_; }; } // namespace waybar::modules diff --git a/include/modules/battery.hpp b/include/modules/battery.hpp index 54a7dd3..017b0e4 100644 --- a/include/modules/battery.hpp +++ b/include/modules/battery.hpp @@ -29,8 +29,8 @@ namespace fs = std::filesystem; class Battery : public ALabel { public: Battery(const std::string&, const Json::Value&); - ~Battery(); - auto update() -> void; + virtual ~Battery(); + auto update() -> void override; private: static inline const fs::path data_dir_ = "/sys/class/power_supply/"; diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index bd9737b..18481e3 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -45,8 +45,8 @@ class Bluetooth : public ALabel { public: Bluetooth(const std::string&, const Json::Value&); - ~Bluetooth() = default; - auto update() -> void; + virtual ~Bluetooth() = default; + auto update() -> void override; private: static auto onInterfaceAddedOrRemoved(GDBusObjectManager*, GDBusObject*, GDBusInterface*, diff --git a/include/modules/cava.hpp b/include/modules/cava.hpp new file mode 100644 index 0000000..d4da2b7 --- /dev/null +++ b/include/modules/cava.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +extern "C" { +#include +} + +namespace waybar::modules { +using namespace std::literals::chrono_literals; + +class Cava final : public ALabel { + public: + Cava(const std::string&, const Json::Value&); + virtual ~Cava(); + auto update() -> void override; + auto doAction(const std::string& name) -> void override; + + private: + util::SleeperThread thread_; + util::SleeperThread thread_fetch_input_; + + struct error_s error_ {}; // cava errors + struct config_params prm_ {}; // cava parameters + struct audio_raw audio_raw_ {}; // cava handled raw audio data(is based on audio_data) + struct audio_data audio_data_ {}; // cava audio data + struct cava_plan* plan_; //{new cava_plan{}}; + // Cava API to read audio source + ptr input_source_; + // Delay to handle audio source + std::chrono::milliseconds frame_time_milsec_{1s}; + // Text to display + std::string text_{""}; + int rePaint_{1}; + std::chrono::seconds fetch_input_delay_{4}; + std::chrono::seconds suspend_silence_delay_{0}; + bool silence_{false}; + int sleep_counter_{0}; + // Cava method + void pause_resume(); + // ModuleActionMap + static inline std::map actionMap_{ + {"mode", &waybar::modules::Cava::pause_resume}}; +}; +} // namespace waybar::modules diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index 08ab05e..fab3811 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -1,44 +1,72 @@ #pragma once -#include - #include "ALabel.hpp" +#include "util/date.hpp" #include "util/sleeper_thread.hpp" -namespace waybar { - -struct waybar_time; - -namespace modules { +namespace waybar::modules { const std::string kCalendarPlaceholder = "calendar"; const std::string KTimezonedTimeListPlaceholder = "timezoned_time_list"; -class Clock : public ALabel { +enum class WeeksSide { + LEFT, + RIGHT, + HIDDEN, +}; + +enum class CldMode { MONTH, YEAR }; + +class Clock final : public ALabel { public: Clock(const std::string&, const Json::Value&); - ~Clock() = default; - auto update() -> void; + virtual ~Clock() = default; + auto update() -> void override; + auto doAction(const std::string& name) -> void override; private: util::SleeperThread thread_; std::locale locale_; std::vector time_zones_; int current_time_zone_idx_; - date::year_month_day calendar_cached_ymd_{date::January / 1 / 0}; - date::months calendar_shift_{0}, calendar_shift_init_{0}; - std::string calendar_cached_text_; bool is_calendar_in_tooltip_; bool is_timezoned_list_in_tooltip_; - bool handleScroll(GdkEventScroll* e); - - auto calendar_text(const waybar_time& wtime) -> std::string; - auto weekdays_header(const date::weekday& first_dow, std::ostream& os) -> void; auto first_day_of_week() -> date::weekday; const date::time_zone* current_timezone(); bool is_timezone_fixed(); auto timezones_text(std::chrono::system_clock::time_point* now) -> std::string; + + /*Calendar properties*/ + WeeksSide cldWPos_{WeeksSide::HIDDEN}; + std::map fmtMap_; + CldMode cldMode_{CldMode::MONTH}; + uint cldMonCols_{3}; // Count of the month in the row + int cldMonColLen_{20}; // Length of the month column + int cldWnLen_{3}; // Length of the week number + date::year_month_day cldYearShift_; + date::year_month cldMonShift_; + date::months cldCurrShift_{0}; + date::months cldShift_{0}; + std::string cldYearCached_{}; + std::string cldMonCached_{}; + date::day cldBaseDay_{0}; + /*Calendar functions*/ + auto get_calendar(const date::zoned_seconds& now, const date::zoned_seconds& wtime) + -> std::string; + /*Clock actions*/ + void cldModeSwitch(); + void cldShift_up(); + void cldShift_down(); + void tz_up(); + void tz_down(); + + // ModuleActionMap + static inline std::map actionMap_{ + {"mode", &waybar::modules::Clock::cldModeSwitch}, + {"shift_up", &waybar::modules::Clock::cldShift_up}, + {"shift_down", &waybar::modules::Clock::cldShift_down}, + {"tz_up", &waybar::modules::Clock::tz_up}, + {"tz_down", &waybar::modules::Clock::tz_down}}; }; -} // namespace modules -} // namespace waybar +} // namespace waybar::modules diff --git a/include/modules/cpu.hpp b/include/modules/cpu.hpp index 539f926..a523548 100644 --- a/include/modules/cpu.hpp +++ b/include/modules/cpu.hpp @@ -17,8 +17,8 @@ namespace waybar::modules { class Cpu : public ALabel { public: Cpu(const std::string&, const Json::Value&); - ~Cpu() = default; - auto update() -> void; + virtual ~Cpu() = default; + auto update() -> void override; private: double getCpuLoad(); diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index 711d07e..a6024a8 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -15,9 +15,9 @@ namespace waybar::modules { class Custom : public ALabel { public: Custom(const std::string&, const std::string&, const Json::Value&); - ~Custom(); - auto update() -> void; - void refresh(int /*signal*/); + virtual ~Custom(); + auto update() -> void override; + void refresh(int /*signal*/) override; private: void delayWorker(); @@ -25,8 +25,8 @@ class Custom : public ALabel { void parseOutputRaw(); void parseOutputJson(); void handleEvent(); - bool handleScroll(GdkEventScroll* e); - bool handleToggle(GdkEventButton* const& e); + bool handleScroll(GdkEventScroll* e) override; + bool handleToggle(GdkEventButton* const& e) override; const std::string name_; std::string text_; diff --git a/include/modules/disk.hpp b/include/modules/disk.hpp index ec386b2..2a307c9 100644 --- a/include/modules/disk.hpp +++ b/include/modules/disk.hpp @@ -14,8 +14,8 @@ namespace waybar::modules { class Disk : public ALabel { public: Disk(const std::string&, const Json::Value&); - ~Disk() = default; - auto update() -> void; + virtual ~Disk() = default; + auto update() -> void override; private: util::SleeperThread thread_; diff --git a/include/modules/dwl/tags.hpp b/include/modules/dwl/tags.hpp new file mode 100644 index 0000000..53dff98 --- /dev/null +++ b/include/modules/dwl/tags.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "dwl-ipc-unstable-v2-client-protocol.h" +#include "xdg-output-unstable-v1-client-protocol.h" + +namespace waybar::modules::dwl { + +class Tags : public waybar::AModule { + public: + Tags(const std::string &, const waybar::Bar &, const Json::Value &); + virtual ~Tags(); + + // Handlers for wayland events + void handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused); + + void handle_primary_clicked(uint32_t tag); + bool handle_button_press(GdkEventButton *event_button, uint32_t tag); + + struct zdwl_ipc_manager_v2 *status_manager_; + struct wl_seat *seat_; + + private: + const waybar::Bar &bar_; + Gtk::Box box_; + std::vector buttons_; + struct zdwl_ipc_output_v2 *output_status_; +}; + +} /* namespace waybar::modules::dwl */ diff --git a/include/modules/gamemode.hpp b/include/modules/gamemode.hpp index b027393..69c0c3a 100644 --- a/include/modules/gamemode.hpp +++ b/include/modules/gamemode.hpp @@ -18,8 +18,8 @@ namespace waybar::modules { class Gamemode : public AModule { public: Gamemode(const std::string &, const Json::Value &); - ~Gamemode(); - auto update() -> void; + virtual ~Gamemode(); + auto update() -> void override; private: const std::string DEFAULT_ICON_NAME = "input-gaming-symbolic"; @@ -39,7 +39,7 @@ class Gamemode : public AModule { const Glib::VariantContainerBase &arguments); void getData(); - bool handleToggle(GdkEventButton *const &); + bool handleToggle(GdkEventButton *const &) override; // Config std::string format = DEFAULT_FORMAT; diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index d876781..e23b158 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -6,6 +6,8 @@ #include #include +#include "util/json.hpp" + namespace waybar::modules::hyprland { class EventHandler { @@ -22,12 +24,14 @@ class IPC { void unregisterForIPC(EventHandler*); std::string getSocket1Reply(const std::string& rq); + Json::Value getSocket1JsonReply(const std::string& rq); private: void startIPC(); void parseIPC(const std::string&); std::mutex callbackMutex; + util::JsonParser parser_; std::list> callbacks; }; diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index a07cfc0..30789d0 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -10,20 +10,29 @@ namespace waybar::modules::hyprland { class Language : public waybar::ALabel, public EventHandler { public: Language(const std::string&, const waybar::Bar&, const Json::Value&); - ~Language(); + virtual ~Language(); - auto update() -> void; + auto update() -> void override; private: - void onEvent(const std::string&); + void onEvent(const std::string&) override; void initLanguage(); - std::string getShortFrom(const std::string&); + + struct Layout { + std::string full_name; + std::string short_name; + std::string variant; + std::string short_description; + }; + + auto getLayout(const std::string&) -> Layout; std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; - std::string layoutName_; + + Layout layout_; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/submap.hpp b/include/modules/hyprland/submap.hpp new file mode 100644 index 0000000..e2a8498 --- /dev/null +++ b/include/modules/hyprland/submap.hpp @@ -0,0 +1,26 @@ +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "util/json.hpp" + +namespace waybar::modules::hyprland { + +class Submap : public waybar::ALabel, public EventHandler { + public: + Submap(const std::string&, const waybar::Bar&, const Json::Value&); + virtual ~Submap(); + + auto update() -> void override; + + private: + void onEvent(const std::string&) override; + + std::mutex mutex_; + const Bar& bar_; + util::JsonParser parser_; + std::string submap_; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index 35438cd..950be05 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -1,7 +1,5 @@ #include -#include - #include "ALabel.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" @@ -12,20 +10,37 @@ namespace waybar::modules::hyprland { class Window : public waybar::ALabel, public EventHandler { public: Window(const std::string&, const waybar::Bar&, const Json::Value&); - ~Window(); + virtual ~Window(); - auto update() -> void; + auto update() -> void override; private: - int getActiveWorkspaceID(std::string); - std::string getLastWindowTitle(int); - void onEvent(const std::string&); + struct Workspace { + int id; + int windows; + std::string last_window; + std::string last_window_title; + + static auto parse(const Json::Value&) -> Workspace; + }; + + auto getActiveWorkspace(const std::string&) -> Workspace; + auto getActiveWorkspace() -> Workspace; + void onEvent(const std::string&) override; + void queryActiveWorkspace(); + void setClass(const std::string&, bool enable); bool separate_outputs; std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; - std::string lastView; + std::string last_title_; + Workspace workspace_; + std::string solo_class_; + std::string last_solo_class_; + bool solo_; + bool all_floating_; + bool fullscreen_; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp new file mode 100644 index 0000000..500bbe3 --- /dev/null +++ b/include/modules/hyprland/workspaces.hpp @@ -0,0 +1,64 @@ +#include +#include + +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" + +namespace waybar::modules::hyprland { + +struct WorkspaceDto { + int id; + + static WorkspaceDto parse(const Json::Value& value); +}; + +class Workspace { + public: + Workspace(int id); + Workspace(WorkspaceDto dto); + int id() const { return id_; }; + int active() const { return active_; }; + std::string& select_icon(std::map& icons_map); + void set_active(bool value = true) { active_ = value; }; + Gtk::Button& button() { return button_; }; + + void update(const std::string& format, const std::string& icon); + + private: + int id_; + bool active_; + + Gtk::Button button_; + Gtk::Box content_; + Gtk::Label label_; +}; + +class Workspaces : public AModule, public EventHandler { + public: + Workspaces(const std::string&, const waybar::Bar&, const Json::Value&); + virtual ~Workspaces(); + void update() override; + void init(); + + private: + void onEvent(const std::string&) override; + void sort_workspaces(); + void create_workspace(int id); + void remove_workspace(int id); + + std::string format_; + std::map icons_map_; + bool with_icon_; + int active_workspace_id; + std::vector> workspaces_; + std::vector workspaces_to_create_; + std::vector workspaces_to_remove_; + std::mutex mutex_; + const Bar& bar_; + Gtk::Box box_; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 8378e58..22bd808 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -13,13 +13,13 @@ class IdleInhibitor : public ALabel { public: IdleInhibitor(const std::string&, const waybar::Bar&, const Json::Value&); - ~IdleInhibitor(); - auto update() -> void; + virtual ~IdleInhibitor(); + auto update() -> void override; static std::list modules; static bool status; private: - bool handleToggle(GdkEventButton* const& e); + bool handleToggle(GdkEventButton* const& e) override; void toggleStatus(); const Bar& bar_; diff --git a/include/modules/image.hpp b/include/modules/image.hpp index 00b8393..7c0d014 100644 --- a/include/modules/image.hpp +++ b/include/modules/image.hpp @@ -7,6 +7,7 @@ #include #include "ALabel.hpp" +#include "gtkmm/box.h" #include "util/command.hpp" #include "util/json.hpp" #include "util/sleeper_thread.hpp" @@ -15,18 +16,23 @@ namespace waybar::modules { class Image : public AModule { public: - Image(const std::string&, const std::string&, const Json::Value&); - auto update() -> void; - void refresh(int /*signal*/); + Image(const std::string&, const Json::Value&); + virtual ~Image() = default; + auto update() -> void override; + void refresh(int /*signal*/) override; private: void delayWorker(); void handleEvent(); + void parseOutputRaw(); - Gtk::Image image_; + Gtk::Box box_; + Gtk::Image image_; std::string path_; - int size_; - int interval_; + std::string tooltip_; + int size_; + int interval_; + util::command::res output_; util::SleeperThread thread_; }; diff --git a/include/modules/inhibitor.hpp b/include/modules/inhibitor.hpp index a5f300d..43cb6ca 100644 --- a/include/modules/inhibitor.hpp +++ b/include/modules/inhibitor.hpp @@ -12,12 +12,12 @@ namespace waybar::modules { class Inhibitor : public ALabel { public: Inhibitor(const std::string&, const waybar::Bar&, const Json::Value&); - ~Inhibitor() override; - auto update() -> void; + virtual ~Inhibitor(); + auto update() -> void override; auto activated() -> bool; private: - auto handleToggle(::GdkEventButton* const& e) -> bool; + auto handleToggle(::GdkEventButton* const& e) -> bool override; const std::unique_ptr<::GDBusConnection, void (*)(::GDBusConnection*)> dbus_; const std::string inhibitors_; diff --git a/include/modules/jack.hpp b/include/modules/jack.hpp index fbab062..452c43a 100644 --- a/include/modules/jack.hpp +++ b/include/modules/jack.hpp @@ -14,8 +14,8 @@ namespace waybar::modules { class JACK : public ALabel { public: JACK(const std::string &, const Json::Value &); - ~JACK() = default; - auto update() -> void; + virtual ~JACK() = default; + auto update() -> void override; int bufSize(jack_nframes_t size); int sampleRate(jack_nframes_t rate); diff --git a/include/modules/keyboard_state.hpp b/include/modules/keyboard_state.hpp index ce9faba..deb577e 100644 --- a/include/modules/keyboard_state.hpp +++ b/include/modules/keyboard_state.hpp @@ -19,8 +19,8 @@ namespace waybar::modules { class KeyboardState : public AModule { public: KeyboardState(const std::string&, const waybar::Bar&, const Json::Value&); - ~KeyboardState(); - auto update() -> void; + virtual ~KeyboardState(); + auto update() -> void override; private: auto tryAddDevice(const std::string&) -> void; diff --git a/include/modules/memory.hpp b/include/modules/memory.hpp index e23ed84..3b6342b 100644 --- a/include/modules/memory.hpp +++ b/include/modules/memory.hpp @@ -13,8 +13,8 @@ namespace waybar::modules { class Memory : public ALabel { public: Memory(const std::string&, const Json::Value&); - ~Memory() = default; - auto update() -> void; + virtual ~Memory() = default; + auto update() -> void override; private: void parseMeminfo(); diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index ae3f915..32d526e 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -37,7 +37,7 @@ class MPD : public ALabel { public: MPD(const std::string&, const Json::Value&); virtual ~MPD() noexcept = default; - auto update() -> void; + auto update() -> void override; private: std::string getTag(mpd_tag_type type, unsigned idx = 0) const; diff --git a/include/modules/mpris/mpris.hpp b/include/modules/mpris/mpris.hpp new file mode 100644 index 0000000..a0aee3b --- /dev/null +++ b/include/modules/mpris/mpris.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include + +#include "gtkmm/box.h" +#include "gtkmm/label.h" + +extern "C" { +#include +} + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules::mpris { + +class Mpris : public ALabel { + public: + Mpris(const std::string&, const Json::Value&); + virtual ~Mpris(); + auto update() -> void override; + bool handleToggle(GdkEventButton* const&) override; + + private: + static auto onPlayerNameAppeared(PlayerctlPlayerManager*, PlayerctlPlayerName*, gpointer) -> void; + static auto onPlayerNameVanished(PlayerctlPlayerManager*, PlayerctlPlayerName*, gpointer) -> void; + static auto onPlayerPlay(PlayerctlPlayer*, gpointer) -> void; + static auto onPlayerPause(PlayerctlPlayer*, gpointer) -> void; + static auto onPlayerStop(PlayerctlPlayer*, gpointer) -> void; + static auto onPlayerMetadata(PlayerctlPlayer*, GVariant*, gpointer) -> void; + + struct PlayerInfo { + std::string name; + PlayerctlPlaybackStatus status; + std::string status_string; + + std::optional artist; + std::optional album; + std::optional title; + std::optional length; // as HH:MM:SS + std::optional position; // same format + }; + + auto getPlayerInfo() -> std::optional; + auto getIconFromJson(const Json::Value&, const std::string&) -> std::string; + auto getArtistStr(const PlayerInfo&, bool) -> std::string; + auto getAlbumStr(const PlayerInfo&, bool) -> std::string; + auto getTitleStr(const PlayerInfo&, bool) -> std::string; + auto getLengthStr(const PlayerInfo&, bool) -> std::string; + auto getPositionStr(const PlayerInfo&, bool) -> std::string; + auto getDynamicStr(const PlayerInfo&, bool, bool) -> std::string; + + // config + std::string format_playing_; + std::string format_paused_; + std::string format_stopped_; + + std::string tooltip_; + std::string tooltip_playing_; + std::string tooltip_paused_; + std::string tooltip_stopped_; + + int artist_len_; + int album_len_; + int title_len_; + int dynamic_len_; + std::string dynamic_separator_; + std::vector dynamic_order_; + std::vector dynamic_prio_; + bool truncate_hours_; + bool tooltip_len_limits_; + std::string ellipsis_; + + std::string player_; + std::vector ignored_players_; + + PlayerctlPlayerManager* manager; + PlayerctlPlayer* player; + std::string lastStatus; + std::string lastPlayer; + + util::SleeperThread thread_; + std::chrono::time_point last_update_; +}; + +} // namespace waybar::modules::mpris diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 8ec6b6d..47701b4 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -21,8 +21,8 @@ namespace waybar::modules { class Network : public ALabel { public: Network(const std::string&, const Json::Value&); - ~Network(); - auto update() -> void; + virtual ~Network(); + auto update() -> void override; private: static const uint8_t MAX_RETRY = 5; @@ -62,6 +62,7 @@ class Network : public ALabel { bool want_link_dump_; bool want_addr_dump_; bool dump_in_progress_; + bool is_p2p_; unsigned long long bandwidth_down_total_; unsigned long long bandwidth_up_total_; @@ -77,7 +78,6 @@ class Network : public ALabel { int32_t signal_strength_dbm_; uint8_t signal_strength_; std::string signal_strength_app_; - float frequency_; uint32_t route_priority; util::SleeperThread thread_; @@ -85,6 +85,7 @@ class Network : public ALabel { #ifdef WANT_RFKILL util::Rfkill rfkill_; #endif + float frequency_; }; } // namespace waybar::modules diff --git a/include/modules/pulseaudio.hpp b/include/modules/pulseaudio.hpp index a8e4890..d0b17e4 100644 --- a/include/modules/pulseaudio.hpp +++ b/include/modules/pulseaudio.hpp @@ -14,8 +14,8 @@ namespace waybar::modules { class Pulseaudio : public ALabel { public: Pulseaudio(const std::string&, const Json::Value&); - ~Pulseaudio(); - auto update() -> void; + virtual ~Pulseaudio(); + auto update() -> void override; private: static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); @@ -25,7 +25,7 @@ class Pulseaudio : public ALabel { static void serverInfoCb(pa_context*, const pa_server_info*, void*); static void volumeModifyCb(pa_context*, int, void*); - bool handleScroll(GdkEventScroll* e); + bool handleScroll(GdkEventScroll* e) override; const std::vector getPulseIcon() const; pa_threaded_mainloop* mainloop_; diff --git a/include/modules/river/layout.hpp b/include/modules/river/layout.hpp new file mode 100644 index 0000000..dc78ee2 --- /dev/null +++ b/include/modules/river/layout.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "river-status-unstable-v1-client-protocol.h" + +namespace waybar::modules::river { + +class Layout : public waybar::ALabel { + public: + Layout(const std::string &, const waybar::Bar &, const Json::Value &); + virtual ~Layout(); + + // Handlers for wayland events + void handle_name(const char *name); + void handle_clear(); + void handle_focused_output(struct wl_output *output); + void handle_unfocused_output(struct wl_output *output); + + struct zriver_status_manager_v1 *status_manager_; + struct wl_seat *seat_; + + private: + const waybar::Bar &bar_; + struct wl_output *output_; // stores the output this module belongs to + struct wl_output *focused_output_; // stores the currently focused output + struct zriver_output_status_v1 *output_status_; + struct zriver_seat_status_v1 *seat_status_; +}; + +} /* namespace waybar::modules::river */ diff --git a/include/modules/river/mode.hpp b/include/modules/river/mode.hpp index 2aff495..246ceca 100644 --- a/include/modules/river/mode.hpp +++ b/include/modules/river/mode.hpp @@ -11,7 +11,7 @@ namespace waybar::modules::river { class Mode : public waybar::ALabel { public: Mode(const std::string &, const waybar::Bar &, const Json::Value &); - ~Mode(); + virtual ~Mode(); // Handlers for wayland events void handle_mode(const char *mode); diff --git a/include/modules/river/tags.hpp b/include/modules/river/tags.hpp index c2b1a11..fb3eefa 100644 --- a/include/modules/river/tags.hpp +++ b/include/modules/river/tags.hpp @@ -14,7 +14,7 @@ namespace waybar::modules::river { class Tags : public waybar::AModule { public: Tags(const std::string &, const waybar::Bar &, const Json::Value &); - ~Tags(); + virtual ~Tags(); // Handlers for wayland events void handle_focused_tags(uint32_t tags); diff --git a/include/modules/river/window.hpp b/include/modules/river/window.hpp index 1f6c525..bf29ebb 100644 --- a/include/modules/river/window.hpp +++ b/include/modules/river/window.hpp @@ -13,7 +13,7 @@ namespace waybar::modules::river { class Window : public waybar::ALabel { public: Window(const std::string &, const waybar::Bar &, const Json::Value &); - ~Window(); + virtual ~Window(); // Handlers for wayland events void handle_focused_view(const char *title); diff --git a/include/modules/simpleclock.hpp b/include/modules/simpleclock.hpp index 5cbee4c..43472ef 100644 --- a/include/modules/simpleclock.hpp +++ b/include/modules/simpleclock.hpp @@ -10,8 +10,8 @@ namespace waybar::modules { class Clock : public ALabel { public: Clock(const std::string&, const Json::Value&); - ~Clock() = default; - auto update() -> void; + virtual ~Clock() = default; + auto update() -> void override; private: util::SleeperThread thread_; diff --git a/include/modules/sndio.hpp b/include/modules/sndio.hpp index eb9b218..3fe36fa 100644 --- a/include/modules/sndio.hpp +++ b/include/modules/sndio.hpp @@ -12,12 +12,12 @@ namespace waybar::modules { class Sndio : public ALabel { public: Sndio(const std::string &, const Json::Value &); - ~Sndio(); - auto update() -> void; + virtual ~Sndio(); + auto update() -> void override; auto set_desc(struct sioctl_desc *, unsigned int) -> void; auto put_val(unsigned int, unsigned int) -> void; - bool handleScroll(GdkEventScroll *); - bool handleToggle(GdkEventButton *const &); + bool handleScroll(GdkEventScroll *) override; + bool handleToggle(GdkEventButton *const &) override; private: auto connect_to_sndio() -> void; diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index c1499b9..6cda35d 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -13,8 +13,8 @@ namespace waybar::modules::SNI { class Tray : public AModule { public: Tray(const std::string&, const Bar&, const Json::Value&); - ~Tray() = default; - auto update() -> void; + virtual ~Tray() = default; + auto update() -> void override; private: void onAdd(std::unique_ptr& item); diff --git a/include/modules/sway/ipc/client.hpp b/include/modules/sway/ipc/client.hpp index 77dab08..a9a3e4e 100644 --- a/include/modules/sway/ipc/client.hpp +++ b/include/modules/sway/ipc/client.hpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "ipc.hpp" #include "util/sleeper_thread.hpp" diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index f760276..3e9519f 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -17,8 +17,8 @@ namespace waybar::modules::sway { class Language : public ALabel, public sigc::trackable { public: Language(const std::string& id, const Json::Value& config); - ~Language() = default; - auto update() -> void; + virtual ~Language() = default; + auto update() -> void override; private: enum class DispayedShortFlag { None = 0, ShortName = 1, ShortDescription = 1 << 1 }; diff --git a/include/modules/sway/mode.hpp b/include/modules/sway/mode.hpp index 5543c4e..4458535 100644 --- a/include/modules/sway/mode.hpp +++ b/include/modules/sway/mode.hpp @@ -13,8 +13,8 @@ namespace waybar::modules::sway { class Mode : public ALabel, public sigc::trackable { public: Mode(const std::string&, const Json::Value&); - ~Mode() = default; - auto update() -> void; + virtual ~Mode() = default; + auto update() -> void override; private: void onEvent(const struct Ipc::ipc_response&); diff --git a/include/modules/sway/scratchpad.hpp b/include/modules/sway/scratchpad.hpp index e68e772..551cc8c 100644 --- a/include/modules/sway/scratchpad.hpp +++ b/include/modules/sway/scratchpad.hpp @@ -15,8 +15,8 @@ namespace waybar::modules::sway { class Scratchpad : public ALabel { public: Scratchpad(const std::string&, const Json::Value&); - ~Scratchpad() = default; - auto update() -> void; + virtual ~Scratchpad() = default; + auto update() -> void override; private: auto getTree() -> void; @@ -32,4 +32,4 @@ class Scratchpad : public ALabel { Ipc ipc_; util::JsonParser parser_; }; -} // namespace waybar::modules::sway \ No newline at end of file +} // namespace waybar::modules::sway diff --git a/include/modules/sway/window.hpp b/include/modules/sway/window.hpp index c13d5ce..22f5a59 100644 --- a/include/modules/sway/window.hpp +++ b/include/modules/sway/window.hpp @@ -15,14 +15,15 @@ namespace waybar::modules::sway { class Window : public AIconLabel, public sigc::trackable { public: Window(const std::string&, const waybar::Bar&, const Json::Value&); - ~Window() = default; - auto update() -> void; + virtual ~Window() = default; + auto update() -> void override; private: + void setClass(std::string classname, bool enable); void onEvent(const struct Ipc::ipc_response&); void onCmd(const struct Ipc::ipc_response&); - std::tuple getFocusedNode( - const Json::Value& nodes, std::string& output); + std::tuple + getFocusedNode(const Json::Value& nodes, std::string& output); void getTree(); void updateAppIconName(); void updateAppIcon(); @@ -32,12 +33,14 @@ class Window : public AIconLabel, public sigc::trackable { int windowId_; std::string app_id_; std::string app_class_; + std::string layout_; std::string old_app_id_; std::size_t app_nb_; std::string shell_; unsigned app_icon_size_{24}; bool update_app_icon_{true}; std::string app_icon_name_; + int floating_count_; util::JsonParser parser_; std::mutex mutex_; Ipc ipc_; diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index e6df067..d07edb4 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "AModule.hpp" @@ -17,11 +18,13 @@ namespace waybar::modules::sway { class Workspaces : public AModule, public sigc::trackable { public: Workspaces(const std::string&, const waybar::Bar&, const Json::Value&); - ~Workspaces() = default; - auto update() -> void; + virtual ~Workspaces() = default; + auto update() -> void override; private: - static inline const std::string workspace_switch_cmd_ = "workspace {} \"{}\""; + static constexpr std::string_view workspace_switch_cmd_ = "workspace {} \"{}\""; + static constexpr std::string_view persistent_workspace_switch_cmd_ = + R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; static int convertWorkspaceNameToNum(std::string name); @@ -34,7 +37,7 @@ class Workspaces : public AModule, public sigc::trackable { const std::string getCycleWorkspace(std::vector::iterator, bool prev) const; uint16_t getWorkspaceIndex(const std::string& name) const; std::string trimWorkspaceName(std::string); - bool handleScroll(GdkEventScroll*); + bool handleScroll(GdkEventScroll*) override; const Bar& bar_; std::vector workspaces_; diff --git a/include/modules/temperature.hpp b/include/modules/temperature.hpp index 04caafc..5440df7 100644 --- a/include/modules/temperature.hpp +++ b/include/modules/temperature.hpp @@ -12,8 +12,8 @@ namespace waybar::modules { class Temperature : public ALabel { public: Temperature(const std::string&, const Json::Value&); - ~Temperature() = default; - auto update() -> void; + virtual ~Temperature() = default; + auto update() -> void override; private: float getTemperature(); diff --git a/include/modules/upower/upower.hpp b/include/modules/upower/upower.hpp index 2724443..446d1f5 100644 --- a/include/modules/upower/upower.hpp +++ b/include/modules/upower/upower.hpp @@ -19,8 +19,8 @@ namespace waybar::modules::upower { class UPower : public AModule { public: UPower(const std::string &, const Json::Value &); - ~UPower(); - auto update() -> void; + virtual ~UPower(); + auto update() -> void override; private: typedef std::unordered_map Devices; @@ -45,7 +45,7 @@ class UPower : public AModule { void resetDevices(); void removeDevices(); bool show_tooltip_callback(int, int, bool, const Glib::RefPtr &tooltip); - bool handleToggle(GdkEventButton *const &); + bool handleToggle(GdkEventButton *const &) override; std::string timeToString(gint64 time); const std::string getDeviceStatus(UpDeviceState &state); @@ -74,6 +74,7 @@ class UPower : public AModule { bool showAltText; 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 index 730ee93..05e9dcb 100644 --- a/include/modules/upower/upower_tooltip.hpp +++ b/include/modules/upower/upower_tooltip.hpp @@ -24,7 +24,7 @@ class UPowerTooltip : public Gtk::Window { public: UPowerTooltip(uint iconSize, uint tooltipSpacing, uint tooltipPadding); - ~UPowerTooltip(); + virtual ~UPowerTooltip(); uint updateTooltip(Devices& devices); }; diff --git a/include/modules/user.hpp b/include/modules/user.hpp index ac14d25..bcb03da 100644 --- a/include/modules/user.hpp +++ b/include/modules/user.hpp @@ -11,8 +11,8 @@ namespace waybar::modules { class User : public AIconLabel { public: User(const std::string&, const Json::Value&); - ~User() = default; - auto update() -> void; + virtual ~User() = default; + auto update() -> void override; bool handleToggle(GdkEventButton* const& e) override; diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index c0ee7f0..9bbf4d4 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -13,25 +13,32 @@ namespace waybar::modules { class Wireplumber : public ALabel { public: Wireplumber(const std::string&, const Json::Value&); - ~Wireplumber(); - auto update() -> void; + virtual ~Wireplumber(); + auto update() -> void override; private: void loadRequiredApiModules(); void prepare(); void activatePlugins(); - static void updateVolume(waybar::modules::Wireplumber* self); - static void updateNodeName(waybar::modules::Wireplumber* self); - static uint32_t getDefaultNodeId(waybar::modules::Wireplumber* self); + 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 onObjectManagerInstalled(waybar::modules::Wireplumber* self); + static void onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id); + static void onDefaultNodesApiChanged(waybar::modules::Wireplumber* self); + + bool handleScroll(GdkEventScroll* e) override; WpCore* wp_core_; GPtrArray* apis_; WpObjectManager* om_; + WpPlugin* mixer_api_; + WpPlugin* def_nodes_api_; + gchar* default_node_name_; uint32_t pending_plugins_; bool muted_; double volume_; + double min_step_; uint32_t node_id_{0}; std::string node_name_; }; diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 7a1f17a..4465dd0 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -40,6 +40,8 @@ class Task { FULLSCREEN = (1 << 3), INVALID = (1 << 4) }; + // made public so TaskBar can reorder based on configuration. + Gtk::Button button; private: static uint32_t global_id; @@ -53,7 +55,6 @@ class Task { uint32_t id_; - Gtk::Button button_; Gtk::Box content_; Gtk::Image icon_; Gtk::Label text_before_; diff --git a/include/modules/wlr/workspace_manager.hpp b/include/modules/wlr/workspace_manager.hpp index 963d610..f7cc759 100644 --- a/include/modules/wlr/workspace_manager.hpp +++ b/include/modules/wlr/workspace_manager.hpp @@ -22,7 +22,7 @@ class WorkspaceGroup; class Workspace { public: Workspace(const waybar::Bar &bar, const Json::Value &config, WorkspaceGroup &workspace_group, - zext_workspace_handle_v1 *workspace, uint32_t id); + zext_workspace_handle_v1 *workspace, uint32_t id, std::string name); ~Workspace(); auto update() -> void; @@ -30,11 +30,15 @@ class Workspace { auto is_active() const -> bool { return state_ & static_cast(State::ACTIVE); } auto is_urgent() const -> bool { return state_ & static_cast(State::URGENT); } auto is_hidden() const -> bool { return state_ & static_cast(State::HIDDEN); } + auto is_empty() const -> bool { return state_ & static_cast(State::EMPTY); } + auto is_persistent() const -> bool { return persistent_; } // wlr stuff auto handle_name(const std::string &name) -> void; auto handle_coordinates(const std::vector &coordinates) -> void; auto handle_state(const std::vector &state) -> void; auto handle_remove() -> void; + auto make_persistent() -> void; + auto handle_duplicate() -> void; auto handle_done() -> void; auto handle_clicked(GdkEventButton *bt) -> bool; @@ -48,6 +52,7 @@ class Workspace { ACTIVE = (1 << 0), URGENT = (1 << 1), HIDDEN = (1 << 2), + EMPTY = (1 << 3), }; private: @@ -67,6 +72,7 @@ class Workspace { static std::map icons_map_; std::string format_; bool with_icon_ = false; + bool persistent_ = false; Gtk::Button button_; Gtk::Box content_; @@ -87,11 +93,14 @@ class WorkspaceGroup { auto active_only() const -> bool; auto creation_delayed() const -> bool; auto workspaces() -> std::vector> & { return workspaces_; } + auto persistent_workspaces() -> std::vector & { return persistent_workspaces_; } auto sort_workspaces() -> void; auto set_need_to_sort() -> void { need_to_sort = true; } auto add_button(Gtk::Button &button) -> void; auto remove_button(Gtk::Button &button) -> void; + auto fill_persistent_workspaces() -> void; + auto create_persistent_workspaces() -> void; // wlr stuff auto handle_workspace_create(zext_workspace_handle_v1 *workspace_handle) -> void; @@ -115,6 +124,8 @@ class WorkspaceGroup { uint32_t id_; std::vector> workspaces_; bool need_to_sort = false; + std::vector persistent_workspaces_; + bool persistent_created_ = false; }; class WorkspaceManager : public AModule { diff --git a/include/util/clara.hpp b/include/util/clara.hpp index 4cc6f48..73fa541 100644 --- a/include/util/clara.hpp +++ b/include/util/clara.hpp @@ -989,7 +989,7 @@ struct Help : Opt { showHelpFlag = flag; return ParserResult::ok(ParseResultType::ShortCircuitAll); }) { - static_cast (*this)("display usage information")["-?"]["-h"]["--help"].optional(); + static_cast(*this)("display usage information")["-?"]["-h"]["--help"].optional(); } }; diff --git a/include/util/command.hpp b/include/util/command.hpp index c9f238c..0d729b7 100644 --- a/include/util/command.hpp +++ b/include/util/command.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -68,7 +69,11 @@ inline int close(FILE* fp, pid_t pid) { inline FILE* open(const std::string& cmd, int& pid) { if (cmd == "") return nullptr; int fd[2]; - if (pipe(fd) != 0) { + // Open the pipe with the close-on-exec flag set, so it will not be inherited + // by any other subprocesses launched by other threads (which could result in + // the pipe staying open after this child dies, causing us to hang when trying + // to read from it) + if (pipe2(fd, O_CLOEXEC) != 0) { spdlog::error("Unable to pipe fd"); return nullptr; } @@ -77,6 +82,8 @@ inline FILE* open(const std::string& cmd, int& pid) { if (child_pid < 0) { spdlog::error("Unable to exec cmd {}, error {}", cmd.c_str(), strerror(errno)); + ::close(fd[0]); + ::close(fd[1]); return nullptr; } diff --git a/include/util/date.hpp b/include/util/date.hpp new file mode 100644 index 0000000..380bb6e --- /dev/null +++ b/include/util/date.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include + +#if HAVE_CHRONO_TIMEZONES +#include +#include + +/* Compatibility layer for on top of C++20 */ +namespace date { + +using namespace std::chrono; + +namespace literals { +using std::chrono::last; +} + +inline auto format(const std::string& spec, const auto& ztime) { + return spec.empty() ? "" : std::vformat("{:L" + spec + "}", std::make_format_args(ztime)); +} + +inline auto format(const std::locale& loc, const std::string& spec, const auto& ztime) { + return spec.empty() ? "" : std::vformat(loc, "{:L" + spec + "}", std::make_format_args(ztime)); +} + +} // namespace date + +#else +#include +#endif + +template +struct fmt::formatter> { + std::string_view specs; + + template + constexpr auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { + auto it = ctx.begin(); + if (it != ctx.end() && *it == ':') { + ++it; + } + auto end = it; + while (end != ctx.end() && *end != '}') { + ++end; + } + if (end != it) { + specs = {it, std::string_view::size_type(end - it)}; + } + return end; + } + + template + auto format(const date::zoned_time& ztime, FormatContext& ctx) { + if (ctx.locale()) { + const auto loc = ctx.locale().template get(); + return fmt::format_to(ctx.out(), "{}", date::format(loc, fmt::to_string(specs), ztime)); + } + return fmt::format_to(ctx.out(), "{}", date::format(fmt::to_string(specs), ztime)); + } +}; diff --git a/include/util/format.hpp b/include/util/format.hpp index fac0377..00b6a31 100644 --- a/include/util/format.hpp +++ b/include/util/format.hpp @@ -66,9 +66,9 @@ struct formatter { std::string string; switch (spec) { case '>': - return format_to(ctx.out(), "{:>{}}", fmt::format("{}", s), max_width); + return fmt::format_to(ctx.out(), "{:>{}}", fmt::format("{}", s), max_width); case '<': - return format_to(ctx.out(), "{:<{}}", fmt::format("{}", s), max_width); + return fmt::format_to(ctx.out(), "{:<{}}", fmt::format("{}", s), max_width); case '=': format = "{coefficient:<{number_width}.1f}{padding}{prefix}{unit}"; break; @@ -77,8 +77,8 @@ struct formatter { format = "{coefficient:.1f}{prefix}{unit}"; break; } - return format_to( - ctx.out(), format, fmt::arg("coefficient", fraction), + return fmt::format_to( + ctx.out(), fmt::runtime(format), fmt::arg("coefficient", fraction), fmt::arg("number_width", number_width), fmt::arg("prefix", std::string() + units[pow] + ((s.binary_ && pow) ? "i" : "")), fmt::arg("unit", s.unit_), diff --git a/include/util/gtk_icon.hpp b/include/util/gtk_icon.hpp new file mode 100644 index 0000000..44555f6 --- /dev/null +++ b/include/util/gtk_icon.hpp @@ -0,0 +1,14 @@ +#pragma once +#include + +#include +#include + +class DefaultGtkIconThemeWrapper { + private: + static std::mutex default_theme_mutex; + + public: + static bool has_icon(const std::string&); + static Glib::RefPtr load_icon(const char*, int, Gtk::IconLookupFlags); +}; diff --git a/include/util/prepare_for_sleep.h b/include/util/prepare_for_sleep.h new file mode 100644 index 0000000..68db8d8 --- /dev/null +++ b/include/util/prepare_for_sleep.h @@ -0,0 +1,9 @@ +#pragma once + +#include "SafeSignal.hpp" + +namespace waybar::util { + +// Get a signal emited with value true when entering sleep, and false when exiting +SafeSignal& prepare_for_sleep(); +} // namespace waybar::util diff --git a/include/util/rewrite_title.hpp b/include/util/rewrite_string.hpp similarity index 55% rename from include/util/rewrite_title.hpp rename to include/util/rewrite_string.hpp index c477339..2ab39ad 100644 --- a/include/util/rewrite_title.hpp +++ b/include/util/rewrite_string.hpp @@ -4,5 +4,5 @@ #include namespace waybar::util { -std::string rewriteTitle(const std::string&, const Json::Value&); +std::string rewriteString(const std::string&, const Json::Value&); } diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index e12287a..a724b1e 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -6,6 +6,8 @@ #include #include +#include "prepare_for_sleep.h" + namespace waybar::util { /** @@ -33,7 +35,11 @@ class SleeperThread { signal_ = false; func(); } - }} {} + }} { + connection_ = prepare_for_sleep().connect([this](bool sleep) { + if (not sleep) wake_up(); + }); + } SleeperThread& operator=(std::function func) { thread_ = std::thread([this, func] { @@ -42,6 +48,11 @@ class SleeperThread { func(); } }); + if (connection_.empty()) { + connection_ = prepare_for_sleep().connect([this](bool sleep) { + if (not sleep) wake_up(); + }); + } return *this; } @@ -61,7 +72,7 @@ class SleeperThread { return condvar_.wait_until(lk, time_point, [this] { return signal_ || !do_run_; }); } - auto wake_up() { + void wake_up() { { std::lock_guard lck(mutex_); signal_ = true; @@ -96,6 +107,7 @@ class SleeperThread { std::mutex mutex_; bool do_run_ = true; bool signal_ = false; + sigc::connection connection_; }; } // namespace waybar::util diff --git a/include/util/waybar_time.hpp b/include/util/waybar_time.hpp deleted file mode 100644 index b9f9ea9..0000000 --- a/include/util/waybar_time.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include -#include - -namespace waybar { - -struct waybar_time { - std::locale locale; - date::zoned_seconds ztime; -}; - -} // namespace waybar - -template <> -struct fmt::formatter { - std::string_view specs; - - template - constexpr auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { - auto it = ctx.begin(); - if (it != ctx.end() && *it == ':') { - ++it; - } - auto end = it; - while (end != ctx.end() && *end != '}') { - ++end; - } - if (end != it) { - specs = {it, std::string_view::size_type(end - it)}; - } - return end; - } - - template - auto format(const waybar::waybar_time& t, FormatContext& ctx) { - return format_to(ctx.out(), "{}", date::format(t.locale, fmt::to_string(specs), t.ztime)); - } -}; diff --git a/man/waybar-backlight.5.scd b/man/waybar-backlight.5.scd index 9c8ba79..ca3d922 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -58,16 +58,25 @@ The *backlight* module displays the current backlight level. *on-scroll-up*: ++ typeof: string ++ - Command to execute when performing a scroll up on the module. + Command to execute when performing a scroll up on the module. This replaces the default behaviour of brightness control. *on-scroll-down*: ++ typeof: string - Command to execute when performing a scroll down on the module. + Command to execute when performing a scroll down on the module. This replaces the default behaviour of brightness control. *smooth-scrolling-threshold*: ++ typeof: double Threshold to be used when scrolling. +*reverse-scrolling*: ++ + typeof: bool ++ + Option to reverse the scroll direction. + +*scroll-step*: ++ + typeof: float ++ + default: 1.0 ++ + The speed in which to change the brightness when scrolling. + # EXAMPLE: ``` diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index fb470e0..3c67056 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -1,105 +1,167 @@ -waybar-clock(5) +waybar-clock(5) "waybar-clock" "User Manual" # NAME -waybar - clock module +clock # DESCRIPTION -The *clock* module displays the current date and time. +*clock* module displays current date and time + +# FILES + +$XDG_CONFIG_HOME/waybar/config ++ + Per user configuration file # CONFIGURATION -*interval*: ++ - typeof: integer ++ - default: 60 ++ - The interval in which the information gets polled. +1. Addressed by *clock* +[- *Option* +:- *Typeof* +:- *Default* +:- *Description* +|[ *interval* +:[ integer +:[ 60 +:[ The interval in which the information gets polled +|[ *format* +:[ string +:[ *{:%H:%M}* +:[ The format, how the date and time should be displayed. See format options below +|[ *timezone* +:[ string +:[ +:[ The timezone to display the time in, e.g. America/New_York. "" represents + the system's local timezone. See Wikipedia's unofficial list of timezones +|[ *timezones* +:[ list of strings +:[ +:[ A list of timezones (as in *timezone*) to use for time display, changed using + the scroll wheel. Do not specify *timezone* option when *timezones* is specified. + "" represents the system's local timezone +|[ *locale* +:[ string +:[ +:[ A locale to be used to display the time. Intended to render times in custom + timezones with the proper language and format +|[ *max-length* +:[ integer +:[ +:[ The maximum length in character the module should display +|[ *rotate* +:[ integer +:[ +:[ Positive value to rotate the text label +|[ *on-click* +:[ string +:[ +:[ Command to execute when clicked on the module +|[ *on-click-middle* +:[ string +:[ +:[ Command to execute when you middle clicked on the module using mousewheel +|[ *on-click-right* +:[ string +:[ +:[ Command to execute when you right clicked on the module +|[ *on-scroll-up* +:[ string +:[ +:[ Command to execute when scrolling up on the module +|[ *on-scroll-down* +:[ string +:[ +:[ Command to execute when scrolling down on the module +|[ *smooth-scrolling-threshold* +:[ double +:[ +:[ Threshold to be used when scrolling +|[ *tooltip* +:[ bool +:[ true +:[ Option to enable tooltip on hover +|[ *tooltip-format* +:[ string +:[ same as format +:[ Tooltip on hover -*format*: ++ - typeof: string ++ - default: {:%H:%M} ++ - The format, how the date and time should be displayed. ++ - It uses the format of the date library. See https://howardhinnant.github.io/date/date.html#to_stream_formatting for details. +View all valid format options in *strftime(3)* or have a look -*timezone*: ++ - typeof: string ++ - default: inferred local timezone ++ - The timezone to display the time in, e.g. America/New_York. ++ - This field will be ignored if *timezones* field is set and have at least one value. +2. Addressed by *clock: calendar* +[- *Option* +:- *Typeof* +:- *Default* +:- *Description* +|[ *mode* +:[ string +:[ month +:[ Calendar view mode. Possible values: year|month +|[ *mode-mon-col* +:[ integer +:[ 3 +:[ Relevant for *mode=year*. Count of months per row +|[ *weeks-pos* +:[ integer +:[ +:[ The position where week numbers should be displayed. Disabled when is empty. + Possible values: left|right +|[ *on-scroll* +:[ integer +:[ 1 +:[ Value to scroll months/years forward/backward. Can be negative. Is + configured under *on-scroll* option -*timezones*: ++ - typeof: list of strings ++ - A list of timezones to use for time display, changed using the scroll wheel. ++ - Use "" to represent the system's local timezone. Using %Z in the format or tooltip format is useful to track which time zone is currently displayed. +3. Adressed by *clock: calendar: format* +[- *Option* +:- *Typeof* +:- *Default* +:- *Description* +|[ *months* +:[ string +:[ +:[ Format is applied to months header(January, February,...etc.) +|[ *days* +:[ string +:[ +:[ Format is applied to days +|[ *weeks* +:[ string +:[ *{:%U}* +:[ Format is applied to week numbers. When weekday format is not provided then + is used default format: '{:%W}' when week starts with Monday, '{:%U}' otherwise +|[ *weekdays* +:[ string +:[ +:[ Format is applied to weeks header(Su,Mo,...etc.) +|[ *today* +:[ string +:[ *{}* +:[ Format is applied to Today -*locale*: ++ - typeof: string ++ - default: inferred from current locale ++ - A locale to be used to display the time. Intended to render times in custom timezones with the proper language and format. +## Actions -*today-format*: ++ - typeof: string ++ - default: {} ++ - The format of today's date in the calendar. - -*max-length*: ++ - typeof: integer ++ - The maximum length in character the module should display. - -*min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. - -*align*: ++ - typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. - -*rotate*: ++ - typeof: integer ++ - Positive value to rotate the text label. - -*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 clicked on the module. - -*on-update*: ++ - typeof: string ++ - Command to execute when the module is updated. - -*on-scroll-up*: ++ - typeof: string ++ - Command to execute when scrolling up on the module. - -*on-scroll-down*: ++ - typeof: string ++ - Command to execute when scrolling down on the module. - -*smooth-scrolling-threshold*: ++ - typeof: double ++ - Threshold to be used when scrolling. - -*tooltip*: ++ - typeof: bool ++ - default: true ++ - Option to disable tooltip on hover. - -View all valid format options in *strftime(3)*. +[- *String* +:- *Action* +|[ *mode* +:[ Switch calendar mode between year/month +|[ *tz_up* +:[ Switch to the next provided time zone +|[ *tz_down* +:[ Switch to the previous provided time zone +|[ *shift_up* +:[ Switch to the next calendar month/year +|[ *shift_down* +:[ Switch to the previous calendar month/year # FORMAT REPLACEMENTS -*{calendar}*: Current month calendar -*{timezoned_time_list}*: List of time in the rest timezones, if more than one timezone is set in the config +- *{calendar}*: Current month calendar +- *{timezoned_time_list}*: List of time in the rest timezones, if more than one timezone is set in the config # EXAMPLES +1. General + ``` "clock": { "interval": 60, @@ -108,6 +170,101 @@ View all valid format options in *strftime(3)*. } ``` +2. Calendar + +``` +"clock": { + "format": "{:%H:%M}  ", + "format-alt": "{:%A, %B %d, %Y (%R)}  ", + "tooltip-format": "{calendar}", + "calendar": { + "mode" : "year", + "mode-mon-col" : 3, + "weeks-pos" : "right", + "on-scroll" : 1, + "on-click-right": "mode", + "format": { + "months": "{}", + "days": "{}", + "weeks": "W{}", + "weekdays": "{}", + "today": "{}" + } + }, + "actions": { + "on-click-right": "mode", + "on-click-forward": "tz_up", + "on-click-backward": "tz_down", + "on-scroll-up": "shift_up", + "on-scroll-down": "shift_down" + } +}, +``` + +3. Full date on hover + +``` +"clock": { + "interval": 60, + "tooltip": true, + "format": "{:%H.%M}", + "tooltip-format": "{:%Y-%m-%d}", +} +``` + # STYLE - *#clock* + +# Troubleshooting + +If clock module is disabled at startup with locale::facet::\_S\_create\_c\_locale ++ +name not valid error message try one of the followings: + +- check if LC_TIME is set properly (glibc) +- set locale to C in the config file (musl) + +The locale option must be set for {calendar} to use the correct start-of-week, regardless of system locale. + +## Calendar in Chinese. Alignment + +In order to have aligned Chinese calendar there are some useful recommendations: + +. Use "WenQuanYi Zen Hei Mono" which is provided in most Linux distributions +. Try different font sizes and find best for you. size = 9pt should be fine +. In case when "WenQuanYi Zen Hei Mono" font is used disable monospace font pango tag + +Example of working config + +``` +"clock": { + "format": "{:%H:%M}  ", + "format-alt": "{:%A, %B %d, %Y (%R)}  ", + "tooltip-format": "\n{calendar}", + "calendar": { + "mode" : "year", + "mode-mon-col" : 3, + "weeks-pos" : "right", + "on-scroll" : 1, + "on-click-right": "mode", + "format": { + "months": "{}", + "days": "{}", + "weeks": "W{}", + "weekdays": "{}", + "today": "{}" + } + }, + "actions": { + "on-click-right": "mode", + "on-click-forward": "tz_up", + "on-click-backward": "tz_down", + "on-scroll-up": "shift_up", + "on-scroll-down": "shift_down" + } + }, +``` + +# AUTHOR + +Alexis Rouillard diff --git a/man/waybar-dwl-tags.5.scd b/man/waybar-dwl-tags.5.scd new file mode 100644 index 0000000..06fb577 --- /dev/null +++ b/man/waybar-dwl-tags.5.scd @@ -0,0 +1,49 @@ +waybar-dwl-tags(5) + +# NAME + +waybar - dwl tags module + +# DESCRIPTION + +The *tags* module displays the current state of tags in dwl. + +# CONFIGURATION + +Addressed by *dwl/tags* + +*num-tags*: ++ + typeof: uint ++ + default: 9 ++ + The number of tags that should be displayed. Max 32. + +*tag-labels*: ++ + typeof: array ++ + The label to display for each tag. + +*disable-click*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can left click to set focused tag. Right click to toggle tag focus. If set to true this behaviour is disabled. + +# EXAMPLE + +``` +"dwl/tags": { + "num-tags": 5 +} +``` + +# STYLE + +- *#tags button* +- *#tags button.occupied* +- *#tags button.focused* +- *#tags button.urgent* + +Note that occupied/focused/urgent status may overlap. That is, a tag may be +both occupied and focused at the same time. + +# SEE ALSO + +waybar(5), dwl(1) diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index cb16995..3e92def 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -15,7 +15,7 @@ Addressed by *hyprland/language* *format*: ++ typeof: string ++ default: {} ++ - The format, how information should be displayed. On {} the currently selected language is displayed. + The format, how information should be displayed. *format-* ++ typeof: string++ @@ -23,18 +23,28 @@ Addressed by *hyprland/language* *keyboard-name*: ++ typeof: string ++ - Specifies which keyboard to use from hyprctl devices output. Using the option that begins with "AT Translated set..." is recommended. + Specifies which keyboard to use from hyprctl devices output. Using the option that begins with "at-translated-set..." is recommended. +# FORMAT REPLACEMENTS + +*{short}*: Short name of layout (e.g. "us"). Equals to {}. + +*{shortDescription}*: Short description of layout (e.g. "en"). + +*{long}*: Long name of layout (e.g. "English (Dvorak)"). + +*{variant}*: Variant of layout (e.g. "dvorak"). + # EXAMPLES ``` "hyprland/language": { - "format": "Lang: {}" - "format-us": "AMERICA, HELL YEAH!" // For American English - "format-tr": "As bayrakları" // For Turkish - "keyboard-name": "AT Translated Set 2 keyboard" + "format": "Lang: {long}" + "format-en": "AMERICA, HELL YEAH!" + "format-tr": "As bayrakları" + "keyboard-name": "at-translated-set-2-keyboard" } ``` diff --git a/man/waybar-hyprland-submap.5.scd b/man/waybar-hyprland-submap.5.scd new file mode 100644 index 0000000..a00a276 --- /dev/null +++ b/man/waybar-hyprland-submap.5.scd @@ -0,0 +1,82 @@ +waybar-hyprland-submap(5) + +# NAME + +waybar - hyprland submap module + +# DESCRIPTION + +The *submap* module displays the currently active submap similar to *sway/mode*. + +# CONFIGURATION + +Addressed by *hyprland/submap* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format, how information should be displayed. On {} the currently active submap is 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 take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right clicked on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + + +# EXAMPLES + +``` +"hyprland/submap": { + "format": "✌️ {}", + "max-length": 8, + "tooltip": false +} +``` + +# STYLE + +- *#submap* diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd new file mode 100644 index 0000000..0678fb2 --- /dev/null +++ b/man/waybar-hyprland-workspaces.5.scd @@ -0,0 +1,59 @@ +waybar-wlr-workspaces(5) + +# NAME + +waybar - hyprland workspaces module + +# DESCRIPTION + +The *workspaces* module displays the currently used workspaces in hyprland compositor. + +# CONFIGURATION + +Addressed by *hyprland/workspaces* + +*format*: ++ + typeof: string ++ + default: {id} ++ + The format, how information should be displayed. + +*format-icons*: ++ + typeof: array ++ + Based on the workspace id and state, the corresponding icon gets selected. See *icons*. + +# FORMAT REPLACEMENTS + +*{id}*: id of workspace assigned by compositor + +*{icon}*: Icon, as defined in *format-icons*. + +# ICONS + +Additional to workspace name matching, the following *format-icons* can be set. + +- *default*: Will be shown, when no string match is found. +- *active*: Will be shown, when workspace is active + +# EXAMPLES + +``` +"wlr/workspaces": { + "format": "{name}: {icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "active": "", + "default": "" + }, + "sort-by-number": true +} +``` + +# Style + +- *#workspaces* +- *#workspaces button* +- *#workspaces button.active* diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index feff9f6..d47dba3 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -1,4 +1,4 @@ -waybar-custom(5) +waybar-image(5) # NAME @@ -10,12 +10,13 @@ The *image* module displays an image from a path. # CONFIGURATION -Addressed by *custom/* - *path*: ++ typeof: string ++ The path to the image. - +*exec*: ++ + typeof: string ++ + The path to the script, which should return image path file + it will only execute if the path is not set *size*: ++ typeof: integer ++ The width/height to render the image. @@ -56,17 +57,31 @@ Addressed by *custom/* typeof: double ++ Threshold to be used when scrolling. -# EXAMPLES +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to enable tooltip on hover. -## Spotify: +# SCRIPT OUTPUT -## mpd: +Similar to the *custom* module, output values of the script is *newline* separated. +The following is the output format: ``` -"image/album-art": { +$path\\n$tooltip +``` + +# EXAMPLES + +``` +"image#album-art": { "path": "/tmp/mpd_art", "size": 32, "interval": 5, "on-click": "mpc toggle" } -``` \ No newline at end of file +``` + +# STYLE + +- *#image* diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index 044af98..1dde8f7 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -74,20 +74,20 @@ Addressed by *mpd* Tooltip information displayed when the MPD server can't be reached. *artist-len*: ++ - typeof: integer ++ - Maximum length of the Artist tag. + typeof: integer ++ + Maximum length of the Artist tag. *album-len*: ++ - typeof: integer ++ - Maximum length of the Album tag. + typeof: integer ++ + Maximum length of the Album tag. *album-artist-len*: ++ - typeof: integer ++ - Maximum length of the Album Artist tag. + typeof: integer ++ + Maximum length of the Album Artist tag. *title-len*: ++ - typeof: integer ++ - Maximum length of the Title tag. + typeof: integer ++ + Maximum length of the Title tag. *rotate*: ++ typeof: integer ++ @@ -98,12 +98,12 @@ Addressed by *mpd* The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should take up. *align*: ++ - typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. *on-click*: ++ typeof: string ++ diff --git a/man/waybar-mpris.5.scd b/man/waybar-mpris.5.scd new file mode 100644 index 0000000..ad5c1df --- /dev/null +++ b/man/waybar-mpris.5.scd @@ -0,0 +1,197 @@ +waybar-mpris(5) + +# NAME + +waybar - MPRIS module + +# DESCRIPTION + +The *mpris* module displays currently playing media via libplayerctl. + +# CONFIGURATION + +*player*: ++ + typeof: string ++ + default: playerctld ++ + Name of the MPRIS player to attach to. Using the default value always + follows the currenly active player. + +*ignored-players*: ++ + typeof: []string ++ + Ignore updates of the listed players, when using playerctld. + +*interval*: ++ + typeof: integer ++ + Refresh MPRIS information on a timer. + +*format*: ++ + typeof: string ++ + default: {player} ({status}) {dynamic} ++ + The text format. + +*format-[status]*: ++ + typeof: string ++ + The status-specific text format. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: {player} ({status}) {dynamic} ++ + The tooltip text format. + +*tooltip-format-[status]*: ++ + typeof: string ++ + The status-specific tooltip format. + +*artist-len*: ++ + typeof: integer ++ + Maximum length of the Artist tag (Wide/Fullwidth Unicode characters + count as two). Set to zero to hide the artist in `{dynamic}` tag. + +*album-len*: ++ + typeof: integer ++ + Maximum length of the Album tag (Wide/Fullwidth Unicode characters count + as two). Set to zero to hide the album in `{dynamic}` tag. + +*title-len*: ++ + typeof: integer ++ + Maximum length of the Title tag (Wide/Fullwidth Unicode characters count + as two). Set to zero to hide the title in `{dynamic}` tag. + +*dynamic-len*: ++ + typeof: integer ++ + Maximum length of the Dynamic tag (Wide/Fullwidth Unicode characters ++ + count as two). The dynamic tag will not truncate any tags beyond their ++ + set length limits, instead, it will attempt to fit as much of the ++ + available tags as possible. It is recommended you set title-len to ++ + something less than or equal to this value, so the title will always be ++ + displayed. + +*dynamic-order*: ++ + typeof: []string ++ + default: ["title", "artist", "album", "position", "length"] ++ + Order of the tags shown by Dynamic tag. The position and length tags ++ + will always be combined in the format [{position}/{length}]. The order ++ + of these tags in relation to other tags will be determined based on the ++ + declaration of the first among the two tags. Absence in this list means ++ + force exclusion. + +*dynamic-importance-order*: ++ + typeof: []string ++ + default: ["title", "artist", "album", "position", "length"] ++ + Priority of the tags when truncating the Dynamic tag. The final ones ++ + will be the first to be truncated. Absence in this list means force ++ + inclusion. + +*dynamic-separator*: ++ + typeof: string ++ + default: " - " ++ + These characters will be used to separate two different tags, except ++ + when one of these tags is position and length. + +*truncate-hours*: ++ + typeof: bool ++ + default: true ++ + Whether to hide hours when media duration is less than an hour long. + +*enable-tooltip-len-limits*: ++ + typeof: bool ++ + default: false ++ + Option to enable the length limits for the tooltip as well. By default + the tooltip ignores all length limits. + +*ellipsis*: ++ + typeof: string ++ + default: "…" ++ + This character will be used when any of the tags exceed their maximum + length. If you don't want to use an ellipsis, set this to empty string. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. + If the module is rotated, it will follow the flow of the text. + +*on-click*: ++ + typeof: string ++ + default: play-pause ++ + Overwrite default action toggles. + +*on-middle-click*: ++ + typeof: string ++ + default: previous track ++ + Overwrite default action toggles. + +*on-right-click*: ++ + typeof: string ++ + default: next track ++ + Overwrite default action toggles. + +*player-icons*: ++ + typeof: map[string]string ++ + 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). + + +# FORMAT REPLACEMENTS + +*{player}*: The name of the current media player + +*{status}*: The current status (playing, paused, stopped) + +*{artist}*: The artist of the current track + +*{album}*: The album title of the current track + +*{title}*: The title of the current track + +*{length}*: Length of the track, formatted as HH:MM:SS + +*{dynamic}*: Use _{artist}_, _{album}_, _{title}_ and _{length}_, automatically omit++ + empty values + +*{player_icon}*: Chooses an icon from _player-icons_ based on _{player}_ + +*{status_icon}*: Chooses an icon from _status-icons_ based on _{status}_ + +# EXAMPLES + +``` +"mpris": { + "format": "{player_icon} {dynamic}", + "format-paused": "{status_icon} {dynamic}", + "player-icons": { + "default": "▶", + "mpv": "🎵" + }, + "status-icons": { + "paused": "⏸" + }, + // "ignored-players": ["firefox"] +} +``` + +# STYLE + +- *#mpris* +- *#mpris.${status}* +- *#mpris.${player}* diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index b941c22..bdb9c99 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -51,12 +51,12 @@ Additionally you can control the volume by scrolling *up* or *down* while the cu The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should take up. *align*: ++ - typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + 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. *scroll-step*: ++ typeof: float ++ @@ -91,6 +91,10 @@ Additionally you can control the volume by scrolling *up* or *down* while the cu typeof: double ++ Threshold to be used when scrolling. +*reverse-scrolling*: ++ + typeof: bool ++ + Option to reverse the scroll direction. + *tooltip*: ++ typeof: bool ++ default: true ++ diff --git a/man/waybar-river-layout.5.scd b/man/waybar-river-layout.5.scd new file mode 100644 index 0000000..5b18eee --- /dev/null +++ b/man/waybar-river-layout.5.scd @@ -0,0 +1,67 @@ +waybar-river-layout(5) + +# NAME + +waybar - river layout module + +# DESCRIPTION + +The *layout* module displays the current layout in river. + +It may not be set until a layout is first applied. + +# CONFIGURATION + +Addressed by *river/layout* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format, how information should be displayed. On {} data gets inserted. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right clicked on the module. + +# EXAMPLE + +``` +"river/layout": { + "format": "{}", + "min-length": 4, + "align": "right" +} +``` + +# STYLE + +- *#layout* +- *#layout.focused* Applied when the output this module's bar belongs to is focused. + +# SEE ALSO + +waybar(5), river(1) diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index 6e5ebdb..0dd1629 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -66,9 +66,28 @@ Addressed by *sway/window* default: true ++ Option to disable tooltip on hover. +*all-outputs*: ++ + typeof: bool ++ + default: false ++ + Option to show the focused window along with its workspace styles on all outputs. + +*offscreen-css*: ++ + typeof: bool ++ + default: false ++ + Only effective when all-outputs is true. Adds style according to present windows on unfocused outputs instead of showing the focused window and style. + +*offscreen-css-text*: ++ + typeof: string ++ + Only effective when both all-outputs and offscreen-style are true. On screens currently not focused, show the given text along with that workspaces styles. + +*show-focused-workspace-name*: ++ + typeof: bool ++ + default: false ++ + If the workspace itself is focused and the workspace contains nodes or floating_nodes, show the workspace name. If not set, text remains empty but styles according to nodes in the workspace are still applied. + *rewrite*: ++ typeof: object ++ - Rules to rewrite window title. See *rewrite rules*. + Rules to rewrite the module format output. See *rewrite rules*. *icon*: ++ typeof: bool ++ @@ -97,7 +116,7 @@ captures of the expression. Regular expression and replacement follow ECMA-script rules. -If no expression matches, the title is left unchanged. +If no expression matches, the format output is left unchanged. Invalid expressions (e.g., mismatched parentheses) are skipped. @@ -117,6 +136,10 @@ Invalid expressions (e.g., mismatched parentheses) are skipped. # STYLE - *#window* -- *window#waybar.empty* When no windows is in the workspace -- *window#waybar.solo* When one window is in the workspace +- *window#waybar.empty* When no windows are in the workspace, or screen is not focused and offscreen-css option is not set +- *window#waybar.solo* When one tiled window is in the workspace +- *window#waybar.floating* When there are only floating windows in the workspace +- *window#waybar.stacked* When there is more than one window in the workspace and the workspace layout is stacked +- *window#waybar.tabbed* When there is more than one window in the workspace and the workspace layout is tabbed +- *window#waybar.tiled* When there is more than one window in the workspace and the workspace layout is splith or splitv - *window#waybar.* Where *app_id* is the app_id or *instance* name like (*chromium*) of the only window in the workspace diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index b575e09..1e5f45d 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -73,6 +73,15 @@ Addressed by *sway/workspaces* typeof: bool ++ Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration. +*alphabetical_sort*: ++ + typeof: bool ++ + Whether to sort workspaces alphabetically. Please note this can make "swaymsg workspace prev/next" move to workspaces inconsistent with the ordering shown in Waybar. + +warp-on-scroll: ++ + typeof: bool ++ + default: true ++ + If set to false, you can scroll to cycle through workspaces without mouse warping being enabled. If set to true this behaviour is disabled. + # FORMAT REPLACEMENTS *{value}*: Name of the workspace, as defined by sway. @@ -83,6 +92,8 @@ Addressed by *sway/workspaces* *{index}*: Index of the workspace. +*{output}*: Output where the workspace is located. + # ICONS Additional to workspace name matching, the following *format-icons* can be set. diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 8d11e51..cc689dc 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -19,6 +19,8 @@ Addressed by *temperature* *hwmon-path*: ++ typeof: string ++ The temperature path to use, e.g. */sys/class/hwmon/hwmon2/temp1_input* instead of one in */sys/class/thermal/*. + This can also be an array of strings. In this case, waybar will check each item in the array and use the first valid one. + This is suitable if you want to share the same configuration file among different machines with different hardware configurations. *hwmon-path-abs*: ++ typeof: string ++ @@ -117,7 +119,7 @@ Addressed by *temperature* ``` "temperature": { // "thermal-zone": 2, - // "hwmon-path": "/sys/class/hwmon/hwmon2/temp1_input", + // "hwmon-path": ["/sys/class/hwmon/hwmon2/temp1_input", "/sys/class/thermal/thermal_zone0/temp"], // "critical-threshold": 80, // "format-critical": "{temperatureC}°C ", "format": "{temperatureC}°C " diff --git a/man/waybar-upower.5.scd b/man/waybar-upower.5.scd index a6ba4df..fc37b66 100644 --- a/man/waybar-upower.5.scd +++ b/man/waybar-upower.5.scd @@ -11,6 +11,12 @@ compatible devices in the tooltip. # CONFIGURATION +*native-path*: ++ + typeof: string ++ + default: ++ + The battery to monitor. Refer to the https://upower.freedesktop.org/docs/UpDevice.html#UpDevice--native-path ++ + Can be obtained using `upower --dump` + *icon-size*: ++ typeof: integer ++ default: 20 ++ @@ -68,6 +74,25 @@ depending on the charging state. "tooltip-spacing": 20 } +``` +``` +"upower": { + "native-path": "/org/bluez/hci0/dev_D4_AE_41_38_D0_EF", + "icon-size": 20, + "hide-if-empty": true, + "tooltip": true, + "tooltip-spacing": 20 +} + +``` +``` +"upower": { + "native-path": "battery_sony_controller_battery_d0o27o88o32ofcoee", + "icon-size": 20, + "hide-if-empty": true, + "tooltip": true, + "tooltip-spacing": 20 +} ``` # STYLE diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index 3cf5694..473df92 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -11,59 +11,77 @@ The *wireplumber* module displays the current volume reported by WirePlumber. # CONFIGURATION *format*: ++ - typeof: string ++ - default: *{volume}%* ++ - The format, how information should be displayed. This format is used when other formats aren't specified. + typeof: string ++ + default: *{volume}%* ++ + The format, how information should be displayed. This format is used when other formats aren't specified. - *format-muted*: ++ - typeof: string ++ - This format is used when the sound is muted. +*format-muted*: ++ + typeof: string ++ + This format is used when the sound is muted. *tooltip*: ++ - typeof: bool ++ - default: *true* ++ - Option to disable tooltip on hover. + typeof: bool ++ + default: *true* ++ + Option to disable tooltip on hover. *tooltip-format*: ++ - typeof: string ++ - default: *{node_name}* ++ - The format of information displayed in the tooltip. + typeof: string ++ + default: *{node_name}* ++ + The format of information displayed in the tooltip. *rotate*: ++ - typeof: integer ++ - Positive value to rotate the text label. + typeof: integer ++ + Positive value to rotate the text label. *states*: ++ - typeof: object ++ - A number of volume states which get activated on certain volume levels. See *waybar-states(5)*. + typeof: object ++ + A number of volume states which get activated on certain volume levels. See *waybar-states(5)*. *max-length*: ++ - typeof: integer ++ - The maximum length in character the module should display. + typeof: integer ++ + The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should take up. *align*: ++ - typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + 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. + +*scroll-step*: ++ + typeof: float ++ + default: 1.0 ++ + The speed in which to change the volume when scrolling. *on-click*: ++ - typeof: string ++ - Command to execute when clicked on the module. + 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. + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. *on-click-right*: ++ - typeof: string ++ - Command to execute when you right clicked on the module. + typeof: string ++ + Command to execute when you right clicked on the module. *on-update*: ++ - typeof: string ++ - Command to execute when the module is updated. + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. This replaces the default behaviour of volume control. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. This replaces the default behaviour of volume control. + +*max-volume*: ++ + typeof: float ++ + default: 100 ++ + The maximum volume that can be set, in percentage. # FORMAT REPLACEMENTS diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index b2946ac..5626eae 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -52,6 +52,11 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, always reorder the tasks in the taskbar so that the currently active one is first. Otherwise don't reorder. +*sort-by-app-id*: ++ + typeof: bool ++ + default: false ++ + If set to true, group tasks by their app_id. Cannot be used with 'active-first'. + *on-click*: ++ typeof: string ++ The action which should be triggered when clicking on the application button with the left mouse button. @@ -76,6 +81,10 @@ Addressed by *wlr/taskbar* typeof: object ++ Dictionary of app_id to be replaced with +*rewrite*: ++ + typeof: object ++ + Rules to rewrite the module format output. See *rewrite rules*. + # FORMAT REPLACEMENTS *{icon}*: The icon of the application. @@ -104,6 +113,18 @@ Addressed by *wlr/taskbar* *close*: Close the application. +# 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 ``` @@ -119,6 +140,10 @@ Addressed by *wlr/taskbar* ], "app_ids-mapping": { "firefoxdeveloperedition": "firefox-developer-edition" + }, + "rewrite": { + "Firefox Web Browser": "Firefox", + "Foot Server": "Terminal" } } ``` diff --git a/man/waybar-wlr-workspaces.5.scd b/man/waybar-wlr-workspaces.5.scd index 169112f..4a256f0 100644 --- a/man/waybar-wlr-workspaces.5.scd +++ b/man/waybar-wlr-workspaces.5.scd @@ -65,7 +65,7 @@ Addressed by *wlr/workspaces* Additional to workspace name matching, the following *format-icons* can be set. - *default*: Will be shown, when no string match is found. -- *focused*: Will be shown, when workspace is focused +- *active*: Will be shown, when workspace is active # EXAMPLES @@ -78,7 +78,7 @@ Additional to workspace name matching, the following *format-icons* can be set. "3": "", "4": "", "5": "", - "focused": "", + "active": "", "default": "" }, "sort-by-number": true diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index db94da6..0b3dc74 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -31,6 +31,7 @@ Also a minimal example configuration can be found on the at the bottom of this m typeof: string|array ++ Specifies on which screen this bar will be displayed. Exclamation mark(*!*) can be used to exclude specific output. Output specification follows sway's and can either be the output port such as "HDMI-A-1" or a string consisting of the make, model and serial such as "Some Company ABC123 0x00000000". See *sway-output(5)* for details. + In an array, star '*\**' can be used at the end to accept all outputs, in case all previous entries are exclusions. *position* ++ typeof: string ++ @@ -78,7 +79,12 @@ Also a minimal example configuration can be found on the at the bottom of this m Selects one of the preconfigured display modes. This is an equivalent of the sway-bar(5) *mode* command and supports the same values: *dock*, *hide*, *invisible*, *overlay*. ++ Note: *hide* and *invisible* modes may be not as useful without Sway IPC. -modifier-reset ++ +*start_hidden* ++ + typeof: bool ++ + default: *false* ++ + Option to start the bar hidden. + +*modifier-reset* ++ typeof: string ++ default: *press* Defines the timing of modifier key to reset the bar visibility. @@ -243,6 +249,7 @@ A module group is defined by specifying a module named "group/some-group-name". "modules-right": ["group/hardware", "clock"], "group/hardware": { + "orientation": "vertical", "modules": [ "cpu", "memory", @@ -254,6 +261,8 @@ A module group is defined by specifying a module named "group/some-group-name". } ``` +Valid options for the (optional) "orientation" property are: "horizontal", "vertical", "inherit", and "orthogonal" (default). + # SUPPORTED MODULES - *waybar-backlight(5)* @@ -264,19 +273,23 @@ A module group is defined by specifying a module named "group/some-group-name". - *waybar-custom(5)* - *waybar-disk(5)* - *waybar-idle-inhibitor(5)* +- *waybar-image(5)* - *waybar-keyboard-state(5)* - *waybar-memory(5)* - *waybar-mpd(5)* +- *waybar-mpris(5)* - *waybar-network(5)* - *waybar-pulseaudio(5)* - *waybar-river-mode(5)* - *waybar-river-tags(5)* - *waybar-river-window(5)* +- *waybar-river-layout(5)* - *waybar-states(5)* - *waybar-sway-mode(5)* - *waybar-sway-scratchpad(5)* - *waybar-sway-window(5)* - *waybar-sway-workspaces(5)* +- *waybar-wireplumber(5)* - *waybar-wlr-taskbar(5)* - *waybar-wlr-workspaces(5)* - *waybar-temperature(5)* diff --git a/meson.build b/meson.build index 557a02d..aa250b7 100644 --- a/meson.build +++ b/meson.build @@ -1,8 +1,8 @@ project( 'waybar', 'cpp', 'c', - version: '0.9.16', + version: '0.9.18', license: 'MIT', - meson_version: '>= 0.49.0', + meson_version: '>= 0.50.0', default_options : [ 'cpp_std=c++17', 'buildtype=release', @@ -86,7 +86,10 @@ wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols') gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0']) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk')) -giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or get_option('logind').enabled() or get_option('upower_glib').enabled())) +giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or + get_option('logind').enabled() or + get_option('upower_glib').enabled() or + get_option('mpris').enabled())) jsoncpp = dependency('jsoncpp', version : ['>=1.9.2'], fallback : ['jsoncpp', 'jsoncpp_dep']) sigcpp = dependency('sigc++-2.0') libinotify = dependency('libinotify', required: false) @@ -95,6 +98,7 @@ libinput = dependency('libinput', required: get_option('libinput')) libnl = dependency('libnl-3.0', required: get_option('libnl')) libnlgen = dependency('libnl-genl-3.0', required: get_option('libnl')) upower_glib = dependency('upower-glib', required: get_option('upower_glib')) +playerctl = dependency('playerctl', version : ['>=2.0.0'], required: get_option('mpris')) libpulse = dependency('libpulse', required: get_option('pulseaudio')) libudev = dependency('libudev', required: get_option('libudev')) libevdev = dependency('libevdev', required: get_option('libevdev')) @@ -119,11 +123,18 @@ gtk_layer_shell = dependency('gtk-layer-shell-0', required: get_option('gtk-layer-shell'), fallback : ['gtk-layer-shell', 'gtk_layer_shell_dep']) systemd = dependency('systemd', required: get_option('systemd')) -tz_dep = dependency('date', - required: false, - default_options : [ 'use_system_tzdb=true' ], - modules : [ 'date::date', 'date::date-tz' ], - fallback: [ 'date', 'tz_dep' ]) + +cpp_lib_chrono = compiler.compute_int('__cpp_lib_chrono', prefix : '#include ') +have_chrono_timezones = cpp_lib_chrono >= 201907 +if have_chrono_timezones + tz_dep = declare_dependency() +else + tz_dep = dependency('date', + required: false, + default_options : [ 'use_system_tzdb=true' ], + modules : [ 'date::date', 'date::date-tz' ], + fallback: [ 'date', 'tz_dep' ]) +endif prefix = get_option('prefix') sysconfdir = get_option('sysconfdir') @@ -159,11 +170,15 @@ src_files = files( 'src/client.cpp', 'src/config.cpp', 'src/group.cpp', + 'src/util/prepare_for_sleep.cpp', 'src/util/ustring_clen.cpp', 'src/util/sanitize_str.cpp', - 'src/util/rewrite_title.cpp' + 'src/util/rewrite_string.cpp', + 'src/util/gtk_icon.cpp' ) +inc_dirs = ['include'] + if is_linux add_project_arguments('-DHAVE_CPU_LINUX', language: 'cpp') add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp') @@ -213,6 +228,12 @@ if true src_files += 'src/modules/river/mode.cpp' src_files += 'src/modules/river/tags.cpp' src_files += 'src/modules/river/window.cpp' + src_files += 'src/modules/river/layout.cpp' +endif + +if true + add_project_arguments('-DHAVE_DWL', language: 'cpp') + src_files += 'src/modules/dwl/tags.cpp' endif if true @@ -220,6 +241,8 @@ if true src_files += 'src/modules/hyprland/backend.cpp' src_files += 'src/modules/hyprland/window.cpp' src_files += 'src/modules/hyprland/language.cpp' + src_files += 'src/modules/hyprland/submap.cpp' + src_files += 'src/modules/hyprland/workspaces.cpp' endif if libnl.found() and libnlgen.found() @@ -238,6 +261,11 @@ if (upower_glib.found() and giounix.found() and not get_option('logind').disable src_files += 'src/modules/upower/upower_tooltip.cpp' endif +if (playerctl.found() and giounix.found() and not get_option('logind').disabled()) + add_project_arguments('-DHAVE_MPRIS', language: 'cpp') + src_files += 'src/modules/mpris/mpris.cpp' +endif + if libpulse.found() add_project_arguments('-DHAVE_LIBPULSE', language: 'cpp') src_files += 'src/modules/pulseaudio.cpp' @@ -302,7 +330,10 @@ if get_option('rfkill').enabled() and is_linux ) endif -if tz_dep.found() +if have_chrono_timezones + add_project_arguments('-DHAVE_CHRONO_TIMEZONES', language: 'cpp') + src_files += 'src/modules/clock.cpp' +elif tz_dep.found() add_project_arguments('-DHAVE_LIBDATE', language: 'cpp') src_files += 'src/modules/clock.cpp' else @@ -313,6 +344,17 @@ if get_option('experimental') add_project_arguments('-DUSE_EXPERIMENTAL', language: 'cpp') endif +cava = dependency('cava', + version : '>=0.8.4', + required: get_option('cava'), + fallback : ['cava', 'cava_dep'], + not_found_message: 'cava is not found. Building waybar without cava') + +if cava.found() + add_project_arguments('-DHAVE_LIBCAVA', language: 'cpp') + src_files += 'src/modules/cava.cpp' +endif + subdir('protocol') executable( @@ -334,6 +376,7 @@ executable( libnl, libnlgen, upower_glib, + playerctl, libpulse, libjack, libwireplumber, @@ -345,9 +388,10 @@ executable( gtk_layer_shell, libsndio, tz_dep, - xkbregistry + xkbregistry, + cava ], - include_directories: [include_directories('include')], + include_directories: inc_dirs, install: true, ) @@ -384,14 +428,17 @@ if scdoc.found() 'waybar-disk.5.scd', 'waybar-gamemode.5.scd', 'waybar-idle-inhibitor.5.scd', + 'waybar-image.5.scd', 'waybar-keyboard-state.5.scd', 'waybar-memory.5.scd', 'waybar-mpd.5.scd', + 'waybar-mpris.5.scd', 'waybar-network.5.scd', 'waybar-pulseaudio.5.scd', 'waybar-river-mode.5.scd', 'waybar-river-tags.5.scd', 'waybar-river-window.5.scd', + 'waybar-river-layout.5.scd', 'waybar-sway-language.5.scd', 'waybar-sway-mode.5.scd', 'waybar-sway-scratchpad.5.scd', @@ -405,6 +452,8 @@ if scdoc.found() 'waybar-bluetooth.5.scd', 'waybar-sndio.5.scd', 'waybar-upower.5.scd', + 'waybar-wireplumber.5.scd', + 'waybar-dwl-tags.5.scd', ] if (giounix.found() and not get_option('logind').disabled()) @@ -435,7 +484,7 @@ endif catch2 = dependency( 'catch2', - version: '>=3.0.0', + version: '>=2.0.0', fallback: ['catch2', 'catch2_dep'], required: get_option('tests'), ) diff --git a/meson_options.txt b/meson_options.txt index 402912f..7dacf08 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,6 +5,7 @@ option('libudev', type: 'feature', value: 'auto', description: 'Enable libudev s option('libevdev', type: 'feature', value: 'auto', description: 'Enable libevdev support for evdev related features') option('pulseaudio', type: 'feature', value: 'auto', description: 'Enable support for pulseaudio') option('upower_glib', type: 'feature', value: 'auto', description: 'Enable support for upower') +option('mpris', type: 'feature', value: 'auto', description: 'Enable support for mpris') option('systemd', type: 'feature', value: 'auto', description: 'Install systemd user service unit') option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable support for tray') option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') @@ -16,4 +17,5 @@ option('logind', type: 'feature', value: 'auto', description: 'Enable support fo option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('experimental', type : 'boolean', value : false, description: 'Enable experimental features') option('jack', type: 'feature', value: 'auto', description: 'Enable support for JACK') -option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') \ No newline at end of file +option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') +option('cava', type: 'feature', value: 'auto', description: 'Enable support for Cava') diff --git a/nix/default.nix b/nix/default.nix new file mode 100644 index 0000000..fc77225 --- /dev/null +++ b/nix/default.nix @@ -0,0 +1,20 @@ +{ lib +, waybar +, version +}: + +waybar.overrideAttrs (prev: { + inherit version; + # version = "0.9.17"; + + src = lib.cleanSourceWith { + filter = name: type: + let + baseName = baseNameOf (toString name); + in + ! ( + lib.hasSuffix ".nix" baseName + ); + src = lib.cleanSource ../.; + }; +}) diff --git a/protocol/dwl-ipc-unstable-v2.xml b/protocol/dwl-ipc-unstable-v2.xml new file mode 100644 index 0000000..74a212f --- /dev/null +++ b/protocol/dwl-ipc-unstable-v2.xml @@ -0,0 +1,166 @@ + + + + + This protocol allows clients to update and get updates from dwl. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible + changes may be added together with the corresponding interface + version bump. + Backward incompatible changes are done by bumping the version + number in the protocol and interface names and resetting the + interface version. Once the protocol is to be declared stable, + the 'z' prefix and the version number in the protocol and + interface names are removed and the interface version number is + reset. + + + + + This interface is exposed as a global in wl_registry. + + Clients can use this interface to get a dwl_ipc_output. + After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events. + The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client. + + + + + Indicates that the client will not the dwl_ipc_manager object anymore. + Objects created through this instance are not affected. + + + + + + Get a dwl_ipc_outout for the specified wl_output. + + + + + + + + This event is sent after binding. + A roundtrip after binding guarantees the client recieved all tags. + + + + + + + This event is sent after binding. + A roundtrip after binding guarantees the client recieved all layouts. + + + + + + + + Observe and control a dwl output. + + Events are double-buffered: + Clients should cache events and redraw when a dwl_ipc_output.frame event is sent. + + Request are not double-buffered: + The compositor will update immediately upon request. + + + + + + + + + + + Indicates to that the client no longer needs this dwl_ipc_output. + + + + + + Indicates the client should hide or show themselves. + If the client is visible then hide, if hidden then show. + + + + + + Indicates if the output is active. Zero is invalid, nonzero is valid. + + + + + + + Indicates that a tag has been updated. + + + + + + + + + + Indicates a new layout is selected. + + + + + + + Indicates the title has changed. + + + + + + + Indicates the appid has changed. + + + + + + + Indicates the layout has changed. Since layout symbols are dynamic. + As opposed to the zdwl_ipc_manager.layout event, this should take precendence when displaying. + You can ignore the zdwl_ipc_output.layout event. + + + + + + + Indicates that a sequence of status updates have finished and the client should redraw. + + + + + + + + + + + + The tags are updated as follows: + new_tags = (current_tags AND and_tags) XOR xor_tags + + + + + + + + + + + diff --git a/protocol/meson.build b/protocol/meson.build index 6e82d63..e1e745a 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -30,6 +30,7 @@ client_protocols = [ ['ext-workspace-unstable-v1.xml'], ['river-status-unstable-v1.xml'], ['river-control-unstable-v1.xml'], + ['dwl-ipc-unstable-v2.xml'], ] client_protos_src = [] diff --git a/protocol/river-status-unstable-v1.xml b/protocol/river-status-unstable-v1.xml index 6a74256..e9629dd 100644 --- a/protocol/river-status-unstable-v1.xml +++ b/protocol/river-status-unstable-v1.xml @@ -16,7 +16,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - + A global factory for objects that receive status information specific to river. It could be used to implement, for example, a status bar. @@ -47,7 +47,7 @@ - + This interface allows clients to receive information about the current windowing state of an output. @@ -83,6 +83,21 @@ + + + + Sent once on binding the interface should a layout name exist and again + whenever the name changes. + + + + + + + Sent when the current layout name has been removed without a new one + being set, for example when the active layout generator disconnects. + + diff --git a/resources/config b/resources/config index ad76e93..daad8ab 100644 --- a/resources/config +++ b/resources/config @@ -12,6 +12,7 @@ // "sway/workspaces": { // "disable-scroll": true, // "all-outputs": true, + // "warp-on-scroll": false, // "format": "{name}: {icon}", // "format-icons": { // "1": "", diff --git a/src/AModule.cpp b/src/AModule.cpp index b19594a..2626cd8 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -12,6 +12,20 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: config_(std::move(config)), 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) { + eventActionMap_.insert({it.key().asString(), it->asString()}); + enable_click = true; + enable_scroll = true; + } else + spdlog::warn("Dublicate action is ignored: {0}", it.key().asString()); + else + spdlog::warn("Wrong actions section configuration. See config by index: {}", it.index()); + } + // configure events' user commands if (enable_click) { event_box_.add_events(Gdk::BUTTON_PRESS_MASK); @@ -48,19 +62,33 @@ auto AModule::update() -> void { pid_.push_back(util::command::forkExec(config_["on-update"].asString())); } } +// Get mapping between event name and module action name +// Then call overrided doAction in order to call appropriate module action +auto AModule::doAction(const std::string& name) -> void { + if (!name.empty()) { + const std::map::const_iterator& recA{eventActionMap_.find(name)}; + // Call overrided action if derrived class has implemented it + if (recA != eventActionMap_.cend() && name != recA->second) this->doAction(recA->second); + } +} bool AModule::handleToggle(GdkEventButton* const& e) { + std::string format{}; const std::map, std::string>::const_iterator& rec{ eventMap_.find(std::pair(e->button, e->type))}; - std::string format{(rec != eventMap_.cend()) ? rec->second : std::string{""}}; + if (rec != eventMap_.cend()) { + // First call module actions + this->AModule::doAction(rec->second); + format = rec->second; + } + // Second call user scripts if (!format.empty()) { if (config_[format].isString()) format = config_[format].asString(); else format.clear(); } - if (!format.empty()) { pid_.push_back(util::command::forkExec(format)); } @@ -69,11 +97,21 @@ bool AModule::handleToggle(GdkEventButton* const& e) { } AModule::SCROLL_DIR AModule::getScrollDir(GdkEventScroll* e) { + // only affects up/down + bool reverse = config_["reverse-scrolling"].asBool(); + bool reverse_mouse = config_["reverse-mouse-scrolling"].asBool(); + + // 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) { + reverse = reverse_mouse; + } + switch (e->direction) { case GDK_SCROLL_UP: - return SCROLL_DIR::UP; + return reverse ? SCROLL_DIR::DOWN : SCROLL_DIR::UP; case GDK_SCROLL_DOWN: - return SCROLL_DIR::DOWN; + return reverse ? SCROLL_DIR::UP : SCROLL_DIR::DOWN; case GDK_SCROLL_LEFT: return SCROLL_DIR::LEFT; case GDK_SCROLL_RIGHT: @@ -90,9 +128,9 @@ AModule::SCROLL_DIR AModule::getScrollDir(GdkEventScroll* e) { } if (distance_scrolled_y_ < -threshold) { - dir = SCROLL_DIR::UP; + dir = reverse ? SCROLL_DIR::DOWN : SCROLL_DIR::UP; } else if (distance_scrolled_y_ > threshold) { - dir = SCROLL_DIR::DOWN; + dir = reverse ? SCROLL_DIR::UP : SCROLL_DIR::DOWN; } else if (distance_scrolled_x_ > threshold) { dir = SCROLL_DIR::RIGHT; } else if (distance_scrolled_x_ < -threshold) { @@ -122,11 +160,19 @@ AModule::SCROLL_DIR AModule::getScrollDir(GdkEventScroll* e) { bool AModule::handleScroll(GdkEventScroll* e) { auto dir = getScrollDir(e); - if (dir == SCROLL_DIR::UP && config_["on-scroll-up"].isString()) { - pid_.push_back(util::command::forkExec(config_["on-scroll-up"].asString())); - } else if (dir == SCROLL_DIR::DOWN && config_["on-scroll-down"].isString()) { - pid_.push_back(util::command::forkExec(config_["on-scroll-down"].asString())); - } + std::string eventName{}; + + if (dir == SCROLL_DIR::UP) + eventName = "on-scroll-up"; + else if (dir == SCROLL_DIR::DOWN) + eventName = "on-scroll-down"; + + // First call module actions + this->AModule::doAction(eventName); + // Second call user scripts + if (config_[eventName].isString()) + pid_.push_back(util::command::forkExec(config_[eventName].asString())); + dp.emit(); return true; } diff --git a/src/bar.cpp b/src/bar.cpp index f46b7d0..60104f0 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -593,6 +593,10 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) setMode(MODE_DEFAULT); } + if (config["start_hidden"].asBool()) { + setVisible(false); + } + window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); #if HAVE_SWAY @@ -725,10 +729,7 @@ void waybar::Bar::setupAltFormatKeyForModuleList(const char* module_list_name) { void waybar::Bar::handleSignal(int signal) { for (auto& module : modules_all_) { - auto* custom = dynamic_cast(module.get()); - if (custom != nullptr) { - custom->refresh(signal); - } + module->refresh(signal); } } @@ -742,7 +743,13 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, AModule* module; if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) { - auto group_module = new waybar::Group(ref, *this, config[ref]); + auto hash_pos = ref.find('#'); + auto id_name = ref.substr(6, hash_pos - 6); + auto class_name = hash_pos != std::string::npos ? ref.substr(hash_pos + 1) : ""; + + auto parent = group ? group : &this->box_; + auto vertical = parent->get_orientation() == Gtk::ORIENTATION_VERTICAL; + auto group_module = new waybar::Group(id_name, class_name, config[ref], vertical); getModules(factory, ref, &group_module->box); module = group_module; } else { @@ -764,11 +771,11 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, modules_right_.emplace_back(module_sp); } } - module->dp.connect([module, name] { + module->dp.connect([module, ref] { try { module->update(); } catch (const std::exception& e) { - spdlog::error("{}: {}", name.asString(), e.what()); + spdlog::error("{}: {}", ref, e.what()); } }); } catch (const std::exception& e) { diff --git a/src/config.cpp b/src/config.cpp index a4d9090..45f5ee3 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -124,9 +124,21 @@ bool isValidOutput(const Json::Value &config, const std::string &name, const std::string &identifier) { if (config["output"].isArray()) { for (auto const &output_conf : config["output"]) { - if (output_conf.isString() && - (output_conf.asString() == name || output_conf.asString() == identifier)) { - return true; + if (output_conf.isString()) { + auto config_output = output_conf.asString(); + if (config_output.substr(0, 1) == "!") { + if (config_output.substr(1) == name || config_output.substr(1) == identifier) { + return false; + } else { + continue; + } + } + if (config_output == name || config_output == identifier) { + return true; + } + if (config_output.substr(0, 1) == "*") { + return true; + } } } return false; diff --git a/src/factory.cpp b/src/factory.cpp index d16cb52..1d7a00b 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -22,6 +22,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::upower::UPower(id, config_[name]); } #endif +#ifdef HAVE_MPRIS + if (ref == "mpris") { + return new waybar::modules::mpris::Mpris(id, config_[name]); + } +#endif #ifdef HAVE_SWAY if (ref == "sway/mode") { return new waybar::modules::sway::Mode(id, config_[name]); @@ -59,6 +64,14 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "river/window") { return new waybar::modules::river::Window(id, bar_, config_[name]); } + if (ref == "river/layout") { + return new waybar::modules::river::Layout(id, bar_, config_[name]); + } +#endif +#ifdef HAVE_DWL + if (ref == "dwl/tags") { + return new waybar::modules::dwl::Tags(id, bar_, config_[name]); + } #endif #ifdef HAVE_HYPRLAND if (ref == "hyprland/window") { @@ -67,6 +80,12 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "hyprland/language") { return new waybar::modules::hyprland::Language(id, bar_, config_[name]); } + if (ref == "hyprland/submap") { + return new waybar::modules::hyprland::Submap(id, bar_, config_[name]); + } + if (ref == "hyprland/workspaces") { + return new waybar::modules::hyprland::Workspaces(id, bar_, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); @@ -90,6 +109,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "disk") { return new waybar::modules::Disk(id, config_[name]); } + if (ref == "image") { + return new waybar::modules::Image(id, config_[name]); + } #ifdef HAVE_DBUSMENU if (ref == "tray") { return new waybar::modules::SNI::Tray(id, bar_, config_[name]); @@ -142,14 +164,17 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "wireplumber") { return new waybar::modules::Wireplumber(id, config_[name]); } +#endif +#ifdef HAVE_LIBCAVA + if (ref == "cava") { + return new waybar::modules::Cava(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); } if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name]); - } else if (ref.compare(0, 6, "image/") == 0 && ref.size() > 6) { - return new waybar::modules::Image(ref.substr(6), id, config_[name]); } } catch (const std::exception& e) { auto err = fmt::format("Disabling module \"{}\", {}", name, e.what()); diff --git a/src/group.cpp b/src/group.cpp index 8324365..548fb0d 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -6,9 +6,30 @@ namespace waybar { -Group::Group(const std::string& name, const Bar& bar, const Json::Value& config) - : AModule(config, name, "", false, false), - box{bar.vertical ? Gtk::ORIENTATION_HORIZONTAL : Gtk::ORIENTATION_VERTICAL, 0} {} +Group::Group(const std::string& name, const std::string& id, const Json::Value& config, + bool vertical) + : AModule(config, name, id, false, false), + box{vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0} { + box.set_name(name_); + if (!id.empty()) { + box.get_style_context()->add_class(id); + } + + // default orientation: orthogonal to parent + auto orientation = + config_["orientation"].empty() ? "orthogonal" : config_["orientation"].asString(); + if (orientation == "inherit") { + // keep orientation passed + } else if (orientation == "orthogonal") { + box.set_orientation(vertical ? Gtk::ORIENTATION_HORIZONTAL : Gtk::ORIENTATION_VERTICAL); + } else if (orientation == "vertical") { + box.set_orientation(Gtk::ORIENTATION_VERTICAL); + } else if (orientation == "horizontal") { + box.set_orientation(Gtk::ORIENTATION_HORIZONTAL); + } else { + throw std::runtime_error("Invalid orientation value: " + orientation); + } +} auto Group::update() -> void { // noop diff --git a/src/main.cpp b/src/main.cpp index e06774b..ff446ff 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -85,6 +85,12 @@ int main(int argc, char* argv[]) { waybar::Client::inst()->reset(); }); + std::signal(SIGINT, [](int /*signal*/) { + spdlog::info("Quitting."); + reload = false; + waybar::Client::inst()->reset(); + }); + for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { std::signal(sig, [](int sig) { for (auto& bar : waybar::Client::inst()->bars) { diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index aa734a4..58d14dd 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -48,13 +48,13 @@ struct UdevMonitorDeleter { void check_eq(int rc, int expected, const char *message = "eq, rc was: ") { if (rc != expected) { - throw std::runtime_error(fmt::format(message, rc)); + throw std::runtime_error(fmt::format(fmt::runtime(message), rc)); } } void check_neq(int rc, int bad_rc, const char *message = "neq, rc was: ") { if (rc == bad_rc) { - throw std::runtime_error(fmt::format(message, rc)); + throw std::runtime_error(fmt::format(fmt::runtime(message), rc)); } } @@ -62,7 +62,7 @@ void check0(int rc, const char *message = "rc wasn't 0") { check_eq(rc, 0, messa void check_gte(int rc, int gte, const char *message = "rc was: ") { if (rc < gte) { - throw std::runtime_error(fmt::format(message, rc)); + throw std::runtime_error(fmt::format(fmt::runtime(message), rc)); } } @@ -106,6 +106,15 @@ waybar::modules::Backlight::Backlight(const std::string &id, const Json::Value & dp.emit(); } + // Set up scroll handler + event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Backlight::handleScroll)); + + // Connect to the login interface + login_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.login1", + "/org/freedesktop/login1/session/self", "org.freedesktop.login1.Session"); + udev_thread_ = [this] { std::unique_ptr udev{udev_new()}; check_nn(udev.get(), "Udev new failed"); @@ -181,9 +190,24 @@ auto waybar::modules::Backlight::update() -> void { event_box_.show(); const uint8_t percent = best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max()); - label_.set_markup(fmt::format(format_, fmt::arg("percent", std::to_string(percent)), - fmt::arg("icon", getIcon(percent)))); + std::string desc = + fmt::format(fmt::runtime(format_), fmt::arg("percent", std::to_string(percent)), + fmt::arg("icon", getIcon(percent))); + label_.set_markup(desc); getState(percent); + if (tooltipEnabled()) { + std::string tooltip_format; + if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format), + fmt::arg("percent", std::to_string(percent)), + fmt::arg("icon", getIcon(percent)))); + } else { + label_.set_tooltip_text(desc); + } + } } else { event_box_.hide(); } @@ -263,3 +287,63 @@ void waybar::modules::Backlight::enumerate_devices(ForwardIt first, ForwardIt la upsert_device(first, last, inserter, dev.get()); } } + +bool waybar::modules::Backlight::handleScroll(GdkEventScroll *e) { + // Check if the user has set a custom command for scrolling + if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { + return AModule::handleScroll(e); + } + + // Fail fast if the proxy could not be initialized + if (!login_proxy_) { + return true; + } + + // Check scroll direction + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + + // Get scroll step + double step = 1; + + if (config_["scroll-step"].isDouble()) { + step = config_["scroll-step"].asDouble(); + } + + // Get the best device + decltype(devices_) devices; + { + std::scoped_lock lock(udev_thread_mutex_); + devices = devices_; + } + const auto best = best_device(devices.cbegin(), devices.cend(), preferred_device_); + + if (best == nullptr) { + return true; + } + + // Compute the absolute step + const auto abs_step = static_cast(round(step * best->get_max() / 100.0f)); + + // Compute the new value + int new_value = best->get_actual(); + + if (dir == SCROLL_DIR::UP) { + new_value += abs_step; + } else if (dir == SCROLL_DIR::DOWN) { + new_value -= abs_step; + } + + // Clamp the value + new_value = std::clamp(new_value, 0, best->get_max()); + + // Set the new value + auto call_args = Glib::VariantContainerBase( + g_variant_new("(ssu)", "backlight", std::string(best->name()).c_str(), new_value)); + + login_proxy_->call_sync("SetBrightness", call_args); + + return true; +} diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index b7a9cd0..c0f433a 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -36,7 +36,8 @@ waybar::modules::Battery::~Battery() { } close(global_watch_fd_); - for (auto it = batteries_.cbegin(); it != batteries_.cend(); it++) { + for (auto it = batteries_.cbegin(), next_it = it; it != batteries_.cend(); it = next_it) { + ++next_it; auto watch_id = (*it).second; if (watch_id >= 0) { inotify_rm_watch(battery_watch_fd_, watch_id); @@ -107,6 +108,15 @@ void waybar::modules::Battery::refreshBatteries() { std::ifstream(node.path() / "type") >> type; if (!type.compare("Battery")) { + // Ignore non-system power supplies unless explicitly requested + if (!bat_defined && fs::exists(node.path() / "scope")) { + std::string scope; + std::ifstream(node.path() / "scope") >> scope; + if (g_ascii_strcasecmp(scope.data(), "device") == 0) { + continue; + } + } + check_map[node.path()] = true; auto search = batteries_.find(node.path()); if (search == batteries_.end()) { @@ -496,11 +506,12 @@ const std::tuple waybar::modules::Battery::g float time_remaining{0.0f}; if (status == "Discharging" && time_to_empty_now_exists) { - if (time_to_empty_now != 0) time_remaining = (float)time_to_empty_now / 1000.0f; + if (time_to_empty_now != 0) time_remaining = (float)time_to_empty_now / 3600.0f; } else if (status == "Discharging" && total_power_exists && total_energy_exists) { if (total_power != 0) time_remaining = (float)total_energy / total_power; } else if (status == "Charging" && time_to_full_now_exists) { - if (time_to_full_now_exists && (time_to_full_now != 0)) time_remaining = -(float)time_to_full_now / 1000.0f; + if (time_to_full_now_exists && (time_to_full_now != 0)) + time_remaining = -(float)time_to_full_now / 3600.0f; // If we've turned positive it means the battery is past 100% and so just report that as no // time remaining if (time_remaining > 0.0f) time_remaining = 0.0f; @@ -594,7 +605,7 @@ const std::string waybar::modules::Battery::formatTimeRemaining(float hoursRemai format = config_["format-time"].asString(); } std::string zero_pad_minutes = fmt::format("{:02d}", minutes); - return fmt::format(format, fmt::arg("H", full_hours), fmt::arg("M", minutes), + return fmt::format(fmt::runtime(format), fmt::arg("H", full_hours), fmt::arg("M", minutes), fmt::arg("m", zero_pad_minutes)); } @@ -634,7 +645,8 @@ auto waybar::modules::Battery::update() -> void { } else if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text(fmt::format(tooltip_format, fmt::arg("timeTo", tooltip_text_default), + 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))); } @@ -655,9 +667,9 @@ auto waybar::modules::Battery::update() -> void { } else { event_box_.show(); auto icons = std::vector{status + "-" + state, status, state}; - label_.set_markup(fmt::format(format, fmt::arg("capacity", capacity), fmt::arg("power", power), - fmt::arg("icon", getIcon(capacity, icons)), - fmt::arg("time", time_remaining_formatted))); + 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))); } // Call parent update ALabel::update(); diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index e6a1fe3..c3a2547 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -206,7 +206,8 @@ auto waybar::modules::Bluetooth::update() -> void { state_ = state; label_.set_markup(fmt::format( - format_, fmt::arg("status", state_), fmt::arg("num_connections", connected_devices_.size()), + fmt::runtime(format_), fmt::arg("status", state_), + fmt::arg("num_connections", connected_devices_.size()), fmt::arg("controller_address", cur_controller_.address), fmt::arg("controller_address_type", cur_controller_.address_type), fmt::arg("controller_alias", cur_controller_.alias), @@ -234,7 +235,7 @@ auto waybar::modules::Bluetooth::update() -> void { enumerate_format = config_["tooltip-format-enumerate-connected"].asString(); } ss << fmt::format( - enumerate_format, fmt::arg("device_address", dev.address), + fmt::runtime(enumerate_format), fmt::arg("device_address", dev.address), fmt::arg("device_address_type", dev.address_type), fmt::arg("device_alias", dev.alias), fmt::arg("icon", enumerate_icon), fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0))); @@ -247,7 +248,7 @@ auto waybar::modules::Bluetooth::update() -> void { } } label_.set_tooltip_text(fmt::format( - tooltip_format, fmt::arg("status", state_), + fmt::runtime(tooltip_format), fmt::arg("status", state_), fmt::arg("num_connections", connected_devices_.size()), fmt::arg("controller_address", cur_controller_.address), fmt::arg("controller_address_type", cur_controller_.address_type), diff --git a/src/modules/cava.cpp b/src/modules/cava.cpp new file mode 100644 index 0000000..be9bef4 --- /dev/null +++ b/src/modules/cava.cpp @@ -0,0 +1,202 @@ +#include "modules/cava.hpp" + +#include + +waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) + : ALabel(config, "cava", id, "{}", 60, false, false, false) { + // Load waybar module config + char cfgPath[PATH_MAX]; + cfgPath[0] = '\0'; + + if (config_["cava_config"].isString()) { + 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()); + } + // Load cava config + error_.length = 0; + + if (!load_config(cfgPath, &prm_, false, &error_)) { + spdlog::error("Error loading config. {0}", error_.message); + exit(EXIT_FAILURE); + } + + // Override cava parameters by the user config + prm_.inAtty = 0; + prm_.output = output_method::OUTPUT_RAW; + strcpy(prm_.data_format, "ascii"); + strcpy(prm_.raw_target, "/dev/stdout"); + prm_.ascii_range = config_["format-icons"].size() - 1; + + prm_.bar_width = 2; + prm_.bar_spacing = 0; + prm_.bar_height = 32; + prm_.bar_width = 1; + prm_.orientation = ORIENT_TOP; + prm_.xaxis = xaxis_scale::NONE; + prm_.mono_opt = AVERAGE; + prm_.autobars = 0; + prm_.gravity = 0; + prm_.integral = 1; + + if (config_["framerate"].isInt()) prm_.framerate = config_["framerate"].asInt(); + if (config_["autosens"].isInt()) prm_.autosens = config_["autosens"].asInt(); + if (config_["sensitivity"].isInt()) prm_.sens = config_["sensitivity"].asInt(); + if (config_["bars"].isInt()) prm_.fixedbars = config_["bars"].asInt(); + if (config_["lower_cutoff_freq"].isNumeric()) + prm_.lower_cut_off = config_["lower_cutoff_freq"].asLargestInt(); + if (config_["higher_cutoff_freq"].isNumeric()) + prm_.upper_cut_off = config_["higher_cutoff_freq"].asLargestInt(); + if (config_["sleep_timer"].isInt()) prm_.sleep_timer = config_["sleep_timer"].asInt(); + if (config_["method"].isString()) + prm_.input = input_method_by_name(config_["method"].asString().c_str()); + if (config_["source"].isString()) prm_.audio_source = config_["source"].asString().data(); + if (config_["sample_rate"].isNumeric()) prm_.fifoSample = config_["sample_rate"].asLargestInt(); + if (config_["sample_bits"].isInt()) prm_.fifoSampleBits = config_["sample_bits"].asInt(); + if (config_["stereo"].isBool()) prm_.stereo = config_["stereo"].asBool(); + if (config_["reverse"].isBool()) prm_.reverse = config_["reverse"].asBool(); + if (config_["bar_delimiter"].isInt()) prm_.bar_delim = config_["bar_delimiter"].asInt(); + if (config_["monstercat"].isBool()) prm_.monstercat = config_["monstercat"].asBool(); + if (config_["waves"].isBool()) prm_.waves = config_["waves"].asBool(); + if (config_["noise_reduction"].isDouble()) + prm_.noise_reduction = config_["noise_reduction"].asDouble(); + if (config_["input_delay"].isInt()) + fetch_input_delay_ = std::chrono::seconds(config_["input_delay"].asInt()); + // Make cava parameters configuration + plan_ = new cava_plan{}; + + audio_raw_.height = prm_.ascii_range; + audio_data_.format = -1; + audio_data_.source = new char[1 + strlen(prm_.audio_source)]; + audio_data_.source[0] = '\0'; + strcpy(audio_data_.source, prm_.audio_source); + + audio_data_.rate = 0; + audio_data_.samples_counter = 0; + audio_data_.channels = 2; + audio_data_.IEEE_FLOAT = 0; + + audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels; + audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8; + + audio_data_.cava_in = new double[audio_data_.cava_buffer_size]{0.0}; + + audio_data_.terminate = 0; + audio_data_.suspendFlag = false; + input_source_ = get_input(&audio_data_, &prm_); + + if (!input_source_) { + spdlog::error("cava API didn't provide input audio source method"); + exit(EXIT_FAILURE); + } + // Calculate delay for Update() thread + frame_time_milsec_ = std::chrono::milliseconds((int)(1e3 / prm_.framerate)); + + // Init cava plan, audio_raw structure + audio_raw_init(&audio_data_, &audio_raw_, &prm_, plan_); + if (!plan_) spdlog::error("cava plan is not provided"); + audio_raw_.previous_frame[0] = -1; // For first Update() call need to rePaint text message + // Read audio source trough cava API. Cava orginizes this process via infinity loop + thread_fetch_input_ = [this] { + thread_fetch_input_.sleep_for(fetch_input_delay_); + input_source_(&audio_data_); + }; + + thread_ = [this] { + dp.emit(); + thread_.sleep_for(frame_time_milsec_); + }; +} + +waybar::modules::Cava::~Cava() { + thread_fetch_input_.stop(); + thread_.stop(); + delete plan_; + plan_ = nullptr; +} + +void upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { + if (delta == std::chrono::seconds{0}) { + delta += std::chrono::seconds{1}; + delay += delta; + } +} + +void downThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { + if (delta > std::chrono::seconds{0}) { + delay -= delta; + delta -= std::chrono::seconds{1}; + } +} + +auto waybar::modules::Cava::update() -> void { + if (audio_data_.suspendFlag) return; + silence_ = true; + + for (int i{0}; i < audio_data_.input_buffer_size; ++i) { + if (audio_data_.cava_in[i]) { + silence_ = false; + sleep_counter_ = 0; + break; + } + } + + if (silence_ && prm_.sleep_timer) { + if (sleep_counter_ <= + (int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { + ++sleep_counter_; + silence_ = false; + } + } + + if (!silence_) { + downThreadDelay(frame_time_milsec_, suspend_silence_delay_); + // Process: execute cava + pthread_mutex_lock(&audio_data_.lock); + cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out, plan_); + if (audio_data_.samples_counter > 0) audio_data_.samples_counter = 0; + pthread_mutex_unlock(&audio_data_.lock); + + // Do transformation under raw data + audio_raw_fetch(&audio_raw_, &prm_, &rePaint_); + + if (rePaint_ == 1) { + text_.clear(); + + for (int i{0}; i < audio_raw_.number_of_bars; ++i) { + audio_raw_.previous_frame[i] = audio_raw_.bars[i]; + text_.append( + getIcon((audio_raw_.bars[i] > prm_.ascii_range) ? prm_.ascii_range : audio_raw_.bars[i], + "", prm_.ascii_range + 1)); + if (prm_.bar_delim != 0) text_.push_back(prm_.bar_delim); + } + + label_.set_markup(text_); + ALabel::update(); + } + } else + upThreadDelay(frame_time_milsec_, suspend_silence_delay_); +} + +auto waybar::modules::Cava::doAction(const std::string& name) -> void { + if ((actionMap_[name])) { + (this->*actionMap_[name])(); + } else + spdlog::error("Cava. Unsupported action \"{0}\"", name); +} + +// Cava actions +void waybar::modules::Cava::pause_resume() { + pthread_mutex_lock(&audio_data_.lock); + if (audio_data_.suspendFlag) { + audio_data_.suspendFlag = false; + pthread_cond_broadcast(&audio_data_.resumeCond); + downThreadDelay(frame_time_milsec_, suspend_silence_delay_); + } else { + audio_data_.suspendFlag = true; + upThreadDelay(frame_time_milsec_, suspend_silence_delay_); + } + pthread_mutex_unlock(&audio_data_.lock); +} diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index a40d412..17cfd8d 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -5,18 +5,16 @@ #include #include +#include #include #include #include "util/ustring_clen.hpp" -#include "util/waybar_time.hpp" #ifdef HAVE_LANGINFO_1STDAY #include #include #endif -using waybar::waybar_time; - waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true), current_time_zone_idx_(0), @@ -24,26 +22,31 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) is_timezoned_list_in_tooltip_(false) { if (config_["timezones"].isArray() && !config_["timezones"].empty()) { for (const auto& zone_name : config_["timezones"]) { - if (!zone_name.isString() || zone_name.asString().empty()) { - time_zones_.push_back(nullptr); - continue; - } - time_zones_.push_back(date::locate_zone(zone_name.asString())); + if (!zone_name.isString()) continue; + if (zone_name.asString().empty()) + time_zones_.push_back(date::current_zone()); + else + try { + time_zones_.push_back(date::locate_zone(zone_name.asString())); + } catch (const std::exception& e) { + spdlog::warn("Timezone: {0}. {1}", zone_name.asString(), e.what()); + } } - } else if (config_["timezone"].isString() && !config_["timezone"].asString().empty()) { - time_zones_.push_back(date::locate_zone(config_["timezone"].asString())); + } else if (config_["timezone"].isString()) { + if (config_["timezone"].asString().empty()) + time_zones_.push_back(date::current_zone()); + else + try { + time_zones_.push_back(date::locate_zone(config_["timezone"].asString())); + } catch (const std::exception& e) { + spdlog::warn("Timezone: {0}. {1}", config_["timezone"].asString(), e.what()); + } } - // If all timezones are parsed and no one is good, add nullptr to the timezones vector, to mark - // that local time should be shown. + // If all timezones are parsed and no one is good, add current time zone. nullptr in timezones + // vector means that local time should be shown if (!time_zones_.size()) { - time_zones_.push_back(nullptr); - } - - if (!is_timezone_fixed()) { - spdlog::warn( - "As using a timezone, some format args may be missing as the date library haven't got a " - "release since 2018."); + time_zones_.push_back(date::current_zone()); } // Check if a particular placeholder is present in the tooltip format, to know what to calculate @@ -61,18 +64,86 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) } } + // Calendar configuration if (is_calendar_in_tooltip_) { - if (config_["on-scroll"][kCalendarPlaceholder].isInt()) { - calendar_shift_init_ = - date::months{config_["on-scroll"].get(kCalendarPlaceholder, 0).asInt()}; + if (config_[kCalendarPlaceholder]["weeks-pos"].isString()) { + if (config_[kCalendarPlaceholder]["weeks-pos"].asString() == "left") { + cldWPos_ = WeeksSide::LEFT; + } else if (config_[kCalendarPlaceholder]["weeks-pos"].asString() == "right") { + cldWPos_ = WeeksSide::RIGHT; + } + } + if (config_[kCalendarPlaceholder]["format"]["months"].isString()) + fmtMap_.insert({0, config_[kCalendarPlaceholder]["format"]["months"].asString()}); + else + fmtMap_.insert({0, "{}"}); + if (config_[kCalendarPlaceholder]["format"]["days"].isString()) + fmtMap_.insert({2, config_[kCalendarPlaceholder]["format"]["days"].asString()}); + else + fmtMap_.insert({2, "{}"}); + if (config_[kCalendarPlaceholder]["format"]["weeks"].isString() && + cldWPos_ != WeeksSide::HIDDEN) { + fmtMap_.insert( + {4, std::regex_replace(config_[kCalendarPlaceholder]["format"]["weeks"].asString(), + std::regex("\\{\\}"), + (first_day_of_week() == date::Monday) ? "{:%W}" : "{:%U}")}); + Glib::ustring tmp{std::regex_replace(fmtMap_[4], std::regex("]+>|\\{.*\\}"), "")}; + cldWnLen_ += tmp.size(); + } else { + if (cldWPos_ != WeeksSide::HIDDEN) + fmtMap_.insert({4, (first_day_of_week() == date::Monday) ? "{:%W}" : "{:%U}"}); + else + cldWnLen_ = 0; + } + if (config_[kCalendarPlaceholder]["format"]["weekdays"].isString()) + fmtMap_.insert({1, config_[kCalendarPlaceholder]["format"]["weekdays"].asString()}); + else + fmtMap_.insert({1, "{}"}); + if (config_[kCalendarPlaceholder]["format"]["today"].isString()) { + fmtMap_.insert({3, config_[kCalendarPlaceholder]["format"]["today"].asString()}); + cldBaseDay_ = + date::year_month_day{date::floor(std::chrono::system_clock::now())}.day(); + } else + fmtMap_.insert({3, "{}"}); + if (config_[kCalendarPlaceholder]["mode"].isString()) { + const std::string cfgMode{(config_[kCalendarPlaceholder]["mode"].isString()) + ? config_[kCalendarPlaceholder]["mode"].asString() + : "month"}; + const std::map monthModes{{"month", CldMode::MONTH}, + {"year", CldMode::YEAR}}; + if (monthModes.find(cfgMode) != monthModes.end()) + cldMode_ = monthModes.at(cfgMode); + else + spdlog::warn( + "Clock calendar configuration \"mode\"\"\" \"{0}\" is not recognized. Mode = \"month\" " + "is using instead", + cfgMode); + } + if (config_[kCalendarPlaceholder]["mode-mon-col"].isInt()) { + cldMonCols_ = config_[kCalendarPlaceholder]["mode-mon-col"].asInt(); + if (cldMonCols_ == 0u || 12 % cldMonCols_ != 0u) { + cldMonCols_ = 3u; + spdlog::warn( + "Clock calendar configuration \"mode-mon-col\" = {0} must be one of [1, 2, 3, 4, 6, " + "12]. Value 3 is using instead", + cldMonCols_); + } + } else + cldMonCols_ = 1; + if (config_[kCalendarPlaceholder]["on-scroll"].isInt()) { + cldShift_ = date::months{config_[kCalendarPlaceholder]["on-scroll"].asInt()}; + event_box_.add_events(Gdk::LEAVE_NOTIFY_MASK); + event_box_.signal_leave_notify_event().connect([this](GdkEventCrossing*) { + cldCurrShift_ = date::months{0}; + return false; + }); } } - if (config_["locale"].isString()) { + if (config_["locale"].isString()) locale_ = std::locale(config_["locale"].asString()); - } else { + else locale_ = std::locale(""); - } thread_ = [this] { dp.emit(); @@ -94,18 +165,25 @@ bool waybar::modules::Clock::is_timezone_fixed() { } auto waybar::modules::Clock::update() -> void { - auto time_zone = current_timezone(); + const auto* time_zone = current_timezone(); auto now = std::chrono::system_clock::now(); - waybar_time wtime = {locale_, date::make_zoned(time_zone, date::floor(now) + - calendar_shift_)}; - std::string text = ""; + auto ztime = date::zoned_time{time_zone, date::floor(now)}; + + auto shifted_date = date::year_month_day{date::floor(now)} + cldCurrShift_; + if (cldCurrShift_.count()) { + shifted_date = date::year_month_day(shifted_date.year(), shifted_date.month(), date::day(1)); + } + auto now_shifted = date::sys_days{shifted_date} + (now - date::floor(now)); + auto shifted_ztime = date::zoned_time{time_zone, date::floor(now_shifted)}; + + std::string text{""}; if (!is_timezone_fixed()) { // As date dep is not fully compatible, prefer fmt tzset(); auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now)); - text = fmt::format(locale_, format_, localtime); + text = fmt::format(locale_, fmt::runtime(format_), localtime); } else { - text = fmt::format(format_, wtime); + text = fmt::format(locale_, fmt::runtime(format_), ztime); } label_.set_markup(text); @@ -114,15 +192,15 @@ auto waybar::modules::Clock::update() -> void { std::string calendar_lines{""}; std::string timezoned_time_lines{""}; if (is_calendar_in_tooltip_) { - calendar_lines = calendar_text(wtime); + calendar_lines = get_calendar(ztime, shifted_ztime); } if (is_timezoned_list_in_tooltip_) { timezoned_time_lines = timezones_text(&now); } auto tooltip_format = config_["tooltip-format"].asString(); - text = - fmt::format(tooltip_format, wtime, fmt::arg(kCalendarPlaceholder.c_str(), calendar_lines), - fmt::arg(KTimezonedTimeListPlaceholder.c_str(), timezoned_time_lines)); + text = fmt::format(locale_, fmt::runtime(tooltip_format), shifted_ztime, + fmt::arg(kCalendarPlaceholder.c_str(), calendar_lines), + fmt::arg(KTimezonedTimeListPlaceholder.c_str(), timezoned_time_lines)); label_.set_tooltip_markup(text); } } @@ -131,172 +209,251 @@ auto waybar::modules::Clock::update() -> void { ALabel::update(); } -bool waybar::modules::Clock::handleScroll(GdkEventScroll* e) { - // defer to user commands if set - if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { - return AModule::handleScroll(e); - } - - auto dir = AModule::getScrollDir(e); - - // Shift calendar date - if (calendar_shift_init_.count() != 0) { - if (dir == SCROLL_DIR::UP) - calendar_shift_ += calendar_shift_init_; - else - calendar_shift_ -= calendar_shift_init_; - } else { - // Change time zone - if (dir != SCROLL_DIR::UP && dir != SCROLL_DIR::DOWN) { - return true; - } - if (time_zones_.size() == 1) { - return true; - } - - auto nr_zones = time_zones_.size(); - if (dir == SCROLL_DIR::UP) { - size_t new_idx = current_time_zone_idx_ + 1; - current_time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx; - } else { - current_time_zone_idx_ = - current_time_zone_idx_ == 0 ? nr_zones - 1 : current_time_zone_idx_ - 1; - } - } - - update(); - return true; -} - -auto waybar::modules::Clock::calendar_text(const waybar_time& wtime) -> std::string { - const auto daypoint = date::floor(wtime.ztime.get_local_time()); - const auto ymd{date::year_month_day{daypoint}}; - - if (calendar_cached_ymd_ == ymd) { - return calendar_cached_text_; - } - - const auto curr_day{(calendar_shift_init_.count() != 0 && calendar_shift_.count() != 0) - ? date::day{0} - : ymd.day()}; - const date::year_month ym{ymd.year(), ymd.month()}; - const auto weeks_format{config_["format-calendar-weeks"].isString() - ? config_["format-calendar-weeks"].asString() - : ""}; - - std::stringstream os; - - const date::weekday first_week_day = first_day_of_week(); - - enum class WeeksPlacement { - LEFT, - RIGHT, - HIDDEN, - }; - WeeksPlacement weeks_pos = WeeksPlacement::HIDDEN; - - if (config_["calendar-weeks-pos"].isString()) { - if (config_["calendar-weeks-pos"].asString() == "left") { - weeks_pos = WeeksPlacement::LEFT; - // Add paddings before the header - os << std::string(4, ' '); - } else if (config_["calendar-weeks-pos"].asString() == "right") { - weeks_pos = WeeksPlacement::RIGHT; - } - } - - weekdays_header(first_week_day, os); - - // First week prefixed with spaces if needed. - auto first_month_day = date::weekday(ym / 1); - int empty_days = (first_week_day - first_month_day).count() + 1; - date::sys_days last_week_day{static_cast(ym / 1) + date::days{7 - empty_days}}; - - if (first_week_day == date::Monday) { - last_week_day -= date::days{1}; - } - /* Print weeknumber on the left for the first row*/ - if (weeks_pos == WeeksPlacement::LEFT) { - os << fmt::format(weeks_format, date::format("%U", last_week_day)) << ' '; - last_week_day += date::weeks{1}; - } - - if (empty_days > 0) { - os << std::string(empty_days * 3 - 1, ' '); - } - const auto last_day = (ym / date::literals::last).day(); - auto weekday = first_month_day; - for (auto d = date::day(1); d <= last_day; ++d, ++weekday) { - if (weekday != first_week_day) { - os << ' '; - } else if (unsigned(d) != 1) { - last_week_day -= date::days{1}; - if (weeks_pos == WeeksPlacement::RIGHT) { - os << ' '; - os << fmt::format(weeks_format, date::format("%U", last_week_day)); - } - - os << "\n"; - - if (weeks_pos == WeeksPlacement::LEFT) { - os << fmt::format(weeks_format, date::format("%U", last_week_day)); - os << ' '; - } - last_week_day += date::weeks{1} + date::days{1}; - } - if (d == curr_day) { - if (config_["today-format"].isString()) { - auto today_format = config_["today-format"].asString(); - os << fmt::format(today_format, date::format("%e", d)); - } else { - os << "" << date::format("%e", d) << ""; - } - } else if (config_["format-calendar"].isString()) { - os << fmt::format(config_["format-calendar"].asString(), date::format("%e", d)); - } else { - os << date::format("%e", d); - } - /*Print weeks on the right when the endings with spaces*/ - if (weeks_pos == WeeksPlacement::RIGHT && d == last_day) { - last_week_day -= date::days{1}; - empty_days = 6 - (weekday - first_week_day).count(); - os << std::string(empty_days * 3 + 1, ' '); - os << fmt::format(weeks_format, date::format("%U", last_week_day)); - last_week_day += date::days{1}; - } - } - - auto result = os.str(); - calendar_cached_ymd_ = ymd; - calendar_cached_text_ = result; - return result; -} - -auto waybar::modules::Clock::weekdays_header(const date::weekday& first_week_day, std::ostream& os) - -> void { - std::stringstream res; - auto wd = first_week_day; - do { - if (wd != first_week_day) { - res << ' '; - } - Glib::ustring wd_ustring(date::format(locale_, "%a", wd)); - auto clen = ustring_clen(wd_ustring); - auto wd_len = wd_ustring.length(); - while (clen > 2) { - wd_ustring = wd_ustring.substr(0, wd_len - 1); - wd_len--; - clen = ustring_clen(wd_ustring); - } - const std::string pad(2 - clen, ' '); - res << pad << wd_ustring; - } while (++wd != first_week_day); - res << "\n"; - - if (config_["format-calendar-weekdays"].isString()) { - os << fmt::format(config_["format-calendar-weekdays"].asString(), res.str()); +auto waybar::modules::Clock::doAction(const std::string& name) -> void { + if ((actionMap_[name])) { + (this->*actionMap_[name])(); + update(); } else - os << res.str(); + spdlog::error("Clock. Unsupported action \"{0}\"", name); +} + +// The number of weeks in calendar month layout plus 1 more for calendar titles +unsigned cldRowsInMonth(date::year_month const ym, date::weekday const firstdow) { + using namespace date; + return static_cast( + ceil((weekday{ym / 1} - firstdow) + ((ym / last).day() - day{0})).count()) + + 2; +} + +auto cldGetWeekForLine(date::year_month const ym, date::weekday const firstdow, unsigned const line) + -> const date::year_month_weekday { + unsigned index = line - 2; + auto sd = date::sys_days{ym / 1}; + if (date::weekday{sd} == firstdow) ++index; + auto ymdw = ym / firstdow[index]; + return ymdw; +} + +auto getCalendarLine(date::year_month_day const currDate, date::year_month const ym, + unsigned const line, date::weekday const firstdow, + const std::locale* const locale_) -> std::string { + using namespace date::literals; + std::ostringstream res; + + switch (line) { + case 0: { + // Output month and year title + res << date::format(*locale_, "%B %Y", ym); + break; + } + case 1: { + // Output weekday names title + auto wd{firstdow}; + do { + Glib::ustring wd_ustring{date::format(*locale_, "%a", wd)}; + auto clen{ustring_clen(wd_ustring)}; + auto wd_len{wd_ustring.length()}; + while (clen > 2) { + wd_ustring = wd_ustring.substr(0, wd_len - 1); + --wd_len; + clen = ustring_clen(wd_ustring); + } + const std::string pad(2 - clen, ' '); + + if (wd != firstdow) res << ' '; + + res << pad << wd_ustring; + } while (++wd != firstdow); + break; + } + case 2: { + // Output first week prefixed with spaces if necessary + auto wd = date::weekday{ym / 1}; + res << std::string(static_cast((wd - firstdow).count()) * 3, ' '); + + if (currDate.year() != ym.year() || currDate.month() != ym.month() || currDate != ym / 1_d) + res << date::format("%e", 1_d); + else + res << "{today}"; + + auto d = 2_d; + + while (++wd != firstdow) { + if (currDate.year() != ym.year() || currDate.month() != ym.month() || currDate != ym / d) + res << date::format(" %e", d); + else + res << " {today}"; + + ++d; + } + break; + } + default: { + // Output a non-first week: + auto ymdw{cldGetWeekForLine(ym, firstdow, line)}; + if (ymdw.ok()) { + auto d = date::year_month_day{ymdw}.day(); + auto const e = (ym / last).day(); + auto wd = firstdow; + + if (currDate.year() != ym.year() || currDate.month() != ym.month() || currDate != ym / d) + res << date::format("%e", d); + else + res << "{today}"; + + while (++wd != firstdow && ++d <= e) { + if (currDate.year() != ym.year() || currDate.month() != ym.month() || currDate != ym / d) + res << date::format(" %e", d); + else + res << " {today}"; + } + // Append row with spaces if the week did not complete + res << std::string(static_cast((firstdow - wd).count()) * 3, ' '); + } + break; + } + } + + return res.str(); +} + +auto waybar::modules::Clock::get_calendar(const date::zoned_seconds& now, + const date::zoned_seconds& wtime) -> std::string { + auto daypoint = date::floor(wtime.get_local_time()); + const auto ymd{date::year_month_day{daypoint}}; + const auto ym{ymd.year() / ymd.month()}; + const auto y{ymd.year()}; + const auto d{ymd.day()}; + const auto firstdow = first_day_of_week(); + const auto maxRows{12 / cldMonCols_}; + std::ostringstream os; + std::ostringstream tmp; + // get currdate + daypoint = date::floor(now.get_local_time()); + const auto currDate{date::year_month_day{daypoint}}; + + if (cldMode_ == CldMode::YEAR) { + if (y / date::month{1} / 1 == cldYearShift_) + if (d == cldBaseDay_ || (uint)cldBaseDay_ == 0u) + return cldYearCached_; + else + cldBaseDay_ = d; + else + cldYearShift_ = y / date::month{1} / 1; + } + if (cldMode_ == CldMode::MONTH) { + if (ym == cldMonShift_) + if (d == cldBaseDay_ || (uint)cldBaseDay_ == 0u) + return cldMonCached_; + else + cldBaseDay_ = d; + else + cldMonShift_ = ym; + } + + // Compute number of lines needed for each calendar month + unsigned ml[12]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + + for (auto& m : ml) { + if (cldMode_ == CldMode::YEAR || m == static_cast(ymd.month())) + m = cldRowsInMonth(y / date::month{m}, firstdow); + else + m = 0u; + } + for (auto row{0u}; row < maxRows; ++row) { + const auto lines = *std::max_element(std::begin(ml) + (row * cldMonCols_), + std::begin(ml) + ((row + 1) * cldMonCols_)); + for (auto line{0u}; line < lines; ++line) { + for (auto col{0u}; col < cldMonCols_; ++col) { + const auto mon{date::month{row * cldMonCols_ + col + 1}}; + if (cldMode_ == CldMode::YEAR || y / mon == ym) { + date::year_month ymTmp{y / mon}; + if (col != 0 && cldMode_ == CldMode::YEAR) os << " "; + + // Week numbers on the left + if (cldWPos_ == WeeksSide::LEFT && line > 0) { + if (line > 1) { + if (line < ml[static_cast(ymTmp.month()) - 1u]) + os << fmt::format(fmt::runtime(fmtMap_[4]), + (line == 2) + ? date::sys_days{ymTmp / 1} + : date::sys_days{cldGetWeekForLine(ymTmp, firstdow, line)}) + << ' '; + else + os << std::string(cldWnLen_, ' '); + } + } + + os << fmt::format( + fmt::runtime((cldWPos_ != WeeksSide::LEFT || line == 0) ? "{:<{}}" : "{:>{}}"), + getCalendarLine(currDate, ymTmp, line, firstdow, &locale_), + (cldMonColLen_ + ((line < 2) ? cldWnLen_ : 0))); + + // Week numbers on the right + if (cldWPos_ == WeeksSide ::RIGHT && line > 0) { + if (line > 1) { + if (line < ml[static_cast(ymTmp.month()) - 1u]) + os << ' ' + << fmt::format(fmt::runtime(fmtMap_[4]), + (line == 2) + ? date::sys_days{ymTmp / 1} + : date::sys_days{cldGetWeekForLine(ymTmp, firstdow, line)}); + else + os << std::string(cldWnLen_, ' '); + } + } + } + } + + // Apply user formats to calendar + if (line < 2) + tmp << fmt::format(fmt::runtime(fmtMap_[line]), os.str()); + else + tmp << os.str(); + // Clear ostringstream + std::ostringstream().swap(os); + if (line + 1u != lines || (row + 1u != maxRows && cldMode_ == CldMode::YEAR)) tmp << '\n'; + } + if (row + 1u != maxRows && cldMode_ == CldMode::YEAR) tmp << '\n'; + } + + os << fmt::format( // Apply days format + fmt::runtime(fmt::format(fmt::runtime(fmtMap_[2]), tmp.str())), + // Apply today format + fmt::arg("today", fmt::format(fmt::runtime(fmtMap_[3]), date::format("%e", ymd.day())))); + + if (cldMode_ == CldMode::YEAR) + cldYearCached_ = os.str(); + else + cldMonCached_ = os.str(); + + return os.str(); +} + +/*Clock actions*/ +void waybar::modules::Clock::cldModeSwitch() { + cldMode_ = (cldMode_ == CldMode::YEAR) ? CldMode::MONTH : CldMode::YEAR; +} +void waybar::modules::Clock::cldShift_up() { + cldCurrShift_ += ((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_; +} +void waybar::modules::Clock::cldShift_down() { + cldCurrShift_ -= ((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_; +} +void waybar::modules::Clock::tz_up() { + auto nr_zones = time_zones_.size(); + + if (nr_zones == 1) return; + + size_t new_idx = current_time_zone_idx_ + 1; + current_time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx; +} +void waybar::modules::Clock::tz_down() { + auto nr_zones = time_zones_.size(); + + if (nr_zones == 1) return; + + current_time_zone_idx_ = current_time_zone_idx_ == 0 ? nr_zones - 1 : current_time_zone_idx_ - 1; } auto waybar::modules::Clock::timezones_text(std::chrono::system_clock::time_point* now) @@ -305,7 +462,6 @@ auto waybar::modules::Clock::timezones_text(std::chrono::system_clock::time_poin return ""; } std::stringstream os; - waybar_time wtime; for (size_t time_zone_idx = 0; time_zone_idx < time_zones_.size(); ++time_zone_idx) { if (static_cast(time_zone_idx) == current_time_zone_idx_) { continue; @@ -314,8 +470,8 @@ auto waybar::modules::Clock::timezones_text(std::chrono::system_clock::time_poin if (!timezone) { timezone = date::current_zone(); } - wtime = {locale_, date::make_zoned(timezone, date::floor(*now))}; - os << fmt::format(format_, wtime) << "\n"; + auto ztime = date::zoned_time{timezone, date::floor(*now)}; + os << fmt::format(locale_, fmt::runtime(format_), ztime) << '\n'; } return os.str(); } diff --git a/src/modules/cpu/common.cpp b/src/modules/cpu/common.cpp index cdbbc3d..8fedf84 100644 --- a/src/modules/cpu/common.cpp +++ b/src/modules/cpu/common.cpp @@ -39,7 +39,6 @@ auto waybar::modules::Cpu::update() -> void { auto icons = std::vector{state}; fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("load", cpu_load)); - store.push_back(fmt::arg("load", cpu_load)); store.push_back(fmt::arg("usage", total_usage)); store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); store.push_back(fmt::arg("max_frequency", max_frequency)); diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 23dba38..5a246af 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -88,6 +88,7 @@ void waybar::modules::Custom::continuousWorker() { output_ = {0, output}; dp.emit(); } + free(buff); }; } @@ -126,7 +127,7 @@ auto waybar::modules::Custom::update() -> void { } else { parseOutputRaw(); } - auto str = fmt::format(format_, text_, fmt::arg("alt", alt_), + 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()) { @@ -209,8 +210,8 @@ void waybar::modules::Custom::parseOutputJson() { class_.push_back(c.asString()); } } - if (!parsed["percentage"].asString().empty() && parsed["percentage"].isUInt()) { - percentage_ = parsed["percentage"].asUInt(); + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { + percentage_ = (int)lround(parsed["percentage"].asFloat()); } else { percentage_ = 0; } diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index 5578dc2..eb4d902 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -58,11 +58,11 @@ auto waybar::modules::Disk::update() -> void { event_box_.hide(); } else { event_box_.show(); - label_.set_markup( - fmt::format(format, stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), - fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), - fmt::arg("used", used), fmt::arg("percentage_used", percentage_used), - fmt::arg("total", total), fmt::arg("path", path_))); + label_.set_markup(fmt::format( + fmt::runtime(format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), + fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used), + fmt::arg("percentage_used", percentage_used), fmt::arg("total", total), + fmt::arg("path", path_))); } if (tooltipEnabled()) { @@ -70,11 +70,11 @@ auto waybar::modules::Disk::update() -> void { if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text( - fmt::format(tooltip_format, stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), - fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), - fmt::arg("used", used), fmt::arg("percentage_used", percentage_used), - fmt::arg("total", total), fmt::arg("path", path_))); + label_.set_tooltip_text(fmt::format( + fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), + fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used), + fmt::arg("percentage_used", percentage_used), fmt::arg("total", total), + fmt::arg("path", path_))); } // Call parent update ALabel::update(); diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp new file mode 100644 index 0000000..7faa5c5 --- /dev/null +++ b/src/modules/dwl/tags.cpp @@ -0,0 +1,198 @@ +#include "modules/dwl/tags.hpp" + +#include +#include +#include +#include + +#include + +#include "client.hpp" +#include "dwl-ipc-unstable-v2-client-protocol.h" + +#define TAG_INACTIVE 0 +#define TAG_ACTIVE 1 +#define TAG_URGENT 2 + +namespace waybar::modules::dwl { + +/* dwl stuff */ +wl_array tags, layouts; + +static uint num_tags = 0; + +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) { + // 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) { + static_cast(data)->handle_view_tags(tag, state, clients, focused); + + num_tags = (state & ZDWL_IPC_OUTPUT_V2_TAG_STATE_ACTIVE) ? num_tags | (1 << tag) + : num_tags & ~(1 << tag); +} + +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) { + // Intentionally empty +} + +void dwl_frame(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { + // Intentionally empty +} + +static void set_layout(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t layout) { + // Intentionally empty +} + +static void appid(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *appid){ + // Intentionally empty +}; + +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)); + } + if (std::strcmp(interface, wl_seat_interface.name) == 0) { + version = std::min(version, 1); + static_cast(data)->seat_ = static_cast( + wl_registry_bind(registry, name, &wl_seat_interface, version)); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { + /* Ignore event */ +} + +static const wl_registry_listener registry_listener_impl = {.global = handle_global, + .global_remove = handle_global_remove}; + +Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &config) + : waybar::AModule(config, "tags", id, false, false), + status_manager_{nullptr}, + seat_{nullptr}, + bar_(bar), + box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + 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); + + if (!status_manager_) { + spdlog::error("dwl_status_manager_v2 not advertised"); + return; + } + + if (!seat_) { + spdlog::error("wl_seat not advertised"); + } + + box_.set_name("tags"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + + // Default to 9 tags, cap at 32 + const uint32_t num_tags = + config["num-tags"].isUInt() ? std::min(32, config_["num-tags"].asUInt()) : 9; + + std::vector tag_labels(num_tags); + for (uint32_t tag = 0; tag < num_tags; ++tag) { + tag_labels[tag] = std::to_string(tag + 1); + } + const Json::Value custom_labels = config["tag-labels"]; + if (custom_labels.isArray() && !custom_labels.empty()) { + for (uint32_t tag = 0; tag < std::min(num_tags, custom_labels.size()); ++tag) { + tag_labels[tag] = custom_labels[tag].asString(); + } + } + + uint32_t i = 1; + for (const auto &tag_label : tag_labels) { + Gtk::Button &button = buttons_.emplace_back(tag_label); + button.set_relief(Gtk::RELIEF_NONE); + box_.pack_start(button, false, false, 0); + if (!config_["disable-click"].asBool()) { + button.signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &Tags::handle_primary_clicked), i)); + button.signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), i)); + } + button.show(); + i <<= 1; + } + + struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + 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_); + status_manager_ = nullptr; +} + +Tags::~Tags() { + if (status_manager_) { + zdwl_ipc_manager_v2_destroy(status_manager_); + } +} + +void Tags::handle_primary_clicked(uint32_t tag) { + if (!output_status_) return; + + zdwl_ipc_output_v2_set_tags(output_status_, tag, 1); +} + +bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) { + if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) { + if (!output_status_) return true; + zdwl_ipc_output_v2_set_tags(output_status_, num_tags ^ tag, 0); + } + return true; +} + +void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused) { + // First clear all occupied state + auto &button = buttons_[tag]; + if (clients) { + button.get_style_context()->add_class("occupied"); + } else { + button.get_style_context()->remove_class("occupied"); + } + + if (state & TAG_ACTIVE) { + button.get_style_context()->add_class("focused"); + } else { + button.get_style_context()->remove_class("focused"); + } + + if (state & TAG_URGENT) { + button.get_style_context()->add_class("urgent"); + } else { + button.get_style_context()->remove_class("urgent"); + } +} + +} /* namespace waybar::modules::dwl */ diff --git a/src/modules/gamemode.cpp b/src/modules/gamemode.cpp index 7129297..811f13c 100644 --- a/src/modules/gamemode.cpp +++ b/src/modules/gamemode.cpp @@ -16,9 +16,9 @@ #include "glibmm/ustring.h" #include "glibmm/variant.h" #include "glibmm/varianttype.h" -#include "gtkmm/icontheme.h" #include "gtkmm/label.h" #include "gtkmm/tooltip.h" +#include "util/gtk_icon.hpp" namespace waybar::modules { Gamemode::Gamemode(const std::string& id, const Json::Value& config) @@ -55,7 +55,7 @@ Gamemode::Gamemode(const std::string& id, const Json::Value& config) } box_.set_spacing(iconSpacing); - // Wether to use icon or not + // Whether to use icon or not if (config_["use-icon"].isBool()) { useIcon = config_["use-icon"].asBool(); } @@ -213,18 +213,18 @@ auto Gamemode::update() -> void { // Tooltip if (tooltip) { - std::string text = fmt::format(tooltip_format, fmt::arg("count", gameCount)); + std::string text = fmt::format(fmt::runtime(tooltip_format), fmt::arg("count", gameCount)); box_.set_tooltip_text(text); } // Label format - std::string str = - fmt::format(showAltText ? format_alt : format, fmt::arg("glyph", useIcon ? "" : glyph), - fmt::arg("count", gameCount > 0 ? std::to_string(gameCount) : "")); + std::string str = fmt::format(fmt::runtime(showAltText ? format_alt : format), + fmt::arg("glyph", useIcon ? "" : glyph), + fmt::arg("count", gameCount > 0 ? std::to_string(gameCount) : "")); label_.set_markup(str); if (useIcon) { - if (!Gtk::IconTheme::get_default()->has_icon(iconName)) { + if (!DefaultGtkIconThemeWrapper::has_icon(iconName)) { iconName = DEFAULT_ICON_NAME; } icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 76c071c..79bc637 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -133,6 +133,8 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) { std::string IPC::getSocket1Reply(const std::string& rq) { // basically hyprctl + struct addrinfo ai_hints; + struct addrinfo* ai_res = NULL; const auto SERVERSOCKET = socket(AF_UNIX, SOCK_STREAM, 0); if (SERVERSOCKET < 0) { @@ -140,9 +142,11 @@ std::string IPC::getSocket1Reply(const std::string& rq) { return ""; } - const auto SERVER = gethostbyname("localhost"); + memset(&ai_hints, 0, sizeof(struct addrinfo)); + ai_hints.ai_family = AF_UNSPEC; + ai_hints.ai_socktype = SOCK_STREAM; - if (!SERVER) { + if (getaddrinfo("localhost", NULL, &ai_hints, &ai_res) != 0) { spdlog::error("Hyprland IPC: Couldn't get host (2)"); return ""; } @@ -177,17 +181,25 @@ std::string IPC::getSocket1Reply(const std::string& rq) { } char buffer[8192] = {0}; + std::string response; - sizeWritten = read(SERVERSOCKET, buffer, 8192); + do { + sizeWritten = read(SERVERSOCKET, buffer, 8192); - if (sizeWritten < 0) { - spdlog::error("Hyprland IPC: Couldn't read (5)"); - return ""; - } + if (sizeWritten < 0) { + spdlog::error("Hyprland IPC: Couldn't read (5)"); + close(SERVERSOCKET); + return ""; + } + response.append(buffer, sizeWritten); + } while (sizeWritten == 8192); close(SERVERSOCKET); + return response; +} - return std::string(buffer); +Json::Value IPC::getSocket1JsonReply(const std::string& rq) { + return parser_.parse(getSocket1Reply("j/" + rq)); } } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 13e30ec..aa22a48 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -6,7 +6,7 @@ #include -#include "modules/hyprland/backend.hpp" +#include "util/string.hpp" namespace waybar::modules::hyprland { @@ -22,7 +22,7 @@ Language::Language(const std::string& id, const Bar& bar, const Json::Value& con initLanguage(); label_.hide(); - ALabel::update(); + update(); // register for hyprland ipc gIPC->registerForIPC("activelayout", this); @@ -37,9 +37,20 @@ Language::~Language() { auto Language::update() -> void { std::lock_guard lg(mutex_); + std::string layoutName = std::string{}; + if (config_.isMember("format-" + layout_.short_description)) { + const auto propName = "format-" + layout_.short_description; + layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString()); + } else { + layoutName = trim(fmt::format(fmt::runtime(format_), fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); + } + if (!format_.empty()) { label_.show(); - label_.set_markup(layoutName_); + label_.set_markup(layoutName); } else { label_.hide(); } @@ -49,27 +60,15 @@ auto Language::update() -> void { void Language::onEvent(const std::string& ev) { std::lock_guard lg(mutex_); - auto layoutName = ev.substr(ev.find_last_of(',') + 1); - auto keebName = ev.substr(0, ev.find_last_of(',')); - keebName = keebName.substr(keebName.find_first_of('>') + 2); + 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); - if (config_.isMember("keyboard-name") && keebName != config_["keyboard-name"].asString()) + if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) return; // ignore - const auto BRIEFNAME = getShortFrom(layoutName); - - if (config_.isMember("format-" + BRIEFNAME)) { - const auto PROPNAME = "format-" + BRIEFNAME; - layoutName = fmt::format(format_, config_[PROPNAME].asString()); - } else { - layoutName = fmt::format(format_, layoutName); - } - layoutName = waybar::util::sanitize_string(layoutName); - if (layoutName == layoutName_) return; - - layoutName_ = layoutName; + layout_ = getLayout(layoutName); spdlog::debug("hyprland language onevent with {}", layoutName); @@ -77,20 +76,22 @@ void Language::onEvent(const std::string& ev) { } void Language::initLanguage() { - const auto INPUTDEVICES = gIPC->getSocket1Reply("devices"); + const auto inputDevices = gIPC->getSocket1Reply("devices"); - if (!config_.isMember("keyboard-name")) return; - - const auto KEEBNAME = config_["keyboard-name"].asString(); + const auto kbName = config_["keyboard-name"].asString(); try { - auto searcher = INPUTDEVICES.substr(INPUTDEVICES.find(KEEBNAME) + KEEBNAME.length()); - searcher = searcher.substr(searcher.find("keymap:") + 7); + auto searcher = kbName.empty() + ? inputDevices + : inputDevices.substr(inputDevices.find(kbName) + kbName.length()); + searcher = searcher.substr(searcher.find("keymap:") + 8); searcher = searcher.substr(0, searcher.find_first_of("\n\t")); - layoutName_ = searcher; + searcher = waybar::util::sanitize_string(searcher); - spdlog::debug("hyprland language initLanguage found {}", layoutName_); + layout_ = getLayout(searcher); + + spdlog::debug("hyprland language initLanguage found {}", layout_.full_name); dp.emit(); @@ -99,11 +100,10 @@ void Language::initLanguage() { } } -std::string Language::getShortFrom(const std::string& fullName) { +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); - std::string foundName = ""; rxkb_layout* layout = rxkb_layout_first(CONTEXT); while (layout) { std::string nameOfLayout = rxkb_layout_get_description(layout); @@ -113,16 +113,26 @@ std::string Language::getShortFrom(const std::string& fullName) { continue; } - std::string briefName = rxkb_layout_get_brief(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_); + + auto short_description_ = rxkb_layout_get_brief(layout); + std::string short_description = + short_description_ == nullptr ? "" : std::string(short_description_); + + Layout info = Layout{nameOfLayout, name, variant, short_description}; rxkb_context_unref(CONTEXT); - return briefName; + return info; } rxkb_context_unref(CONTEXT); - return ""; + spdlog::debug("hyprland language didn't find matching layout"); + + return Layout{"", "", "", ""}; } } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp new file mode 100644 index 0000000..22acbf3 --- /dev/null +++ b/src/modules/hyprland/submap.cpp @@ -0,0 +1,63 @@ +#include "modules/hyprland/submap.hpp" + +#include + +#include + +namespace waybar::modules::hyprland { + +Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "submap", id, "{}", 0, true), bar_(bar) { + modulesReady = true; + + if (!gIPC.get()) { + gIPC = std::make_unique(); + } + + label_.hide(); + ALabel::update(); + + // register for hyprland ipc + gIPC->registerForIPC("submap", this); + dp.emit(); +} + +Submap::~Submap() { + gIPC->unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lg(mutex_); +} + +auto Submap::update() -> void { + std::lock_guard lg(mutex_); + + if (submap_.empty()) { + event_box_.hide(); + } else { + label_.set_markup(fmt::format(fmt::runtime(format_), submap_)); + if (tooltipEnabled()) { + label_.set_tooltip_text(submap_); + } + event_box_.show(); + } + // Call parent update + ALabel::update(); +} + +void Submap::onEvent(const std::string& ev) { + std::lock_guard lg(mutex_); + + if (ev.find("submap") == std::string::npos) { + return; + } + + auto submapName = ev.substr(ev.find_last_of('>') + 1); + submapName = waybar::util::sanitize_string(submapName); + + submap_ = submapName; + + spdlog::debug("hyprland submap onevent with {}", submap_); + + dp.emit(); +} +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index d3d06cc..cb820bc 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -2,30 +2,35 @@ #include +#include #include #include +#include #include "modules/hyprland/backend.hpp" -#include "util/command.hpp" #include "util/json.hpp" -#include "util/rewrite_title.hpp" +#include "util/rewrite_string.hpp" namespace waybar::modules::hyprland { Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "window", id, "{}", 0, true), bar_(bar) { modulesReady = true; - separate_outputs = config["separate-outputs"].as(); + separate_outputs = config["separate-outputs"].asBool(); if (!gIPC.get()) { gIPC = std::make_unique(); } - label_.hide(); - ALabel::update(); + queryActiveWorkspace(); + update(); // register for hyprland ipc gIPC->registerForIPC("activewindow", this); + gIPC->registerForIPC("closewindow", this); + gIPC->registerForIPC("movewindow", this); + gIPC->registerForIPC("changefloatingmode", this); + gIPC->registerForIPC("fullscreen", this); } Window::~Window() { @@ -38,63 +43,136 @@ auto Window::update() -> void { // fix ampersands std::lock_guard lg(mutex_); + std::string window_name = waybar::util::sanitize_string(workspace_.last_window_title); + + if (window_name != last_title_) { + if (window_name.empty()) { + label_.get_style_context()->add_class("empty"); + } else { + label_.get_style_context()->remove_class("empty"); + } + last_title_ = window_name; + } + if (!format_.empty()) { label_.show(); - label_.set_markup( - fmt::format(format_, waybar::util::rewriteTitle(lastView, config_["rewrite"]))); + label_.set_markup(fmt::format(fmt::runtime(format_), + waybar::util::rewriteString(window_name, config_["rewrite"]))); } else { label_.hide(); } + setClass("empty", workspace_.windows == 0); + setClass("solo", solo_); + setClass("fullscreen", fullscreen_); + setClass("floating", all_floating_); + + 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 (!solo_class_.empty() && solo_class_ != last_solo_class_) { + bar_.window.get_style_context()->add_class(solo_class_); + spdlog::trace("Adding solo class: {}", solo_class_); + } + last_solo_class_ = solo_class_; + ALabel::update(); } -int Window::getActiveWorkspaceID(std::string monitorName) { - auto cmd = waybar::util::command::exec("hyprctl monitors -j"); - assert(cmd.exit_code == 0); - Json::Value json = parser_.parse(cmd.out); - assert(json.isArray()); - auto monitor = std::find_if(json.begin(), json.end(), - [&](Json::Value monitor) { return monitor["name"] == monitorName; }); - if (monitor == std::end(json)) { - return 0; - } - return (*monitor)["activeWorkspace"]["id"].as(); +auto Window::getActiveWorkspace() -> Workspace { + const auto workspace = gIPC->getSocket1JsonReply("activeworkspace"); + assert(workspace.isObject()); + return Workspace::parse(workspace); } -std::string Window::getLastWindowTitle(int workspaceID) { - auto cmd = waybar::util::command::exec("hyprctl workspaces -j"); - assert(cmd.exit_code == 0); - Json::Value json = parser_.parse(cmd.out); - assert(json.isArray()); - auto workspace = std::find_if(json.begin(), json.end(), [&](Json::Value workspace) { - return workspace["id"].as() == workspaceID; - }); - - if (workspace == std::end(json)) { - return ""; +auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { + const auto monitors = gIPC->getSocket1JsonReply("monitors"); + assert(monitors.isArray()); + auto monitor = std::find_if(monitors.begin(), monitors.end(), + [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + if (monitor == std::end(monitors)) { + spdlog::warn("Monitor not found: {}", monitorName); + return Workspace{-1, 0, "", ""}; + } + const int id = (*monitor)["activeWorkspace"]["id"].asInt(); + + const auto workspaces = gIPC->getSocket1JsonReply("workspaces"); + assert(workspaces.isArray()); + auto workspace = std::find_if(monitors.begin(), monitors.end(), + [&](Json::Value workspace) { return workspace["id"] == id; }); + if (workspace == std::end(monitors)) { + spdlog::warn("No workspace with id {}", id); + return Workspace{-1, 0, "", ""}; + } + return Workspace::parse(*workspace); +} + +auto Window::Workspace::parse(const Json::Value& value) -> Window::Workspace { + return Workspace{value["id"].asInt(), value["windows"].asInt(), value["lastwindow"].asString(), + value["lastwindowtitle"].asString()}; +} + +void Window::queryActiveWorkspace() { + std::lock_guard lg(mutex_); + + if (separate_outputs) { + workspace_ = getActiveWorkspace(this->bar_.output->name); + } else { + workspace_ = getActiveWorkspace(); + } + + if (workspace_.windows > 0) { + const auto clients = gIPC->getSocket1Reply("j/clients"); + Json::Value json = parser_.parse(clients); + assert(json.isArray()); + auto active_window = std::find_if(json.begin(), json.end(), [&](Json::Value window) { + return window["address"] == workspace_.last_window; + }); + if (active_window == std::end(json)) { + return; + } + + if (workspace_.windows == 1 && !(*active_window)["floating"].asBool()) { + solo_class_ = (*active_window)["class"].asString(); + } else { + solo_class_ = ""; + } + std::vector workspace_windows; + std::copy_if(json.begin(), json.end(), std::back_inserter(workspace_windows), + [&](Json::Value window) { + return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); + }); + solo_ = 1 == std::count_if(workspace_windows.begin(), workspace_windows.end(), + [&](Json::Value window) { return !window["floating"].asBool(); }); + all_floating_ = std::all_of(workspace_windows.begin(), workspace_windows.end(), + [&](Json::Value window) { return window["floating"].asBool(); }); + fullscreen_ = (*active_window)["fullscreen"].asBool(); + } else { + solo_class_ = ""; + solo_ = false; + all_floating_ = false; + fullscreen_ = false; } - return (*workspace)["lastwindowtitle"].as(); } void Window::onEvent(const std::string& ev) { - std::lock_guard lg(mutex_); - - std::string windowName; - if (separate_outputs) { - windowName = getLastWindowTitle(getActiveWorkspaceID(this->bar_.output->name)); - } else { - windowName = ev.substr(ev.find_first_of(',') + 1).substr(0, 256); - } - - windowName = waybar::util::sanitize_string(windowName); - - if (windowName == lastView) return; - - lastView = windowName; - - spdlog::debug("hyprland window onevent with {}", windowName); + queryActiveWorkspace(); dp.emit(); } + +void Window::setClass(const std::string& classname, bool enable) { + if (enable) { + if (!bar_.window.get_style_context()->has_class(classname)) { + bar_.window.get_style_context()->add_class(classname); + } + } else { + bar_.window.get_style_context()->remove_class(classname); + } +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp new file mode 100644 index 0000000..e169f91 --- /dev/null +++ b/src/modules/hyprland/workspaces.cpp @@ -0,0 +1,199 @@ +#include "modules/hyprland/workspaces.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace waybar::modules::hyprland { + +Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) + : AModule(config, "workspaces", id, false, false), + bar_(bar), + box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) { + Json::Value config_format = config["format"]; + + format_ = config_format.isString() ? config_format.asString() : "{id}"; + with_icon_ = format_.find("{icon}") != std::string::npos; + + if (with_icon_ && icons_map_.empty()) { + Json::Value format_icons = config["format-icons"]; + for (std::string &name : format_icons.getMemberNames()) { + icons_map_.emplace(name, format_icons[name].asString()); + } + + icons_map_.emplace("", ""); + } + + box_.set_name("workspaces"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); + modulesReady = true; + if (!gIPC.get()) { + gIPC = std::make_unique(); + } + + init(); + + gIPC->registerForIPC("workspace", this); + gIPC->registerForIPC("createworkspace", this); + gIPC->registerForIPC("destroyworkspace", this); +} + +auto Workspaces::update() -> void { + for (int &workspace_to_remove : workspaces_to_remove_) { + remove_workspace(workspace_to_remove); + } + + workspaces_to_remove_.clear(); + + for (int &workspace_to_create : workspaces_to_create_) { + create_workspace(workspace_to_create); + } + + workspaces_to_create_.clear(); + + for (std::unique_ptr &workspace : workspaces_) { + workspace->set_active(workspace->id() == active_workspace_id); + + std::string &workspace_icon = icons_map_[""]; + if (with_icon_) { + workspace_icon = workspace->select_icon(icons_map_); + } + + workspace->update(format_, workspace_icon); + } + + AModule::update(); +} + +void Workspaces::onEvent(const std::string &ev) { + std::lock_guard lock(mutex_); + std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); + std::string payload = ev.substr(eventName.size() + 2); + if (eventName == "workspace") { + std::from_chars(payload.data(), payload.data() + payload.size(), active_workspace_id); + } else if (eventName == "destroyworkspace") { + int deleted_workspace_id; + std::from_chars(payload.data(), payload.data() + payload.size(), deleted_workspace_id); + workspaces_to_remove_.push_back(deleted_workspace_id); + } else if (eventName == "createworkspace") { + int new_workspace_id; + std::from_chars(payload.data(), payload.data() + payload.size(), new_workspace_id); + workspaces_to_create_.push_back(new_workspace_id); + } + + dp.emit(); +} + +void Workspaces::create_workspace(int id) { + workspaces_.push_back(std::make_unique(id)); + Gtk::Button &new_workspace_button = workspaces_.back()->button(); + box_.pack_start(new_workspace_button, false, false); + sort_workspaces(); + new_workspace_button.show_all(); +} + +void Workspaces::remove_workspace(int id) { + auto workspace = std::find_if(workspaces_.begin(), workspaces_.end(), + [&](std::unique_ptr &x) { return x->id() == id; }); + + if (workspace == workspaces_.end()) { + spdlog::warn("Can't find workspace with id {}", id); + return; + } + + box_.remove(workspace->get()->button()); + workspaces_.erase(workspace); +} + +void Workspaces::init() { + const auto activeWorkspace = WorkspaceDto::parse(gIPC->getSocket1JsonReply("activeworkspace")); + active_workspace_id = activeWorkspace.id; + const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); + for (const Json::Value &workspace_json : workspaces_json) { + workspaces_.push_back( + std::make_unique(Workspace(WorkspaceDto::parse(workspace_json)))); + } + + for (auto &workspace : workspaces_) { + box_.pack_start(workspace->button(), false, false); + } + + sort_workspaces(); + + dp.emit(); +} + +Workspaces::~Workspaces() { + gIPC->unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lg(mutex_); +} + +WorkspaceDto WorkspaceDto::parse(const Json::Value &value) { + return WorkspaceDto{value["id"].asInt()}; +} + +Workspace::Workspace(WorkspaceDto dto) : Workspace(dto.id){}; + +Workspace::Workspace(int id) : id_(id) { + button_.set_relief(Gtk::RELIEF_NONE); + content_.set_center_widget(label_); + button_.add(content_); +}; + +void add_or_remove_class(Glib::RefPtr context, bool condition, + const std::string &class_name) { + if (condition) { + context->add_class(class_name); + } else { + context->remove_class(class_name); + } +} + +void Workspace::update(const std::string &format, const std::string &icon) { + Glib::RefPtr style_context = button_.get_style_context(); + add_or_remove_class(style_context, active(), "active"); + + label_.set_markup( + fmt::format(fmt::runtime(format), fmt::arg("id", id()), fmt::arg("icon", icon))); +} + +void Workspaces::sort_workspaces() { + std::sort(workspaces_.begin(), workspaces_.end(), + [](std::unique_ptr &lhs, std::unique_ptr &rhs) { + return lhs->id() < rhs->id(); + }); + + for (size_t i = 0; i < workspaces_.size(); ++i) { + box_.reorder_child(workspaces_[i]->button(), i); + } +} + +std::string &Workspace::select_icon(std::map &icons_map) { + if (active()) { + auto active_icon_it = icons_map.find("active"); + if (active_icon_it != icons_map.end()) { + return active_icon_it->second; + } + } + + auto named_icon_it = icons_map.find(std::to_string(id())); + if (named_icon_it != icons_map.end()) { + return named_icon_it->second; + } + + auto default_icon_it = icons_map.find("default"); + if (default_icon_it != icons_map.end()) { + return default_icon_it->second; + } + + return icons_map[""]; +} +} // namespace waybar::modules::hyprland diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index c4109b0..a5fc9ac 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -63,21 +63,15 @@ auto waybar::modules::IdleInhibitor::update() -> void { } std::string status_text = status ? "activated" : "deactivated"; - label_.set_markup(fmt::format(format_, fmt::arg("status", status_text), + label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("status", status_text), fmt::arg("icon", getIcon(0, status_text)))); label_.get_style_context()->add_class(status_text); if (tooltipEnabled()) { - label_.set_tooltip_markup( - status ? fmt::format(config_["tooltip-format-activated"].isString() - ? config_["tooltip-format-activated"].asString() - : "{status}", - fmt::arg("status", status_text), - fmt::arg("icon", getIcon(0, status_text))) - : fmt::format(config_["tooltip-format-deactivated"].isString() - ? config_["tooltip-format-deactivated"].asString() - : "{status}", - fmt::arg("status", status_text), - fmt::arg("icon", getIcon(0, status_text)))); + auto config = config_[status ? "tooltip-format-activated" : "tooltip-format-deactivated"]; + auto tooltip_format = config.isString() ? config.asString() : "{status}"; + label_.set_tooltip_markup(fmt::format(fmt::runtime(tooltip_format), + fmt::arg("status", status_text), + fmt::arg("icon", getIcon(0, status_text)))); } // Call parent update ALabel::update(); diff --git a/src/modules/image.cpp b/src/modules/image.cpp index eed19ae..843cd95 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -1,15 +1,16 @@ #include "modules/image.hpp" -#include - -waybar::modules::Image::Image(const std::string& name, const std::string& id, - const Json::Value& config) - : AModule(config, "image-" + name, id, "{}") { - event_box_.add(image_); +waybar::modules::Image::Image(const std::string& id, const Json::Value& config) + : AModule(config, "image", id), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + box_.pack_start(image_); + box_.set_name("image"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + event_box_.add(box_); dp.emit(); - path_ = config["path"].asString(); size_ = config["size"].asInt(); interval_ = config_["interval"].asInt(); @@ -41,13 +42,25 @@ void waybar::modules::Image::refresh(int sig) { auto waybar::modules::Image::update() -> void { Glib::RefPtr pixbuf; - + if (config_["path"].isString()) { + path_ = config_["path"].asString(); + } else if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString()); + parseOutputRaw(); + } else { + path_ = ""; + } if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) pixbuf = Gdk::Pixbuf::create_from_file(path_, size_, size_); else pixbuf = {}; if (pixbuf) { + if (tooltipEnabled() && !tooltip_.empty()) { + if (box_.get_tooltip_markup() != tooltip_) { + box_.set_tooltip_markup(tooltip_); + } + } image_.set(pixbuf); image_.show(); } else { @@ -57,3 +70,19 @@ auto waybar::modules::Image::update() -> void { AModule::update(); } + +void waybar::modules::Image::parseOutputRaw() { + std::istringstream output(output_.out); + std::string line; + int i = 0; + while (getline(output, line)) { + if (i == 0) { + path_ = line; + } else if (i == 1) { + tooltip_ = line; + } else { + break; + } + i++; + } +} diff --git a/src/modules/inhibitor.cpp b/src/modules/inhibitor.cpp index e4340b1..fe2a4be 100644 --- a/src/modules/inhibitor.cpp +++ b/src/modules/inhibitor.cpp @@ -118,7 +118,7 @@ auto Inhibitor::update() -> void { std::string status_text = activated() ? "activated" : "deactivated"; label_.get_style_context()->remove_class(activated() ? "deactivated" : "activated"); - label_.set_markup(fmt::format(format_, fmt::arg("status", status_text), + label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("status", status_text), fmt::arg("icon", getIcon(0, status_text)))); label_.get_style_context()->add_class(status_text); diff --git a/src/modules/jack.cpp b/src/modules/jack.cpp index 3a92110..9bd6fcd 100644 --- a/src/modules/jack.cpp +++ b/src/modules/jack.cpp @@ -72,7 +72,7 @@ auto JACK::update() -> void { } else format = "{load}%"; - label_.set_markup(fmt::format(format, fmt::arg("load", std::round(load_)), + label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("load", std::round(load_)), fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_), fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_))); @@ -81,9 +81,9 @@ auto JACK::update() -> void { std::string tooltip_format = "{bufsize}/{samplerate} {latency}ms"; if (config_["tooltip-format"].isString()) tooltip_format = config_["tooltip-format"].asString(); label_.set_tooltip_text(fmt::format( - tooltip_format, fmt::arg("load", std::round(load_)), fmt::arg("bufsize", bufsize_), - fmt::arg("samplerate", samplerate_), fmt::arg("latency", fmt::format("{:.2f}", latency)), - fmt::arg("xruns", xruns_))); + fmt::runtime(tooltip_format), fmt::arg("load", std::round(load_)), + fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_), + fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_))); } // Call parent update diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index b2750b6..4c081d6 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -278,7 +278,7 @@ auto waybar::modules::KeyboardState::update() -> void { }; for (auto& label_state : label_states) { std::string text; - text = fmt::format(label_state.format, + text = fmt::format(fmt::runtime(label_state.format), fmt::arg("icon", label_state.state ? icon_locked_ : icon_unlocked_), fmt::arg("name", label_state.name)); label_state.label.set_markup(text); diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 4a0e634..544d781 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -56,7 +56,8 @@ auto waybar::modules::Memory::update() -> void { event_box_.show(); auto icons = std::vector{state}; label_.set_markup(fmt::format( - format, used_ram_percentage, fmt::arg("icon", getIcon(used_ram_percentage, icons)), + fmt::runtime(format), used_ram_percentage, + fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("percentage", used_ram_percentage), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), @@ -68,8 +69,8 @@ auto waybar::modules::Memory::update() -> void { if (config_["tooltip-format"].isString()) { auto tooltip_format = config_["tooltip-format"].asString(); label_.set_tooltip_text(fmt::format( - tooltip_format, used_ram_percentage, fmt::arg("total", total_ram_gigabytes), - fmt::arg("swapTotal", total_swap_gigabytes), + fmt::runtime(tooltip_format), used_ram_percentage, + fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("percentage", used_ram_percentage), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 2c855d3..73062c7 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -102,7 +102,6 @@ void waybar::modules::MPD::setLabel() { } else { label_.hide(); } - if (tooltipEnabled()) { std::string tooltip_format; @@ -123,7 +122,9 @@ void waybar::modules::MPD::setLabel() { std::chrono::seconds elapsedTime, totalTime; std::string stateIcon = ""; - if (stopped()) { + bool no_song = song_.get() == nullptr; + if (stopped() || no_song) { + if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped."); format = config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped"; label_.get_style_context()->add_class("stopped"); @@ -175,14 +176,14 @@ void waybar::modules::MPD::setLabel() { try { auto text = fmt::format( - format, fmt::arg("artist", artist.raw()), fmt::arg("albumArtist", album_artist.raw()), - fmt::arg("album", album.raw()), fmt::arg("title", title.raw()), fmt::arg("date", date), - fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), - fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), - fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), - fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon), - fmt::arg("repeatIcon", repeatIcon), fmt::arg("singleIcon", singleIcon), - fmt::arg("filename", filename)); + fmt::runtime(format), fmt::arg("artist", artist.raw()), + fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), + fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), + fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), + fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), + fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon), + fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename)); if (text.empty()) { label_.hide(); } else { @@ -199,7 +200,7 @@ void waybar::modules::MPD::setLabel() { : "MPD (connected)"; try { auto tooltip_text = - fmt::format(tooltip_format, fmt::arg("artist", artist.raw()), + fmt::format(fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()), fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp new file mode 100644 index 0000000..a562175 --- /dev/null +++ b/src/modules/mpris/mpris.cpp @@ -0,0 +1,723 @@ +#include "modules/mpris/mpris.hpp" + +#include + +#include +#include +#include + +extern "C" { +#include +} + +#include +#include + +namespace waybar::modules::mpris { + +const std::string DEFAULT_FORMAT = "{player} ({status}): {dynamic}"; + +Mpris::Mpris(const std::string& id, const Json::Value& config) + : ALabel(config, "mpris", id, DEFAULT_FORMAT, 5, false, true), + tooltip_(DEFAULT_FORMAT), + artist_len_(-1), + album_len_(-1), + title_len_(-1), + dynamic_len_(-1), + dynamic_prio_({"title", "artist", "album", "position", "length"}), + dynamic_order_({"title", "artist", "album", "position", "length"}), + dynamic_separator_(" - "), + truncate_hours_(true), + tooltip_len_limits_(false), + // this character is used in Gnome so it's fine to use it here + ellipsis_("\u2026"), + player_("playerctld"), + manager(), + player(), + last_update_(std::chrono::system_clock::now() - interval_) { + if (config_["format-playing"].isString()) { + format_playing_ = config_["format-playing"].asString(); + } + if (config_["format-paused"].isString()) { + format_paused_ = config_["format-paused"].asString(); + } + if (config_["format-stopped"].isString()) { + format_stopped_ = config_["format-stopped"].asString(); + } + if (config_["ellipsis"].isString()) { + ellipsis_ = config_["ellipsis"].asString(); + } + if (config_["dynamic-separator"].isString()) { + dynamic_separator_ = config_["dynamic-separator"].asString(); + } + if (tooltipEnabled()) { + if (config_["tooltip-format"].isString()) { + tooltip_ = config_["tooltip-format"].asString(); + } + if (config_["tooltip-format-playing"].isString()) { + tooltip_playing_ = config_["tooltip-format-playing"].asString(); + } + if (config_["tooltip-format-paused"].isString()) { + tooltip_paused_ = config_["tooltip-format-paused"].asString(); + } + if (config_["tooltip-format-stopped"].isString()) { + tooltip_stopped_ = config_["tooltip-format-stopped"].asString(); + } + if (config_["enable-tooltip-len-limits"].isBool()) { + tooltip_len_limits_ = config["enable-tooltip-len-limits"].asBool(); + } + } + + if (config["artist-len"].isUInt()) { + artist_len_ = config["artist-len"].asUInt(); + } + if (config["album-len"].isUInt()) { + album_len_ = config["album-len"].asUInt(); + } + if (config["title-len"].isUInt()) { + title_len_ = config["title-len"].asUInt(); + } + if (config["dynamic-len"].isUInt()) { + dynamic_len_ = config["dynamic-len"].asUInt(); + } + // "dynamic-priority" has been kept for backward compatibility + if (config_["dynamic-importance-order"].isArray() || config_["dynamic-priority"].isArray()) { + dynamic_prio_.clear(); + const auto& dynamic_priority = config_["dynamic-importance-order"].isArray() ? config_["dynamic-importance-order"] : config_["dynamic-priority"]; + for (const auto& value : dynamic_priority) { + if (value.isString()) { + dynamic_prio_.push_back(value.asString()); + } + } + } + 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()); + } + } + } + + if (config_["truncate-hours"].isBool()) { + truncate_hours_ = config["truncate-hours"].asBool(); + } + if (config_["player"].isString()) { + 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()); + } + } + } + + GError* error = nullptr; + manager = playerctl_player_manager_new(&error); + if (error) { + throw std::runtime_error(fmt::format("unable to create MPRIS client: {}", error->message)); + } + + g_object_connect(manager, "signal::name-appeared", G_CALLBACK(onPlayerNameAppeared), this, NULL); + g_object_connect(manager, "signal::name-vanished", G_CALLBACK(onPlayerNameVanished), this, NULL); + + if (player_ == "playerctld") { + // use playerctld proxy + PlayerctlPlayerName name = { + .instance = (gchar*)player_.c_str(), + .source = PLAYERCTL_SOURCE_DBUS_SESSION, + }; + player = playerctl_player_new_from_name(&name, &error); + + } else { + GList* players = playerctl_list_players(&error); + if (error) { + auto e = fmt::format("unable to list players: {}", error->message); + g_error_free(error); + throw std::runtime_error(e); + } + + for (auto p = players; p != NULL; 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; + } + } + } + + if (error) { + throw std::runtime_error( + fmt::format("unable to connect to player {}: {}", player_, error->message)); + } + + if (player) { + g_object_connect(player, "signal::play", G_CALLBACK(onPlayerPlay), this, "signal::pause", + G_CALLBACK(onPlayerPause), this, "signal::stop", G_CALLBACK(onPlayerStop), + this, "signal::stop", G_CALLBACK(onPlayerStop), this, "signal::metadata", + G_CALLBACK(onPlayerMetadata), this, NULL); + } + + // allow setting an interval count that triggers periodic refreshes + if (interval_.count() > 0) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; + } + + // trigger initial update + dp.emit(); +} + +Mpris::~Mpris() { + if (manager != NULL) g_object_unref(manager); + if (player != NULL) 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(); + } + } + return ""; +} + +// Wide characters count as two, zero-width characters count as zero +// Modifies str in-place (unless width = std::string::npos) +// Returns the total width of the string pre-truncating +size_t utf8_truncate(std::string& str, size_t width = std::string::npos) { + if (str.length() == 0) return 0; + + const gchar* trunc_end = nullptr; + + size_t total_width = 0; + + for (gchar *data = str.data(), *end = data + str.size(); data;) { + gunichar c = g_utf8_get_char_validated(data, end - data); + if (c == -1U || c == -2U) { + // invalid unicode, treat string as ascii + if (width != std::string::npos && str.length() > width) str.resize(width); + return str.length(); + } else if (g_unichar_iswide(c)) { + total_width += 2; + } else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen + total_width += 1; + } + + data = g_utf8_find_next_char(data, end); + if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c)) + trunc_end = data; + } + + if (trunc_end) str.resize(trunc_end - str.data()); + + return total_width; +} + +size_t utf8_width(const std::string& str) { return utf8_truncate(const_cast(str)); } + +void truncate(std::string& s, const std::string& ellipsis, size_t max_len) { + if (max_len == 0) { + s.resize(0); + return; + } + size_t len = utf8_truncate(s, max_len); + if (len > max_len) { + size_t ellipsis_len = utf8_width(ellipsis); + if (max_len >= ellipsis_len) { + if (ellipsis_len) utf8_truncate(s, max_len - ellipsis_len); + s += ellipsis; + } else { + s.resize(0); + } + } +} + +auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string { + auto artist = info.artist.value_or(std::string()); + if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_); + return artist; +} + +auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string { + auto album = info.album.value_or(std::string()); + if (truncated && album_len_ >= 0) truncate(album, ellipsis_, album_len_); + return album; +} + +auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string { + auto title = info.title.value_or(std::string()); + if (truncated && title_len_ >= 0) truncate(title, ellipsis_, title_len_); + return title; +} + +auto Mpris::getLengthStr(const PlayerInfo& info, bool truncated) -> std::string { + if (info.length.has_value()) { + auto length = info.length.value(); + return (truncated && length.substr(0, 3) == "00:") ? length.substr(3) : length; + } + return std::string(); +} + +auto Mpris::getPositionStr(const PlayerInfo& info, bool truncated) -> std::string { + if (info.position.has_value()) { + auto position = info.position.value(); + return (truncated && position.substr(0, 3) == "00:") ? position.substr(3) : position; + } + return std::string(); +} + +auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> std::string { + auto artist = getArtistStr(info, truncated); + auto album = getAlbumStr(info, truncated); + auto title = getTitleStr(info, truncated); + auto length = getLengthStr(info, truncated && truncate_hours_); + // keep position format same as length format + auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6); + + size_t artistLen = utf8_width(artist); + size_t albumLen = utf8_width(album); + size_t titleLen = utf8_width(title); + size_t lengthLen = length.length(); + size_t posLen = position.length(); + + bool showArtist = (artistLen != 0) && (std::find(dynamic_order_.begin(), dynamic_order_.end(), + "artist") != dynamic_order_.end()); + bool showAlbum = (albumLen != 0) && (std::find(dynamic_order_.begin(), dynamic_order_.end(), + "album") != dynamic_order_.end()); + bool showTitle = (titleLen != 0) && (std::find(dynamic_order_.begin(), dynamic_order_.end(), + "title") != dynamic_order_.end()); + bool showLength = (lengthLen != 0) && (std::find(dynamic_order_.begin(), dynamic_order_.end(), + "length") != dynamic_order_.end()); + bool showPos = (posLen != 0) && (std::find(dynamic_order_.begin(), dynamic_order_.end(), + "position") != dynamic_order_.end()); + + if (truncated && dynamic_len_ >= 0) { + //Since the first element doesn't present a separator and we don't know a priori which one + //it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the + //separatorLen to all the other lengths. + size_t separatorLen = utf8_width(dynamic_separator_); + size_t dynamicLen = dynamic_len_ + separatorLen; + if (showArtist) artistLen += separatorLen; + if (showAlbum) albumLen += separatorLen; + if (showTitle) albumLen += separatorLen; + if (showLength) lengthLen += separatorLen; + if (showPos) posLen += separatorLen; + + size_t totalLen = 0; + + for (auto it = dynamic_prio_.begin(); it != dynamic_prio_.end(); ++it) { + if (*it == "artist") { + if (totalLen + artistLen > dynamicLen) { + showArtist = false; + } else if (showArtist) { + totalLen += artistLen; + } + } else if (*it == "album") { + if (totalLen + albumLen > dynamicLen) { + showAlbum = false; + } else if (showAlbum) { + totalLen += albumLen; + } + } else if (*it == "title") { + if (totalLen + titleLen > dynamicLen) { + showTitle = false; + } else if (showTitle) { + totalLen += titleLen; + } + } else if (*it == "length") { + if (totalLen + lengthLen > dynamicLen) { + showLength = false; + } else if (showLength) { + totalLen += lengthLen; + posLen = std::max((size_t)2, posLen) - 2; + } + } else if (*it == "position") { + if (totalLen + posLen > dynamicLen) { + showPos = false; + } else if (showPos) { + totalLen += posLen; + lengthLen = std::max((size_t)2, lengthLen) - 2; + } + } + } + } + + std::stringstream dynamic; + if (html) { + artist = Glib::Markup::escape_text(artist); + album = Glib::Markup::escape_text(album); + title = Glib::Markup::escape_text(title); + } + + bool lengthOrPositionShown = false; + bool previousShown = false; + std::string previousOrder = ""; + + for (const std::string& order : dynamic_order_) { + if ((order == "artist" && showArtist) || + (order == "album" && showAlbum) || + (order == "title" && showTitle)) { + if (previousShown && + previousOrder != "length" && + previousOrder != "position") { + dynamic << dynamic_separator_; + } + + if (order == "artist") { + dynamic << artist; + } else if (order == "album") { + dynamic << album; + } else if (order == "title") { + dynamic << title; + } + + previousShown = true; + } else if (order == "length" || order == "position") { + if (!lengthOrPositionShown && (showLength || showPos)) { + if (html) dynamic << ""; + if (previousShown) dynamic << ' '; + dynamic << '['; + if (showPos) { + dynamic << position; + if (showLength) dynamic << '/'; + } + if (showLength) dynamic << length; + dynamic << ']'; + if (!dynamic.str().empty()) dynamic << ' '; + if (html) dynamic << ""; + lengthOrPositionShown = true; + } + } + previousOrder = order; + } + return dynamic.str(); +} + +auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name, + gpointer data) -> void { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: name-appeared callback: {}", player_name->name); + + if (std::string(player_name->name) != mpris->player_) { + return; + } + + GError* error = nullptr; + mpris->player = playerctl_player_new_from_name(player_name, &error); + 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", + G_CALLBACK(onPlayerMetadata), mpris, NULL); + + mpris->dp.emit(); +} + +auto Mpris::onPlayerNameVanished(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name, + gpointer data) -> void { + Mpris* 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->dp.emit(); + } +} + +auto Mpris::onPlayerPlay(PlayerctlPlayer* player, gpointer data) -> void { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: player-play callback"); + // update widget + mpris->dp.emit(); +} + +auto Mpris::onPlayerPause(PlayerctlPlayer* player, gpointer data) -> void { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: player-pause callback"); + // update widget + mpris->dp.emit(); +} + +auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: player-stop callback"); + + // hide widget + mpris->event_box_.set_visible(false); + // update widget + mpris->dp.emit(); +} + +auto Mpris::onPlayerMetadata(PlayerctlPlayer* player, GVariant* metadata, gpointer data) -> void { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: player-metadata callback"); + // update widget + mpris->dp.emit(); +} + +auto Mpris::getPlayerInfo() -> std::optional { + if (!player) { + return std::nullopt; + } + + GError* error = nullptr; + + char* player_status = nullptr; + auto player_playback_status = PLAYERCTL_PLAYBACK_STATUS_STOPPED; + g_object_get(player, "status", &player_status, "playback-status", &player_playback_status, NULL); + + std::string player_name = player_; + if (player_name == "playerctld") { + GList* players = playerctl_list_players(&error); + if (error) { + auto e = fmt::format("unable to list players: {}", error->message); + g_error_free(error); + throw std::runtime_error(e); + } + // > get the list of players [..] in order of activity + // https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249 + players = g_list_first(players); + if (players) player_name = static_cast(players->data)->name; + } + + if (std::any_of(ignored_players_.begin(), ignored_players_.end(), + [&](const std::string& pn) { return player_name == pn; })) { + spdlog::warn("mpris[{}]: ignoring player update", player_name); + return std::nullopt; + } + + // make status lowercase + player_status[0] = std::tolower(player_status[0]); + + PlayerInfo info = { + .name = player_name, + .status = player_playback_status, + .status_string = player_status, + .artist = std::nullopt, + .album = std::nullopt, + .title = std::nullopt, + .length = std::nullopt, + }; + + 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)) { + 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)) { + 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)) { + spdlog::debug("mpris[{}]: mpris:length = {}", info.name, length_); + std::chrono::microseconds 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); + info.length = fmt::format("{:02}:{:02}:{:02}", len_h.count(), len_m.count(), len_s.count()); + g_free(length_); + } + if (error) goto errorexit; + + { + auto position_ = playerctl_player_get_position(player, &error); + if (error) { + // it's fine to have an error here because not all players report a position + g_error_free(error); + error = nullptr; + } else { + spdlog::debug("mpris[{}]: position = {}", info.name, position_); + std::chrono::microseconds 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); + info.position = fmt::format("{:02}:{:02}:{:02}", len_h.count(), len_m.count(), len_s.count()); + } + } + + return info; + +errorexit: + spdlog::error("mpris[{}]: {}", info.name, error->message); + g_error_free(error); + return std::nullopt; +} + +bool Mpris::handleToggle(GdkEventButton* const& e) { + GError* error = nullptr; + + auto info = getPlayerInfo(); + if (!info) return false; + + if (e->type == GdkEventType::GDK_BUTTON_PRESS) { + switch (e->button) { + case 1: // left-click + if (config_["on-click"].isString()) { + return ALabel::handleToggle(e); + } + playerctl_player_play_pause(player, &error); + break; + case 2: // middle-click + if (config_["on-middle-click"].isString()) { + return ALabel::handleToggle(e); + } + playerctl_player_previous(player, &error); + break; + case 3: // right-click + if (config_["on-right-click"].isString()) { + return ALabel::handleToggle(e); + } + playerctl_player_next(player, &error); + break; + } + } + if (error) { + spdlog::error("mpris[{}]: error running builtin on-click action: {}", (*info).name, + error->message); + g_error_free(error); + return false; + } + return true; +} + +auto Mpris::update() -> void { + const auto now = std::chrono::system_clock::now(); + if (now - last_update_ < interval_) return; + last_update_ = now; + + auto opt = getPlayerInfo(); + if (!opt) { + event_box_.set_visible(false); + ALabel::update(); + return; + } + auto info = *opt; + + if (info.status == PLAYERCTL_PLAYBACK_STATUS_STOPPED) { + spdlog::debug("mpris[{}]: player stopped, skipping update", info.name); + return; + } + + spdlog::debug("mpris[{}]: running update", info.name); + + // set css class for player status + if (!lastStatus.empty() && label_.get_style_context()->has_class(lastStatus)) { + label_.get_style_context()->remove_class(lastStatus); + } + if (!label_.get_style_context()->has_class(info.status_string)) { + label_.get_style_context()->add_class(info.status_string); + } + lastStatus = info.status_string; + + // set css class for player name + if (!lastPlayer.empty() && label_.get_style_context()->has_class(lastPlayer)) { + label_.get_style_context()->remove_class(lastPlayer); + } + if (!label_.get_style_context()->has_class(info.name)) { + label_.get_style_context()->add_class(info.name); + } + lastPlayer = info.name; + + auto formatstr = format_; + auto tooltipstr = tooltip_; + switch (info.status) { + case PLAYERCTL_PLAYBACK_STATUS_PLAYING: + if (!format_playing_.empty()) formatstr = format_playing_; + if (!tooltip_playing_.empty()) tooltipstr = tooltip_playing_; + break; + case PLAYERCTL_PLAYBACK_STATUS_PAUSED: + if (!format_paused_.empty()) formatstr = format_paused_; + if (!tooltip_paused_.empty()) tooltipstr = tooltip_paused_; + break; + case PLAYERCTL_PLAYBACK_STATUS_STOPPED: + if (!format_stopped_.empty()) formatstr = format_stopped_; + if (!tooltip_stopped_.empty()) tooltipstr = tooltip_stopped_; + break; + } + + std::string length = getLengthStr(info, truncate_hours_); + std::string tooltipLength = + (tooltip_len_limits_ || length.length() > 5) ? length : getLengthStr(info, false); + // keep position format same as length format + std::string position = getPositionStr(info, truncate_hours_ && length.length() < 6); + std::string tooltipPosition = + (tooltip_len_limits_ || position.length() > 5) ? position : getPositionStr(info, false); + + try { + auto label_format = fmt::format( + fmt::runtime(formatstr), + fmt::arg("player", std::string(Glib::Markup::escape_text(info.name))), + fmt::arg("status", info.status_string), + fmt::arg("artist", std::string(Glib::Markup::escape_text(getArtistStr(info, true)))), + fmt::arg("title", std::string(Glib::Markup::escape_text(getTitleStr(info, true)))), + fmt::arg("album", std::string(Glib::Markup::escape_text(getAlbumStr(info, true)))), + fmt::arg("length", length), fmt::arg("position", position), + fmt::arg("dynamic", getDynamicStr(info, true, true)), + fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), + fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string))); + + if (label_format.empty()) { + label_.hide(); + } else { + label_.set_markup(label_format); + label_.show(); + } + } catch (fmt::format_error const& e) { + spdlog::warn("mpris: format error: {}", e.what()); + } + + if (tooltipEnabled()) { + try { + auto tooltip_text = fmt::format( + fmt::runtime(tooltipstr), fmt::arg("player", info.name), + fmt::arg("status", info.status_string), + fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)), + fmt::arg("title", getTitleStr(info, tooltip_len_limits_)), + fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)), + fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition), + fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)), + fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), + fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string))); + + label_.set_tooltip_text(tooltip_text); + } catch (fmt::format_error const& e) { + spdlog::warn("mpris: format error (tooltip): {}", e.what()); + } + } + + event_box_.set_visible(true); + // call parent update + ALabel::update(); +} + +} // namespace waybar::modules::mpris diff --git a/src/modules/network.cpp b/src/modules/network.cpp index a4797ee..5eef166 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -87,6 +87,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf want_link_dump_(false), want_addr_dump_(false), dump_in_progress_(false), + is_p2p_(false), cidr_(0), signal_strength_dbm_(0), signal_strength_(0), @@ -98,7 +99,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf // Start with some "text" in the module's label_. update() will then // update it. Since the text should be different, update() will be able // to show or hide the event_box_. This is to work around the case where - // the module start with no text, but the the event_box_ is shown. + // the module start with no text, but the event_box_ is shown. label_.set_markup(""); auto bandwidth = readBandwidthUsage(); @@ -187,7 +188,7 @@ void waybar::modules::Network::createEventSocket() { throw std::runtime_error("Can't create epoll"); } { - ev_fd_ = eventfd(0, EFD_NONBLOCK); + ev_fd_ = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); struct epoll_event event; memset(&event, 0, sizeof(event)); event.events = EPOLLIN | EPOLLET; @@ -331,7 +332,7 @@ auto waybar::modules::Network::update() -> void { getState(signal_strength_); auto text = fmt::format( - format_, fmt::arg("essid", essid_), fmt::arg("signaldBm", signal_strength_dbm_), + fmt::runtime(format_), fmt::arg("essid", essid_), fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_), @@ -363,8 +364,8 @@ auto waybar::modules::Network::update() -> void { } if (!tooltip_format.empty()) { auto tooltip_text = fmt::format( - tooltip_format, fmt::arg("essid", essid_), fmt::arg("signaldBm", signal_strength_dbm_), - fmt::arg("signalStrength", signal_strength_), + fmt::runtime(tooltip_format), fmt::arg("essid", essid_), + fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), @@ -456,6 +457,8 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { case IFLA_IFNAME: ifname = static_cast(RTA_DATA(ifla)); ifname_len = RTA_PAYLOAD(ifla) - 1; // minus \0 + if (ifi->ifi_flags & IFF_POINTOPOINT && net->checkInterface(ifname)) + net->is_p2p_ = true; break; case IFLA_CARRIER: { carrier = *(char *)RTA_DATA(ifla) == 1; @@ -494,6 +497,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { net->ifname_ = new_ifname; net->ifid_ = ifi->ifi_index; + if (ifi->ifi_flags & IFF_POINTOPOINT) net->is_p2p_ = true; if (carrier.has_value()) { net->carrier_ = carrier.value(); } @@ -537,7 +541,9 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { for (; RTA_OK(ifa_rta, attrlen); ifa_rta = RTA_NEXT(ifa_rta, attrlen)) { switch (ifa_rta->rta_type) { - case IFA_ADDRESS: { + case IFA_ADDRESS: + if (net->is_p2p_) continue; + case IFA_LOCAL: char ipaddr[INET6_ADDRSTRLEN]; if (!is_del_event) { net->ipaddr_ = inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); @@ -570,7 +576,6 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { } net->dp.emit(); break; - } } } break; diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index c797997..d35e298 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -81,13 +81,6 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { if (dir == SCROLL_DIR::NONE) { return true; } - if (config_["reverse-scrolling"].asInt() == 1) { - if (dir == SCROLL_DIR::UP) { - dir = SCROLL_DIR::DOWN; - } else if (dir == SCROLL_DIR::DOWN) { - dir = SCROLL_DIR::UP; - } - } double volume_tick = static_cast(PA_VOLUME_NORM) / 100; pa_volume_t change = volume_tick; pa_cvolume pa_volume = pa_volume_; @@ -279,7 +272,12 @@ auto waybar::modules::Pulseaudio::update() -> void { label_.get_style_context()->remove_class("muted"); label_.get_style_context()->remove_class("sink-muted"); } - format = config_[format_name].isString() ? config_[format_name].asString() : format; + auto state = getState(volume_, true); + if (!state.empty() && config_[format_name + "-" + state].isString()) { + format = config_[format_name + "-" + state].asString(); + } else if (config_[format_name].isString()) { + format = config_[format_name].asString(); + } } // TODO: find a better way to split source/sink std::string format_source = "{volume}%"; @@ -294,9 +292,9 @@ auto waybar::modules::Pulseaudio::update() -> void { format_source = config_["format-source"].asString(); } } - format_source = fmt::format(format_source, fmt::arg("volume", source_volume_)); + format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume_)); auto text = fmt::format( - format, fmt::arg("desc", desc_), fmt::arg("volume", volume_), + fmt::runtime(format), fmt::arg("desc", desc_), fmt::arg("volume", volume_), fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume_), fmt::arg("source_desc", source_desc_), fmt::arg("icon", getIcon(volume_, getPulseIcon()))); if (text.empty()) { @@ -305,7 +303,6 @@ auto waybar::modules::Pulseaudio::update() -> void { label_.set_markup(text); label_.show(); } - getState(volume_); if (tooltipEnabled()) { if (tooltip_format.empty() && config_["tooltip-format"].isString()) { @@ -313,7 +310,7 @@ auto waybar::modules::Pulseaudio::update() -> void { } if (!tooltip_format.empty()) { label_.set_tooltip_text(fmt::format( - tooltip_format, fmt::arg("desc", desc_), fmt::arg("volume", volume_), + fmt::runtime(tooltip_format), fmt::arg("desc", desc_), fmt::arg("volume", volume_), fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume_), fmt::arg("source_desc", source_desc_), fmt::arg("icon", getIcon(volume_, getPulseIcon())))); diff --git a/src/modules/river/layout.cpp b/src/modules/river/layout.cpp new file mode 100644 index 0000000..e938400 --- /dev/null +++ b/src/modules/river/layout.cpp @@ -0,0 +1,174 @@ +#include "modules/river/layout.hpp" + +#include +#include + +#include "client.hpp" + +namespace waybar::modules::river { + +static void listen_focused_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, + uint32_t tags) { + // Intentionally empty +} + +static void listen_view_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, + struct wl_array *tags) { + // Intentionally empty +} + +static void listen_urgent_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, + uint32_t tags) { + // Intentionally empty +} + +static void listen_layout_name(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, + const char *layout) { + static_cast(data)->handle_name(layout); +} + +static void listen_layout_name_clear(void *data, + struct zriver_output_status_v1 *zriver_output_status_v1) { + static_cast(data)->handle_clear(); +} + +static void listen_focused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + struct wl_output *output) { + static_cast(data)->handle_focused_output(output); +} + +static void listen_unfocused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + struct wl_output *output) { + static_cast(data)->handle_unfocused_output(output); +} + +static void listen_focused_view(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + const char *title) { + // Intentionally empty +} + +static void listen_mode(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + const char *mode) { + // Intentionally empty +} + +static const zriver_output_status_v1_listener output_status_listener_impl{ + .focused_tags = listen_focused_tags, + .view_tags = listen_view_tags, + .urgent_tags = listen_urgent_tags, + .layout_name = listen_layout_name, + .layout_name_clear = listen_layout_name_clear, +}; + +static const zriver_seat_status_v1_listener seat_status_listener_impl{ + .focused_output = listen_focused_output, + .unfocused_output = listen_unfocused_output, + .focused_view = listen_focused_view, + .mode = listen_mode, +}; + +static void handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) { + if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) { + version = std::min(version, 4); + + // implies ZRIVER_OUTPUT_STATUS_V1_LAYOUT_NAME_CLEAR_SINCE_VERSION + if (version < ZRIVER_OUTPUT_STATUS_V1_LAYOUT_NAME_SINCE_VERSION) { + spdlog::error( + "river server does not support the \"layout_name\" and \"layout_clear\" events; the " + "module will be disabled" + + std::to_string(version)); + return; + } + static_cast(data)->status_manager_ = static_cast( + wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, version)); + } + + if (std::strcmp(interface, wl_seat_interface.name) == 0) { + version = std::min(version, 1); + static_cast(data)->seat_ = static_cast( + wl_registry_bind(registry, name, &wl_seat_interface, version)); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { + // Nobody cares +} + +static const wl_registry_listener registry_listener_impl = {.global = handle_global, + .global_remove = handle_global_remove}; + +Layout::Layout(const std::string &id, const waybar::Bar &bar, const Json::Value &config) + : waybar::ALabel(config, "layout", id, "{}"), + status_manager_{nullptr}, + seat_{nullptr}, + bar_(bar), + 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); + + output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + + if (!status_manager_) { + spdlog::error("river_status_manager_v1 not advertised"); + return; + } + + if (!seat_) { + spdlog::error("wl_seat not advertised"); + } + + label_.hide(); + ALabel::update(); + + seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); + zriver_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this); + + output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output_); + zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this); + + zriver_status_manager_v1_destroy(status_manager_); +} + +Layout::~Layout() { + if (output_status_) { + zriver_output_status_v1_destroy(output_status_); + } + if (seat_status_) { + zriver_seat_status_v1_destroy(seat_status_); + } +} + +void Layout::handle_name(const char *name) { + if (std::strcmp(name, "") == 0 || format_.empty()) { + label_.hide(); // hide empty labels or labels with empty format + } else { + label_.show(); + label_.set_markup(fmt::format(fmt::runtime(format_), Glib::Markup::escape_text(name).raw())); + } + ALabel::update(); +} + +void Layout::handle_clear() { + label_.hide(); + ALabel::update(); +} + +void Layout::handle_focused_output(struct wl_output *output) { + if (output_ == output) { // if we focused the output this bar belongs to + label_.get_style_context()->add_class("focused"); + ALabel::update(); + } + focused_output_ = output; +} + +void Layout::handle_unfocused_output(struct wl_output *output) { + if (output_ == output) { // if we unfocused the output this bar belongs to + label_.get_style_context()->remove_class("focused"); + ALabel::update(); + } +} + +} /* namespace waybar::modules::river */ diff --git a/src/modules/river/mode.cpp b/src/modules/river/mode.cpp index 4a51c83..1f788e0 100644 --- a/src/modules/river/mode.cpp +++ b/src/modules/river/mode.cpp @@ -103,7 +103,7 @@ void Mode::handle_mode(const char *mode) { } label_.get_style_context()->add_class(mode); - label_.set_markup(fmt::format(format_, Glib::Markup::escape_text(mode).raw())); + label_.set_markup(fmt::format(fmt::runtime(format_), Glib::Markup::escape_text(mode).raw())); label_.show(); } diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index ccafc16..baa6b7e 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -53,7 +53,7 @@ static const zriver_command_callback_v1_listener command_callback_listener_impl{ static void handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { if (std::strcmp(interface, zriver_status_manager_v1_interface.name) == 0) { - version = std::min(version, 2); + version = std::min(version, 2u); if (version < ZRIVER_OUTPUT_STATUS_V1_URGENT_TAGS_SINCE_VERSION) { spdlog::warn("river server does not support urgent tags"); } @@ -62,13 +62,13 @@ static void handle_global(void *data, struct wl_registry *registry, uint32_t nam } if (std::strcmp(interface, zriver_control_v1_interface.name) == 0) { - version = std::min(version, 1); + version = std::min(version, 1u); static_cast(data)->control_ = static_cast( wl_registry_bind(registry, name, &zriver_control_v1_interface, version)); } if (std::strcmp(interface, wl_seat_interface.name) == 0) { - version = std::min(version, 1); + version = std::min(version, 1u); static_cast(data)->seat_ = static_cast( wl_registry_bind(registry, name, &wl_seat_interface, version)); } @@ -114,33 +114,39 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con event_box_.add(box_); // Default to 9 tags, cap at 32 - const uint32_t num_tags = - config["num-tags"].isUInt() ? std::min(32, config_["num-tags"].asUInt()) : 9; + const int num_tags = + config["num-tags"].isUInt() ? std::min(32, config_["num-tags"].asUInt()) : 9; - std::vector tag_labels(num_tags); - for (uint32_t tag = 0; tag < num_tags; ++tag) { - tag_labels[tag] = std::to_string(tag + 1); - } - const Json::Value custom_labels = config["tag-labels"]; - if (custom_labels.isArray() && !custom_labels.empty()) { - for (uint32_t tag = 0; tag < std::min(num_tags, custom_labels.size()); ++tag) { - tag_labels[tag] = custom_labels[tag].asString(); + const auto tag_labels = config["tag-labels"]; + const auto set_tags = config["set-tags"]; + const auto toggle_tags = config["toggle-tags"]; + for (int tag = 0; tag < num_tags; ++tag) { + if (tag_labels.isArray() && !tag_labels.empty()) { + buttons_.emplace_back(tag_labels[tag].asString()); + } else { + // default name is the tag value + buttons_.emplace_back(std::to_string(tag + 1)); } - } - uint32_t i = 1; - for (const auto &tag_label : tag_labels) { - Gtk::Button &button = buttons_.emplace_back(tag_label); + auto &button = buttons_[tag]; button.set_relief(Gtk::RELIEF_NONE); box_.pack_start(button, false, false, 0); + if (!config_["disable-click"].asBool()) { - button.signal_clicked().connect( - sigc::bind(sigc::mem_fun(*this, &Tags::handle_primary_clicked), i)); - button.signal_button_press_event().connect( - sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), i)); + if (set_tags.isArray() && !set_tags.empty()) + button.signal_clicked().connect(sigc::bind( + sigc::mem_fun(*this, &Tags::handle_primary_clicked), set_tags[tag].asUInt())); + else + button.signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &Tags::handle_primary_clicked), (1 << tag))); + if (toggle_tags.isArray() && !toggle_tags.empty()) + button.signal_button_press_event().connect(sigc::bind( + sigc::mem_fun(*this, &Tags::handle_button_press), toggle_tags[tag].asUInt())); + else + button.signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), (1 << tag))); } button.show(); - i <<= 1; } struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); @@ -182,45 +188,38 @@ bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) { } void Tags::handle_focused_tags(uint32_t tags) { - uint32_t i = 0; - for (auto &button : buttons_) { + for (size_t i = 0; i < buttons_.size(); ++i) { if ((1 << i) & tags) { - button.get_style_context()->add_class("focused"); + buttons_[i].get_style_context()->add_class("focused"); } else { - button.get_style_context()->remove_class("focused"); + buttons_[i].get_style_context()->remove_class("focused"); } - ++i; } } void Tags::handle_view_tags(struct wl_array *view_tags) { - // First clear all occupied state - for (auto &button : buttons_) { - button.get_style_context()->remove_class("occupied"); + uint32_t tags = 0; + auto view_tag = reinterpret_cast(view_tags->data); + auto end = view_tag + (view_tags->size / sizeof(uint32_t)); + for (; view_tag < end; ++view_tag) { + tags |= *view_tag; } - - // Set tags with a view to occupied - uint32_t *start = static_cast(view_tags->data); - for (uint32_t *tags = start; tags < start + view_tags->size / sizeof(uint32_t); ++tags) { - uint32_t i = 0; - for (auto &button : buttons_) { - if (*tags & (1 << i)) { - button.get_style_context()->add_class("occupied"); - } - ++i; + for (size_t i = 0; i < buttons_.size(); ++i) { + if ((1 << i) & tags) { + buttons_[i].get_style_context()->add_class("occupied"); + } else { + buttons_[i].get_style_context()->remove_class("occupied"); } } } void Tags::handle_urgent_tags(uint32_t tags) { - uint32_t i = 0; - for (auto &button : buttons_) { + for (size_t i = 0; i < buttons_.size(); ++i) { if ((1 << i) & tags) { - button.get_style_context()->add_class("urgent"); + buttons_[i].get_style_context()->add_class("urgent"); } else { - button.get_style_context()->remove_class("urgent"); + buttons_[i].get_style_context()->remove_class("urgent"); } - ++i; } } diff --git a/src/modules/river/window.cpp b/src/modules/river/window.cpp index d0f492f..dc7a645 100644 --- a/src/modules/river/window.cpp +++ b/src/modules/river/window.cpp @@ -106,7 +106,11 @@ void Window::handle_focused_view(const char *title) { label_.hide(); // hide empty labels or labels with empty format } else { label_.show(); - label_.set_markup(fmt::format(format_, Glib::Markup::escape_text(title).raw())); + auto text = fmt::format(fmt::runtime(format_), Glib::Markup::escape_text(title).raw()); + label_.set_markup(text); + if (tooltipEnabled()) { + label_.set_tooltip_markup(text); + } } ALabel::update(); diff --git a/src/modules/sndio.cpp b/src/modules/sndio.cpp index 7a358c1..72e7207 100644 --- a/src/modules/sndio.cpp +++ b/src/modules/sndio.cpp @@ -110,14 +110,14 @@ auto Sndio::update() -> void { label_.get_style_context()->remove_class("muted"); } - auto text = fmt::format(format, fmt::arg("volume", vol), fmt::arg("raw_value", volume_)); + auto text = + fmt::format(fmt::runtime(format), fmt::arg("volume", vol), fmt::arg("raw_value", volume_)); if (text.empty()) { label_.hide(); } else { label_.set_markup(text); label_.show(); } - ALabel::update(); } diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 007862d..fff8e01 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -22,10 +22,6 @@ Host::~Host() { Gio::DBus::unwatch_name(bus_name_id_); bus_name_id_ = 0; } - if (watcher_id_ > 0) { - Gio::DBus::unwatch_name(watcher_id_); - watcher_id_ = 0; - } g_cancellable_cancel(cancellable_); g_clear_object(&cancellable_); g_clear_object(&watcher_); diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index e5bd6ab..9d3fc4b 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -9,6 +9,7 @@ #include #include "util/format.hpp" +#include "util/gtk_icon.hpp" template <> struct fmt::formatter : formatter { @@ -379,10 +380,8 @@ Glib::RefPtr Item::getIconByName(const std::string& name, int reque return icon_theme->load_icon(name.c_str(), tmp_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); } - Glib::RefPtr default_theme = Gtk::IconTheme::get_default(); - default_theme->rescan_if_needed(); - return default_theme->load_icon(name.c_str(), tmp_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + return DefaultGtkIconThemeWrapper::load_icon(name.c_str(), tmp_size, + Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); } double Item::getScaledIconSize() { diff --git a/src/modules/sni/watcher.cpp b/src/modules/sni/watcher.cpp index 34bcef6..663fdcd 100644 --- a/src/modules/sni/watcher.cpp +++ b/src/modules/sni/watcher.cpp @@ -14,11 +14,6 @@ Watcher::Watcher() watcher_(sn_watcher_skeleton_new()) {} Watcher::~Watcher() { - if (hosts_ != nullptr) { - g_slist_free_full(hosts_, gfWatchFree); - hosts_ = nullptr; - } - if (items_ != nullptr) { g_slist_free_full(items_, gfWatchFree); items_ = nullptr; diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index 4d6495c..5c3df7b 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -2,6 +2,8 @@ #include +#include + namespace waybar::modules::sway { Ipc::Ipc() { diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index d3730a1..a5860bd 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -96,14 +96,14 @@ void Language::onEvent(const struct Ipc::ipc_response& res) { auto Language::update() -> void { std::lock_guard lock(mutex_); auto display_layout = trim(fmt::format( - format_, fmt::arg("short", layout_.short_name), + fmt::runtime(format_), fmt::arg("short", layout_.short_name), fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name), fmt::arg("variant", layout_.variant), fmt::arg("flag", layout_.country_flag()))); label_.set_markup(display_layout); if (tooltipEnabled()) { if (tooltip_format_ != "") { auto tooltip_display_layout = trim( - fmt::format(tooltip_format_, fmt::arg("short", layout_.short_name), + fmt::format(fmt::runtime(tooltip_format_), fmt::arg("short", layout_.short_name), fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name), fmt::arg("variant", layout_.variant), fmt::arg("flag", layout_.country_flag()))); diff --git a/src/modules/sway/mode.cpp b/src/modules/sway/mode.cpp index 7eaa523..b81735e 100644 --- a/src/modules/sway/mode.cpp +++ b/src/modules/sway/mode.cpp @@ -42,7 +42,7 @@ auto Mode::update() -> void { if (mode_.empty()) { event_box_.hide(); } else { - label_.set_markup(fmt::format(format_, mode_)); + label_.set_markup(fmt::format(fmt::runtime(format_), mode_)); if (tooltipEnabled()) { label_.set_tooltip_text(mode_); } diff --git a/src/modules/sway/scratchpad.cpp b/src/modules/sway/scratchpad.cpp index 59e3053..17dc270 100644 --- a/src/modules/sway/scratchpad.cpp +++ b/src/modules/sway/scratchpad.cpp @@ -32,7 +32,8 @@ auto Scratchpad::update() -> void { if (count_ || show_empty_) { event_box_.show(); label_.set_markup( - fmt::format(format_, fmt::arg("icon", getIcon(count_, "", config_["format-icons"].size())), + fmt::format(fmt::runtime(format_), + fmt::arg("icon", getIcon(count_, "", config_["format-icons"].size())), fmt::arg("count", count_))); if (tooltip_enabled_) { label_.set_tooltip_markup(tooltip_text_); @@ -64,7 +65,7 @@ auto Scratchpad::onCmd(const struct Ipc::ipc_response& res) -> void { if (tooltip_enabled_) { tooltip_text_.clear(); for (const auto& window : tree["nodes"][0]["nodes"][0]["floating_nodes"]) { - tooltip_text_.append(fmt::format(tooltip_format_ + '\n', + tooltip_text_.append(fmt::format(fmt::runtime(tooltip_format_ + '\n'), fmt::arg("app", window["app_id"].asString()), fmt::arg("title", window["name"].asString()))); } diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index 5da7d3d..50aea60 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -5,19 +5,19 @@ #include #include #include -#include #include #include #include #include -#include "util/rewrite_title.hpp" +#include "util/gtk_icon.hpp" +#include "util/rewrite_string.hpp" namespace waybar::modules::sway { Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) - : AIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), windowId_(-1) { + : AIconLabel(config, "window", id, "{}", 0, true), bar_(bar), windowId_(-1) { // Icon size if (config_["icon-size"].isUInt()) { app_icon_size_ = config["icon-size"].asUInt(); @@ -35,6 +35,7 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) ipc_.handleEvent(); } catch (const std::exception& e) { spdlog::error("Window: {}", e.what()); + spdlog::trace("Window::Window exception"); } }); } @@ -46,12 +47,13 @@ void Window::onCmd(const struct Ipc::ipc_response& res) { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); auto output = payload["output"].isString() ? payload["output"].asString() : ""; - std::tie(app_nb_, windowId_, window_, app_id_, app_class_, shell_) = + std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_) = getFocusedNode(payload["nodes"], output); updateAppIconName(); dp.emit(); } catch (const std::exception& e) { spdlog::error("Window: {}", e.what()); + spdlog::trace("Window::onCmd exception"); } } @@ -79,13 +81,12 @@ std::optional getIconName(const std::string& app_id, const std::s if (!desktop_file_path.has_value()) { // Try some heuristics to find a matching icon - const auto default_icon_theme = Gtk::IconTheme::get_default(); - if (default_icon_theme->has_icon(app_id)) { + if (DefaultGtkIconThemeWrapper::has_icon(app_id)) { return app_id; } const auto app_id_desktop = app_id + "-desktop"; - if (default_icon_theme->has_icon(app_id_desktop)) { + if (DefaultGtkIconThemeWrapper::has_icon(app_id_desktop)) { return app_id_desktop; } @@ -99,7 +100,7 @@ std::optional getIconName(const std::string& app_id, const std::s const auto first_space = app_id.find_first_of(' '); if (first_space != std::string::npos) { const auto first_word = to_lower(app_id.substr(0, first_space)); - if (default_icon_theme->has_icon(first_word)) { + if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { return first_word; } } @@ -107,7 +108,7 @@ std::optional getIconName(const std::string& app_id, const std::s const auto first_dash = app_id.find_first_of('-'); if (first_dash != std::string::npos) { const auto first_word = to_lower(app_id.substr(0, first_dash)); - if (default_icon_theme->has_icon(first_word)) { + if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { return first_word; } } @@ -156,30 +157,52 @@ void Window::updateAppIcon() { } auto Window::update() -> void { - if (!old_app_id_.empty()) { - bar_.window.get_style_context()->remove_class(old_app_id_); - } + spdlog::trace("workspace layout {}, tiled count {}, floating count {}", layout_, app_nb_, + floating_count_); + + int mode = 0; if (app_nb_ == 0) { - bar_.window.get_style_context()->remove_class("solo"); - if (!bar_.window.get_style_context()->has_class("empty")) { - bar_.window.get_style_context()->add_class("empty"); + if (floating_count_ == 0) { + mode += 1; + } else { + mode += 4; } } else if (app_nb_ == 1) { - bar_.window.get_style_context()->remove_class("empty"); - if (!bar_.window.get_style_context()->has_class("solo")) { - bar_.window.get_style_context()->add_class("solo"); - } - if (!app_id_.empty() && !bar_.window.get_style_context()->has_class(app_id_)) { - bar_.window.get_style_context()->add_class(app_id_); - old_app_id_ = app_id_; - } + mode += 2; } else { - bar_.window.get_style_context()->remove_class("solo"); - bar_.window.get_style_context()->remove_class("empty"); + if (layout_ == "tabbed") { + mode += 8; + } else if (layout_ == "stacked") { + mode += 16; + } else { + mode += 32; + } } - label_.set_markup(fmt::format( - format_, fmt::arg("title", waybar::util::rewriteTitle(window_, config_["rewrite"])), - fmt::arg("app_id", app_id_), fmt::arg("shell", shell_))); + + if (!old_app_id_.empty() && ((mode & 2) == 0 || old_app_id_ != app_id_) && + bar_.window.get_style_context()->has_class(old_app_id_)) { + spdlog::trace("Removing app_id class: {}", old_app_id_); + bar_.window.get_style_context()->remove_class(old_app_id_); + old_app_id_ = ""; + } + + setClass("empty", ((mode & 1) > 0)); + setClass("solo", ((mode & 2) > 0)); + setClass("floating", ((mode & 4) > 0)); + setClass("tabbed", ((mode & 8) > 0)); + setClass("stacked", ((mode & 16) > 0)); + setClass("tiled", ((mode & 32) > 0)); + + if ((mode & 2) > 0 && !app_id_.empty() && !bar_.window.get_style_context()->has_class(app_id_)) { + spdlog::trace("Adding app_id class: {}", app_id_); + bar_.window.get_style_context()->add_class(app_id_); + old_app_id_ = app_id_; + } + + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_), + fmt::arg("shell", shell_)), + config_["rewrite"])); if (tooltipEnabled()) { label_.set_tooltip_text(window_); } @@ -190,71 +213,143 @@ auto Window::update() -> void { AIconLabel::update(); } -int leafNodesInWorkspace(const Json::Value& node) { +void Window::setClass(std::string classname, bool enable) { + if (enable) { + if (!bar_.window.get_style_context()->has_class(classname)) { + bar_.window.get_style_context()->add_class(classname); + } + } else { + bar_.window.get_style_context()->remove_class(classname); + } +} + +std::pair leafNodesInWorkspace(const Json::Value& node) { auto const& nodes = node["nodes"]; auto const& floating_nodes = node["floating_nodes"]; if (nodes.empty() && floating_nodes.empty()) { - if (node["type"] == "workspace") - return 0; - else - return 1; + if (node["type"].asString() == "workspace") + return {0, 0}; + else if (node["type"].asString() == "floating_con") { + return {0, 1}; + } else { + return {1, 0}; + } } int sum = 0; - if (!nodes.empty()) { - for (auto const& node : nodes) sum += leafNodesInWorkspace(node); - } - if (!floating_nodes.empty()) { - for (auto const& node : floating_nodes) sum += leafNodesInWorkspace(node); - } - return sum; -} - -std::tuple gfnWithWorkspace( - const Json::Value& nodes, std::string& output, const Json::Value& config_, const Bar& bar_, - Json::Value& parentWorkspace) { + int floating_sum = 0; for (auto const& node : nodes) { - if (node["output"].isString()) { - output = node["output"].asString(); - } - // found node - if (node["focused"].asBool() && (node["type"] == "con" || node["type"] == "floating_con")) { - if ((!config_["all-outputs"].asBool() && output == bar_.output->name) || - config_["all-outputs"].asBool()) { - auto app_id = node["app_id"].isString() ? node["app_id"].asString() - : node["window_properties"]["instance"].asString(); - const auto app_class = node["window_properties"]["class"].isString() - ? node["window_properties"]["class"].asString() - : ""; - - const auto shell = node["shell"].isString() ? node["shell"].asString() : ""; - - int nb = node.size(); - if (parentWorkspace != 0) nb = leafNodesInWorkspace(parentWorkspace); - return {nb, node["id"].asInt(), Glib::Markup::escape_text(node["name"].asString()), - app_id, app_class, shell}; - } - } - // iterate - if (node["type"] == "workspace") parentWorkspace = node; - auto [nb, id, name, app_id, app_class, shell] = - gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace); - if (id > -1 && !name.empty()) { - return {nb, id, name, app_id, app_class, shell}; - } - // Search for floating node - std::tie(nb, id, name, app_id, app_class, shell) = - gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace); - if (id > -1 && !name.empty()) { - return {nb, id, name, app_id, app_class, shell}; - } + std::pair all_leaf_nodes = leafNodesInWorkspace(node); + sum += all_leaf_nodes.first; + floating_sum += all_leaf_nodes.second; } - return {0, -1, "", "", "", ""}; + for (auto const& node : floating_nodes) { + std::pair all_leaf_nodes = leafNodesInWorkspace(node); + sum += all_leaf_nodes.first; + floating_sum += all_leaf_nodes.second; + } + return {sum, floating_sum}; } -std::tuple +std::tuple +gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Value& config_, + const Bar& bar_, Json::Value& parentWorkspace, + const Json::Value& immediateParent) { + for (auto const& node : nodes) { + if (node["type"].asString() == "output") { + if ((!config_["all-outputs"].asBool() || config_["offscreen-css"].asBool()) && + (node["name"].asString() != bar_.output->name)) { + continue; + } + output = node["name"].asString(); + } else if (node["type"].asString() == "workspace") { + // needs to be a string comparison, because filterWorkspace is the current_workspace + if (node["name"].asString() != immediateParent["current_workspace"].asString()) { + continue; + } + if (node["focused"].asBool()) { + std::pair all_leaf_nodes = leafNodesInWorkspace(node); + return {all_leaf_nodes.first, + all_leaf_nodes.second, + node["id"].asInt(), + (((all_leaf_nodes.first > 0) || (all_leaf_nodes.second > 0)) && + (config_["show-focused-workspace-name"].asBool())) + ? node["name"].asString() + : "", + "", + "", + "", + node["layout"].asString()}; + } + parentWorkspace = node; + } else if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") && + (node["focused"].asBool())) { + // found node + spdlog::trace("actual output {}, output found {}, node (focused) found {}", bar_.output->name, + output, node["name"].asString()); + auto app_id = node["app_id"].isString() ? node["app_id"].asString() + : node["window_properties"]["instance"].asString(); + const auto app_class = node["window_properties"]["class"].isString() + ? node["window_properties"]["class"].asString() + : ""; + const auto shell = node["shell"].isString() ? node["shell"].asString() : ""; + int nb = node.size(); + int floating_count = 0; + std::string workspace_layout = ""; + if (!parentWorkspace.isNull()) { + std::pair all_leaf_nodes = leafNodesInWorkspace(parentWorkspace); + nb = all_leaf_nodes.first; + floating_count = all_leaf_nodes.second; + workspace_layout = parentWorkspace["layout"].asString(); + } + return {nb, + floating_count, + node["id"].asInt(), + Glib::Markup::escape_text(node["name"].asString()), + app_id, + app_class, + shell, + workspace_layout}; + } + + // iterate + auto [nb, f, id, name, app_id, app_class, shell, workspace_layout] = + gfnWithWorkspace(node["nodes"], output, config_, bar_, parentWorkspace, node); + auto [nb2, f2, id2, name2, app_id2, app_class2, shell2, workspace_layout2] = + gfnWithWorkspace(node["floating_nodes"], output, config_, bar_, parentWorkspace, node); + + // if ((id > 0 || ((id2 < 0 || name2.empty()) && id > -1)) && !name.empty()) { + if ((id > 0) || (id2 < 0 && id > -1)) { + return {nb, f, id, name, app_id, app_class, shell, workspace_layout}; + } else if (id2 > 0 && !name2.empty()) { + return {nb2, f2, id2, name2, app_id2, app_class, shell2, workspace_layout2}; + } + } + + // this only comes into effect when no focused children are present + if (config_["all-outputs"].asBool() && config_["offscreen-css"].asBool() && + immediateParent["type"].asString() == "workspace") { + std::pair all_leaf_nodes = leafNodesInWorkspace(immediateParent); + // using an empty string as default ensures that no window depending styles are set due to the + // checks above for !name.empty() + return {all_leaf_nodes.first, + all_leaf_nodes.second, + 0, + (all_leaf_nodes.first > 0 || all_leaf_nodes.second > 0) + ? config_["offscreen-css-text"].asString() + : "", + "", + "", + "", + immediateParent["layout"].asString()}; + } + + return {0, 0, -1, "", "", "", "", ""}; +} + +std::tuple Window::getFocusedNode(const Json::Value& nodes, std::string& output) { - Json::Value placeholder = 0; - return gfnWithWorkspace(nodes, output, config_, bar_, placeholder); + Json::Value placeholder = Json::Value::null; + return gfnWithWorkspace(nodes, output, config_, bar_, placeholder, placeholder); } void Window::getTree() { @@ -262,6 +357,7 @@ void Window::getTree() { ipc_.sendCmd(IPC_GET_TREE); } catch (const std::exception& e) { spdlog::error("Window: {}", e.what()); + spdlog::trace("Window::getTree exception"); } } diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index b7e51e4..c1cfd5a 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -130,6 +130,10 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) { // In a first pass, the maximum "num" value is computed to enqueue // unnumbered workspaces behind numbered ones when computing the sort // attribute. + // + // Note: if the 'alphabetical_sort' option is true, the user is in + // agreement that the "workspace prev/next" commands may not follow + // the order displayed in Waybar. int max_num = -1; for (auto &workspace : workspaces_) { max_num = std::max(workspace["num"].asInt(), max_num); @@ -143,16 +147,19 @@ void Workspaces::onCmd(const struct Ipc::ipc_response &res) { } } std::sort(workspaces_.begin(), workspaces_.end(), - [](const Json::Value &lhs, const Json::Value &rhs) { + [this](const Json::Value &lhs, const Json::Value &rhs) { auto lname = lhs["name"].asString(); auto rname = rhs["name"].asString(); int l = lhs["sort"].asInt(); int r = rhs["sort"].asInt(); - if (l == r) { + if (l == r || config_["alphabetical_sort"].asBool()) { // In case both integers are the same, lexicographical // sort. The code above already ensure that this will only - // happend in case of explicitly numbered workspaces. + // happened in case of explicitly numbered workspaces. + // + // Additionally, if the config specifies to sort workspaces + // alphabetically do this here. return lname < rname; } @@ -226,9 +233,10 @@ auto Workspaces::update() -> void { std::string output = (*it)["name"].asString(); if (config_["format"].isString()) { auto format = config_["format"].asString(); - output = fmt::format(format, fmt::arg("icon", getIcon(output, *it)), + output = fmt::format(fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)), fmt::arg("value", output), fmt::arg("name", trimWorkspaceName(output)), - fmt::arg("index", (*it)["num"].asString())); + fmt::arg("index", (*it)["num"].asString()), + fmt::arg("output", (*it)["output"].asString())); } if (!config_["disable-markup"].asBool()) { static_cast(button.get_children()[0])->set_markup(output); @@ -252,11 +260,9 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { try { if (node["target_output"].isString()) { ipc_.sendCmd(IPC_COMMAND, - fmt::format(workspace_switch_cmd_ + "; move workspace to output \"{}\"; " + - workspace_switch_cmd_, - "--no-auto-back-and-forth", node["name"].asString(), - node["target_output"].asString(), "--no-auto-back-and-forth", - node["name"].asString())); + fmt::format(persistent_workspace_switch_cmd_, "--no-auto-back-and-forth", + node["name"].asString(), node["target_output"].asString(), + "--no-auto-back-and-forth", node["name"].asString())); } else { ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"", config_["disable-auto-back-and-forth"].asBool() @@ -273,7 +279,7 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { } std::string Workspaces::getIcon(const std::string &name, const Json::Value &node) { - std::vector keys = {name, "urgent", "focused", "visible", "default"}; + std::vector keys = {"urgent", "focused", name, "visible", "default"}; for (auto const &key : keys) { if (key == "focused" || key == "visible" || key == "urgent") { if (config_["format-icons"][key].isString() && node[key].asBool()) { @@ -321,11 +327,17 @@ bool Workspaces::handleScroll(GdkEventScroll *e) { return true; } } + if (!config_["warp-on-scroll"].asBool()) { + ipc_.sendCmd(IPC_COMMAND, fmt::format("mouse_warping none")); + } try { ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, "--no-auto-back-and-forth", name)); } catch (const std::exception &e) { spdlog::error("Workspaces: {}", e.what()); } + if (!config_["warp-on-scroll"].asBool()) { + ipc_.sendCmd(IPC_COMMAND, fmt::format("mouse_warping container")); + } return true; } diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index eca05a7..5ef2f4c 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -11,8 +11,18 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val #if defined(__FreeBSD__) // try to read sysctl? #else - if (config_["hwmon-path"].isString()) { - file_path_ = config_["hwmon-path"].asString(); + 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()) { file_path_ = (*std::filesystem::directory_iterator(config_["hwmon-path-abs"].asString())) .path() @@ -55,7 +65,7 @@ auto waybar::modules::Temperature::update() -> void { } auto max_temp = config_["critical-threshold"].isInt() ? config_["critical-threshold"].asInt() : 0; - label_.set_markup(fmt::format(format, fmt::arg("temperatureC", temperature_c), + label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("temperatureC", temperature_c), fmt::arg("temperatureF", temperature_f), fmt::arg("temperatureK", temperature_k), fmt::arg("icon", getIcon(temperature_c, "", max_temp)))); @@ -64,9 +74,9 @@ auto waybar::modules::Temperature::update() -> void { if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text(fmt::format(tooltip_format, fmt::arg("temperatureC", temperature_c), - fmt::arg("temperatureF", temperature_f), - fmt::arg("temperatureK", temperature_k))); + label_.set_tooltip_text(fmt::format( + fmt::runtime(tooltip_format), fmt::arg("temperatureC", temperature_c), + fmt::arg("temperatureF", temperature_f), fmt::arg("temperatureK", temperature_k))); } // Call parent update ALabel::update(); diff --git a/src/modules/upower/upower.cpp b/src/modules/upower/upower.cpp index eb29913..1262d0a 100644 --- a/src/modules/upower/upower.cpp +++ b/src/modules/upower/upower.cpp @@ -5,10 +5,8 @@ #include #include -#include "gtkmm/icontheme.h" -#include "gtkmm/label.h" #include "gtkmm/tooltip.h" -#include "modules/upower/upower_tooltip.hpp" +#include "util/gtk_icon.hpp" namespace waybar::modules::upower { UPower::UPower(const std::string& id, const Json::Value& config) @@ -25,6 +23,8 @@ UPower::UPower(const std::string& id, const Json::Value& config) 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(); @@ -195,8 +195,26 @@ void UPower::addDevice(UpDevice* device) { void UPower::setDisplayDevice() { std::lock_guard guard(m_Mutex); - displayDevice = up_client_get_display_device(client); - g_signal_connect(displayDevice, "notify", G_CALLBACK(deviceNotify_cb), this); + + 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() { @@ -278,14 +296,22 @@ auto UPower::update() -> void { double percentage; gint64 time_empty; gint64 time_full; - gchar* icon_name; + gchar* icon_name{(char*)'\0'}; + std::string percentString{""}; + std::string time_format{""}; - 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); + bool displayDeviceValid{false}; - bool displayDeviceValid = - kind == UpDeviceKind::UP_DEVICE_KIND_BATTERY || kind == UpDeviceKind::UP_DEVICE_KIND_UPS; + 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); @@ -308,36 +334,34 @@ auto UPower::update() -> void { event_box_.set_visible(true); - // 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 - std::string percentString = ""; if (displayDeviceValid) { - percentString = std::to_string(int(percentage + 0.5)) + "%"; - } + // 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); + } - // Label format - std::string time_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; + // 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(showAltText ? format_alt : format, fmt::arg("percentage", percentString), - fmt::arg("time", time_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) { @@ -348,7 +372,7 @@ auto UPower::update() -> void { label_.set_markup(onlySpaces ? "" : label_format); // Set icon - if (icon_name == NULL || !Gtk::IconTheme::get_default()->has_icon(icon_name)) { + 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); diff --git a/src/modules/upower/upower_tooltip.cpp b/src/modules/upower/upower_tooltip.cpp index bec55c7..45544bb 100644 --- a/src/modules/upower/upower_tooltip.cpp +++ b/src/modules/upower/upower_tooltip.cpp @@ -2,9 +2,9 @@ #include "gtkmm/box.h" #include "gtkmm/enums.h" -#include "gtkmm/icontheme.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_) @@ -29,7 +29,7 @@ UPowerTooltip::~UPowerTooltip() {} uint UPowerTooltip::updateTooltip(Devices& devices) { // Removes all old devices for (auto child : contentBox->get_children()) { - child->~Widget(); + delete child; } uint deviceCount = 0; @@ -62,7 +62,7 @@ uint UPowerTooltip::updateTooltip(Devices& devices) { std::string deviceIconName = getDeviceIcon(kind); Gtk::Image* deviceIcon = new Gtk::Image(); deviceIcon->set_pixel_size(iconSize); - if (!Gtk::IconTheme::get_default()->has_icon(deviceIconName)) { + if (!DefaultGtkIconThemeWrapper::has_icon(deviceIconName)) { deviceIconName = "battery-missing-symbolic"; } deviceIcon->set_from_icon_name(deviceIconName, Gtk::ICON_SIZE_INVALID); @@ -79,7 +79,7 @@ uint UPowerTooltip::updateTooltip(Devices& devices) { // Set icon Gtk::Image* icon = new Gtk::Image(); icon->set_pixel_size(iconSize); - if (icon_name == NULL || !Gtk::IconTheme::get_default()->has_icon(icon_name)) { + 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); diff --git a/src/modules/user.cpp b/src/modules/user.cpp index 2f7c6e9..418fc58 100644 --- a/src/modules/user.cpp +++ b/src/modules/user.cpp @@ -127,16 +127,16 @@ auto User::update() -> void { auto startSystemTime = currentSystemTime - workSystemTimeSeconds; long workSystemDays = uptimeSeconds / 86400; - auto label = fmt::format(ALabel::format_, fmt::arg("up_H", fmt::format("{:%H}", startSystemTime)), - fmt::arg("up_M", fmt::format("{:%M}", startSystemTime)), - fmt::arg("up_d", fmt::format("{:%d}", startSystemTime)), - fmt::arg("up_m", fmt::format("{:%m}", startSystemTime)), - fmt::arg("up_Y", fmt::format("{:%Y}", startSystemTime)), - fmt::arg("work_d", workSystemDays), - fmt::arg("work_H", fmt::format("{:%H}", workSystemTimeSeconds)), - fmt::arg("work_M", fmt::format("{:%M}", workSystemTimeSeconds)), - fmt::arg("work_S", fmt::format("{:%S}", workSystemTimeSeconds)), - fmt::arg("user", systemUser)); + auto label = fmt::format( + fmt::runtime(ALabel::format_), fmt::arg("up_H", fmt::format("{:%H}", startSystemTime)), + fmt::arg("up_M", fmt::format("{:%M}", startSystemTime)), + fmt::arg("up_d", fmt::format("{:%d}", startSystemTime)), + fmt::arg("up_m", fmt::format("{:%m}", startSystemTime)), + fmt::arg("up_Y", fmt::format("{:%Y}", startSystemTime)), fmt::arg("work_d", workSystemDays), + fmt::arg("work_H", fmt::format("{:%H}", workSystemTimeSeconds)), + fmt::arg("work_M", fmt::format("{:%M}", workSystemTimeSeconds)), + fmt::arg("work_S", fmt::format("{:%S}", workSystemTimeSeconds)), + fmt::arg("user", systemUser)); ALabel::label_.set_markup(label); AIconLabel::update(); } diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 9a12a9b..b2d9b39 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -1,15 +1,23 @@ #include "modules/wireplumber.hpp" +#include + +bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; } + waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Value& config) : ALabel(config, "wireplumber", id, "{volume}%"), wp_core_(nullptr), apis_(nullptr), om_(nullptr), + mixer_api_(nullptr), + def_nodes_api_(nullptr), + default_node_name_(nullptr), pending_plugins_(0), muted_(false), volume_(0.0), + min_step_(0.0), node_id_(0) { - wp_init(WP_INIT_ALL); + wp_init(WP_INIT_PIPEWIRE); wp_core_ = wp_core_new(NULL, NULL); apis_ = g_ptr_array_new_with_free_func(g_object_unref); om_ = wp_object_manager_new(); @@ -18,48 +26,49 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val loadRequiredApiModules(); + spdlog::debug("[{}]: connecting to pipewire...", this->name_); + if (!wp_core_connect(wp_core_)) { + spdlog::error("[{}]: Could not connect to PipeWire", this->name_); throw std::runtime_error("Could not connect to PipeWire\n"); } + spdlog::debug("[{}]: connected!", this->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)); } waybar::modules::Wireplumber::~Wireplumber() { g_clear_pointer(&apis_, g_ptr_array_unref); g_clear_object(&om_); g_clear_object(&wp_core_); + g_clear_object(&mixer_api_); + g_clear_object(&def_nodes_api_); + g_free(default_node_name_); } -uint32_t waybar::modules::Wireplumber::getDefaultNodeId(waybar::modules::Wireplumber* self) { - uint32_t id; - g_autoptr(WpPlugin) def_nodes_api = wp_plugin_find(self->wp_core_, "default-nodes-api"); +void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* self, uint32_t id) { + spdlog::debug("[{}]: updating node name with node.id {}", self->name_, id); - if (!def_nodes_api) { - throw std::runtime_error("Default nodes API is not loaded\n"); + if (!isValidNodeId(id)) { + spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node name update.", self->name_, id); + return; } - g_signal_emit_by_name(def_nodes_api, "get-default-node", "Audio/Sink", &id); - - if (id <= 0 || id >= G_MAXUINT32) { - auto err = fmt::format("'{}' is not a valid ID (returned by default-nodes-api)\n", id); - throw std::runtime_error(err); - } - - return id; -} - -void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* self) { - auto proxy = static_cast( - wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, - "bound-id", "=u", self->node_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, NULL)); if (!proxy) { - throw std::runtime_error(fmt::format("Object '{}' not found\n", self->node_id_)); + auto err = fmt::format("Object '{}' not found\n", id); + spdlog::error("[{}]: {}", self->name_, err); + throw std::runtime_error(err); } g_autoptr(WpProperties) properties = @@ -73,42 +82,150 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* auto description = wp_properties_get(properties, "node.description"); self->node_name_ = nick ? nick : description; + spdlog::debug("[{}]: Updating node name to: {}", self->name_, self->node_name_); } -void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self) { - double vol; +void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self, uint32_t id) { + spdlog::debug("[{}]: updating volume", self->name_); GVariant* variant = NULL; - g_autoptr(WpPlugin) mixer_api = wp_plugin_find(self->wp_core_, "mixer-api"); - g_signal_emit_by_name(mixer_api, "get-volume", self->node_id_, &variant); + + if (!isValidNodeId(id)) { + spdlog::error("[{}]: '{}' is not a valid node ID. Ignoring volume update.", self->name_, id); + return; + } + + g_signal_emit_by_name(self->mixer_api_, "get-volume", id, &variant); + if (!variant) { - auto err = fmt::format("Node {} does not support volume\n", self->node_id_); + auto err = fmt::format("Node {} does not support volume\n", id); + spdlog::error("[{}]: {}", self->name_, err); throw std::runtime_error(err); } - g_variant_lookup(variant, "volume", "d", &vol); + g_variant_lookup(variant, "volume", "d", &self->volume_); + g_variant_lookup(variant, "step", "d", &self->min_step_); g_variant_lookup(variant, "mute", "b", &self->muted_); g_clear_pointer(&variant, g_variant_unref); - self->volume_ = std::round(vol * 100.0F); self->dp.emit(); } +void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id) { + 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)); + + if (!node) { + spdlog::warn("[{}]: (onMixerChanged) - Object with id {} not found", self->name_, id); + return; + } + + const gchar* name = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name"); + + if (self->node_id_ != id) { + spdlog::debug( + "[{}]: (onMixerChanged) - ignoring mixer update for node: id: {}, name: {} as it is not " + "the default node: {} with id: {}", + self->name_, id, name, self->default_node_name_, self->node_id_); + return; + } + + spdlog::debug("[{}]: (onMixerChanged) - Need to update volume for node with id {} and name {}", + self->name_, id, name); + updateVolume(self, id); +} + +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); + + if (!isValidNodeId(default_node_id)) { + spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node change.", self->name_, + default_node_id); + 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)); + + if (!node) { + spdlog::warn("[{}]: (onDefaultNodesApiChanged) - Object with id {} not found", self->name_, + default_node_id); + return; + } + + const gchar* default_node_name = + 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); + + if (g_strcmp0(self->default_node_name_, default_node_name) == 0) { + spdlog::debug( + "[{}]: (onDefaultNodesApiChanged) - Default node has not changed. Node(name: {}, id: {}). " + "Ignoring.", + self->name_, self->default_node_name_, default_node_id); + return; + } + + spdlog::debug( + "[{}]: (onDefaultNodesApiChanged) - Default node changed to -> Node(name: {}, id: {})", + self->name_, default_node_name, default_node_id); + + 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); +} + void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wireplumber* self) { - self->node_id_ = - self->config_["node-id"].isInt() ? self->config_["node-id"].asInt() : getDefaultNodeId(self); + spdlog::debug("[{}]: onObjectManagerInstalled", self->name_); - g_autoptr(WpPlugin) mixer_api = wp_plugin_find(self->wp_core_, "mixer-api"); + self->def_nodes_api_ = wp_plugin_find(self->wp_core_, "default-nodes-api"); - updateVolume(self); - updateNodeName(self); - g_signal_connect_swapped(mixer_api, "changed", (GCallback)updateVolume, self); + if (!self->def_nodes_api_) { + 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_) { + spdlog::error("[{}]: mixer api is not loaded.", self->name_); + throw std::runtime_error("Mixer api is not loaded\n"); + } + + g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Sink", + &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_) { + spdlog::debug("[{}]: (onObjectManagerInstalled) - default configured node name: {} and id: {}", + self->name_, self->default_node_name_, self->node_id_); + } + + updateVolume(self, self->node_id_); + updateNodeName(self, self->node_id_); + + g_signal_connect_swapped(self->mixer_api_, "changed", (GCallback)onMixerChanged, self); + g_signal_connect_swapped(self->def_nodes_api_, "changed", (GCallback)onDefaultNodesApiChanged, + self); } 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; if (!wp_object_activate_finish(p, res, &error)) { + spdlog::error("[{}]: error activating plugin: {}", self->name_, error->message); throw std::runtime_error(error->message); } @@ -118,6 +235,7 @@ void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* } void waybar::modules::Wireplumber::activatePlugins() { + spdlog::debug("[{}]: activating plugins", name_); for (uint16_t i = 0; i < apis_->len; i++) { WpPlugin* plugin = static_cast(g_ptr_array_index(apis_, i)); pending_plugins_++; @@ -127,13 +245,13 @@ void waybar::modules::Wireplumber::activatePlugins() { } void waybar::modules::Wireplumber::prepare() { - wp_object_manager_add_interest(om_, WP_TYPE_NODE, NULL); - wp_object_manager_add_interest(om_, WP_TYPE_GLOBAL_PROXY, NULL); - wp_object_manager_request_object_features(om_, WP_TYPE_GLOBAL_PROXY, - WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL); + 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); } void waybar::modules::Wireplumber::loadRequiredApiModules() { + spdlog::debug("[{}]: loading required modules", name_); g_autoptr(GError) error = NULL; if (!wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", NULL, @@ -165,11 +283,12 @@ auto waybar::modules::Wireplumber::update() -> void { label_.get_style_context()->remove_class("muted"); } - std::string markup = fmt::format(format, fmt::arg("node_name", node_name_), - fmt::arg("volume", volume_), fmt::arg("icon", getIcon(volume_))); + int vol = round(volume_ * 100.0); + std::string markup = fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_), + fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol))); label_.set_markup(markup); - getState(volume_); + getState(vol); if (tooltipEnabled()) { if (tooltip_format.empty() && config_["tooltip-format"].isString()) { @@ -177,9 +296,9 @@ auto waybar::modules::Wireplumber::update() -> void { } if (!tooltip_format.empty()) { - label_.set_tooltip_text(fmt::format(tooltip_format, fmt::arg("node_name", node_name_), - fmt::arg("volume", volume_), - fmt::arg("icon", getIcon(volume_)))); + label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format), + fmt::arg("node_name", node_name_), + fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol)))); } else { label_.set_tooltip_text(node_name_); } @@ -188,3 +307,49 @@ auto waybar::modules::Wireplumber::update() -> void { // Call parent update ALabel::update(); } + +bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { + if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { + return AModule::handleScroll(e); + } + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + if (config_["reverse-scrolling"].asInt() == 1) { + if (dir == SCROLL_DIR::UP) { + dir = SCROLL_DIR::DOWN; + } else if (dir == SCROLL_DIR::DOWN) { + dir = SCROLL_DIR::UP; + } + } + double max_volume = 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; + } + + if (step < min_step_) step = min_step_; + + double new_vol = volume_; + if (dir == SCROLL_DIR::UP) { + if (volume_ < max_volume) { + new_vol = volume_ + step; + if (new_vol > max_volume) new_vol = max_volume; + } + } else if (dir == SCROLL_DIR::DOWN) { + if (volume_ > 0) { + new_vol = volume_ - step; + if (new_vol < 0) new_vol = 0; + } + } + if (new_vol != volume_) { + GVariant* variant = g_variant_new_double(new_vol); + gboolean ret; + g_signal_emit_by_name(mixer_api_, "set-volume", node_id_, variant, &ret); + } + return true; +} diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 97d84bd..9e09d7a 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -20,6 +20,7 @@ #include "glibmm/fileutils.h" #include "glibmm/refptr.h" #include "util/format.hpp" +#include "util/rewrite_string.hpp" #include "util/string.hpp" namespace waybar::modules::wlr { @@ -102,8 +103,11 @@ Glib::RefPtr get_desktop_app_info(const std::string &app_id desktop_file = desktop_list[0][i]; } else { auto tmp_info = Gio::DesktopAppInfo::create(desktop_list[0][i]); - auto startup_class = tmp_info->get_startup_wm_class(); + if (!tmp_info) + // see https://github.com/Alexays/Waybar/issues/1446 + continue; + auto startup_class = tmp_info->get_startup_wm_class(); if (startup_class == app_id) { desktop_file = desktop_list[0][i]; break; @@ -265,14 +269,14 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0} { zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); - button_.set_relief(Gtk::RELIEF_NONE); + button.set_relief(Gtk::RELIEF_NONE); content_.add(text_before_); content_.add(icon_); content_.add(text_after_); content_.show(); - button_.add(content_); + button.add(content_); format_before_.clear(); format_after_.clear(); @@ -314,20 +318,20 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, config_["on-click-right"].isString()) { } - 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_motion_notify_event().connect(sigc::mem_fun(*this, &Task::handle_motion_notify), + 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_.drag_source_set(target_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE); - button_.drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE); + button.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Task::handle_motion_notify), + false); - button_.signal_drag_data_get().connect(sigc::mem_fun(*this, &Task::handle_drag_data_get), false); - button_.signal_drag_data_received().connect( - sigc::mem_fun(*this, &Task::handle_drag_data_received), false); + button.drag_source_set(target_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE); + button.drag_dest_set(target_entries, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_MOVE); + + button.signal_drag_data_get().connect(sigc::mem_fun(*this, &Task::handle_drag_data_get), false); + button.signal_drag_data_received().connect(sigc::mem_fun(*this, &Task::handle_drag_data_received), + false); } Task::~Task() { @@ -336,7 +340,7 @@ Task::~Task() { handle_ = nullptr; } if (button_visible_) { - tbar_->remove_button(button_); + tbar_->remove_button(button); button_visible_ = false; } } @@ -435,8 +439,8 @@ void Task::handle_output_enter(struct wl_output *output) { if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) { /* The task entered the output of the current bar make the button visible */ - tbar_->add_button(button_); - button_.show(); + tbar_->add_button(button); + button.show(); button_visible_ = true; spdlog::debug("{} now visible on {}", repr(), bar_.output->name); } @@ -447,8 +451,8 @@ void Task::handle_output_leave(struct wl_output *output) { if (button_visible_ && !tbar_->all_outputs() && tbar_->show_output(output)) { /* The task left the output of the current bar, make the button invisible */ - tbar_->remove_button(button_); - button_.hide(); + tbar_->remove_button(button); + button.hide(); button_visible_ = false; spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); } @@ -470,31 +474,31 @@ void Task::handle_done() { spdlog::debug("{} changed", repr()); if (state_ & MAXIMIZED) { - button_.get_style_context()->add_class("maximized"); + button.get_style_context()->add_class("maximized"); } else if (!(state_ & MAXIMIZED)) { - button_.get_style_context()->remove_class("maximized"); + button.get_style_context()->remove_class("maximized"); } if (state_ & MINIMIZED) { - button_.get_style_context()->add_class("minimized"); + button.get_style_context()->add_class("minimized"); } else if (!(state_ & MINIMIZED)) { - button_.get_style_context()->remove_class("minimized"); + button.get_style_context()->remove_class("minimized"); } if (state_ & ACTIVE) { - button_.get_style_context()->add_class("active"); + button.get_style_context()->add_class("active"); } else if (!(state_ & ACTIVE)) { - button_.get_style_context()->remove_class("active"); + button.get_style_context()->remove_class("active"); } if (state_ & FULLSCREEN) { - button_.get_style_context()->add_class("fullscreen"); + button.get_style_context()->add_class("fullscreen"); } else if (!(state_ & FULLSCREEN)) { - button_.get_style_context()->remove_class("fullscreen"); + button.get_style_context()->remove_class("fullscreen"); } if (config_["active-first"].isBool() && config_["active-first"].asBool() && active()) - tbar_->move_button(button_, 0); + tbar_->move_button(button, 0); tbar_->dp.emit(); } @@ -503,11 +507,11 @@ void Task::handle_closed() { spdlog::debug("{} closed", repr()); zwlr_foreign_toplevel_handle_v1_destroy(handle_); handle_ = nullptr; + tbar_->remove_task(id_); if (button_visible_) { - tbar_->remove_button(button_); + tbar_->remove_button(button); button_visible_ = false; } - tbar_->remove_task(id_); } bool Task::handle_clicked(GdkEventButton *bt) { @@ -560,12 +564,12 @@ bool Task::handle_button_release(GdkEventButton *bt) { bool Task::handle_motion_notify(GdkEventMotion *mn) { if (drag_start_button == -1) return false; - if (button_.drag_check_threshold(drag_start_x, drag_start_y, mn->x, mn->y)) { + if (button.drag_check_threshold(drag_start_x, drag_start_y, mn->x, mn->y)) { /* start drag in addition to other assigned action */ auto target_list = Gtk::TargetList::create(target_entries); auto refptr = Glib::RefPtr(target_list); auto drag_context = - button_.drag_begin(refptr, Gdk::DragAction::ACTION_MOVE, drag_start_button, (GdkEvent *)mn); + button.drag_begin(refptr, Gdk::DragAction::ACTION_MOVE, drag_start_button, (GdkEvent *)mn); } return false; @@ -574,7 +578,7 @@ bool Task::handle_motion_notify(GdkEventMotion *mn) { void Task::handle_drag_data_get(const Glib::RefPtr &context, Gtk::SelectionData &selection_data, guint info, guint time) { spdlog::debug("drag_data_get"); - void *button_addr = (void *)&this->button_; + void *button_addr = (void *)&this->button; selection_data.set("WAYBAR_TOPLEVEL", 32, (const guchar *)&button_addr, sizeof(gpointer)); } @@ -585,16 +589,16 @@ void Task::handle_drag_data_received(const Glib::RefPtr &conte gpointer handle = *(gpointer *)selection_data.get_data(); auto dragged_button = (Gtk::Button *)handle; - if (dragged_button == &this->button_) return; + if (dragged_button == &this->button) return; auto parent_of_dragged = dragged_button->get_parent(); - auto parent_of_dest = this->button_.get_parent(); + auto parent_of_dest = this->button.get_parent(); if (parent_of_dragged != parent_of_dest) return; auto box = (Gtk::Box *)parent_of_dragged; - auto position_prop = box->child_property_position(this->button_); + auto position_prop = box->child_property_position(this->button); auto position = position_prop.get_value(); box->reorder_child(*dragged_button, position); @@ -615,9 +619,13 @@ void Task::update() { app_id = Glib::Markup::escape_text(app_id); } if (!format_before_.empty()) { - auto txt = fmt::format(format_before_, fmt::arg("title", title), fmt::arg("name", name), - fmt::arg("app_id", app_id), fmt::arg("state", state_string()), - fmt::arg("short_state", state_string(true))); + auto txt = + fmt::format(fmt::runtime(format_before_), fmt::arg("title", title), fmt::arg("name", name), + fmt::arg("app_id", app_id), fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true))); + + txt = waybar::util::rewriteString(txt, config_["rewrite"]); + if (markup) text_before_.set_markup(txt); else @@ -625,9 +633,13 @@ void Task::update() { text_before_.show(); } if (!format_after_.empty()) { - auto txt = fmt::format(format_after_, fmt::arg("title", title), fmt::arg("name", name), - fmt::arg("app_id", app_id), fmt::arg("state", state_string()), - fmt::arg("short_state", state_string(true))); + auto txt = + fmt::format(fmt::runtime(format_after_), fmt::arg("title", title), fmt::arg("name", name), + fmt::arg("app_id", app_id), fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true))); + + txt = waybar::util::rewriteString(txt, config_["rewrite"]); + if (markup) text_after_.set_markup(txt); else @@ -636,13 +648,14 @@ void Task::update() { } if (!format_tooltip_.empty()) { - auto txt = fmt::format(format_tooltip_, fmt::arg("title", title), fmt::arg("name", name), - fmt::arg("app_id", app_id), fmt::arg("state", state_string()), - fmt::arg("short_state", state_string(true))); + auto txt = + fmt::format(fmt::runtime(format_tooltip_), fmt::arg("title", title), fmt::arg("name", name), + fmt::arg("app_id", app_id), fmt::arg("state", state_string()), + fmt::arg("short_state", state_string(true))); if (markup) - button_.set_tooltip_markup(txt); + button.set_tooltip_markup(txt); else - button_.set_tooltip_text(txt); + button.set_tooltip_text(txt); } } @@ -704,6 +717,7 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class("empty"); event_box_.add(box_); struct wl_display *display = Client::inst()->wl_display; @@ -785,6 +799,17 @@ void Taskbar::update() { t->update(); } + if (config_["sort-by-app-id"].asBool()) { + std::stable_sort(tasks_.begin(), tasks_.end(), + [](const std::unique_ptr &a, const std::unique_ptr &b) { + return a->app_id() < b->app_id(); + }); + + for (unsigned long i = 0; i < tasks_.size(); i++) { + move_button(tasks_[i]->button, i); + } + } + AModule::update(); } @@ -845,11 +870,19 @@ void Taskbar::handle_finished() { manager_ = nullptr; } -void Taskbar::add_button(Gtk::Button &bt) { box_.pack_start(bt, false, false); } +void Taskbar::add_button(Gtk::Button &bt) { + box_.pack_start(bt, false, false); + box_.get_style_context()->remove_class("empty"); +} void Taskbar::move_button(Gtk::Button &bt, int pos) { box_.reorder_child(bt, pos); } -void Taskbar::remove_button(Gtk::Button &bt) { box_.remove(bt); } +void Taskbar::remove_button(Gtk::Button &bt) { + box_.remove(bt); + if (tasks_.empty()) { + box_.get_style_context()->add_class("empty"); + } +} void Taskbar::remove_task(uint32_t id) { auto it = std::find_if(std::begin(tasks_), std::end(tasks_), diff --git a/src/modules/wlr/workspace_manager.cpp b/src/modules/wlr/workspace_manager.cpp index 6f11e1f..8933d69 100644 --- a/src/modules/wlr/workspace_manager.cpp +++ b/src/modules/wlr/workspace_manager.cpp @@ -9,6 +9,7 @@ #include #include +#include "client.hpp" #include "gtkmm/widget.h" #include "modules/wlr/workspace_manager_binding.hpp" @@ -63,7 +64,7 @@ WorkspaceManager::WorkspaceManager(const std::string &id, const waybar::Bar &bar auto WorkspaceManager::workspace_comparator() const -> std::function &, std::unique_ptr &)> { - return [=](std::unique_ptr &lhs, std::unique_ptr &rhs) { + return [=, this](std::unique_ptr &lhs, std::unique_ptr &rhs) { auto is_name_less = lhs->get_name() < rhs->get_name(); auto is_name_eq = lhs->get_name() == rhs->get_name(); auto is_coords_less = lhs->get_coords() < rhs->get_coords(); @@ -72,7 +73,7 @@ auto WorkspaceManager::workspace_comparator() const try { auto is_number_less = std::stoi(lhs->get_name()) < std::stoi(rhs->get_name()); return is_number_less; - } catch (std::invalid_argument) { + } catch (const std::invalid_argument &) { } } @@ -166,8 +167,20 @@ WorkspaceManager::~WorkspaceManager() { return; } - zext_workspace_manager_v1_destroy(workspace_manager_); - workspace_manager_ = nullptr; + wl_display *display = Client::inst()->wl_display; + + // Send `stop` request and wait for one roundtrip. This is not quite correct as + // the protocol encourages us to wait for the .finished event, but it should work + // with wlroots workspace manager implementation. + zext_workspace_manager_v1_stop(workspace_manager_); + wl_display_roundtrip(display); + + // If the .finished handler is still not executed, destroy the workspace manager here. + if (workspace_manager_) { + spdlog::warn("Foreign toplevel manager destroyed before .finished event"); + zext_workspace_manager_v1_destroy(workspace_manager_); + workspace_manager_ = nullptr; + } } auto WorkspaceManager::remove_workspace_group(uint32_t id) -> void { @@ -195,6 +208,38 @@ WorkspaceGroup::WorkspaceGroup(const Bar &bar, Gtk::Box &box, const Json::Value add_workspace_group_listener(workspace_group_handle, this); } +auto WorkspaceGroup::fill_persistent_workspaces() -> void { + if (config_["persistent_workspaces"].isObject() && !workspace_manager_.all_outputs()) { + const Json::Value &p_workspaces = config_["persistent_workspaces"]; + const std::vector p_workspaces_names = p_workspaces.getMemberNames(); + + for (const std::string &p_w_name : p_workspaces_names) { + const Json::Value &p_w = p_workspaces[p_w_name]; + if (p_w.isArray() && !p_w.empty()) { + // Adding to target outputs + for (const Json::Value &output : p_w) { + if (output.asString() == bar_.output->name) { + persistent_workspaces_.push_back(p_w_name); + break; + } + } + } else { + // Adding to all outputs + persistent_workspaces_.push_back(p_w_name); + } + } + } +} + +auto WorkspaceGroup::create_persistent_workspaces() -> void { + for (const std::string &p_w_name : persistent_workspaces_) { + auto new_id = ++workspace_global_id; + workspaces_.push_back( + std::make_unique(bar_, config_, *this, nullptr, new_id, p_w_name)); + spdlog::debug("Workspace {} created", new_id); + } +} + auto WorkspaceGroup::active_only() const -> bool { return workspace_manager_.active_only(); } auto WorkspaceGroup::creation_delayed() const -> bool { return workspace_manager_.creation_delayed(); @@ -215,8 +260,13 @@ WorkspaceGroup::~WorkspaceGroup() { auto WorkspaceGroup::handle_workspace_create(zext_workspace_handle_v1 *workspace) -> void { auto new_id = ++workspace_global_id; - workspaces_.push_back(std::make_unique(bar_, config_, *this, workspace, new_id)); + workspaces_.push_back(std::make_unique(bar_, config_, *this, workspace, new_id, "")); spdlog::debug("Workspace {} created", new_id); + if (!persistent_created_) { + fill_persistent_workspaces(); + create_persistent_workspaces(); + persistent_created_ = true; + } } auto WorkspaceGroup::handle_remove() -> void { @@ -315,13 +365,18 @@ auto WorkspaceGroup::sort_workspaces() -> void { auto WorkspaceGroup::remove_button(Gtk::Button &button) -> void { box_.remove(button); } Workspace::Workspace(const Bar &bar, const Json::Value &config, WorkspaceGroup &workspace_group, - zext_workspace_handle_v1 *workspace, uint32_t id) + zext_workspace_handle_v1 *workspace, uint32_t id, std::string name) : bar_(bar), config_(config), workspace_group_(workspace_group), workspace_handle_(workspace), - id_(id) { - add_workspace_listener(workspace, this); + id_(id), + name_(name) { + if (workspace) { + add_workspace_listener(workspace, this); + } else { + state_ = (uint32_t)State::EMPTY; + } auto config_format = config["format"]; @@ -366,7 +421,7 @@ Workspace::~Workspace() { } auto Workspace::update() -> void { - label_.set_markup(fmt::format(format_, fmt::arg("name", name_), + label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("name", name_), fmt::arg("icon", with_icon_ ? get_icon() : ""))); } @@ -388,9 +443,15 @@ auto Workspace::handle_state(const std::vector &state) -> void { } auto Workspace::handle_remove() -> void { - zext_workspace_handle_v1_destroy(workspace_handle_); - workspace_handle_ = nullptr; - workspace_group_.remove_workspace(id_); + if (workspace_handle_) { + zext_workspace_handle_v1_destroy(workspace_handle_); + workspace_handle_ = nullptr; + } + if (!persistent_) { + workspace_group_.remove_workspace(id_); + } else { + state_ = (uint32_t)State::EMPTY; + } } auto add_or_remove_class(Glib::RefPtr context, bool condition, @@ -408,6 +469,7 @@ auto Workspace::handle_done() -> void { add_or_remove_class(style_context, is_active(), "active"); add_or_remove_class(style_context, is_urgent(), "urgent"); add_or_remove_class(style_context, is_hidden(), "hidden"); + add_or_remove_class(style_context, is_empty(), "persistent"); if (workspace_group_.creation_delayed()) { return; @@ -433,6 +495,13 @@ auto Workspace::get_icon() -> std::string { return named_icon_it->second; } + if (is_empty()) { + auto persistent_icon_it = icons_map_.find("persistent"); + if (persistent_icon_it != icons_map_.end()) { + return persistent_icon_it->second; + } + } + auto default_icon_it = icons_map_.find("default"); if (default_icon_it != icons_map_.end()) { return default_icon_it->second; @@ -474,6 +543,29 @@ auto Workspace::handle_name(const std::string &name) -> void { workspace_group_.set_need_to_sort(); } name_ = name; + spdlog::debug("Workspace {} added to group {}", name, workspace_group_.id()); + + make_persistent(); + handle_duplicate(); +} + +auto Workspace::make_persistent() -> void { + auto p_workspaces = workspace_group_.persistent_workspaces(); + + if (std::find(p_workspaces.begin(), p_workspaces.end(), name_) != p_workspaces.end()) { + persistent_ = true; + } +} + +auto Workspace::handle_duplicate() -> void { + auto duplicate = + std::find_if(workspace_group_.workspaces().begin(), workspace_group_.workspaces().end(), + [this](const std::unique_ptr &g) { + return g->get_name() == name_ && g->id() != id_; + }); + if (duplicate != workspace_group_.workspaces().end()) { + workspace_group_.remove_workspace(duplicate->get()->id()); + } } auto Workspace::handle_coordinates(const std::vector &coordinates) -> void { diff --git a/src/util/gtk_icon.cpp b/src/util/gtk_icon.cpp new file mode 100644 index 0000000..5dd741f --- /dev/null +++ b/src/util/gtk_icon.cpp @@ -0,0 +1,25 @@ +#include "util/gtk_icon.hpp" + +/* We need a global mutex for accessing the object returned by Gtk::IconTheme::get_default() + * because it always returns the same object across different threads, and concurrent + * access can cause data corruption and lead to invalid memory access and crashes. + * Even concurrent calls that seem read only such as has_icon can cause issues because + * the GTK lib may update the internal icon cache on this calls. + */ + +std::mutex DefaultGtkIconThemeWrapper::default_theme_mutex; + +bool DefaultGtkIconThemeWrapper::has_icon(const std::string& value) { + const std::lock_guard lock(default_theme_mutex); + + return Gtk::IconTheme::get_default()->has_icon(value); +} + +Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon(const char* name, int tmp_size, + Gtk::IconLookupFlags flags) { + const std::lock_guard lock(default_theme_mutex); + + auto default_theme = Gtk::IconTheme::get_default(); + default_theme->rescan_if_needed(); + return default_theme->load_icon(name, tmp_size, flags); +} diff --git a/src/util/prepare_for_sleep.cpp b/src/util/prepare_for_sleep.cpp new file mode 100644 index 0000000..221497e --- /dev/null +++ b/src/util/prepare_for_sleep.cpp @@ -0,0 +1,49 @@ +#include "util/prepare_for_sleep.h" + +#include + +namespace { +class PrepareForSleep { + private: + PrepareForSleep() { + GError *error = NULL; + login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + 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); + } + } + + 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) { + if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)"))) { + gboolean sleeping; + g_variant_get(parameters, "(b)", &sleeping); + + PrepareForSleep *self = static_cast(user_data); + self->signal.emit(sleeping); + } + } + + public: + static PrepareForSleep &GetInstance() { + static PrepareForSleep instance; + return instance; + } + waybar::SafeSignal signal; + + private: + guint login1_id; + GDBusConnection *login1_connection; +}; +} // namespace + +waybar::SafeSignal &waybar::util::prepare_for_sleep() { + return PrepareForSleep::GetInstance().signal; +} diff --git a/src/util/rewrite_title.cpp b/src/util/rewrite_string.cpp similarity index 75% rename from src/util/rewrite_title.cpp rename to src/util/rewrite_string.cpp index fae59bb..40c71e9 100644 --- a/src/util/rewrite_title.cpp +++ b/src/util/rewrite_string.cpp @@ -1,16 +1,16 @@ -#include "util/rewrite_title.hpp" +#include "util/rewrite_string.hpp" #include #include namespace waybar::util { -std::string rewriteTitle(const std::string& title, const Json::Value& rules) { +std::string rewriteString(const std::string& value, const Json::Value& rules) { if (!rules.isObject()) { - return title; + return value; } - std::string res = title; + std::string res = value; for (auto it = rules.begin(); it != rules.end(); ++it) { if (it.key().isString() && it->isString()) { @@ -18,7 +18,7 @@ std::string rewriteTitle(const std::string& title, const Json::Value& rules) { // malformated regexes will cause an exception. // in this case, log error and try the next rule. const std::regex rule{it.key().asString()}; - if (std::regex_match(title, rule)) { + if (std::regex_match(value, rule)) { res = std::regex_replace(res, rule, it->asString()); } } catch (const std::regex_error& e) { diff --git a/src/util/rfkill.cpp b/src/util/rfkill.cpp index 47da3b5..61be7c5 100644 --- a/src/util/rfkill.cpp +++ b/src/util/rfkill.cpp @@ -63,7 +63,7 @@ bool waybar::util::Rfkill::on_event(Glib::IOCondition cond) { return false; } - if (len < RFKILL_EVENT_SIZE_V1) { + if (static_cast(len) < RFKILL_EVENT_SIZE_V1) { spdlog::error("Wrong size of RFKILL event: {} < {}", len, RFKILL_EVENT_SIZE_V1); return true; } @@ -73,10 +73,9 @@ bool waybar::util::Rfkill::on_event(Glib::IOCondition cond) { on_update.emit(event); } return true; - } else { - spdlog::error("Failed to poll RFKILL control device"); - return false; } + spdlog::error("Failed to poll RFKILL control device"); + return false; } bool waybar::util::Rfkill::getState() const { return state_; } diff --git a/src/util/sanitize_str.cpp b/src/util/sanitize_str.cpp index 72c72f8..131b9f2 100644 --- a/src/util/sanitize_str.cpp +++ b/src/util/sanitize_str.cpp @@ -6,7 +6,7 @@ namespace waybar::util { // replaces ``<>&"'`` with their encoded counterparts std::string sanitize_string(std::string str) { - // note: it's important that '&' is replaced first; therefor we *can't* use std::map + // note: it's important that '&' is replaced first; therefore we *can't* use std::map const std::pair replacement_table[] = { {'&', "&"}, {'<', "<"}, {'>', ">"}, {'"', """}, {'\'', "'"}}; size_t startpoint; diff --git a/subprojects/catch2.wrap b/subprojects/catch2.wrap index ea61b94..4a6f836 100644 --- a/subprojects/catch2.wrap +++ b/subprojects/catch2.wrap @@ -1,12 +1,10 @@ [wrap-file] -directory = Catch2-3.1.0 -source_url = https://github.com/catchorg/Catch2/archive/v3.1.0.tar.gz -source_filename = Catch2-3.1.0.tar.gz -source_hash = c252b2d9537e18046d8b82535069d2567f77043f8e644acf9a9fffc22ea6e6f7 -patch_filename = catch2_3.1.0-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/catch2_3.1.0-1/get_patch -patch_hash = 4ebf4277aed574a9912a79f4817a310d837798e099bbafa6097be23a7f5e3ae4 -wrapdb_version = 3.1.0-1 +directory = Catch2-3.3.2 +source_url = https://github.com/catchorg/Catch2/archive/v3.3.2.tar.gz +source_filename = Catch2-3.3.2.tar.gz +source_hash = 8361907f4d9bff3ae7c1edb027f813659f793053c99b67837a0c0375f065bae2 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/catch2_3.3.2-1/Catch2-3.3.2.tar.gz +wrapdb_version = 3.3.2-1 [provide] catch2 = catch2_dep diff --git a/subprojects/cava.wrap b/subprojects/cava.wrap new file mode 100644 index 0000000..59044b1 --- /dev/null +++ b/subprojects/cava.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = cava-0.8.4 +source_url = https://github.com/LukashonakV/cava/archive/0.8.4.tar.gz +source_filename = cava-0.8.4.tar.gz +source_hash = 523353f446570277d40b8e1efb84468d70fdec53e1356a555c14bf466557a3ed +[provide] +cava = cava_dep diff --git a/test/SafeSignal.cpp b/test/SafeSignal.cpp index 7ff6f2a..f496d7a 100644 --- a/test/SafeSignal.cpp +++ b/test/SafeSignal.cpp @@ -2,7 +2,11 @@ #include -#include +#if __has_include() +#include +#else +#include +#endif #include #include diff --git a/test/config.cpp b/test/config.cpp index cdc96b0..ad3df06 100644 --- a/test/config.cpp +++ b/test/config.cpp @@ -1,6 +1,10 @@ #include "config.hpp" -#include +#if __has_include() +#include +#else +#include +#endif TEST_CASE("Load simple config", "[config]") { waybar::Config conf; @@ -27,7 +31,7 @@ TEST_CASE("Load config with multiple bars", "[config]") { SECTION("select multiple configs #1") { auto data = conf.getOutputConfigs("DP-0", "Fake DisplayPort output #0"); - REQUIRE(data.size() == 3); + REQUIRE(data.size() == 4); REQUIRE(data[0]["layer"].asString() == "bottom"); REQUIRE(data[0]["height"].asInt() == 20); REQUIRE(data[1]["layer"].asString() == "top"); @@ -36,6 +40,7 @@ TEST_CASE("Load config with multiple bars", "[config]") { REQUIRE(data[2]["layer"].asString() == "overlay"); REQUIRE(data[2]["position"].asString() == "right"); REQUIRE(data[2]["height"].asInt() == 23); + REQUIRE(data[3]["height"].asInt() == 24); } SECTION("select multiple configs #2") { auto data = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); diff --git a/test/config/multi.json b/test/config/multi.json index ed43a39..06f6ae2 100644 --- a/test/config/multi.json +++ b/test/config/multi.json @@ -21,5 +21,9 @@ "layer": "overlay", "height": 23, "output": "!HDMI-1" + }, + { + "height": 24, + "output": ["!HDMI-0", "!HDMI-1", "*"] } ] diff --git a/test/date.cpp b/test/date.cpp new file mode 100644 index 0000000..aa6d79b --- /dev/null +++ b/test/date.cpp @@ -0,0 +1,162 @@ +#include "util/date.hpp" + +#include +#include +#include +#include +#include + +#if __has_include() +#include +#include +#else +#include +#endif + +#ifndef SKIP +#define SKIP(...) \ + WARN(__VA_ARGS__); \ + return +#endif + +using namespace std::literals::chrono_literals; + +/* + * Check that the date/time formatter with locale and timezone support is working as expected. + */ + +const date::zoned_time TEST_TIME = date::zoned_time{ + "UTC", date::local_days{date::Monday[1] / date::January / 2022} + 13h + 4min + 5s}; + +/* + * Check if the date formatted with LC_TIME=en_US is within expectations. + * + * The check expects Glibc output style and will fail with FreeBSD (different implementation) + * or musl (no implementation). + */ +static const bool LC_TIME_is_sane = []() { + try { + std::stringstream ss; + ss.imbue(std::locale("en_US.UTF-8")); + + time_t t = 1641211200; + std::tm tm = *std::gmtime(&t); + + ss << std::put_time(&tm, "%x %X"); + return ss.str() == "01/03/2022 12:00:00 PM"; + } catch (std::exception &) { + return false; + } +}(); + +TEST_CASE("Format UTC time", "[clock][util]") { + const auto loc = std::locale("C"); + const auto tm = TEST_TIME; + + CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified + CHECK(fmt::format(loc, "{:%c %Z}", tm) == "Mon Jan 3 13:04:05 2022 UTC"); + CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); + + if (!LC_TIME_is_sane) { + SKIP("Locale support check failed, skip tests"); + } + + /* Test a few locales that are most likely to be present */ + SECTION("US locale") { + try { + const auto loc = std::locale("en_US.UTF-8"); + + CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified + CHECK_THAT(fmt::format(loc, "{:%c}", tm), // HowardHinnant/date#704 + Catch::Matchers::StartsWith("Mon 03 Jan 2022 01:04:05 PM")); + CHECK(fmt::format(loc, "{:%x %X}", tm) == "01/03/2022 01:04:05 PM"); + CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); + } catch (const std::runtime_error &) { + WARN("Locale en_US not found, skip tests"); + } + } + SECTION("GB locale") { + try { + const auto loc = std::locale("en_GB.UTF-8"); + + CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified + CHECK_THAT(fmt::format(loc, "{:%c}", tm), // HowardHinnant/date#704 + Catch::Matchers::StartsWith("Mon 03 Jan 2022 13:04:05")); + CHECK(fmt::format(loc, "{:%x %X}", tm) == "03/01/22 13:04:05"); + CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); + } catch (const std::runtime_error &) { + WARN("Locale en_GB not found, skip tests"); + } + } + SECTION("Global locale") { + try { + const auto loc = std::locale::global(std::locale("en_US.UTF-8")); + + CHECK(fmt::format("{}", tm).empty()); // no format specified + CHECK_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704 + Catch::Matchers::StartsWith("Mon 03 Jan 2022 01:04:05 PM")); + CHECK(fmt::format("{:%x %X}", tm) == "01/03/2022 01:04:05 PM"); + CHECK(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); + + std::locale::global(loc); + } catch (const std::runtime_error &) { + WARN("Locale en_US not found, skip tests"); + } + } +} + +TEST_CASE("Format zoned time", "[clock][util]") { + const auto loc = std::locale("C"); + const auto tm = date::zoned_time{"America/New_York", TEST_TIME}; + + CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified + CHECK(fmt::format(loc, "{:%c %Z}", tm) == "Mon Jan 3 08:04:05 2022 EST"); + CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); + + if (!LC_TIME_is_sane) { + SKIP("Locale support check failed, skip tests"); + } + + /* Test a few locales that are most likely to be present */ + SECTION("US locale") { + try { + const auto loc = std::locale("en_US.UTF-8"); + + CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified + CHECK_THAT(fmt::format(loc, "{:%c}", tm), // HowardHinnant/date#704 + Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05 AM")); + CHECK(fmt::format(loc, "{:%x %X}", tm) == "01/03/2022 08:04:05 AM"); + CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); + } catch (const std::runtime_error &) { + WARN("Locale en_US not found, skip tests"); + } + } + SECTION("GB locale") { + try { + const auto loc = std::locale("en_GB.UTF-8"); + + CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified + CHECK_THAT(fmt::format(loc, "{:%c}", tm), // HowardHinnant/date#704 + Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05")); + CHECK(fmt::format(loc, "{:%x %X}", tm) == "03/01/22 08:04:05"); + CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); + } catch (const std::runtime_error &) { + WARN("Locale en_GB not found, skip tests"); + } + } + SECTION("Global locale") { + try { + const auto loc = std::locale::global(std::locale("en_US.UTF-8")); + + CHECK(fmt::format("{}", tm).empty()); // no format specified + CHECK_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704 + Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05 AM")); + CHECK(fmt::format("{:%x %X}", tm) == "01/03/2022 08:04:05 AM"); + CHECK(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); + + std::locale::global(loc); + } catch (const std::runtime_error &) { + WARN("Locale en_US not found, skip tests"); + } + } +} diff --git a/test/main.cpp b/test/main.cpp index 7970c26..daeee69 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -3,8 +3,13 @@ #include #include +#if __has_include() #include #include +#else +#include +#include +#endif #include int main(int argc, char* argv[]) { @@ -13,10 +18,16 @@ int main(int argc, char* argv[]) { session.applyCommandLine(argc, argv); const auto logger = spdlog::default_logger(); +#if CATCH_VERSION_MAJOR >= 3 for (const auto& spec : session.config().getReporterSpecs()) { - if (spec.name() == "tap") { + const auto& reporter_name = spec.name(); +#else + { + const auto& reporter_name = session.config().getReporterName(); +#endif + if (reporter_name == "tap") { spdlog::set_pattern("# [%l] %v"); - } else if (spec.name() == "compact") { + } else if (reporter_name == "compact") { logger->sinks().clear(); } else { logger->sinks().assign({std::make_shared()}); diff --git a/test/meson.build b/test/meson.build index b1e1123..02cbb2a 100644 --- a/test/meson.build +++ b/test/meson.build @@ -15,7 +15,7 @@ test_src = files( if tz_dep.found() test_dep += tz_dep - test_src += files('waybar_time.cpp') + test_src += files('date.cpp') endif waybar_test = executable( diff --git a/test/waybar_time.cpp b/test/waybar_time.cpp deleted file mode 100644 index 79469d4..0000000 --- a/test/waybar_time.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "util/waybar_time.hpp" - -#include -#include - -#include -#include -#include - -using namespace std::literals::chrono_literals; - -/* - * Check that the date/time formatter with locale and timezone support is working as expected. - */ - -const date::zoned_time TEST_TIME = date::make_zoned( - "UTC", date::local_days{date::Monday[1] / date::January / 2022} + 13h + 4min + 5s); - -TEST_CASE("Format UTC time", "[clock][util]") { - waybar::waybar_time tm{std::locale("C"), TEST_TIME}; - - REQUIRE(fmt::format("{}", tm).empty()); // no format specified - REQUIRE(fmt::format("{:%c %Z}", tm) == "Mon Jan 3 13:04:05 2022 UTC"); - REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); - - /* Test a few locales that are most likely to be present */ - SECTION("US locale") { - try { - tm.locale = std::locale("en_US"); - - REQUIRE(fmt::format("{}", tm).empty()); // no format specified - REQUIRE_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 01:04:05 PM")); - REQUIRE(fmt::format("{:%x %X}", tm) == "01/03/2022 01:04:05 PM"); - REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); - } catch (const std::runtime_error&) { - // locale not found; ignore - } - } - SECTION("GB locale") { - try { - tm.locale = std::locale("en_GB"); - - REQUIRE(fmt::format("{}", tm).empty()); // no format specified - REQUIRE_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 13:04:05")); - REQUIRE(fmt::format("{:%x %X}", tm) == "03/01/22 13:04:05"); - REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); - } catch (const std::runtime_error&) { - // locale not found; ignore - } - } -} - -TEST_CASE("Format zoned time", "[clock][util]") { - waybar::waybar_time tm{std::locale("C"), date::make_zoned("America/New_York", TEST_TIME)}; - - REQUIRE(fmt::format("{}", tm).empty()); // no format specified - REQUIRE(fmt::format("{:%c %Z}", tm) == "Mon Jan 3 08:04:05 2022 EST"); - REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); - - /* Test a few locales that are most likely to be present */ - SECTION("US locale") { - try { - tm.locale = std::locale("en_US"); - - REQUIRE(fmt::format("{}", tm).empty()); // no format specified - REQUIRE_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05 AM")); - REQUIRE(fmt::format("{:%x %X}", tm) == "01/03/2022 08:04:05 AM"); - REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); - } catch (const std::runtime_error&) { - // locale not found; ignore - } - } - - SECTION("GB locale") { - try { - tm.locale = std::locale("en_GB"); - - REQUIRE(fmt::format("{}", tm).empty()); // no format specified - REQUIRE_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05")); - REQUIRE(fmt::format("{:%x %X}", tm) == "03/01/22 08:04:05"); - REQUIRE(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); - } catch (const std::runtime_error&) { - // locale not found; ignore - } - } -}