diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..f74eae65 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,29 @@ +Checks: > + -*, + bugprone-* + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + -fuchsia-trailing-return, + -readability-magic-numbers, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -readability-braces-around-statements, + -readability-redundant-access-specifiers, + -readability-redundant-member-init, + -readability-redundant-string-init, + -readability-identifier-length +CheckOptions: + - { key: readability-identifier-naming.NamespaceCase, value: lower_case } + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.FunctionCase, value: camelBack } + - { key: readability-identifier-naming.VariableCase, value: camelBack } + - { key: readability-identifier-naming.PrivateMemberCase, value: camelBack } + - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } + - { key: readability-identifier-naming.EnumCase, value: CamelCase } + - { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } + - { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE } diff --git a/.envrc.sample b/.envrc similarity index 100% rename from .envrc.sample rename to .envrc diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..a89e734f --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,65 @@ +bug: + - "(crash|bug|error|coredump|freeze|segfault|issue|problem)" + +enhancement: + - "(feature|enhancement|improvement|request|suggestion)" + +hyprland: + - "(hyprland)" + +network: + - "(network|wifi|ethernet)" + +bluetooth: + - "(bluetooth|bluez)" + +sway: + - "(sway)" + +cpu: + - "(cpu)" + +memory: + - "(memory|ram)" + +disk: + - "(disk|storage)" + +battery: + - "(upower|battery)" + +sni: + - "(sni|tray)" + +dwl: + - "(dwl)" + +custom: + - "(custom|module|extension|plugin|script)" + +mpd: + - "(mpd|music)" + +audio: + - "(pulseaudio|alsa|jack|audio|pirewire|wireplumber)" + +temperature: + - "(temperature|thermal|hwmon)" + +clock: + - "(clock|time|date)" + +gamemode: + - "(gamemode|game|gaming)" + +inhibitor: + - "(inhibitor|idle|lock|suspend|hibernate|logout)" + +cava: + - "(cava|audio-visualizer)" + +backlight: + - "(backlight|brightness)" + +keyboard: + - "(keyboard|keymap|layout|shortcut)" diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 00000000..40fd3126 --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,19 @@ +name: clang-format + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-format-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: DoozyX/clang-format-lint-action@v0.16.2 + name: clang-format + with: + source: "." + extensions: "hpp,h,cpp,c" + clangFormatVersion: 16 diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml new file mode 100644 index 00000000..a39bd23d --- /dev/null +++ b/.github/workflows/clang-tidy.yml @@ -0,0 +1,39 @@ +name: clang-tidy + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-tidy-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + container: + image: alexays/waybar:debian + steps: + - uses: actions/checkout@v3 + - name: configure + run: | + meson -Dcpp_std=c++20 build # necessary to generate compile_commands.json + ninja -C build # necessary to find certain .h files (xdg, wayland, etc.) + - uses: actions/setup-python@v5 + with: + python-version: '3.10' # to be kept in sync with cpp-linter-action + update-environment: true # the python dist installed by the action needs LD_LIBRARY_PATH to work + - uses: cpp-linter/cpp-linter-action@v2.9.1 + name: clang-tidy + id: clang-tidy-check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PIP_NO_CACHE_DIR: false + with: + style: "" # empty string => don't do clang-format checks here, we do them in clang-format.yml + files-changed-only: true # only check files that have changed + lines-changed-only: true # only check lines that have changed + tidy-checks: "" # empty string => use the .clang-tidy file + version: "17" # clang-tools version + database: "build" # path to the compile_commands.json file + - name: Check if clang-tidy failed on any files + if: steps.clang-tidy-check.outputs.checks-failed > 0 + run: echo "Some files failed the linting checks!" && exit 1 diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 550f9453..7b27fdb6 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -1,29 +1,36 @@ name: freebsd -on: [ push, pull_request ] +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-freebsd-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: clang: - # Run actions in a FreeBSD VM on the macos-12 runner + # Run actions in a FreeBSD VM on the ubuntu runner # https://github.com/actions/runner/issues/385 - for FreeBSD runner support - # https://github.com/actions/virtual-environments/issues/4060 - for lack of VirtualBox on MacOS 11 runners - runs-on: macos-12 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Test in FreeBSD VM - uses: vmactions/freebsd-vm@v0 - with: - mem: 2048 - usesh: true - prepare: | - export CPPFLAGS=-isystem/usr/local/include LDFLAGS=-L/usr/local/lib # sndio - sed -i '' 's/quarterly/latest/' /etc/pkg/FreeBSD.conf - pkg install -y git # subprojects/date - pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ - libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \ - pkgconf pulseaudio scdoc sndio spdlog wayland-protocols upower \ - libinotify - run: | - meson build -Dman-pages=enabled - ninja -C build - meson test -C build --no-rebuild --print-errorlogs --suite waybar + - uses: actions/checkout@v3 + - name: Test in FreeBSD VM + uses: cross-platform-actions/action@v0.23.0 + timeout-minutes: 180 + env: + CPPFLAGS: '-isystem/usr/local/include' + LDFLAGS: '-L/usr/local/lib' + with: + operating_system: freebsd + version: "13.2" + environment_variables: CPPFLAGS LDFLAGS + sync_files: runner-to-vm + run: | + sudo sed -i '' 's/quarterly/latest/' /etc/pkg/FreeBSD.conf + sudo pkg install -y git # subprojects/date + sudo pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ + libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \ + pkgconf pulseaudio scdoc sndio spdlog wayland-protocols upower \ + libinotify + meson build -Dman-pages=enabled + ninja -C build + meson test -C build --no-rebuild --print-errorlogs --suite waybar diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 00000000..94dc42d2 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,19 @@ +name: "Issue Labeler" +on: + issues: + types: [opened, edited] + +permissions: + issues: write + contents: read + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: github/issue-labeler@v3.4 + with: + configuration-path: .github/labeler.yml + enable-versioned-regex: 0 + include-title: 1 + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index d11d2ccc..00000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Linter - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: DoozyX/clang-format-lint-action@v0.13 - with: - source: '.' - extensions: 'h,cpp,c' - clangFormatVersion: 12 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index d97612d5..dc6b7ede 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -2,6 +2,10 @@ name: linux on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-linux-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: strategy: @@ -14,9 +18,6 @@ jobs: - opensuse - gentoo cpp_std: [c++20] - include: - - distro: fedora - cpp_std: c++20 runs-on: ubuntu-latest container: diff --git a/.gitignore b/.gitignore index 4d7babf3..68bc0dc4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,7 @@ packagecache *.out *.app /.direnv/ + +# Nix +result +result-* diff --git a/Dockerfiles/debian b/Dockerfiles/debian index 578588c7..0745935e 100644 --- a/Dockerfiles/debian +++ b/Dockerfiles/debian @@ -2,6 +2,48 @@ 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 libplayerctl-dev && \ - apt-get clean +RUN apt update && \ + apt install --no-install-recommends --no-install-suggests -y \ + build-essential \ + catch2 \ + cmake \ + git \ + gobject-introspection \ + libdbusmenu-gtk3-dev \ + libegl1-mesa-dev \ + libfmt-dev \ + libgbm-dev \ + libgirepository1.0-dev \ + libgles2-mesa-dev \ + libgtk-layer-shell-dev \ + libgtkmm-3.0-dev \ + libhowardhinnant-date-dev \ + libiniparser-dev \ + libinput-dev \ + libjack-jackd2-dev \ + libjsoncpp-dev \ + libmpdclient-dev \ + libnl-3-dev \ + libnl-genl-3-dev \ + libpixman-1-dev \ + libplayerctl-dev \ + libpugixml-dev \ + libpulse-dev \ + libsndio-dev \ + libspdlog-dev \ + libudev-dev \ + libupower-glib-dev \ + libwayland-dev \ + libwireplumber-0.4-dev \ + libxkbcommon-dev \ + libxkbregistry-dev \ + locales \ + meson \ + ninja-build \ + pkg-config \ + python3-pip \ + python3-venv \ + scdoc \ + sudo \ + wayland-protocols \ + && apt clean diff --git a/README.md b/README.md index 718ceb44..65be764c 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,23 @@ # 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/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).
+> Available in [all major distributions](https://github.com/Alexays/Waybar/wiki/Installation)
> *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)* #### Current features - Sway (Workspaces, Binding mode, Focused window name) - River (Mapping mode, Tags, Focused window name) -- Hyprland (Focused window name) +- Hyprland (Window Icons, Workspaces, 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 - UPower +- Power profiles daemon - Network - Bluetooth - Pulseaudio +- Privacy Info - Wireplumber - Disk - Memory diff --git a/flake.lock b/flake.lock index b10c9bf7..25f12644 100644 --- a/flake.lock +++ b/flake.lock @@ -1,32 +1,13 @@ { "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=", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { @@ -35,59 +16,13 @@ "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=", + "lastModified": 1704538339, + "narHash": "sha256-1734d3mQuux9ySvwf6axRWZRBhtcZA9Q8eftD6EZg6U=", "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", + "rev": "46ae0210ce163b3cba6c7da08840c1d63de9c701", "type": "github" }, "original": { @@ -99,10 +34,8 @@ }, "root": { "inputs": { - "devshell": "devshell", "flake-compat": "flake-compat", - "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 97f4ed57..ebaeb81f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,27 +1,22 @@ { - description = "Highly customizable Wayland bar for Sway and Wlroots based compositors."; + 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 }: + outputs = { self, nixpkgs, ... }: let inherit (nixpkgs) lib; - genSystems = lib.genAttrs [ + genSystems = func: lib.genAttrs [ "x86_64-linux" - ]; - - pkgsFor = genSystems (system: - import nixpkgs { - inherit system; - }); + "aarch64-linux" + ] + (system: func (import nixpkgs { inherit system; })); mkDate = longDate: (lib.concatStringsSep "-" [ (builtins.substring 0 4 longDate) @@ -30,64 +25,43 @@ ]); in { - overlays.default = _: prev: { - waybar = prev.callPackage ./nix/default.nix { - version = prev.waybar.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); + devShells = genSystems + (pkgs: + { + default = + pkgs.mkShell + { + name = "waybar-shell"; + + # inherit attributes from upstream nixpkgs derivation + inherit (pkgs.waybar) buildInputs depsBuildBuild depsBuildBuildPropagated depsBuildTarget + depsBuildTargetPropagated depsHostHost depsHostHostPropagated depsTargetTarget + depsTargetTargetPropagated propagatedBuildInputs propagatedNativeBuildInputs strictDeps; + + # overrides for local development + nativeBuildInputs = pkgs.waybar.nativeBuildInputs ++ (with pkgs; [ + clang-tools + gdb + ]); + }; + }); + + overlays.default = final: prev: { + waybar = final.callPackage ./nix/default.nix { + # take the first "version: '...'" from meson.build + version = + (builtins.head (builtins.split "'" + (builtins.elemAt + (builtins.split " version: '" (builtins.readFile ./meson.build)) + 2))) + + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); }; }; - 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"; } - ]; - }; - }); + packages = genSystems (pkgs: + let packages = self.overlays.default pkgs pkgs; + in packages // { + default = packages.waybar; + }); + }; } diff --git a/include/AModule.hpp b/include/AModule.hpp index 9b16076b..c15efb00 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -11,12 +11,15 @@ namespace waybar { class AModule : public IModule { public: + static constexpr const char *MODULE_CLASS = "module"; + virtual ~AModule(); auto update() -> void override; virtual auto refresh(int) -> void{}; operator Gtk::Widget &() override; auto doAction(const std::string &name) -> void override; + /// Emitting on this dispatcher triggers a update() call Glib::Dispatcher dp; protected: @@ -36,26 +39,34 @@ class AModule : public IModule { virtual bool handleToggle(GdkEventButton *const &ev); virtual bool handleScroll(GdkEventScroll *); + virtual bool handleRelease(GdkEventButton *const &ev); private: + bool handleUserEvent(GdkEventButton *const &ev); + const bool isTooltip; 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_BUTTON_RELEASE), "on-click-release"}, {std::make_pair(1, GdkEventType::GDK_2BUTTON_PRESS), "on-double-click"}, {std::make_pair(1, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click"}, {std::make_pair(2, GdkEventType::GDK_BUTTON_PRESS), "on-click-middle"}, + {std::make_pair(2, GdkEventType::GDK_BUTTON_RELEASE), "on-click-middle-release"}, {std::make_pair(2, GdkEventType::GDK_2BUTTON_PRESS), "on-double-click-middle"}, {std::make_pair(2, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click-middle"}, {std::make_pair(3, GdkEventType::GDK_BUTTON_PRESS), "on-click-right"}, + {std::make_pair(3, GdkEventType::GDK_BUTTON_RELEASE), "on-click-right-release"}, {std::make_pair(3, GdkEventType::GDK_2BUTTON_PRESS), "on-double-click-right"}, {std::make_pair(3, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click-right"}, {std::make_pair(8, GdkEventType::GDK_BUTTON_PRESS), "on-click-backward"}, + {std::make_pair(8, GdkEventType::GDK_BUTTON_RELEASE), "on-click-backward-release"}, {std::make_pair(8, GdkEventType::GDK_2BUTTON_PRESS), "on-double-click-backward"}, {std::make_pair(8, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click-backward"}, {std::make_pair(9, GdkEventType::GDK_BUTTON_PRESS), "on-click-forward"}, + {std::make_pair(9, GdkEventType::GDK_BUTTON_RELEASE), "on-click-forward-release"}, {std::make_pair(9, GdkEventType::GDK_2BUTTON_PRESS), "on-double-click-forward"}, {std::make_pair(9, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click-forward"}}; }; diff --git a/include/ASlider.hpp b/include/ASlider.hpp new file mode 100644 index 00000000..44cde507 --- /dev/null +++ b/include/ASlider.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "AModule.hpp" +#include "gtkmm/scale.h" + +namespace waybar { + +class ASlider : public AModule { + public: + ASlider(const Json::Value& config, const std::string& name, const std::string& id); + virtual void onValueChanged(); + + protected: + bool vertical_ = false; + int min_ = 0, max_ = 100, curr_ = 50; + Gtk::Scale scale_; +}; + +} // namespace waybar \ No newline at end of file diff --git a/include/bar.hpp b/include/bar.hpp index 7c5525f6..6dc3c03d 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -12,6 +12,7 @@ #include #include "AModule.hpp" +#include "group.hpp" #include "xdg-output-unstable-v1-client-protocol.h" namespace waybar { @@ -52,34 +53,18 @@ class BarIpcClient; } #endif // HAVE_SWAY -class BarSurface { - protected: - BarSurface() = default; - - public: - virtual void setExclusiveZone(bool enable) = 0; - virtual void setLayer(bar_layer layer) = 0; - virtual void setMargins(const struct bar_margins &margins) = 0; - virtual void setPassThrough(bool enable) = 0; - virtual void setPosition(const std::string_view &position) = 0; - virtual void setSize(uint32_t width, uint32_t height) = 0; - virtual void commit(){}; - - virtual ~BarSurface() = default; -}; - class Bar { public: - using bar_mode_map = std::map; + using bar_mode_map = std::map; static const bar_mode_map PRESET_MODES; - static const std::string_view MODE_DEFAULT; - static const std::string_view MODE_INVISIBLE; + static const std::string MODE_DEFAULT; + static const std::string MODE_INVISIBLE; Bar(struct waybar_output *w_output, const Json::Value &); Bar(const Bar &) = delete; ~Bar(); - void setMode(const std::string_view &); + void setMode(const std::string &mode); void setVisible(bool visible); void toggle(); void handleSignal(int); @@ -88,8 +73,12 @@ class Bar { Json::Value config; struct wl_surface *surface; bool visible = true; - bool vertical = false; Gtk::Window window; + Gtk::Orientation orientation = Gtk::ORIENTATION_HORIZONTAL; + Gtk::PositionType position = Gtk::POS_TOP; + + int x_global; + int y_global; #ifdef HAVE_SWAY std::string bar_id; @@ -98,16 +87,24 @@ class Bar { private: void onMap(GdkEventAny *); auto setupWidgets() -> void; - void getModules(const Factory &, const std::string &, Gtk::Box *); + void getModules(const Factory &, const std::string &, waybar::Group *); void setupAltFormatKeyForModule(const std::string &module_name); void setupAltFormatKeyForModuleList(const char *module_list_name); void setMode(const bar_mode &); + void setPassThrough(bool passthrough); + void setPosition(Gtk::PositionType position); + void onConfigure(GdkEventConfigure *ev); + void configureGlobalOffset(int width, int height); + void onOutputGeometryChanged(); /* Copy initial set of modes to allow customization */ bar_mode_map configured_modes = PRESET_MODES; std::string last_mode_{MODE_DEFAULT}; - std::unique_ptr surface_impl_; + struct bar_margins margins_; + uint32_t width_, height_; + bool passthrough_; + Gtk::Box left_; Gtk::Box center_; Gtk::Box right_; diff --git a/include/client.hpp b/include/client.hpp index aaba3b6b..0e68f002 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -7,8 +7,9 @@ #include "bar.hpp" #include "config.hpp" +#include "util/css_reload_helper.hpp" +#include "util/portal.hpp" -struct zwlr_layer_shell_v1; struct zwp_idle_inhibitor_v1; struct zwp_idle_inhibit_manager_v1; @@ -24,7 +25,6 @@ class Client { Glib::RefPtr gdk_display; struct wl_display *wl_display = nullptr; struct wl_registry *registry = nullptr; - struct zwlr_layer_shell_v1 *layer_shell = nullptr; struct zxdg_output_manager_v1 *xdg_output_manager = nullptr; struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; std::vector> bars; @@ -33,7 +33,7 @@ class Client { private: Client() = default; - const std::string getStyle(const std::string &style); + const std::string getStyle(const std::string &style, std::optional appearance); void bindInterfaces(); void handleOutput(struct waybar_output &output); auto setupCss(const std::string &css_file) -> void; @@ -52,7 +52,10 @@ class Client { Glib::RefPtr style_context_; Glib::RefPtr css_provider_; + std::unique_ptr portal; std::list outputs_; + std::unique_ptr m_cssReloadHelper; + std::string m_cssFile; }; } // namespace waybar diff --git a/include/factory.hpp b/include/factory.hpp index 90d0ac1d..f805aab5 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -1,104 +1,17 @@ #pragma once #include -#if defined(HAVE_CHRONO_TIMEZONES) || defined(HAVE_LIBDATE) -#include "modules/clock.hpp" -#else -#include "modules/simpleclock.hpp" -#endif -#ifdef HAVE_SWAY -#include "modules/sway/language.hpp" -#include "modules/sway/mode.hpp" -#include "modules/sway/scratchpad.hpp" -#include "modules/sway/window.hpp" -#include "modules/sway/workspaces.hpp" -#endif -#ifdef HAVE_WLR -#include "modules/wlr/taskbar.hpp" -#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" -#endif -#if defined(HAVE_CPU_LINUX) || defined(HAVE_CPU_BSD) -#include "modules/cpu.hpp" -#endif -#include "modules/idle_inhibitor.hpp" -#if defined(HAVE_MEMORY_LINUX) || defined(HAVE_MEMORY_BSD) -#include "modules/memory.hpp" -#endif -#include "modules/disk.hpp" -#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 -#ifdef HAVE_LIBUDEV -#include "modules/backlight.hpp" -#endif -#ifdef HAVE_LIBEVDEV -#include "modules/keyboard_state.hpp" -#endif -#ifdef HAVE_GAMEMODE -#include "modules/gamemode.hpp" -#endif -#ifdef HAVE_UPOWER -#include "modules/upower/upower.hpp" -#endif -#ifdef HAVE_LIBPULSE -#include "modules/pulseaudio.hpp" -#endif -#ifdef HAVE_LIBMPDCLIENT -#include "modules/mpd/mpd.hpp" -#endif -#ifdef HAVE_LIBSNDIO -#include "modules/sndio.hpp" -#endif -#ifdef HAVE_GIO_UNIX -#include "modules/bluetooth.hpp" -#include "modules/inhibitor.hpp" -#endif -#ifdef HAVE_LIBJACK -#include "modules/jack.hpp" -#endif -#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" -#include "modules/temperature.hpp" -#include "modules/user.hpp" + +#include namespace waybar { +class Bar; + class Factory { public: Factory(const Bar& bar, const Json::Value& config); - AModule* makeModule(const std::string& name) const; + AModule* makeModule(const std::string& name, const std::string& pos) const; private: const Bar& bar_; diff --git a/include/group.hpp b/include/group.hpp index 60e31c96..67cf4385 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -5,18 +5,31 @@ #include #include "AModule.hpp" -#include "bar.hpp" -#include "factory.hpp" +#include "gtkmm/revealer.h" namespace waybar { class Group : public AModule { public: Group(const std::string&, const std::string&, const Json::Value&, bool); - ~Group() = default; + virtual ~Group() = default; auto update() -> void override; operator Gtk::Widget&() override; + + virtual Gtk::Box& getBox(); + void addWidget(Gtk::Widget& widget); + + bool handleMouseHover(GdkEventCrossing* const& e); + + protected: Gtk::Box box; + Gtk::Box revealer_box; + Gtk::Revealer revealer; + bool is_first_widget = true; + bool is_drawer = false; + std::string add_class_to_drawer_children; + + void addHoverHandlerTo(Gtk::Widget& widget); }; } // namespace waybar diff --git a/include/modules/backlight.hpp b/include/modules/backlight.hpp index ade4bc78..110cd434 100644 --- a/include/modules/backlight.hpp +++ b/include/modules/backlight.hpp @@ -1,14 +1,14 @@ #pragma once +#include #include #include #include #include #include "ALabel.hpp" -#include "giomm/dbusproxy.h" +#include "util/backlight_backend.hpp" #include "util/json.hpp" -#include "util/sleeper_thread.hpp" struct udev; struct udev_device; @@ -16,54 +16,17 @@ struct udev_device; namespace waybar::modules { class Backlight : public ALabel { - class BacklightDev { - public: - BacklightDev() = default; - BacklightDev(std::string name, int actual, int max, bool powered); - std::string_view name() const; - int get_actual() const; - void set_actual(int actual); - int get_max() const; - void set_max(int max); - bool get_powered() const; - void set_powered(bool powered); - friend inline bool operator==(const BacklightDev &lhs, const BacklightDev &rhs) { - return lhs.name_ == rhs.name_ && lhs.actual_ == rhs.actual_ && lhs.max_ == rhs.max_; - } - - private: - std::string name_; - int actual_ = 1; - int max_ = 1; - bool powered_ = true; - }; - public: Backlight(const std::string &, const Json::Value &); - virtual ~Backlight(); + virtual ~Backlight() = default; auto update() -> void override; - private: - template - static const BacklightDev *best_device(ForwardIt first, ForwardIt last, std::string_view); - template - static void upsert_device(ForwardIt first, ForwardIt last, Inserter inserter, udev_device *dev); - template - static void enumerate_devices(ForwardIt first, ForwardIt last, Inserter inserter, udev *udev); - bool handleScroll(GdkEventScroll *e) override; const std::string preferred_device_; - static constexpr int EPOLL_MAX_EVENTS = 16; - std::optional previous_best_; std::string previous_format_; - std::mutex udev_thread_mutex_; - std::vector devices_; - // thread must destruct before shared data - util::SleeperThread udev_thread_; - - Glib::RefPtr login_proxy_; + util::BacklightBackend backend; }; } // namespace waybar::modules diff --git a/include/modules/backlight_slider.hpp b/include/modules/backlight_slider.hpp new file mode 100644 index 00000000..437c53c4 --- /dev/null +++ b/include/modules/backlight_slider.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include "ASlider.hpp" +#include "util/backlight_backend.hpp" + +namespace waybar::modules { + +class BacklightSlider : public ASlider { + public: + BacklightSlider(const std::string&, const Json::Value&); + virtual ~BacklightSlider() = default; + + void update() override; + void onValueChanged() override; + + private: + std::chrono::milliseconds interval_; + std::string preferred_device_; + util::BacklightBackend backend; +}; + +} // namespace waybar::modules \ No newline at end of file diff --git a/include/modules/battery.hpp b/include/modules/battery.hpp index 017b0e48..7955e598 100644 --- a/include/modules/battery.hpp +++ b/include/modules/battery.hpp @@ -1,11 +1,8 @@ #pragma once -#ifdef FILESYSTEM_EXPERIMENTAL -#include -#else -#include -#endif #include + +#include #if defined(__linux__) #include #endif @@ -16,19 +13,16 @@ #include #include "ALabel.hpp" +#include "bar.hpp" #include "util/sleeper_thread.hpp" namespace waybar::modules { -#ifdef FILESYSTEM_EXPERIMENTAL -namespace fs = std::experimental::filesystem; -#else namespace fs = std::filesystem; -#endif class Battery : public ALabel { public: - Battery(const std::string&, const Json::Value&); + Battery(const std::string&, const waybar::Bar&, const Json::Value&); virtual ~Battery(); auto update() -> void override; @@ -40,6 +34,7 @@ class Battery : public ALabel { const std::string getAdapterStatus(uint8_t capacity) const; const std::tuple getInfos(); const std::string formatTimeRemaining(float hoursRemaining); + void setBarClass(std::string&); int global_watch; std::map batteries_; @@ -49,6 +44,7 @@ class Battery : public ALabel { std::mutex battery_list_mutex_; std::string old_status_; bool warnFirstTime_{true}; + const Bar& bar_; util::SleeperThread thread_; util::SleeperThread thread_battery_update_; diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index 18481e31..89658dcf 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -59,7 +59,8 @@ class Bluetooth : public ALabel { auto getDeviceProperties(GDBusObject*, DeviceInfo&) -> bool; auto getControllerProperties(GDBusObject*, ControllerInfo&) -> bool; - auto findCurController(ControllerInfo&) -> bool; + // Returns std::nullopt if no controller could be found + auto findCurController() -> std::optional; auto findConnectedDevices(const std::string&, std::vector&) -> void; #ifdef WANT_RFKILL @@ -68,7 +69,7 @@ class Bluetooth : public ALabel { const std::unique_ptr manager_; std::string state_; - ControllerInfo cur_controller_; + std::optional cur_controller_; std::vector connected_devices_; DeviceInfo cur_focussed_device_; std::string device_enumerate_; diff --git a/include/modules/cava.hpp b/include/modules/cava.hpp index d4da2b77..430c71b7 100644 --- a/include/modules/cava.hpp +++ b/include/modules/cava.hpp @@ -3,9 +3,11 @@ #include "ALabel.hpp" #include "util/sleeper_thread.hpp" +namespace cava { extern "C" { #include } +} // namespace cava namespace waybar::modules { using namespace std::literals::chrono_literals; @@ -21,13 +23,13 @@ class Cava final : public ALabel { 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{}}; + struct cava::error_s error_ {}; // cava errors + struct cava::config_params prm_ {}; // cava parameters + struct cava::audio_raw audio_raw_ {}; // cava handled raw audio data(is based on audio_data) + struct cava::audio_data audio_data_ {}; // cava audio data + struct cava::cava_plan* plan_; //{new cava_plan{}}; // Cava API to read audio source - ptr input_source_; + cava::ptr input_source_; // Delay to handle audio source std::chrono::milliseconds frame_time_milsec_{1s}; // Text to display @@ -36,6 +38,7 @@ class Cava final : public ALabel { std::chrono::seconds fetch_input_delay_{4}; std::chrono::seconds suspend_silence_delay_{0}; bool silence_{false}; + bool hide_on_silence_{false}; int sleep_counter_{0}; // Cava method void pause_resume(); diff --git a/include/modules/cffi.hpp b/include/modules/cffi.hpp new file mode 100644 index 00000000..85f12989 --- /dev/null +++ b/include/modules/cffi.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include "AModule.hpp" +#include "util/command.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +namespace ffi { +extern "C" { +typedef struct wbcffi_module wbcffi_module; + +typedef struct { + wbcffi_module* obj; + const char* waybar_version; + GtkContainer* (*get_root_widget)(wbcffi_module*); + void (*queue_update)(wbcffi_module*); +} wbcffi_init_info; + +struct wbcffi_config_entry { + const char* key; + const char* value; +}; +} +} // namespace ffi + +class CFFI : public AModule { + public: + CFFI(const std::string&, const std::string&, const Json::Value&); + virtual ~CFFI(); + + virtual auto refresh(int signal) -> void override; + virtual auto doAction(const std::string& name) -> void override; + virtual auto update() -> void override; + + private: + /// + void* cffi_instance_ = nullptr; + + typedef void*(InitFn)(const ffi::wbcffi_init_info* init_info, + const ffi::wbcffi_config_entry* config_entries, size_t config_entries_len); + typedef void(DenitFn)(void* instance); + typedef void(RefreshFn)(void* instance, int signal); + typedef void(DoActionFn)(void* instance, const char* name); + typedef void(UpdateFn)(void* instance); + + // FFI hooks + struct { + std::function init = nullptr; + std::function deinit = nullptr; + std::function refresh = [](void*, int) {}; + std::function doAction = [](void*, const char*) {}; + std::function update = [](void*) {}; + } hooks_; +}; + +} // namespace waybar::modules diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index 0fcd0af2..8b597c4e 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -6,38 +6,27 @@ namespace waybar::modules { -const std::string kCalendarPlaceholder = "calendar"; -const std::string KTimezonedTimeListPlaceholder = "timezoned_time_list"; - -enum class WeeksSide { - LEFT, - RIGHT, - HIDDEN, -}; +const std::string kCldPlaceholder{"calendar"}; +const std::string kTZPlaceholder{"tz_list"}; +const std::string kOrdPlaceholder{"ordinal_date"}; enum class CldMode { MONTH, YEAR }; +enum class WS { LEFT, RIGHT, HIDDEN }; class Clock final : public ALabel { public: Clock(const std::string&, const Json::Value&); virtual ~Clock() = default; auto update() -> void override; - auto doAction(const std::string& name) -> void override; + auto doAction(const std::string&) -> void override; private: - util::SleeperThread thread_; - std::locale locale_; - std::vector time_zones_; - int current_time_zone_idx_; - bool is_calendar_in_tooltip_; - bool is_timezoned_list_in_tooltip_; - - auto first_day_of_week() -> date::weekday; - const date::time_zone* current_timezone(); - auto timezones_text(std::chrono::system_clock::time_point now) -> std::string; - - /*Calendar properties*/ - WeeksSide cldWPos_{WeeksSide::HIDDEN}; + const std::locale locale_; + // tooltip + const std::string tlpFmt_; + std::string tlpText_{""}; // tooltip text to print + // Calendar + const bool cldInTooltip_; // calendar in tooltip /* 0 - calendar.format.months 1 - calendar.format.weekdays @@ -47,30 +36,42 @@ class Clock final : public ALabel { 5 - tooltip-format */ std::map fmtMap_; + uint cldMonCols_{3}; // calendar count month columns + int cldWnLen_{3}; // calendar week number length + const int cldMonColLen_{20}; // calendar month column length + WS cldWPos_{WS::HIDDEN}; // calendar week side to print + months cldCurrShift_{0}; // calendar months shift + year_month_day cldYearShift_; // calendar Year mode. Cached ymd + std::string cldYearCached_; // calendar Year mode. Cached calendar + year_month cldMonShift_; // calendar Month mode. Cached ym + std::string cldMonCached_; // calendar Month mode. Cached calendar + day cldBaseDay_{0}; // calendar Cached day. Is used when today is changing(midnight) + std::string cldText_{""}; // calendar text to print 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::year_month_day& today, - const date::year_month_day& ymd, - const date::time_zone* tz) + auto get_calendar(const year_month_day& today, const year_month_day& ymd, const time_zone* tz) -> const std::string; - /*Clock actions*/ + + // time zoned time in tooltip + const bool tzInTooltip_; // if need to print time zones text + std::vector tzList_; // time zones list + int tzCurrIdx_; // current time zone index for tzList_ + std::string tzText_{""}; // time zones text to print + util::SleeperThread thread_; + + // ordinal date in tooltip + const bool ordInTooltip_; + std::string ordText_{""}; + auto get_ordinal_date(const year_month_day& today) -> std::string; + + auto getTZtext(sys_seconds now) -> std::string; + auto first_day_of_week() -> weekday; + // Module actions void cldModeSwitch(); void cldShift_up(); void cldShift_down(); void tz_up(); void tz_down(); - - // ModuleActionMap + // Module Action Map static inline std::map actionMap_{ {"mode", &waybar::modules::Clock::cldModeSwitch}, {"shift_up", &waybar::modules::Clock::cldShift_up}, @@ -78,4 +79,5 @@ class Clock final : public ALabel { {"tz_up", &waybar::modules::Clock::tz_up}, {"tz_down", &waybar::modules::Clock::tz_down}}; }; + } // namespace waybar::modules diff --git a/include/modules/cpu.hpp b/include/modules/cpu.hpp index a5235486..7f78c165 100644 --- a/include/modules/cpu.hpp +++ b/include/modules/cpu.hpp @@ -21,12 +21,6 @@ class Cpu : public ALabel { auto update() -> void override; private: - double getCpuLoad(); - std::tuple, std::string> getCpuUsage(); - std::tuple getCpuFrequency(); - std::vector> parseCpuinfo(); - std::vector parseCpuFrequencies(); - std::vector> prev_times_; util::SleeperThread thread_; diff --git a/include/modules/cpu_frequency.hpp b/include/modules/cpu_frequency.hpp new file mode 100644 index 00000000..49ca1b86 --- /dev/null +++ b/include/modules/cpu_frequency.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CpuFrequency : public ALabel { + public: + CpuFrequency(const std::string&, const Json::Value&); + virtual ~CpuFrequency() = default; + auto update() -> void override; + + // This is a static member because it is also used by the cpu module. + static std::tuple getCpuFrequency(); + + private: + static std::vector parseCpuFrequencies(); + + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/include/modules/cpu_usage.hpp b/include/modules/cpu_usage.hpp new file mode 100644 index 00000000..c93a1734 --- /dev/null +++ b/include/modules/cpu_usage.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CpuUsage : public ALabel { + public: + CpuUsage(const std::string&, const Json::Value&); + virtual ~CpuUsage() = default; + auto update() -> void override; + + // This is a static member because it is also used by the cpu module. + static std::tuple, std::string> getCpuUsage( + std::vector>&); + + private: + static std::vector> parseCpuinfo(); + + std::vector> prev_times_; + + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index a6024a84..2c7ba8a8 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -14,7 +14,7 @@ namespace waybar::modules { class Custom : public ALabel { public: - Custom(const std::string&, const std::string&, const Json::Value&); + Custom(const std::string&, const std::string&, const Json::Value&, const std::string&); virtual ~Custom(); auto update() -> void override; void refresh(int /*signal*/) override; @@ -22,6 +22,7 @@ class Custom : public ALabel { private: void delayWorker(); void continuousWorker(); + void waitingWorker(); void parseOutputRaw(); void parseOutputJson(); void handleEvent(); @@ -29,6 +30,7 @@ class Custom : public ALabel { bool handleToggle(GdkEventButton* const& e) override; const std::string name_; + const std::string output_name_; std::string text_; std::string id_; std::string alt_; diff --git a/include/modules/disk.hpp b/include/modules/disk.hpp index 2a307c9e..1b4f3176 100644 --- a/include/modules/disk.hpp +++ b/include/modules/disk.hpp @@ -20,6 +20,9 @@ class Disk : public ALabel { private: util::SleeperThread thread_; std::string path_; + std::string unit_; + + float calc_specific_divisor(const std::string divisor); }; } // namespace waybar::modules diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index e23b1582..9ce0ec33 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -1,10 +1,10 @@ #pragma once -#include + #include #include #include #include -#include +#include #include "util/json.hpp" @@ -20,19 +20,19 @@ class IPC { public: IPC() { startIPC(); } - void registerForIPC(const std::string&, EventHandler*); - void unregisterForIPC(EventHandler*); + void registerForIPC(const std::string& ev, EventHandler* ev_handler); + void unregisterForIPC(EventHandler* handler); - std::string getSocket1Reply(const std::string& rq); + static 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; + std::mutex callbackMutex_; util::JsonParser parser_; - std::list> callbacks; + std::list> callbacks_; }; inline std::unique_ptr gIPC; diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index 30789d06..47a4d69c 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -1,5 +1,9 @@ +#pragma once + #include +#include + #include "ALabel.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" @@ -26,7 +30,7 @@ class Language : public waybar::ALabel, public EventHandler { std::string short_description; }; - auto getLayout(const std::string&) -> Layout; + static auto getLayout(const std::string&) -> Layout; std::mutex mutex_; const Bar& bar_; diff --git a/include/modules/hyprland/submap.hpp b/include/modules/hyprland/submap.hpp index e2a84981..98b52efb 100644 --- a/include/modules/hyprland/submap.hpp +++ b/include/modules/hyprland/submap.hpp @@ -1,5 +1,9 @@ +#pragma once + #include +#include + #include "ALabel.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" @@ -10,12 +14,12 @@ namespace waybar::modules::hyprland { class Submap : public waybar::ALabel, public EventHandler { public: Submap(const std::string&, const waybar::Bar&, const Json::Value&); - virtual ~Submap(); + ~Submap() override; auto update() -> void override; private: - void onEvent(const std::string&) override; + void onEvent(const std::string& ev) override; std::mutex mutex_; const Bar& bar_; diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index fd68b049..f2c266bd 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -1,5 +1,9 @@ +#pragma once + #include +#include + #include "AAppIconLabel.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" @@ -10,7 +14,7 @@ namespace waybar::modules::hyprland { class Window : public waybar::AAppIconLabel, public EventHandler { public: Window(const std::string&, const waybar::Bar&, const Json::Value&); - virtual ~Window(); + ~Window() override; auto update() -> void override; @@ -21,7 +25,7 @@ class Window : public waybar::AAppIconLabel, public EventHandler { std::string last_window; std::string last_window_title; - static auto parse(const Json::Value&) -> Workspace; + static auto parse(const Json::Value& value) -> Workspace; }; struct WindowData { @@ -37,24 +41,25 @@ class Window : public waybar::AAppIconLabel, public EventHandler { static auto parse(const Json::Value&) -> WindowData; }; - auto getActiveWorkspace(const std::string&) -> Workspace; - auto getActiveWorkspace() -> Workspace; - void onEvent(const std::string&) override; + static auto getActiveWorkspace(const std::string&) -> Workspace; + static auto getActiveWorkspace() -> Workspace; + void onEvent(const std::string& ev) override; void queryActiveWorkspace(); void setClass(const std::string&, bool enable); - bool separate_outputs; + bool separateOutputs_; std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; - WindowData window_data_; + WindowData windowData_; Workspace workspace_; - std::string solo_class_; - std::string last_solo_class_; + std::string soloClass_; + std::string lastSoloClass_; bool solo_; - bool all_floating_; + bool allFloating_; bool swallowing_; bool fullscreen_; + bool focused_; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 353edb7a..91ea1653 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -1,47 +1,119 @@ +#pragma once + #include #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include "AModule.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" +#include "util/enum.hpp" +#include "util/regex_collection.hpp" + +using WindowAddress = std::string; namespace waybar::modules::hyprland { +class Workspaces; + +class WindowCreationPayload { + public: + WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + std::string window_repr); + WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + std::string window_class, std::string window_title); + WindowCreationPayload(Json::Value const& client_data); + + int incrementTimeSpentUncreated(); + bool isEmpty(Workspaces& workspace_manager); + bool reprIsReady() const { return std::holds_alternative(m_window); } + std::string repr(Workspaces& workspace_manager); + + std::string getWorkspaceName() const { return m_workspaceName; } + WindowAddress getAddress() const { return m_windowAddress; } + + void moveToWorksace(std::string& new_workspace_name); + + private: + void clearAddr(); + void clearWorkspaceName(); + + using Repr = std::string; + using ClassAndTitle = std::pair; + std::variant m_window; + + WindowAddress m_windowAddress; + std::string m_workspaceName; + + int m_timeSpentUncreated = 0; +}; + class Workspace { public: - Workspace(const Json::Value& workspace_data); - std::string& select_icon(std::map& icons_map); - Gtk::Button& button() { return button_; }; + explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, + const Json::Value& clients_data = Json::Value::nullRef); + std::string& selectIcon(std::map& icons_map); + Gtk::Button& button() { return m_button; }; - int id() const { return id_; }; - std::string name() const { return name_; }; - std::string output() const { return output_; }; - bool active() const { return active_; }; - bool is_special() const { return is_special_; }; - bool is_persistent() const { return is_persistent_; }; - bool is_empty() const { return windows_ == 0; }; + int id() const { return m_id; }; + std::string name() const { return m_name; }; + std::string output() const { return m_output; }; + bool isActive() const { return m_isActive; }; + bool isSpecial() const { return m_isSpecial; }; + bool isPersistent() const { return m_isPersistentRule || m_isPersistentConfig; }; + bool isPersistentConfig() const { return m_isPersistentConfig; }; + bool isPersistentRule() const { return m_isPersistentRule; }; + bool isVisible() const { return m_isVisible; }; + bool isEmpty() const { return m_windows == 0; }; + bool isUrgent() const { return m_isUrgent; }; - auto handle_clicked(GdkEventButton* bt) -> bool; - void set_active(bool value = true) { active_ = value; }; - void set_persistent(bool value = true) { is_persistent_ = value; }; - void set_windows(uint value) { windows_ = value; }; + bool handleClicked(GdkEventButton* bt) const; + void setActive(bool value = true) { m_isActive = value; }; + void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; + void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; + void setUrgent(bool value = true) { m_isUrgent = value; }; + void setVisible(bool value = true) { m_isVisible = value; }; + void setWindows(uint value) { m_windows = value; }; + void setName(std::string const& value) { m_name = value; }; + bool containsWindow(WindowAddress const& addr) const { return m_windowMap.contains(addr); } + void insertWindow(WindowCreationPayload create_window_paylod); + std::string removeWindow(WindowAddress const& addr); + void initializeWindowMap(const Json::Value& clients_data); + + bool onWindowOpened(WindowCreationPayload const& create_window_paylod); + std::optional closeWindow(WindowAddress const& addr); void update(const std::string& format, const std::string& icon); private: - int id_; - std::string name_; - std::string output_; - uint windows_; - bool active_ = false; - bool is_special_ = false; - bool is_persistent_ = false; + Workspaces& m_workspaceManager; - Gtk::Button button_; - Gtk::Box content_; - Gtk::Label label_; + int m_id; + std::string m_name; + std::string m_output; + uint m_windows; + bool m_isActive = false; + bool m_isSpecial = false; + bool m_isPersistentRule = false; // represents the persistent state in hyprland + bool m_isPersistentConfig = false; // represents the persistent state in the Waybar config + bool m_isUrgent = false; + bool m_isVisible = false; + + std::map m_windowMap; + + Gtk::Button m_button; + Gtk::Box m_content; + Gtk::Label m_label; }; class Workspaces : public AModule, public EventHandler { @@ -51,37 +123,101 @@ class Workspaces : public AModule, public EventHandler { void update() override; void init(); - auto all_outputs() const -> bool { return all_outputs_; } - auto show_special() const -> bool { return show_special_; } + auto allOutputs() const -> bool { return m_allOutputs; } + auto showSpecial() const -> bool { return m_showSpecial; } + auto activeOnly() const -> bool { return m_activeOnly; } - auto get_bar_output() const -> std::string { return bar_.output->name; } + auto getBarOutput() const -> std::string { return m_bar.output->name; } + + std::string getRewrite(std::string window_class, std::string window_title); + std::string& getWindowSeparator() { return m_formatWindowSeparator; } + bool isWorkspaceIgnored(std::string const& workspace_name); + + bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } private: - void onEvent(const std::string&) override; - void update_window_count(); - void sort_workspaces(); - void create_workspace(Json::Value& value); - void remove_workspace(std::string name); + void onEvent(const std::string& e) override; + void updateWindowCount(); + void sortWorkspaces(); + void createWorkspace(Json::Value const& workspaceData, + Json::Value const& clientsData = Json::Value::nullRef); + void removeWorkspace(std::string const& name); + void setUrgentWorkspace(std::string const& windowaddress); + void parseConfig(const Json::Value& config); + void registerIpc(); - bool all_outputs_ = false; - bool show_special_ = false; + // workspace events + void onWorkspaceActivated(std::string const& payload); + void onSpecialWorkspaceActivated(std::string const& payload); + void onWorkspaceDestroyed(std::string const& payload); + void onWorkspaceCreated(std::string const& workspaceName, + Json::Value const& clientsData = Json::Value::nullRef); + void onWorkspaceMoved(std::string const& payload); + void onWorkspaceRenamed(std::string const& payload); - void fill_persistent_workspaces(); - void create_persistent_workspaces(); - std::vector persistent_workspaces_to_create_; - bool persistent_created_ = false; + // monitor events + void onMonitorFocused(std::string const& payload); - std::string format_; - std::map icons_map_; - bool with_icon_; - uint64_t monitor_id_; - std::string active_workspace_name_; - std::vector> workspaces_; - std::vector workspaces_to_create_; - std::vector workspaces_to_remove_; - std::mutex mutex_; - const Bar& bar_; - Gtk::Box box_; + // window events + void onWindowOpened(std::string const& payload); + void onWindowClosed(std::string const& addr); + void onWindowMoved(std::string const& payload); + + void onWindowTitleEvent(std::string const& payload); + + void onConfigReloaded(); + + int windowRewritePriorityFunction(std::string const& window_rule); + + void doUpdate(); + + void extendOrphans(int workspaceId, Json::Value const& clientsJson); + void registerOrphanWindow(WindowCreationPayload create_window_payload); + + void initializeWorkspaces(); + void setCurrentMonitorId(); + void loadPersistentWorkspacesFromConfig(Json::Value const& clientsJson); + void loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& clientsJson); + + bool m_allOutputs = false; + bool m_showSpecial = false; + bool m_activeOnly = false; + Json::Value m_persistentWorkspaceConfig; + + // Map for windows stored in workspaces not present in the current bar. + // This happens when the user has multiple monitors (hence, multiple bars) + // and doesn't share windows accross bars (a.k.a `all-outputs` = false) + std::map m_orphanWindowMap; + + enum class SortMethod { ID, NAME, NUMBER, DEFAULT }; + util::EnumParser m_enumParser; + SortMethod m_sortBy = SortMethod::DEFAULT; + std::map m_sortMap = {{"ID", SortMethod::ID}, + {"NAME", SortMethod::NAME}, + {"NUMBER", SortMethod::NUMBER}, + {"DEFAULT", SortMethod::DEFAULT}}; + + std::string m_format; + + std::map m_iconsMap; + util::RegexCollection m_windowRewriteRules; + bool m_anyWindowRewriteRuleUsesTitle = false; + std::string m_formatWindowSeparator; + + bool m_withIcon; + uint64_t m_monitorId; + std::string m_activeWorkspaceName; + std::string m_activeSpecialWorkspaceName; + std::vector> m_workspaces; + std::vector> m_workspacesToCreate; + std::vector m_workspacesToRemove; + std::vector m_windowsToCreate; + + std::vector m_ignoreWorkspaces; + + std::mutex m_mutex; + const Bar& m_bar; + Gtk::Box m_box; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/keyboard_state.hpp b/include/modules/keyboard_state.hpp index deb577e2..be90eee4 100644 --- a/include/modules/keyboard_state.hpp +++ b/include/modules/keyboard_state.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "AModule.hpp" @@ -40,6 +41,7 @@ class KeyboardState : public AModule { struct libinput* libinput_; std::unordered_map libinput_devices_; + std::set binding_keys; util::SleeperThread libinput_thread_, hotplug_thread_; }; diff --git a/include/modules/load.hpp b/include/modules/load.hpp new file mode 100644 index 00000000..c4c06d26 --- /dev/null +++ b/include/modules/load.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class Load : public ALabel { + public: + Load(const std::string&, const Json::Value&); + virtual ~Load() = default; + auto update() -> void override; + + // This is a static member because it is also used by the cpu module. + static std::tuple getLoad(); + + private: + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 1276e3c3..2c9071b4 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -148,6 +148,7 @@ class Stopped : public State { class Disconnected : public State { Context* const ctx_; sigc::connection timer_connection_; + int last_interval_; public: Disconnected(Context* const ctx) : ctx_{ctx} {} @@ -162,7 +163,7 @@ class Disconnected : public State { Disconnected(Disconnected const&) = delete; Disconnected& operator=(Disconnected const&) = delete; - void arm_timer(int interval) noexcept; + bool arm_timer(int interval) noexcept; void disarm_timer() noexcept; bool on_timer(); diff --git a/include/modules/power_profiles_daemon.hpp b/include/modules/power_profiles_daemon.hpp new file mode 100644 index 00000000..edd9fe00 --- /dev/null +++ b/include/modules/power_profiles_daemon.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "ALabel.hpp" +#include "giomm/dbusproxy.h" + +namespace waybar::modules { + +struct Profile { + std::string name; + std::string driver; +}; + +class PowerProfilesDaemon : public ALabel { + public: + PowerProfilesDaemon(const std::string &, const Json::Value &); + auto update() -> void override; + void profileChangedCb(const Gio::DBus::Proxy::MapChangedProperties &, + const std::vector &); + void busConnectedCb(Glib::RefPtr &r); + void getAllPropsCb(Glib::RefPtr &r); + void setPropCb(Glib::RefPtr &r); + void populateInitState(); + bool handleToggle(GdkEventButton *const &e) override; + + private: + // True if we're connected to the dbug interface. False if we're + // not. + bool connected_; + // Look for a profile name in the list of available profiles and + // switch activeProfile_ to it. + void switchToProfile(std::string const &); + // Used to toggle/display the profiles + std::vector availableProfiles_; + // Points to the active profile in the profiles list + std::vector::iterator activeProfile_; + // Current CSS class applied to the label + std::string currentStyle_; + // Format string + std::string tooltipFormat_; + // DBus Proxy used to track the current active profile + Glib::RefPtr powerProfilesProxy_; +}; + +} // namespace waybar::modules diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp new file mode 100644 index 00000000..b8e76768 --- /dev/null +++ b/include/modules/privacy/privacy.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +#include "ALabel.hpp" +#include "gtkmm/box.h" +#include "modules/privacy/privacy_item.hpp" +#include "util/pipewire/pipewire_backend.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +using waybar::util::PipewireBackend::PrivacyNodeInfo; + +namespace waybar::modules::privacy { + +class Privacy : public AModule { + public: + Privacy(const std::string &, const Json::Value &, const std::string &pos); + auto update() -> void override; + + void onPrivacyNodesChanged(); + + private: + std::list nodes_screenshare; // Screen is being shared + std::list nodes_audio_in; // Application is using the microphone + std::list nodes_audio_out; // Application is outputting audio + + std::mutex mutex_; + sigc::connection visibility_conn; + + // Config + Gtk::Box box_; + uint iconSpacing = 4; + uint iconSize = 20; + uint transition_duration = 250; + + std::shared_ptr backend = nullptr; +}; + +} // namespace waybar::modules::privacy diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp new file mode 100644 index 00000000..a0e3038b --- /dev/null +++ b/include/modules/privacy/privacy_item.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "gtkmm/box.h" +#include "gtkmm/image.h" +#include "gtkmm/revealer.h" +#include "util/pipewire/privacy_node_info.hpp" + +using waybar::util::PipewireBackend::PrivacyNodeInfo; +using waybar::util::PipewireBackend::PrivacyNodeType; + +namespace waybar::modules::privacy { + +class PrivacyItem : public Gtk::Revealer { + public: + PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, + std::list *nodes, const std::string &pos, const uint icon_size, + const uint transition_duration); + + enum PrivacyNodeType privacy_type; + + void set_in_use(bool in_use); + + private: + std::list *nodes; + + sigc::connection signal_conn; + + Gtk::Box tooltip_window; + + bool init = false; + bool in_use = false; + + // Config + std::string iconName = "image-missing-symbolic"; + bool tooltip = true; + uint tooltipIconSize = 24; + + Gtk::Box box_; + Gtk::Image icon_; + + void update_tooltip(); +}; + +} // namespace waybar::modules::privacy diff --git a/include/modules/pulseaudio.hpp b/include/modules/pulseaudio.hpp index d0b17e47..eead664f 100644 --- a/include/modules/pulseaudio.hpp +++ b/include/modules/pulseaudio.hpp @@ -1,54 +1,27 @@ #pragma once #include -#include -#include #include #include +#include #include "ALabel.hpp" +#include "util/audio_backend.hpp" namespace waybar::modules { class Pulseaudio : public ALabel { public: Pulseaudio(const std::string&, const Json::Value&); - virtual ~Pulseaudio(); + virtual ~Pulseaudio() = default; auto update() -> void override; private: - static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); - static void contextStateCb(pa_context*, void*); - static void sinkInfoCb(pa_context*, const pa_sink_info*, int, void*); - static void sourceInfoCb(pa_context*, const pa_source_info* i, int, void* data); - static void serverInfoCb(pa_context*, const pa_server_info*, void*); - static void volumeModifyCb(pa_context*, int, void*); - bool handleScroll(GdkEventScroll* e) override; const std::vector getPulseIcon() const; - pa_threaded_mainloop* mainloop_; - pa_mainloop_api* mainloop_api_; - pa_context* context_; - // SINK - uint32_t sink_idx_{0}; - uint16_t volume_; - pa_cvolume pa_volume_; - bool muted_; - std::string port_name_; - std::string form_factor_; - std::string desc_; - std::string monitor_; - std::string current_sink_name_; - bool current_sink_running_; - // SOURCE - uint32_t source_idx_{0}; - uint16_t source_volume_; - bool source_muted_; - std::string source_port_name_; - std::string source_desc_; - std::string default_source_name_; + std::shared_ptr backend = nullptr; }; } // namespace waybar::modules diff --git a/include/modules/pulseaudio_slider.hpp b/include/modules/pulseaudio_slider.hpp new file mode 100644 index 00000000..3ef44684 --- /dev/null +++ b/include/modules/pulseaudio_slider.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "ASlider.hpp" +#include "util/audio_backend.hpp" +namespace waybar::modules { + +enum class PulseaudioSliderTarget { + Sink, + Source, +}; + +class PulseaudioSlider : public ASlider { + public: + PulseaudioSlider(const std::string&, const Json::Value&); + virtual ~PulseaudioSlider() = default; + + void update() override; + void onValueChanged() override; + + private: + std::shared_ptr backend = nullptr; + PulseaudioSliderTarget target = PulseaudioSliderTarget::Sink; +}; + +} // namespace waybar::modules \ No newline at end of file diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 423ec7c5..1043157c 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -84,6 +84,8 @@ class Item : public sigc::trackable { // visibility of items with Status == Passive bool show_passive_ = false; + const Bar& bar_; + Glib::RefPtr proxy_; Glib::RefPtr cancellable_; std::set update_pending_; diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index 3e9519f5..ea58c4f0 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -56,6 +56,7 @@ class Language : public ALabel, public sigc::trackable { Layout layout_; std::string tooltip_format_ = ""; std::map layouts_map_; + bool hide_single_; bool is_variant_displayed; std::byte displayed_short_flag = static_cast(DispayedShortFlag::None); diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index 0efffe64..4258252a 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -12,6 +12,7 @@ #include "client.hpp" #include "modules/sway/ipc/client.hpp" #include "util/json.hpp" +#include "util/regex_collection.hpp" namespace waybar::modules::sway { @@ -27,10 +28,13 @@ class Workspaces : public AModule, public sigc::trackable { R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; static int convertWorkspaceNameToNum(std::string name); + static int windowRewritePriorityFunction(std::string const& window_rule); void onCmd(const struct Ipc::ipc_response&); void onEvent(const struct Ipc::ipc_response&); bool filterButtons(); + static bool hasFlag(const Json::Value&, const std::string&); + void updateWindows(const Json::Value&, std::string&); Gtk::Button& addButton(const Json::Value&); void onButtonReady(const Json::Value&, Gtk::Button&); std::string getIcon(const std::string&, const Json::Value&); @@ -44,6 +48,9 @@ class Workspaces : public AModule, public sigc::trackable { std::vector high_priority_named_; std::vector workspaces_order_; Gtk::Box box_; + std::string m_formatWindowSeperator; + std::string m_windowRewriteDefault; + util::RegexCollection m_windowRewriteRules; util::JsonParser parser_; std::unordered_map buttons_; std::mutex mutex_; diff --git a/include/modules/systemd_failed_units.hpp b/include/modules/systemd_failed_units.hpp new file mode 100644 index 00000000..9c3fbcee --- /dev/null +++ b/include/modules/systemd_failed_units.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +#include "ALabel.hpp" + +namespace waybar::modules { + +class SystemdFailedUnits : public ALabel { + public: + SystemdFailedUnits(const std::string &, const Json::Value &); + virtual ~SystemdFailedUnits(); + auto update() -> void override; + + private: + bool hide_on_ok; + std::string format_ok; + + bool update_pending; + uint32_t nr_failed_system, nr_failed_user; + std::string last_status; + Glib::RefPtr system_proxy, user_proxy; + + void notify_cb(const Glib::ustring &sender_name, const Glib::ustring &signal_name, + const Glib::VariantContainerBase &arguments); + void updateData(); +}; + +} // namespace waybar::modules diff --git a/include/modules/upower/upower.hpp b/include/modules/upower/upower.hpp index 446d1f53..d763259b 100644 --- a/include/modules/upower/upower.hpp +++ b/include/modules/upower/upower.hpp @@ -66,12 +66,13 @@ class UPower : public AModule { Devices devices; std::mutex m_Mutex; UpClient *client; - UpDevice *displayDevice; + UpDevice *displayDevice = nullptr; guint login1_id; GDBusConnection *login1_connection; - UPowerTooltip *upower_tooltip; + std::unique_ptr upower_tooltip; std::string lastStatus; bool showAltText; + bool showIcon = true; bool upowerRunning; guint upowerWatcher_id; std::string nativePath_; diff --git a/include/modules/upower/upower_tooltip.hpp b/include/modules/upower/upower_tooltip.hpp index 05e9dcb3..bc99abed 100644 --- a/include/modules/upower/upower_tooltip.hpp +++ b/include/modules/upower/upower_tooltip.hpp @@ -2,6 +2,7 @@ #include +#include #include #include "gtkmm/box.h" @@ -16,7 +17,7 @@ class UPowerTooltip : public Gtk::Window { const std::string getDeviceIcon(UpDeviceKind& kind); - Gtk::Box* contentBox; + std::unique_ptr contentBox; uint iconSize; uint tooltipSpacing; diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp new file mode 100644 index 00000000..2f53103e --- /dev/null +++ b/include/util/audio_backend.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "util/backend_common.hpp" + +namespace waybar::util { + +class AudioBackend { + private: + static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); + static void contextStateCb(pa_context*, void*); + static void sinkInfoCb(pa_context*, const pa_sink_info*, int, void*); + static void sourceInfoCb(pa_context*, const pa_source_info* i, int, void* data); + static void serverInfoCb(pa_context*, const pa_server_info*, void*); + static void volumeModifyCb(pa_context*, int, void*); + void connectContext(); + + pa_threaded_mainloop* mainloop_; + pa_mainloop_api* mainloop_api_; + pa_context* context_; + pa_cvolume pa_volume_; + + // SINK + uint32_t sink_idx_{0}; + uint16_t volume_; + bool muted_; + std::string port_name_; + std::string form_factor_; + std::string desc_; + std::string monitor_; + std::string current_sink_name_; + bool current_sink_running_; + // SOURCE + uint32_t source_idx_{0}; + uint16_t source_volume_; + bool source_muted_; + std::string source_port_name_; + std::string source_desc_; + std::string default_source_name_; + + std::vector ignored_sinks_; + + std::function on_updated_cb_ = NOOP; + + /* Hack to keep constructor inaccessible but still public. + * This is required to be able to use std::make_shared. + * It is important to keep this class only accessible via a reference-counted + * pointer because the destructor will manually free memory, and this could be + * a problem with C++20's copy and move semantics. + */ + struct private_constructor_tag {}; + + public: + static std::shared_ptr getInstance(std::function on_updated_cb = NOOP); + + AudioBackend(std::function on_updated_cb, private_constructor_tag tag); + ~AudioBackend(); + + void changeVolume(uint16_t volume, uint16_t min_volume = 0, uint16_t max_volume = 100); + void changeVolume(ChangeType change_type, double step = 1, uint16_t max_volume = 100); + + void setIgnoredSinks(const Json::Value& config); + + std::string getSinkPortName() const { return port_name_; } + std::string getFormFactor() const { return form_factor_; } + std::string getSinkDesc() const { return desc_; } + std::string getMonitor() const { return monitor_; } + std::string getCurrentSinkName() const { return current_sink_name_; } + bool getCurrentSinkRunning() const { return current_sink_running_; } + uint16_t getSinkVolume() const { return volume_; } + bool getSinkMuted() const { return muted_; } + uint16_t getSourceVolume() const { return source_volume_; } + bool getSourceMuted() const { return source_muted_; } + std::string getSourcePortName() const { return source_port_name_; } + std::string getSourceDesc() const { return source_desc_; } + std::string getDefaultSourceName() const { return default_source_name_; } + + void toggleSinkMute(); + void toggleSinkMute(bool); + + void toggleSourceMute(); + void toggleSourceMute(bool); + + bool isBluetooth(); +}; + +} // namespace waybar::util \ No newline at end of file diff --git a/include/util/backend_common.hpp b/include/util/backend_common.hpp new file mode 100644 index 00000000..dda6ac57 --- /dev/null +++ b/include/util/backend_common.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "AModule.hpp" + +namespace waybar::util { + +const static auto NOOP = []() {}; +enum class ChangeType : char { Increase, Decrease }; + +} // namespace waybar::util \ No newline at end of file diff --git a/include/util/backlight_backend.hpp b/include/util/backlight_backend.hpp new file mode 100644 index 00000000..8dcb8958 --- /dev/null +++ b/include/util/backlight_backend.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "giomm/dbusproxy.h" +#include "util/backend_common.hpp" +#include "util/sleeper_thread.hpp" + +#define GET_BEST_DEVICE(varname, backend, preferred_device) \ + decltype((backend).devices_) __devices; \ + { \ + std::scoped_lock lock((backend).udev_thread_mutex_); \ + __devices = (backend).devices_; \ + } \ + auto varname = (backend).best_device(__devices.cbegin(), __devices.cend(), preferred_device); + +namespace waybar::util { + +class BacklightDevice { + public: + BacklightDevice() = default; + BacklightDevice(std::string name, int actual, int max, bool powered); + + std::string name() const; + int get_actual() const; + void set_actual(int actual); + int get_max() const; + void set_max(int max); + bool get_powered() const; + void set_powered(bool powered); + friend inline bool operator==(const BacklightDevice &lhs, const BacklightDevice &rhs) { + return lhs.name_ == rhs.name_ && lhs.actual_ == rhs.actual_ && lhs.max_ == rhs.max_; + } + + private: + std::string name_; + int actual_ = 1; + int max_ = 1; + bool powered_ = true; +}; + +class BacklightBackend { + public: + BacklightBackend(std::chrono::milliseconds interval, std::function on_updated_cb = NOOP); + + // const inline BacklightDevice *get_best_device(std::string_view preferred_device); + const BacklightDevice *get_previous_best_device(); + + void set_previous_best_device(const BacklightDevice *device); + + void set_brightness(std::string preferred_device, ChangeType change_type, double step); + + void set_scaled_brightness(std::string preferred_device, int brightness); + int get_scaled_brightness(std::string preferred_device); + + template + static void upsert_device(ForwardIt first, ForwardIt last, Inserter inserter, udev_device *dev); + + template + static void enumerate_devices(ForwardIt first, ForwardIt last, Inserter inserter, udev *udev); + + bool is_login_proxy_initialized() const { return static_cast(login_proxy_); } + + template + static const BacklightDevice *best_device(ForwardIt first, ForwardIt last, std::string_view); + + std::vector devices_; + std::mutex udev_thread_mutex_; + + private: + void set_brightness_internal(std::string device_name, int brightness, int max_brightness); + + std::function on_updated_cb_; + std::chrono::milliseconds polling_interval_; + + std::optional previous_best_; + // thread must destruct before shared data + util::SleeperThread udev_thread_; + + Glib::RefPtr login_proxy_; + + static constexpr int EPOLL_MAX_EVENTS = 16; +}; + +} // namespace waybar::util \ No newline at end of file diff --git a/include/util/command.hpp b/include/util/command.hpp index 0d729b77..4b9decaa 100644 --- a/include/util/command.hpp +++ b/include/util/command.hpp @@ -66,7 +66,7 @@ inline int close(FILE* fp, pid_t pid) { return stat; } -inline FILE* open(const std::string& cmd, int& pid) { +inline FILE* open(const std::string& cmd, int& pid, const std::string& output_name) { if (cmd == "") return nullptr; int fd[2]; // Open the pipe with the close-on-exec flag set, so it will not be inherited @@ -109,6 +109,9 @@ inline FILE* open(const std::string& cmd, int& pid) { ::close(fd[0]); dup2(fd[1], 1); setpgid(child_pid, child_pid); + if (output_name != "") { + setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); + } execlp("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0); exit(0); } else { @@ -118,9 +121,9 @@ inline FILE* open(const std::string& cmd, int& pid) { return fdopen(fd[0], "r"); } -inline struct res exec(const std::string& cmd) { +inline struct res exec(const std::string& cmd, const std::string& output_name) { int pid; - auto fp = command::open(cmd, pid); + auto fp = command::open(cmd, pid, output_name); if (!fp) return {-1, ""}; auto output = command::read(fp); auto stat = command::close(fp, pid); @@ -129,7 +132,7 @@ inline struct res exec(const std::string& cmd) { inline struct res execNoRead(const std::string& cmd) { int pid; - auto fp = command::open(cmd, pid); + auto fp = command::open(cmd, pid, ""); if (!fp) return {-1, ""}; auto stat = command::close(fp, pid); return {WEXITSTATUS(stat), ""}; diff --git a/include/util/css_reload_helper.hpp b/include/util/css_reload_helper.hpp new file mode 100644 index 00000000..032b2382 --- /dev/null +++ b/include/util/css_reload_helper.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "giomm/file.h" +#include "giomm/filemonitor.h" +#include "glibmm/refptr.h" + +struct pollfd; + +namespace waybar { +class CssReloadHelper { + public: + CssReloadHelper(std::string cssFile, std::function callback); + + virtual ~CssReloadHelper() = default; + + virtual void monitorChanges(); + + protected: + std::vector parseImports(const std::string& cssFile); + + void parseImports(const std::string& cssFile, std::unordered_map& imports); + + void watchFiles(const std::vector& files); + + bool handleInotifyEvents(int fd); + + bool watch(int inotifyFd, pollfd* pollFd); + + virtual std::string getFileContents(const std::string& filename); + + virtual std::string findPath(const std::string& filename); + + void handleFileChange(Glib::RefPtr const& file, + Glib::RefPtr const& other_type, + Gio::FileMonitorEvent event_type); + + private: + std::string m_cssFile; + + std::function m_callback; + + std::vector>> m_fileMonitors; +}; +} // namespace waybar diff --git a/include/util/date.hpp b/include/util/date.hpp index 380bb6e7..2431b766 100644 --- a/include/util/date.hpp +++ b/include/util/date.hpp @@ -1,34 +1,48 @@ #pragma once -#include +#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 +#include + +#include #endif +// Date +namespace date { +#if HAVE_CHRONO_TIMEZONES +using namespace std::chrono; +using namespace std; +#else + +using system_clock = std::chrono::system_clock; +using seconds = std::chrono::seconds; + +template +inline auto format(const char* spec, const T& arg) { + return date::format(std::regex_replace(spec, std::regex("\\{:L|\\}"), ""), arg); +} + +template +inline auto format(const std::locale& loc, const char* spec, const T& arg) { + return date::format(loc, std::regex_replace(spec, std::regex("\\{:L|\\}"), ""), arg); +} +#endif +} // namespace date + +// Format +namespace waybar::util::date::format { +#if HAVE_CHRONO_TIMEZONES +using namespace std; +#else +using namespace fmt; +#endif +} // namespace waybar::util::date::format + +#if not HAVE_CHRONO_TIMEZONES template struct fmt::formatter> { std::string_view specs; @@ -58,3 +72,6 @@ struct fmt::formatter> { return fmt::format_to(ctx.out(), "{}", date::format(fmt::to_string(specs), ztime)); } }; +#endif + +using namespace date; diff --git a/include/util/enum.hpp b/include/util/enum.hpp new file mode 100644 index 00000000..681385fd --- /dev/null +++ b/include/util/enum.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +namespace waybar::util { + +template +struct EnumParser { + public: + EnumParser(); + ~EnumParser(); + + EnumType parseStringToEnum(const std::string& str, + const std::map& enumMap); +}; + +} // namespace waybar::util diff --git a/include/util/format.hpp b/include/util/format.hpp index 00b6a31c..069d8897 100644 --- a/include/util/format.hpp +++ b/include/util/format.hpp @@ -93,7 +93,7 @@ template <> struct formatter : formatter { template auto format(const Glib::ustring& value, FormatContext& ctx) { - return formatter::format(value, ctx); + return formatter::format(static_cast(value), ctx); } }; } // namespace fmt diff --git a/include/util/json.hpp b/include/util/json.hpp index 7cd43552..f0736f9b 100644 --- a/include/util/json.hpp +++ b/include/util/json.hpp @@ -3,6 +3,12 @@ #include #include +#include +#include +#include +#include +#include + #if (FMT_VERSION >= 90000) template <> @@ -12,25 +18,30 @@ struct fmt::formatter : ostream_formatter {}; namespace waybar::util { -struct JsonParser { - JsonParser() {} +class JsonParser { + public: + JsonParser() = default; - const Json::Value parse(const std::string& data) const { - Json::Value root(Json::objectValue); - if (data.empty()) { - return root; + Json::Value parse(const std::string& jsonStr) { + Json::Value root; + + // replace all occurrences of "\x" with "\u00", because JSON doesn't allow "\x" escape sequences + std::string modifiedJsonStr = replaceHexadecimalEscape(jsonStr); + + std::istringstream jsonStream(modifiedJsonStr); + std::string errs; + if (!Json::parseFromStream(m_readerBuilder, jsonStream, &root, &errs)) { + throw std::runtime_error("Error parsing JSON: " + errs); } - std::unique_ptr const reader(builder_.newCharReader()); - std::string err; - bool res = reader->parse(data.c_str(), data.c_str() + data.size(), &root, &err); - if (!res) throw std::runtime_error(err); return root; } - ~JsonParser() = default; - private: - Json::CharReaderBuilder builder_; -}; + Json::CharReaderBuilder m_readerBuilder; + static std::string replaceHexadecimalEscape(const std::string& str) { + static std::regex re("\\\\x"); + return std::regex_replace(str, re, "\\u00"); + } +}; } // namespace waybar::util diff --git a/include/util/pipewire/pipewire_backend.hpp b/include/util/pipewire/pipewire_backend.hpp new file mode 100644 index 00000000..4e23b282 --- /dev/null +++ b/include/util/pipewire/pipewire_backend.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "util/backend_common.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +class PipewireBackend { + private: + pw_thread_loop* mainloop_; + pw_context* context_; + pw_core* core_; + + spa_hook registry_listener; + + /* Hack to keep constructor inaccessible but still public. + * This is required to be able to use std::make_shared. + * It is important to keep this class only accessible via a reference-counted + * pointer because the destructor will manually free memory, and this could be + * a problem with C++20's copy and move semantics. + */ + struct private_constructor_tag {}; + + public: + std::mutex mutex_; + + pw_registry* registry; + + sigc::signal privacy_nodes_changed_signal_event; + + std::unordered_map privacy_nodes; + + static std::shared_ptr getInstance(); + + PipewireBackend(private_constructor_tag tag); + ~PipewireBackend(); +}; +} // namespace waybar::util::PipewireBackend diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp new file mode 100644 index 00000000..3b7f446d --- /dev/null +++ b/include/util/pipewire/privacy_node_info.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include + +#include "util/gtk_icon.hpp" + +namespace waybar::util::PipewireBackend { + +enum PrivacyNodeType { + PRIVACY_NODE_TYPE_NONE, + PRIVACY_NODE_TYPE_VIDEO_INPUT, + PRIVACY_NODE_TYPE_AUDIO_INPUT, + PRIVACY_NODE_TYPE_AUDIO_OUTPUT +}; + +class PrivacyNodeInfo { + public: + PrivacyNodeType type = PRIVACY_NODE_TYPE_NONE; + uint32_t id; + uint32_t client_id; + enum pw_node_state state = PW_NODE_STATE_IDLE; + std::string media_class; + std::string media_name; + std::string node_name; + std::string application_name; + + std::string pipewire_access_portal_app_id; + std::string application_icon_name; + + struct spa_hook object_listener; + struct spa_hook proxy_listener; + + void *data; + + std::string get_name() { + const std::vector names{&application_name, &node_name}; + std::string name = "Unknown Application"; + for (auto &name_ : names) { + if (name_ != nullptr && name_->length() > 0) { + name = *name_; + name[0] = toupper(name[0]); + break; + } + } + return name; + } + + std::string get_icon_name() { + const std::vector names{&application_icon_name, &pipewire_access_portal_app_id, + &application_name, &node_name}; + const std::string name = "application-x-executable-symbolic"; + for (auto &name_ : names) { + if (name_ != nullptr && name_->length() > 0 && DefaultGtkIconThemeWrapper::has_icon(*name_)) { + return *name_; + } + } + return name; + } +}; +} // namespace waybar::util::PipewireBackend diff --git a/include/util/portal.hpp b/include/util/portal.hpp new file mode 100644 index 00000000..23619169 --- /dev/null +++ b/include/util/portal.hpp @@ -0,0 +1,38 @@ +#include + +#include + +#include "fmt/format.h" + +namespace waybar { + +using namespace Gio; + +enum class Appearance { + UNKNOWN = 0, + DARK = 1, + LIGHT = 2, +}; +class Portal : private DBus::Proxy { + public: + Portal(); + void refreshAppearance(); + Appearance getAppearance(); + + typedef sigc::signal type_signal_appearance_changed; + type_signal_appearance_changed signal_appearance_changed() { return m_signal_appearance_changed; } + + private: + type_signal_appearance_changed m_signal_appearance_changed; + Appearance currentMode; + void on_signal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, + const Glib::VariantContainerBase& parameters); +}; + +} // namespace waybar + +template <> +struct fmt::formatter : formatter { + // parse is inherited from formatter. + auto format(waybar::Appearance c, format_context& ctx) const; +}; diff --git a/include/util/regex_collection.hpp b/include/util/regex_collection.hpp new file mode 100644 index 00000000..5ea2882e --- /dev/null +++ b/include/util/regex_collection.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include +#include +#include + +namespace waybar::util { + +struct Rule { + std::regex rule; + std::string repr; + int priority; + + // Fix for Clang < 16 + // See https://en.cppreference.com/w/cpp/compiler_support/20 "Parenthesized initialization of + // aggregates" + Rule(std::regex rule, std::string repr, int priority) + : rule(rule), repr(repr), priority(priority) {} +}; + +int default_priority_function(std::string& key); + +/* A collection of regexes and strings, with a default string to return if no regexes. + * When a regex is matched, the corresponding string is returned. + * All regexes that are matched are cached, so that the regexes are only + * evaluated once against a given string. + * Regexes may be given a higher priority than others, so that they are matched + * first. The priority function is given the regex string, and should return a + * higher number for higher priority regexes. + */ +class RegexCollection { + private: + std::vector rules; + std::map regex_cache; + std::string default_repr; + + std::string& find_match(std::string& value, bool& matched_any); + + public: + RegexCollection() = default; + RegexCollection(const Json::Value& map, std::string default_repr = "", + std::function priority_function = default_priority_function); + ~RegexCollection() = default; + + std::string& get(std::string& value, bool& matched_any); + std::string& get(std::string& value); +}; + +} // namespace waybar::util \ No newline at end of file diff --git a/include/util/rewrite_string.hpp b/include/util/rewrite_string.hpp index 2ab39ad8..3352a47a 100644 --- a/include/util/rewrite_string.hpp +++ b/include/util/rewrite_string.hpp @@ -5,4 +5,6 @@ namespace waybar::util { std::string rewriteString(const std::string&, const Json::Value&); -} +std::string rewriteStringOnce(const std::string& value, const Json::Value& rules, + bool& matched_any); +} // namespace waybar::util diff --git a/include/util/scope_guard.hpp b/include/util/scope_guard.hpp new file mode 100644 index 00000000..0d78cad6 --- /dev/null +++ b/include/util/scope_guard.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace waybar::util { + +template +class ScopeGuard { + public: + explicit ScopeGuard(Func&& exit_function) : f{std::forward(exit_function)} {} + ScopeGuard(const ScopeGuard&) = delete; + ScopeGuard(ScopeGuard&&) = default; + ScopeGuard& operator=(const ScopeGuard&) = delete; + ScopeGuard& operator=(ScopeGuard&&) = default; + ~ScopeGuard() { f(); } + + private: + Func f; +}; + +} // namespace waybar::util diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 861d5f1f..62d12931 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -58,10 +58,22 @@ class SleeperThread { bool isRunning() const { return do_run_; } + auto sleep() { + std::unique_lock lk(mutex_); + CancellationGuard cancel_lock; + return condvar_.wait(lk, [this] { return signal_ || !do_run_; }); + } + auto sleep_for(std::chrono::system_clock::duration dur) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; - return condvar_.wait_for(lk, dur, [this] { return signal_ || !do_run_; }); + constexpr auto max_time_point = std::chrono::steady_clock::time_point::max(); + auto wait_end = max_time_point; + auto now = std::chrono::steady_clock::now(); + if (now < max_time_point - dur) { + wait_end = now + dur; + } + return condvar_.wait_until(lk, wait_end, [this] { return signal_ || !do_run_; }); } auto sleep_until( diff --git a/include/util/string.hpp b/include/util/string.hpp index 24a9b2b9..d06557c1 100644 --- a/include/util/string.hpp +++ b/include/util/string.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include const std::string WHITESPACE = " \n\r\t\f\v"; @@ -15,3 +16,10 @@ inline std::string rtrim(const std::string& s) { } inline std::string trim(const std::string& s) { return rtrim(ltrim(s)); } + +inline std::string capitalize(const std::string& str) { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::toupper(c); }); + return result; +} diff --git a/man/waybar-backlight-slider.5.scd b/man/waybar-backlight-slider.5.scd new file mode 100644 index 00000000..cd5b5184 --- /dev/null +++ b/man/waybar-backlight-slider.5.scd @@ -0,0 +1,88 @@ +waybar-backlight-slider(5) + +# NAME + +waybar - backlight slider module + +# DESCRIPTION + +The *backlight slider* module displays and controls the current brightness of the default or preferred device. + +The brightness can be controlled by dragging the slider across the bar or clicking on a specific position. + +# CONFIGURATION + +*min*: ++ + typeof: int ++ + default: 0 ++ + The minimum volume value the slider should display and set. + +*max*: ++ + typeof: int ++ + default: 100 ++ + The maximum volume value the slider should display and set. + +*orientation*: ++ + typeof: string ++ + default: horizontal ++ + The orientation of the slider. Can be either `horizontal` or `vertical`. + +*device*: ++ + typeof: string ++ + The name of the preferred device to control. If left empty, a device will be chosen automatically. + +# EXAMPLES + +``` +"modules-right": [ + "backlight-slider", +], +"backlight/slider": { + "min": 0, + "max": 100, + "orientation": "horizontal", + "device": "intel_backlight" +} +``` + +# STYLE + +The slider is a component with multiple CSS Nodes, of which the following are exposed: + +*#backlight-slider*: ++ + Controls the style of the box *around* the slider and bar. + +*#backlight-slider slider*: ++ + Controls the style of the slider handle. + +*#backlight-slider trough*: ++ + Controls the style of the part of the bar that has not been filled. + +*#backlight-slider highlight*: ++ + Controls the style of the part of the bar that has been filled. + +## STYLE EXAMPLE + +``` +#backlight-slider slider { + min-height: 0px; + min-width: 0px; + opacity: 0; + background-image: none; + border: none; + box-shadow: none; +} + +#backlight-slider trough { + min-height: 80px; + min-width: 10px; + border-radius: 5px; + background-color: black; +} + +#backlight-slider highlight { + min-width: 10px; + border-radius: 5px; + background-color: red; +} +``` diff --git a/man/waybar-backlight.5.scd b/man/waybar-backlight.5.scd index cbadb8b9..b92abd12 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -26,11 +26,15 @@ The *backlight* module displays the current backlight level. *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *rotate*: ++ typeof: integer ++ @@ -46,11 +50,11 @@ The *backlight* module displays the current backlight level. *on-click-middle*: ++ typeof: string ++ - Command to execute when middle-clicked on the module using mousewheel. + Command to execute when middle-clicked on the module using mouse scroll wheel. *on-click-right*: ++ typeof: string ++ - Command to execute when the module is right clicked. + Command to execute when the module is right-clicked. *on-update*: ++ typeof: string ++ @@ -75,7 +79,7 @@ The *backlight* module displays the current backlight level. *scroll-step*: ++ typeof: float ++ default: 1.0 ++ - The speed in which to change the brightness when scrolling. + The speed at which to change the brightness when scrolling. # EXAMPLE: diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 52bc9f64..e359ea2e 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -25,7 +25,7 @@ The *battery* module displays the current capacity and state (eg. charging) of y *design-capacity*: ++ typeof: bool ++ default: false ++ - Option to use the battery design capacity instead of it's current maximal capacity. + Option to use the battery design capacity instead of its current maximal capacity. *interval*: ++ typeof: integer ++ @@ -57,11 +57,15 @@ The *battery* module displays the current capacity and state (eg. charging) of y *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *rotate*: ++ typeof: integer++ @@ -77,7 +81,7 @@ The *battery* module displays the current capacity and state (eg. charging) of y *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -100,6 +104,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: true ++ Option to disable tooltip on hover. +*bat-compatibility*: ++ + typeof: bool ++ + default: false ++ + Option to enable battery compatibility if not detected. + # FORMAT REPLACEMENTS *{capacity}*: Capacity in percentage @@ -121,7 +130,7 @@ The three arguments are: # CUSTOM FORMATS -The *battery* module allows one to define custom formats based on up to two factors. The best fitting format will be selected. +The *battery* module allows one to define custom formats based on up to two factors. The best-fitting format will be selected. *format-*: With *states*, a custom format can be set depending on the capacity of your battery. @@ -132,8 +141,8 @@ The *battery* module allows one to define custom formats based on up to two fact # STATES - Every entry (*state*) consists of a ** (typeof: *string*) and a ** (typeof: *integer*). -- The state can be addressed as a CSS class in the *style.css*. The name of the CSS class is the ** of the state. Each class gets activated when the current capacity is equal or below the configured **. -- Also each state can have its own *format*. Those con be configured via *format-*. Or if you want to differentiate a bit more even as *format--*. For more information see *custom-formats*. +- The state can be addressed as a CSS class in the *style.css*. The name of the CSS class is the ** of the state. Each class gets activated when the current capacity is equal to or below the configured **. +- Also each state can have its own *format*. Those can be configured via *format-*. Or if you want to differentiate a bit more even as *format--*. For more information see *custom-formats*. @@ -162,3 +171,10 @@ The *battery* module allows one to define custom formats based on up to two fact - ** can be defined in the *config*. For more information see *states*. - *#battery..* - Combination of both ** and **. + +The following classes are applied to the entire Waybar rather than just the +battery widget: + +- *window#waybar.battery-* + - ** can be defined in the *config*, as previously mentioned. + diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index cca7c35f..3808e855 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -14,7 +14,7 @@ Addressed by *bluetooth* *controller*: ++ typeof: string ++ - Use the controller with the defined alias. Otherwise a random controller is used. Recommended to define when there is more than 1 controller available to the system. + Use the controller with the defined alias. Otherwise, a random controller is used. Recommended to define when there is more than 1 controller available to the system. *format-device-preference*: ++ typeof: array ++ @@ -42,6 +42,10 @@ Addressed by *bluetooth* typeof: string ++ This format is used when the displayed controller is connected to at least 1 device. +*format-no-controller*: ++ + typeof: string ++ + This format is used when no bluetooth controller can be found + *format-icons*: ++ typeof: array/object ++ Based on the current battery percentage (see section *EXPERIMENTAL BATTERY PERCENTAGE FEATURE*), the corresponding icon gets selected. ++ @@ -58,11 +62,15 @@ Addressed by *bluetooth* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -74,7 +82,7 @@ Addressed by *bluetooth* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-scroll-up*: ++ typeof: string ++ @@ -113,6 +121,10 @@ Addressed by *bluetooth* typeof: string ++ This format is used when the displayed controller is connected to at least 1 device. +*tooltip-format-no-controller*: ++ + typeof: string ++ + This format is used when no bluetooth controller can be found + *tooltip-format-enumerate-connected*: ++ typeof: string ++ This format is used to define how each connected device should be displayed within the *device_enumerate* format replacement in the tooltip menu. @@ -138,7 +150,7 @@ Addressed by *bluetooth* *{device_alias}*: Alias of the displayed device. *{device_enumerate}*: Show a list of all connected devices, each on a separate line. Define the format of each device with the *tooltip-format-enumerate-connected* ++ -and/or *tooltip-format-enumerate-connected-battery* config options. Can only be used in the tooltip related format options. +and/or *tooltip-format-enumerate-connected-battery* config options. Can only be used in the tooltip-related format options. # EXPERIMENTAL BATTERY PERCENTAGE FEATURE diff --git a/man/waybar-cava.5.scd b/man/waybar-cava.5.scd index 5d62572e..cf75441b 100644 --- a/man/waybar-cava.5.scd +++ b/man/waybar-cava.5.scd @@ -35,7 +35,7 @@ libcava lives in: |[ *framerate* :[ integer :[ 30 -:[ rames per second. Is used as a replacement for *interval* +:[ Frames per second. Is used as a replacement for *interval* |[ *autosens* :[ integer :[ 1 @@ -60,6 +60,10 @@ libcava lives in: :[ integer :[ 5 :[ Seconds with no input before cava main thread goes to sleep mode +|[ *hide_on_silence* +:[ bool +:[ false +:[ Hides the widget if no input (after sleep_timer elapsed) |[ *method* :[ string :[ pulse @@ -91,19 +95,19 @@ libcava lives in: |[ *monstercat* :[ bool :[ false -:[ Disables or enables the so-called "Monstercat smoothing" with of without "waves" +:[ Disables or enables the so-called "Monstercat smoothing" with or without "waves" |[ *waves* :[ bool :[ false -:[ Disables or enables the so-called "Monstercat smoothing" with of without "waves" +:[ Disables or enables the so-called "Monstercat smoothing" with or without "waves" |[ *noise_reduction* :[ double :[ 0.77 -:[ Range between 0 - 1. The raw visualization is very noisy, this factor adjust the integral and gravity filters to keep the signal smooth. 1 - will be very slow and smooth, 0 - will be fast but noisy +:[ Range between 0 - 1. The raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth. 1 - will be very slow and smooth, 0 - will be fast but noisy |[ *input_delay* :[ integer :[ 2 -:[ Sets the delay before fetching audio source thread start working. On author machine Waybar starts much faster then pipewire audio server, and without a little delay cava module fails due to pipewire is not ready +:[ Sets the delay before fetching audio source thread start working. On author's machine, Waybar starts much faster than pipewire audio server, and without a little delay cava module fails because pipewire is not ready |[ *ascii_max_range* :[ integer :[ 7 @@ -120,14 +124,14 @@ libcava lives in: Configuration can be provided as: - The only cava configuration file which is provided through *cava_config*. The rest configuration can be skipped - Without cava configuration file. In such case cava should be configured through provided list of the configuration option -- Mix. When provided both And cava configuration file And configuration options. In such case waybar applies configuration file first then overrides particular options by the provided list of configuration options +- Mix. When provided both And cava configuration file And configuration options. In such case, waybar applies configuration file first and then overrides particular options by the provided list of configuration options # ACTIONS [- *String* :- *Action* |[ *mode* -:< Switch main cava thread and fetching audio source thread from/to pause/resume +:< Switch main cava thread and fetch audio source thread from/to pause/resume # DEPENDENCIES @@ -138,16 +142,16 @@ Configuration can be provided as: . On start Waybar throws an exception "error while loading shared libraries: libcava.so: cannot open shared object file: No such file or directory". It might happen when libcava for some reason hasn't been registered in the system. sudo ldconfig should help -. Waybar is starting but cava module doesn't react on the music - 1. In such case for at first need to make sure usual cava application is working as well +. Waybar is starting but cava module doesn't react to the music + 1. In such cases at first need to make sure usual cava application is working as well 2. If so, need to comment all configuration options. Uncomment cava_config and provide the path to the working cava config - 3. You might set too huge or too small input_delay. Try to setup to 4 seconds, restart waybar and check again 4 seconds past. Usual even on weak machines it should be enough - 4. You might accidentally switched action mode to pause mode + 3. You might set too huge or too small input_delay. Try to setup to 4 seconds, restart waybar, and check again 4 seconds past. Usual even on weak machines it should be enough + 4. You might accidentally switch action mode to pause mode # RISING ISSUES -For clear understanding: this module is a cava API's consumer. So for any bugs related to cava engine you should contact to Cava upstream(https://github.com/karlstav/cava) ++ -with the one Exception. Cava upstream doesn't provide cava as a shared library. For that this module author made a fork libcava(https://github.com/LukashonakV/cava). ++ +For clear understanding: this module is a cava API's consumer. So for any bugs related to cava engine you should contact Cava upstream(https://github.com/karlstav/cava) ++ +with the one Exception. Cava upstream doesn't provide cava as a shared library. For that, this module author made a fork libcava(https://github.com/LukashonakV/cava). ++ So the order is: . cava upstream . libcava upstream. diff --git a/man/waybar-cffi.5.scd b/man/waybar-cffi.5.scd new file mode 100644 index 00000000..926511d8 --- /dev/null +++ b/man/waybar-cffi.5.scd @@ -0,0 +1,37 @@ +waybar-cffi(5) +# NAME + +waybar - cffi module + +# DESCRIPTION + +The *cffi* module gives full control of a GTK widget to a third-party dynamic library, to create more complex modules using different programming languages. + +# CONFIGURATION + +Addressed by *cffi/* + +*module_path*: ++ + typeof: string ++ + The path to the dynamic library to load to control the widget. + +Some additional configuration may be required depending on the cffi dynamic library being used. + + +# EXAMPLES + +## C example: + +An example module written in C can be found at https://github.com/Alexays/Waybar/resources/custom_modules/cffi_example/ + +Waybar config to enable the module: +``` +"cffi/c_example": { + "module_path": ".config/waybar/cffi/wb_cffi_example.so" +} +``` + + +# STYLE + +The classes and IDs are managed by the cffi dynamic library. diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 0e855afa..e8ef7bed 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -63,7 +63,7 @@ $XDG_CONFIG_HOME/waybar/config ++ |[ *on-click-right* :[ string :[ -:[ Command to execute when you right clicked on the module +:[ Command to execute when you right-click on the module |[ *on-scroll-up* :[ string :[ @@ -85,7 +85,7 @@ $XDG_CONFIG_HOME/waybar/config ++ :[ same as format :[ Tooltip on hover -View all valid format options in *strftime(3)* or have a look +View all valid format options in *strftime(3)* or have a look https://en.cppreference.com/w/cpp/chrono/duration/formatter 2. Addressed by *clock: calendar* [- *Option* @@ -147,7 +147,7 @@ View all valid format options in *strftime(3)* or have a look * typeof: integer ++ The interval (in seconds) in which the information gets polled. ++ Use *once* if you want to execute the module only on startup. ++ - You can update it manually with a signal. If no *interval* is defined, it is assumed that the out script loops it self. + You can update it manually with a signal. If no *interval* or *signal* is defined, it is assumed that the out script loops itself. ++ + If a *signal* is defined then the script will run once on startup and will only update with a signal. *restart-interval*: ++ typeof: integer ++ The restart interval (in seconds). ++ Can't be used with the *interval* option, so only with continuous scripts. ++ - Once the script exit, it'll be re-executed after the *restart-interval*. + Once the script exits, it'll be re-executed after the *restart-interval*. *signal*: ++ typeof: integer ++ The signal number used to update the module. ++ - The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. + The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ + If no interval is defined then a signal will be the only way to update the module. *format*: ++ typeof: string ++ @@ -66,11 +68,15 @@ Addressed by *custom/* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -82,7 +88,7 @@ Addressed by *custom/* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -105,6 +111,11 @@ Addressed by *custom/* default: true ++ Option to disable tooltip on hover. +*tooltip-format*: ++ + typeof: string ++ + The tooltip format. If specified, overrides any tooltip output from the script in *exec*. ++ + Uses the same format replacements as *format*. + *escape*: ++ typeof: bool ++ default: false ++ @@ -134,7 +145,7 @@ $text\\n$tooltip\\n$class* *{}*: Output of the script. -*{percentage}* Percentage which can be set via a json return-type. +*{percentage}* Percentage which can be set via a json return type. *{icon}*: An icon from 'format-icons' according to percentage. diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index 8d9b8191..a279718b 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -33,7 +33,7 @@ Addressed by *disk* *states*: ++ typeof: object ++ - A number of disk utilization states which get activated on certain percentage thresholds (percentage_used). See *waybar-states(5)*. + A number of disk utilization states that get activated on certain percentage thresholds (percentage_used). See *waybar-states(5)*. *max-length*: ++ typeof: integer ++ @@ -41,11 +41,15 @@ Addressed by *disk* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -57,7 +61,7 @@ Addressed by *disk* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -85,20 +89,30 @@ Addressed by *disk* default: "{used} out of {total} used ({percentage_used}%)" ++ The format of the information displayed in the tooltip. +*unit*: ++ + typeof: string ++ + Use with specific_free, specific_used, and specific_total to force calculation to always be in a certain unit. Accepts kB, kiB, MB, Mib, GB, GiB, TB, TiB. + # FORMAT REPLACEMENTS *{percentage_used}*: Percentage of disk in use. *{percentage_free}*: Percentage of free disk space -*{total}*: Total amount of space on the disk, partition or mountpoint. +*{total}*: Total amount of space on the disk, partition, or mountpoint. Automatically selects unit based on size remaining. -*{used}*: Amount of used disk space. +*{used}*: Amount of used disk space. Automatically selects unit based on size remaining. -*{free}*: Amount of available disk space for normal users. +*{free}*: Amount of available disk space for normal users. Automatically selects unit based on size remaining. *{path}*: The path specified in the configuration. +*{specific_total}*: Total amount of space on the disk, partition, or mountpoint in a specific unit. Defaults to bytes. + +*{specific_used}*: Amount of used disk space in a specific unit. Defaults to bytes. + +*{specific_free}*: Amount of available disk space for normal users in a specific unit. Defaults to bytes. + # EXAMPLES ``` @@ -108,6 +122,15 @@ Addressed by *disk* } ``` +``` +"disk": { + "interval": 30, + "format": "{specific_free:0.2f} GB out of {specific_total:0.2f} GB available. Alternatively {free} out of {total} available", + "unit": "GB" + // 1434.25 GB out of 2000.00 GB available. Alternatively 1.4TiB out of 1.9TiB available. +} +``` + # STYLE - *#disk* diff --git a/man/waybar-dwl-tags.5.scd b/man/waybar-dwl-tags.5.scd index c9f1162d..07c94be9 100644 --- a/man/waybar-dwl-tags.5.scd +++ b/man/waybar-dwl-tags.5.scd @@ -24,7 +24,7 @@ Addressed by *dwl/tags* *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. + If set to false, you can left-click to set focused tag. Right-click to toggle tag focus. If set to true this behaviour is disabled. # EXAMPLE diff --git a/man/waybar-gamemode.5.scd b/man/waybar-gamemode.5.scd index 257c9c91..492e9850 100644 --- a/man/waybar-gamemode.5.scd +++ b/man/waybar-gamemode.5.scd @@ -65,11 +65,11 @@ Feral Gamemode optimizations. *{glyph}*: The string icon glyph to use instead. -*{count}*: The amount of games running with gamemode optimizations. +*{count}*: The number of games running with gamemode optimizations. # TOOLTIP FORMAT REPLACEMENTS -*{count}*: The amount of games running with gamemode optimizations. +*{count}*: The number of games running with gamemode optimizations. # EXAMPLES diff --git a/man/waybar-hyprland-submap.5.scd b/man/waybar-hyprland-submap.5.scd index 51c23cb9..849d0ca0 100644 --- a/man/waybar-hyprland-submap.5.scd +++ b/man/waybar-hyprland-submap.5.scd @@ -27,11 +27,15 @@ Addressed by *hyprland/submap* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -43,7 +47,7 @@ Addressed by *hyprland/submap* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -80,3 +84,4 @@ Addressed by *hyprland/submap* # STYLE - *#submap* +- *#submap.* diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index c3deb52e..12c1fe39 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -19,17 +19,51 @@ Addressed by *hyprland/workspaces* *format-icons*: ++ typeof: array ++ - Based on the workspace id and state, the corresponding icon gets selected. See *icons*. + Based on the workspace ID and state, the corresponding icon gets selected. See *icons*. + +*window-rewrite*: ++ + typeof: object ++ + Regex rules to map window class to an icon or preferred method of representation for a workspace's window. + Keys are the rules, while the values are the methods of representation. Values may use the placeholders {class} and {title} to use the window's original class and/or title respectively. + Rules may specify `class<...>`, `title<...>`, or both in order to fine-tune the matching. + +*window-rewrite-default*: + typeof: string ++ + default: "?" ++ + The default method of representation for a workspace's window. This will be used for windows whose classes do not match any of the rules in *window-rewrite*. + +*format-window-separator*: ++ + typeof: string ++ + default: " " ++ + The separator to be used between windows in a workspace. *show-special*: ++ typeof: bool ++ default: false ++ - If set to true special workspaces will be shown. + If set to true, special workspaces will be shown. *all-outputs*: ++ typeof: bool ++ default: false ++ - If set to false workspaces group will be shown only in assigned output. Otherwise all workspace groups are shown. + If set to false workspaces group will be shown only in assigned output. Otherwise, all workspace groups are shown. + +*active-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only the active workspace will be shown. + +*ignore-workspaces*: ++ + typeof: array ++ + default: [] ++ + Regexes to match against workspaces names. If there's a match, the workspace will not be shown. This takes precedence over *show-special*, *all-outputs*, and *active-only*. + +*sort-by*: ++ + typeof: string ++ + default: "default" ++ + If set to number, workspaces will sort by number. + If set to name, workspaces will sort by name. + If set to id, workspaces will sort by id. + If none of those, workspaces will sort with default behavior. # FORMAT REPLACEMENTS @@ -43,13 +77,17 @@ Addressed by *hyprland/workspaces* Additional to workspace name matching, the following *format-icons* can be set. -- *default*: Will be shown, when no string match is found. +- *default*: Will be shown, when no string match is found and none of the below conditions have defined icons. - *active*: Will be shown, when workspace is active +- *special*: Will be shown on non-active special workspaces +- *empty*: Will be shown on non-active, non-special empty persistent workspaces +- *visible*: Will be shown on workspaces that are visible but not active. For example: this is useful if you want your visible workspaces on other monitors to have the same look as active. +- *persistent*: Will be shown on non-empty persistent workspaces # EXAMPLES ``` -"wlr/workspaces": { +"hyprland/workspaces": { "format": "{name}: {icon}", "format-icons": { "1": "", @@ -60,8 +98,53 @@ Additional to workspace name matching, the following *format-icons* can be set. "active": "", "default": "" }, - "all-outputs": false, - "show-special": false + "persistent-workspaces": { + "*": 5, // 5 workspaces by default on every monitor + "HDMI-A-1": 3 // but only three on HDMI-A-1 + } +} +``` + +``` +"hyprland/workspaces": { + "format": "{name}: {icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "active": "", + "default": "" + }, + "persistent-workspaces": { + "*": [ 2,3,4,5 ], // 2-5 on every monitor + "HDMI-A-1": [ 1 ] // but only workspace 1 on HDMI-A-1 + } +} +``` + +``` +"hyprland/workspaces": { + "format": "{name}\n{windows}", + "format-window-separator": "\n", + "window-rewrite-default": "", + "window-rewrite": { + "title<.*youtube.*>": "", // Windows whose titles contain "youtube" + "class": "", // Windows whose classes are "firefox" + "class title<.*github.*>": "", // Windows whose class is "firefox" and title contains "github". Note that "class" always comes first. + "foot": "", // Windows that contain "foot" in either class or title. For optimization reasons, it will only match against a title if at least one other window explicitly matches against a title. + "code": "󰨞", + } +} +``` + +``` +"hyprland/workspaces": { + // Formatting omitted for brevity + "ignore-workspaces": [ + "(special:)?chrome-sharing-indicator" + ] } ``` @@ -70,3 +153,8 @@ Additional to workspace name matching, the following *format-icons* can be set. - *#workspaces* - *#workspaces button* - *#workspaces button.active* +- *#workspaces button.empty* +- *#workspaces button.visible* +- *#workspaces button.persistent* +- *#workspaces button.special* +- *#workspaces button.urgent* diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 7be3b568..71b3b30c 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -6,8 +6,8 @@ waybar - idle_inhibitor module # DESCRIPTION -The *idle_inhibitor* module can inhibiting the idle behavior such as screen blanking, locking, and -screensaving, also known as "presentation mode". +The *idle_inhibitor* module can inhibit the idle behavior such as screen blanking, locking, and +screensaver, also known as "presentation mode". # CONFIGURATION @@ -29,11 +29,15 @@ screensaving, also known as "presentation mode". *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -45,7 +49,7 @@ screensaving, also known as "presentation mode". *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -70,7 +74,7 @@ screensaving, also known as "presentation mode". *timeout*: ++ typeof: double ++ - The number of minutes the inhibit should last. + The number of minutes the inhibition should last. *tooltip*: ++ typeof: bool ++ diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index 401d0cd2..e3a69e38 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -66,7 +66,7 @@ The *image* module displays an image from a path. # SCRIPT OUTPUT -Similar to the *custom* module, output values of the script is *newline* separated. +Similar to the *custom* module, output values of the script are *newline* separated. The following is the output format: ``` @@ -87,3 +87,4 @@ $path\\n$tooltip # STYLE - *#image* +- *#image.empty* diff --git a/man/waybar-inhibitor.5.scd b/man/waybar-inhibitor.5.scd index bf37d351..47b6ffce 100644 --- a/man/waybar-inhibitor.5.scd +++ b/man/waybar-inhibitor.5.scd @@ -33,11 +33,15 @@ See *systemd-inhibit*(1) for more information. *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -49,7 +53,7 @@ See *systemd-inhibit*(1) for more information. *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ diff --git a/man/waybar-jack.5.scd b/man/waybar-jack.5.scd index e04314a9..87a38354 100644 --- a/man/waybar-jack.5.scd +++ b/man/waybar-jack.5.scd @@ -27,7 +27,7 @@ Addressed by *jack* *format-xrun*: ++ typeof: string ++ - This format is used for one polling interval, when the JACK server reports an xrun. + This format is used for one polling interval when the JACK server reports an xrun. *realtime*: ++ typeof: bool ++ @@ -59,11 +59,15 @@ Addressed by *jack* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -75,7 +79,7 @@ Addressed by *jack* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -87,7 +91,7 @@ Addressed by *jack* *{bufsize}*: The size of the JACK buffer. -*{samplerate}*: The samplerate at which the JACK server is running. +*{samplerate}*: The sample rate at which the JACK server is running. *{latency}*: The duration, in ms, of the current buffer size. diff --git a/man/waybar-keyboard-state.5.scd b/man/waybar-keyboard-state.5.scd index f07d6854..9ecc5515 100644 --- a/man/waybar-keyboard-state.5.scd +++ b/man/waybar-keyboard-state.5.scd @@ -13,7 +13,7 @@ You must be a member of the input group to use this module. # CONFIGURATION *interval*: ++ - Deprecated, this module use event loop now, the interval has no effect. + Deprecated, this module uses event loop now, the interval has no effect. typeof: integer ++ default: 1 ++ The interval, in seconds, to poll the keyboard state. @@ -48,6 +48,11 @@ You must be a member of the input group to use this module. default: chooses first valid input device ++ Which libevdev input device to show the state of. Libevdev devices can be found in /dev/input. The device should support number lock, caps lock, and scroll lock events. +*binding-keys*: ++ + typeof: array ++ + default: [58, 69, 70] ++ + Customize the key to trigger this module, the key number can be found in /usr/include/linux/input-event-codes.h or running sudo libinput debug-events --show-keycodes. + # FORMAT REPLACEMENTS *{name}*: Caps, Num, or Scroll. diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index 77e00638..e0252caf 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -41,11 +41,15 @@ Addressed by *memory* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -57,7 +61,7 @@ Addressed by *memory* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index 1dde8f79..fe6ee5a1 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -99,11 +99,15 @@ Addressed by *mpd* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -115,7 +119,7 @@ Addressed by *mpd* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -182,7 +186,7 @@ Addressed by *mpd* *{queueLength}*: The length of the current queue. -*{stateIcon}*: The icon corresponding the playing or paused status of the player (see *state-icons* option) +*{stateIcon}*: The icon corresponding to the playing or paused status of the player (see *state-icons* option) *{consumeIcon}*: The icon corresponding the "consume" option (see *consume-icons* option) diff --git a/man/waybar-mpris.5.scd b/man/waybar-mpris.5.scd index 2e445696..186d73c6 100644 --- a/man/waybar-mpris.5.scd +++ b/man/waybar-mpris.5.scd @@ -13,7 +13,7 @@ The *mpris* module displays currently playing media via libplayerctl. *player*: ++ typeof: string ++ default: playerctld ++ - Name of the MPRIS player to attach to. Using the default value always follows the currenly active player. + Name of the MPRIS player to attach to. Using the default value always follows the currently active player. *ignored-players*: ++ typeof: []string ++ @@ -21,6 +21,7 @@ The *mpris* module displays currently playing media via libplayerctl. *interval*: ++ typeof: integer ++ + default: 0 ++ Refresh MPRIS information on a timer. *format*: ++ @@ -97,7 +98,7 @@ The *mpris* module displays currently playing media via libplayerctl. *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. + Option to enable the length limits for the tooltip as well. By default, the tooltip ignores all length limits. *ellipsis*: ++ typeof: string ++ @@ -114,24 +115,27 @@ The *mpris* module displays currently playing media via libplayerctl. *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. ++ - If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ default: play-pause ++ Overwrite default action toggles. -*on-middle-click*: ++ +*on-click-middle*: ++ typeof: string ++ default: previous track ++ Overwrite default action toggles. -*on-right-click*: ++ +*on-click-right*: ++ typeof: string ++ default: next track ++ Overwrite default action toggles. diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index 2068ba7d..0081206e 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -14,7 +14,7 @@ Addressed by *network* *interface*: ++ typeof: string ++ - Use the defined interface instead of auto detection. Accepts wildcard. + Use the defined interface instead of auto-detection. Accepts wildcard. *interval*: ++ typeof: integer ++ @@ -49,7 +49,7 @@ Addressed by *network* *format-linked*: ++ typeof: string ++ - This format is used when a linked interface with no ip address is displayed. + This format is used when a linked interface with no IP address is displayed. *format-disconnected*: ++ typeof: string ++ @@ -78,11 +78,15 @@ Addressed by *network* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -94,7 +98,7 @@ Addressed by *network* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd new file mode 100644 index 00000000..82fad13b --- /dev/null +++ b/man/waybar-power-profiles-daemon.5.scd @@ -0,0 +1,72 @@ +waybar-power-profiles-daemon(5) + +# NAME + +waybar - power-profiles-daemon module + +# DESCRIPTION + +The *power-profiles-daemon* module displays the active power-profiles-daemon profile and cycle through the available profiles on click. + +# FILES + +$XDG_CONFIG_HOME/waybar/config + +# CONFIGURATION + + +[- *Option* +:- *Typeof* +:- *Default* +:= *Description* +|[ *format* +:[ string +:[ "{icon}" +:[ Message displayed on the bar. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. +|[ *tooltip-format* +:[ string +:[ "Power profile: {profile}\\nDriver: {driver}" +:[ Messaged displayed in the module tooltip. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. +|[ *tooltip* +:[ bool +:[ true +:[ Display the tooltip. +|[ *format-icons* +:[ object +:[ See default value in the example below. +:[ Icons used to represent the various power-profile. *Note*: the default configuration uses the font-awesome icons. You may want to override it if you don't have this font installed on your system. + + +# CONFIGURATION EXAMPLES + +Compact display (default config): + +``` +"power-profiles-daemon": { + "format": "{icon}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "", + "performance": "", + "balanced": "", + "power-saver": "" + } +} +``` + +Display the full profile name: + +``` +"power-profiles-daemon": { + "format": "{icon} {profile}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "", + "performance": "", + "balanced": "", + "power-saver": "" + } +} +``` diff --git a/man/waybar-privacy.5.scd b/man/waybar-privacy.5.scd new file mode 100644 index 00000000..d13d8ed3 --- /dev/null +++ b/man/waybar-privacy.5.scd @@ -0,0 +1,85 @@ +waybar-privacy(5) + +# NAME + +waybar - privacy module + +# DESCRIPTION + +The *privacy* module displays if any application is capturing audio, sharing ++ +the screen or playing audio. + +# CONFIGURATION + +*icon-spacing*: ++ + typeof: integer ++ + default: 4 ++ + The spacing between each privacy icon. + +*icon-size*: ++ + typeof: integer ++ + default: 20 ++ + The size of each privacy icon. + +*transition-duration*: ++ + typeof: integer ++ + default: 250 ++ + Option to disable tooltip on hover. + +*modules* ++ + typeof: array of objects ++ + default: [{"type": "screenshare"}, {"type": "audio-in"}] ++ + Which privacy modules to monitor. See *MODULES CONFIGURATION* for++ + more information. + +# MODULES CONFIGURATION + +*type*: ++ + typeof: string ++ + values: "screenshare", "audio-in", "audio-out" ++ + Specifies which module to use and configure. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-icon-size*: ++ + typeof: integer ++ + default: 24 ++ + The size of each icon in the tooltip. + +# EXAMPLES + +``` +"privacy": { + "icon-spacing": 4, + "icon-size": 18, + "transition-duration": 250, + "modules": [ + { + "type": "screenshare", + "tooltip": true, + "tooltip-icon-size": 24 + }, + { + "type": "audio-out", + "tooltip": true, + "tooltip-icon-size": 24 + }, + { + "type": "audio-in", + "tooltip": true, + "tooltip-icon-size": 24 + } + ] +}, +``` + +# STYLE + +- *#privacy* +- *#privacy-item* +- *#privacy-item.screenshare* +- *#privacy-item.audio-in* +- *#privacy-item.audio-out* diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd new file mode 100644 index 00000000..fc1da1c4 --- /dev/null +++ b/man/waybar-pulseaudio-slider.5.scd @@ -0,0 +1,83 @@ +waybar-pulseaudio-slider(5) + +# NAME + +waybar - pulseaudio slider module + +# DESCRIPTION + +The *pulseaudio slider* module displays and controls the current volume of the default sink or source as a bar. + +The volume can be controlled by dragging the slider across the bar or clicking on a specific position. + +# CONFIGURATION + +*min*: ++ + typeof: int ++ + default: 0 ++ + The minimum volume value the slider should display and set. + +*max*: ++ + typeof: int ++ + default: 100 ++ + The maximum volume value the slider should display and set. + +*orientation*: ++ + typeof: string ++ + default: horizontal ++ + The orientation of the slider. Can be either `horizontal` or `vertical`. + +# EXAMPLES + +``` +"modules-right": [ + "pulseaudio-slider", +], +"pulseaudio/slider": { + "min": 0, + "max": 100, + "orientation": "horizontal" +} +``` + +# STYLE + +The slider is a component with multiple CSS Nodes, of which the following are exposed: + +*#pulseaudio-slider*: ++ + Controls the style of the box *around* the slider and bar. + +*#pulseaudio-slider slider*: ++ + Controls the style of the slider handle. + +*#pulseaudio-slider trough*: ++ + Controls the style of the part of the bar that has not been filled. + +*#pulseaudio-slider highlight*: ++ + Controls the style of the part of the bar that has been filled. + +## STYLE EXAMPLE + +``` +#pulseaudio-slider slider { + min-height: 0px; + min-width: 0px; + opacity: 0; + background-image: none; + border: none; + box-shadow: none; +} + +#pulseaudio-slider trough { + min-height: 80px; + min-width: 10px; + border-radius: 5px; + background-color: black; +} + +#pulseaudio-slider highlight { + min-width: 10px; + border-radius: 5px; + background-color: green; +} +``` diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index bdb9c993..4bc75258 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -8,7 +8,7 @@ waybar - pulseaudio module The *pulseaudio* module displays the current volume reported by PulseAudio. -Additionally you can control the volume by scrolling *up* or *down* while the cursor is over the module. +Additionally, you can control the volume by scrolling *up* or *down* while the cursor is over the module. # CONFIGURATION @@ -36,7 +36,7 @@ Additionally you can control the volume by scrolling *up* or *down* while the cu *format-icons*: ++ typeof: array ++ - Based on the current port-name and volume, the corresponding icon gets selected. The order is *low* to *high*. See *Icons*. + Based on the current port name and volume, the corresponding icon gets selected. The order is *low* to *high*. See *Icons*. *rotate*: ++ typeof: integer ++ @@ -52,16 +52,20 @@ Additionally you can control the volume by scrolling *up* or *down* while the cu *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *scroll-step*: ++ typeof: float ++ default: 1.0 ++ - The speed in which to change the volume when scrolling. + The speed at which to change the volume when scrolling. *on-click*: ++ typeof: string ++ @@ -73,7 +77,7 @@ Additionally you can control the volume by scrolling *up* or *down* while the cu *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -107,7 +111,7 @@ Additionally you can control the volume by scrolling *up* or *down* while the cu *ignored-sinks*: ++ typeof: array ++ - Sinks in this list will not be shown as the active sink by Waybar. Entries should be the sink's description field. + Sinks in this list will not be shown as active sink by Waybar. Entries should be the sink's description field. # FORMAT REPLACEMENTS diff --git a/man/waybar-river-layout.5.scd b/man/waybar-river-layout.5.scd index 2a1206ce..1c09d6f6 100644 --- a/man/waybar-river-layout.5.scd +++ b/man/waybar-river-layout.5.scd @@ -29,11 +29,15 @@ Addressed by *river/layout* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -45,7 +49,7 @@ Addressed by *river/layout* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. # EXAMPLE diff --git a/man/waybar-river-mode.5.scd b/man/waybar-river-mode.5.scd index 8dfb0ec6..2d63b5e1 100644 --- a/man/waybar-river-mode.5.scd +++ b/man/waybar-river-mode.5.scd @@ -27,11 +27,15 @@ Addressed by *river/mode* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -43,7 +47,7 @@ Addressed by *river/mode* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index b117546d..f0b2b84e 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -24,7 +24,7 @@ Addressed by *river/tags* *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. + If set to false, you can left-click to set focused tag. Right-click to toggle tag focus. If set to true this behaviour is disabled. # EXAMPLE diff --git a/man/waybar-river-window.5.scd b/man/waybar-river-window.5.scd index 2f01e5c3..dbd9f130 100644 --- a/man/waybar-river-window.5.scd +++ b/man/waybar-river-window.5.scd @@ -27,11 +27,15 @@ Addressed by *river/window* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -43,7 +47,7 @@ Addressed by *river/window* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. # EXAMPLES diff --git a/man/waybar-sndio.5.scd b/man/waybar-sndio.5.scd index 01471392..197aaba0 100644 --- a/man/waybar-sndio.5.scd +++ b/man/waybar-sndio.5.scd @@ -28,16 +28,20 @@ cursor is over the module, and clicking on the module toggles mute. *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *scroll-step*: ++ typeof: int ++ default: 5 ++ - The speed in which to change the volume when scrolling. + The speed at which to change the volume when scrolling. *on-click*: ++ typeof: string ++ @@ -50,7 +54,7 @@ cursor is over the module, and clicking on the module toggles mute. *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd index c257ed75..1c62fd95 100644 --- a/man/waybar-sway-language.5.scd +++ b/man/waybar-sway-language.5.scd @@ -17,6 +17,11 @@ Addressed by *sway/language* default: {} ++ The format, how layout should be displayed. +*hide-single-layout*: ++ + typeof: bool ++ + default: false ++ + Defines visibility of the module if a single layout is configured + *tooltip-format*: ++ typeof: string ++ default: {} ++ diff --git a/man/waybar-sway-mode.5.scd b/man/waybar-sway-mode.5.scd index 2aca7b0c..44c8b81a 100644 --- a/man/waybar-sway-mode.5.scd +++ b/man/waybar-sway-mode.5.scd @@ -27,11 +27,15 @@ Addressed by *sway/mode* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -43,7 +47,7 @@ Addressed by *sway/mode* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index ef137873..037e6b55 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -27,11 +27,15 @@ Addressed by *sway/window* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -43,7 +47,7 @@ Addressed by *sway/window* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ @@ -78,7 +82,7 @@ Addressed by *sway/window* *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. + Only effective when both all-outputs and offscreen-style are true. On screens currently not focused, show the given text along with that workspace styles. *show-focused-workspace-name*: ++ typeof: bool ++ @@ -106,7 +110,7 @@ Addressed by *sway/window* *{app_id}*: The app_id of the focused window. *{shell}*: The shell of the focused window. It's 'xwayland' when the window is -running through xwayland, otherwise it's 'xdg-shell'. +running through xwayland, otherwise, it's 'xdg-shell'. # REWRITE RULES diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 82d858de..3343b8d5 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -60,10 +60,10 @@ Addressed by *sway/workspaces* default: false ++ If set to true. Only focused workspaces will be shown. -*persistent_workspaces*: ++ +*persistent-workspaces*: ++ typeof: json (see below) ++ default: empty ++ - Lists workspaces that should always be shown, even when non existent + Lists workspaces that should always be shown, even when non-existent *on-update*: ++ typeof: string ++ @@ -82,6 +82,23 @@ warp-on-scroll: ++ 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. +*window-rewrite*: ++ + typeof: object ++ + Regex rules to map window class to an icon or preferred method of representation for a workspace's window. + Keys are the rules, while the values are the methods of representation. + Rules may specify `class<...>`, `title<...>`, or both in order to fine-tune the matching. + +*window-rewrite-default*: + typeof: string ++ + default: "?" ++ + The default method of representation for a workspace's window. This will be used for windows whose classes do not match any of the rules in *window-rewrite*. + +*format-window-separator*: ++ + typeof: string ++ + default: " " ++ + The separator to be used between windows in a workspace. + + # FORMAT REPLACEMENTS *{value}*: Name of the workspace, as defined by sway. @@ -94,15 +111,17 @@ warp-on-scroll: ++ *{output}*: Output where the workspace is located. +*{windows}*: Result from window-rewrite + # ICONS Additional to workspace name matching, the following *format-icons* can be set. -- *default*: Will be shown, when no string matches is found. +- *default*: Will be shown, when no string matches are found. - *urgent*: Will be shown, when workspace is flagged as urgent - *focused*: Will be shown, when workspace is focused -- *persistent*: Will be shown, when workspace is persistent one. -- *high-priority-named*: Icons by names will be shown always for that workspaces, independent by state. +- *persistent*: Will be shown, when workspace is persistent. +- *high-priority-named*: Icons by names will be shown always for those workspaces, independent by state. # PERSISTENT WORKSPACES @@ -112,10 +131,10 @@ an empty list denoting all outputs. ``` "sway/workspaces": { - "persistent_workspaces": { - "3": [], // Always show a workspace with name '3', on all outputs if it does not exists - "4": ["eDP-1"], // Always show a workspace with name '4', on output 'eDP-1' if it does not exists - "5": ["eDP-1", "DP-2"] // Always show a workspace with name '5', on outputs 'eDP-1' and 'DP-2' if it does not exists + "persistent-workspaces": { + "3": [], // Always show a workspace with name '3', on all outputs if it does not exist + "4": ["eDP-1"], // Always show a workspace with name '4', on output 'eDP-1' if it does not exist + "5": ["eDP-1", "DP-2"] // Always show a workspace with name '5', on outputs 'eDP-1' and 'DP-2' if it does not exist } } ``` @@ -143,6 +162,19 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge } ``` +``` +"sway/workspaces": { + "format": "{name} {windows}", + "format-window-separator": " | ", + "window-rewrite-default": "{name}", + "window-format": "{name}", + "window-rewrite": { + "class": "", + "class": "k", + } +} +``` + # Style - *#workspaces button* diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd new file mode 100644 index 00000000..ac92c533 --- /dev/null +++ b/man/waybar-systemd-failed-units.5.scd @@ -0,0 +1,63 @@ +waybar-systemd-failed-units(5) + +# NAME + +waybar - systemd failed units monitor module + +# DESCRIPTION + +The *systemd-failed-units* module displays the number of failed systemd units. + +# CONFIGURATION + +Addressed by *systemd-failed-units* + +*format*: ++ + typeof: string ++ + default: *{nr_failed} failed* ++ + The format, how information should be displayed. This format is used when other formats aren't specified. + +*format-ok*: ++ + typeof: string ++ + This format is used when there is no failing units. + +*user*: ++ + typeof: bool ++ + default: *true* ++ + Option to count user systemd units. + +*system*: ++ + typeof: bool ++ + default: *true* ++ + Option to count systemwide (PID=1) systemd units. + +*hide-on-ok*: ++ + typeof: bool ++ + default: *true* ++ + Option to hide this module when there is no failing units. + +# FORMAT REPLACEMENTS + +*{nr_failed_system}*: Number of failed units from systemwide (PID=1) systemd. + +*{nr_failed_user}*: Number of failed units from user systemd. + +*{nr_failed}*: Number of total failed units. + +# EXAMPLES + +``` +"systemd-failed-units": { + "hide-on-ok": false, + "format": "✗ {nr_failed}", + "format-ok": "✓", + "system": true, + "user": false, +} +``` + +# STYLE + +- *#systemd-failed-units* +- *#systemd-failed-units.ok* +- *#systemd-failed-units.degraded* diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 8cb7367c..ff2168ea 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -67,15 +67,19 @@ Addressed by *temperature* *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ - Command to execute when you clicked on the module. + Command to execute when you click on the module. *on-click-middle*: ++ typeof: string ++ @@ -83,7 +87,7 @@ Addressed by *temperature* *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ diff --git a/man/waybar-upower.5.scd b/man/waybar-upower.5.scd index 5ccda07c..5e2a8eb8 100644 --- a/man/waybar-upower.5.scd +++ b/man/waybar-upower.5.scd @@ -57,6 +57,11 @@ compatible devices in the tooltip. typeof: string ++ Command to execute when clicked on the module. +*show-icon*: ++ + typeof: bool ++ + default: true ++ + Option to disable battery icon. + # FORMAT REPLACEMENTS *{percentage}*: The battery capacity in percentage @@ -93,6 +98,15 @@ depending on the charging state. "tooltip": true, "tooltip-spacing": 20 } +``` +``` +"upower": { + "show-icon": false, + "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 4d13b4f1..b08fd90f 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -43,16 +43,20 @@ The *wireplumber* module displays the current volume reported by WirePlumber. *min-length*: ++ typeof: integer ++ - The minimum length in characters the module should take up. + The minimum length in characters the module should accept. *align*: ++ typeof: float ++ - The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *scroll-step*: ++ typeof: float ++ default: 1.0 ++ - The speed in which to change the volume when scrolling. + The speed at which to change the volume when scrolling. *on-click*: ++ typeof: string ++ @@ -64,7 +68,7 @@ The *wireplumber* module displays the current volume reported by WirePlumber. *on-click-right*: ++ typeof: string ++ - Command to execute when you right clicked on the module. + Command to execute when you right-click on the module. *on-update*: ++ typeof: string ++ diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index 6c724b08..af1ba97f 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -16,7 +16,7 @@ Addressed by *wlr/taskbar* *all-outputs*: ++ typeof: bool ++ default: false ++ - If set to false applications on the waybar's current output will be shown. Otherwise all applications are shown. + If set to false applications on the waybar's current output will be shown. Otherwise, all applications are shown. *format*: ++ typeof: string ++ @@ -89,7 +89,7 @@ Addressed by *wlr/taskbar* *{icon}*: The icon of the application. -*{name}*: The application name as in desktop file if appropriate desktop fils found, otherwise same as {app_id} +*{name}*: The application name as in desktop file if appropriate desktop files are found, otherwise same as {app_id} *{title}*: The title of the application. diff --git a/man/waybar-wlr-workspaces.5.scd b/man/waybar-wlr-workspaces.5.scd index 1c9f2d89..62d3f636 100644 --- a/man/waybar-wlr-workspaces.5.scd +++ b/man/waybar-wlr-workspaces.5.scd @@ -30,17 +30,17 @@ Addressed by *wlr/workspaces* typeof: bool ++ default: true ++ Should workspaces be sorted by coordinates. ++ - Note that if both *sort-by-name* and *sort-by-coordinates* are true sort by name will be first. If both are false - sort by id will be performed. + Note that if both *sort-by-name* and *sort-by-coordinates* are true sort-by name will be first. If both are false - sort by id will be performed. *sort-by-number*: ++ typeof: bool ++ default: false ++ - If set to true, workspace names will be sorted numerically. Takes presedence over any other sort-by option. + If set to true, workspace names will be sorted numerically. Takes precedence over any other sort-by option. *all-outputs*: ++ typeof: bool ++ default: false ++ - If set to false workspaces group will be shown only in assigned output. Otherwise all workspace groups are shown. + If set to false workspaces group will be shown only in assigned output. Otherwise, all workspace groups are shown. *active-only*: ++ typeof: bool ++ @@ -61,7 +61,7 @@ Addressed by *wlr/workspaces* # ICONS -Additional to workspace name matching, the following *format-icons* can be set. +In addition 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 diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index a8376697..2d4de0c9 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -6,18 +6,18 @@ waybar - configuration file # DESCRIPTION -The configuration uses the JSON file format and is named *config*. +The configuration uses the JSONC file format and is named *config* or *config.jsonc*. Valid locations for this file are: -- *$XDG_CONFIG_HOME/waybar/config* -- *~/.config/waybar/config* -- *~/waybar/config* -- */etc/xdg/waybar/config* -- *@sysconfdir@/xdg/waybar/config* +- *$XDG_CONFIG_HOME/waybar/* +- *~/.config/waybar/* +- *~/waybar/* +- */etc/xdg/waybar/* +- *@sysconfdir@/xdg/waybar/* -A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config -Also a minimal example configuration can be found on the at the bottom of this man page. +A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config.jsonc +Also, a minimal example configuration can be found at the bottom of this man page. # BAR CONFIGURATION @@ -30,7 +30,7 @@ Also a minimal example configuration can be found on the at the bottom of this m *output* ++ 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. + 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* ++ @@ -68,7 +68,7 @@ Also a minimal example configuration can be found on the at the bottom of this m *spacing* ++ typeof: integer ++ - Size of gaps in between of the different modules. + Size of gaps in between the different modules. *name* ++ typeof: string ++ @@ -89,7 +89,7 @@ Also a minimal example configuration can be found on the at the bottom of this m default: *press* Defines the timing of modifier key to reset the bar visibility. To reset the visibility of the bar with the press of the modifier key use *press*. - Use *release* to reset the visibility upon the release of the modifier key and only if no other action happened while the key was pressed. This prevents hiding the bar when the modifier is used to switch a workspace, change binding mode or start a keybinding. + Use *release* to reset the visibility upon the release of the modifier key and only if no other action happened while the key was pressed. This prevents hiding the bar when the modifier is used to switch a workspace, change binding mode, or start a keybinding. *exclusive* ++ typeof: bool ++ @@ -108,21 +108,15 @@ Also a minimal example configuration can be found on the at the bottom of this m Option to pass any pointer events to the window under the bar. Intended to be used with either *top* or *overlay* layers and without exclusive zone. -*gtk-layer-shell* ++ - typeof: bool ++ - default: true ++ - Option to disable the use of gtk-layer-shell for popups. - Only functional if compiled with gtk-layer-shell support. - *ipc* ++ typeof: bool ++ default: false ++ Option to subscribe to the Sway IPC bar configuration and visibility events and control waybar with *swaymsg bar* commands. ++ - Requires *bar_id* value from sway configuration to be either passed with the *-b* commandline argument or specified with the *id* option. + Requires *bar_id* value from sway configuration to be either passed with the *-b* command line argument or specified with the *id* option. *id* ++ typeof: string ++ - *bar_id* for the Sway IPC. Use this if you need to override the value passed with the *-b bar_id* commandline argument for the specific bar instance. + *bar_id* for the Sway IPC. Use this if you need to override the value passed with the *-b bar_id* command line argument for the specific bar instance. *include* ++ typeof: string|array ++ @@ -130,6 +124,11 @@ Also a minimal example configuration can be found on the at the bottom of this m Each file can contain a single object with any of the bar configuration options. In case of duplicate options, the first defined value takes precedence, i.e. including file -> first included file -> etc. Nested includes are permitted, but make sure to avoid circular imports. For a multi-bar config, the include directive affects only current bar configuration object. +*reload_style_on_change* ++ + typeof: bool ++ + default: *false* ++ + Option to enable reloading the css style if a modification is detected on the style sheet file or any imported css files. + # MODULE FORMAT You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). @@ -142,7 +141,7 @@ e.g. # MULTIPLE INSTANCES OF A MODULE If you want to have a second instance of a module, you can suffix it by a '#' and a custom name. -For example if you want a second battery module, you can add *"battery#bat2"* to your modules. +For example, if you want a second battery module, you can add *"battery#bat2"* to your modules. To configure the newly added module, you then also add a module configuration with the same name. This could then look something like this *(this is an incomplete example)*: @@ -236,11 +235,11 @@ When positioning Waybar on the left or right side of the screen, sometimes it's } ``` -Valid options for the "rotate" property are: 0, 90, 180 and 270. +Valid options for the "rotate" property are: 0, 90, 180, and 270. ## Grouping modules -Module groups allow stacking modules in the direction orthogonal to the bar direction. When the bar is positioned on the top or bottom of the screen, modules in a group are stacked vertically. Likewise, when positioned on the left or right, modules in a group are stacked horizontally. +Module groups allow stacking modules in any direction. By default, when the bar is positioned on the top or bottom of the screen, modules in a group are stacked vertically. Likewise, when positioned on the left or right, modules in a group are stacked horizontally. This can be changed with the "orientation" property. A module group is defined by specifying a module named "group/some-group-name". The group must also be configured with a list of contained modules. Example: @@ -263,6 +262,43 @@ 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). +## Group Drawers + +A group may hide all but one element, showing them only on mouse hover. In order to configure this, you can use the `drawer` property, whose value is an object with the following properties: + +*transition-duration*: ++ + typeof: integer ++ + default: 500 ++ + Defines the duration of the transition animation in milliseconds. + +*children-class*: ++ + typeof: string ++ + default: "hidden" ++ + Defines the CSS class to be applied to the hidden elements. + +*transition-left-to-right*: ++ + typeof: bool ++ + default: true ++ + Defines the direction of the transition animation. If true, the hidden elements will slide from left to right. If false, they will slide from right to left. + When the bar is vertical, it reads as top-to-bottom. + +``` +"group/power": { + "orientation": "inherit", + "drawer": { + "transition-duration": 500, + "children-class": "not-power", + "transition-left-to-right": false, + }, + "modules": [ + "custom/power", // First element is the "group leader" and won't ever be hidden + "custom/quit", + "custom/lock", + "custom/reboot", + ] +}, +``` + # SUPPORTED MODULES - *waybar-backlight(5)* @@ -273,28 +309,39 @@ Valid options for the (optional) "orientation" property are: "horizontal", "vert - *waybar-cpu(5)* - *waybar-custom(5)* - *waybar-disk(5)* +- *waybar-dwl-tags(5)* +- *waybar-gamemode(5)* +- *waybar-hyprland-language(5)* +- *waybar-hyprland-submap(5)* +- *waybar-hyprland-window(5)* +- *waybar-hyprland-workspaces(5)* - *waybar-idle-inhibitor(5)* - *waybar-image(5)* +- *waybar-inhibitor(5)* +- *waybar-jack(5)* - *waybar-keyboard-state(5)* - *waybar-memory(5)* - *waybar-mpd(5)* - *waybar-mpris(5)* - *waybar-network(5)* - *waybar-pulseaudio(5)* +- *waybar-river-layout(5)* - *waybar-river-mode(5)* - *waybar-river-tags(5)* - *waybar-river-window(5)* -- *waybar-river-layout(5)* +- *waybar-sndio(5)* - *waybar-states(5)* +- *waybar-sway-language(5)* - *waybar-sway-mode(5)* - *waybar-sway-scratchpad(5)* - *waybar-sway-window(5)* - *waybar-sway-workspaces(5)* +- *waybar-temperature(5)* +- *waybar-tray(5)* +- *waybar-upower(5)* - *waybar-wireplumber(5)* - *waybar-wlr-taskbar(5)* - *waybar-wlr-workspaces(5)* -- *waybar-temperature(5)* -- *waybar-tray(5)* # SEE ALSO diff --git a/meson.build b/meson.build index 18b056ad..4ce7363d 100644 --- a/meson.build +++ b/meson.build @@ -1,8 +1,8 @@ project( 'waybar', 'cpp', 'c', - version: '0.9.20', + version: '0.9.24', license: 'MIT', - meson_version: '>= 0.50.0', + meson_version: '>= 0.59.0', default_options : [ 'cpp_std=c++20', 'buildtype=release', @@ -22,8 +22,6 @@ endif if compiler.has_link_argument('-lc++fs') cpp_link_args += ['-lc++fs'] -elif compiler.has_link_argument('-lc++experimental') - cpp_link_args += ['-lc++experimental'] elif compiler.has_link_argument('-lstdc++fs') cpp_link_args += ['-lstdc++fs'] endif @@ -33,10 +31,10 @@ git = find_program('git', native: true, required: false) if not git.found() add_project_arguments('-DVERSION="@0@"'.format(meson.project_version()), language: 'cpp') else - git_path = run_command([git.path(), 'rev-parse', '--show-toplevel']).stdout().strip() - if meson.source_root() == git_path - git_commit_hash = run_command([git.path(), 'describe', '--always', '--tags']).stdout().strip() - git_branch = run_command([git.path(), 'rev-parse', '--abbrev-ref', 'HEAD']).stdout().strip() + git_path = run_command(git, 'rev-parse', '--show-toplevel', check: false).stdout().strip() + if meson.project_source_root() == git_path + git_commit_hash = run_command(git, 'describe', '--always', '--tags', check: false).stdout().strip() + git_branch = run_command(git, 'rev-parse', '--abbrev-ref', 'HEAD', check: false).stdout().strip() version = '"@0@ (branch \'@1@\')"'.format(git_commit_hash, git_branch) add_project_arguments('-DVERSION=@0@'.format(version), language: 'cpp') else @@ -44,15 +42,6 @@ else endif endif -if not compiler.has_header('filesystem') - if compiler.has_header('experimental/filesystem') - add_project_arguments('-DFILESYSTEM_EXPERIMENTAL', language: 'cpp') - else - add_project_arguments('-DNO_FILESYSTEM', language: 'cpp') - warning('No filesystem header found, some modules may not work') - endif -endif - code = ''' #include #include @@ -86,10 +75,7 @@ wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols') gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0']) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk')) -giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or - get_option('logind').enabled() or - get_option('upower_glib').enabled() or - get_option('mpris').enabled())) +giounix = dependency('gio-unix-2.0') jsoncpp = dependency('jsoncpp', version : ['>=1.9.2'], fallback : ['jsoncpp', 'jsoncpp_dep']) sigcpp = dependency('sigc++-2.0') libinotify = dependency('libinotify', required: false) @@ -98,6 +84,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')) +pipewire = dependency('libpipewire-0.3', required: get_option('pipewire')) 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')) @@ -119,13 +106,28 @@ if libsndio.found() endif endif -gtk_layer_shell = dependency('gtk-layer-shell-0', - required: get_option('gtk-layer-shell'), - fallback : ['gtk-layer-shell', 'gtk_layer_shell_dep']) +gtk_layer_shell = dependency('gtk-layer-shell-0', version: ['>=0.6.0'], + default_options: ['introspection=false', 'vapi=false'], + fallback: ['gtk-layer-shell', 'gtk_layer_shell']) systemd = dependency('systemd', required: get_option('systemd')) cpp_lib_chrono = compiler.compute_int('__cpp_lib_chrono', prefix : '#include ') -have_chrono_timezones = cpp_lib_chrono >= 201907 +have_chrono_timezones = cpp_lib_chrono >= 201611 + +if have_chrono_timezones + code = ''' +#include +using namespace std::chrono; +int main(int argc, char** argv) { + const time_zone* tz; + return 0; +} +''' + if not compiler.links(code) + have_chrono_timezones = false + endif +endif + if have_chrono_timezones tz_dep = declare_dependency() else @@ -141,10 +143,10 @@ sysconfdir = get_option('sysconfdir') conf_data = configuration_data() conf_data.set('prefix', prefix) -add_project_arguments('-DSYSCONFDIR="/@0@"'.format(join_paths(prefix, sysconfdir)), language : 'cpp') +add_project_arguments('-DSYSCONFDIR="@0@"'.format(prefix / sysconfdir), language : 'cpp') if systemd.found() - user_units_dir = systemd.get_pkgconfig_variable('systemduserunitdir') + user_units_dir = systemd.get_variable(pkgconfig: 'systemduserunitdir') configure_file( configuration: conf_data, @@ -164,18 +166,33 @@ src_files = files( 'src/modules/disk.cpp', 'src/modules/idle_inhibitor.cpp', 'src/modules/image.cpp', + 'src/modules/load.cpp', 'src/modules/temperature.cpp', 'src/modules/user.cpp', + 'src/ASlider.cpp', 'src/main.cpp', 'src/bar.cpp', 'src/client.cpp', 'src/config.cpp', 'src/group.cpp', + 'src/util/portal.cpp', + 'src/util/enum.cpp', 'src/util/prepare_for_sleep.cpp', 'src/util/ustring_clen.cpp', 'src/util/sanitize_str.cpp', 'src/util/rewrite_string.cpp', - 'src/util/gtk_icon.cpp' + 'src/util/gtk_icon.cpp', + 'src/util/regex_collection.cpp', + 'src/util/css_reload_helper.cpp' +) + +man_files = files( + 'man/waybar-custom.5.scd', + 'man/waybar-disk.5.scd', + 'man/waybar-idle-inhibitor.5.scd', + 'man/waybar-image.5.scd', + 'man/waybar-states.5.scd', + 'man/waybar-temperature.5.scd', ) inc_dirs = ['include'] @@ -183,103 +200,186 @@ inc_dirs = ['include'] if is_linux add_project_arguments('-DHAVE_CPU_LINUX', language: 'cpp') add_project_arguments('-DHAVE_MEMORY_LINUX', language: 'cpp') + add_project_arguments('-DHAVE_SYSTEMD_MONITOR', language: 'cpp') src_files += files( 'src/modules/battery.cpp', - 'src/modules/cpu/common.cpp', - 'src/modules/cpu/linux.cpp', + 'src/modules/bluetooth.cpp', + 'src/modules/cffi.cpp', + 'src/modules/cpu.cpp', + 'src/modules/cpu_frequency/common.cpp', + 'src/modules/cpu_frequency/linux.cpp', + 'src/modules/cpu_usage/common.cpp', + 'src/modules/cpu_usage/linux.cpp', 'src/modules/memory/common.cpp', 'src/modules/memory/linux.cpp', + 'src/modules/power_profiles_daemon.cpp', + 'src/modules/systemd_failed_units.cpp', + ) + man_files += files( + 'man/waybar-battery.5.scd', + 'man/waybar-bluetooth.5.scd', + 'man/waybar-cffi.5.scd', + 'man/waybar-cpu.5.scd', + 'man/waybar-memory.5.scd', + 'man/waybar-systemd-failed-units.5.scd', + 'man/waybar-power-profiles-daemon.5.scd', ) elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd add_project_arguments('-DHAVE_CPU_BSD', language: 'cpp') add_project_arguments('-DHAVE_MEMORY_BSD', language: 'cpp') src_files += files( - 'src/modules/cpu/bsd.cpp', - 'src/modules/cpu/common.cpp', + 'src/modules/cffi.cpp', + 'src/modules/cpu.cpp', + 'src/modules/cpu_frequency/bsd.cpp', + 'src/modules/cpu_frequency/common.cpp', + 'src/modules/cpu_usage/bsd.cpp', + 'src/modules/cpu_usage/common.cpp', 'src/modules/memory/bsd.cpp', 'src/modules/memory/common.cpp', ) + man_files += files( + 'man/waybar-cffi.5.scd', + 'man/waybar-cpu.5.scd', + 'man/waybar-memory.5.scd', + ) if is_freebsd - src_files += files( - 'src/modules/battery.cpp', - ) + src_files += files('src/modules/battery.cpp') + man_files += files('man/waybar-battery.5.scd') endif endif -add_project_arguments('-DHAVE_SWAY', language: 'cpp') -src_files += [ - 'src/modules/sway/ipc/client.cpp', - 'src/modules/sway/bar.cpp', - 'src/modules/sway/mode.cpp', - 'src/modules/sway/language.cpp', - 'src/modules/sway/window.cpp', - 'src/modules/sway/workspaces.cpp', - 'src/modules/sway/scratchpad.cpp' -] +if true + add_project_arguments('-DHAVE_SWAY', language: 'cpp') + src_files += files( + 'src/modules/sway/ipc/client.cpp', + 'src/modules/sway/bar.cpp', + 'src/modules/sway/mode.cpp', + 'src/modules/sway/language.cpp', + 'src/modules/sway/window.cpp', + 'src/modules/sway/workspaces.cpp', + 'src/modules/sway/scratchpad.cpp' + ) + man_files += files( + 'man/waybar-sway-language.5.scd', + 'man/waybar-sway-mode.5.scd', + 'man/waybar-sway-scratchpad.5.scd', + 'man/waybar-sway-window.5.scd', + 'man/waybar-sway-workspaces.5.scd', + ) +endif if true - add_project_arguments('-DHAVE_WLR', language: 'cpp') - src_files += 'src/modules/wlr/taskbar.cpp' - src_files += 'src/modules/wlr/workspace_manager.cpp' - src_files += 'src/modules/wlr/workspace_manager_binding.cpp' + add_project_arguments('-DHAVE_WLR_TASKBAR', language: 'cpp') + src_files += files('src/modules/wlr/taskbar.cpp') + man_files += files('man/waybar-wlr-taskbar.5.scd') endif if true add_project_arguments('-DHAVE_RIVER', language: 'cpp') - 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' + src_files += files( + 'src/modules/river/layout.cpp', + 'src/modules/river/mode.cpp', + 'src/modules/river/tags.cpp', + 'src/modules/river/window.cpp', + ) + man_files += files( + 'man/waybar-river-layout.5.scd', + 'man/waybar-river-mode.5.scd', + 'man/waybar-river-tags.5.scd', + 'man/waybar-river-window.5.scd', + ) endif if true add_project_arguments('-DHAVE_DWL', language: 'cpp') - src_files += 'src/modules/dwl/tags.cpp' + src_files += files('src/modules/dwl/tags.cpp') + man_files += files('man/waybar-dwl-tags.5.scd') endif if true add_project_arguments('-DHAVE_HYPRLAND', language: 'cpp') - 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' + src_files += files( + 'src/modules/hyprland/backend.cpp', + 'src/modules/hyprland/language.cpp', + 'src/modules/hyprland/submap.cpp', + 'src/modules/hyprland/window.cpp', + 'src/modules/hyprland/workspaces.cpp', + ) + man_files += files( + 'man/waybar-hyprland-language.5.scd', + 'man/waybar-hyprland-submap.5.scd', + 'man/waybar-hyprland-window.5.scd', + 'man/waybar-hyprland-workspaces.5.scd', + ) endif if libnl.found() and libnlgen.found() add_project_arguments('-DHAVE_LIBNL', language: 'cpp') - src_files += 'src/modules/network.cpp' + src_files += files('src/modules/network.cpp') + man_files += files('man/waybar-network.5.scd') endif -if (giounix.found() and not get_option('logind').disabled()) - add_project_arguments('-DHAVE_GAMEMODE', language: 'cpp') - src_files += 'src/modules/gamemode.cpp' +if not get_option('logind').disabled() + add_project_arguments('-DHAVE_GAMEMODE', '-DHAVE_LOGIND_INHIBITOR', language: 'cpp') + src_files += files( + 'src/modules/gamemode.cpp', + 'src/modules/inhibitor.cpp', + ) + man_files += files( + 'man/waybar-gamemode.5.scd', + 'man/waybar-inhibitor.5.scd', + ) endif -if (upower_glib.found() and giounix.found() and not get_option('logind').disabled()) +if (upower_glib.found() and not get_option('logind').disabled()) add_project_arguments('-DHAVE_UPOWER', language: 'cpp') - src_files += 'src/modules/upower/upower.cpp' - src_files += 'src/modules/upower/upower_tooltip.cpp' + src_files += files( + 'src/modules/upower/upower.cpp', + 'src/modules/upower/upower_tooltip.cpp', + ) + man_files += files('man/waybar-upower.5.scd') endif -if (playerctl.found() and giounix.found() and not get_option('logind').disabled()) + +if pipewire.found() + add_project_arguments('-DHAVE_PIPEWIRE', language: 'cpp') + src_files += files( + 'src/modules/privacy/privacy.cpp', + 'src/modules/privacy/privacy_item.cpp', + 'src/util/pipewire_backend.cpp', + ) + man_files += files('man/waybar-privacy.5.scd') +endif + +if playerctl.found() add_project_arguments('-DHAVE_MPRIS', language: 'cpp') - src_files += 'src/modules/mpris/mpris.cpp' + src_files += files('src/modules/mpris/mpris.cpp') + man_files += files('man/waybar-mpris.5.scd') endif if libpulse.found() add_project_arguments('-DHAVE_LIBPULSE', language: 'cpp') - src_files += 'src/modules/pulseaudio.cpp' + src_files += files( + 'src/modules/pulseaudio.cpp', + 'src/modules/pulseaudio_slider.cpp', + 'src/util/audio_backend.cpp', + ) + man_files += files( + 'man/waybar-pulseaudio.5.scd', + 'man/waybar-pulseaudio-slider.5.scd', + ) endif if libjack.found() add_project_arguments('-DHAVE_LIBJACK', language: 'cpp') - src_files += 'src/modules/jack.cpp' + src_files += files('src/modules/jack.cpp') + man_files += files('man/waybar-jack.5.scd') endif if libwireplumber.found() add_project_arguments('-DHAVE_LIBWIREPLUMBER', language: 'cpp') - src_files += 'src/modules/wireplumber.cpp' + src_files += files('src/modules/wireplumber.cpp') + man_files += files('man/waybar-wireplumber.5.scd') endif if dbusmenu_gtk.found() @@ -290,38 +390,46 @@ if dbusmenu_gtk.found() 'src/modules/sni/host.cpp', 'src/modules/sni/item.cpp' ) + man_files += files( + 'man/waybar-tray.5.scd', + ) endif if libudev.found() and (is_linux or libepoll.found()) add_project_arguments('-DHAVE_LIBUDEV', language: 'cpp') - src_files += 'src/modules/backlight.cpp' + src_files += files( + 'src/modules/backlight.cpp', + 'src/modules/backlight_slider.cpp', + 'src/util/backlight_backend.cpp', + ) + man_files += files( + 'man/waybar-backlight.5.scd', + 'man/waybar-backlight-slider.5.scd', + ) endif if libevdev.found() and (is_linux or libepoll.found()) and libinput.found() and (is_linux or libinotify.found()) add_project_arguments('-DHAVE_LIBEVDEV', language: 'cpp') add_project_arguments('-DHAVE_LIBINPUT', language: 'cpp') - src_files += 'src/modules/keyboard_state.cpp' + src_files += files('src/modules/keyboard_state.cpp') + man_files += files('man/waybar-keyboard-state.5.scd') endif if libmpdclient.found() add_project_arguments('-DHAVE_LIBMPDCLIENT', language: 'cpp') - src_files += 'src/modules/mpd/mpd.cpp' - src_files += 'src/modules/mpd/state.cpp' -endif - -if gtk_layer_shell.found() - add_project_arguments('-DHAVE_GTK_LAYER_SHELL', language: 'cpp') + src_files += files( + 'src/modules/mpd/mpd.cpp', + 'src/modules/mpd/state.cpp', + ) + man_files += files( + 'man/waybar-mpd.5.scd', + ) endif if libsndio.found() add_project_arguments('-DHAVE_LIBSNDIO', language: 'cpp') - src_files += 'src/modules/sndio.cpp' -endif - -if (giounix.found() and not get_option('logind').disabled()) - add_project_arguments('-DHAVE_GIO_UNIX', language: 'cpp') - src_files += 'src/modules/inhibitor.cpp' - src_files += 'src/modules/bluetooth.cpp' + src_files += files('src/modules/sndio.cpp') + man_files += files('man/waybar-sndio.5.scd') endif if get_option('rfkill').enabled() and is_linux @@ -333,34 +441,48 @@ endif if have_chrono_timezones add_project_arguments('-DHAVE_CHRONO_TIMEZONES', language: 'cpp') - src_files += 'src/modules/clock.cpp' + src_files += files('src/modules/clock.cpp') + man_files += files('man/waybar-clock.5.scd') elif tz_dep.found() add_project_arguments('-DHAVE_LIBDATE', language: 'cpp') - src_files += 'src/modules/clock.cpp' + src_files += files('src/modules/clock.cpp') + man_files += files('man/waybar-clock.5.scd') else - src_files += 'src/modules/simpleclock.cpp' + src_files += files('src/modules/simpleclock.cpp') + man_files += files('man/waybar-clock.5.scd') endif if get_option('experimental') - add_project_arguments('-DUSE_EXPERIMENTAL', language: 'cpp') + add_project_arguments('-DHAVE_WLR_WORKSPACES', language: 'cpp') + src_files += files( + 'src/modules/wlr/workspace_manager.cpp', + 'src/modules/wlr/workspace_manager_binding.cpp', + ) + man_files += files( + 'man/waybar-wlr-workspaces.5.scd', + ) endif cava = dependency('cava', - version : '>=0.8.5', + version : '>=0.10.1', 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' + src_files += files('src/modules/cava.cpp') + man_files += files('man/waybar-cava.5.scd') endif subdir('protocol') +app_resources = [] +subdir('resources/icons') + executable( 'waybar', - src_files, + [src_files, app_resources], dependencies: [ thread_dep, client_protos, @@ -377,6 +499,7 @@ executable( libnl, libnlgen, upower_glib, + pipewire, playerctl, libpulse, libjack, @@ -397,74 +520,26 @@ executable( ) install_data( - './resources/config', - './resources/style.css', - install_dir: sysconfdir + '/xdg/waybar' + 'resources/config.jsonc', + 'resources/style.css', + install_dir: sysconfdir / 'xdg/waybar' ) scdoc = dependency('scdoc', version: '>=1.9.2', native: true, required: get_option('man-pages')) if scdoc.found() - scdoc_prog = find_program(scdoc.get_pkgconfig_variable('scdoc'), native: true) - sh = find_program('sh', native: true) - - main_manpage = configure_file( + man_files += configure_file( input: 'man/waybar.5.scd.in', output: 'waybar.5.scd', configuration: { - 'sysconfdir': join_paths(prefix, sysconfdir) + 'sysconfdir': prefix / sysconfdir } ) - main_manpage_path = join_paths(meson.build_root(), '@0@'.format(main_manpage)) - + fs = import('fs') mandir = get_option('mandir') - man_files = [ - main_manpage_path, - 'waybar-backlight.5.scd', - 'waybar-battery.5.scd', - 'waybar-cava.5.scd', - 'waybar-clock.5.scd', - 'waybar-cpu.5.scd', - 'waybar-custom.5.scd', - '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', - 'waybar-sway-window.5.scd', - 'waybar-sway-workspaces.5.scd', - 'waybar-temperature.5.scd', - 'waybar-tray.5.scd', - 'waybar-states.5.scd', - 'waybar-wlr-taskbar.5.scd', - 'waybar-wlr-workspaces.5.scd', - 'waybar-bluetooth.5.scd', - 'waybar-sndio.5.scd', - 'waybar-upower.5.scd', - 'waybar-wireplumber.5.scd', - 'waybar-dwl-tags.5.scd', - ] - - if (giounix.found() and not get_option('logind').disabled()) - man_files += 'waybar-inhibitor.5.scd' - endif - foreach file : man_files - path = '@0@'.format(file) - basename = path.split('/')[-1] + basename = fs.name(file) topic = basename.split('.')[-3] section = basename.split('.')[-2] @@ -472,12 +547,11 @@ if scdoc.found() custom_target( output, - # drops the 'man' if `path` is an absolute path - input: join_paths('man', path), + input: file, output: output, - command: [ - sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc_prog.path(), output) - ], + command: scdoc.get_variable('scdoc'), + feed: true, + capture: true, install: true, install_dir: '@0@/man@1@'.format(mandir, section) ) @@ -486,7 +560,7 @@ endif catch2 = dependency( 'catch2', - version: '>=2.0.0', + default_options: [ 'tests=false' ], fallback: ['catch2', 'catch2_dep'], required: get_option('tests'), ) @@ -502,7 +576,6 @@ if clangtidy.found() command: [ clangtidy, '-checks=*,-fuchsia-default-arguments', - '-p', meson.build_root() + '-p', meson.project_build_root() ] + src_files) endif - diff --git a/meson_options.txt b/meson_options.txt index 7dacf087..fef50839 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,12 +5,12 @@ 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('pipewire', type: 'feature', value: 'auto', description: 'Enable support for pipewire') 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') option('mpd', type: 'feature', value: 'auto', description: 'Enable support for the Music Player Daemon') -option('gtk-layer-shell', type: 'feature', value: 'auto', description: 'Use gtk-layer-shell library for popups support') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio') option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind') diff --git a/nix/default.nix b/nix/default.nix index fc77225d..bf8f2f21 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,20 +1,35 @@ { lib +, pkgs , 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 ../.; +let + libcava = rec { + version = "0.10.1"; + src = pkgs.fetchFromGitHub { + owner = "LukashonakV"; + repo = "cava"; + rev = version; + hash = "sha256-iIYKvpOWafPJB5XhDOSIW9Mb4I3A4pcgIIPQdQYEqUw="; + }; }; -}) +in +(waybar.overrideAttrs ( + oldAttrs: { + inherit version; + + src = lib.cleanSourceWith { + filter = name: type: type != "regular" || !lib.hasSuffix ".nix" name; + src = lib.cleanSource ../.; + }; + + mesonFlags = lib.remove "-Dgtk-layer-shell=enabled" oldAttrs.mesonFlags; + + postUnpack = '' + pushd "$sourceRoot" + cp -R --no-preserve=mode,ownership ${libcava.src} subprojects/cava-${libcava.version} + patchShebangs . + popd + ''; + } +)) \ No newline at end of file diff --git a/protocol/meson.build b/protocol/meson.build index e1e745a9..cd9a77b1 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -1,4 +1,4 @@ -wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') +wl_protocol_dir = wayland_protos.get_variable(pkgconfig: 'pkgdatadir') wayland_scanner = find_program('wayland-scanner') @@ -25,7 +25,6 @@ client_protocols = [ [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'], [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], [wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'], - ['wlr-layer-shell-unstable-v1.xml'], ['wlr-foreign-toplevel-management-unstable-v1.xml'], ['ext-workspace-unstable-v1.xml'], ['river-status-unstable-v1.xml'], @@ -44,7 +43,7 @@ endforeach gdbus_codegen = find_program('gdbus-codegen') -r = run_command(gdbus_codegen, '--body', '--output', '/dev/null') +r = run_command(gdbus_codegen, '--body', '--output', '/dev/null', check: false) if r.returncode() != 0 gdbus_code_dsnw = custom_target( 'dbus-status-notifier-watcher.[ch]', diff --git a/protocol/wlr-layer-shell-unstable-v1.xml b/protocol/wlr-layer-shell-unstable-v1.xml deleted file mode 100644 index f9a4fe05..00000000 --- a/protocol/wlr-layer-shell-unstable-v1.xml +++ /dev/null @@ -1,311 +0,0 @@ - - - - Copyright © 2017 Drew DeVault - - Permission to use, copy, modify, distribute, and sell this - software and its documentation for any purpose is hereby granted - without fee, provided that the above copyright notice appear in - all copies and that both that copyright notice and this permission - notice appear in supporting documentation, and that the name of - the copyright holders not be used in advertising or publicity - pertaining to distribution of the software without specific, - written prior permission. The copyright holders make no - representations about the suitability of this software for any - purpose. It is provided "as is" without express or implied - warranty. - - THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS - SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY - SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN - AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, - ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF - THIS SOFTWARE. - - - - - Clients can use this interface to assign the surface_layer role to - wl_surfaces. Such surfaces are assigned to a "layer" of the output and - rendered with a defined z-depth respective to each other. They may also be - anchored to the edges and corners of a screen and specify input handling - semantics. This interface should be suitable for the implementation of - many desktop shell components, and a broad number of other applications - that interact with the desktop. - - - - - Create a layer surface for an existing surface. This assigns the role of - layer_surface, or raises a protocol error if another role is already - assigned. - - Creating a layer surface from a wl_surface which has a buffer attached - or committed is a client error, and any attempts by a client to attach - or manipulate a buffer prior to the first layer_surface.configure call - must also be treated as errors. - - You may pass NULL for output to allow the compositor to decide which - output to use. Generally this will be the one that the user most - recently interacted with. - - Clients can specify a namespace that defines the purpose of the layer - surface. - - - - - - - - - - - - - - - - - These values indicate which layers a surface can be rendered in. They - are ordered by z depth, bottom-most first. Traditional shell surfaces - will typically be rendered between the bottom and top layers. - Fullscreen shell surfaces are typically rendered at the top layer. - Multiple surfaces can share a single layer, and ordering within a - single layer is undefined. - - - - - - - - - - - - - This request indicates that the client will not use the layer_shell - object any more. Objects that have been created through this instance - are not affected. - - - - - - - An interface that may be implemented by a wl_surface, for surfaces that - are designed to be rendered as a layer of a stacked desktop-like - environment. - - Layer surface state (layer, size, anchor, exclusive zone, - margin, interactivity) is double-buffered, and will be applied at the - time wl_surface.commit of the corresponding wl_surface is called. - - - - - Sets the size of the surface in surface-local coordinates. The - compositor will display the surface centered with respect to its - anchors. - - If you pass 0 for either value, the compositor will assign it and - inform you of the assignment in the configure event. You must set your - anchor to opposite edges in the dimensions you omit; not doing so is a - protocol error. Both values are 0 by default. - - Size is double-buffered, see wl_surface.commit. - - - - - - - - Requests that the compositor anchor the surface to the specified edges - and corners. If two orthogonal edges are specified (e.g. 'top' and - 'left'), then the anchor point will be the intersection of the edges - (e.g. the top left corner of the output); otherwise the anchor point - will be centered on that edge, or in the center if none is specified. - - Anchor is double-buffered, see wl_surface.commit. - - - - - - - Requests that the compositor avoids occluding an area with other - surfaces. The compositor's use of this information is - implementation-dependent - do not assume that this region will not - actually be occluded. - - A positive value is only meaningful if the surface is anchored to one - edge or an edge and both perpendicular edges. If the surface is not - anchored, anchored to only two perpendicular edges (a corner), anchored - to only two parallel edges or anchored to all edges, a positive value - will be treated the same as zero. - - A positive zone is the distance from the edge in surface-local - coordinates to consider exclusive. - - Surfaces that do not wish to have an exclusive zone may instead specify - how they should interact with surfaces that do. If set to zero, the - surface indicates that it would like to be moved to avoid occluding - surfaces with a positive exclusive zone. If set to -1, the surface - indicates that it would not like to be moved to accommodate for other - surfaces, and the compositor should extend it all the way to the edges - it is anchored to. - - For example, a panel might set its exclusive zone to 10, so that - maximized shell surfaces are not shown on top of it. A notification - might set its exclusive zone to 0, so that it is moved to avoid - occluding the panel, but shell surfaces are shown underneath it. A - wallpaper or lock screen might set their exclusive zone to -1, so that - they stretch below or over the panel. - - The default value is 0. - - Exclusive zone is double-buffered, see wl_surface.commit. - - - - - - - Requests that the surface be placed some distance away from the anchor - point on the output, in surface-local coordinates. Setting this value - for edges you are not anchored to has no effect. - - The exclusive zone includes the margin. - - Margin is double-buffered, see wl_surface.commit. - - - - - - - - - - Set to 1 to request that the seat send keyboard events to this layer - surface. For layers below the shell surface layer, the seat will use - normal focus semantics. For layers above the shell surface layers, the - seat will always give exclusive keyboard focus to the top-most layer - which has keyboard interactivity set to true. - - Layer surfaces receive pointer, touch, and tablet events normally. If - you do not want to receive them, set the input region on your surface - to an empty region. - - Events is double-buffered, see wl_surface.commit. - - - - - - - This assigns an xdg_popup's parent to this layer_surface. This popup - should have been created via xdg_surface::get_popup with the parent set - to NULL, and this request must be invoked before committing the popup's - initial state. - - See the documentation of xdg_popup for more details about what an - xdg_popup is and how it is used. - - - - - - - When a configure event is received, if a client commits the - surface in response to the configure event, then the client - must make an ack_configure request sometime before the commit - request, passing along the serial of the configure event. - - If the client receives multiple configure events before it - can respond to one, it only has to ack the last configure event. - - A client is not required to commit immediately after sending - an ack_configure request - it may even ack_configure several times - before its next surface commit. - - A client may send multiple ack_configure requests before committing, but - only the last request sent before a commit indicates which configure - event the client really is responding to. - - - - - - - This request destroys the layer surface. - - - - - - The configure event asks the client to resize its surface. - - Clients should arrange their surface for the new states, and then send - an ack_configure request with the serial sent in this configure event at - some point before committing the new surface. - - The client is free to dismiss all but the last configure event it - received. - - The width and height arguments specify the size of the window in - surface-local coordinates. - - The size is a hint, in the sense that the client is free to ignore it if - it doesn't resize, pick a smaller size (to satisfy aspect ratio or - resize in steps of NxM pixels). If the client picks a smaller size and - is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the - surface will be centered on this axis. - - If the width or height arguments are zero, it means the client should - decide its own window dimension. - - - - - - - - - The closed event is sent by the compositor when the surface will no - longer be shown. The output may have been destroyed or the user may - have asked for it to be removed. Further changes to the surface will be - ignored. The client should destroy the resource after receiving this - event, and create a new surface if they so choose. - - - - - - - - - - - - - - - - - - - - - Change the layer that the surface is rendered on. - - Layer is double-buffered, see wl_surface.commit. - - - - - diff --git a/resources/config b/resources/config.jsonc similarity index 85% rename from resources/config rename to resources/config.jsonc index 4452249c..7103034d 100644 --- a/resources/config +++ b/resources/config.jsonc @@ -1,3 +1,4 @@ +// -*- mode: jsonc -*- { // "layer": "top", // Waybar at top layer // "position": "bottom", // Waybar position (top|bottom|left|right) @@ -5,9 +6,32 @@ // "width": 1280, // Waybar width "spacing": 4, // Gaps between modules (4px) // Choose the order of the modules - "modules-left": ["sway/workspaces", "sway/mode", "sway/scratchpad", "custom/media"], - "modules-center": ["sway/window"], - "modules-right": ["mpd", "idle_inhibitor", "pulseaudio", "network", "cpu", "memory", "temperature", "backlight", "keyboard-state", "sway/language", "battery", "battery#bat2", "clock", "tray"], + "modules-left": [ + "sway/workspaces", + "sway/mode", + "sway/scratchpad", + "custom/media" + ], + "modules-center": [ + "sway/window" + ], + "modules-right": [ + "mpd", + "idle_inhibitor", + "pulseaudio", + "network", + "power-profiles-daemon", + "cpu", + "memory", + "temperature", + "backlight", + "keyboard-state", + "sway/language", + "battery", + "battery#bat2", + "clock", + "tray" + ], // Modules configuration // "sway/workspaces": { // "disable-scroll": true, @@ -49,7 +73,7 @@ "format-disconnected": "Disconnected ", "format-stopped": "{consumeIcon}{randomIcon}{repeatIcon}{singleIcon}Stopped ", "unknown-tag": "N/A", - "interval": 2, + "interval": 5, "consume-icons": { "on": " " }, @@ -113,6 +137,7 @@ "critical": 15 }, "format": "{capacity}% {icon}", + "format-full": "{capacity}% {icon}", "format-charging": "{capacity}% ", "format-plugged": "{capacity}% ", "format-alt": "{time} {icon}", @@ -123,6 +148,17 @@ "battery#bat2": { "bat": "BAT2" }, + "power-profiles-daemon": { + "format": "{icon}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "", + "performance": "", + "balanced": "", + "power-saver": "" + } + }, "network": { // "interface": "wlp2*", // (Optional) To force the use of this interface "format-wifi": "{essid} ({signalStrength}%) ", @@ -167,4 +203,3 @@ // "exec": "$HOME/.config/waybar/mediaplayer.py --player spotify 2> /dev/null" // Filter player based on name } } - diff --git a/resources/custom_modules/cffi_example/.gitignore b/resources/custom_modules/cffi_example/.gitignore new file mode 100644 index 00000000..988107fe --- /dev/null +++ b/resources/custom_modules/cffi_example/.gitignore @@ -0,0 +1 @@ +.cache/ \ No newline at end of file diff --git a/resources/custom_modules/cffi_example/README.md b/resources/custom_modules/cffi_example/README.md new file mode 100644 index 00000000..88396c19 --- /dev/null +++ b/resources/custom_modules/cffi_example/README.md @@ -0,0 +1,38 @@ +# C FFI module + +A C FFI module is a dynamic library that exposes standard C functions and +constants, that Waybar can load and execute to create custom advanced widgets. + +Most language can implement the required functions and constants (C, C++, Rust, +Go, Python, ...), meaning you can develop custom modules using your language of +choice, as long as there's GTK bindings. + +Symbols to implement are documented in the +[waybar_cffi_module.h](waybar_cffi_module.h) file. + +# Usage + +## Building this module + +```bash +meson setup build +meson compile -C build +``` + +## Load the module + +Edit your waybar config: +```json +{ + // ... + "modules-center": [ + // ... + "cffi/c_example" + ], + // ... + "cffi/c_example": { + // Path to the compiled dynamic library file + "module_path": "resources/custom_modules/cffi_example/build/wb_cffi_example.so" + } +} +``` diff --git a/resources/custom_modules/cffi_example/main.c b/resources/custom_modules/cffi_example/main.c new file mode 100644 index 00000000..ba2c8cf4 --- /dev/null +++ b/resources/custom_modules/cffi_example/main.c @@ -0,0 +1,70 @@ + +#include "waybar_cffi_module.h" + +typedef struct { + wbcffi_module* waybar_module; + GtkBox* container; + GtkButton* button; +} ExampleMod; + +// This static variable is shared between all instances of this module +static int instance_count = 0; + +void onclicked(GtkButton* button) { + char text[256]; + snprintf(text, 256, "Dice throw result: %d", rand() % 6 + 1); + gtk_button_set_label(button, text); +} + +// You must +const size_t wbcffi_version = 1; + +void* wbcffi_init(const wbcffi_init_info* init_info, const wbcffi_config_entry* config_entries, + size_t config_entries_len) { + printf("cffi_example: init config:\n"); + for (size_t i = 0; i < config_entries_len; i++) { + printf(" %s = %s\n", config_entries[i].key, config_entries[i].value); + } + + // Allocate the instance object + ExampleMod* inst = malloc(sizeof(ExampleMod)); + inst->waybar_module = init_info->obj; + + GtkContainer* root = init_info->get_root_widget(init_info->obj); + + // Add a container for displaying the next widgets + inst->container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5)); + gtk_container_add(GTK_CONTAINER(root), GTK_WIDGET(inst->container)); + + // Add a label + GtkLabel* label = GTK_LABEL(gtk_label_new("[Example C FFI Module:")); + gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(label)); + + // Add a button + inst->button = GTK_BUTTON(gtk_button_new_with_label("click me !")); + g_signal_connect(inst->button, "clicked", G_CALLBACK(onclicked), NULL); + gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(inst->button)); + + // Add a label + label = GTK_LABEL(gtk_label_new("]")); + gtk_container_add(GTK_CONTAINER(inst->container), GTK_WIDGET(label)); + + // Return instance object + printf("cffi_example inst=%p: init success ! (%d total instances)\n", inst, ++instance_count); + return inst; +} + +void wbcffi_deinit(void* instance) { + printf("cffi_example inst=%p: free memory\n", instance); + free(instance); +} + +void wbcffi_update(void* instance) { printf("cffi_example inst=%p: Update request\n", instance); } + +void wbcffi_refresh(void* instance, int signal) { + printf("cffi_example inst=%p: Received refresh signal %d\n", instance, signal); +} + +void wbcffi_doaction(void* instance, const char* name) { + printf("cffi_example inst=%p: doAction(%s)\n", instance, name); +} \ No newline at end of file diff --git a/resources/custom_modules/cffi_example/meson.build b/resources/custom_modules/cffi_example/meson.build new file mode 100644 index 00000000..dcde1048 --- /dev/null +++ b/resources/custom_modules/cffi_example/meson.build @@ -0,0 +1,13 @@ +project( + 'waybar_cffi_example', 'c', + version: '0.1.0', + license: 'MIT', +) + +shared_library('wb_cffi_example', + ['main.c'], + dependencies: [ + dependency('gtk+-3.0', version : ['>=3.22.0']) + ], + name_prefix: '' +) diff --git a/resources/custom_modules/cffi_example/waybar_cffi_module.h b/resources/custom_modules/cffi_example/waybar_cffi_module.h new file mode 100644 index 00000000..a7886bea --- /dev/null +++ b/resources/custom_modules/cffi_example/waybar_cffi_module.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Waybar ABI version. 1 is the latest version +extern const size_t wbcffi_version; + +/// Private Waybar CFFI module +typedef struct wbcffi_module wbcffi_module; + +/// Waybar module information +typedef struct { + /// Waybar CFFI object pointer + wbcffi_module* obj; + + /// Waybar version string + const char* waybar_version; + + /// Returns the waybar widget allocated for this module + /// @param obj Waybar CFFI object pointer + GtkContainer* (*get_root_widget)(wbcffi_module* obj); + + /// Queues a request for calling wbcffi_update() on the next GTK main event + /// loop iteration + /// @param obj Waybar CFFI object pointer + void (*queue_update)(wbcffi_module*); +} wbcffi_init_info; + +/// Config key-value pair +typedef struct { + /// Entry key + const char* key; + /// Entry value as string. JSON object and arrays are serialized. + const char* value; +} wbcffi_config_entry; + +/// Module init/new function, called on module instantiation +/// +/// MANDATORY CFFI function +/// +/// @param init_info Waybar module information +/// @param config_entries Flat representation of the module JSON config. The data only available +/// during wbcffi_init call. +/// @param config_entries_len Number of entries in `config_entries` +/// +/// @return A untyped pointer to module data, NULL if the module failed to load. +void* wbcffi_init(const wbcffi_init_info* init_info, const wbcffi_config_entry* config_entries, + size_t config_entries_len); + +/// Module deinit/delete function, called when Waybar is closed or when the module is removed +/// +/// MANDATORY CFFI function +/// +/// @param instance Module instance data (as returned by `wbcffi_init`) +void wbcffi_deinit(void* instance); + +/// Called from the GTK main event loop, to update the UI +/// +/// Optional CFFI function +/// +/// @param instance Module instance data (as returned by `wbcffi_init`) +/// @param action_name Action name +void wbcffi_update(void* instance); + +/// Called when Waybar receives a POSIX signal and forwards it to each module +/// +/// Optional CFFI function +/// +/// @param instance Module instance data (as returned by `wbcffi_init`) +/// @param signal Signal ID +void wbcffi_refresh(void* instance, int signal); + +/// Called on module action (see +/// https://github.com/Alexays/Waybar/wiki/Configuration#module-actions-config) +/// +/// Optional CFFI function +/// +/// @param instance Module instance data (as returned by `wbcffi_init`) +/// @param action_name Action name +void wbcffi_doaction(void* instance, const char* action_name); + +#ifdef __cplusplus +} +#endif diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 51a48373..4aea4171 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -23,7 +23,7 @@ def signal_handler(sig, frame): class PlayerManager: - def __init__(self, selected_player=None): + def __init__(self, selected_player=None, excluded_player=[]): self.manager = Playerctl.PlayerManager() self.loop = GLib.MainLoop() self.manager.connect( @@ -35,11 +35,14 @@ class PlayerManager: signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGPIPE, signal.SIG_DFL) self.selected_player = selected_player + self.excluded_player = excluded_player.split(',') if excluded_player else [] self.init_players() def init_players(self): for player in self.manager.props.player_names: + if player.name in self.excluded_player: + continue if self.selected_player is not None and self.selected_player != player.name: logger.debug(f"{player.name} is not the filtered player, skipping it") continue @@ -149,6 +152,8 @@ def parse_arguments(): # Increase verbosity with every occurrence of -v parser.add_argument("-v", "--verbose", action="count", default=0) + parser.add_argument("-x", "--exclude", "- Comma-separated list of excluded player") + # Define for which player we"re listening parser.add_argument("--player") @@ -174,7 +179,10 @@ def main(): logger.info("Creating player manager") if arguments.player: logger.info(f"Filtering for player: {arguments.player}") - player = PlayerManager(arguments.player) + if arguments.exclude: + logger.info(f"Exclude player {arguments.exclude}") + + player = PlayerManager(arguments.player, arguments.exclude) player.run() diff --git a/resources/icons/meson.build b/resources/icons/meson.build new file mode 100644 index 00000000..05532d3d --- /dev/null +++ b/resources/icons/meson.build @@ -0,0 +1,6 @@ +gnome = import('gnome') + +app_resources += gnome.compile_resources('icon-resources', + 'waybar_icons.gresource.xml', + c_name: 'waybar_icons' +) diff --git a/resources/icons/waybar-privacy-audio-input-symbolic.svg b/resources/icons/waybar-privacy-audio-input-symbolic.svg new file mode 100644 index 00000000..61356891 --- /dev/null +++ b/resources/icons/waybar-privacy-audio-input-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/waybar-privacy-audio-output-symbolic.svg b/resources/icons/waybar-privacy-audio-output-symbolic.svg new file mode 100644 index 00000000..10ad4f9d --- /dev/null +++ b/resources/icons/waybar-privacy-audio-output-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/icons/waybar-privacy-screen-share-symbolic.svg b/resources/icons/waybar-privacy-screen-share-symbolic.svg new file mode 100644 index 00000000..9738c571 --- /dev/null +++ b/resources/icons/waybar-privacy-screen-share-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/icons/waybar_icons.gresource.xml b/resources/icons/waybar_icons.gresource.xml new file mode 100644 index 00000000..077049bf --- /dev/null +++ b/resources/icons/waybar_icons.gresource.xml @@ -0,0 +1,8 @@ + + + + waybar-privacy-audio-input-symbolic.svg + waybar-privacy-audio-output-symbolic.svg + waybar-privacy-screen-share-symbolic.svg + + diff --git a/resources/style.css b/resources/style.css index cf5c5fb0..b5859390 100644 --- a/resources/style.css +++ b/resources/style.css @@ -69,7 +69,7 @@ button:hover { #mode { background-color: #64727D; - border-bottom: 3px solid #ffffff; + box-shadow: inset 0 -3px #ffffff; } #clock, @@ -87,6 +87,7 @@ button:hover { #mode, #idle_inhibitor, #scratchpad, +#power-profiles-daemon, #mpd { padding: 0 10px; color: #ffffff; @@ -128,16 +129,36 @@ button:hover { } } +/* Using steps() instead of linear as a timing function to limit cpu usage */ #battery.critical:not(.charging) { background-color: #f53c3c; color: #ffffff; animation-name: blink; animation-duration: 0.5s; - animation-timing-function: linear; + animation-timing-function: steps(12); animation-iteration-count: infinite; animation-direction: alternate; } +#power-profiles-daemon { + padding-right: 15px; +} + +#power-profiles-daemon.performance { + background-color: #f53c3c; + color: #ffffff; +} + +#power-profiles-daemon.balanced { + background-color: #2980b9; + color: #ffffff; +} + +#power-profiles-daemon.power-saver { + background-color: #2ecc71; + color: #000000; +} + label:focus { background-color: #000000; } @@ -278,3 +299,24 @@ label:focus { #scratchpad.empty { background-color: transparent; } + +#privacy { + padding: 0; +} + +#privacy-item { + padding: 0 5px; + color: white; +} + +#privacy-item.screenshare { + background-color: #cf5700; +} + +#privacy-item.audio-in { + background-color: #1ca000; +} + +#privacy-item.audio-out { + background-color: #0069d4; +} diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index a238143b..e64e6daa 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -24,18 +24,61 @@ AAppIconLabel::AAppIconLabel(const Json::Value& config, const std::string& name, image_.set_pixel_size(app_icon_size_); } +std::string toLowerCase(const std::string& input) { + std::string result = input; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return std::tolower(c); }); + return result; +} + +std::optional getFileBySuffix(const std::string& dir, const std::string& suffix, + bool check_lower_case) { + if (!std::filesystem::exists(dir)) { + return {}; + } + for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + if (filename.size() < suffix.size()) { + continue; + } + if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) || + (check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(), + toLowerCase(suffix)) == 0)) { + return entry.path().string(); + } + } + } + + return {}; +} + +std::optional getFileBySuffix(const std::string& dir, const std::string& suffix) { + return getFileBySuffix(dir, suffix, false); +} + std::optional getDesktopFilePath(const std::string& app_identifier, const std::string& alternative_app_identifier) { + if (app_identifier.empty()) { + return {}; + } + const auto data_dirs = Glib::get_system_data_dirs(); for (const auto& data_dir : data_dirs) { - const auto data_app_dir = data_dir + "applications/"; - auto desktop_file_path = data_app_dir + app_identifier + ".desktop"; - if (std::filesystem::exists(desktop_file_path)) { + const auto data_app_dir = data_dir + "/applications/"; + auto desktop_file_suffix = app_identifier + ".desktop"; + // searching for file by suffix catches cases like terminal emulator "foot" where class is + // "footclient" and desktop file is named "org.codeberg.dnkl.footclient.desktop" + auto desktop_file_path = getFileBySuffix(data_app_dir, desktop_file_suffix, true); + // "true" argument allows checking for lowercase - this catches cases where class name is + // "LibreWolf" and desktop file is named "librewolf.desktop" + if (desktop_file_path.has_value()) { return desktop_file_path; } if (!alternative_app_identifier.empty()) { - desktop_file_path = data_app_dir + alternative_app_identifier + ".desktop"; - if (std::filesystem::exists(desktop_file_path)) { + desktop_file_suffix = alternative_app_identifier + ".desktop"; + desktop_file_path = getFileBySuffix(data_app_dir, desktop_file_suffix, true); + if (desktop_file_path.has_value()) { return desktop_file_path; } } @@ -58,16 +101,9 @@ std::optional getIconName(const std::string& app_identifier, return app_identifier_desktop; } - const auto to_lower = [](const std::string& str) { - auto str_cpy = str; - std::transform(str_cpy.begin(), str_cpy.end(), str_cpy.begin(), - [](unsigned char c) { return std::tolower(c); }); - return str; - }; - const auto first_space = app_identifier.find_first_of(' '); if (first_space != std::string::npos) { - const auto first_word = to_lower(app_identifier.substr(0, first_space)); + const auto first_word = toLowerCase(app_identifier.substr(0, first_space)); if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { return first_word; } @@ -75,7 +111,7 @@ std::optional getIconName(const std::string& app_identifier, const auto first_dash = app_identifier.find_first_of('-'); if (first_dash != std::string::npos) { - const auto first_word = to_lower(app_identifier.substr(0, first_dash)); + const auto first_word = toLowerCase(app_identifier.substr(0, first_dash)); if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { return first_word; } diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index a7e2380a..d7ee666e 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -9,10 +9,23 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const bool enable_click, bool enable_scroll) : ALabel(config, name, id, format, interval, ellipsize, enable_click, enable_scroll) { event_box_.remove(); + label_.unset_name(); + label_.get_style_context()->remove_class(MODULE_CLASS); + box_.get_style_context()->add_class(MODULE_CLASS); + if (!id.empty()) { + label_.get_style_context()->remove_class(id); + box_.get_style_context()->add_class(id); + } + box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); - box_.set_spacing(8); + box_.set_name(name); + + int spacing = config_["icon-spacing"].isInt() ? config_["icon-spacing"].asInt() : 8; + box_.set_spacing(spacing); + box_.add(image_); box_.add(label_); + event_box_.add(box_); } diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 4d8b2218..b8d39df5 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -12,7 +12,7 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st : AModule(config, name, id, config["format-alt"].isString() || enable_click, enable_scroll), format_(config_["format"].isString() ? config_["format"].asString() : format), interval_(config_["interval"] == "once" - ? std::chrono::seconds(100000000) + ? std::chrono::seconds::max() : std::chrono::seconds( config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)), default_format_(format_) { @@ -20,6 +20,7 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st if (!id.empty()) { label_.get_style_context()->add_class(id); } + label_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(label_); if (config_["max-length"].isUInt()) { label_.set_max_width_chars(config_["max-length"].asInt()); @@ -49,6 +50,17 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st label_.set_xalign(align); } } + + if (config_["justify"].isString()) { + auto justify_str = config_["justify"].asString(); + if (justify_str == "left") { + label_.set_justify(Gtk::Justification::JUSTIFY_LEFT); + } else if (justify_str == "right") { + label_.set_justify(Gtk::Justification::JUSTIFY_RIGHT); + } else if (justify_str == "center") { + label_.set_justify(Gtk::Justification::JUSTIFY_CENTER); + } + } } auto ALabel::update() -> void { AModule::update(); } diff --git a/src/AModule.cpp b/src/AModule.cpp index 2626cd89..9a9f1386 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -10,6 +10,7 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: bool enable_click, bool enable_scroll) : name_(std::move(name)), config_(std::move(config)), + isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, distance_scrolled_y_(0.0), distance_scrolled_x_(0.0) { // Configure module action Map @@ -27,20 +28,28 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } // configure events' user commands - if (enable_click) { + // hasUserEvent is true if any element from eventMap_ is satisfying the condition in the lambda + bool hasUserEvent = + std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) { + // True if there is any non-release type event + return eventEntry.first.second != GdkEventType::GDK_BUTTON_RELEASE && + config[eventEntry.second].isString(); + }) != eventMap_.cend(); + + if (enable_click || hasUserEvent) { event_box_.add_events(Gdk::BUTTON_PRESS_MASK); event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &AModule::handleToggle)); - } else { - std::map, std::string>::const_iterator it{eventMap_.cbegin()}; - while (it != eventMap_.cend()) { - if (config_[it->second].isString()) { - event_box_.add_events(Gdk::BUTTON_PRESS_MASK); - event_box_.signal_button_press_event().connect( - sigc::mem_fun(*this, &AModule::handleToggle)); - break; - } - ++it; - } + } + + bool hasReleaseEvent = + std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) { + // True if there is any non-release type event + return eventEntry.first.second == GdkEventType::GDK_BUTTON_RELEASE && + config[eventEntry.second].isString(); + }) != eventMap_.cend(); + if (hasReleaseEvent) { + event_box_.add_events(Gdk::BUTTON_RELEASE_MASK); + event_box_.signal_button_release_event().connect(sigc::mem_fun(*this, &AModule::handleRelease)); } if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() || enable_scroll) { event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); @@ -72,7 +81,11 @@ auto AModule::doAction(const std::string& name) -> void { } } -bool AModule::handleToggle(GdkEventButton* const& e) { +bool AModule::handleToggle(GdkEventButton* const& e) { return handleUserEvent(e); } + +bool AModule::handleRelease(GdkEventButton* const& e) { return handleUserEvent(e); } + +bool AModule::handleUserEvent(GdkEventButton* const& e) { std::string format{}; const std::map, std::string>::const_iterator& rec{ eventMap_.find(std::pair(e->button, e->type))}; @@ -177,9 +190,7 @@ bool AModule::handleScroll(GdkEventScroll* e) { return true; } -bool AModule::tooltipEnabled() { - return config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true; -} +bool AModule::tooltipEnabled() { return isTooltip; } AModule::operator Gtk::Widget&() { return event_box_; } diff --git a/src/ASlider.cpp b/src/ASlider.cpp new file mode 100644 index 00000000..b434be30 --- /dev/null +++ b/src/ASlider.cpp @@ -0,0 +1,35 @@ +#include "ASlider.hpp" + +#include "gtkmm/adjustment.h" +#include "gtkmm/enums.h" + +namespace waybar { + +ASlider::ASlider(const Json::Value& config, const std::string& name, const std::string& id) + : AModule(config, name, id, false, false), + vertical_(config_["orientation"].asString() == "vertical"), + scale_(vertical_ ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL) { + scale_.set_name(name); + if (!id.empty()) { + scale_.get_style_context()->add_class(id); + } + scale_.get_style_context()->add_class(MODULE_CLASS); + event_box_.add(scale_); + scale_.signal_value_changed().connect(sigc::mem_fun(*this, &ASlider::onValueChanged)); + + if (config_["min"].isUInt()) { + min_ = config_["min"].asUInt(); + } + + if (config_["max"].isUInt()) { + max_ = config_["max"].asUInt(); + } + + scale_.set_inverted(vertical_); + scale_.set_draw_value(false); + scale_.set_adjustment(Gtk::Adjustment::create(curr_, min_, max_ + 1, 1, 1, 1)); +} + +void ASlider::onValueChanged() {} + +} // namespace waybar \ No newline at end of file diff --git a/src/bar.cpp b/src/bar.cpp index 60104f0d..31afcd43 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -1,16 +1,13 @@ -#ifdef HAVE_GTK_LAYER_SHELL -#include -#endif +#include "bar.hpp" +#include #include #include -#include "bar.hpp" #include "client.hpp" #include "factory.hpp" #include "group.hpp" -#include "wlr-layer-shell-unstable-v1-client-protocol.h" #ifdef HAVE_SWAY #include "modules/sway/bar.hpp" @@ -25,9 +22,6 @@ static constexpr const char* MIN_WIDTH_MSG = static constexpr const char* BAR_SIZE_MSG = "Bar configured (width: {}, height: {}) for output: {}"; -static constexpr const char* SIZE_DEFINED = - "{} size is defined in the config file so it will stay like that"; - const Bar::bar_mode_map Bar::PRESET_MODES = { // {"default", {// Special mode to hold the global bar configuration @@ -60,8 +54,8 @@ const Bar::bar_mode_map Bar::PRESET_MODES = { // .passthrough = true, .visible = true}}}; -const std::string_view Bar::MODE_DEFAULT = "default"; -const std::string_view Bar::MODE_INVISIBLE = "invisible"; +const std::string Bar::MODE_DEFAULT = "default"; +const std::string Bar::MODE_INVISIBLE = "invisible"; const std::string_view DEFAULT_BAR_ID = "bar-0"; /* Deserializer for enum bar_layer */ @@ -93,11 +87,38 @@ void from_json(const Json::Value& j, bar_mode& m) { } } +/* Deserializer for enum Gtk::PositionType */ +void from_json(const Json::Value& j, Gtk::PositionType& pos) { + if (j == "left") { + pos = Gtk::POS_LEFT; + } else if (j == "right") { + pos = Gtk::POS_RIGHT; + } else if (j == "top") { + pos = Gtk::POS_TOP; + } else if (j == "bottom") { + pos = Gtk::POS_BOTTOM; + } +} + +Glib::ustring to_string(Gtk::PositionType pos) { + switch (pos) { + case Gtk::POS_LEFT: + return "left"; + case Gtk::POS_RIGHT: + return "right"; + case Gtk::POS_TOP: + return "top"; + case Gtk::POS_BOTTOM: + return "bottom"; + } + throw std::runtime_error("Invalid Gtk::PositionType"); +} + /* Deserializer for JSON Object -> map * Assumes that all the values in the object are deserializable to the same type. */ template ::value>> + typename = std::enable_if_t::value>> void from_json(const Json::Value& j, std::map& m) { if (j.isObject()) { for (auto it = j.begin(); it != j.end(); ++it) { @@ -106,381 +127,15 @@ void from_json(const Json::Value& j, std::map& m) { } } -#ifdef HAVE_GTK_LAYER_SHELL -struct GLSSurfaceImpl : public BarSurface, public sigc::trackable { - GLSSurfaceImpl(Gtk::Window& window, struct waybar_output& output) : window_{window} { - output_name_ = output.name; - // this has to be executed before GtkWindow.realize - gtk_layer_init_for_window(window_.gobj()); - gtk_layer_set_keyboard_interactivity(window.gobj(), FALSE); - gtk_layer_set_monitor(window_.gobj(), output.monitor->gobj()); - gtk_layer_set_namespace(window_.gobj(), "waybar"); - - window.signal_map_event().connect_notify(sigc::mem_fun(*this, &GLSSurfaceImpl::onMap)); - window.signal_configure_event().connect_notify( - sigc::mem_fun(*this, &GLSSurfaceImpl::onConfigure)); - } - - void setExclusiveZone(bool enable) override { - if (enable) { - gtk_layer_auto_exclusive_zone_enable(window_.gobj()); - } else { - gtk_layer_set_exclusive_zone(window_.gobj(), 0); - } - } - - void setMargins(const struct bar_margins& margins) override { - gtk_layer_set_margin(window_.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, margins.left); - gtk_layer_set_margin(window_.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, margins.right); - gtk_layer_set_margin(window_.gobj(), GTK_LAYER_SHELL_EDGE_TOP, margins.top); - gtk_layer_set_margin(window_.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, margins.bottom); - } - - void setLayer(bar_layer value) override { - auto layer = GTK_LAYER_SHELL_LAYER_BOTTOM; - if (value == bar_layer::TOP) { - layer = GTK_LAYER_SHELL_LAYER_TOP; - } else if (value == bar_layer::OVERLAY) { - layer = GTK_LAYER_SHELL_LAYER_OVERLAY; - } - gtk_layer_set_layer(window_.gobj(), layer); - } - - void setPassThrough(bool enable) override { - passthrough_ = enable; - auto gdk_window = window_.get_window(); - if (gdk_window) { - Cairo::RefPtr region; - if (enable) { - region = Cairo::Region::create(); - } - gdk_window->input_shape_combine_region(region, 0, 0); - } - } - - void setPosition(const std::string_view& position) override { - auto unanchored = GTK_LAYER_SHELL_EDGE_BOTTOM; - vertical_ = false; - if (position == "bottom") { - unanchored = GTK_LAYER_SHELL_EDGE_TOP; - } else if (position == "left") { - unanchored = GTK_LAYER_SHELL_EDGE_RIGHT; - vertical_ = true; - } else if (position == "right") { - vertical_ = true; - unanchored = GTK_LAYER_SHELL_EDGE_LEFT; - } - for (auto edge : {GTK_LAYER_SHELL_EDGE_LEFT, GTK_LAYER_SHELL_EDGE_RIGHT, - GTK_LAYER_SHELL_EDGE_TOP, GTK_LAYER_SHELL_EDGE_BOTTOM}) { - gtk_layer_set_anchor(window_.gobj(), edge, unanchored != edge); - } - - // Disable anchoring for other edges too if the width - // or the height has been set to a value other than 'auto' - // otherwise the bar will use all space - if (vertical_ && height_ > 1) { - gtk_layer_set_anchor(window_.gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, false); - gtk_layer_set_anchor(window_.gobj(), GTK_LAYER_SHELL_EDGE_TOP, false); - } else if (!vertical_ && width_ > 1) { - gtk_layer_set_anchor(window_.gobj(), GTK_LAYER_SHELL_EDGE_LEFT, false); - gtk_layer_set_anchor(window_.gobj(), GTK_LAYER_SHELL_EDGE_RIGHT, false); - } - } - - void setSize(uint32_t width, uint32_t height) override { - width_ = width; - height_ = height; - window_.set_size_request(width_, height_); - }; - - private: - Gtk::Window& window_; - std::string output_name_; - uint32_t width_; - uint32_t height_; - bool passthrough_ = false; - bool vertical_ = false; - - void onMap(GdkEventAny* ev) { setPassThrough(passthrough_); } - - void onConfigure(GdkEventConfigure* ev) { - /* - * GTK wants new size for the window. - * Actual resizing and management of the exclusve zone is handled within the gtk-layer-shell - * code. This event handler only updates stored size of the window and prints some warnings. - * - * Note: forced resizing to a window smaller than required by GTK would not work with - * gtk-layer-shell. - */ - if (vertical_) { - if (width_ > 1 && ev->width > static_cast(width_)) { - spdlog::warn(MIN_WIDTH_MSG, width_, ev->width); - } - } else { - if (height_ > 1 && ev->height > static_cast(height_)) { - spdlog::warn(MIN_HEIGHT_MSG, height_, ev->height); - } - } - width_ = ev->width; - height_ = ev->height; - spdlog::info(BAR_SIZE_MSG, width_, height_, output_name_); - } -}; -#endif - -struct RawSurfaceImpl : public BarSurface, public sigc::trackable { - RawSurfaceImpl(Gtk::Window& window, struct waybar_output& output) : window_{window} { - output_ = gdk_wayland_monitor_get_wl_output(output.monitor->gobj()); - output_name_ = output.name; - - window.signal_realize().connect_notify(sigc::mem_fun(*this, &RawSurfaceImpl::onRealize)); - window.signal_map_event().connect_notify(sigc::mem_fun(*this, &RawSurfaceImpl::onMap)); - window.signal_configure_event().connect_notify( - sigc::mem_fun(*this, &RawSurfaceImpl::onConfigure)); - - if (window.get_realized()) { - onRealize(); - } - } - - void setExclusiveZone(bool enable) override { - exclusive_zone_ = enable; - if (layer_surface_) { - auto zone = 0; - if (enable) { - // exclusive zone already includes margin for anchored edge, - // only opposite margin should be added - if ((anchor_ & VERTICAL_ANCHOR) == VERTICAL_ANCHOR) { - zone += width_; - zone += (anchor_ & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT) ? margins_.right : margins_.left; - } else { - zone += height_; - zone += (anchor_ & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP) ? margins_.bottom : margins_.top; - } - } - spdlog::debug("Set exclusive zone {} for output {}", zone, output_name_); - zwlr_layer_surface_v1_set_exclusive_zone(layer_surface_.get(), zone); - } - } - - void setLayer(bar_layer layer) override { - layer_ = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; - if (layer == bar_layer::TOP) { - layer_ = ZWLR_LAYER_SHELL_V1_LAYER_TOP; - } else if (layer == bar_layer::OVERLAY) { - layer_ = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; - } - // updating already mapped window - if (layer_surface_) { - if (zwlr_layer_surface_v1_get_version(layer_surface_.get()) >= - ZWLR_LAYER_SURFACE_V1_SET_LAYER_SINCE_VERSION) { - zwlr_layer_surface_v1_set_layer(layer_surface_.get(), layer_); - } else { - spdlog::warn("Unable to change layer: layer-shell implementation is too old"); - } - } - } - - void setMargins(const struct bar_margins& margins) override { - margins_ = margins; - // updating already mapped window - if (layer_surface_) { - zwlr_layer_surface_v1_set_margin(layer_surface_.get(), margins_.top, margins_.right, - margins_.bottom, margins_.left); - } - } - - void setPassThrough(bool enable) override { - passthrough_ = enable; - /* GTK overwrites any region changes applied directly to the wl_surface, - * thus the same GTK region API as in the GLS impl has to be used. */ - auto gdk_window = window_.get_window(); - if (gdk_window) { - Cairo::RefPtr region; - if (enable) { - region = Cairo::Region::create(); - } - gdk_window->input_shape_combine_region(region, 0, 0); - } - } - - void setPosition(const std::string_view& position) override { - anchor_ = HORIZONTAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; - if (position == "bottom") { - anchor_ = HORIZONTAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; - } else if (position == "left") { - anchor_ = VERTICAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; - } else if (position == "right") { - anchor_ = VERTICAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; - } - - // updating already mapped window - if (layer_surface_) { - zwlr_layer_surface_v1_set_anchor(layer_surface_.get(), anchor_); - } - } - - void setSize(uint32_t width, uint32_t height) override { - configured_width_ = width_ = width; - configured_height_ = height_ = height; - // layer_shell.configure handler should update exclusive zone if size changes - window_.set_size_request(width, height); - }; - - void commit() override { - if (surface_) { - wl_surface_commit(surface_); - } - } - - private: - constexpr static uint8_t VERTICAL_ANCHOR = - ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; - constexpr static uint8_t HORIZONTAL_ANCHOR = - ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; - - template - using deleter_fn = std::integral_constant; - using layer_surface_ptr = - std::unique_ptr>; - - Gtk::Window& window_; - std::string output_name_; - uint32_t configured_width_ = 0; - uint32_t configured_height_ = 0; - uint32_t width_ = 0; - uint32_t height_ = 0; - uint8_t anchor_ = HORIZONTAL_ANCHOR | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; - bool exclusive_zone_ = true; - bool passthrough_ = false; - struct bar_margins margins_; - - zwlr_layer_shell_v1_layer layer_ = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; - struct wl_output* output_ = nullptr; // owned by GTK - struct wl_surface* surface_ = nullptr; // owned by GTK - layer_surface_ptr layer_surface_; - - void onRealize() { - auto gdk_window = window_.get_window()->gobj(); - gdk_wayland_window_set_use_custom_surface(gdk_window); - } - - void onMap(GdkEventAny* ev) { - static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { - .configure = onSurfaceConfigure, - .closed = onSurfaceClosed, - }; - auto client = Client::inst(); - auto gdk_window = window_.get_window()->gobj(); - surface_ = gdk_wayland_window_get_wl_surface(gdk_window); - - layer_surface_.reset(zwlr_layer_shell_v1_get_layer_surface(client->layer_shell, surface_, - output_, layer_, "waybar")); - - zwlr_layer_surface_v1_add_listener(layer_surface_.get(), &layer_surface_listener, this); - zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface_.get(), false); - zwlr_layer_surface_v1_set_anchor(layer_surface_.get(), anchor_); - zwlr_layer_surface_v1_set_margin(layer_surface_.get(), margins_.top, margins_.right, - margins_.bottom, margins_.left); - - setSurfaceSize(width_, height_); - setExclusiveZone(exclusive_zone_); - setPassThrough(passthrough_); - - commit(); - wl_display_roundtrip(client->wl_display); - } - - void onConfigure(GdkEventConfigure* ev) { - /* - * GTK wants new size for the window. - * - * Prefer configured size if it's non-default. - * If the size is not set and the window is smaller than requested by GTK, request resize from - * layer surface. - */ - auto tmp_height = height_; - auto tmp_width = width_; - if (ev->height > static_cast(height_)) { - // Default minimal value - if (height_ > 1) { - spdlog::warn(MIN_HEIGHT_MSG, height_, ev->height); - } - if (configured_height_ > 1) { - spdlog::info(SIZE_DEFINED, "Height"); - } else { - tmp_height = ev->height; - } - } - if (ev->width > static_cast(width_)) { - // Default minimal value - if (width_ > 1) { - spdlog::warn(MIN_WIDTH_MSG, width_, ev->width); - } - if (configured_width_ > 1) { - spdlog::info(SIZE_DEFINED, "Width"); - } else { - tmp_width = ev->width; - } - } - if (tmp_width != width_ || tmp_height != height_) { - setSurfaceSize(tmp_width, tmp_height); - commit(); - } - } - - void setSurfaceSize(uint32_t width, uint32_t height) { - /* If the client is anchored to two opposite edges, layer_surface.configure will return - * size without margins for the axis. - * layer_surface.set_size, however, expects size with margins for the anchored axis. - * This is not specified by wlr-layer-shell and based on actual behavior of sway. - * - * If the size for unanchored axis is not set (0), change request to 1 to avoid automatic - * assignment by the compositor. - */ - if ((anchor_ & VERTICAL_ANCHOR) == VERTICAL_ANCHOR) { - width = width > 0 ? width : 1; - if (height > 1) { - height += margins_.top + margins_.bottom; - } - } else { - height = height > 0 ? height : 1; - if (width > 1) { - width += margins_.right + margins_.left; - } - } - spdlog::debug("Set surface size {}x{} for output {}", width, height, output_name_); - zwlr_layer_surface_v1_set_size(layer_surface_.get(), width, height); - } - - static void onSurfaceConfigure(void* data, struct zwlr_layer_surface_v1* surface, uint32_t serial, - uint32_t width, uint32_t height) { - auto o = static_cast(data); - if (width != o->width_ || height != o->height_) { - o->width_ = width; - o->height_ = height; - o->window_.set_size_request(o->width_, o->height_); - o->window_.resize(o->width_, o->height_); - o->setExclusiveZone(o->exclusive_zone_); - spdlog::info(BAR_SIZE_MSG, o->width_ == 1 ? "auto" : std::to_string(o->width_), - o->height_ == 1 ? "auto" : std::to_string(o->height_), o->output_name_); - o->commit(); - } - zwlr_layer_surface_v1_ack_configure(surface, serial); - } - - static void onSurfaceClosed(void* data, struct zwlr_layer_surface_v1* /* surface */) { - auto o = static_cast(data); - o->layer_surface_.reset(); - } -}; - }; // namespace waybar waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) : output(w_output), config(w_config), window{Gtk::WindowType::WINDOW_TOPLEVEL}, + x_global(0), + y_global(0), + margins_{.top = 0, .right = 0, .bottom = 0, .left = 0}, left_(Gtk::ORIENTATION_HORIZONTAL, 0), center_(Gtk::ORIENTATION_HORIZONTAL, 0), right_(Gtk::ORIENTATION_HORIZONTAL, 0), @@ -490,17 +145,18 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) window.set_decorated(false); window.get_style_context()->add_class(output->name); window.get_style_context()->add_class(config["name"].asString()); - window.get_style_context()->add_class(config["position"].asString()); - auto position = config["position"].asString(); + from_json(config["position"], position); + orientation = (position == Gtk::POS_LEFT || position == Gtk::POS_RIGHT) + ? Gtk::ORIENTATION_VERTICAL + : Gtk::ORIENTATION_HORIZONTAL; - if (position == "right" || position == "left") { - left_ = Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0); - center_ = Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0); - right_ = Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0); - box_ = Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0); - vertical = true; - } + window.get_style_context()->add_class(to_string(position)); + + left_ = Gtk::Box(orientation, 0); + center_ = Gtk::Box(orientation, 0); + right_ = Gtk::Box(orientation, 0); + box_ = Gtk::Box(orientation, 0); left_.get_style_context()->add_class("modules-left"); center_.get_style_context()->add_class("modules-center"); @@ -513,10 +169,8 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) right_.set_spacing(spacing); } - uint32_t height = config["height"].isUInt() ? config["height"].asUInt() : 0; - uint32_t width = config["width"].isUInt() ? config["width"].asUInt() : 0; - - struct bar_margins margins_; + height_ = config["height"].isUInt() ? config["height"].asUInt() : 0; + width_ = config["width"].isUInt() ? config["width"].asUInt() : 0; if (config["margin-top"].isInt() || config["margin-right"].isInt() || config["margin-bottom"].isInt() || config["margin-left"].isInt()) { @@ -563,21 +217,27 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) margins_ = {.top = gaps, .right = gaps, .bottom = gaps, .left = gaps}; } -#ifdef HAVE_GTK_LAYER_SHELL - bool use_gls = config["gtk-layer-shell"].isBool() ? config["gtk-layer-shell"].asBool() : true; - if (use_gls) { - surface_impl_ = std::make_unique(window, *output); - } else -#endif - { - surface_impl_ = std::make_unique(window, *output); - } + window.signal_configure_event().connect_notify(sigc::mem_fun(*this, &Bar::onConfigure)); + output->monitor->property_geometry().signal_changed().connect( + sigc::mem_fun(*this, &Bar::onOutputGeometryChanged)); + + // this has to be executed before GtkWindow.realize + auto* gtk_window = window.gobj(); + gtk_layer_init_for_window(gtk_window); + gtk_layer_set_keyboard_mode(gtk_window, GTK_LAYER_SHELL_KEYBOARD_MODE_NONE); + gtk_layer_set_monitor(gtk_window, output->monitor->gobj()); + gtk_layer_set_namespace(gtk_window, "waybar"); + + gtk_layer_set_margin(gtk_window, GTK_LAYER_SHELL_EDGE_LEFT, margins_.left); + gtk_layer_set_margin(gtk_window, GTK_LAYER_SHELL_EDGE_RIGHT, margins_.right); + gtk_layer_set_margin(gtk_window, GTK_LAYER_SHELL_EDGE_TOP, margins_.top); + gtk_layer_set_margin(gtk_window, GTK_LAYER_SHELL_EDGE_BOTTOM, margins_.bottom); + + window.set_size_request(width_, height_); - surface_impl_->setMargins(margins_); - surface_impl_->setSize(width, height); // Position needs to be set after calculating the height due to the // GTK layer shell anchors logic relying on the dimensions of the bar. - surface_impl_->setPosition(position); + setPosition(position); /* Read custom modes if available */ if (auto modes = config.get("modes", {}); modes.isObject()) { @@ -633,7 +293,7 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) /* Need to define it here because of forward declared members */ waybar::Bar::~Bar() = default; -void waybar::Bar::setMode(const std::string_view& mode) { +void waybar::Bar::setMode(const std::string& mode) { using namespace std::literals::string_literals; auto style = window.get_style_context(); @@ -654,9 +314,23 @@ void waybar::Bar::setMode(const std::string_view& mode) { } void waybar::Bar::setMode(const struct bar_mode& mode) { - surface_impl_->setLayer(mode.layer); - surface_impl_->setExclusiveZone(mode.exclusive); - surface_impl_->setPassThrough(mode.passthrough); + auto* gtk_window = window.gobj(); + + auto layer = GTK_LAYER_SHELL_LAYER_BOTTOM; + if (mode.layer == bar_layer::TOP) { + layer = GTK_LAYER_SHELL_LAYER_TOP; + } else if (mode.layer == bar_layer::OVERLAY) { + layer = GTK_LAYER_SHELL_LAYER_OVERLAY; + } + gtk_layer_set_layer(gtk_window, layer); + + if (mode.exclusive) { + gtk_layer_auto_exclusive_zone_enable(gtk_window); + } else { + gtk_layer_set_exclusive_zone(gtk_window, 0); + } + + setPassThrough(passthrough_ = mode.passthrough); if (mode.visible) { window.get_style_context()->remove_class("hidden"); @@ -665,7 +339,58 @@ void waybar::Bar::setMode(const struct bar_mode& mode) { window.get_style_context()->add_class("hidden"); window.set_opacity(0); } - surface_impl_->commit(); +} + +void waybar::Bar::setPassThrough(bool passthrough) { + auto gdk_window = window.get_window(); + if (gdk_window) { + Cairo::RefPtr region; + if (passthrough) { + region = Cairo::Region::create(); + } + gdk_window->input_shape_combine_region(region, 0, 0); + } +} + +void waybar::Bar::setPosition(Gtk::PositionType position) { + std::array anchors; + anchors.fill(TRUE); + + auto orientation = (position == Gtk::POS_LEFT || position == Gtk::POS_RIGHT) + ? Gtk::ORIENTATION_VERTICAL + : Gtk::ORIENTATION_HORIZONTAL; + + switch (position) { + case Gtk::POS_LEFT: + anchors[GTK_LAYER_SHELL_EDGE_RIGHT] = FALSE; + break; + case Gtk::POS_RIGHT: + anchors[GTK_LAYER_SHELL_EDGE_LEFT] = FALSE; + break; + case Gtk::POS_BOTTOM: + anchors[GTK_LAYER_SHELL_EDGE_TOP] = FALSE; + break; + default: /* Gtk::POS_TOP */ + anchors[GTK_LAYER_SHELL_EDGE_BOTTOM] = FALSE; + break; + }; + // Disable anchoring for other edges too if the width + // or the height has been set to a value other than 'auto' + // otherwise the bar will use all space + uint32_t configured_width = config["width"].isUInt() ? config["width"].asUInt() : 0; + uint32_t configured_height = config["height"].isUInt() ? config["height"].asUInt() : 0; + if (orientation == Gtk::ORIENTATION_VERTICAL && configured_height > 1) { + anchors[GTK_LAYER_SHELL_EDGE_TOP] = FALSE; + anchors[GTK_LAYER_SHELL_EDGE_BOTTOM] = FALSE; + } else if (orientation == Gtk::ORIENTATION_HORIZONTAL && configured_width > 1) { + anchors[GTK_LAYER_SHELL_EDGE_LEFT] = FALSE; + anchors[GTK_LAYER_SHELL_EDGE_RIGHT] = FALSE; + } + + for (auto edge : {GTK_LAYER_SHELL_EDGE_LEFT, GTK_LAYER_SHELL_EDGE_RIGHT, GTK_LAYER_SHELL_EDGE_TOP, + GTK_LAYER_SHELL_EDGE_BOTTOM}) { + gtk_layer_set_anchor(window.gobj(), edge, anchors[edge]); + } } void waybar::Bar::onMap(GdkEventAny*) { @@ -674,6 +399,9 @@ void waybar::Bar::onMap(GdkEventAny*) { */ auto gdk_window = window.get_window()->gobj(); surface = gdk_wayland_window_get_wl_surface(gdk_window); + configureGlobalOffset(gdk_window_get_width(gdk_window), gdk_window_get_height(gdk_window)); + + setPassThrough(passthrough_); } void waybar::Bar::setVisible(bool value) { @@ -734,7 +462,7 @@ void waybar::Bar::handleSignal(int signal) { } void waybar::Bar::getModules(const Factory& factory, const std::string& pos, - Gtk::Box* group = nullptr) { + waybar::Group* group = nullptr) { auto module_list = group ? config[pos]["modules"] : config[pos]; if (module_list.isArray()) { for (const auto& name : module_list) { @@ -747,19 +475,20 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, auto id_name = ref.substr(6, hash_pos - 6); auto class_name = hash_pos != std::string::npos ? ref.substr(hash_pos + 1) : ""; - auto parent = group ? group : &this->box_; - auto vertical = parent->get_orientation() == Gtk::ORIENTATION_VERTICAL; + auto vertical = (group ? group->getBox().get_orientation() : box_.get_orientation()) == + Gtk::ORIENTATION_VERTICAL; + auto group_module = new waybar::Group(id_name, class_name, config[ref], vertical); - getModules(factory, ref, &group_module->box); + getModules(factory, ref, group_module); module = group_module; } else { - module = factory.makeModule(ref); + module = factory.makeModule(ref, pos); } std::shared_ptr module_sp(module); modules_all_.emplace_back(module_sp); if (group) { - group->pack_start(*module, false, false); + group->addWidget(*module); } else { if (pos == "modules-left") { modules_left_.emplace_back(module_sp); @@ -815,3 +544,71 @@ auto waybar::Bar::setupWidgets() -> void { right_.pack_end(*module, false, false); } } + +void waybar::Bar::onConfigure(GdkEventConfigure* ev) { + /* + * GTK wants new size for the window. + * Actual resizing and management of the exclusve zone is handled within the gtk-layer-shell + * code. This event handler only updates stored size of the window and prints some warnings. + * + * Note: forced resizing to a window smaller than required by GTK would not work with + * gtk-layer-shell. + */ + if (orientation == Gtk::ORIENTATION_VERTICAL) { + if (width_ > 1 && ev->width > static_cast(width_)) { + spdlog::warn(MIN_WIDTH_MSG, width_, ev->width); + } + } else { + if (height_ > 1 && ev->height > static_cast(height_)) { + spdlog::warn(MIN_HEIGHT_MSG, height_, ev->height); + } + } + width_ = ev->width; + height_ = ev->height; + + configureGlobalOffset(ev->width, ev->height); + spdlog::info(BAR_SIZE_MSG, ev->width, ev->height, output->name); +} + +void waybar::Bar::configureGlobalOffset(int width, int height) { + auto monitor_geometry = *output->monitor->property_geometry().get_value().gobj(); + int x; + int y; + switch (position) { + case Gtk::POS_BOTTOM: + if (width + margins_.left + margins_.right >= monitor_geometry.width) + x = margins_.left; + else + x = (monitor_geometry.width - width) / 2; + y = monitor_geometry.height - height - margins_.bottom; + break; + case Gtk::POS_LEFT: + x = margins_.left; + if (height + margins_.top + margins_.bottom >= monitor_geometry.height) + y = margins_.top; + else + y = (monitor_geometry.height - height) / 2; + break; + case Gtk::POS_RIGHT: + x = monitor_geometry.width - width - margins_.right; + if (height + margins_.top + margins_.bottom >= monitor_geometry.height) + y = margins_.top; + else + y = (monitor_geometry.height - height) / 2; + break; + default: /* Gtk::POS_TOP */ + if (width + margins_.left + margins_.right >= monitor_geometry.width) + x = margins_.left; + else + x = (monitor_geometry.width - width) / 2; + y = margins_.top; + break; + } + + x_global = x + monitor_geometry.x; + y_global = y + monitor_geometry.y; +} + +void waybar::Bar::onOutputGeometryChanged() { + configureGlobalOffset(window.get_width(), window.get_height()); +} diff --git a/src/client.cpp b/src/client.cpp index a815e2fe..7c59dd5e 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -1,13 +1,14 @@ #include "client.hpp" +#include #include #include +#include "gtkmm/icontheme.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" #include "util/format.hpp" -#include "wlr-layer-shell-unstable-v1-client-protocol.h" waybar::Client *waybar::Client::inst() { static auto c = new Client(); @@ -17,13 +18,8 @@ waybar::Client *waybar::Client::inst() { void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { auto client = static_cast(data); - if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { - // limit version to a highest supported by the client protocol file - version = std::min(version, zwlr_layer_shell_v1_interface.version); - client->layer_shell = static_cast( - wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, version)); - } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && - version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) { + if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && + version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) { client->xdg_output_manager = static_cast(wl_registry_bind( registry, name, &zxdg_output_manager_v1_interface, ZXDG_OUTPUT_V1_NAME_SINCE_VERSION)); } else if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) { @@ -151,8 +147,26 @@ void waybar::Client::handleDeferredMonitorRemoval(Glib::RefPtr mon outputs_.remove_if([&monitor](const auto &output) { return output.monitor == monitor; }); } -const std::string waybar::Client::getStyle(const std::string &style) { - auto css_file = style.empty() ? Config::findConfigPath({"style.css"}) : style; +const std::string waybar::Client::getStyle(const std::string &style, + std::optional appearance = std::nullopt) { + std::optional css_file; + if (style.empty()) { + std::vector search_files; + switch (appearance.value_or(portal->getAppearance())) { + case waybar::Appearance::LIGHT: + search_files.push_back("style-light.css"); + break; + case waybar::Appearance::DARK: + search_files.push_back("style-dark.css"); + break; + case waybar::Appearance::UNKNOWN: + break; + } + search_files.push_back("style.css"); + css_file = Config::findConfigPath(search_files); + } else { + css_file = style; + } if (!css_file) { throw std::runtime_error("Missing required resource files"); } @@ -181,7 +195,12 @@ void waybar::Client::bindInterfaces() { }; wl_registry_add_listener(registry, ®istry_listener, this); wl_display_roundtrip(wl_display); - if (layer_shell == nullptr || xdg_output_manager == nullptr) { + + if (!gtk_layer_is_supported()) { + throw std::runtime_error("The Wayland compositor does not support wlr-layer-shell protocol"); + } + + if (xdg_output_manager == nullptr) { throw std::runtime_error("Failed to acquire required resources."); } // add existing outputs and subscribe to updates @@ -226,6 +245,11 @@ int waybar::Client::main(int argc, char *argv[]) { } gtk_app = Gtk::Application::create(argc, argv, "fr.arouillard.waybar", Gio::APPLICATION_HANDLES_COMMAND_LINE); + + // Initialize Waybars GTK resources with our custom icons + auto theme = Gtk::IconTheme::get_default(); + theme->add_resource_path("/fr/arouillard/waybar/icons"); + gdk_display = Gdk::Display::get_default(); if (!gdk_display) { throw std::runtime_error("Can't find display"); @@ -235,13 +259,39 @@ int waybar::Client::main(int argc, char *argv[]) { } wl_display = gdk_wayland_display_get_wl_display(gdk_display->gobj()); config.load(config_opt); - auto css_file = getStyle(style_opt); - setupCss(css_file); + if (!portal) { + portal = std::make_unique(); + } + m_cssFile = getStyle(style_opt); + setupCss(m_cssFile); + m_cssReloadHelper = std::make_unique(m_cssFile, [&]() { setupCss(m_cssFile); }); + portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) { + auto css_file = getStyle(style_opt, appearance); + setupCss(css_file); + }); + + auto m_config = config.getConfig(); + if (m_config.isObject() && m_config["reload_style_on_change"].asBool()) { + m_cssReloadHelper->monitorChanges(); + } else if (m_config.isArray()) { + for (const auto &conf : m_config) { + if (conf["reload_style_on_change"].asBool()) { + m_cssReloadHelper->monitorChanges(); + break; + } + } + } + bindInterfaces(); gtk_app->hold(); gtk_app->run(); + m_cssReloadHelper.reset(); // stop watching css file bars.clear(); return 0; } -void waybar::Client::reset() { gtk_app->quit(); } +void waybar::Client::reset() { + gtk_app->quit(); + // delete signal handler for css changes + portal->signal_appearance_changed().clear(); +} diff --git a/src/factory.cpp b/src/factory.cpp index 1d7a00b5..94076201 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -1,15 +1,125 @@ #include "factory.hpp" +#include "bar.hpp" + +#if defined(HAVE_CHRONO_TIMEZONES) || defined(HAVE_LIBDATE) +#include "modules/clock.hpp" +#else +#include "modules/simpleclock.hpp" +#endif +#ifdef HAVE_SWAY +#include "modules/sway/language.hpp" +#include "modules/sway/mode.hpp" +#include "modules/sway/scratchpad.hpp" +#include "modules/sway/window.hpp" +#include "modules/sway/workspaces.hpp" +#endif +#ifdef HAVE_WLR_TASKBAR +#include "modules/wlr/taskbar.hpp" +#endif +#ifdef HAVE_WLR_WORKSPACES +#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/language.hpp" +#include "modules/hyprland/submap.hpp" +#include "modules/hyprland/window.hpp" +#include "modules/hyprland/workspaces.hpp" +#endif +#if defined(__FreeBSD__) || defined(__linux__) +#include "modules/battery.hpp" +#endif +#if defined(HAVE_CPU_LINUX) || defined(HAVE_CPU_BSD) +#include "modules/cpu.hpp" +#include "modules/cpu_frequency.hpp" +#include "modules/cpu_usage.hpp" +#include "modules/load.hpp" +#endif +#include "modules/idle_inhibitor.hpp" +#if defined(HAVE_MEMORY_LINUX) || defined(HAVE_MEMORY_BSD) +#include "modules/memory.hpp" +#endif +#include "modules/disk.hpp" +#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 +#ifdef HAVE_LIBUDEV +#include "modules/backlight.hpp" +#include "modules/backlight_slider.hpp" +#endif +#ifdef HAVE_LIBEVDEV +#include "modules/keyboard_state.hpp" +#endif +#ifdef HAVE_GAMEMODE +#include "modules/gamemode.hpp" +#endif +#ifdef HAVE_UPOWER +#include "modules/upower/upower.hpp" +#endif +#ifdef HAVE_PIPEWIRE +#include "modules/privacy/privacy.hpp" +#endif +#ifdef HAVE_LIBPULSE +#include "modules/pulseaudio.hpp" +#include "modules/pulseaudio_slider.hpp" +#endif +#ifdef HAVE_LIBMPDCLIENT +#include "modules/mpd/mpd.hpp" +#endif +#ifdef HAVE_LIBSNDIO +#include "modules/sndio.hpp" +#endif +#if defined(__linux__) +#include "modules/bluetooth.hpp" +#include "modules/power_profiles_daemon.hpp" +#endif +#ifdef HAVE_LOGIND_INHIBITOR +#include "modules/inhibitor.hpp" +#endif +#ifdef HAVE_LIBJACK +#include "modules/jack.hpp" +#endif +#ifdef HAVE_LIBWIREPLUMBER +#include "modules/wireplumber.hpp" +#endif +#ifdef HAVE_LIBCAVA +#include "modules/cava.hpp" +#endif +#ifdef HAVE_SYSTEMD_MONITOR +#include "modules/systemd_failed_units.hpp" +#endif +#include "modules/cffi.hpp" +#include "modules/custom.hpp" +#include "modules/image.hpp" +#include "modules/temperature.hpp" +#include "modules/user.hpp" + waybar::Factory::Factory(const Bar& bar, const Json::Value& config) : bar_(bar), config_(config) {} -waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { +waybar::AModule* waybar::Factory::makeModule(const std::string& name, + const std::string& pos) const { try { auto hash_pos = name.find('#'); auto ref = name.substr(0, hash_pos); auto id = hash_pos != std::string::npos ? name.substr(hash_pos + 1) : ""; -#if defined(__FreeBSD__) || (defined(__linux__) && !defined(NO_FILESYSTEM)) +#if defined(__FreeBSD__) || defined(__linux__) if (ref == "battery") { - return new waybar::modules::Battery(id, config_[name]); + return new waybar::modules::Battery(id, bar_, config_[name]); } #endif #ifdef HAVE_GAMEMODE @@ -22,6 +132,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::upower::UPower(id, config_[name]); } #endif +#ifdef HAVE_PIPEWIRE + if (ref == "privacy") { + return new waybar::modules::privacy::Privacy(id, config_[name], pos); + } +#endif #ifdef HAVE_MPRIS if (ref == "mpris") { return new waybar::modules::mpris::Mpris(id, config_[name]); @@ -44,16 +159,16 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::sway::Scratchpad(id, config_[name]); } #endif -#ifdef HAVE_WLR +#ifdef HAVE_WLR_TASKBAR if (ref == "wlr/taskbar") { return new waybar::modules::wlr::Taskbar(id, bar_, config_[name]); } -#ifdef USE_EXPERIMENTAL +#endif +#ifdef HAVE_WLR_WORKSPACES if (ref == "wlr/workspaces") { return new waybar::modules::wlr::WorkspaceManager(id, bar_, config_[name]); } #endif -#endif #ifdef HAVE_RIVER if (ref == "river/mode") { return new waybar::modules::river::Mode(id, bar_, config_[name]); @@ -99,6 +214,17 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "cpu") { return new waybar::modules::Cpu(id, config_[name]); } +#if defined(HAVE_CPU_LINUX) + if (ref == "cpu_frequency") { + return new waybar::modules::CpuFrequency(id, config_[name]); + } +#endif + if (ref == "cpu_usage") { + return new waybar::modules::CpuUsage(id, config_[name]); + } + if (ref == "load") { + return new waybar::modules::Load(id, config_[name]); + } #endif if (ref == "clock") { return new waybar::modules::Clock(id, config_[name]); @@ -126,6 +252,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "backlight") { return new waybar::modules::Backlight(id, config_[name]); } + if (ref == "backlight/slider") { + return new waybar::modules::BacklightSlider(id, config_[name]); + } #endif #ifdef HAVE_LIBEVDEV if (ref == "keyboard-state") { @@ -136,6 +265,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "pulseaudio") { return new waybar::modules::Pulseaudio(id, config_[name]); } + if (ref == "pulseaudio/slider") { + return new waybar::modules::PulseaudioSlider(id, config_[name]); + } #endif #ifdef HAVE_LIBMPDCLIENT if (ref == "mpd") { @@ -147,10 +279,15 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::Sndio(id, config_[name]); } #endif -#ifdef HAVE_GIO_UNIX +#if defined(__linux__) if (ref == "bluetooth") { return new waybar::modules::Bluetooth(id, config_[name]); } + if (ref == "power-profiles-daemon") { + return new waybar::modules::PowerProfilesDaemon(id, config_[name]); + } +#endif +#ifdef HAVE_LOGIND_INHIBITOR if (ref == "inhibitor") { return new waybar::modules::Inhibitor(id, bar_, config_[name]); } @@ -169,12 +306,20 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "cava") { return new waybar::modules::Cava(id, config_[name]); } +#endif +#ifdef HAVE_SYSTEMD_MONITOR + if (ref == "systemd-failed-units") { + return new waybar::modules::SystemdFailedUnits(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]); + return new waybar::modules::Custom(ref.substr(7), id, config_[name], bar_.output->name); + } + if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) { + return new waybar::modules::CFFI(ref.substr(5), 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 548fb0da..262cae65 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -4,12 +4,31 @@ #include +#include "gdkmm/device.h" +#include "gtkmm/widget.h" + namespace waybar { +const Gtk::RevealerTransitionType getPreferredTransitionType(bool is_vertical) { + /* The transition direction of a drawer is not actually determined by the transition type, + * but rather by the order of 'box' and 'revealer_box': + * 'REVEALER_TRANSITION_TYPE_SLIDE_LEFT' and 'REVEALER_TRANSITION_TYPE_SLIDE_RIGHT' + * will result in the same thing. + * However: we still need to differentiate between vertical and horizontal transition types. + */ + + if (is_vertical) { + return Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_UP; + } else { + return Gtk::RevealerTransitionType::REVEALER_TRANSITION_TYPE_SLIDE_LEFT; + } +} + 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} { + : AModule(config, name, id, true, true), + box{vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + revealer_box{vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0} { box.set_name(name_); if (!id.empty()) { box.get_style_context()->add_class(id); @@ -29,12 +48,82 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& } else { throw std::runtime_error("Invalid orientation value: " + orientation); } + + if (config_["drawer"].isObject()) { + is_drawer = true; + + const auto& drawer_config = config_["drawer"]; + const int transition_duration = + (drawer_config["transition-duration"].isInt() ? drawer_config["transition-duration"].asInt() + : 500); + add_class_to_drawer_children = + (drawer_config["children-class"].isString() ? drawer_config["children-class"].asString() + : "drawer-child"); + const bool left_to_right = (drawer_config["transition-left-to-right"].isBool() + ? drawer_config["transition-left-to-right"].asBool() + : true); + + auto transition_type = getPreferredTransitionType(vertical); + + revealer.set_transition_type(transition_type); + revealer.set_transition_duration(transition_duration); + revealer.set_reveal_child(false); + + revealer.get_style_context()->add_class("drawer"); + + revealer.add(revealer_box); + + if (left_to_right) { + box.pack_end(revealer); + } else { + box.pack_start(revealer); + } + + addHoverHandlerTo(revealer); + } +} + +bool Group::handleMouseHover(GdkEventCrossing* const& e) { + switch (e->type) { + case GDK_ENTER_NOTIFY: + revealer.set_reveal_child(true); + break; + case GDK_LEAVE_NOTIFY: + revealer.set_reveal_child(false); + break; + default: + break; + } + + return true; +} + +void Group::addHoverHandlerTo(Gtk::Widget& widget) { + widget.add_events(Gdk::EventMask::ENTER_NOTIFY_MASK | Gdk::EventMask::LEAVE_NOTIFY_MASK); + widget.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Group::handleMouseHover)); + widget.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Group::handleMouseHover)); } auto Group::update() -> void { // noop } +Gtk::Box& Group::getBox() { return is_drawer ? (is_first_widget ? box : revealer_box) : box; } + +void Group::addWidget(Gtk::Widget& widget) { + getBox().pack_start(widget, false, false); + + if (is_drawer) { + // Necessary because of GTK's hitbox detection + addHoverHandlerTo(widget); + if (!is_first_widget) { + widget.get_style_context()->add_class(add_class_to_drawer_children); + } + } + + is_first_widget = false; +} + Group::operator Gtk::Widget&() { return box; } } // namespace waybar diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 27871048..4ae511eb 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -9,179 +10,26 @@ #include #include -namespace { -class FileDescriptor { - public: - explicit FileDescriptor(int fd) : fd_(fd) {} - FileDescriptor(const FileDescriptor &other) = delete; - FileDescriptor(FileDescriptor &&other) noexcept = delete; - FileDescriptor &operator=(const FileDescriptor &other) = delete; - FileDescriptor &operator=(FileDescriptor &&other) noexcept = delete; - ~FileDescriptor() { - if (fd_ != -1) { - if (close(fd_) != 0) { - fmt::print(stderr, "Failed to close fd: {}\n", errno); - } - } - } - int get() const { return fd_; } - - private: - int fd_; -}; - -struct UdevDeleter { - void operator()(udev *ptr) { udev_unref(ptr); } -}; - -struct UdevDeviceDeleter { - void operator()(udev_device *ptr) { udev_device_unref(ptr); } -}; - -struct UdevEnumerateDeleter { - void operator()(udev_enumerate *ptr) { udev_enumerate_unref(ptr); } -}; - -struct UdevMonitorDeleter { - void operator()(udev_monitor *ptr) { udev_monitor_unref(ptr); } -}; - -void check_eq(int rc, int expected, const char *message = "eq, rc was: ") { - if (rc != expected) { - 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(fmt::runtime(message), rc)); - } -} - -void check0(int rc, const char *message = "rc wasn't 0") { check_eq(rc, 0, message); } - -void check_gte(int rc, int gte, const char *message = "rc was: ") { - if (rc < gte) { - throw std::runtime_error(fmt::format(fmt::runtime(message), rc)); - } -} - -void check_nn(const void *ptr, const char *message = "ptr was null") { - if (ptr == nullptr) { - throw std::runtime_error(message); - } -} -} // namespace - -waybar::modules::Backlight::BacklightDev::BacklightDev(std::string name, int actual, int max, - bool powered) - : name_(std::move(name)), actual_(actual), max_(max), powered_(powered) {} - -std::string_view waybar::modules::Backlight::BacklightDev::name() const { return name_; } - -int waybar::modules::Backlight::BacklightDev::get_actual() const { return actual_; } - -void waybar::modules::Backlight::BacklightDev::set_actual(int actual) { actual_ = actual; } - -int waybar::modules::Backlight::BacklightDev::get_max() const { return max_; } - -void waybar::modules::Backlight::BacklightDev::set_max(int max) { max_ = max; } - -bool waybar::modules::Backlight::BacklightDev::get_powered() const { return powered_; } - -void waybar::modules::Backlight::BacklightDev::set_powered(bool powered) { powered_ = powered; } +#include "util/backend_common.hpp" +#include "util/backlight_backend.hpp" waybar::modules::Backlight::Backlight(const std::string &id, const Json::Value &config) : ALabel(config, "backlight", id, "{percent}%", 2), - preferred_device_(config["device"].isString() ? config["device"].asString() : "") { - // Get initial state - { - std::unique_ptr udev_check{udev_new()}; - check_nn(udev_check.get(), "Udev check new failed"); - enumerate_devices(devices_.begin(), devices_.end(), std::back_inserter(devices_), - udev_check.get()); - if (devices_.empty()) { - throw std::runtime_error("No backlight found"); - } - dp.emit(); - } + preferred_device_(config["device"].isString() ? config["device"].asString() : ""), + backend(interval_, [this] { dp.emit(); }) { + 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"); - - std::unique_ptr mon{ - udev_monitor_new_from_netlink(udev.get(), "udev")}; - check_nn(mon.get(), "udev monitor new failed"); - check_gte(udev_monitor_filter_add_match_subsystem_devtype(mon.get(), "backlight", nullptr), 0, - "udev failed to add monitor filter: "); - udev_monitor_enable_receiving(mon.get()); - - auto udev_fd = udev_monitor_get_fd(mon.get()); - - auto epoll_fd = FileDescriptor{epoll_create1(EPOLL_CLOEXEC)}; - check_neq(epoll_fd.get(), -1, "epoll init failed: "); - epoll_event ctl_event{}; - ctl_event.events = EPOLLIN; - ctl_event.data.fd = udev_fd; - - check0(epoll_ctl(epoll_fd.get(), EPOLL_CTL_ADD, ctl_event.data.fd, &ctl_event), - "epoll_ctl failed: {}"); - epoll_event events[EPOLL_MAX_EVENTS]; - - while (udev_thread_.isRunning()) { - const int event_count = epoll_wait(epoll_fd.get(), events, EPOLL_MAX_EVENTS, - std::chrono::milliseconds{interval_}.count()); - if (!udev_thread_.isRunning()) { - break; - } - decltype(devices_) devices; - { - std::scoped_lock lock(udev_thread_mutex_); - devices = devices_; - } - for (int i = 0; i < event_count; ++i) { - const auto &event = events[i]; - check_eq(event.data.fd, udev_fd, "unexpected udev fd"); - std::unique_ptr dev{udev_monitor_receive_device(mon.get())}; - check_nn(dev.get(), "epoll dev was null"); - upsert_device(devices.begin(), devices.end(), std::back_inserter(devices), dev.get()); - } - - // Refresh state if timed out - if (event_count == 0) { - enumerate_devices(devices.begin(), devices.end(), std::back_inserter(devices), udev.get()); - } - { - std::scoped_lock lock(udev_thread_mutex_); - devices_ = devices; - } - dp.emit(); - } - }; } -waybar::modules::Backlight::~Backlight() = default; - auto waybar::modules::Backlight::update() -> void { - decltype(devices_) devices; - { - std::scoped_lock lock(udev_thread_mutex_); - devices = devices_; - } + GET_BEST_DEVICE(best, backend, preferred_device_); - const auto best = best_device(devices.cbegin(), devices.cend(), preferred_device_); + const auto previous_best_device = backend.get_previous_best_device(); if (best != nullptr) { - if (previous_best_.has_value() && previous_best_.value() == *best && + if (previous_best_device != nullptr && *previous_best_device == *best && !previous_format_.empty() && previous_format_ == format_) { return; } @@ -211,82 +59,16 @@ auto waybar::modules::Backlight::update() -> void { event_box_.hide(); } } else { - if (!previous_best_.has_value()) { + if (previous_best_device == nullptr) { return; } label_.set_markup(""); } - previous_best_ = best == nullptr ? std::nullopt : std::optional{*best}; + backend.set_previous_best_device(best); previous_format_ = format_; - // Call parent update ALabel::update(); } -template -const waybar::modules::Backlight::BacklightDev *waybar::modules::Backlight::best_device( - ForwardIt first, ForwardIt last, std::string_view preferred_device) { - const auto found = std::find_if( - first, last, [preferred_device](const auto &dev) { return dev.name() == preferred_device; }); - if (found != last) { - return &(*found); - } - - const auto max = std::max_element( - first, last, [](const auto &l, const auto &r) { return l.get_max() < r.get_max(); }); - - return max == last ? nullptr : &(*max); -} - -template -void waybar::modules::Backlight::upsert_device(ForwardIt first, ForwardIt last, Inserter inserter, - udev_device *dev) { - const char *name = udev_device_get_sysname(dev); - check_nn(name); - - const char *actual_brightness_attr = - strncmp(name, "amdgpu_bl", 9) == 0 ? "brightness" : "actual_brightness"; - - const char *actual = udev_device_get_sysattr_value(dev, actual_brightness_attr); - const char *max = udev_device_get_sysattr_value(dev, "max_brightness"); - const char *power = udev_device_get_sysattr_value(dev, "bl_power"); - - auto found = - std::find_if(first, last, [name](const auto &device) { return device.name() == name; }); - if (found != last) { - if (actual != nullptr) { - found->set_actual(std::stoi(actual)); - } - if (max != nullptr) { - found->set_max(std::stoi(max)); - } - if (power != nullptr) { - found->set_powered(std::stoi(power) == 0); - } - } else { - const int actual_int = actual == nullptr ? 0 : std::stoi(actual); - const int max_int = max == nullptr ? 0 : std::stoi(max); - const bool power_bool = power == nullptr ? true : std::stoi(power) == 0; - *inserter = BacklightDev{name, actual_int, max_int, power_bool}; - ++inserter; - } -} - -template -void waybar::modules::Backlight::enumerate_devices(ForwardIt first, ForwardIt last, - Inserter inserter, udev *udev) { - std::unique_ptr enumerate{udev_enumerate_new(udev)}; - udev_enumerate_add_match_subsystem(enumerate.get(), "backlight"); - udev_enumerate_scan_devices(enumerate.get()); - udev_list_entry *enum_devices = udev_enumerate_get_list_entry(enumerate.get()); - udev_list_entry *dev_list_entry; - udev_list_entry_foreach(dev_list_entry, enum_devices) { - const char *path = udev_list_entry_get_name(dev_list_entry); - std::unique_ptr dev{udev_device_new_from_syspath(udev, path)}; - check_nn(dev.get(), "dev new failed"); - upsert_device(first, last, inserter, dev.get()); - } -} - 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()) { @@ -294,14 +76,33 @@ bool waybar::modules::Backlight::handleScroll(GdkEventScroll *e) { } // Fail fast if the proxy could not be initialized - if (!login_proxy_) { + if (!backend.is_login_proxy_initialized()) { return true; } // Check scroll direction auto dir = AModule::getScrollDir(e); - if (dir == SCROLL_DIR::NONE) { - return true; + + // No worries, it will always be set because of the switch below. This is purely to suppress a + // warning + util::ChangeType ct = util::ChangeType::Increase; + + switch (dir) { + case SCROLL_DIR::UP: + [[fallthrough]]; + case SCROLL_DIR::RIGHT: + ct = util::ChangeType::Increase; + break; + + case SCROLL_DIR::DOWN: + [[fallthrough]]; + case SCROLL_DIR::LEFT: + ct = util::ChangeType::Decrease; + break; + + case SCROLL_DIR::NONE: + return true; + break; } // Get scroll step @@ -311,38 +112,7 @@ bool waybar::modules::Backlight::handleScroll(GdkEventScroll *e) { 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); + backend.set_brightness(preferred_device_, ct, step); return true; } diff --git a/src/modules/backlight_slider.cpp b/src/modules/backlight_slider.cpp new file mode 100644 index 00000000..6269dddb --- /dev/null +++ b/src/modules/backlight_slider.cpp @@ -0,0 +1,23 @@ +#include "modules/backlight_slider.hpp" + +#include "ASlider.hpp" + +namespace waybar::modules { + +BacklightSlider::BacklightSlider(const std::string& id, const Json::Value& config) + : ASlider(config, "backlight-slider", id), + interval_(config_["interval"].isUInt() ? config_["interval"].asUInt() : 1000), + preferred_device_(config["device"].isString() ? config["device"].asString() : ""), + backend(interval_, [this] { this->dp.emit(); }) {} + +void BacklightSlider::update() { + uint16_t brightness = backend.get_scaled_brightness(preferred_device_); + scale_.set_value(brightness); +} + +void BacklightSlider::onValueChanged() { + auto brightness = scale_.get_value(); + backend.set_scaled_brightness(preferred_device_, brightness); +} + +} // namespace waybar::modules \ No newline at end of file diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index c0f433ae..9003db6e 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -1,12 +1,14 @@ #include "modules/battery.hpp" + +#include #if defined(__FreeBSD__) #include #endif #include #include -waybar::modules::Battery::Battery(const std::string& id, const Json::Value& config) - : ALabel(config, "battery", id, "{capacity}%", 60) { +waybar::modules::Battery::Battery(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "battery", id, "{capacity}%", 60), bar_(bar) { #if defined(__linux__) battery_watch_fd_ = inotify_init1(IN_CLOEXEC); if (battery_watch_fd_ == -1) { @@ -100,9 +102,11 @@ void waybar::modules::Battery::refreshBatteries() { } auto dir_name = node.path().filename(); auto bat_defined = config_["bat"].isString(); + bool bat_compatibility = config_["bat-compatibility"].asBool(); if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) && (fs::exists(node.path() / "capacity") || fs::exists(node.path() / "charge_now")) && - fs::exists(node.path() / "uevent") && fs::exists(node.path() / "status") && + fs::exists(node.path() / "uevent") && + (fs::exists(node.path() / "status") || bat_compatibility) && fs::exists(node.path() / "type")) { std::string type; std::ifstream(node.path() / "type") >> type; @@ -252,7 +256,13 @@ const std::tuple waybar::modules::Battery::g for (auto const& item : batteries_) { auto bat = item.first; std::string _status; - std::getline(std::ifstream(bat / "status"), _status); + + /* Check for adapter status if battery is not available */ + if (!std::ifstream(bat / "status")) { + std::getline(std::ifstream(adapter_ / "status"), _status); + } else { + std::getline(std::ifstream(bat / "status"), _status); + } // Some battery will report current and charge in μA/μAh. // Scale these by the voltage to get μW/μWh. @@ -534,6 +544,13 @@ const std::tuple waybar::modules::Battery::g } } + // Handle weighted-average + if ((config_["weighted-average"].isBool() ? config_["weighted-average"].asBool() : false) && + total_energy_exists && total_energy_full_exists) { + if (total_energy_full > 0.0f) + calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full); + } + // Handle design-capacity if ((config_["design-capacity"].isBool() ? config_["design-capacity"].asBool() : false) && total_energy_exists && total_energy_full_design_exists) { @@ -626,6 +643,7 @@ auto waybar::modules::Battery::update() -> void { [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); auto format = format_; auto state = getState(capacity, true); + setBarClass(state); auto time_remaining_formatted = formatTimeRemaining(time_remaining); if (tooltipEnabled()) { std::string tooltip_text_default; @@ -674,3 +692,38 @@ auto waybar::modules::Battery::update() -> void { // Call parent update ALabel::update(); } + +void waybar::modules::Battery::setBarClass(std::string& state) { + auto classes = bar_.window.get_style_context()->list_classes(); + const std::string prefix = "battery-"; + + auto old_class_it = std::find_if(classes.begin(), classes.end(), [&prefix](auto classname) { + return classname.rfind(prefix, 0) == 0; + }); + + auto new_class = prefix + state; + + // If the bar doesn't have any `battery-` class + if (old_class_it == classes.end()) { + if (!state.empty()) { + bar_.window.get_style_context()->add_class(new_class); + } + return; + } + + auto old_class = *old_class_it; + + // If the bar has a `battery-` class, + // but `state` is empty + if (state.empty()) { + bar_.window.get_style_context()->remove_class(old_class); + return; + } + + // If the bar has a `battery-` class, + // and `state` is NOT empty + if (old_class != new_class) { + bar_.window.get_style_context()->remove_class(old_class); + bar_.window.get_style_context()->add_class(new_class); + } +} diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index c3a25473..80e4731b 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -6,12 +6,19 @@ #include #include +#include "util/scope_guard.hpp" + namespace { using GDBusManager = std::unique_ptr; auto generateManager() -> GDBusManager { GError* error = nullptr; + waybar::util::ScopeGuard error_deleter([error]() { + if (error) { + g_error_free(error); + } + }); GDBusObjectManager* manager = g_dbus_object_manager_client_new_for_bus_sync( G_BUS_TYPE_SYSTEM, GDBusObjectManagerClientFlags::G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, @@ -19,7 +26,6 @@ auto generateManager() -> GDBusManager { if (error) { spdlog::error("g_dbus_object_manager_client_new_for_bus_sync() failed: {}", error->message); - g_error_free(error); } auto destructor = [](GDBusObjectManager* manager) { @@ -92,28 +98,30 @@ waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& std::back_inserter(device_preference_), [](auto x) { return x.asString(); }); } - // NOTE: assumption made that the controller that is selcected stays unchanged + // NOTE: assumption made that the controller that is selected stays unchanged // for duration of the module - if (!findCurController(cur_controller_)) { + if (cur_controller_ = findCurController(); !cur_controller_) { if (config_["controller-alias"].isString()) { spdlog::error("findCurController() failed: no bluetooth controller found with alias '{}'", config_["controller-alias"].asString()); } else { spdlog::error("findCurController() failed: no bluetooth controller found"); } - event_box_.hide(); - return; - } - findConnectedDevices(cur_controller_.path, connected_devices_); + update(); + } else { + // These calls only make sense if a controller could be found + findConnectedDevices(cur_controller_->path, connected_devices_); + g_signal_connect(manager_.get(), "interface-proxy-properties-changed", + G_CALLBACK(onInterfaceProxyPropertiesChanged), this); + g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), + this); + g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), + this); - g_signal_connect(manager_.get(), "interface-proxy-properties-changed", - G_CALLBACK(onInterfaceProxyPropertiesChanged), this); - g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), this); - g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), - this); #ifdef WANT_RFKILL - rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); + rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); #endif + } dp.emit(); } @@ -144,12 +152,16 @@ auto waybar::modules::Bluetooth::update() -> void { std::string state; std::string tooltip_format; - if (!cur_controller_.powered) - state = "off"; - else if (!connected_devices_.empty()) - state = "connected"; - else - state = "on"; + if (cur_controller_) { + if (!cur_controller_->powered) + state = "off"; + else if (!connected_devices_.empty()) + state = "connected"; + else + state = "on"; + } else { + state = "no-controller"; + } #ifdef WANT_RFKILL if (rfkill_.getState()) state = "disabled"; #endif @@ -187,8 +199,6 @@ auto waybar::modules::Bluetooth::update() -> void { tooltip_format = config_["tooltip-format"].asString(); } - format_.empty() ? event_box_.hide() : event_box_.show(); - auto update_style_context = [this](const std::string& style_class, bool in_next_state) { if (in_next_state && !label_.get_style_context()->has_class(style_class)) { label_.get_style_context()->add_class(style_class); @@ -196,25 +206,32 @@ auto waybar::modules::Bluetooth::update() -> void { label_.get_style_context()->remove_class(style_class); } }; - update_style_context("discoverable", cur_controller_.discoverable); - update_style_context("discovering", cur_controller_.discovering); - update_style_context("pairable", cur_controller_.pairable); + update_style_context("discoverable", cur_controller_ ? cur_controller_->discoverable : false); + update_style_context("discovering", cur_controller_ ? cur_controller_->discovering : false); + update_style_context("pairable", cur_controller_ ? cur_controller_->pairable : false); if (!state_.empty()) { update_style_context(state_, false); } update_style_context(state, true); state_ = state; - label_.set_markup(fmt::format( - 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), - fmt::arg("device_address", cur_focussed_device_.address), - fmt::arg("device_address_type", cur_focussed_device_.address_type), - fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_label), - fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)))); + if (format_.empty()) { + event_box_.hide(); + } else { + event_box_.show(); + label_.set_markup(fmt::format( + fmt::runtime(format_), fmt::arg("status", state_), + fmt::arg("num_connections", connected_devices_.size()), + fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"), + fmt::arg("controller_address_type", + cur_controller_ ? cur_controller_->address_type : "null"), + fmt::arg("controller_alias", cur_controller_ ? cur_controller_->alias : "null"), + fmt::arg("device_address", cur_focussed_device_.address), + fmt::arg("device_address_type", cur_focussed_device_.address_type), + fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_label), + fmt::arg("device_battery_percentage", + cur_focussed_device_.battery_percentage.value_or(0)))); + } if (tooltipEnabled()) { bool tooltip_enumerate_connections_ = config_["tooltip-format-enumerate-connected"].isString(); @@ -250,9 +267,10 @@ auto waybar::modules::Bluetooth::update() -> void { label_.set_tooltip_text(fmt::format( 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), - fmt::arg("controller_alias", cur_controller_.alias), + fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"), + fmt::arg("controller_address_type", + cur_controller_ ? cur_controller_->address_type : "null"), + fmt::arg("controller_alias", cur_controller_ ? cur_controller_->alias : "null"), fmt::arg("device_address", cur_focussed_device_.address), fmt::arg("device_address_type", cur_focussed_device_.address_type), fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_tooltip), @@ -292,8 +310,8 @@ auto waybar::modules::Bluetooth::onInterfaceProxyPropertiesChanged( Bluetooth* bt = static_cast(user_data); if (interface_name == "org.bluez.Adapter1") { - if (object_path == bt->cur_controller_.path) { - bt->getControllerProperties(G_DBUS_OBJECT(object_proxy), bt->cur_controller_); + if (object_path == bt->cur_controller_->path) { + bt->getControllerProperties(G_DBUS_OBJECT(object_proxy), *bt->cur_controller_); bt->dp.emit(); } } else if (interface_name == "org.bluez.Device1" || interface_name == "org.bluez.Battery1") { @@ -378,22 +396,23 @@ auto waybar::modules::Bluetooth::getControllerProperties(GDBusObject* object, return false; } -auto waybar::modules::Bluetooth::findCurController(ControllerInfo& controller_info) -> bool { - bool found_controller = false; +auto waybar::modules::Bluetooth::findCurController() -> std::optional { + std::optional controller_info; GList* objects = g_dbus_object_manager_get_objects(manager_.get()); for (GList* l = objects; l != NULL; l = l->next) { GDBusObject* object = G_DBUS_OBJECT(l->data); - if (getControllerProperties(object, controller_info) && + ControllerInfo info; + if (getControllerProperties(object, info) && (!config_["controller-alias"].isString() || - config_["controller-alias"].asString() == controller_info.alias)) { - found_controller = true; + config_["controller-alias"].asString() == info.alias)) { + controller_info = std::move(info); break; } } g_list_free_full(objects, g_object_unref); - return found_controller; + return controller_info; } auto waybar::modules::Bluetooth::findConnectedDevices(const std::string& cur_controller_path, @@ -404,7 +423,7 @@ auto waybar::modules::Bluetooth::findConnectedDevices(const std::string& cur_con GDBusObject* object = G_DBUS_OBJECT(l->data); DeviceInfo device; if (getDeviceProperties(object, device) && device.connected && - device.paired_controller == cur_controller_.path) { + device.paired_controller == cur_controller_->path) { connected_devices.push_back(device); } } diff --git a/src/modules/cava.cpp b/src/modules/cava.cpp index be9bef4e..07227546 100644 --- a/src/modules/cava.cpp +++ b/src/modules/cava.cpp @@ -25,7 +25,7 @@ waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) // Override cava parameters by the user config prm_.inAtty = 0; - prm_.output = output_method::OUTPUT_RAW; + prm_.output = cava::output_method::OUTPUT_RAW; strcpy(prm_.data_format, "ascii"); strcpy(prm_.raw_target, "/dev/stdout"); prm_.ascii_range = config_["format-icons"].size() - 1; @@ -34,9 +34,9 @@ waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) 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_.orientation = cava::ORIENT_TOP; + prm_.xaxis = cava::xaxis_scale::NONE; + prm_.mono_opt = cava::AVERAGE; prm_.autobars = 0; prm_.gravity = 0; prm_.integral = 1; @@ -51,10 +51,10 @@ waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) 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()); + prm_.input = cava::input_method_by_name(config_["method"].asString().c_str()); if (config_["source"].isString()) prm_.audio_source = config_["source"].asString().data(); - if (config_["sample_rate"].isNumeric()) prm_.fifoSample = config_["sample_rate"].asLargestInt(); - if (config_["sample_bits"].isInt()) prm_.fifoSampleBits = config_["sample_bits"].asInt(); + if (config_["sample_rate"].isNumeric()) prm_.samplerate = config_["sample_rate"].asLargestInt(); + if (config_["sample_bits"].isInt()) prm_.samplebits = config_["sample_bits"].asInt(); if (config_["stereo"].isBool()) prm_.stereo = config_["stereo"].asBool(); if (config_["reverse"].isBool()) prm_.reverse = config_["reverse"].asBool(); if (config_["bar_delimiter"].isInt()) prm_.bar_delim = config_["bar_delimiter"].asInt(); @@ -64,8 +64,9 @@ waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) prm_.noise_reduction = config_["noise_reduction"].asDouble(); if (config_["input_delay"].isInt()) fetch_input_delay_ = std::chrono::seconds(config_["input_delay"].asInt()); + if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool(); // Make cava parameters configuration - plan_ = new cava_plan{}; + plan_ = new cava::cava_plan{}; audio_raw_.height = prm_.ascii_range; audio_data_.format = -1; @@ -155,12 +156,13 @@ auto waybar::modules::Cava::update() -> void { 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_); + cava::cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out, + plan_); if (audio_data_.samples_counter > 0) audio_data_.samples_counter = 0; pthread_mutex_unlock(&audio_data_.lock); // Do transformation under raw data - audio_raw_fetch(&audio_raw_, &prm_, &rePaint_); + audio_raw_fetch(&audio_raw_, &prm_, &rePaint_, plan_); if (rePaint_ == 1) { text_.clear(); @@ -174,10 +176,13 @@ auto waybar::modules::Cava::update() -> void { } label_.set_markup(text_); + label_.show(); ALabel::update(); } - } else + } else { upThreadDelay(frame_time_milsec_, suspend_silence_delay_); + if (hide_on_silence_) label_.hide(); + } } auto waybar::modules::Cava::doAction(const std::string& name) -> void { diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp new file mode 100644 index 00000000..e560659b --- /dev/null +++ b/src/modules/cffi.cpp @@ -0,0 +1,119 @@ +#include "modules/cffi.hpp" + +#include +#include + +#include +#include +#include + +namespace waybar::modules { + +CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& config) + : AModule(config, name, id, true, true) { + const auto dynlib_path = config_["module_path"].asString(); + if (dynlib_path.empty()) { + throw std::runtime_error{"Missing or empty 'module_path' in module config"}; + } + + void* handle = dlopen(dynlib_path.c_str(), RTLD_LAZY); + if (handle == nullptr) { + throw std::runtime_error{std::string{"Failed to load CFFI module: "} + dlerror()}; + } + + // Fetch ABI version + auto wbcffi_version = reinterpret_cast(dlsym(handle, "wbcffi_version")); + if (wbcffi_version == nullptr) { + throw std::runtime_error{std::string{"Missing wbcffi_version function: "} + dlerror()}; + } + + // Fetch functions + if (*wbcffi_version == 1) { + // Mandatory functions + hooks_.init = reinterpret_cast(dlsym(handle, "wbcffi_init")); + if (!hooks_.init) { + throw std::runtime_error{std::string{"Missing wbcffi_init function: "} + dlerror()}; + } + hooks_.deinit = reinterpret_cast(dlsym(handle, "wbcffi_deinit")); + if (!hooks_.init) { + throw std::runtime_error{std::string{"Missing wbcffi_deinit function: "} + dlerror()}; + } + // Optional functions + if (auto fn = reinterpret_cast(dlsym(handle, "wbcffi_update"))) { + hooks_.update = fn; + } + if (auto fn = reinterpret_cast(dlsym(handle, "wbcffi_refresh"))) { + hooks_.refresh = fn; + } + if (auto fn = reinterpret_cast(dlsym(handle, "wbcffi_doaction"))) { + hooks_.doAction = fn; + } + } else { + throw std::runtime_error{"Unknown wbcffi_version " + std::to_string(*wbcffi_version)}; + } + + // Prepare init() arguments + // Convert JSON values to string + std::vector config_entries_stringstor; + const auto& keys = config.getMemberNames(); + for (size_t i = 0; i < keys.size(); i++) { + const auto& value = config[keys[i]]; + if (value.isConvertibleTo(Json::ValueType::stringValue)) { + config_entries_stringstor.push_back(config[keys[i]].asString()); + } else { + config_entries_stringstor.push_back(config[keys[i]].toStyledString()); + } + } + + // Prepare config_entries array + std::vector config_entries; + for (size_t i = 0; i < keys.size(); i++) { + config_entries.push_back({keys[i].c_str(), config_entries_stringstor[i].c_str()}); + } + + ffi::wbcffi_init_info init_info = { + .obj = (ffi::wbcffi_module*)this, + .waybar_version = VERSION, + .get_root_widget = + [](ffi::wbcffi_module* obj) { + return dynamic_cast(&((CFFI*)obj)->event_box_)->gobj(); + }, + .queue_update = [](ffi::wbcffi_module* obj) { ((CFFI*)obj)->dp.emit(); }, + }; + + // Call init + cffi_instance_ = hooks_.init(&init_info, config_entries.data(), config_entries.size()); + + // Handle init failures + if (cffi_instance_ == nullptr) { + throw std::runtime_error{"Failed to initialize C ABI module"}; + } +} + +CFFI::~CFFI() { + if (cffi_instance_ != nullptr) { + hooks_.deinit(cffi_instance_); + } +} + +auto CFFI::update() -> void { + assert(cffi_instance_ != nullptr); + hooks_.update(cffi_instance_); + + // Execute the on-update command set in config + AModule::update(); +} + +auto CFFI::refresh(int signal) -> void { + assert(cffi_instance_ != nullptr); + hooks_.refresh(cffi_instance_, signal); +} + +auto CFFI::doAction(const std::string& name) -> void { + assert(cffi_instance_ != nullptr); + if (!name.empty()) { + hooks_.doAction(cffi_instance_, name.c_str()); + } +} + +} // namespace waybar::modules diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 3d6b8919..b54a360f 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -1,325 +1,293 @@ #include "modules/clock.hpp" -#include #include -#include +#include #include #include #include -#include #include "util/ustring_clen.hpp" + #ifdef HAVE_LANGINFO_1STDAY #include #include #endif +namespace fmt_lib = waybar::util::date::format; + waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true), - current_time_zone_idx_{0}, - is_calendar_in_tooltip_{false}, - is_timezoned_list_in_tooltip_{false} { + locale_{std::locale(config_["locale"].isString() ? config_["locale"].asString() : "")}, + tlpFmt_{(config_["tooltip-format"].isString()) ? config_["tooltip-format"].asString() : ""}, + cldInTooltip_{tlpFmt_.find("{" + kCldPlaceholder + "}") != std::string::npos}, + tzInTooltip_{tlpFmt_.find("{" + kTZPlaceholder + "}") != std::string::npos}, + tzCurrIdx_{0}, + ordInTooltip_{tlpFmt_.find("{" + kOrdPlaceholder + "}") != std::string::npos} { + tlpText_ = tlpFmt_; + if (config_["timezones"].isArray() && !config_["timezones"].empty()) { for (const auto& zone_name : config_["timezones"]) { if (!zone_name.isString()) continue; if (zone_name.asString().empty()) // local time should be shown - time_zones_.push_back(date::current_zone()); + tzList_.push_back(nullptr); else try { - time_zones_.push_back(date::locate_zone(zone_name.asString())); + tzList_.push_back(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()) { if (config_["timezone"].asString().empty()) - time_zones_.push_back(date::current_zone()); + // local time should be shown + tzList_.push_back(nullptr); else try { - time_zones_.push_back(date::locate_zone(config_["timezone"].asString())); + tzList_.push_back(locate_zone(config_["timezone"].asString())); } catch (const std::exception& e) { spdlog::warn("Timezone: {0}. {1}", config_["timezone"].asString(), e.what()); } } + if (!tzList_.size()) tzList_.push_back(nullptr); - // If all timezones are parsed and no one is good - if (!time_zones_.size()) { - time_zones_.push_back(date::current_zone()); - } - - // Check if a particular placeholder is present in the tooltip format, to know what to calculate - // on update. - if (config_["tooltip-format"].isString()) { - std::string trimmed_format{config_["tooltip-format"].asString()}; - fmtMap_.insert({5, trimmed_format}); - trimmed_format.erase(std::remove_if(trimmed_format.begin(), trimmed_format.end(), - [](unsigned char x) { return std::isspace(x); }), - trimmed_format.end()); - - if (trimmed_format.find("{" + kCalendarPlaceholder + "}") != std::string::npos) { - is_calendar_in_tooltip_ = true; - } - if (trimmed_format.find("{" + KTimezonedTimeListPlaceholder + "}") != std::string::npos) { - is_timezoned_list_in_tooltip_ = true; - } - } - - // Calendar configuration - if (is_calendar_in_tooltip_) { - 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}}; + // Calendar properties + if (cldInTooltip_) { + if (config_[kCldPlaceholder]["mode"].isString()) { + const std::string cfgMode{config_[kCldPlaceholder]["mode"].asString()}; + const std::map monthModes{{"month", CldMode::MONTH}, + {"year", CldMode::YEAR}}; 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", + "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; + if (config_[kCldPlaceholder]["weeks-pos"].isString()) { + if (config_[kCldPlaceholder]["weeks-pos"].asString() == "left") cldWPos_ = WS::LEFT; + if (config_[kCldPlaceholder]["weeks-pos"].asString() == "right") cldWPos_ = WS::RIGHT; + } + if (config_[kCldPlaceholder]["format"]["months"].isString()) + fmtMap_.insert({0, config_[kCldPlaceholder]["format"]["months"].asString()}); + else + fmtMap_.insert({0, "{}"}); + if (config_[kCldPlaceholder]["format"]["weekdays"].isString()) + fmtMap_.insert({1, config_[kCldPlaceholder]["format"]["weekdays"].asString()}); + else + fmtMap_.insert({1, "{}"}); + + if (config_[kCldPlaceholder]["format"]["days"].isString()) + fmtMap_.insert({2, config_[kCldPlaceholder]["format"]["days"].asString()}); + else + fmtMap_.insert({2, "{}"}); + if (config_[kCldPlaceholder]["format"]["today"].isString()) { + fmtMap_.insert({3, config_[kCldPlaceholder]["format"]["today"].asString()}); + cldBaseDay_ = + year_month_day{ + floor(zoned_time{current_zone(), system_clock::now()}.get_local_time())} + .day(); + } else + fmtMap_.insert({3, "{}"}); + if (config_[kCldPlaceholder]["format"]["weeks"].isString() && cldWPos_ != WS::HIDDEN) { + fmtMap_.insert({4, std::regex_replace(config_[kCldPlaceholder]["format"]["weeks"].asString(), + std::regex("\\{\\}"), + (first_day_of_week() == Monday) ? "{:%W}" : "{:%U}")}); + Glib::ustring tmp{std::regex_replace(fmtMap_[4], std::regex("]+>|\\{.*\\}"), "")}; + cldWnLen_ += tmp.size(); + } else { + if (cldWPos_ != WS::HIDDEN) + fmtMap_.insert({4, (first_day_of_week() == Monday) ? "{:%W}" : "{:%U}"}); + else + cldWnLen_ = 0; + } + if (config_[kCldPlaceholder]["mode-mon-col"].isInt()) { + cldMonCols_ = config_[kCldPlaceholder]["mode-mon-col"].asInt(); + if (cldMonCols_ == 0u || (12 % cldMonCols_) != 0u) { spdlog::warn( - "Clock calendar configuration \"mode-mon-col\" = {0} must be one of [1, 2, 3, 4, 6, " - "12]. Value 3 is using instead", + "Clock calendar configuration mode-mon-col = {0} must be one of [1, 2, 3, 4, 6, 12]. " + "Value 3 is using instead", cldMonCols_); + cldMonCols_ = 3u; } } else cldMonCols_ = 1; - if (config_[kCalendarPlaceholder]["on-scroll"].isInt()) { - cldShift_ = date::months{config_[kCalendarPlaceholder]["on-scroll"].asInt()}; + if (config_[kCldPlaceholder]["on-scroll"].isInt()) { event_box_.add_events(Gdk::LEAVE_NOTIFY_MASK); event_box_.signal_leave_notify_event().connect([this](GdkEventCrossing*) { - cldCurrShift_ = date::months{0}; + cldCurrShift_ = months{0}; return false; }); } } - if (config_["locale"].isString()) - locale_ = std::locale(config_["locale"].asString()); - else - locale_ = std::locale(""); - thread_ = [this] { dp.emit(); - auto now = std::chrono::system_clock::now(); - /* difference with projected wakeup time */ - auto diff = now.time_since_epoch() % interval_; - /* sleep until the next projected time */ - thread_.sleep_for(interval_ - diff); + thread_.sleep_for(interval_ - system_clock::now().time_since_epoch() % interval_); }; } -const date::time_zone* waybar::modules::Clock::current_timezone() { - return time_zones_[current_time_zone_idx_]; -} - auto waybar::modules::Clock::update() -> void { - const auto* tz{current_timezone()}; - const date::zoned_time now{ - tz, - date::floor( - std::chrono::system_clock::now())}; // Define local time is based on provided time zone - const date::year_month_day today{ - date::floor(now.get_local_time())}; // Convert now to year_month_day - const date::year_month_day shiftedDay{today + cldCurrShift_}; // Shift today - // Define shift local time - const auto shiftedNow{date::make_zoned( - tz, date::local_days(shiftedDay) + - (now.get_local_time() - date::floor(now.get_local_time())))}; + const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : current_zone(); + const zoned_time now{tz, floor(system_clock::now())}; - label_.set_markup(fmt::format(locale_, fmt::runtime(format_), now)); + label_.set_markup(fmt_lib::vformat(locale_, format_, fmt_lib::make_format_args(now))); if (tooltipEnabled()) { - const std::string tz_text{(is_timezoned_list_in_tooltip_) ? timezones_text(now.get_sys_time()) - : ""}; - const std::string cld_text{(is_calendar_in_tooltip_) ? get_calendar(today, shiftedDay, tz) - : ""}; + const year_month_day today{floor(now.get_local_time())}; + const auto shiftedDay{today + cldCurrShift_}; + const zoned_time shiftedNow{ + tz, local_days(shiftedDay) + (now.get_local_time() - floor(now.get_local_time()))}; - const std::string text{fmt::format(locale_, fmt::runtime(fmtMap_[5]), shiftedNow, - fmt::arg(KTimezonedTimeListPlaceholder.c_str(), tz_text), - fmt::arg(kCalendarPlaceholder.c_str(), cld_text))}; - label_.set_tooltip_markup(text); + if (tzInTooltip_) tzText_ = getTZtext(now.get_sys_time()); + if (cldInTooltip_) cldText_ = get_calendar(today, shiftedDay, tz); + if (ordInTooltip_) ordText_ = get_ordinal_date(shiftedDay); + if (tzInTooltip_ || cldInTooltip_ || ordInTooltip_) { + // std::vformat doesn't support named arguments. + tlpText_ = std::regex_replace(tlpFmt_, std::regex("\\{" + kTZPlaceholder + "\\}"), tzText_); + tlpText_ = + std::regex_replace(tlpText_, std::regex("\\{" + kCldPlaceholder + "\\}"), cldText_); + tlpText_ = + std::regex_replace(tlpText_, std::regex("\\{" + kOrdPlaceholder + "\\}"), ordText_); + } + + tlpText_ = fmt_lib::vformat(locale_, tlpText_, fmt_lib::make_format_args(shiftedNow)); + + label_.set_tooltip_markup(tlpText_); } - // Call parent update ALabel::update(); } -auto waybar::modules::Clock::doAction(const std::string& name) -> void { - if ((actionMap_[name])) { - (this->*actionMap_[name])(); - update(); - } else - spdlog::error("Clock. Unsupported action \"{0}\"", name); +auto waybar::modules::Clock::getTZtext(sys_seconds now) -> std::string { + if (tzList_.size() == 1) return ""; + + std::stringstream os; + for (size_t tz_idx{0}; tz_idx < tzList_.size(); ++tz_idx) { + if (static_cast(tz_idx) == tzCurrIdx_) continue; + const auto* tz = tzList_[tz_idx] != nullptr ? tzList_[tz_idx] : current_zone(); + auto zt{zoned_time{tz, now}}; + os << fmt_lib::vformat(locale_, format_, fmt_lib::make_format_args(zt)) << '\n'; + } + + return os.str(); } -// The number of weeks in calendar month layout plus 1 more for calendar titles -const unsigned cldRowsInMonth(const date::year_month& ym, const date::weekday& firstdow) { - using namespace date; - return static_cast( - ceil((weekday{ym / 1} - firstdow) + ((ym / last).day() - day{0})).count()) + - 2; +const unsigned cldRowsInMonth(const year_month& ym, const weekday& firstdow) { + return 2u + ceil((weekday{ym / 1} - firstdow) + ((ym / last).day() - day{0})).count(); } -auto cldGetWeekForLine(const date::year_month& ym, const date::weekday& 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 cldGetWeekForLine(const year_month& ym, const weekday& firstdow, const unsigned line) + -> const year_month_weekday { + unsigned index{line - 2}; + if (weekday{ym / 1} == firstdow) ++index; + return ym / firstdow[index]; } -auto getCalendarLine(const date::year_month_day& currDate, const date::year_month ym, - const unsigned line, const date::weekday& firstdow, - const std::locale* const locale_) -> std::string { - using namespace date::literals; - std::ostringstream res; +auto getCalendarLine(const year_month_day& currDate, const year_month ym, const unsigned line, + const weekday& firstdow, const std::locale* const locale_) -> std::string { + std::ostringstream os; switch (line) { + // Print month and year title case 0: { - // Output month and year title - res << date::format(*locale_, "%B %Y", ym); + os << date::format(*locale_, "{:L%B %Y}", ym); break; } + // Print weekday names title case 1: { - // Output weekday names title auto wd{firstdow}; + Glib::ustring wdStr; + Glib::ustring::size_type wdLen{0}; + int clen{0}; do { - Glib::ustring wd_ustring{date::format(*locale_, "%a", wd)}; - auto clen{ustring_clen(wd_ustring)}; - auto wd_len{wd_ustring.length()}; + wdStr = date::format(*locale_, "{:L%a}", wd); + clen = ustring_clen(wdStr); + wdLen = wdStr.length(); while (clen > 2) { - wd_ustring = wd_ustring.substr(0, wd_len - 1); - --wd_len; - clen = ustring_clen(wd_ustring); + wdStr = wdStr.substr(0, wdLen - 1); + --wdLen; + clen = ustring_clen(wdStr); } const std::string pad(2 - clen, ' '); - if (wd != firstdow) res << ' '; + if (wd != firstdow) os << ' '; - res << pad << wd_ustring; + os << pad << wdStr; } while (++wd != firstdow); break; } + // Print first week prefixed with spaces if necessary 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, ' '); + auto d{day{1}}; + auto wd{weekday{ym / 1}}; + os << std::string((wd - firstdow).count() * 3, ' '); - if (currDate.year() != ym.year() || currDate.month() != ym.month() || currDate != ym / 1_d) - res << date::format("%e", 1_d); + if (currDate != ym / d) + os << date::format(*locale_, "{:L%e}", d); else - res << "{today}"; - - auto d = 2_d; + os << "{today}"; while (++wd != firstdow) { - if (currDate.year() != ym.year() || currDate.month() != ym.month() || currDate != ym / d) - res << date::format(" %e", d); - else - res << " {today}"; - ++d; + + if (currDate != ym / d) + os << date::format(*locale_, " {:L%e}", d); + else + os << " {today}"; } break; } + // Print non-first week 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; + auto ymdTmp{cldGetWeekForLine(ym, firstdow, line)}; + if (ymdTmp.ok()) { + auto d{year_month_day{ymdTmp}.day()}; + const auto dlast{(ym / last).day()}; + auto wd{firstdow}; - if (currDate.year() != ym.year() || currDate.month() != ym.month() || currDate != ym / d) - res << date::format("%e", d); + if (currDate != ym / d) + os << date::format(*locale_, "{:L%e}", d); else - res << "{today}"; + os << "{today}"; - while (++wd != firstdow && ++d <= e) { - if (currDate.year() != ym.year() || currDate.month() != ym.month() || currDate != ym / d) - res << date::format(" %e", d); + while (++wd != firstdow && ++d <= dlast) { + if (currDate != ym / d) + os << date::format(*locale_, " {:L%e}", d); else - res << " {today}"; + os << " {today}"; } - // Append row with spaces if the week did not complete - res << std::string(static_cast((firstdow - wd).count()) * 3, ' '); + // Append row with spaces if the week was not completed + os << std::string((firstdow - wd).count() * 3, ' '); } break; } } - return res.str(); + return os.str(); } -auto waybar::modules::Clock::get_calendar(const date::year_month_day& today, - const date::year_month_day& ymd, - const date::time_zone* tz) -> const std::string { +auto waybar::modules::Clock::get_calendar(const year_month_day& today, const year_month_day& ymd, + const time_zone* tz) -> const std::string { + const auto firstdow{first_day_of_week()}; + const auto maxRows{12 / cldMonCols_}; 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; if (cldMode_ == CldMode::YEAR) { - if (y / date::month{1} / 1 == cldYearShift_) + if (y / month{1} / 1 == cldYearShift_) if (d == cldBaseDay_ || (uint)cldBaseDay_ == 0u) return cldYearCached_; else cldBaseDay_ = d; else - cldYearShift_ = y / date::month{1} / 1; + cldYearShift_ = y / month{1} / 1; } if (cldMode_ == CldMode::MONTH) { if (ym == cldMonShift_) @@ -330,67 +298,75 @@ auto waybar::modules::Clock::get_calendar(const date::year_month_day& today, else cldMonShift_ = ym; } - + // Pad object + const std::string pads(cldWnLen_, ' '); // 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); + m = cldRowsInMonth(y / 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_)); + 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}}; + const auto mon{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 << " "; + const year_month ymTmp{y / mon}; + if (col != 0 && cldMode_ == CldMode::YEAR) os << std::string(3, ' '); // Week numbers on the left - if (cldWPos_ == WeeksSide::LEFT && line > 0) { + if (cldWPos_ == WS::LEFT && line > 0) { if (line > 1) { - if (line < ml[static_cast(ymTmp.month()) - 1u]) - os << fmt::format(fmt::runtime(fmtMap_[4]), - (line == 2) - ? date::zoned_seconds{tz, date::local_days{ymTmp / 1}} - : date::zoned_seconds{tz, date::local_days{cldGetWeekForLine( - ymTmp, firstdow, line)}}) + if (line < ml[(unsigned)ymTmp.month() - 1u]) { + os << fmt_lib::vformat( + locale_, fmtMap_[4], + fmt_lib::make_format_args( + (line == 2) + ? static_cast( + zoned_seconds{tz, local_days{ymTmp / 1}}) + : static_cast(zoned_seconds{ + tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}}))) << ' '; - else - os << std::string(cldWnLen_, ' '); + } else + os << pads; } } - os << fmt::format( - fmt::runtime((cldWPos_ != WeeksSide::LEFT || line == 0) ? "{:<{}}" : "{:>{}}"), - getCalendarLine(today, ymTmp, line, firstdow, &locale_), - (cldMonColLen_ + ((line < 2) ? cldWnLen_ : 0))); + os << Glib::ustring::format((cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, + std::setfill(L' '), + std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ : 0)), + getCalendarLine(today, ymTmp, line, firstdow, &locale_)); // Week numbers on the right - if (cldWPos_ == WeeksSide ::RIGHT && line > 0) { + if (cldWPos_ == WS::RIGHT && line > 0) { if (line > 1) { - if (line < ml[static_cast(ymTmp.month()) - 1u]) + if (line < ml[(unsigned)ymTmp.month() - 1u]) os << ' ' - << fmt::format(fmt::runtime(fmtMap_[4]), - (line == 2) - ? date::zoned_seconds{tz, date::local_days{ymTmp / 1}} - : date::zoned_seconds{tz, date::local_days{cldGetWeekForLine( - ymTmp, firstdow, line)}}); + << fmt_lib::vformat( + locale_, fmtMap_[4], + fmt_lib::make_format_args( + (line == 2) ? static_cast( + zoned_seconds{tz, local_days{ymTmp / 1}}) + : static_cast( + zoned_seconds{tz, local_days{cldGetWeekForLine( + ymTmp, firstdow, line)}}))); else - os << std::string(cldWnLen_, ' '); + os << pads; } } } } - - // Apply user formats to calendar + // Apply user's formats if (line < 2) - tmp << fmt::format(fmt::runtime(fmtMap_[line]), os.str()); + tmp << fmt_lib::vformat( + locale_, fmtMap_[line], + fmt_lib::make_format_args(static_cast(os.str()))); else tmp << os.str(); // Clear ostringstream @@ -400,10 +376,13 @@ auto waybar::modules::Clock::get_calendar(const date::year_month_day& today, 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", d)))); + os << std::regex_replace( + fmt_lib::vformat(locale_, fmtMap_[2], + fmt_lib::make_format_args(static_cast(tmp.str()))), + std::regex("\\{today\\}"), + fmt_lib::vformat(locale_, fmtMap_[3], + fmt_lib::make_format_args( + static_cast(date::format("{:L%e}", d))))); if (cldMode_ == CldMode::YEAR) cldYearCached_ = os.str(); @@ -413,50 +392,34 @@ auto waybar::modules::Clock::get_calendar(const date::year_month_day& today, return os.str(); } -/*Clock actions*/ +// Actions handler +auto waybar::modules::Clock::doAction(const std::string& name) -> void { + if (actionMap_[name]) { + (this->*actionMap_[name])(); + } else + spdlog::error("Clock. Unsupported action \"{0}\"", name); +} + +// Module 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_; + cldCurrShift_ += (months)((cldMode_ == CldMode::YEAR) ? 12 : 1); } void waybar::modules::Clock::cldShift_down() { - cldCurrShift_ -= ((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_; + cldCurrShift_ -= (months)((cldMode_ == CldMode::YEAR) ? 12 : 1); } 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; + const auto tzSize{tzList_.size()}; + if (tzSize == 1) return; + size_t newIdx{tzCurrIdx_ + 1lu}; + tzCurrIdx_ = (newIdx == tzSize) ? 0 : newIdx; } 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) - -> std::string { - if (time_zones_.size() == 1) { - return ""; - } - std::stringstream os; - 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; - } - const date::time_zone* timezone = time_zones_[time_zone_idx]; - if (!timezone) { - timezone = date::current_zone(); - } - auto ztime = date::zoned_time{timezone, date::floor(now)}; - os << fmt::format(locale_, fmt::runtime(format_), ztime) << '\n'; - } - return os.str(); + const auto tzSize{tzList_.size()}; + if (tzSize == 1) return; + tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1; } #ifdef HAVE_LANGINFO_1STDAY @@ -468,17 +431,41 @@ using deleting_unique_ptr = std::unique_ptr>; #endif // Computations done similarly to Linux cal utility. -auto waybar::modules::Clock::first_day_of_week() -> date::weekday { +auto waybar::modules::Clock::first_day_of_week() -> weekday { #ifdef HAVE_LANGINFO_1STDAY deleting_unique_ptr::type, freelocale> posix_locale{ newlocale(LC_ALL, locale_.name().c_str(), nullptr)}; if (posix_locale) { - const int i = (std::intptr_t)nl_langinfo_l(_NL_TIME_WEEK_1STDAY, posix_locale.get()); - auto ymd = date::year(i / 10000) / (i / 100 % 100) / (i % 100); - auto wd = date::weekday(ymd); - uint8_t j = *nl_langinfo_l(_NL_TIME_FIRST_WEEKDAY, posix_locale.get()); - return wd + date::days(j - 1); + const auto i{(int)((std::intptr_t)nl_langinfo_l(_NL_TIME_WEEK_1STDAY, posix_locale.get()))}; + const weekday wd{year_month_day{year(i / 10000) / month(i / 100 % 100) / day(i % 100)}}; + const auto j{(uint8_t)*nl_langinfo_l(_NL_TIME_FIRST_WEEKDAY, posix_locale.get())}; + return wd + days{j - 1}; } #endif - return date::Sunday; + return Sunday; } + +auto waybar::modules::Clock::get_ordinal_date(const year_month_day& today) -> std::string { + auto day = static_cast(today.day()); + std::stringstream res; + res << day; + if (day >= 11 && day <= 13) { + res << "th"; + return res.str(); + } + + switch (day % 10) { + case 1: + res << "st"; + break; + case 2: + res << "nd"; + break; + case 3: + res << "rd"; + break; + default: + res << "th"; + } + return res.str(); +} \ No newline at end of file diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp new file mode 100644 index 00000000..0703eaf7 --- /dev/null +++ b/src/modules/cpu.cpp @@ -0,0 +1,63 @@ +#include "modules/cpu.hpp" + +#include "modules/cpu_frequency.hpp" +#include "modules/cpu_usage.hpp" +#include "modules/load.hpp" + +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + +waybar::modules::Cpu::Cpu(const std::string& id, const Json::Value& config) + : ALabel(config, "cpu", id, "{usage}%", 10) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; +} + +auto waybar::modules::Cpu::update() -> void { + // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both + auto [load1, load5, load15] = Load::getLoad(); + auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); + auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); + if (tooltipEnabled()) { + label_.set_tooltip_text(tooltip); + } + auto format = format_; + auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; + auto state = getState(total_usage); + if (!state.empty() && config_["format-" + state].isString()) { + format = config_["format-" + state].asString(); + } + + if (format.empty()) { + event_box_.hide(); + } else { + event_box_.show(); + auto icons = std::vector{state}; + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("load", load1)); + store.push_back(fmt::arg("usage", total_usage)); + store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); + store.push_back(fmt::arg("max_frequency", max_frequency)); + store.push_back(fmt::arg("min_frequency", min_frequency)); + store.push_back(fmt::arg("avg_frequency", avg_frequency)); + for (size_t i = 1; i < cpu_usage.size(); ++i) { + auto core_i = i - 1; + auto core_format = fmt::format("usage{}", core_i); + store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i])); + auto icon_format = fmt::format("icon{}", core_i); + store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); + } + label_.set_markup(fmt::vformat(format, store)); + } + + // Call parent update + ALabel::update(); +} diff --git a/src/modules/cpu_frequency/bsd.cpp b/src/modules/cpu_frequency/bsd.cpp new file mode 100644 index 00000000..743fb288 --- /dev/null +++ b/src/modules/cpu_frequency/bsd.cpp @@ -0,0 +1,27 @@ +#include +#include + +#include "modules/cpu_frequency.hpp" + +std::vector waybar::modules::CpuFrequency::parseCpuFrequencies() { + std::vector frequencies; + char buffer[256]; + size_t len; + int32_t freq; + uint32_t i = 0; + + while (true) { + len = 4; + snprintf(buffer, 256, "dev.cpu.%u.freq", i); + if (sysctlbyname(buffer, &freq, &len, NULL, 0) == -1 || len <= 0) break; + frequencies.push_back(freq); + ++i; + } + + if (frequencies.empty()) { + spdlog::warn("cpu/bsd: parseCpuFrequencies failed, not found in sysctl"); + frequencies.push_back(NAN); + } + + return frequencies; +} diff --git a/src/modules/cpu_frequency/common.cpp b/src/modules/cpu_frequency/common.cpp new file mode 100644 index 00000000..e47364ba --- /dev/null +++ b/src/modules/cpu_frequency/common.cpp @@ -0,0 +1,67 @@ +#include "modules/cpu_frequency.hpp" + +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + +waybar::modules::CpuFrequency::CpuFrequency(const std::string& id, const Json::Value& config) + : ALabel(config, "cpu_frequency", id, "{avg_frequency}", 10) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; +} + +auto waybar::modules::CpuFrequency::update() -> void { + // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both + auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); + if (tooltipEnabled()) { + auto tooltip = + fmt::format("Minimum frequency: {}\nAverage frequency: {}\nMaximum frequency: {}\n", + min_frequency, avg_frequency, max_frequency); + label_.set_tooltip_text(tooltip); + } + auto format = format_; + auto state = getState(avg_frequency); + if (!state.empty() && config_["format-" + state].isString()) { + format = config_["format-" + state].asString(); + } + + if (format.empty()) { + event_box_.hide(); + } else { + event_box_.show(); + auto icons = std::vector{state}; + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("icon", getIcon(avg_frequency, icons))); + store.push_back(fmt::arg("max_frequency", max_frequency)); + store.push_back(fmt::arg("min_frequency", min_frequency)); + store.push_back(fmt::arg("avg_frequency", avg_frequency)); + label_.set_markup(fmt::vformat(format, store)); + } + + // Call parent update + ALabel::update(); +} + +std::tuple waybar::modules::CpuFrequency::getCpuFrequency() { + std::vector frequencies = CpuFrequency::parseCpuFrequencies(); + if (frequencies.empty()) { + return {0.f, 0.f, 0.f}; + } + auto [min, max] = std::minmax_element(std::begin(frequencies), std::end(frequencies)); + float avg_frequency = + std::accumulate(std::begin(frequencies), std::end(frequencies), 0.0) / frequencies.size(); + + // Round frequencies with double decimal precision to get GHz + float max_frequency = std::ceil(*max / 10.0) / 100.0; + float min_frequency = std::ceil(*min / 10.0) / 100.0; + avg_frequency = std::ceil(avg_frequency / 10.0) / 100.0; + + return {max_frequency, min_frequency, avg_frequency}; +} diff --git a/src/modules/cpu/linux.cpp b/src/modules/cpu_frequency/linux.cpp similarity index 61% rename from src/modules/cpu/linux.cpp rename to src/modules/cpu_frequency/linux.cpp index e9b18d70..1f368789 100644 --- a/src/modules/cpu/linux.cpp +++ b/src/modules/cpu_frequency/linux.cpp @@ -1,36 +1,8 @@ #include -#include "modules/cpu.hpp" +#include "modules/cpu_frequency.hpp" -std::vector> waybar::modules::Cpu::parseCpuinfo() { - const std::string data_dir_ = "/proc/stat"; - std::ifstream info(data_dir_); - if (!info.is_open()) { - throw std::runtime_error("Can't open " + data_dir_); - } - std::vector> cpuinfo; - std::string line; - while (getline(info, line)) { - if (line.substr(0, 3).compare("cpu") != 0) { - break; - } - std::stringstream sline(line.substr(5)); - std::vector times; - for (size_t time = 0; sline >> time; times.push_back(time)) - ; - - size_t idle_time = 0; - size_t total_time = 0; - if (times.size() >= 4) { - idle_time = times[3]; - total_time = std::accumulate(times.begin(), times.end(), 0); - } - cpuinfo.emplace_back(idle_time, total_time); - } - return cpuinfo; -} - -std::vector waybar::modules::Cpu::parseCpuFrequencies() { +std::vector waybar::modules::CpuFrequency::parseCpuFrequencies() { const std::string file_path_ = "/proc/cpuinfo"; std::ifstream info(file_path_); if (!info.is_open()) { diff --git a/src/modules/cpu/bsd.cpp b/src/modules/cpu_usage/bsd.cpp similarity index 86% rename from src/modules/cpu/bsd.cpp rename to src/modules/cpu_usage/bsd.cpp index 5eb767d9..d795a817 100644 --- a/src/modules/cpu/bsd.cpp +++ b/src/modules/cpu_usage/bsd.cpp @@ -8,7 +8,8 @@ #include // NAN #include // malloc -#include "modules/cpu.hpp" +#include "modules/cpu_usage.hpp" +#include "util/scope_guard.hpp" #if defined(__NetBSD__) || defined(__OpenBSD__) #include @@ -27,12 +28,17 @@ typedef uint64_t pcp_time_t; typedef long pcp_time_t; #endif -std::vector> waybar::modules::Cpu::parseCpuinfo() { +std::vector> waybar::modules::CpuUsage::parseCpuinfo() { cp_time_t sum_cp_time[CPUSTATES]; size_t sum_sz = sizeof(sum_cp_time); int ncpu = sysconf(_SC_NPROCESSORS_CONF); size_t sz = CPUSTATES * (ncpu + 1) * sizeof(pcp_time_t); pcp_time_t *cp_time = static_cast(malloc(sz)), *pcp_time = cp_time; + waybar::util::ScopeGuard cp_time_deleter([cp_time]() { + if (cp_time) { + free(cp_time); + } + }); #if defined(__NetBSD__) int mib[] = { CTL_KERN, @@ -97,16 +103,5 @@ std::vector> waybar::modules::Cpu::parseCpuinfo() { } cpuinfo.emplace_back(single_cp_time[CP_IDLE], total); } - free(cp_time); return cpuinfo; } - -std::vector waybar::modules::Cpu::parseCpuFrequencies() { - static std::vector frequencies; - if (frequencies.empty()) { - spdlog::warn( - "cpu/bsd: parseCpuFrequencies is not implemented, expect garbage in {*_frequency}"); - frequencies.push_back(NAN); - } - return frequencies; -} diff --git a/src/modules/cpu/common.cpp b/src/modules/cpu_usage/common.cpp similarity index 55% rename from src/modules/cpu/common.cpp rename to src/modules/cpu_usage/common.cpp index 8fedf842..4e36f48e 100644 --- a/src/modules/cpu/common.cpp +++ b/src/modules/cpu_usage/common.cpp @@ -1,4 +1,4 @@ -#include "modules/cpu.hpp" +#include "modules/cpu_usage.hpp" // In the 80000 version of fmt library authors decided to optimize imports // and moved declarations required for fmt::dynamic_format_arg_store in new @@ -9,19 +9,17 @@ #include #endif -waybar::modules::Cpu::Cpu(const std::string& id, const Json::Value& config) - : ALabel(config, "cpu", id, "{usage}%", 10) { +waybar::modules::CpuUsage::CpuUsage(const std::string& id, const Json::Value& config) + : ALabel(config, "cpu_usage", id, "{usage}%", 10) { thread_ = [this] { dp.emit(); thread_.sleep_for(interval_); }; } -auto waybar::modules::Cpu::update() -> void { +auto waybar::modules::CpuUsage::update() -> void { // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both - auto cpu_load = getCpuLoad(); - auto [cpu_usage, tooltip] = getCpuUsage(); - auto [max_frequency, min_frequency, avg_frequency] = getCpuFrequency(); + auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); if (tooltipEnabled()) { label_.set_tooltip_text(tooltip); } @@ -38,12 +36,8 @@ auto waybar::modules::Cpu::update() -> void { event_box_.show(); auto icons = std::vector{state}; fmt::dynamic_format_arg_store store; - store.push_back(fmt::arg("load", cpu_load)); store.push_back(fmt::arg("usage", total_usage)); store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); - store.push_back(fmt::arg("max_frequency", max_frequency)); - store.push_back(fmt::arg("min_frequency", min_frequency)); - store.push_back(fmt::arg("avg_frequency", avg_frequency)); for (size_t i = 1; i < cpu_usage.size(); ++i) { auto core_i = i - 1; auto core_format = fmt::format("usage{}", core_i); @@ -58,25 +52,18 @@ auto waybar::modules::Cpu::update() -> void { ALabel::update(); } -double waybar::modules::Cpu::getCpuLoad() { - double load[1]; - if (getloadavg(load, 1) != -1) { - return std::ceil(load[0] * 100.0) / 100.0; - } - throw std::runtime_error("Can't get Cpu load"); -} - -std::tuple, std::string> waybar::modules::Cpu::getCpuUsage() { - if (prev_times_.empty()) { - prev_times_ = parseCpuinfo(); +std::tuple, std::string> waybar::modules::CpuUsage::getCpuUsage( + std::vector>& prev_times) { + if (prev_times.empty()) { + prev_times = CpuUsage::parseCpuinfo(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); } - std::vector> curr_times = parseCpuinfo(); + std::vector> curr_times = CpuUsage::parseCpuinfo(); std::string tooltip; std::vector usage; for (size_t i = 0; i < curr_times.size(); ++i) { auto [curr_idle, curr_total] = curr_times[i]; - auto [prev_idle, prev_total] = prev_times_[i]; + auto [prev_idle, prev_total] = prev_times[i]; const float delta_idle = curr_idle - prev_idle; const float delta_total = curr_total - prev_total; uint16_t tmp = 100 * (1 - delta_idle / delta_total); @@ -87,23 +74,6 @@ std::tuple, std::string> waybar::modules::Cpu::getCpuUsage } usage.push_back(tmp); } - prev_times_ = curr_times; + prev_times = curr_times; return {usage, tooltip}; } - -std::tuple waybar::modules::Cpu::getCpuFrequency() { - std::vector frequencies = parseCpuFrequencies(); - if (frequencies.empty()) { - return {0.f, 0.f, 0.f}; - } - auto [min, max] = std::minmax_element(std::begin(frequencies), std::end(frequencies)); - float avg_frequency = - std::accumulate(std::begin(frequencies), std::end(frequencies), 0.0) / frequencies.size(); - - // Round frequencies with double decimal precision to get GHz - float max_frequency = std::ceil(*max / 10.0) / 100.0; - float min_frequency = std::ceil(*min / 10.0) / 100.0; - avg_frequency = std::ceil(avg_frequency / 10.0) / 100.0; - - return {max_frequency, min_frequency, avg_frequency}; -} diff --git a/src/modules/cpu_usage/linux.cpp b/src/modules/cpu_usage/linux.cpp new file mode 100644 index 00000000..bcd9594e --- /dev/null +++ b/src/modules/cpu_usage/linux.cpp @@ -0,0 +1,32 @@ +#include + +#include "modules/cpu_usage.hpp" + +std::vector> waybar::modules::CpuUsage::parseCpuinfo() { + const std::string data_dir_ = "/proc/stat"; + std::ifstream info(data_dir_); + if (!info.is_open()) { + throw std::runtime_error("Can't open " + data_dir_); + } + std::vector> cpuinfo; + std::string line; + while (getline(info, line)) { + if (line.substr(0, 3).compare("cpu") != 0) { + break; + } + std::stringstream sline(line.substr(5)); + std::vector times; + for (size_t time = 0; sline >> time; times.push_back(time)) + ; + + size_t idle_time = 0; + size_t total_time = 0; + if (times.size() >= 5) { + // idle + iowait + idle_time = times[3] + times[4]; + total_time = std::accumulate(times.begin(), times.end(), 0); + } + cpuinfo.emplace_back(idle_time, total_time); + } + return cpuinfo; +} diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 5a246aff..5e5d7019 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -2,16 +2,22 @@ #include +#include "util/scope_guard.hpp" + waybar::modules::Custom::Custom(const std::string& name, const std::string& id, - const Json::Value& config) + const Json::Value& config, const std::string& output_name) : ALabel(config, "custom-" + name, id, "{}"), name_(name), + output_name_(output_name), id_(id), percentage_(0), fp_(nullptr), pid_(-1) { dp.emit(); - if (interval_.count() > 0) { + if (!config_["signal"].empty() && config_["interval"].empty() && + config_["restart-interval"].empty()) { + waitingWorker(); + } else if (interval_.count() > 0) { delayWorker(); } else if (config_["exec"].isString()) { continuousWorker(); @@ -38,7 +44,7 @@ void waybar::modules::Custom::delayWorker() { } if (can_update) { if (config_["exec"].isString()) { - output_ = util::command::exec(config_["exec"].asString()); + output_ = util::command::exec(config_["exec"].asString(), output_name_); } dp.emit(); } @@ -49,12 +55,17 @@ void waybar::modules::Custom::delayWorker() { void waybar::modules::Custom::continuousWorker() { auto cmd = config_["exec"].asString(); pid_ = -1; - fp_ = util::command::open(cmd, pid_); + fp_ = util::command::open(cmd, pid_, output_name_); if (!fp_) { throw std::runtime_error("Unable to open " + cmd); } thread_ = [this, cmd] { char* buff = nullptr; + waybar::util::ScopeGuard buff_deleter([buff]() { + if (buff) { + free(buff); + } + }); size_t len = 0; if (getline(&buff, &len, fp_) == -1) { int exit_code = 1; @@ -70,7 +81,7 @@ void waybar::modules::Custom::continuousWorker() { if (config_["restart-interval"].isUInt()) { pid_ = -1; thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt())); - fp_ = util::command::open(cmd, pid_); + fp_ = util::command::open(cmd, pid_, output_name_); if (!fp_) { throw std::runtime_error("Unable to open " + cmd); } @@ -88,7 +99,26 @@ void waybar::modules::Custom::continuousWorker() { output_ = {0, output}; dp.emit(); } - free(buff); + }; +} + +void waybar::modules::Custom::waitingWorker() { + thread_ = [this] { + bool can_update = true; + if (config_["exec-if"].isString()) { + output_ = util::command::execNoRead(config_["exec-if"].asString()); + if (output_.exit_code != 0) { + can_update = false; + dp.emit(); + } + } + if (can_update) { + if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString(), output_name_); + } + dp.emit(); + } + thread_.sleep(); }; } @@ -127,6 +157,7 @@ auto waybar::modules::Custom::update() -> void { } else { parseOutputRaw(); } + auto str = fmt::format(fmt::runtime(format_), text_, fmt::arg("alt", alt_), fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); @@ -139,22 +170,30 @@ auto waybar::modules::Custom::update() -> void { if (label_.get_tooltip_markup() != str) { label_.set_tooltip_markup(str); } + } else if (config_["tooltip-format"].isString()) { + auto tooltip = config_["tooltip-format"].asString(); + tooltip = fmt::format(fmt::runtime(tooltip), text_, fmt::arg("alt", alt_), + fmt::arg("icon", getIcon(percentage_, alt_)), + fmt::arg("percentage", percentage_)); + label_.set_tooltip_markup(tooltip); } else { if (label_.get_tooltip_markup() != tooltip_) { label_.set_tooltip_markup(tooltip_); } } } - auto classes = label_.get_style_context()->list_classes(); + auto style = label_.get_style_context(); + auto classes = style->list_classes(); for (auto const& c : classes) { if (c == id_) continue; - label_.get_style_context()->remove_class(c); + style->remove_class(c); } for (auto const& c : class_) { - label_.get_style_context()->add_class(c); + style->add_class(c); } - label_.get_style_context()->add_class("flat"); - label_.get_style_context()->add_class("text-button"); + style->add_class("flat"); + style->add_class("text-button"); + style->add_class(MODULE_CLASS); event_box_.show(); } } @@ -167,18 +206,23 @@ void waybar::modules::Custom::parseOutputRaw() { std::string line; int i = 0; while (getline(output, line)) { + Glib::ustring validated_line = line; + if (!validated_line.validate()) { + validated_line = validated_line.make_valid(); + } + if (i == 0) { if (config_["escape"].isBool() && config_["escape"].asBool()) { - text_ = Glib::Markup::escape_text(line); + text_ = Glib::Markup::escape_text(validated_line); } else { - text_ = line; + text_ = validated_line; } - tooltip_ = line; + tooltip_ = validated_line; class_.clear(); } else if (i == 1) { - tooltip_ = line; + tooltip_ = validated_line; } else if (i == 2) { - class_.push_back(line); + class_.push_back(validated_line); } else { break; } diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index eb4d902f..ef257b72 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -11,6 +11,9 @@ waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config) if (config["path"].isString()) { path_ = config["path"].asString(); } + if (config["unit"].isString()) { + unit_ = config["unit"].asString(); + } } auto waybar::modules::Disk::update() -> void { @@ -43,6 +46,13 @@ auto waybar::modules::Disk::update() -> void { return; } + float specific_free, specific_used, specific_total, divisor; + + divisor = calc_specific_divisor(unit_); + specific_free = (stats.f_bavail * stats.f_frsize) / divisor; + specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor; + specific_total = (stats.f_blocks * stats.f_frsize) / divisor; + auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true); auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true); auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true); @@ -62,7 +72,8 @@ auto waybar::modules::Disk::update() -> void { 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_))); + fmt::arg("path", path_), fmt::arg("specific_free", specific_free), + fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total))); } if (tooltipEnabled()) { @@ -74,8 +85,31 @@ auto waybar::modules::Disk::update() -> void { 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_))); + fmt::arg("path", path_), fmt::arg("specific_free", specific_free), + fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total))); } // Call parent update ALabel::update(); } + +float waybar::modules::Disk::calc_specific_divisor(std::string divisor) { + if (divisor == "kB") { + return 1000.0; + } else if (divisor == "kiB") { + return 1024.0; + } else if (divisor == "MB") { + return 1000.0 * 1000.0; + } else if (divisor == "MiB") { + return 1024.0 * 1024.0; + } else if (divisor == "GB") { + return 1000.0 * 1000.0 * 1000.0; + } else if (divisor == "GiB") { + return 1024.0 * 1024.0 * 1024.0; + } else if (divisor == "TB") { + return 1000.0 * 1000.0 * 1000.0 * 1000.0; + } else if (divisor == "TiB") { + return 1024.0 * 1024.0 * 1024.0 * 1024.0; + } else { // default to Bytes if it is anything that we don't recongnise + return 1.0; + } +} \ No newline at end of file diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp index 7faa5c52..f36ece1d 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -93,7 +93,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con status_manager_{nullptr}, seat_{nullptr}, bar_(bar), - box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + box_{bar.orientation, 0}, output_status_{nullptr} { struct wl_display *display = Client::inst()->wl_display; struct wl_registry *registry = wl_display_get_registry(display); @@ -113,6 +113,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); // Default to 9 tags, cap at 32 diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 6e586966..05db94ec 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -1,21 +1,16 @@ #include "modules/hyprland/backend.hpp" -#include #include #include #include -#include -#include -#include #include #include #include #include #include -#include -#include #include +#include namespace waybar::modules::hyprland { @@ -24,9 +19,9 @@ void IPC::startIPC() { std::thread([&]() { // check for hyprland - const char* HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + const char* his = getenv("HYPRLAND_INSTANCE_SIGNATURE"); - if (!HIS) { + if (his == nullptr) { spdlog::warn("Hyprland is not running, Hyprland IPC will not be available."); return; } @@ -45,8 +40,8 @@ void IPC::startIPC() { addr.sun_family = AF_UNIX; - // socket path - std::string socketPath = "/tmp/hypr/" + std::string(HIS) + "/.socket2.sock"; + // socket path, specified by EventManager of Hyprland + std::string socketPath = "/tmp/hypr/" + std::string(his) + "/.socket2.sock"; strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); @@ -59,30 +54,22 @@ void IPC::startIPC() { return; } - auto file = fdopen(socketfd, "r"); + auto* file = fdopen(socketfd, "r"); - while (1) { - // read + while (true) { + std::array buffer; // Hyprland socket2 events are max 1024 bytes - char buffer[1024]; // Hyprland socket2 events are max 1024 bytes - auto recievedCharPtr = fgets(buffer, 1024, file); + auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file); - if (!recievedCharPtr) { + if (receivedCharPtr == nullptr) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } - callbackMutex.lock(); - - std::string messageRecieved(buffer); - - messageRecieved = messageRecieved.substr(0, messageRecieved.find_first_of('\n')); - - spdlog::debug("hyprland IPC received {}", messageRecieved); - - parseIPC(messageRecieved); - - callbackMutex.unlock(); + std::string messageReceived(buffer.data()); + messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n')); + spdlog::debug("hyprland IPC received {}", messageReceived); + parseIPC(messageReceived); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } @@ -90,10 +77,10 @@ void IPC::startIPC() { } void IPC::parseIPC(const std::string& ev) { - // todo std::string request = ev.substr(0, ev.find_first_of('>')); + std::unique_lock lock(callbackMutex_); - for (auto& [eventname, handler] : callbacks) { + for (auto& [eventname, handler] : callbacks_) { if (eventname == request) { handler->onEvent(ev); } @@ -101,60 +88,56 @@ void IPC::parseIPC(const std::string& ev) { } void IPC::registerForIPC(const std::string& ev, EventHandler* ev_handler) { - if (!ev_handler) { + if (ev_handler == nullptr) { return; } - callbackMutex.lock(); - callbacks.emplace_back(std::make_pair(ev, ev_handler)); - - callbackMutex.unlock(); + std::unique_lock lock(callbackMutex_); + callbacks_.emplace_back(ev, ev_handler); } void IPC::unregisterForIPC(EventHandler* ev_handler) { - if (!ev_handler) { + if (ev_handler == nullptr) { return; } - callbackMutex.lock(); + std::unique_lock lock(callbackMutex_); - for (auto it = callbacks.begin(); it != callbacks.end();) { - auto it_current = it; - it++; - auto& [eventname, handler] = *it_current; + for (auto it = callbacks_.begin(); it != callbacks_.end();) { + auto& [eventname, handler] = *it; if (handler == ev_handler) { - callbacks.erase(it_current); + callbacks_.erase(it++); + } else { + ++it; } } - - callbackMutex.unlock(); } 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); + struct addrinfo aiHints; + struct addrinfo* aiRes = nullptr; + const auto serverSocket = socket(AF_UNIX, SOCK_STREAM, 0); - if (SERVERSOCKET < 0) { + if (serverSocket < 0) { spdlog::error("Hyprland IPC: Couldn't open a socket (1)"); return ""; } - memset(&ai_hints, 0, sizeof(struct addrinfo)); - ai_hints.ai_family = AF_UNSPEC; - ai_hints.ai_socktype = SOCK_STREAM; + memset(&aiHints, 0, sizeof(struct addrinfo)); + aiHints.ai_family = AF_UNSPEC; + aiHints.ai_socktype = SOCK_STREAM; - if (getaddrinfo("localhost", NULL, &ai_hints, &ai_res) != 0) { + if (getaddrinfo("localhost", nullptr, &aiHints, &aiRes) != 0) { spdlog::error("Hyprland IPC: Couldn't get host (2)"); return ""; } // get the instance signature - auto instanceSig = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + auto* instanceSig = getenv("HYPRLAND_INSTANCE_SIGNATURE"); - if (!instanceSig) { + if (instanceSig == nullptr) { spdlog::error("Hyprland IPC: HYPRLAND_INSTANCE_SIGNATURE was not set! (Is Hyprland running?)"); return ""; } @@ -166,35 +149,41 @@ std::string IPC::getSocket1Reply(const std::string& rq) { std::string socketPath = "/tmp/hypr/" + instanceSigStr + "/.socket.sock"; - strcpy(serverAddress.sun_path, socketPath.c_str()); + // Use snprintf to copy the socketPath string into serverAddress.sun_path + if (snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()) < + 0) { + spdlog::error("Hyprland IPC: Couldn't copy socket path (6)"); + return ""; + } - if (connect(SERVERSOCKET, (sockaddr*)&serverAddress, SUN_LEN(&serverAddress)) < 0) { + if (connect(serverSocket, reinterpret_cast(&serverAddress), sizeof(serverAddress)) < + 0) { spdlog::error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); return ""; } - auto sizeWritten = write(SERVERSOCKET, rq.c_str(), rq.length()); + auto sizeWritten = write(serverSocket, rq.c_str(), rq.length()); if (sizeWritten < 0) { spdlog::error("Hyprland IPC: Couldn't write (4)"); return ""; } - char buffer[8192] = {0}; + std::array buffer = {0}; std::string response; do { - sizeWritten = read(SERVERSOCKET, buffer, 8192); + sizeWritten = read(serverSocket, buffer.data(), 8192); if (sizeWritten < 0) { spdlog::error("Hyprland IPC: Couldn't read (5)"); - close(SERVERSOCKET); + close(serverSocket); return ""; } - response.append(buffer, sizeWritten); + response.append(buffer.data(), sizeWritten); } while (sizeWritten > 0); - close(SERVERSOCKET); + close(serverSocket); return response; } diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 423e2b5c..549faf73 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -4,8 +4,7 @@ #include #include -#include - +#include "util/sanitize_str.hpp" #include "util/string.hpp" namespace waybar::modules::hyprland { @@ -14,7 +13,7 @@ Language::Language(const std::string& id, const Bar& bar, const Json::Value& con : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { modulesReady = true; - if (!gIPC.get()) { + if (!gIPC) { gIPC = std::make_unique(); } @@ -97,18 +96,17 @@ void Language::initLanguage() { spdlog::debug("hyprland language initLanguage found {}", layout_.full_name); dp.emit(); - } catch (std::exception& e) { spdlog::error("hyprland language initLanguage failed with {}", e.what()); } } auto Language::getLayout(const std::string& fullName) -> Layout { - const auto CONTEXT = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); - rxkb_context_parse_default_ruleset(CONTEXT); + auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); + rxkb_context_parse_default_ruleset(context); - rxkb_layout* layout = rxkb_layout_first(CONTEXT); - while (layout) { + rxkb_layout* layout = rxkb_layout_first(context); + while (layout != nullptr) { std::string nameOfLayout = rxkb_layout_get_description(layout); if (nameOfLayout != fullName) { @@ -117,21 +115,20 @@ auto Language::getLayout(const std::string& fullName) -> Layout { } auto name = std::string(rxkb_layout_get_name(layout)); - auto variant_ = rxkb_layout_get_variant(layout); - std::string variant = variant_ == nullptr ? "" : std::string(variant_); + const auto* variantPtr = rxkb_layout_get_variant(layout); + std::string variant = variantPtr == nullptr ? "" : std::string(variantPtr); - auto short_description_ = rxkb_layout_get_brief(layout); - std::string short_description = - short_description_ == nullptr ? "" : std::string(short_description_); + const auto* descriptionPtr = rxkb_layout_get_brief(layout); + std::string description = descriptionPtr == nullptr ? "" : std::string(descriptionPtr); - Layout info = Layout{nameOfLayout, name, variant, short_description}; + Layout info = Layout{nameOfLayout, name, variant, description}; - rxkb_context_unref(CONTEXT); + rxkb_context_unref(context); return info; } - rxkb_context_unref(CONTEXT); + rxkb_context_unref(context); spdlog::debug("hyprland language didn't find matching layout"); diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp index 22acbf31..3575b4ac 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -2,7 +2,7 @@ #include -#include +#include "util/sanitize_str.hpp" namespace waybar::modules::hyprland { @@ -10,7 +10,7 @@ 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()) { + if (!gIPC) { gIPC = std::make_unique(); } @@ -54,8 +54,14 @@ void Submap::onEvent(const std::string& ev) { auto submapName = ev.substr(ev.find_last_of('>') + 1); submapName = waybar::util::sanitize_string(submapName); + if (!submap_.empty()) { + label_.get_style_context()->remove_class(submap_); + } + submap_ = submapName; + label_.get_style_context()->add_class(submap_); + spdlog::debug("hyprland submap onevent with {}", submap_); dp.emit(); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 60de074c..ec151a7b 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -6,21 +6,20 @@ #include #include -#include -#include #include #include "modules/hyprland/backend.hpp" #include "util/rewrite_string.hpp" +#include "util/sanitize_str.hpp" namespace waybar::modules::hyprland { Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) { modulesReady = true; - separate_outputs = config["separate-outputs"].asBool(); + separateOutputs_ = config["separate-outputs"].asBool(); - if (!gIPC.get()) { + if (!gIPC) { gIPC = std::make_unique(); } @@ -46,41 +45,47 @@ auto Window::update() -> void { // fix ampersands std::lock_guard lg(mutex_); - std::string window_name = waybar::util::sanitize_string(workspace_.last_window_title); - std::string window_address = workspace_.last_window; + std::string windowName = waybar::util::sanitize_string(workspace_.last_window_title); + std::string windowAddress = workspace_.last_window; - window_data_.title = window_name; + windowData_.title = windowName; if (!format_.empty()) { label_.show(); label_.set_markup(waybar::util::rewriteString( - fmt::format(fmt::runtime(format_), fmt::arg("title", window_name), - fmt::arg("initialTitle", window_data_.initial_title), - fmt::arg("class", window_data_.class_name), - fmt::arg("initialClass", window_data_.initial_class_name)), + fmt::format(fmt::runtime(format_), fmt::arg("title", windowName), + fmt::arg("initialTitle", windowData_.initial_title), + fmt::arg("class", windowData_.class_name), + fmt::arg("initialClass", windowData_.initial_class_name)), config_["rewrite"])); } else { label_.hide(); } + if (focused_) { + image_.show(); + } else { + image_.hide(); + } + setClass("empty", workspace_.windows == 0); setClass("solo", solo_); - setClass("floating", all_floating_); + setClass("floating", allFloating_); setClass("swallowing", swallowing_); setClass("fullscreen", fullscreen_); - if (!last_solo_class_.empty() && solo_class_ != last_solo_class_) { - if (bar_.window.get_style_context()->has_class(last_solo_class_)) { - bar_.window.get_style_context()->remove_class(last_solo_class_); - spdlog::trace("Removing solo class: {}", last_solo_class_); + if (!lastSoloClass_.empty() && soloClass_ != lastSoloClass_) { + if (bar_.window.get_style_context()->has_class(lastSoloClass_)) { + bar_.window.get_style_context()->remove_class(lastSoloClass_); + spdlog::trace("Removing solo class: {}", lastSoloClass_); } } - if (!solo_class_.empty() && solo_class_ != last_solo_class_) { - bar_.window.get_style_context()->add_class(solo_class_); - spdlog::trace("Adding solo class: {}", solo_class_); + if (!soloClass_.empty() && soloClass_ != lastSoloClass_) { + bar_.window.get_style_context()->add_class(soloClass_); + spdlog::trace("Adding solo class: {}", soloClass_); } - last_solo_class_ = solo_class_; + lastSoloClass_ = soloClass_; AAppIconLabel::update(); } @@ -114,8 +119,12 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> 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()}; + return Workspace{ + value["id"].asInt(), + value["windows"].asInt(), + value["lastwindow"].asString(), + value["lastwindowtitle"].asString(), + }; } auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData { @@ -128,40 +137,45 @@ auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData { void Window::queryActiveWorkspace() { std::lock_guard lg(mutex_); - if (separate_outputs) { + if (separateOutputs_) { workspace_ = getActiveWorkspace(this->bar_.output->name); } else { workspace_ = getActiveWorkspace(); } + focused_ = true; if (workspace_.windows > 0) { const auto clients = gIPC->getSocket1JsonReply("clients"); assert(clients.isArray()); - auto active_window = std::find_if(clients.begin(), clients.end(), [&](Json::Value window) { + auto activeWindow = std::find_if(clients.begin(), clients.end(), [&](Json::Value window) { return window["address"] == workspace_.last_window; }); - if (active_window == std::end(clients)) { + + if (activeWindow == std::end(clients)) { + focused_ = false; return; } - window_data_ = WindowData::parse(*active_window); - updateAppIconName(window_data_.class_name, window_data_.initial_class_name); - std::vector workspace_windows; - std::copy_if(clients.begin(), clients.end(), std::back_inserter(workspace_windows), + windowData_ = WindowData::parse(*activeWindow); + updateAppIconName(windowData_.class_name, windowData_.initial_class_name); + std::vector workspaceWindows; + std::copy_if(clients.begin(), clients.end(), std::back_inserter(workspaceWindows), [&](Json::Value window) { return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); }); - swallowing_ = std::any_of(workspace_windows.begin(), workspace_windows.end(), - [&](Json::Value window) { return !window["swallowing"].isNull(); }); - std::vector visible_windows; - std::copy_if(workspace_windows.begin(), workspace_windows.end(), - std::back_inserter(visible_windows), + swallowing_ = + std::any_of(workspaceWindows.begin(), workspaceWindows.end(), [&](Json::Value window) { + return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; + }); + std::vector visibleWindows; + std::copy_if(workspaceWindows.begin(), workspaceWindows.end(), + std::back_inserter(visibleWindows), [&](Json::Value window) { return !window["hidden"].asBool(); }); - solo_ = 1 == std::count_if(visible_windows.begin(), visible_windows.end(), + solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(), [&](Json::Value window) { return !window["floating"].asBool(); }); - all_floating_ = std::all_of(visible_windows.begin(), visible_windows.end(), - [&](Json::Value window) { return window["floating"].asBool(); }); - fullscreen_ = window_data_.fullscreen; + allFloating_ = std::all_of(visibleWindows.begin(), visibleWindows.end(), + [&](Json::Value window) { return window["floating"].asBool(); }); + fullscreen_ = windowData_.fullscreen; // Fullscreen windows look like they are solo if (fullscreen_) { @@ -169,23 +183,24 @@ void Window::queryActiveWorkspace() { } // Grouped windows have a tab bar and therefore don't look fullscreen or solo - if (window_data_.grouped) { + if (windowData_.grouped) { fullscreen_ = false; solo_ = false; } if (solo_) { - solo_class_ = window_data_.class_name; + soloClass_ = windowData_.class_name; } else { - solo_class_ = ""; + soloClass_ = ""; } } else { - window_data_ = WindowData{}; - all_floating_ = false; + focused_ = false; + windowData_ = WindowData{}; + allFloating_ = false; swallowing_ = false; fullscreen_ = false; solo_ = false; - solo_class_ = ""; + soloClass_ = ""; } } diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 787d812d..882e3806 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -4,313 +4,850 @@ #include #include -#include #include #include +#include +#include + +#include "util/regex_collection.hpp" namespace waybar::modules::hyprland { +int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) { + // Rules that match against title are prioritized + // Rules that don't specify if they're matching against either title or class are deprioritized + bool const hasTitle = window_rule.find("title") != std::string::npos; + bool const hasClass = window_rule.find("class") != std::string::npos; + + if (hasTitle && hasClass) { + m_anyWindowRewriteRuleUsesTitle = true; + return 3; + } + if (hasTitle) { + m_anyWindowRewriteRuleUsesTitle = true; + return 2; + } + if (hasClass) { + return 1; + } + return 0; +} + Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) - : AModule(config, "workspaces", id, false, false), - 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("", ""); - } - - auto config_all_outputs = config_["all-outputs"]; - if (config_all_outputs.isBool()) { - all_outputs_ = config_all_outputs.asBool(); - } - - auto config_show_special = config_["show-special"]; - if (config_show_special.isBool()) { - show_special_ = config_show_special.asBool(); - } - - box_.set_name("workspaces"); - if (!id.empty()) { - box_.get_style_context()->add_class(id); - } - event_box_.add(box_); + : AModule(config, "workspaces", id, false, false), m_bar(bar), m_box(bar.orientation, 0) { modulesReady = true; + parseConfig(config); + + m_box.set_name("workspaces"); + if (!id.empty()) { + m_box.get_style_context()->add_class(id); + } + m_box.get_style_context()->add_class(MODULE_CLASS); + event_box_.add(m_box); + if (!gIPC) { gIPC = std::make_unique(); } + setCurrentMonitorId(); init(); + registerIpc(); +} +auto Workspaces::parseConfig(const Json::Value &config) -> void { + const Json::Value &configFormat = config["format"]; + + m_format = configFormat.isString() ? configFormat.asString() : "{name}"; + m_withIcon = m_format.find("{icon}") != std::string::npos; + + if (m_withIcon && m_iconsMap.empty()) { + Json::Value formatIcons = config["format-icons"]; + for (std::string &name : formatIcons.getMemberNames()) { + m_iconsMap.emplace(name, formatIcons[name].asString()); + } + m_iconsMap.emplace("", ""); + } + + auto configAllOutputs = config_["all-outputs"]; + if (configAllOutputs.isBool()) { + m_allOutputs = configAllOutputs.asBool(); + } + + auto configShowSpecial = config_["show-special"]; + if (configShowSpecial.isBool()) { + m_showSpecial = configShowSpecial.asBool(); + } + + auto configActiveOnly = config_["active-only"]; + if (configActiveOnly.isBool()) { + m_activeOnly = configActiveOnly.asBool(); + } + + auto configSortBy = config_["sort-by"]; + if (configSortBy.isString()) { + auto sortByStr = configSortBy.asString(); + try { + m_sortBy = m_enumParser.parseStringToEnum(sortByStr, m_sortMap); + } catch (const std::invalid_argument &e) { + // Handle the case where the string is not a valid enum representation. + m_sortBy = SortMethod::DEFAULT; + g_warning("Invalid string representation for sort-by. Falling back to default sort method."); + } + } + + Json::Value ignoreWorkspaces = config["ignore-workspaces"]; + if (ignoreWorkspaces.isArray()) { + for (Json::Value &workspaceRegex : ignoreWorkspaces) { + if (workspaceRegex.isString()) { + std::string ruleString = workspaceRegex.asString(); + try { + const std::regex rule{ruleString, std::regex_constants::icase}; + m_ignoreWorkspaces.emplace_back(rule); + } catch (const std::regex_error &e) { + spdlog::error("Invalid rule {}: {}", ruleString, e.what()); + } + } else { + spdlog::error("Not a string: '{}'", workspaceRegex); + } + } + } + + if (config_["persistent_workspaces"].isObject()) { + spdlog::warn( + "persistent_workspaces is deprecated. Please change config to use persistent-workspaces."); + } + + if (config_["persistent-workspaces"].isObject() || config_["persistent_workspaces"].isObject()) { + m_persistentWorkspaceConfig = config_["persistent-workspaces"].isObject() + ? config_["persistent-workspaces"] + : config_["persistent_workspaces"]; + } + + const Json::Value &formatWindowSeparator = config["format-window-separator"]; + m_formatWindowSeparator = + formatWindowSeparator.isString() ? formatWindowSeparator.asString() : " "; + + const Json::Value &windowRewrite = config["window-rewrite"]; + if (!windowRewrite.isObject()) { + spdlog::debug("window-rewrite is not defined or is not an object, using default rules."); + return; + } + + const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"]; + std::string windowRewriteDefault = + windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; + + m_windowRewriteRules = util::RegexCollection( + windowRewrite, windowRewriteDefault, + [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); +} + +void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) { + if (!create_window_payload.isEmpty(*this)) { + m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this); + } +} + +auto Workspaces::registerIpc() -> void { gIPC->registerForIPC("workspace", this); + gIPC->registerForIPC("activespecial", this); gIPC->registerForIPC("createworkspace", this); gIPC->registerForIPC("destroyworkspace", this); gIPC->registerForIPC("focusedmon", this); gIPC->registerForIPC("moveworkspace", this); + gIPC->registerForIPC("renameworkspace", this); gIPC->registerForIPC("openwindow", this); gIPC->registerForIPC("closewindow", this); gIPC->registerForIPC("movewindow", this); + gIPC->registerForIPC("urgent", this); + gIPC->registerForIPC("configreloaded", this); + + if (windowRewriteConfigUsesTitle()) { + spdlog::info( + "Registering for Hyprland's 'windowtitle' events because a user-defined window " + "rewrite rule uses the 'title' field."); + gIPC->registerForIPC("windowtitle", this); + } +} + +/** + * Workspaces::doUpdate - update workspaces in UI thread. + * + * Note: some memberfields are modified by both UI thread and event listener thread, use m_mutex to + * protect these member fields, and lock should released before calling AModule::update(). + */ +void Workspaces::doUpdate() { + std::unique_lock lock(m_mutex); + + // remove workspaces that wait to be removed + for (auto &elem : m_workspacesToRemove) { + removeWorkspace(elem); + } + m_workspacesToRemove.clear(); + + // add workspaces that wait to be created + for (auto &[workspaceData, clientsData] : m_workspacesToCreate) { + createWorkspace(workspaceData, clientsData); + } + m_workspacesToCreate.clear(); + + // get all active workspaces + spdlog::trace("Getting active workspaces"); + auto monitors = gIPC->getSocket1JsonReply("monitors"); + std::vector visibleWorkspaces; + for (Json::Value &monitor : monitors) { + auto ws = monitor["activeWorkspace"]; + if (ws.isObject() && (ws["name"].isString())) { + visibleWorkspaces.push_back(ws["name"].asString()); + } + auto sws = monitor["specialWorkspace"]; + auto name = sws["name"].asString(); + if (sws.isObject() && (sws["name"].isString()) && !name.empty()) { + visibleWorkspaces.push_back(!name.starts_with("special:") ? name : name.substr(8)); + } + } + + spdlog::trace("Updating workspace states"); + for (auto &workspace : m_workspaces) { + // active + workspace->setActive(workspace->name() == m_activeWorkspaceName || + workspace->name() == m_activeSpecialWorkspaceName); + // disable urgency if workspace is active + if (workspace->name() == m_activeWorkspaceName && workspace->isUrgent()) { + workspace->setUrgent(false); + } + + // visible + workspace->setVisible(std::find(visibleWorkspaces.begin(), visibleWorkspaces.end(), + workspace->name()) != visibleWorkspaces.end()); + + // set workspace icon + std::string &workspaceIcon = m_iconsMap[""]; + if (m_withIcon) { + workspaceIcon = workspace->selectIcon(m_iconsMap); + } + workspace->update(m_format, workspaceIcon); + } + + spdlog::trace("Updating window count"); + bool anyWindowCreated = false; + std::vector notCreated; + + for (auto &windowPayload : m_windowsToCreate) { + bool created = false; + for (auto &workspace : m_workspaces) { + if (workspace->onWindowOpened(windowPayload)) { + created = true; + anyWindowCreated = true; + break; + } + } + if (!created) { + static auto const WINDOW_CREATION_TIMEOUT = 2; + if (windowPayload.incrementTimeSpentUncreated() < WINDOW_CREATION_TIMEOUT) { + notCreated.push_back(windowPayload); + } else { + registerOrphanWindow(windowPayload); + } + } + } + + if (anyWindowCreated) { + dp.emit(); + } + + m_windowsToCreate.clear(); + m_windowsToCreate = notCreated; } auto Workspaces::update() -> void { - for (std::string workspace_to_remove : workspaces_to_remove_) { - remove_workspace(workspace_to_remove); - } - - workspaces_to_remove_.clear(); - - for (Json::Value &workspace_to_create : workspaces_to_create_) { - create_workspace(workspace_to_create); - } - - workspaces_to_create_.clear(); - - for (auto &workspace : workspaces_) { - workspace->set_active(workspace->name() == active_workspace_name_); - std::string &workspace_icon = icons_map_[""]; - if (with_icon_) { - workspace_icon = workspace->select_icon(icons_map_); - } - workspace->update(format_, workspace_icon); - } + doUpdate(); AModule::update(); } +bool isDoubleSpecial(std::string const &workspace_name) { + // Hyprland's IPC sometimes reports the creation of workspaces strangely named + // `special:special:`. This function checks for that and is used + // to avoid creating (and then removing) such workspaces. + // See hyprwm/Hyprland#3424 for more info. + return workspace_name.find("special:special:") != std::string::npos; +} + +bool Workspaces::isWorkspaceIgnored(std::string const &name) { + for (auto &rule : m_ignoreWorkspaces) { + if (std::regex_match(name, rule)) { + return true; + break; + } + } + + return false; +} + void Workspaces::onEvent(const std::string &ev) { - std::lock_guard lock(mutex_); + std::lock_guard lock(m_mutex); std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); std::string payload = ev.substr(eventName.size() + 2); if (eventName == "workspace") { - active_workspace_name_ = payload; - + onWorkspaceActivated(payload); + } else if (eventName == "activespecial") { + onSpecialWorkspaceActivated(payload); } else if (eventName == "destroyworkspace") { - workspaces_to_remove_.push_back(payload); - + onWorkspaceDestroyed(payload); } else if (eventName == "createworkspace") { - const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); - for (Json::Value workspace_json : workspaces_json) { - if (workspace_json["name"].asString() == payload && - (all_outputs() || bar_.output->name == workspace_json["monitor"].asString()) && - (show_special() || workspace_json["name"].asString().starts_with("special"))) { - workspaces_to_create_.push_back(workspace_json); - break; - } - } - + onWorkspaceCreated(payload); } else if (eventName == "focusedmon") { - active_workspace_name_ = payload.substr(payload.find(',') + 1); - - } else if (eventName == "moveworkspace" && !all_outputs()) { - std::string workspace = payload.substr(0, payload.find(',')); - std::string new_output = payload.substr(payload.find(',') + 1); - if (bar_.output->name == new_output) { // TODO: implement this better - const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); - for (Json::Value workspace_json : workspaces_json) { - if (workspace_json["name"].asString() == workspace && - bar_.output->name == workspace_json["monitor"].asString()) { - workspaces_to_create_.push_back(workspace_json); - break; - } - } - } else { - workspaces_to_remove_.push_back(workspace); - } - } else if (eventName == "openwindow" || eventName == "closewindow" || eventName == "movewindow") { - update_window_count(); + onMonitorFocused(payload); + } else if (eventName == "moveworkspace" && !allOutputs()) { + onWorkspaceMoved(payload); + } else if (eventName == "openwindow") { + onWindowOpened(payload); + } else if (eventName == "closewindow") { + onWindowClosed(payload); + } else if (eventName == "movewindow") { + onWindowMoved(payload); + } else if (eventName == "urgent") { + setUrgentWorkspace(payload); + } else if (eventName == "renameworkspace") { + onWorkspaceRenamed(payload); + } else if (eventName == "windowtitle") { + onWindowTitleEvent(payload); + } else if (eventName == "configreloaded") { + onConfigReloaded(); } dp.emit(); } -void Workspaces::update_window_count() { - const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); - for (auto &workspace : workspaces_) { - auto workspace_json = std::find_if( - workspaces_json.begin(), workspaces_json.end(), - [&](Json::Value const &x) { return x["name"].asString() == workspace->name(); }); - if (workspace_json != workspaces_json.end()) { - try { - workspace->set_windows((*workspace_json)["windows"].asUInt()); - } catch (const std::exception &e) { - spdlog::error("Failed to update window count: {}", e.what()); +void Workspaces::onWorkspaceActivated(std::string const &payload) { + m_activeWorkspaceName = payload; +} + +void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) { + std::string name(begin(payload), begin(payload) + payload.find_first_of(',')); + m_activeSpecialWorkspaceName = (!name.starts_with("special:") ? name : name.substr(8)); +} + +void Workspaces::onWorkspaceDestroyed(std::string const &payload) { + if (!isDoubleSpecial(payload)) { + m_workspacesToRemove.push_back(payload); + } +} + +void Workspaces::onWorkspaceCreated(std::string const &workspaceName, + Json::Value const &clientsData) { + spdlog::debug("Workspace created: {}", workspaceName); + auto const workspacesJson = gIPC->getSocket1JsonReply("workspaces"); + + if (!isWorkspaceIgnored(workspaceName)) { + auto const workspaceRules = gIPC->getSocket1JsonReply("workspacerules"); + for (Json::Value workspaceJson : workspacesJson) { + std::string name = workspaceJson["name"].asString(); + if (name == workspaceName) { + if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && + (showSpecial() || !name.starts_with("special")) && !isDoubleSpecial(workspaceName)) { + for (Json::Value const &rule : workspaceRules) { + if (rule["workspaceString"].asString() == workspaceName) { + workspaceJson["persistent-rule"] = rule["persistent"].asBool(); + break; + } + } + + m_workspacesToCreate.emplace_back(workspaceJson, clientsData); + break; + } + } else { + extendOrphans(workspaceJson["id"].asInt(), clientsData); } - } else { - workspace->set_windows(0); + } + } else { + spdlog::trace("Not creating workspace because it is ignored: {}", workspaceName); + } +} + +void Workspaces::onWorkspaceMoved(std::string const &payload) { + spdlog::debug("Workspace moved: {}", payload); + std::string workspaceName = payload.substr(0, payload.find(',')); + std::string monitorName = payload.substr(payload.find(',') + 1); + + if (m_bar.output->name == monitorName) { + Json::Value clientsData = gIPC->getSocket1JsonReply("clients"); + onWorkspaceCreated(workspaceName, clientsData); + } else { + spdlog::debug("Removing workspace because it was moved to another monitor: {}"); + onWorkspaceDestroyed(workspaceName); + } +} + +void Workspaces::onWorkspaceRenamed(std::string const &payload) { + spdlog::debug("Workspace renamed: {}", payload); + std::string workspaceIdStr = payload.substr(0, payload.find(',')); + int workspaceId = workspaceIdStr == "special" ? -99 : std::stoi(workspaceIdStr); + std::string newName = payload.substr(payload.find(',') + 1); + for (auto &workspace : m_workspaces) { + if (workspace->id() == workspaceId) { + if (workspace->name() == m_activeWorkspaceName) { + m_activeWorkspaceName = newName; + } + workspace->setName(newName); + break; + } + } + sortWorkspaces(); +} + +void Workspaces::onMonitorFocused(std::string const &payload) { + spdlog::trace("Monitor focused: {}", payload); + m_activeWorkspaceName = payload.substr(payload.find(',') + 1); + + for (Json::Value &monitor : gIPC->getSocket1JsonReply("monitors")) { + if (monitor["name"].asString() == payload.substr(0, payload.find(','))) { + auto name = monitor["specialWorkspace"]["name"].asString(); + m_activeSpecialWorkspaceName = !name.starts_with("special:") ? name : name.substr(8); } } } -void Workspaces::create_workspace(Json::Value &value) { - // replace the existing persistent workspace if it exists +void Workspaces::onWindowOpened(std::string const &payload) { + spdlog::trace("Window opened: {}", payload); + updateWindowCount(); + size_t lastCommaIdx = 0; + size_t nextCommaIdx = payload.find(','); + std::string windowAddress = payload.substr(lastCommaIdx, nextCommaIdx - lastCommaIdx); + + lastCommaIdx = nextCommaIdx; + nextCommaIdx = payload.find(',', nextCommaIdx + 1); + std::string workspaceName = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1); + + lastCommaIdx = nextCommaIdx; + nextCommaIdx = payload.find(',', nextCommaIdx + 1); + std::string windowClass = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1); + + std::string windowTitle = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx); + + m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle); +} + +void Workspaces::onWindowClosed(std::string const &addr) { + spdlog::trace("Window closed: {}", addr); + updateWindowCount(); + for (auto &workspace : m_workspaces) { + if (workspace->closeWindow(addr)) { + break; + } + } +} + +void Workspaces::onWindowMoved(std::string const &payload) { + spdlog::trace("Window moved: {}", payload); + updateWindowCount(); + size_t lastCommaIdx = 0; + size_t nextCommaIdx = payload.find(','); + std::string windowAddress = payload.substr(lastCommaIdx, nextCommaIdx - lastCommaIdx); + + std::string workspaceName = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx); + + std::string windowRepr; + + // If the window was still queued to be created, just change its destination + // and exit + for (auto &window : m_windowsToCreate) { + if (window.getAddress() == windowAddress) { + window.moveToWorksace(workspaceName); + return; + } + } + + // Take the window's representation from the old workspace... + for (auto &workspace : m_workspaces) { + if (auto windowAddr = workspace->closeWindow(windowAddress); windowAddr != std::nullopt) { + windowRepr = windowAddr.value(); + break; + } + } + + // ...if it was empty, check if the window is an orphan... + if (windowRepr.empty() && m_orphanWindowMap.contains(windowAddress)) { + windowRepr = m_orphanWindowMap[windowAddress]; + } + + // ...and then add it to the new workspace + if (!windowRepr.empty()) { + m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowRepr); + } +} + +void Workspaces::onWindowTitleEvent(std::string const &payload) { + spdlog::trace("Window title changed: {}", payload); + std::optional> inserter; + + // If the window was an orphan, rename it at the orphan's vector + if (m_orphanWindowMap.contains(payload)) { + inserter = [this](WindowCreationPayload wcp) { this->registerOrphanWindow(std::move(wcp)); }; + } else { + auto windowWorkspace = + std::find_if(m_workspaces.begin(), m_workspaces.end(), + [payload](auto &workspace) { return workspace->containsWindow(payload); }); + + // If the window exists on a workspace, rename it at the workspace's window + // map + if (windowWorkspace != m_workspaces.end()) { + inserter = [windowWorkspace](WindowCreationPayload wcp) { + (*windowWorkspace)->insertWindow(std::move(wcp)); + }; + } else { + auto queuedWindow = std::find_if( + m_windowsToCreate.begin(), m_windowsToCreate.end(), + [payload](auto &windowPayload) { return windowPayload.getAddress() == payload; }); + + // If the window was queued, rename it in the queue + if (queuedWindow != m_windowsToCreate.end()) { + inserter = [queuedWindow](WindowCreationPayload wcp) { *queuedWindow = std::move(wcp); }; + } + } + } + + if (inserter.has_value()) { + Json::Value clientsData = gIPC->getSocket1JsonReply("clients"); + std::string jsonWindowAddress = fmt::format("0x{}", payload); + + auto client = + std::find_if(clientsData.begin(), clientsData.end(), [jsonWindowAddress](auto &client) { + return client["address"].asString() == jsonWindowAddress; + }); + + if (!client->empty()) { + (*inserter)({*client}); + } + } +} + +void Workspaces::onConfigReloaded() { + spdlog::info("Hyprland config reloaded, reinitializing hyprland/workspaces module..."); + init(); +} + +void Workspaces::updateWindowCount() { + const Json::Value workspacesJson = gIPC->getSocket1JsonReply("workspaces"); + for (auto &workspace : m_workspaces) { + auto workspaceJson = + std::find_if(workspacesJson.begin(), workspacesJson.end(), [&](Json::Value const &x) { + return x["name"].asString() == workspace->name() || + (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); + }); + uint32_t count = 0; + if (workspaceJson != workspacesJson.end()) { + try { + count = (*workspaceJson)["windows"].asUInt(); + } catch (const std::exception &e) { + spdlog::error("Failed to update window count: {}", e.what()); + } + } + workspace->setWindows(count); + } +} + +void Workspace::initializeWindowMap(const Json::Value &clients_data) { + m_windowMap.clear(); + for (auto client : clients_data) { + if (client["workspace"]["id"].asInt() == id()) { + insertWindow({client}); + } + } +} + +void Workspace::insertWindow(WindowCreationPayload create_window_paylod) { + if (!create_window_paylod.isEmpty(m_workspaceManager)) { + m_windowMap[create_window_paylod.getAddress()] = create_window_paylod.repr(m_workspaceManager); + } +}; + +std::string Workspace::removeWindow(WindowAddress const &addr) { + std::string windowRepr = m_windowMap[addr]; + m_windowMap.erase(addr); + return windowRepr; +} + +bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_paylod) { + if (create_window_paylod.getWorkspaceName() == name()) { + insertWindow(create_window_paylod); + return true; + } + return false; +} + +std::optional Workspace::closeWindow(WindowAddress const &addr) { + if (m_windowMap.contains(addr)) { + return removeWindow(addr); + } + return std::nullopt; +} + +void Workspaces::createWorkspace(Json::Value const &workspace_data, + Json::Value const &clients_data) { + auto workspaceName = workspace_data["name"].asString(); + spdlog::debug("Creating workspace {}", workspaceName); + + // avoid recreating existing workspaces auto workspace = std::find_if( - workspaces_.begin(), workspaces_.end(), [&](std::unique_ptr const &x) { - auto name = value["name"].asString(); - return x->is_persistent() && - ((name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name()); + m_workspaces.begin(), m_workspaces.end(), + [workspaceName](std::unique_ptr const &w) { + return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || + workspaceName == w->name(); }); - if (workspace != workspaces_.end()) { - // replace workspace, but keep persistent flag - workspaces_.erase(workspace); - value["persistent"] = true; + + if (workspace != m_workspaces.end()) { + // don't recreate workspace, but update persistency if necessary + const auto keys = workspace_data.getMemberNames(); + + const auto *k = "persistent-rule"; + if (std::find(keys.begin(), keys.end(), k) != keys.end()) { + spdlog::debug("Set dynamic persistency of workspace {} to: {}", workspaceName, + workspace_data[k].asBool() ? "true" : "false"); + (*workspace)->setPersistentRule(workspace_data[k].asBool()); + } + + k = "persistent-config"; + if (std::find(keys.begin(), keys.end(), k) != keys.end()) { + spdlog::debug("Set config persistency of workspace {} to: {}", workspaceName, + workspace_data[k].asBool() ? "true" : "false"); + (*workspace)->setPersistentConfig(workspace_data[k].asBool()); + } + + return; } // create new workspace - workspaces_.emplace_back(std::make_unique(value)); - Gtk::Button &new_workspace_button = workspaces_.back()->button(); - box_.pack_start(new_workspace_button, false, false); - sort_workspaces(); - new_workspace_button.show_all(); + m_workspaces.emplace_back(std::make_unique(workspace_data, *this, clients_data)); + Gtk::Button &newWorkspaceButton = m_workspaces.back()->button(); + m_box.pack_start(newWorkspaceButton, false, false); + sortWorkspaces(); + newWorkspaceButton.show_all(); } -void Workspaces::remove_workspace(std::string name) { - auto workspace = std::find_if(workspaces_.begin(), workspaces_.end(), - [&](std::unique_ptr &x) { return x->name() == name; }); +void Workspaces::removeWorkspace(std::string const &name) { + spdlog::debug("Removing workspace {}", name); + auto workspace = + std::find_if(m_workspaces.begin(), m_workspaces.end(), [&](std::unique_ptr &x) { + return (name.starts_with("special:") && name.substr(8) == x->name()) || name == x->name(); + }); - if (workspace == workspaces_.end()) { + if (workspace == m_workspaces.end()) { // happens when a workspace on another monitor is destroyed return; } - if ((*workspace)->is_persistent()) { - // don't remove persistent workspaces, create_workspace will take care of replacement + if ((*workspace)->isPersistentConfig()) { + spdlog::trace("Not removing config persistent workspace {}", name); return; } - box_.remove(workspace->get()->button()); - workspaces_.erase(workspace); + m_box.remove(workspace->get()->button()); + m_workspaces.erase(workspace); } -void Workspaces::fill_persistent_workspaces() { - if (config_["persistent_workspaces"].isObject() && !all_outputs()) { - const Json::Value persistent_workspaces = config_["persistent_workspaces"]; - const std::vector keys = persistent_workspaces.getMemberNames(); +Json::Value createMonitorWorkspaceData(std::string const &name, std::string const &monitor) { + spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor); + Json::Value workspaceData; + try { + // numbered persistent workspaces get the name as ID + workspaceData["id"] = name == "special" ? -99 : std::stoi(name); + } catch (const std::exception &e) { + // named persistent workspaces start with ID=0 + workspaceData["id"] = 0; + } + workspaceData["name"] = name; + workspaceData["monitor"] = monitor; + workspaceData["windows"] = 0; + return workspaceData; +} - for (const std::string &key : keys) { - const Json::Value &value = persistent_workspaces[key]; - if (value.isInt()) { - // value is a number => create that many workspaces for this monitor - // only add if either: - // 1. key is "*" and this monitor is not already defined in the config - // 2. key is the current monitor name - if ((key == "*" && std::find(keys.begin(), keys.end(), bar_.output->name) == keys.end()) || - key == bar_.output->name) { - int amount = value.asInt(); - spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, - bar_.output->name); - for (int i = 0; i < amount; i++) { - persistent_workspaces_to_create_.emplace_back( - std::to_string(monitor_id_ * amount + i + 1)); +void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJson) { + spdlog::info("Loading persistent workspaces from Waybar config"); + const std::vector keys = m_persistentWorkspaceConfig.getMemberNames(); + std::vector persistentWorkspacesToCreate; + + const std::string currentMonitor = m_bar.output->name; + const bool monitorInConfig = std::find(keys.begin(), keys.end(), currentMonitor) != keys.end(); + for (const std::string &key : keys) { + // only add if either: + // 1. key is the current monitor name + // 2. key is "*" and this monitor is not already defined in the config + bool canCreate = key == currentMonitor || (key == "*" && !monitorInConfig); + const Json::Value &value = m_persistentWorkspaceConfig[key]; + spdlog::trace("Parsing persistent workspace config: {} => {}", key, value.toStyledString()); + + if (value.isInt()) { + // value is a number => create that many workspaces for this monitor + if (canCreate) { + int amount = value.asInt(); + spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, currentMonitor); + for (int i = 0; i < amount; i++) { + persistentWorkspacesToCreate.emplace_back(std::to_string(m_monitorId * amount + i + 1)); + } + } + } else if (value.isArray() && !value.empty()) { + // value is an array => create defined workspaces for this monitor + if (canCreate) { + for (const Json::Value &workspace : value) { + if (workspace.isInt()) { + spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor); + persistentWorkspacesToCreate.emplace_back(std::to_string(workspace.asInt())); } } - - } else if (value.isArray() && !value.empty()) { - // value is an array => key is a workspace name - // values are monitor names this workspace should be shown on + } else { + // key is the workspace and value is array of monitors to create on for (const Json::Value &monitor : value) { - if (monitor.isString() && monitor.asString() == bar_.output->name) { - persistent_workspaces_to_create_.emplace_back(key); + if (monitor.isString() && monitor.asString() == currentMonitor) { + persistentWorkspacesToCreate.emplace_back(currentMonitor); break; } } } + } else { + // this workspace should be displayed on all monitors + persistentWorkspacesToCreate.emplace_back(key); + } + } + + for (auto const &workspace : persistentWorkspacesToCreate) { + auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); + workspaceData["persistent-config"] = true; + m_workspacesToCreate.emplace_back(workspaceData, clientsJson); + } +} + +void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &clientsJson) { + spdlog::info("Loading persistent workspaces from Hyprland workspace rules"); + + auto const workspaceRules = gIPC->getSocket1JsonReply("workspacerules"); + for (Json::Value const &rule : workspaceRules) { + if (!rule["workspaceString"].isString()) { + spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule); + continue; + } + if (!rule["persistent"].asBool()) { + continue; + } + auto const &workspace = rule["workspaceString"].asString(); + auto const &monitor = rule["monitor"].asString(); + // create this workspace persistently if: + // 1. the allOutputs config option is enabled + // 2. the rule's monitor is the current monitor + // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor + if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { + // => persistent workspace should be shown on this monitor + auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); + workspaceData["persistent-rule"] = true; + m_workspacesToCreate.emplace_back(workspaceData, clientsJson); + } else { + m_workspacesToRemove.emplace_back(workspace); } } } -void Workspaces::create_persistent_workspaces() { - for (const std::string &workspace_name : persistent_workspaces_to_create_) { - Json::Value new_workspace; - try { - // numbered persistent workspaces get the name as ID - new_workspace["id"] = workspace_name == "special" ? -99 : std::stoi(workspace_name); - } catch (const std::exception &e) { - // named persistent workspaces start with ID=0 - new_workspace["id"] = 0; - } - new_workspace["name"] = workspace_name; - new_workspace["monitor"] = bar_.output->name; - new_workspace["windows"] = 0; - new_workspace["persistent"] = true; +void Workspaces::setCurrentMonitorId() { + // get monitor ID from name (used by persistent workspaces) + m_monitorId = 0; + auto monitors = gIPC->getSocket1JsonReply("monitors"); + auto currentMonitor = std::find_if( + monitors.begin(), monitors.end(), + [this](const Json::Value &m) { return m["name"].asString() == m_bar.output->name; }); + if (currentMonitor == monitors.end()) { + spdlog::error("Monitor '{}' does not have an ID? Using 0", m_bar.output->name); + } else { + m_monitorId = (*currentMonitor)["id"].asInt(); + spdlog::trace("Current monitor ID: {}", m_monitorId); + } +} - create_workspace(new_workspace); +void Workspaces::initializeWorkspaces() { + spdlog::debug("Initializing workspaces"); + + // if the workspace rules changed since last initialization, make sure we reset everything: + for (auto &workspace : m_workspaces) { + m_workspacesToRemove.push_back(workspace->name()); + } + + // get all current workspaces + auto const workspacesJson = gIPC->getSocket1JsonReply("workspaces"); + auto const clientsJson = gIPC->getSocket1JsonReply("clients"); + + for (Json::Value workspaceJson : workspacesJson) { + std::string workspaceName = workspaceJson["name"].asString(); + if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && + (!workspaceName.starts_with("special") || showSpecial()) && + !isWorkspaceIgnored(workspaceName)) { + m_workspacesToCreate.emplace_back(workspaceJson, clientsJson); + } else { + extendOrphans(workspaceJson["id"].asInt(), clientsJson); + } + } + + spdlog::debug("Initializing persistent workspaces"); + if (m_persistentWorkspaceConfig.isObject()) { + // a persistent workspace config is defined, so use that instead of workspace rules + loadPersistentWorkspacesFromConfig(clientsJson); + } + // load Hyprland's workspace rules + loadPersistentWorkspacesFromWorkspaceRules(clientsJson); +} + +void Workspaces::extendOrphans(int workspaceId, Json::Value const &clientsJson) { + spdlog::trace("Extending orphans with workspace {}", workspaceId); + for (const auto &client : clientsJson) { + if (client["workspace"]["id"].asInt() == workspaceId) { + registerOrphanWindow({client}); + } } } void Workspaces::init() { - active_workspace_name_ = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); - - // get monitor ID from name (used by persistent workspaces) - monitor_id_ = 0; - auto monitors = gIPC->getSocket1JsonReply("monitors"); - auto current_monitor = std::find_if( - monitors.begin(), monitors.end(), - [this](const Json::Value &m) { return m["name"].asString() == bar_.output->name; }); - if (current_monitor == monitors.end()) { - spdlog::error("Monitor '{}' does not have an ID? Using 0", bar_.output->name); - } else { - monitor_id_ = (*current_monitor)["id"].asInt(); - } - - fill_persistent_workspaces(); - create_persistent_workspaces(); - - const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); - for (Json::Value workspace_json : workspaces_json) { - if ((all_outputs() || bar_.output->name == workspace_json["monitor"].asString()) && - (workspace_json["name"].asString().starts_with("special") || show_special())) - create_workspace(workspace_json); - } - - update_window_count(); - - sort_workspaces(); + m_activeWorkspaceName = (gIPC->getSocket1JsonReply("activeworkspace"))["name"].asString(); + initializeWorkspaces(); + updateWindowCount(); + sortWorkspaces(); dp.emit(); } Workspaces::~Workspaces() { gIPC->unregisterForIPC(this); // wait for possible event handler to finish - std::lock_guard lg(mutex_); + std::lock_guard lg(m_mutex); } -Workspace::Workspace(const Json::Value &workspace_data) - : id_(workspace_data["id"].asInt()), - name_(workspace_data["name"].asString()), - output_(workspace_data["monitor"].asString()), // TODO:allow using monitor desc - windows_(workspace_data["windows"].asInt()), - active_(true) { - if (name_.starts_with("name:")) { - name_ = name_.substr(5); - } else if (name_.starts_with("special")) { - name_ = id_ == -99 ? name_ : name_.substr(8); - is_special_ = true; +Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_manager, + const Json::Value &clients_data) + : m_workspaceManager(workspace_manager), + m_id(workspace_data["id"].asInt()), + m_name(workspace_data["name"].asString()), + m_output(workspace_data["monitor"].asString()), // TODO:allow using monitor desc + m_windows(workspace_data["windows"].asInt()), + m_isActive(true), + m_isPersistentRule(workspace_data["persistent-rule"].asBool()), + m_isPersistentConfig(workspace_data["persistent-config"].asBool()) { + if (m_name.starts_with("name:")) { + m_name = m_name.substr(5); + } else if (m_name.starts_with("special")) { + m_name = m_id == -99 ? m_name : m_name.substr(8); + m_isSpecial = true; } - if (workspace_data.isMember("persistent")) { - is_persistent_ = workspace_data["persistent"].asBool(); - } + m_button.add_events(Gdk::BUTTON_PRESS_MASK); + m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), + false); - button_.add_events(Gdk::BUTTON_PRESS_MASK); - button_.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handle_clicked), - false); + m_button.set_relief(Gtk::RELIEF_NONE); + m_content.set_center_widget(m_label); + m_button.add(m_content); - button_.set_relief(Gtk::RELIEF_NONE); - content_.set_center_widget(label_); - button_.add(content_); -}; + initializeWindowMap(clients_data); +} -void add_or_remove_class(const Glib::RefPtr &context, bool condition, - const std::string &class_name) { +void addOrRemoveClass(const Glib::RefPtr &context, bool condition, + const std::string &class_name) { if (condition) { context->add_class(class_name); } else { @@ -319,104 +856,308 @@ void add_or_remove_class(const Glib::RefPtr &context, bool co } void Workspace::update(const std::string &format, const std::string &icon) { - auto style_context = button_.get_style_context(); - add_or_remove_class(style_context, active(), "active"); - add_or_remove_class(style_context, is_special(), "special"); - add_or_remove_class(style_context, is_empty(), "persistent"); + // clang-format off + if (this->m_workspaceManager.activeOnly() && \ + !this->isActive() && \ + !this->isPersistent() && \ + !this->isVisible() && \ + !this->isSpecial()) { + // clang-format on + // if activeOnly is true, hide if not active, persistent, visible or special + m_button.hide(); + return; + } + m_button.show(); - label_.set_markup( - fmt::format(fmt::runtime(format), fmt::arg("name", name()), fmt::arg("icon", icon))); + auto styleContext = m_button.get_style_context(); + addOrRemoveClass(styleContext, isActive(), "active"); + addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isEmpty(), "empty"); + addOrRemoveClass(styleContext, isPersistent(), "persistent"); + addOrRemoveClass(styleContext, isUrgent(), "urgent"); + addOrRemoveClass(styleContext, isVisible(), "visible"); + + std::string windows; + auto windowSeparator = m_workspaceManager.getWindowSeparator(); + + bool isNotFirst = false; + + for (auto &[_pid, window_repr] : m_windowMap) { + if (isNotFirst) { + windows.append(windowSeparator); + } + isNotFirst = true; + windows.append(window_repr); + } + + m_label.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), + fmt::arg("name", name()), fmt::arg("icon", icon), + fmt::arg("windows", windows))); } -void Workspaces::sort_workspaces() { - std::sort(workspaces_.begin(), workspaces_.end(), - [](std::unique_ptr &a, std::unique_ptr &b) { - // normal -> named persistent -> named -> special -> named special +void Workspaces::sortWorkspaces() { + std::sort(m_workspaces.begin(), m_workspaces.end(), + [&](std::unique_ptr &a, std::unique_ptr &b) { + // Helper comparisons + auto isIdLess = a->id() < b->id(); + auto isNameLess = a->name() < b->name(); - // both normal (includes numbered persistent) => sort by ID - if (a->id() > 0 && b->id() > 0) { - return a->id() < b->id(); + switch (m_sortBy) { + case SortMethod::ID: + return isIdLess; + case SortMethod::NAME: + return isNameLess; + case SortMethod::NUMBER: + try { + return std::stoi(a->name()) < std::stoi(b->name()); + } catch (const std::invalid_argument &) { + // Handle the exception if necessary. + break; + } + case SortMethod::DEFAULT: + default: + // Handle the default case here. + // normal -> named persistent -> named -> special -> named special + + // both normal (includes numbered persistent) => sort by ID + if (a->id() > 0 && b->id() > 0) { + return isIdLess; + } + + // one normal, one special => normal first + if ((a->isSpecial()) ^ (b->isSpecial())) { + return b->isSpecial(); + } + + // only one normal, one named + if ((a->id() > 0) ^ (b->id() > 0)) { + return a->id() > 0; + } + + // both special + if (a->isSpecial() && b->isSpecial()) { + // if one is -99 => put it last + if (a->id() == -99 || b->id() == -99) { + return b->id() == -99; + } + // both are 0 (not yet named persistents) / named specials (-98 <= ID <= -1) + return isNameLess; + } + + // sort non-special named workspaces by name (ID <= -1377) + return isNameLess; + break; } - // one normal, one special => normal first - if ((a->is_special()) ^ (b->is_special())) { - return b->is_special(); - } - - // only one normal, one named - if ((a->id() > 0) ^ (b->id() > 0)) { - return a->id() > 0; - } - - // both special - if (a->is_special() && b->is_special()) { - // if one is -99 => put it last - if (a->id() == -99 || b->id() == -99) { - return b->id() == -99; - } - // both are 0 (not yet named persistents) / both are named specials (-98 <= ID <=-1) - return a->name() < b->name(); - } - - // sort non-special named workspaces by name (ID <= -1377) - return a->name() < b->name(); + // Return a default value if none of the cases match. + return isNameLess; // You can adjust this to your specific needs. }); - for (size_t i = 0; i < workspaces_.size(); ++i) { - box_.reorder_child(workspaces_[i]->button(), i); + for (size_t i = 0; i < m_workspaces.size(); ++i) { + m_box.reorder_child(m_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; +std::string &Workspace::selectIcon(std::map &icons_map) { + spdlog::trace("Selecting icon for workspace {}", name()); + if (isUrgent()) { + auto urgentIconIt = icons_map.find("urgent"); + if (urgentIconIt != icons_map.end()) { + return urgentIconIt->second; } } - if (is_special()) { - auto special_icon_it = icons_map.find("special"); - if (special_icon_it != icons_map.end()) { - return special_icon_it->second; + if (isActive()) { + auto activeIconIt = icons_map.find("active"); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; } } - auto named_icon_it = icons_map.find(std::to_string(id())); - if (named_icon_it != icons_map.end()) { - return named_icon_it->second; - } - - if (is_persistent()) { - auto persistent_icon_it = icons_map.find("persistent"); - if (persistent_icon_it != icons_map.end()) { - return persistent_icon_it->second; + if (isSpecial()) { + auto specialIconIt = icons_map.find("special"); + if (specialIconIt != icons_map.end()) { + return specialIconIt->second; } } - auto default_icon_it = icons_map.find("default"); - if (default_icon_it != icons_map.end()) { - return default_icon_it->second; + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; } - return icons_map[""]; + + if (isVisible()) { + auto visibleIconIt = icons_map.find("visible"); + if (visibleIconIt != icons_map.end()) { + return visibleIconIt->second; + } + } + + if (isEmpty()) { + auto emptyIconIt = icons_map.find("empty"); + if (emptyIconIt != icons_map.end()) { + return emptyIconIt->second; + } + } + + if (isPersistent()) { + auto persistentIconIt = icons_map.find("persistent"); + if (persistentIconIt != icons_map.end()) { + return persistentIconIt->second; + } + } + + auto defaultIconIt = icons_map.find("default"); + if (defaultIconIt != icons_map.end()) { + return defaultIconIt->second; + } + + return m_name; } -auto Workspace::handle_clicked(GdkEventButton *bt) -> bool { - try { - if (id() > 0) { // normal or numbered persistent - gIPC->getSocket1Reply("dispatch workspace " + std::to_string(id())); - } else if (!is_special()) { // named - gIPC->getSocket1Reply("dispatch workspace name:" + name()); - } else if (id() != -99) { // named special - gIPC->getSocket1Reply("dispatch togglespecialworkspace " + name()); - } else { // special - gIPC->getSocket1Reply("dispatch togglespecialworkspace"); +bool Workspace::handleClicked(GdkEventButton *bt) const { + if (bt->type == GDK_BUTTON_PRESS) { + try { + if (id() > 0) { // normal + gIPC->getSocket1Reply("dispatch workspace " + std::to_string(id())); + } else if (!isSpecial()) { // named (this includes persistent) + gIPC->getSocket1Reply("dispatch workspace name:" + name()); + } else if (id() != -99) { // named special + gIPC->getSocket1Reply("dispatch togglespecialworkspace " + name()); + } else { // special + gIPC->getSocket1Reply("dispatch togglespecialworkspace"); + } + return true; + } catch (const std::exception &e) { + spdlog::error("Failed to dispatch workspace: {}", e.what()); } - return true; - } catch (const std::exception &e) { - spdlog::error("Failed to dispatch workspace: {}", e.what()); } return false; } +void Workspaces::setUrgentWorkspace(std::string const &windowaddress) { + const Json::Value clientsJson = gIPC->getSocket1JsonReply("clients"); + int workspaceId = -1; + + for (Json::Value clientJson : clientsJson) { + if (clientJson["address"].asString().ends_with(windowaddress)) { + workspaceId = clientJson["workspace"]["id"].asInt(); + break; + } + } + + auto workspace = + std::find_if(m_workspaces.begin(), m_workspaces.end(), + [workspaceId](std::unique_ptr &x) { return x->id() == workspaceId; }); + if (workspace != m_workspaces.end()) { + workspace->get()->setUrgent(); + } +} + +std::string Workspaces::getRewrite(std::string window_class, std::string window_title) { + std::string windowReprKey; + if (windowRewriteConfigUsesTitle()) { + windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title); + } else { + windowReprKey = fmt::format("class<{}>", window_class); + } + auto const rewriteRule = m_windowRewriteRules.get(windowReprKey); + return fmt::format(fmt::runtime(rewriteRule), fmt::arg("class", window_class), + fmt::arg("title", window_title)); +} + +WindowCreationPayload::WindowCreationPayload(std::string workspace_name, + WindowAddress window_address, std::string window_repr) + : m_window(std::move(window_repr)), + m_windowAddress(std::move(window_address)), + m_workspaceName(std::move(workspace_name)) { + clearAddr(); + clearWorkspaceName(); +} + +WindowCreationPayload::WindowCreationPayload(std::string workspace_name, + WindowAddress window_address, std::string window_class, + std::string window_title) + : m_window(std::make_pair(std::move(window_class), std::move(window_title))), + m_windowAddress(std::move(window_address)), + m_workspaceName(std::move(workspace_name)) { + clearAddr(); + clearWorkspaceName(); +} + +WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data) + : m_window(std::make_pair(client_data["class"].asString(), client_data["title"].asString())), + m_windowAddress(client_data["address"].asString()), + m_workspaceName(client_data["workspace"]["name"].asString()) { + clearAddr(); + clearWorkspaceName(); +} + +std::string WindowCreationPayload::repr(Workspaces &workspace_manager) { + if (std::holds_alternative(m_window)) { + return std::get(m_window); + } + if (std::holds_alternative(m_window)) { + auto [window_class, window_title] = std::get(m_window); + return workspace_manager.getRewrite(window_class, window_title); + } + // Unreachable + spdlog::error("WorkspaceWindow::repr: Unreachable"); + throw std::runtime_error("WorkspaceWindow::repr: Unreachable"); +} + +bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) { + if (std::holds_alternative(m_window)) { + return std::get(m_window).empty(); + } + if (std::holds_alternative(m_window)) { + auto [window_class, window_title] = std::get(m_window); + return (window_class.empty() && + (!workspace_manager.windowRewriteConfigUsesTitle() || window_title.empty())); + } + // Unreachable + spdlog::error("WorkspaceWindow::isEmpty: Unreachable"); + throw std::runtime_error("WorkspaceWindow::isEmpty: Unreachable"); +} + +int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; } + +void WindowCreationPayload::clearAddr() { + // substr(2, ...) is necessary because Hyprland's JSON follows this format: + // 0x{ADDR} + // While Hyprland's IPC follows this format: + // {ADDR} + static const std::string ADDR_PREFIX = "0x"; + static const int ADDR_PREFIX_LEN = ADDR_PREFIX.length(); + + if (m_windowAddress.starts_with(ADDR_PREFIX)) { + m_windowAddress = + m_windowAddress.substr(ADDR_PREFIX_LEN, m_windowAddress.length() - ADDR_PREFIX_LEN); + } +} + +void WindowCreationPayload::clearWorkspaceName() { + // The workspace name may optionally feature "special:" at the beginning. + // If so, we need to remove it because the workspace is saved WITHOUT the + // special qualifier. The reasoning is that not all of Hyprland's IPC events + // use this qualifier, so it's better to be consistent about our uses. + + static const std::string SPECIAL_QUALIFIER_PREFIX = "special:"; + static const int SPECIAL_QUALIFIER_PREFIX_LEN = SPECIAL_QUALIFIER_PREFIX.length(); + + if (m_workspaceName.starts_with(SPECIAL_QUALIFIER_PREFIX)) { + m_workspaceName = m_workspaceName.substr( + SPECIAL_QUALIFIER_PREFIX_LEN, m_workspaceName.length() - SPECIAL_QUALIFIER_PREFIX_LEN); + } + + std::size_t spaceFound = m_workspaceName.find(' '); + if (spaceFound != std::string::npos) { + m_workspaceName.erase(m_workspaceName.begin() + spaceFound, m_workspaceName.end()); + } +} + +void WindowCreationPayload::moveToWorksace(std::string &new_workspace_name) { + m_workspaceName = new_workspace_name; +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 843cd954..8274d323 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -7,6 +7,7 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config) if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); dp.emit(); @@ -45,7 +46,7 @@ auto waybar::modules::Image::update() -> void { if (config_["path"].isString()) { path_ = config_["path"].asString(); } else if (config_["exec"].isString()) { - output_ = util::command::exec(config_["exec"].asString()); + output_ = util::command::exec(config_["exec"].asString(), ""); parseOutputRaw(); } else { path_ = ""; @@ -63,9 +64,11 @@ auto waybar::modules::Image::update() -> void { } image_.set(pixbuf); image_.show(); + box_.get_style_context()->remove_class("empty"); } else { image_.clear(); image_.hide(); + box_.get_style_context()->add_class("empty"); } AModule::update(); diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 4c081d6a..18ce0a7c 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -81,7 +81,7 @@ auto supportsLockStates(const libevdev* dev) -> bool { waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& bar, const Json::Value& config) : AModule(config, "keyboard-state", id, false, !config["disable-scroll"].asBool()), - box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0), + box_(bar.orientation, 0), numlock_label_(""), capslock_label_(""), numlock_format_(config_["format"].isString() ? config_["format"].asString() @@ -132,6 +132,7 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); if (config_["device-path"].isString()) { @@ -142,6 +143,21 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& } } + auto keys = config_["binding-keys"]; + if (keys.isArray()) { + for (const auto& key : keys) { + if (key.isInt()) { + binding_keys.insert(key.asInt()); + } else { + spdlog::warn("Cannot read key binding {} as int.", key.asString()); + } + } + } else { + binding_keys.insert(KEY_CAPSLOCK); + binding_keys.insert(KEY_NUMLOCK); + binding_keys.insert(KEY_SCROLLLOCK); + } + DIR* dev_dir = opendir(devices_path_.c_str()); if (dev_dir == nullptr) { throw errno_error(errno, "Failed to open " + devices_path_); @@ -171,14 +187,8 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& auto state = libinput_event_keyboard_get_key_state(keyboard_event); if (state == LIBINPUT_KEY_STATE_RELEASED) { uint32_t key = libinput_event_keyboard_get_key(keyboard_event); - switch (key) { - case KEY_CAPSLOCK: - case KEY_NUMLOCK: - case KEY_SCROLLLOCK: - dp.emit(); - break; - default: - break; + if (binding_keys.contains(key)) { + dp.emit(); } } } diff --git a/src/modules/load.cpp b/src/modules/load.cpp new file mode 100644 index 00000000..69a37b4e --- /dev/null +++ b/src/modules/load.cpp @@ -0,0 +1,61 @@ +#include "modules/load.hpp" + +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + +waybar::modules::Load::Load(const std::string& id, const Json::Value& config) + : ALabel(config, "load", id, "{load1}", 10) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; +} + +auto waybar::modules::Load::update() -> void { + // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both + auto [load1, load5, load15] = Load::getLoad(); + if (tooltipEnabled()) { + auto tooltip = fmt::format("Load 1: {}\nLoad 5: {}\nLoad 15: {}", load1, load5, load15); + label_.set_tooltip_text(tooltip); + } + auto format = format_; + auto state = getState(load1); + if (!state.empty() && config_["format-" + state].isString()) { + format = config_["format-" + state].asString(); + } + + if (format.empty()) { + event_box_.hide(); + } else { + event_box_.show(); + auto icons = std::vector{state}; + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("load1", load1)); + store.push_back(fmt::arg("load5", load5)); + store.push_back(fmt::arg("load15", load15)); + store.push_back(fmt::arg("icon1", getIcon(load1, icons))); + store.push_back(fmt::arg("icon5", getIcon(load5, icons))); + store.push_back(fmt::arg("icon15", getIcon(load15, icons))); + label_.set_markup(fmt::vformat(format, store)); + } + + // Call parent update + ALabel::update(); +} + +std::tuple waybar::modules::Load::getLoad() { + double load[3]; + if (getloadavg(load, 3) != -1) { + double load1 = std::ceil(load[0] * 100.0) / 100.0; + double load5 = std::ceil(load[1] * 100.0) / 100.0; + double load15 = std::ceil(load[2] * 100.0) / 100.0; + return {load1, load5, load15}; + } + throw std::runtime_error("Can't get system load"); +} diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 73062c76..192e6c1a 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -4,6 +4,7 @@ #include #include +#include #include using namespace waybar::util; @@ -52,10 +53,10 @@ auto waybar::modules::MPD::update() -> void { void waybar::modules::MPD::queryMPD() { if (connection_ != nullptr) { - spdlog::debug("{}: fetching state information", module_name_); + spdlog::trace("{}: fetching state information", module_name_); try { fetchState(); - spdlog::debug("{}: fetch complete", module_name_); + spdlog::trace("{}: fetch complete", module_name_); } catch (std::exception const& e) { spdlog::error("{}: {}", module_name_, e.what()); state_ = MPD_STATE_UNKNOWN; @@ -254,6 +255,21 @@ std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool act } } +static bool isServerUnavailable(const std::error_code& ec) { + if (ec.category() == std::system_category()) { + switch (ec.value()) { + case ECONNREFUSED: + case ECONNRESET: + case ENETDOWN: + case ENETUNREACH: + case EHOSTDOWN: + case ENOENT: + return true; + } + } + return false; +} + void waybar::modules::MPD::tryConnect() { if (connection_ != nullptr) { return; @@ -281,6 +297,11 @@ void waybar::modules::MPD::tryConnect() { } checkErrors(connection_.get()); } + } catch (std::system_error& e) { + /* Tone down logs if it's likely that the mpd server is not running */ + auto level = isServerUnavailable(e.code()) ? spdlog::level::debug : spdlog::level::err; + spdlog::log(level, "{}: Failed to connect to MPD: {}", module_name_, e.what()); + connection_.reset(); } catch (std::runtime_error& e) { spdlog::error("{}: Failed to connect to MPD: {}", module_name_, e.what()); connection_.reset(); @@ -298,6 +319,12 @@ void waybar::modules::MPD::checkErrors(mpd_connection* conn) { connection_.reset(); state_ = MPD_STATE_UNKNOWN; throw std::runtime_error("Connection to MPD closed"); + case MPD_ERROR_SYSTEM: + if (auto ec = mpd_connection_get_system_error(conn); ec != 0) { + mpd_connection_clear_error(conn); + throw std::system_error(ec, std::system_category()); + } + G_GNUC_FALLTHROUGH; default: if (conn) { auto error_message = mpd_connection_get_error_message(conn); diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index aa1a18f8..3d7c8561 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -119,7 +119,7 @@ bool Idle::on_io(Glib::IOCondition const&) { void Playing::entry() noexcept { sigc::slot timer_slot = sigc::mem_fun(*this, &Playing::on_timer); - timer_connection_ = Glib::signal_timeout().connect(timer_slot, /* milliseconds */ 1'000); + timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1); spdlog::debug("mpd: Playing: enabled 1 second periodic timer."); } @@ -327,14 +327,20 @@ void Stopped::pause() { void Stopped::update() noexcept { ctx_->do_update(); } -void Disconnected::arm_timer(int interval) noexcept { +bool Disconnected::arm_timer(int interval) noexcept { + // check if it's necessary to modify the timer + if (timer_connection_ && last_interval_ == interval) { + return true; + } // unregister timer, if present disarm_timer(); // register timer + last_interval_ = interval; sigc::slot timer_slot = sigc::mem_fun(*this, &Disconnected::on_timer); - timer_connection_ = Glib::signal_timeout().connect(timer_slot, interval); - spdlog::debug("mpd: Disconnected: enabled interval timer."); + timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, interval); + spdlog::debug("mpd: Disconnected: enabled {}s interval timer.", interval); + return false; } void Disconnected::disarm_timer() noexcept { @@ -347,7 +353,7 @@ void Disconnected::disarm_timer() noexcept { void Disconnected::entry() noexcept { ctx_->emit(); - arm_timer(1'000); + arm_timer(1 /* second */); } void Disconnected::exit() noexcept { disarm_timer(); } @@ -376,9 +382,7 @@ bool Disconnected::on_timer() { spdlog::warn("mpd: Disconnected: error: {}", e.what()); } - arm_timer(ctx_->interval() * 1'000); - - return false; + return arm_timer(ctx_->interval()); } void Disconnected::update() noexcept { ctx_->do_update(); } diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index aa425489..eea9a82b 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -6,6 +6,8 @@ #include #include +#include "util/scope_guard.hpp" + extern "C" { #include } @@ -18,7 +20,7 @@ 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), + : ALabel(config, "mpris", id, DEFAULT_FORMAT, 0, false, true), tooltip_(DEFAULT_FORMAT), artist_len_(-1), album_len_(-1), @@ -117,6 +119,11 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } GError* error = nullptr; + waybar::util::ScopeGuard error_deleter([error]() { + if (error) { + g_error_free(error); + } + }); manager = playerctl_player_manager_new(&error); if (error) { throw std::runtime_error(fmt::format("unable to create MPRIS client: {}", error->message)); @@ -136,9 +143,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } 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); + throw std::runtime_error(fmt::format("unable to list players: {}", error->message)); } for (auto p = players; p != NULL; p = p->next) { @@ -410,8 +415,7 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye return; } - GError* error = nullptr; - mpris->player = playerctl_player_new_from_name(player_name, &error); + mpris->player = playerctl_player_new_from_name(player_name, NULL); 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", @@ -478,6 +482,11 @@ auto Mpris::getPlayerInfo() -> std::optional { } GError* error = nullptr; + waybar::util::ScopeGuard error_deleter([error]() { + if (error) { + g_error_free(error); + } + }); char* player_status = nullptr; auto player_playback_status = PLAYERCTL_PLAYBACK_STATUS_STOPPED; @@ -487,9 +496,7 @@ auto Mpris::getPlayerInfo() -> std::optional { 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); + throw std::runtime_error(fmt::format("unable to list players: {}", error->message)); } // > get the list of players [..] in order of activity // https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249 @@ -567,13 +574,25 @@ auto Mpris::getPlayerInfo() -> std::optional { return info; errorexit: - spdlog::error("mpris[{}]: {}", info.name, error->message); - g_error_free(error); + std::string errorMsg = error->message; + // When mpris checks for active player sessions periodically(5 secs), NoActivePlayer error + // message is + // thrown when there are no active sessions. This error message is spamming logs without having + // any value addition. Log the error only if the error we recceived is not NoActivePlayer. + if (errorMsg.rfind("GDBus.Error:com.github.altdesktop.playerctld.NoActivePlayer") == + std::string::npos) { + spdlog::error("mpris[{}]: {}", info.name, error->message); + } return std::nullopt; } bool Mpris::handleToggle(GdkEventButton* const& e) { GError* error = nullptr; + waybar::util::ScopeGuard error_deleter([error]() { + if (error) { + g_error_free(error); + } + }); auto info = getPlayerInfo(); if (!info) return false; @@ -587,13 +606,13 @@ bool Mpris::handleToggle(GdkEventButton* const& e) { playerctl_player_play_pause(player, &error); break; case 2: // middle-click - if (config_["on-middle-click"].isString()) { + if (config_["on-click-middle"].isString()) { return ALabel::handleToggle(e); } playerctl_player_previous(player, &error); break; case 3: // right-click - if (config_["on-right-click"].isString()) { + if (config_["on-click-right"].isString()) { return ALabel::handleToggle(e); } playerctl_player_next(player, &error); @@ -603,7 +622,6 @@ bool Mpris::handleToggle(GdkEventButton* const& e) { if (error) { spdlog::error("mpris[{}]: error running builtin on-click action: {}", (*info).name, error->message); - g_error_free(error); return false; } return true; diff --git a/src/modules/network.cpp b/src/modules/network.cpp index ef59c95c..7113cea0 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -670,8 +670,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { higher priority. Disable router -> RTA_GATEWAY -> up new router -> set higher priority added checking route id **/ - if (!is_del_event && - ((net->ifid_ == -1) || (priority < net->route_priority) || (net->ifid_ != temp_idx))) { + if (!is_del_event && ((net->ifid_ == -1) || (priority < net->route_priority))) { // Clear if's state for the case were there is a higher priority // route on a different interface. net->clearIface(); diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp new file mode 100644 index 00000000..ae3d7443 --- /dev/null +++ b/src/modules/power_profiles_daemon.cpp @@ -0,0 +1,206 @@ +#include "modules/power_profiles_daemon.hpp" + +#include +#include +#include +#include + +namespace waybar::modules { + +PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Value& config) + : ALabel(config, "power-profiles-daemon", id, "{icon}", 0, false, true), connected_(false) { + if (config_["tooltip-format"].isString()) { + tooltipFormat_ = config_["tooltip-format"].asString(); + } else { + tooltipFormat_ = "Power profile: {profile}\nDriver: {driver}"; + } + // Fasten your seatbelt, we're up for quite a ride. The rest of the + // init is performed asynchronously. There's 2 callbacks involved. + // Here's the overall idea: + // 1. Async connect to the system bus. + // 2. In the system bus connect callback, try to call + // org.freedesktop.DBus.Properties.GetAll to see if + // power-profiles-daemon is able to respond. + // 3. In the GetAll callback, connect the activeProfile monitoring + // callback, consider the init to be successful. Meaning start + // drawing the module. + // + // There's sadly no other way around that, we have to try to call a + // method on the proxy to see whether or not something's responding + // on the other side. + + // NOTE: the DBus adresses are under migration. They should be + // changed to org.freedesktop.UPower.PowerProfiles at some point. + // + // See + // https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/releases/0.20 + // + // The old name is still announced for now. Let's rather use the old + // adresses for compatibility sake. + // + // Revisit this in 2026, systems should be updated by then. + Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::BUS_TYPE_SYSTEM, "net.hadess.PowerProfiles", + "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", + sigc::mem_fun(*this, &PowerProfilesDaemon::busConnectedCb)); +} + +void PowerProfilesDaemon::busConnectedCb(Glib::RefPtr& r) { + try { + powerProfilesProxy_ = Gio::DBus::Proxy::create_for_bus_finish(r); + using GetAllProfilesVar = Glib::Variant>; + auto callArgs = GetAllProfilesVar::create(std::make_tuple("net.hadess.PowerProfiles")); + powerProfilesProxy_->call("org.freedesktop.DBus.Properties.GetAll", + sigc::mem_fun(*this, &PowerProfilesDaemon::getAllPropsCb), callArgs); + // Connect active profile callback + } catch (const std::exception& e) { + spdlog::error("Failed to create the power profiles daemon DBus proxy: {}", e.what()); + } catch (const Glib::Error& e) { + spdlog::error("Failed to create the power profiles daemon DBus proxy: {}", + std::string(e.what())); + } +} + +// Callback for the GetAll call. +// +// We're abusing this call to make sure power-profiles-daemon is +// available on the host. We're not really using +void PowerProfilesDaemon::getAllPropsCb(Glib::RefPtr& r) { + try { + auto _ = powerProfilesProxy_->call_finish(r); + // Power-profiles-daemon responded something, we can assume it's + // available, we can safely attach the activeProfile monitoring + // now. + connected_ = true; + powerProfilesProxy_->signal_properties_changed().connect( + sigc::mem_fun(*this, &PowerProfilesDaemon::profileChangedCb)); + populateInitState(); + dp.emit(); + } catch (const std::exception& err) { + spdlog::error("Failed to query power-profiles-daemon via dbus: {}", err.what()); + } catch (const Glib::Error& err) { + spdlog::error("Failed to query power-profiles-daemon via dbus: {}", std::string(err.what())); + } +} + +void PowerProfilesDaemon::populateInitState() { + // Retrieve current active profile + Glib::Variant profileStr; + powerProfilesProxy_->get_cached_property(profileStr, "ActiveProfile"); + + // Retrieve profiles list, it's aa{sv}. + using ProfilesType = std::vector>>; + Glib::Variant profilesVariant; + powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles"); + for (auto& variantDict : profilesVariant.get()) { + Glib::ustring name; + Glib::ustring driver; + if (auto p = variantDict.find("Profile"); p != variantDict.end()) { + name = p->second.get(); + } + if (auto d = variantDict.find("Driver"); d != variantDict.end()) { + driver = d->second.get(); + } + if (!name.empty()) { + availableProfiles_.emplace_back(std::move(name), std::move(driver)); + } else { + spdlog::error( + "Power profiles daemon: power-profiles-daemon sent us an empty power profile name. " + "Something is wrong."); + } + } + + // Find the index of the current activated mode (to toggle) + std::string str = profileStr.get(); + switchToProfile(str); + + update(); +} + +void PowerProfilesDaemon::profileChangedCb( + const Gio::DBus::Proxy::MapChangedProperties& changedProperties, + const std::vector& invalidatedProperties) { + // We're likely connected if this callback gets triggered. + // But better be safe than sorry. + if (connected_) { + if (auto activeProfileVariant = changedProperties.find("ActiveProfile"); + activeProfileVariant != changedProperties.end()) { + std::string activeProfile = + Glib::VariantBase::cast_dynamic>(activeProfileVariant->second) + .get(); + switchToProfile(activeProfile); + update(); + } + } +} + +// Look for the profile str in our internal profiles list. Using a +// vector to store the profiles ain't the smartest move +// complexity-wise, but it makes toggling between the mode easy. This +// vector is 3 elements max, we'll be fine :P +void PowerProfilesDaemon::switchToProfile(std::string const& str) { + auto pred = [str](Profile const& p) { return p.name == str; }; + this->activeProfile_ = std::find_if(availableProfiles_.begin(), availableProfiles_.end(), pred); + if (activeProfile_ == availableProfiles_.end()) { + spdlog::error( + "Power profile daemon: can't find the active profile {} in the available profiles list", + str); + } +} + +auto PowerProfilesDaemon::update() -> void { + if (connected_ && activeProfile_ != availableProfiles_.end()) { + auto profile = (*activeProfile_); + // Set label + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("profile", profile.name)); + store.push_back(fmt::arg("driver", profile.driver)); + store.push_back(fmt::arg("icon", getIcon(0, profile.name))); + label_.set_markup(fmt::vformat(format_, store)); + if (tooltipEnabled()) { + label_.set_tooltip_text(fmt::vformat(tooltipFormat_, store)); + } + + // Set CSS class + if (!currentStyle_.empty()) { + label_.get_style_context()->remove_class(currentStyle_); + } + label_.get_style_context()->add_class(profile.name); + currentStyle_ = profile.name; + event_box_.set_visible(true); + } else { + event_box_.set_visible(false); + } + + ALabel::update(); +} + +bool PowerProfilesDaemon::handleToggle(GdkEventButton* const& e) { + if (e->type == GdkEventType::GDK_BUTTON_PRESS && connected_) { + activeProfile_++; + if (activeProfile_ == availableProfiles_.end()) { + activeProfile_ = availableProfiles_.begin(); + } + + using VarStr = Glib::Variant; + using SetPowerProfileVar = Glib::Variant>; + VarStr activeProfileVariant = VarStr::create(activeProfile_->name); + auto callArgs = SetPowerProfileVar::create( + std::make_tuple("net.hadess.PowerProfiles", "ActiveProfile", activeProfileVariant)); + powerProfilesProxy_->call("org.freedesktop.DBus.Properties.Set", + sigc::mem_fun(*this, &PowerProfilesDaemon::setPropCb), callArgs); + } + return true; +} + +void PowerProfilesDaemon::setPropCb(Glib::RefPtr& r) { + try { + auto _ = powerProfilesProxy_->call_finish(r); + update(); + } catch (const std::exception& e) { + spdlog::error("Failed to set the the active power profile: {}", e.what()); + } catch (const Glib::Error& e) { + spdlog::error("Failed to set the active power profile: {}", std::string(e.what())); + } +} + +} // namespace waybar::modules diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp new file mode 100644 index 00000000..64a1572b --- /dev/null +++ b/src/modules/privacy/privacy.cpp @@ -0,0 +1,179 @@ +#include "modules/privacy/privacy.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include "AModule.hpp" +#include "gtkmm/image.h" +#include "modules/privacy/privacy_item.hpp" + +namespace waybar::modules::privacy { + +using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT; +using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT; +using util::PipewireBackend::PRIVACY_NODE_TYPE_NONE; +using util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT; + +Privacy::Privacy(const std::string& id, const Json::Value& config, const std::string& pos) + : AModule(config, "privacy", id), + nodes_screenshare(), + nodes_audio_in(), + nodes_audio_out(), + visibility_conn(), + box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + box_.set_name(name_); + + event_box_.add(box_); + + // Icon Spacing + if (config_["icon-spacing"].isUInt()) { + iconSpacing = config_["icon-spacing"].asUInt(); + } + box_.set_spacing(iconSpacing); + + // Icon Size + if (config_["icon-size"].isUInt()) { + iconSize = config_["icon-size"].asUInt(); + } + + // Transition Duration + if (config_["transition-duration"].isUInt()) { + transition_duration = config_["transition-duration"].asUInt(); + } + + // Initialize each privacy module + Json::Value modules = config_["modules"]; + // Add Screenshare and Mic usage as default modules if none are specified + if (!modules.isArray() || modules.size() == 0) { + modules = Json::Value(Json::arrayValue); + for (auto& type : {"screenshare", "audio-in"}) { + Json::Value obj = Json::Value(Json::objectValue); + obj["type"] = type; + modules.append(obj); + } + } + for (uint i = 0; i < modules.size(); i++) { + const Json::Value& module_config = modules[i]; + if (!module_config.isObject() || !module_config["type"].isString()) continue; + const std::string type = module_config["type"].asString(); + if (type == "screenshare") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_VIDEO_INPUT, + &nodes_screenshare, pos, iconSize, transition_duration); + box_.add(*item); + } else if (type == "audio-in") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_AUDIO_INPUT, + &nodes_audio_in, pos, iconSize, transition_duration); + box_.add(*item); + } else if (type == "audio-out") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_AUDIO_OUTPUT, + &nodes_audio_out, pos, iconSize, transition_duration); + box_.add(*item); + } + } + + backend = util::PipewireBackend::PipewireBackend::getInstance(); + backend->privacy_nodes_changed_signal_event.connect( + sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); + + dp.emit(); +} + +void Privacy::onPrivacyNodesChanged() { + mutex_.lock(); + nodes_audio_out.clear(); + nodes_audio_in.clear(); + nodes_screenshare.clear(); + + for (auto& node : backend->privacy_nodes) { + switch (node.second->state) { + case PW_NODE_STATE_RUNNING: + switch (node.second->type) { + case PRIVACY_NODE_TYPE_VIDEO_INPUT: + nodes_screenshare.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_AUDIO_INPUT: + nodes_audio_in.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + nodes_audio_out.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_NONE: + continue; + } + break; + default: + break; + } + } + + mutex_.unlock(); + dp.emit(); +} + +auto Privacy::update() -> void { + mutex_.lock(); + bool screenshare, audio_in, audio_out; + + for (Gtk::Widget* widget : box_.get_children()) { + PrivacyItem* module = dynamic_cast(widget); + if (!module) continue; + switch (module->privacy_type) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + screenshare = !nodes_screenshare.empty(); + module->set_in_use(screenshare); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + audio_in = !nodes_audio_in.empty(); + module->set_in_use(audio_in); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + audio_out = !nodes_audio_out.empty(); + module->set_in_use(audio_out); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + break; + } + } + mutex_.unlock(); + + // Hide the whole widget if none are in use + bool is_visible = screenshare || audio_in || audio_out; + if (is_visible != event_box_.get_visible()) { + // Disconnect any previous connection so that it doesn't get activated in + // the future, hiding the module when it should be visible + visibility_conn.disconnect(); + if (is_visible) { + event_box_.set_visible(true); + } else { + // Hides the widget when all of the privacy_item revealers animations + // have finished animating + visibility_conn = Glib::signal_timeout().connect( + sigc::track_obj( + [this] { + mutex_.lock(); + bool screenshare = !nodes_screenshare.empty(); + bool audio_in = !nodes_audio_in.empty(); + bool audio_out = !nodes_audio_out.empty(); + mutex_.unlock(); + event_box_.set_visible(screenshare || audio_in || audio_out); + return false; + }, + *this), + transition_duration); + } + } + + // Call parent update + AModule::update(); +} + +} // namespace waybar::modules::privacy diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp new file mode 100644 index 00000000..a0a2da57 --- /dev/null +++ b/src/modules/privacy/privacy_item.cpp @@ -0,0 +1,163 @@ +#include "modules/privacy/privacy_item.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +#include "AModule.hpp" +#include "glibmm/main.h" +#include "glibmm/priorities.h" +#include "gtkmm/enums.h" +#include "gtkmm/label.h" +#include "gtkmm/revealer.h" +#include "gtkmm/tooltip.h" +#include "sigc++/adaptors/bind.h" +#include "util/gtk_icon.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::modules::privacy { + +PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, + std::list *nodes_, const std::string &pos, + const uint icon_size, const uint transition_duration) + : Gtk::Revealer(), + privacy_type(privacy_type_), + nodes(nodes_), + signal_conn(), + tooltip_window(Gtk::ORIENTATION_VERTICAL, 0), + box_(Gtk::ORIENTATION_HORIZONTAL, 0), + icon_() { + switch (privacy_type) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + box_.get_style_context()->add_class("audio-in"); + iconName = "waybar-privacy-audio-input-symbolic"; + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + box_.get_style_context()->add_class("audio-out"); + iconName = "waybar-privacy-audio-output-symbolic"; + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + box_.get_style_context()->add_class("screenshare"); + iconName = "waybar-privacy-screen-share-symbolic"; + break; + default: + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + return; + } + + // Set the reveal transition to not look weird when sliding in + if (pos == "modules-left") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_RIGHT); + } else if (pos == "modules-center") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_CROSSFADE); + } else if (pos == "modules-right") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT); + } + set_transition_duration(transition_duration); + + box_.set_name("privacy-item"); + box_.add(icon_); + icon_.set_pixel_size(icon_size); + add(box_); + + // Icon Name + if (config_["icon-name"].isString()) { + iconName = config_["icon-name"].asString(); + } + icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + + // Tooltip Icon Size + if (config_["tooltip-icon-size"].isUInt()) { + tooltipIconSize = config_["tooltip-icon-size"].asUInt(); + } + // Tooltip + if (config_["tooltip"].isString()) { + tooltip = config_["tooltip"].asBool(); + } + set_has_tooltip(tooltip); + if (tooltip) { + // Sets the window to use when showing the tooltip + update_tooltip(); + this->signal_query_tooltip().connect(sigc::track_obj( + [this](int x, int y, bool keyboard_tooltip, const Glib::RefPtr &tooltip) { + tooltip->set_custom(tooltip_window); + return true; + }, + *this)); + } + + // Don't show by default + set_reveal_child(true); + set_visible(false); +} + +void PrivacyItem::update_tooltip() { + // Removes all old nodes + for (auto child : tooltip_window.get_children()) { + delete child; + } + + for (auto *node : *nodes) { + Gtk::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4); + + // Set device icon + Gtk::Image *node_icon = new Gtk::Image(); + node_icon->set_pixel_size(tooltipIconSize); + node_icon->set_from_icon_name(node->get_icon_name(), Gtk::ICON_SIZE_INVALID); + box->add(*node_icon); + + // Set model + Gtk::Label *node_name = new Gtk::Label(node->get_name()); + box->add(*node_name); + + tooltip_window.add(*box); + } + + tooltip_window.show_all(); +} + +void PrivacyItem::set_in_use(bool in_use) { + if (in_use) { + update_tooltip(); + } + + if (this->in_use == in_use && init) return; + + if (init) { + // Disconnect any previous connection so that it doesn't get activated in + // the future, hiding the module when it should be visible + signal_conn.disconnect(); + + this->in_use = in_use; + guint duration = 0; + if (this->in_use) { + set_visible(true); + } else { + set_reveal_child(false); + duration = get_transition_duration(); + } + + signal_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + if (this->in_use) { + set_reveal_child(true); + } else { + set_visible(false); + } + return false; + }, + *this), + duration); + } else { + set_visible(false); + set_reveal_child(false); + } + this->init = true; +} + +} // namespace waybar::modules::privacy diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index d35e2983..d7dc80d3 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -1,74 +1,12 @@ #include "modules/pulseaudio.hpp" waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value &config) - : ALabel(config, "pulseaudio", id, "{volume}%"), - mainloop_(nullptr), - mainloop_api_(nullptr), - context_(nullptr), - sink_idx_(0), - volume_(0), - muted_(false), - source_idx_(0), - source_volume_(0), - source_muted_(false) { - mainloop_ = pa_threaded_mainloop_new(); - if (mainloop_ == nullptr) { - throw std::runtime_error("pa_mainloop_new() failed."); - } - pa_threaded_mainloop_lock(mainloop_); - mainloop_api_ = pa_threaded_mainloop_get_api(mainloop_); - context_ = pa_context_new(mainloop_api_, "waybar"); - if (context_ == nullptr) { - throw std::runtime_error("pa_context_new() failed."); - } - if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFAIL, nullptr) < 0) { - auto err = - fmt::format("pa_context_connect() failed: {}", pa_strerror(pa_context_errno(context_))); - throw std::runtime_error(err); - } - pa_context_set_state_callback(context_, contextStateCb, this); - if (pa_threaded_mainloop_start(mainloop_) < 0) { - throw std::runtime_error("pa_mainloop_run() failed."); - } - pa_threaded_mainloop_unlock(mainloop_); + : ALabel(config, "pulseaudio", id, "{volume}%") { event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Pulseaudio::handleScroll)); -} -waybar::modules::Pulseaudio::~Pulseaudio() { - pa_context_disconnect(context_); - mainloop_api_->quit(mainloop_api_, 0); - pa_threaded_mainloop_stop(mainloop_); - pa_threaded_mainloop_free(mainloop_); -} - -void waybar::modules::Pulseaudio::contextStateCb(pa_context *c, void *data) { - auto pa = static_cast(data); - switch (pa_context_get_state(c)) { - case PA_CONTEXT_TERMINATED: - pa->mainloop_api_->quit(pa->mainloop_api_, 0); - break; - case PA_CONTEXT_READY: - pa_context_get_server_info(c, serverInfoCb, data); - pa_context_set_subscribe_callback(c, subscribeCb, data); - pa_context_subscribe(c, - static_cast( - static_cast(PA_SUBSCRIPTION_MASK_SERVER) | - static_cast(PA_SUBSCRIPTION_MASK_SINK) | - static_cast(PA_SUBSCRIPTION_MASK_SINK_INPUT) | - static_cast(PA_SUBSCRIPTION_MASK_SOURCE) | - static_cast(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)), - nullptr, nullptr); - break; - case PA_CONTEXT_FAILED: - pa->mainloop_api_->quit(pa->mainloop_api_, 1); - break; - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - default: - break; - } + backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); + backend->setIgnoredSinks(config_["ignored-sinks"]); } bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { @@ -81,9 +19,6 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { if (dir == SCROLL_DIR::NONE) { return true; } - double volume_tick = static_cast(PA_VOLUME_NORM) / 100; - pa_volume_t change = volume_tick; - pa_cvolume pa_volume = pa_volume_; int max_volume = 100; double step = 1; // isDouble returns true for integers as well, just in case @@ -91,152 +26,24 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { step = config_["scroll-step"].asDouble(); } if (config_["max-volume"].isInt()) { - max_volume = std::min(config_["max-volume"].asInt(), static_cast(PA_VOLUME_UI_MAX)); + max_volume = config_["max-volume"].asInt(); } - if (dir == SCROLL_DIR::UP) { - if (volume_ < max_volume) { - if (volume_ + step > max_volume) { - change = round((max_volume - volume_) * volume_tick); - } else { - change = round(step * volume_tick); - } - pa_cvolume_inc(&pa_volume, change); - } - } else if (dir == SCROLL_DIR::DOWN) { - if (volume_ > 0) { - if (volume_ - step < 0) { - change = round(volume_ * volume_tick); - } else { - change = round(step * volume_tick); - } - pa_cvolume_dec(&pa_volume, change); - } - } - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + auto change_type = (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::RIGHT) + ? util::ChangeType::Increase + : util::ChangeType::Decrease; + + backend->changeVolume(change_type, step, max_volume); return true; } -/* - * Called when an event we subscribed to occurs. - */ -void waybar::modules::Pulseaudio::subscribeCb(pa_context *context, - pa_subscription_event_type_t type, uint32_t idx, - void *data) { - unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; - unsigned operation = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK; - if (operation != PA_SUBSCRIPTION_EVENT_CHANGE) { - return; - } - if (facility == PA_SUBSCRIPTION_EVENT_SERVER) { - pa_context_get_server_info(context, serverInfoCb, data); - } else if (facility == PA_SUBSCRIPTION_EVENT_SINK) { - pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data); - } else if (facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT) { - pa_context_get_sink_info_list(context, sinkInfoCb, data); - } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE) { - pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data); - } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT) { - pa_context_get_source_info_list(context, sourceInfoCb, data); - } -} - -/* - * Called in response to a volume change request - */ -void waybar::modules::Pulseaudio::volumeModifyCb(pa_context *c, int success, void *data) { - auto pa = static_cast(data); - if (success != 0) { - pa_context_get_sink_info_by_index(pa->context_, pa->sink_idx_, sinkInfoCb, data); - } -} - -/* - * Called when the requested source information is ready. - */ -void waybar::modules::Pulseaudio::sourceInfoCb(pa_context * /*context*/, const pa_source_info *i, - int /*eol*/, void *data) { - auto pa = static_cast(data); - if (i != nullptr && pa->default_source_name_ == i->name) { - auto source_volume = static_cast(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM}; - pa->source_volume_ = std::round(source_volume * 100.0F); - pa->source_idx_ = i->index; - pa->source_muted_ = i->mute != 0; - pa->source_desc_ = i->description; - pa->source_port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; - pa->dp.emit(); - } -} - -/* - * Called when the requested sink information is ready. - */ -void waybar::modules::Pulseaudio::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, - int /*eol*/, void *data) { - if (i == nullptr) return; - - auto pa = static_cast(data); - - if (pa->config_["ignored-sinks"].isArray()) { - for (const auto &ignored_sink : pa->config_["ignored-sinks"]) { - if (ignored_sink.asString() == i->description) { - return; - } - } - } - - if (pa->current_sink_name_ == i->name) { - if (i->state != PA_SINK_RUNNING) { - pa->current_sink_running_ = false; - } else { - pa->current_sink_running_ = true; - } - } - - if (!pa->current_sink_running_ && i->state == PA_SINK_RUNNING) { - pa->current_sink_name_ = i->name; - pa->current_sink_running_ = true; - } - - if (pa->current_sink_name_ == i->name) { - pa->pa_volume_ = i->volume; - float volume = static_cast(pa_cvolume_avg(&(pa->pa_volume_))) / float{PA_VOLUME_NORM}; - pa->sink_idx_ = i->index; - pa->volume_ = std::round(volume * 100.0F); - pa->muted_ = i->mute != 0; - pa->desc_ = i->description; - pa->monitor_ = i->monitor_source_name; - pa->port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; - if (auto ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) { - pa->form_factor_ = ff; - } else { - pa->form_factor_ = ""; - } - pa->dp.emit(); - } -} - -/* - * Called when the requested information on the server is ready. This is - * used to find the default PulseAudio sink. - */ -void waybar::modules::Pulseaudio::serverInfoCb(pa_context *context, const pa_server_info *i, - void *data) { - auto pa = static_cast(data); - pa->current_sink_name_ = i->default_sink_name; - pa->default_source_name_ = i->default_source_name; - - pa_context_get_sink_info_list(context, sinkInfoCb, data); - pa_context_get_source_info_list(context, sourceInfoCb, data); -} - static const std::array ports = { "headphone", "speaker", "hdmi", "headset", "hands-free", "portable", "car", "hifi", "phone", }; const std::vector waybar::modules::Pulseaudio::getPulseIcon() const { - std::vector res = {current_sink_name_, default_source_name_}; - std::string nameLC = port_name_ + form_factor_; + std::vector res = {backend->getCurrentSinkName(), backend->getDefaultSourceName()}; + std::string nameLC = backend->getSinkPortName() + backend->getFormFactor(); std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower); for (auto const &port : ports) { if (nameLC.find(port) != std::string::npos) { @@ -250,17 +57,16 @@ const std::vector waybar::modules::Pulseaudio::getPulseIcon() const auto waybar::modules::Pulseaudio::update() -> void { auto format = format_; std::string tooltip_format; + auto sink_volume = backend->getSinkVolume(); if (!alt_) { std::string format_name = "format"; - if (monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio - monitor_.find("a2dp-sink") != std::string::npos || // PipeWire - monitor_.find("bluez") != std::string::npos) { + if (backend->isBluetooth()) { format_name = format_name + "-bluetooth"; label_.get_style_context()->add_class("bluetooth"); } else { label_.get_style_context()->remove_class("bluetooth"); } - if (muted_) { + if (backend->getSinkMuted()) { // Check muted bluetooth format exist, otherwise fallback to default muted format if (format_name != "format" && !config_[format_name + "-muted"].isString()) { format_name = "format"; @@ -272,7 +78,7 @@ auto waybar::modules::Pulseaudio::update() -> void { label_.get_style_context()->remove_class("muted"); label_.get_style_context()->remove_class("sink-muted"); } - auto state = getState(volume_, true); + auto state = getState(sink_volume, true); if (!state.empty() && config_[format_name + "-" + state].isString()) { format = config_[format_name + "-" + state].asString(); } else if (config_[format_name].isString()) { @@ -281,7 +87,7 @@ auto waybar::modules::Pulseaudio::update() -> void { } // TODO: find a better way to split source/sink std::string format_source = "{volume}%"; - if (source_muted_) { + if (backend->getSourceMuted()) { label_.get_style_context()->add_class("source-muted"); if (config_["format-source-muted"].isString()) { format_source = config_["format-source-muted"].asString(); @@ -292,11 +98,16 @@ auto waybar::modules::Pulseaudio::update() -> void { format_source = config_["format-source"].asString(); } } - format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume_)); + + auto source_volume = backend->getSourceVolume(); + auto sink_desc = backend->getSinkDesc(); + auto source_desc = backend->getSourceDesc(); + + format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume)); auto text = fmt::format( - 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()))); + fmt::runtime(format), fmt::arg("desc", sink_desc), fmt::arg("volume", sink_volume), + fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume), + fmt::arg("source_desc", source_desc), fmt::arg("icon", getIcon(sink_volume, getPulseIcon()))); if (text.empty()) { label_.hide(); } else { @@ -310,12 +121,12 @@ auto waybar::modules::Pulseaudio::update() -> void { } if (!tooltip_format.empty()) { label_.set_tooltip_text(fmt::format( - 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())))); + fmt::runtime(tooltip_format), fmt::arg("desc", sink_desc), + fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source), + fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc), + fmt::arg("icon", getIcon(sink_volume, getPulseIcon())))); } else { - label_.set_tooltip_text(desc_); + label_.set_tooltip_text(sink_desc); } } diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp new file mode 100644 index 00000000..bf85584e --- /dev/null +++ b/src/modules/pulseaudio_slider.cpp @@ -0,0 +1,82 @@ +#include "modules/pulseaudio_slider.hpp" + +namespace waybar::modules { + +PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& config) + : ASlider(config, "pulseaudio-slider", id) { + backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); + backend->setIgnoredSinks(config_["ignored-sinks"]); + + if (config_["target"].isString()) { + std::string target = config_["target"].asString(); + if (target == "sink") { + this->target = PulseaudioSliderTarget::Sink; + } else if (target == "source") { + this->target = PulseaudioSliderTarget::Source; + } + } +} + +void PulseaudioSlider::update() { + switch (target) { + case PulseaudioSliderTarget::Sink: + if (backend->getSinkMuted()) { + scale_.set_value(min_); + } else { + scale_.set_value(backend->getSinkVolume()); + } + break; + + case PulseaudioSliderTarget::Source: + if (backend->getSourceMuted()) { + scale_.set_value(min_); + } else { + scale_.set_value(backend->getSourceVolume()); + } + break; + } +} + +void PulseaudioSlider::onValueChanged() { + bool is_mute = false; + + switch (target) { + case PulseaudioSliderTarget::Sink: + if (backend->getSinkMuted()) { + is_mute = true; + } + break; + + case PulseaudioSliderTarget::Source: + if (backend->getSourceMuted()) { + is_mute = true; + } + break; + } + + uint16_t volume = scale_.get_value(); + + if (is_mute) { + // Avoid setting sink/source to volume 0 if the user muted if via another mean. + if (volume == 0) { + return; + } + + // If the sink/source is mute, but the user clicked the slider, unmute it! + else { + switch (target) { + case PulseaudioSliderTarget::Sink: + backend->toggleSinkMute(false); + break; + + case PulseaudioSliderTarget::Source: + backend->toggleSourceMute(false); + break; + } + } + } + + backend->changeVolume(volume, min_, max_); +} + +} // namespace waybar::modules \ No newline at end of file diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index baa6b7ec..9e7cd5aa 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -87,7 +87,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con control_{nullptr}, seat_{nullptr}, bar_(bar), - box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + box_{bar.orientation, 0}, output_status_{nullptr} { struct wl_display *display = Client::inst()->wl_display; struct wl_registry *registry = wl_display_get_registry(display); @@ -111,6 +111,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); // Default to 9 tags, cap at 32 diff --git a/src/modules/simpleclock.cpp b/src/modules/simpleclock.cpp index 27c7ac77..b6a96ecc 100644 --- a/src/modules/simpleclock.cpp +++ b/src/modules/simpleclock.cpp @@ -18,13 +18,13 @@ auto waybar::modules::Clock::update() -> void { tzset(); // Update timezone information auto now = std::chrono::system_clock::now(); auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now)); - auto text = fmt::format(format_, localtime); + auto text = fmt::format(fmt::runtime(format_), localtime); label_.set_markup(text); if (tooltipEnabled()) { if (config_["tooltip-format"].isString()) { auto tooltip_format = config_["tooltip-format"].asString(); - auto tooltip_text = fmt::format(tooltip_format, localtime); + auto tooltip_text = fmt::format(fmt::runtime(tooltip_format), localtime); label_.set_tooltip_text(tooltip_text); } else { label_.set_tooltip_text(text); diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 0bbd4d2f..54faa16c 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -2,6 +2,8 @@ #include +#include "util/scope_guard.hpp" + namespace waybar::modules::SNI { Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, @@ -57,17 +59,20 @@ void Host::nameVanished(const Glib::RefPtr& conn, const G void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { GError* error = nullptr; + waybar::util::ScopeGuard error_deleter([error]() { + if (error != nullptr) { + g_error_free(error); + } + }); SnWatcher* watcher = sn_watcher_proxy_new_finish(res, &error); if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { spdlog::error("Host: {}", error->message); - g_error_free(error); return; } auto host = static_cast(data); host->watcher_ = watcher; if (error != nullptr) { spdlog::error("Host: {}", error->message); - g_error_free(error); return; } sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_, @@ -76,16 +81,19 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { GError* error = nullptr; + waybar::util::ScopeGuard error_deleter([error]() { + if (error != nullptr) { + g_error_free(error); + } + }); sn_watcher_call_register_host_finish(SN_WATCHER(src), res, &error); if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { spdlog::error("Host: {}", error->message); - g_error_free(error); return; } auto host = static_cast(data); if (error != nullptr) { spdlog::error("Host: {}", error->message); - g_error_free(error); return; } g_signal_connect(host->watcher_, "item-registered", G_CALLBACK(&Host::itemRegistered), data); diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 9d3fc4bd..c3de2357 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -22,7 +22,7 @@ struct fmt::formatter : formatter { template auto format(const Glib::VariantBase& value, FormatContext& ctx) { if (is_printable(value)) { - return formatter::format(value.print(), ctx); + return formatter::format(static_cast(value.print()), ctx); } else { return formatter::format(value.get_type_string(), ctx); } @@ -39,7 +39,8 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf object_path(op), icon_size(16), effective_icon_size(0), - icon_theme(Gtk::IconTheme::create()) { + icon_theme(Gtk::IconTheme::create()), + bar_(bar) { if (config["icon-size"].isUInt()) { icon_size = config["icon-size"].asUInt(); } @@ -355,32 +356,15 @@ Glib::RefPtr Item::getIconPixbuf() { } Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { - int tmp_size = 0; icon_theme->rescan_if_needed(); - auto sizes = icon_theme->get_icon_sizes(name.c_str()); - for (auto const& size : sizes) { - // -1 == scalable - if (size == request_size || size == -1) { - tmp_size = request_size; - break; - } else if (size < request_size) { - tmp_size = size; - } else if (size > tmp_size && tmp_size > 0) { - tmp_size = request_size; - break; - } - } - if (tmp_size == 0) { - tmp_size = request_size; - } if (!icon_theme_path.empty() && - icon_theme->lookup_icon(name.c_str(), tmp_size, + icon_theme->lookup_icon(name.c_str(), request_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE)) { - return icon_theme->load_icon(name.c_str(), tmp_size, + return icon_theme->load_icon(name.c_str(), request_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); } - return DefaultGtkIconThemeWrapper::load_icon(name.c_str(), tmp_size, + return DefaultGtkIconThemeWrapper::load_icon(name.c_str(), request_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); } @@ -410,7 +394,8 @@ void Item::makeMenu() { bool Item::handleClick(GdkEventButton* const& ev) { auto parameters = Glib::VariantContainerBase::create_tuple( - {Glib::Variant::create(ev->x), Glib::Variant::create(ev->y)}); + {Glib::Variant::create(ev->x_root + bar_.x_global), + Glib::Variant::create(ev->y_root + bar_.y_global)}); if ((ev->button == 1 && item_is_menu) || ev->button == 3) { makeMenu(); if (gtk_menu != nullptr) { diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 09d53e7f..a2c56808 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -6,7 +6,7 @@ namespace waybar::modules::SNI { Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) : AModule(config, "tray", id), - box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0), + box_(bar.orientation, 0), watcher_(SNI::Watcher::getInstance()), host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1), std::bind(&Tray::onRemove, this, std::placeholders::_1)) { @@ -15,6 +15,7 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); if (config_["spacing"].isUInt()) { box_.set_spacing(config_["spacing"].asUInt()); } @@ -38,7 +39,7 @@ void Tray::onRemove(std::unique_ptr& item) { auto Tray::update() -> void { // Show tray only when items are available - box_.set_visible(!box_.get_children().empty()); + event_box_.set_visible(!box_.get_children().empty()); // Call parent update AModule::update(); } diff --git a/src/modules/sni/watcher.cpp b/src/modules/sni/watcher.cpp index dfd076ef..8c035ae1 100644 --- a/src/modules/sni/watcher.cpp +++ b/src/modules/sni/watcher.cpp @@ -2,6 +2,8 @@ #include +#include "util/scope_guard.hpp" + using namespace waybar::modules::SNI; Watcher::Watcher() @@ -29,6 +31,11 @@ Watcher::~Watcher() { void Watcher::busAcquired(const Glib::RefPtr& conn, Glib::ustring name) { GError* error = nullptr; + waybar::util::ScopeGuard error_deleter([error]() { + if (error) { + g_error_free(error); + } + }); g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(watcher_), conn->gobj(), "/StatusNotifierWatcher", &error); if (error != nullptr) { @@ -36,7 +43,6 @@ void Watcher::busAcquired(const Glib::RefPtr& conn, Glib: if (error->code != 2) { spdlog::error("Watcher: {}", error->message); } - g_error_free(error); return; } g_signal_connect_swapped(watcher_, "handle-register-item", diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index a5860bd0..a005df17 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -19,6 +19,7 @@ const std::string Language::XKB_ACTIVE_LAYOUT_NAME_KEY = "xkb_active_layout_name Language::Language(const std::string& id, const Json::Value& config) : ALabel(config, "language", id, "{}", 0, true) { + hide_single_ = config["hide-single-layout"].isBool() && config["hide-single-layout"].asBool(); is_variant_displayed = format_.find("{variant}") != std::string::npos; if (format_.find("{}") != std::string::npos || format_.find("{short}") != std::string::npos) { displayed_short_flag |= static_cast(DispayedShortFlag::ShortName); @@ -95,6 +96,10 @@ void Language::onEvent(const struct Ipc::ipc_response& res) { auto Language::update() -> void { std::lock_guard lock(mutex_); + if (hide_single_ && layouts_map_.size() <= 1) { + event_box_.hide(); + return; + } auto display_layout = trim(fmt::format( fmt::runtime(format_), fmt::arg("short", layout_.short_name), fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name), diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index a5e5fa75..6464bf9a 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -24,10 +24,28 @@ int Workspaces::convertWorkspaceNameToNum(std::string name) { return -1; } +int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) { + // Rules that match against title are prioritized + // Rules that don't specify if they're matching against either title or class are deprioritized + bool const hasTitle = window_rule.find("title") != std::string::npos; + bool const hasClass = window_rule.find("class") != std::string::npos; + + if (hasTitle && hasClass) { + return 3; + } + if (hasTitle) { + return 2; + } + if (hasClass) { + return 1; + } + return 0; +} + Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) : AModule(config, "workspaces", id, false, !config["disable-scroll"].asBool()), bar_(bar), - box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) { + box_(bar.orientation, 0) { if (config["format-icons"]["high-priority-named"].isArray()) { for (auto &it : config["format-icons"]["high-priority-named"]) { high_priority_named_.push_back(it.asString()); @@ -37,11 +55,27 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); + if (config_["format-window-separator"].isString()) { + m_formatWindowSeperator = config_["format-window-separator"].asString(); + } else { + m_formatWindowSeperator = " "; + } + const Json::Value &windowRewrite = config["window-rewrite"]; + + const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"]; + m_windowRewriteDefault = + windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; + + m_windowRewriteRules = waybar::util::RegexCollection( + windowRewrite, m_windowRewriteDefault, + [this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); }); ipc_.subscribe(R"(["workspace"])"); + ipc_.subscribe(R"(["window"])"); ipc_.signal_event.connect(sigc::mem_fun(*this, &Workspaces::onEvent)); ipc_.signal_cmd.connect(sigc::mem_fun(*this, &Workspaces::onCmd)); - ipc_.sendCmd(IPC_GET_WORKSPACES); + ipc_.sendCmd(IPC_GET_TREE); if (config["enable-bar-scroll"].asBool()) { auto &window = const_cast(bar_).window; window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); @@ -59,29 +93,50 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value void Workspaces::onEvent(const struct Ipc::ipc_response &res) { try { - ipc_.sendCmd(IPC_GET_WORKSPACES); + ipc_.sendCmd(IPC_GET_TREE); } catch (const std::exception &e) { spdlog::error("Workspaces: {}", e.what()); } } void Workspaces::onCmd(const struct Ipc::ipc_response &res) { - if (res.type == IPC_GET_WORKSPACES) { + if (res.type == IPC_GET_TREE) { try { { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); workspaces_.clear(); - std::copy_if(payload.begin(), payload.end(), std::back_inserter(workspaces_), - [&](const auto &workspace) { - return !config_["all-outputs"].asBool() - ? workspace["output"].asString() == bar_.output->name - : true; + std::vector outputs; + bool alloutputs = config_["all-outputs"].asBool(); + std::copy_if(payload["nodes"].begin(), payload["nodes"].end(), std::back_inserter(outputs), + [&](const auto &output) { + if (alloutputs && output["name"].asString() != "__i3") { + return true; + } + if (output["name"].asString() == bar_.output->name) { + return true; + } + return false; }); - // adding persistent workspaces (as per the config file) + for (auto &output : outputs) { + std::copy(output["nodes"].begin(), output["nodes"].end(), + std::back_inserter(workspaces_)); + std::copy(output["floating_nodes"].begin(), output["floating_nodes"].end(), + std::back_inserter(workspaces_)); + } if (config_["persistent_workspaces"].isObject()) { - const Json::Value &p_workspaces = config_["persistent_workspaces"]; + spdlog::warn( + "persistent_workspaces is deprecated. Please change config to use " + "persistent-workspaces."); + } + + // adding persistent workspaces (as per the config file) + if (config_["persistent-workspaces"].isObject() || + config_["persistent_workspaces"].isObject()) { + const Json::Value &p_workspaces = config_["persistent-workspaces"].isObject() + ? config_["persistent-workspaces"] + : config_["persistent_workspaces"]; const std::vector p_workspaces_names = p_workspaces.getMemberNames(); for (const std::string &p_w_name : p_workspaces_names) { @@ -194,6 +249,40 @@ bool Workspaces::filterButtons() { return needReorder; } +bool Workspaces::hasFlag(const Json::Value &node, const std::string &flag) { + if (node[flag].asBool()) { + return true; + } + + if (std::any_of(node["nodes"].begin(), node["nodes"].end(), + [&](auto const &e) { return hasFlag(e, flag); })) { + return true; + } + return false; +} + +void Workspaces::updateWindows(const Json::Value &node, std::string &windows) { + auto format = config_["window-format"].asString(); + if ((node["type"].asString() == "con" || node["type"].asString() == "floating_con") && + node["name"].isString()) { + std::string title = g_markup_escape_text(node["name"].asString().c_str(), -1); + std::string windowClass = node["app_id"].asString(); + std::string windowReprKey = fmt::format("class<{}> title<{}>", windowClass, title); + std::string window = m_windowRewriteRules.get(windowReprKey); + // allow result to have formatting + window = + fmt::format(fmt::runtime(window), fmt::arg("name", title), fmt::arg("class", windowClass)); + windows.append(window); + windows.append(m_formatWindowSeperator); + } + for (const Json::Value &child : node["nodes"]) { + updateWindows(child, windows); + } + for (const Json::Value &child : node["floating_nodes"]) { + updateWindows(child, windows); + } +} + auto Workspaces::update() -> void { std::lock_guard lock(mutex_); bool needReorder = filterButtons(); @@ -203,22 +292,25 @@ auto Workspaces::update() -> void { needReorder = true; } auto &button = bit == buttons_.end() ? addButton(*it) : bit->second; - if ((*it)["focused"].asBool()) { + if (needReorder) { + box_.reorder_child(button, it - workspaces_.begin()); + } + if (hasFlag((*it), "focused")) { button.get_style_context()->add_class("focused"); } else { button.get_style_context()->remove_class("focused"); } - if ((*it)["visible"].asBool()) { + if (hasFlag((*it), "visible")) { button.get_style_context()->add_class("visible"); } else { button.get_style_context()->remove_class("visible"); } - if ((*it)["urgent"].asBool()) { + if (hasFlag((*it), "urgent")) { button.get_style_context()->add_class("urgent"); } else { button.get_style_context()->remove_class("urgent"); } - if ((*it)["target_output"].isString()) { + if (hasFlag((*it), "target_output")) { button.get_style_context()->add_class("persistent"); } else { button.get_style_context()->remove_class("persistent"); @@ -232,16 +324,19 @@ auto Workspaces::update() -> void { } else { button.get_style_context()->remove_class("current_output"); } - if (needReorder) { - box_.reorder_child(button, it - workspaces_.begin()); - } std::string output = (*it)["name"].asString(); + std::string windows = ""; + if (config_["window-format"].isString()) { + updateWindows((*it), windows); + } if (config_["format"].isString()) { auto format = config_["format"].asString(); - 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("output", (*it)["output"].asString())); + 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("windows", + windows.substr(0, windows.length() - m_formatWindowSeperator.length())), + fmt::arg("output", (*it)["output"].asString())); } if (!config_["disable-markup"].asBool()) { static_cast(button.get_children()[0])->set_markup(output); @@ -305,7 +400,7 @@ std::string Workspaces::getIcon(const std::string &name, const Json::Value &node if (config_["format-icons"][key].isString() && node[key].asBool()) { return config_["format-icons"][key].asString(); } - } else if (config_["format_icons"]["persistent"].isString() && + } else if (config_["format-icons"]["persistent"].isString() && node["target_output"].isString()) { return config_["format-icons"]["persistent"].asString(); } else if (config_["format-icons"][key].isString()) { @@ -332,7 +427,7 @@ bool Workspaces::handleScroll(GdkEventScroll *e) { { std::lock_guard lock(mutex_); auto it = std::find_if(workspaces_.begin(), workspaces_.end(), - [](const auto &workspace) { return workspace["focused"].asBool(); }); + [](const auto &workspace) { return hasFlag(workspace, "focused"); }); if (it == workspaces_.end()) { return true; } diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp new file mode 100644 index 00000000..56e624cf --- /dev/null +++ b/src/modules/systemd_failed_units.cpp @@ -0,0 +1,130 @@ +#include "modules/systemd_failed_units.hpp" + +#include +#include +#include + +#include + +static const unsigned UPDATE_DEBOUNCE_TIME_MS = 1000; + +namespace waybar::modules { + +SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& config) + : ALabel(config, "systemd-failed-units", id, "{nr_failed} failed", 1), + hide_on_ok(true), + update_pending(false), + nr_failed_system(0), + nr_failed_user(0), + last_status() { + if (config["hide-on-ok"].isBool()) { + hide_on_ok = config["hide-on-ok"].asBool(); + } + if (config["format-ok"].isString()) { + format_ok = config["format-ok"].asString(); + } else { + format_ok = format_; + } + + /* Default to enable both "system" and "user". */ + if (!config["system"].isBool() || config["system"].asBool()) { + system_proxy = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties"); + if (!system_proxy) { + throw std::runtime_error("Unable to connect to systemwide systemd DBus!"); + } + system_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + } + if (!config["user"].isBool() || config["user"].asBool()) { + user_proxy = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties"); + if (!user_proxy) { + throw std::runtime_error("Unable to connect to user systemd DBus!"); + } + user_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + } + + updateData(); + /* Always update for the first time. */ + dp.emit(); +} + +SystemdFailedUnits::~SystemdFailedUnits() { + if (system_proxy) system_proxy.reset(); + if (user_proxy) user_proxy.reset(); +} + +auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, + const Glib::ustring& signal_name, + const Glib::VariantContainerBase& arguments) -> void { + if (signal_name == "PropertiesChanged" && !update_pending) { + update_pending = true; + /* The fail count may fluctuate due to restarting. */ + Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &SystemdFailedUnits::updateData), + UPDATE_DEBOUNCE_TIME_MS); + } +} + +void SystemdFailedUnits::updateData() { + update_pending = false; + + auto load = [](const char* kind, Glib::RefPtr& proxy) -> uint32_t { + try { + auto parameters = Glib::VariantContainerBase( + g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "NFailedUnits")); + Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); + if (data && data.is_of_type(Glib::VariantType("(v)"))) { + Glib::VariantBase variant; + g_variant_get(data.gobj_copy(), "(v)", &variant); + if (variant && variant.is_of_type(Glib::VARIANT_TYPE_UINT32)) { + uint32_t value = 0; + g_variant_get(variant.gobj_copy(), "u", &value); + return value; + } + } + } catch (Glib::Error& e) { + spdlog::error("Failed to get {} failed units: {}", kind, e.what().c_str()); + } + return 0; + }; + + if (system_proxy) { + nr_failed_system = load("systemwide", system_proxy); + } + if (user_proxy) { + nr_failed_user = load("user", user_proxy); + } + dp.emit(); +} + +auto SystemdFailedUnits::update() -> void { + uint32_t nr_failed = nr_failed_system + nr_failed_user; + + // Hide if needed. + if (nr_failed == 0 && hide_on_ok) { + event_box_.set_visible(false); + return; + } + if (!event_box_.get_visible()) { + event_box_.set_visible(true); + } + + // Set state class. + const std::string status = nr_failed == 0 ? "ok" : "degraded"; + if (!last_status.empty() && label_.get_style_context()->has_class(last_status)) { + label_.get_style_context()->remove_class(last_status); + } + if (!label_.get_style_context()->has_class(status)) { + label_.get_style_context()->add_class(status); + } + last_status = status; + + label_.set_markup(fmt::format( + fmt::runtime(nr_failed == 0 ? format_ok : format_), fmt::arg("nr_failed", nr_failed), + fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user))); + ALabel::update(); +} + +} // namespace waybar::modules diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index 5ef2f4c9..accab969 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -24,11 +24,16 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val } } } else if (config_["hwmon-path-abs"].isString() && config_["input-filename"].isString()) { - file_path_ = (*std::filesystem::directory_iterator(config_["hwmon-path-abs"].asString())) - .path() - .string() + - "/" + config_["input-filename"].asString(); - } else { + for (const auto& hwmon : + std::filesystem::directory_iterator(config_["hwmon-path-abs"].asString())) { + if (hwmon.path().filename().string().starts_with("hwmon")) { + file_path_ = hwmon.path().string() + "/" + config_["input-filename"].asString(); + break; + } + } + } + + if (file_path_.empty()) { auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); } diff --git a/src/modules/upower/upower.cpp b/src/modules/upower/upower.cpp index 1262d0a1..3554d43b 100644 --- a/src/modules/upower/upower.cpp +++ b/src/modules/upower/upower.cpp @@ -18,7 +18,15 @@ UPower::UPower(const std::string& id, const Json::Value& config) m_Mutex(), client(), showAltText(false) { - box_.pack_start(icon_); + // Show icon only when "show-icon" isn't set to false + if (config_["show-icon"].isBool()) { + showIcon = config_["show-icon"].asBool(); + } + + if (showIcon) { + box_.pack_start(icon_); + } + box_.pack_start(label_); box_.set_name(name_); event_box_.add(box_); @@ -63,7 +71,7 @@ UPower::UPower(const std::string& id, const Json::Value& config) box_.set_has_tooltip(tooltip_enabled); if (tooltip_enabled) { // Sets the window to use when showing the tooltip - upower_tooltip = new UPowerTooltip(iconSize, tooltip_spacing, tooltip_padding); + upower_tooltip = std::make_unique(iconSize, tooltip_spacing, tooltip_padding); box_.set_tooltip_window(*upower_tooltip); box_.signal_query_tooltip().connect(sigc::mem_fun(*this, &UPower::show_tooltip_callback)); } @@ -72,14 +80,13 @@ UPower::UPower(const std::string& id, const Json::Value& config) G_BUS_NAME_WATCHER_FLAGS_AUTO_START, upowerAppear, upowerDisappear, this, NULL); - GError* error = NULL; - client = up_client_new_full(NULL, &error); + client = up_client_new_full(NULL, NULL); if (client == NULL) { throw std::runtime_error("Unable to create UPower client!"); } // Connect to Login1 PrepareForSleep signal - login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL); if (!login1_connection) { throw std::runtime_error("Unable to connect to the SYSTEM Bus!..."); } else { @@ -99,6 +106,7 @@ UPower::UPower(const std::string& id, const Json::Value& config) } UPower::~UPower() { + if (displayDevice != NULL) g_object_unref(displayDevice); if (client != NULL) g_object_unref(client); if (login1_id > 0) { g_dbus_connection_signal_unsubscribe(login1_connection, login1_id); @@ -289,7 +297,12 @@ auto UPower::update() -> void { std::lock_guard guard(m_Mutex); // Don't update widget if the UPower service isn't running - if (!upowerRunning) return; + if (!upowerRunning) { + if (hideIfEmpty) { + event_box_.set_visible(false); + } + return; + } UpDeviceKind kind; UpDeviceState state; diff --git a/src/modules/upower/upower_tooltip.cpp b/src/modules/upower/upower_tooltip.cpp index 45544bbc..1a653f85 100644 --- a/src/modules/upower/upower_tooltip.cpp +++ b/src/modules/upower/upower_tooltip.cpp @@ -9,11 +9,10 @@ namespace waybar::modules::upower { UPowerTooltip::UPowerTooltip(uint iconSize_, uint tooltipSpacing_, uint tooltipPadding_) : Gtk::Window(), + contentBox(std::make_unique(Gtk::ORIENTATION_VERTICAL)), iconSize(iconSize_), tooltipSpacing(tooltipSpacing_), tooltipPadding(tooltipPadding_) { - contentBox = new Gtk::Box(Gtk::ORIENTATION_VERTICAL); - // Sets the Tooltip Padding contentBox->set_margin_top(tooltipPadding); contentBox->set_margin_bottom(tooltipPadding); diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index b2d9b39d..51bb708d 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -46,6 +46,7 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val } waybar::modules::Wireplumber::~Wireplumber() { + wp_core_disconnect(wp_core_); g_clear_pointer(&apis_, g_ptr_array_unref); g_clear_object(&om_); g_clear_object(&wp_core_); @@ -81,7 +82,7 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* auto nick = wp_properties_get(properties, "node.nick"); auto description = wp_properties_get(properties, "node.description"); - self->node_name_ = nick ? nick : description; + self->node_name_ = nick ? nick : description ? description : "Unknown node name"; spdlog::debug("[{}]: Updating node name to: {}", self->name_, self->node_name_); } @@ -316,13 +317,6 @@ bool waybar::modules::Wireplumber::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 max_volume = 1; double step = 1.0 / 100.0; if (config_["scroll-step"].isDouble()) { diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 9e09d7a9..2709584b 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/gtk_icon.hpp" #include "util/rewrite_string.hpp" #include "util/string.hpp" @@ -182,11 +183,21 @@ bool Task::image_load_icon(Gtk::Image &image, const Glib::RefPtr try { pixbuf = icon_theme->load_icon(ret_icon_name, scaled_icon_size, Gtk::ICON_LOOKUP_FORCE_SIZE); + spdlog::debug("{} Loaded icon '{}'", repr(), ret_icon_name); } catch (...) { - if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS)) + if (Glib::file_test(ret_icon_name, Glib::FILE_TEST_EXISTS)) { pixbuf = load_icon_from_file(ret_icon_name, scaled_icon_size); - else - pixbuf = {}; + spdlog::debug("{} Loaded icon from file '{}'", repr(), ret_icon_name); + } else { + try { + pixbuf = DefaultGtkIconThemeWrapper::load_icon( + "image-missing", scaled_icon_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + spdlog::debug("{} Loaded icon from resource", repr()); + } catch (...) { + pixbuf = {}; + spdlog::debug("{} Unable to load icon.", repr()); + } + } } if (pixbuf) { @@ -266,7 +277,7 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, handle_{tl_handle}, seat_{seat}, id_{global_id++}, - content_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0} { + content_{bar.orientation, 0} { zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); button.set_relief(Gtk::RELIEF_NONE); @@ -304,6 +315,10 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, with_icon_ = true; } + if (app_id_.empty()) { + handle_app_id("unknown"); + } + /* Strip spaces at the beginning and end of the format strings */ format_tooltip_.clear(); if (!config_["tooltip"].isBool() || config_["tooltip"].asBool()) { @@ -392,6 +407,11 @@ void Task::hide_if_ignored() { } void Task::handle_app_id(const char *app_id) { + if (app_id_.empty()) { + spdlog::debug(fmt::format("Task ({}) setting app_id to {}", id_, app_id)); + } else { + spdlog::debug(fmt::format("Task ({}) overwriting app_id '{}' with '{}'", id_, app_id_, app_id)); + } app_id_ = app_id; hide_if_ignored(); @@ -507,17 +527,17 @@ 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); button_visible_ = false; } + tbar_->remove_task(id_); } bool Task::handle_clicked(GdkEventButton *bt) { /* filter out additional events for double/triple clicks */ if (bt->type == GDK_BUTTON_PRESS) { - /* save where the button press ocurred in case it becomes a drag */ + /* save where the button press occurred in case it becomes a drag */ drag_start_button = bt->button; drag_start_x = bt->x; drag_start_y = bt->y; @@ -710,13 +730,14 @@ static const wl_registry_listener registry_listener_impl = {.global = handle_glo Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Value &config) : waybar::AModule(config, "taskbar", id, false, false), bar_(bar), - box_{bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, + box_{bar.orientation, 0}, manager_{nullptr}, seat_{nullptr} { box_.set_name("taskbar"); if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); box_.get_style_context()->add_class("empty"); event_box_.add(box_); diff --git a/src/modules/wlr/workspace_manager.cpp b/src/modules/wlr/workspace_manager.cpp index 8933d691..f556a161 100644 --- a/src/modules/wlr/workspace_manager.cpp +++ b/src/modules/wlr/workspace_manager.cpp @@ -21,9 +21,7 @@ std::map Workspace::icons_map_; WorkspaceManager::WorkspaceManager(const std::string &id, const waybar::Bar &bar, const Json::Value &config) - : waybar::AModule(config, "workspaces", id, false, false), - bar_(bar), - box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) { + : waybar::AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) { auto config_sort_by_name = config_["sort-by-name"]; if (config_sort_by_name.isBool()) { sort_by_name_ = config_sort_by_name.asBool(); @@ -54,6 +52,7 @@ WorkspaceManager::WorkspaceManager(const std::string &id, const waybar::Bar &bar if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); add_registry_listener(this); @@ -209,8 +208,17 @@ WorkspaceGroup::WorkspaceGroup(const Bar &bar, Gtk::Box &box, const Json::Value } auto WorkspaceGroup::fill_persistent_workspaces() -> void { - if (config_["persistent_workspaces"].isObject() && !workspace_manager_.all_outputs()) { - const Json::Value &p_workspaces = config_["persistent_workspaces"]; + if (config_["persistent_workspaces"].isObject()) { + spdlog::warn( + "persistent_workspaces is deprecated. Please change config to use persistent-workspaces."); + } + + if ((config_["persistent-workspaces"].isObject() || + config_["persistent_workspaces"].isObject()) && + !workspace_manager_.all_outputs()) { + const Json::Value &p_workspaces = config_["persistent-workspaces"].isObject() + ? config_["persistent-workspaces"] + : config_["persistent_workspaces"]; const std::vector p_workspaces_names = p_workspaces.getMemberNames(); for (const std::string &p_w_name : p_workspaces_names) { diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp new file mode 100644 index 00000000..f4dd72c4 --- /dev/null +++ b/src/util/audio_backend.cpp @@ -0,0 +1,290 @@ +#include "util/audio_backend.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +namespace waybar::util { + +AudioBackend::AudioBackend(std::function on_updated_cb, private_constructor_tag tag) + : mainloop_(nullptr), + mainloop_api_(nullptr), + context_(nullptr), + sink_idx_(0), + volume_(0), + muted_(false), + source_idx_(0), + source_volume_(0), + source_muted_(false), + on_updated_cb_(on_updated_cb) { + mainloop_ = pa_threaded_mainloop_new(); + if (mainloop_ == nullptr) { + throw std::runtime_error("pa_mainloop_new() failed."); + } + pa_threaded_mainloop_lock(mainloop_); + mainloop_api_ = pa_threaded_mainloop_get_api(mainloop_); + connectContext(); + if (pa_threaded_mainloop_start(mainloop_) < 0) { + throw std::runtime_error("pa_mainloop_run() failed."); + } + pa_threaded_mainloop_unlock(mainloop_); +} + +AudioBackend::~AudioBackend() { + if (context_ != nullptr) { + pa_context_disconnect(context_); + } + + if (mainloop_ != nullptr) { + mainloop_api_->quit(mainloop_api_, 0); + pa_threaded_mainloop_stop(mainloop_); + pa_threaded_mainloop_free(mainloop_); + } +} + +std::shared_ptr AudioBackend::getInstance(std::function on_updated_cb) { + private_constructor_tag tag; + return std::make_shared(on_updated_cb, tag); +} + +void AudioBackend::connectContext() { + context_ = pa_context_new(mainloop_api_, "waybar"); + if (context_ == nullptr) { + throw std::runtime_error("pa_context_new() failed."); + } + pa_context_set_state_callback(context_, contextStateCb, this); + if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFAIL, nullptr) < 0) { + auto err = + fmt::format("pa_context_connect() failed: {}", pa_strerror(pa_context_errno(context_))); + throw std::runtime_error(err); + } +} + +void AudioBackend::contextStateCb(pa_context *c, void *data) { + auto backend = static_cast(data); + switch (pa_context_get_state(c)) { + case PA_CONTEXT_TERMINATED: + backend->mainloop_api_->quit(backend->mainloop_api_, 0); + break; + case PA_CONTEXT_READY: + pa_context_get_server_info(c, serverInfoCb, data); + pa_context_set_subscribe_callback(c, subscribeCb, data); + pa_context_subscribe(c, + static_cast( + static_cast(PA_SUBSCRIPTION_MASK_SERVER) | + static_cast(PA_SUBSCRIPTION_MASK_SINK) | + static_cast(PA_SUBSCRIPTION_MASK_SINK_INPUT) | + static_cast(PA_SUBSCRIPTION_MASK_SOURCE) | + static_cast(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)), + nullptr, nullptr); + break; + case PA_CONTEXT_FAILED: + // When pulseaudio server restarts, the connection is "failed". Try to reconnect. + // pa_threaded_mainloop_lock is already acquired in callback threads. + // So there is no need to lock it again. + if (backend->context_ != nullptr) { + pa_context_disconnect(backend->context_); + } + backend->connectContext(); + break; + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + default: + break; + } +} + +/* + * Called when an event we subscribed to occurs. + */ +void AudioBackend::subscribeCb(pa_context *context, pa_subscription_event_type_t type, uint32_t idx, + void *data) { + unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + unsigned operation = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + if (operation != PA_SUBSCRIPTION_EVENT_CHANGE) { + return; + } + if (facility == PA_SUBSCRIPTION_EVENT_SERVER) { + pa_context_get_server_info(context, serverInfoCb, data); + } else if (facility == PA_SUBSCRIPTION_EVENT_SINK) { + pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data); + } else if (facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT) { + pa_context_get_sink_info_list(context, sinkInfoCb, data); + } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE) { + pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data); + } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT) { + pa_context_get_source_info_list(context, sourceInfoCb, data); + } +} + +/* + * Called in response to a volume change request + */ +void AudioBackend::volumeModifyCb(pa_context *c, int success, void *data) { + auto backend = static_cast(data); + if (success != 0) { + pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data); + } +} + +/* + * Called when the requested sink information is ready. + */ +void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, int /*eol*/, + void *data) { + if (i == nullptr) return; + + auto backend = static_cast(data); + + if (!backend->ignored_sinks_.empty()) { + for (const auto &ignored_sink : backend->ignored_sinks_) { + if (ignored_sink == i->description) { + return; + } + } + } + + if (backend->current_sink_name_ == i->name) { + if (i->state != PA_SINK_RUNNING) { + backend->current_sink_running_ = false; + } else { + backend->current_sink_running_ = true; + } + } + + if (!backend->current_sink_running_ && i->state == PA_SINK_RUNNING) { + backend->current_sink_name_ = i->name; + backend->current_sink_running_ = true; + } + + if (backend->current_sink_name_ == i->name) { + backend->pa_volume_ = i->volume; + float volume = + static_cast(pa_cvolume_avg(&(backend->pa_volume_))) / float{PA_VOLUME_NORM}; + backend->sink_idx_ = i->index; + backend->volume_ = std::round(volume * 100.0F); + backend->muted_ = i->mute != 0; + backend->desc_ = i->description; + backend->monitor_ = i->monitor_source_name; + backend->port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; + if (auto ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) { + backend->form_factor_ = ff; + } else { + backend->form_factor_ = ""; + } + backend->on_updated_cb_(); + } +} + +/* + * Called when the requested source information is ready. + */ +void AudioBackend::sourceInfoCb(pa_context * /*context*/, const pa_source_info *i, int /*eol*/, + void *data) { + auto backend = static_cast(data); + if (i != nullptr && backend->default_source_name_ == i->name) { + auto source_volume = static_cast(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM}; + backend->source_volume_ = std::round(source_volume * 100.0F); + backend->source_idx_ = i->index; + backend->source_muted_ = i->mute != 0; + backend->source_desc_ = i->description; + backend->source_port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; + backend->on_updated_cb_(); + } +} + +/* + * Called when the requested information on the server is ready. This is + * used to find the default PulseAudio sink. + */ +void AudioBackend::serverInfoCb(pa_context *context, const pa_server_info *i, void *data) { + auto backend = static_cast(data); + backend->current_sink_name_ = i->default_sink_name; + backend->default_source_name_ = i->default_source_name; + + pa_context_get_sink_info_list(context, sinkInfoCb, data); + pa_context_get_source_info_list(context, sourceInfoCb, data); +} + +void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) { + double volume_tick = static_cast(PA_VOLUME_NORM) / 100; + pa_cvolume pa_volume = pa_volume_; + + volume = std::clamp(volume, min_volume, max_volume); + pa_cvolume_set(&pa_volume, pa_volume_.channels, volume * volume_tick); + + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); +} + +void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume) { + double volume_tick = static_cast(PA_VOLUME_NORM) / 100; + pa_volume_t change = volume_tick; + pa_cvolume pa_volume = pa_volume_; + + max_volume = std::min(max_volume, static_cast(PA_VOLUME_UI_MAX)); + + if (change_type == ChangeType::Increase) { + if (volume_ < max_volume) { + if (volume_ + step > max_volume) { + change = round((max_volume - volume_) * volume_tick); + } else { + change = round(step * volume_tick); + } + pa_cvolume_inc(&pa_volume, change); + } + } else if (change_type == ChangeType::Decrease) { + if (volume_ > 0) { + if (volume_ - step < 0) { + change = round(volume_ * volume_tick); + } else { + change = round(step * volume_tick); + } + pa_cvolume_dec(&pa_volume, change); + } + } + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); +} + +void AudioBackend::toggleSinkMute() { + muted_ = !muted_; + pa_context_set_sink_mute_by_index(context_, sink_idx_, muted_, nullptr, nullptr); +} + +void AudioBackend::toggleSinkMute(bool mute) { + muted_ = mute; + pa_context_set_sink_mute_by_index(context_, sink_idx_, muted_, nullptr, nullptr); +} + +void AudioBackend::toggleSourceMute() { + source_muted_ = !muted_; + pa_context_set_source_mute_by_index(context_, source_idx_, source_muted_, nullptr, nullptr); +} + +void AudioBackend::toggleSourceMute(bool mute) { + source_muted_ = mute; + pa_context_set_source_mute_by_index(context_, source_idx_, source_muted_, nullptr, nullptr); +} + +bool AudioBackend::isBluetooth() { + return monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio + monitor_.find("a2dp-sink") != std::string::npos || // PipeWire + monitor_.find("bluez") != std::string::npos; +} + +void AudioBackend::setIgnoredSinks(const Json::Value &config) { + if (config.isArray()) { + for (const auto &ignored_sink : config) { + if (ignored_sink.isString()) { + ignored_sinks_.push_back(ignored_sink.asString()); + } + } + } +} + +} // namespace waybar::util \ No newline at end of file diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp new file mode 100644 index 00000000..1512103c --- /dev/null +++ b/src/util/backlight_backend.cpp @@ -0,0 +1,288 @@ +#include "util/backlight_backend.hpp" + +#include +#include +#include + +#include + +namespace { +class FileDescriptor { + public: + explicit FileDescriptor(int fd) : fd_(fd) {} + FileDescriptor(const FileDescriptor &other) = delete; + FileDescriptor(FileDescriptor &&other) noexcept = delete; + FileDescriptor &operator=(const FileDescriptor &other) = delete; + FileDescriptor &operator=(FileDescriptor &&other) noexcept = delete; + ~FileDescriptor() { + if (fd_ != -1) { + if (close(fd_) != 0) { + fmt::print(stderr, "Failed to close fd: {}\n", errno); + } + } + } + int get() const { return fd_; } + + private: + int fd_; +}; + +struct UdevDeleter { + void operator()(udev *ptr) { udev_unref(ptr); } +}; + +struct UdevDeviceDeleter { + void operator()(udev_device *ptr) { udev_device_unref(ptr); } +}; + +struct UdevEnumerateDeleter { + void operator()(udev_enumerate *ptr) { udev_enumerate_unref(ptr); } +}; + +struct UdevMonitorDeleter { + void operator()(udev_monitor *ptr) { udev_monitor_unref(ptr); } +}; + +void check_eq(int rc, int expected, const char *message = "eq, rc was: ") { + if (rc != expected) { + 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(fmt::runtime(message), rc)); + } +} + +void check0(int rc, const char *message = "rc wasn't 0") { check_eq(rc, 0, message); } + +void check_gte(int rc, int gte, const char *message = "rc was: ") { + if (rc < gte) { + throw std::runtime_error(fmt::format(fmt::runtime(message), rc)); + } +} + +void check_nn(const void *ptr, const char *message = "ptr was null") { + if (ptr == nullptr) { + throw std::runtime_error(message); + } +} + +} // namespace + +namespace waybar::util { + +BacklightDevice::BacklightDevice(std::string name, int actual, int max, bool powered) + : name_(name), actual_(actual), max_(max), powered_(powered) {} + +std::string BacklightDevice::name() const { return name_; } + +int BacklightDevice::get_actual() const { return actual_; } + +void BacklightDevice::set_actual(int actual) { actual_ = actual; } + +int BacklightDevice::get_max() const { return max_; } + +void BacklightDevice::set_max(int max) { max_ = max; } + +bool BacklightDevice::get_powered() const { return powered_; } + +void BacklightDevice::set_powered(bool powered) { powered_ = powered; } + +BacklightBackend::BacklightBackend(std::chrono::milliseconds interval, + std::function on_updated_cb) + : on_updated_cb_(on_updated_cb), polling_interval_(interval), previous_best_({}) { + std::unique_ptr udev_check{udev_new()}; + check_nn(udev_check.get(), "Udev check new failed"); + enumerate_devices(devices_.begin(), devices_.end(), std::back_inserter(devices_), + udev_check.get()); + if (devices_.empty()) { + throw std::runtime_error("No backlight found"); + } + + // 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"); + + std::unique_ptr mon{ + udev_monitor_new_from_netlink(udev.get(), "udev")}; + check_nn(mon.get(), "udev monitor new failed"); + check_gte(udev_monitor_filter_add_match_subsystem_devtype(mon.get(), "backlight", nullptr), 0, + "udev failed to add monitor filter: "); + udev_monitor_enable_receiving(mon.get()); + + auto udev_fd = udev_monitor_get_fd(mon.get()); + + auto epoll_fd = FileDescriptor{epoll_create1(EPOLL_CLOEXEC)}; + check_neq(epoll_fd.get(), -1, "epoll init failed: "); + epoll_event ctl_event{}; + ctl_event.events = EPOLLIN; + ctl_event.data.fd = udev_fd; + + check0(epoll_ctl(epoll_fd.get(), EPOLL_CTL_ADD, ctl_event.data.fd, &ctl_event), + "epoll_ctl failed: {}"); + epoll_event events[EPOLL_MAX_EVENTS]; + + while (udev_thread_.isRunning()) { + const int event_count = + epoll_wait(epoll_fd.get(), events, EPOLL_MAX_EVENTS, this->polling_interval_.count()); + if (!udev_thread_.isRunning()) { + break; + } + decltype(devices_) devices; + { + std::scoped_lock lock(udev_thread_mutex_); + devices = devices_; + } + for (int i = 0; i < event_count; ++i) { + const auto &event = events[i]; + check_eq(event.data.fd, udev_fd, "unexpected udev fd"); + std::unique_ptr dev{udev_monitor_receive_device(mon.get())}; + check_nn(dev.get(), "epoll dev was null"); + upsert_device(devices.begin(), devices.end(), std::back_inserter(devices), dev.get()); + } + + // Refresh state if timed out + if (event_count == 0) { + enumerate_devices(devices.begin(), devices.end(), std::back_inserter(devices), udev.get()); + } + { + std::scoped_lock lock(udev_thread_mutex_); + devices_ = devices; + } + this->on_updated_cb_(); + } + }; +} + +template +const BacklightDevice *BacklightBackend::best_device(ForwardIt first, ForwardIt last, + std::string_view preferred_device) { + const auto found = std::find_if( + first, last, [preferred_device](const auto &dev) { return dev.name() == preferred_device; }); + if (found != last) { + return &(*found); + } + + const auto max = std::max_element( + first, last, [](const auto &l, const auto &r) { return l.get_max() < r.get_max(); }); + + return max == last ? nullptr : &(*max); +} + +const BacklightDevice *BacklightBackend::get_previous_best_device() { + return previous_best_.has_value() ? &(*previous_best_) : nullptr; +} + +void BacklightBackend::set_previous_best_device(const BacklightDevice *device) { + if (device == nullptr) { + previous_best_ = std::nullopt; + } else { + previous_best_ = std::optional{*device}; + } +} + +void BacklightBackend::set_scaled_brightness(std::string preferred_device, int brightness) { + GET_BEST_DEVICE(best, (*this), preferred_device); + + if (best != nullptr) { + const auto max = best->get_max(); + const auto abs_val = static_cast(round(brightness * max / 100.0f)); + set_brightness_internal(best->name(), abs_val, best->get_max()); + } +} + +void BacklightBackend::set_brightness(std::string preferred_device, ChangeType change_type, + double step) { + GET_BEST_DEVICE(best, (*this), preferred_device); + + if (best != nullptr) { + const auto max = best->get_max(); + + const auto abs_step = static_cast(round(step * max / 100.0f)); + + const int new_brightness = change_type == ChangeType::Increase ? best->get_actual() + abs_step + : best->get_actual() - abs_step; + set_brightness_internal(best->name(), new_brightness, max); + } +} + +void BacklightBackend::set_brightness_internal(std::string device_name, int brightness, + int max_brightness) { + brightness = std::clamp(brightness, 0, max_brightness); + + auto call_args = Glib::VariantContainerBase( + g_variant_new("(ssu)", "backlight", device_name.c_str(), brightness)); + + login_proxy_->call_sync("SetBrightness", call_args); +} + +int BacklightBackend::get_scaled_brightness(std::string preferred_device) { + GET_BEST_DEVICE(best, (*this), preferred_device); + + if (best != nullptr) { + return best->get_actual() * 100 / best->get_max(); + } + + return 0; +} + +template +void BacklightBackend::upsert_device(ForwardIt first, ForwardIt last, Inserter inserter, + udev_device *dev) { + const char *name = udev_device_get_sysname(dev); + check_nn(name); + + const char *actual_brightness_attr = + strncmp(name, "amdgpu_bl", 9) == 0 || strcmp(name, "apple-panel-bl") == 0 + ? "brightness" + : "actual_brightness"; + + const char *actual = udev_device_get_sysattr_value(dev, actual_brightness_attr); + const char *max = udev_device_get_sysattr_value(dev, "max_brightness"); + const char *power = udev_device_get_sysattr_value(dev, "bl_power"); + + auto found = + std::find_if(first, last, [name](const auto &device) { return device.name() == name; }); + if (found != last) { + if (actual != nullptr) { + found->set_actual(std::stoi(actual)); + } + if (max != nullptr) { + found->set_max(std::stoi(max)); + } + if (power != nullptr) { + found->set_powered(std::stoi(power) == 0); + } + } else { + const int actual_int = actual == nullptr ? 0 : std::stoi(actual); + const int max_int = max == nullptr ? 0 : std::stoi(max); + const bool power_bool = power == nullptr ? true : std::stoi(power) == 0; + *inserter = BacklightDevice{name, actual_int, max_int, power_bool}; + ++inserter; + } +} + +template +void BacklightBackend::enumerate_devices(ForwardIt first, ForwardIt last, Inserter inserter, + udev *udev) { + std::unique_ptr enumerate{udev_enumerate_new(udev)}; + udev_enumerate_add_match_subsystem(enumerate.get(), "backlight"); + udev_enumerate_scan_devices(enumerate.get()); + udev_list_entry *enum_devices = udev_enumerate_get_list_entry(enumerate.get()); + udev_list_entry *dev_list_entry; + udev_list_entry_foreach(dev_list_entry, enum_devices) { + const char *path = udev_list_entry_get_name(dev_list_entry); + std::unique_ptr dev{udev_device_new_from_syspath(udev, path)}; + check_nn(dev.get(), "dev new failed"); + upsert_device(first, last, inserter, dev.get()); + } +} + +} // namespace waybar::util \ No newline at end of file diff --git a/src/util/css_reload_helper.cpp b/src/util/css_reload_helper.cpp new file mode 100644 index 00000000..45fd801a --- /dev/null +++ b/src/util/css_reload_helper.cpp @@ -0,0 +1,144 @@ +#include "util/css_reload_helper.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +#include "config.hpp" +#include "giomm/file.h" +#include "glibmm/refptr.h" + +namespace { +const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)"); +} + +waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, std::function callback) + : m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {} + +std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) { + if (filename.empty()) { + return {}; + } + + std::ifstream file(filename); + if (!file.is_open()) { + return {}; + } + + return {(std::istreambuf_iterator(file)), std::istreambuf_iterator()}; +} + +std::string waybar::CssReloadHelper::findPath(const std::string& filename) { + // try path and fallback to looking relative to the config + std::string result; + if (std::filesystem::exists(filename)) { + result = filename; + } else { + result = Config::findConfigPath({filename}).value_or(""); + } + + // File monitor does not work with symlinks, so resolve them + if (std::filesystem::is_symlink(result)) { + result = std::filesystem::read_symlink(result); + } + + return result; +} + +void waybar::CssReloadHelper::monitorChanges() { + auto files = parseImports(m_cssFile); + for (const auto& file : files) { + auto gioFile = Gio::File::create_for_path(file); + if (!gioFile) { + spdlog::error("Failed to create file for path: {}", file); + continue; + } + + auto fileMonitor = gioFile->monitor_file(); + if (!fileMonitor) { + spdlog::error("Failed to create file monitor for path: {}", file); + continue; + } + + auto connection = fileMonitor->signal_changed().connect( + sigc::mem_fun(*this, &CssReloadHelper::handleFileChange)); + + if (!connection.connected()) { + spdlog::error("Failed to connect to file monitor for path: {}", file); + continue; + } + m_fileMonitors.emplace_back(std::move(fileMonitor)); + } +} + +void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr const& file, + Glib::RefPtr const& other_type, + Gio::FileMonitorEvent event_type) { + // Multiple events are fired on file changed (attributes, write, changes done hint, etc.), only + // fire for one + if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { + spdlog::debug("Reloading style, file changed: {}", file->get_path()); + m_callback(); + } +} + +std::vector waybar::CssReloadHelper::parseImports(const std::string& cssFile) { + std::unordered_map imports; + + auto cssFullPath = findPath(cssFile); + if (cssFullPath.empty()) { + spdlog::error("Failed to find css file: {}", cssFile); + return {}; + } + + spdlog::debug("Parsing imports for file: {}", cssFullPath); + imports[cssFullPath] = false; + + auto previousSize = 0UL; + auto maxIterations = 100U; + do { + previousSize = imports.size(); + for (const auto& [file, parsed] : imports) { + if (!parsed) { + parseImports(file, imports); + } + } + + } while (imports.size() > previousSize && maxIterations-- > 0); + + std::vector result; + for (const auto& [file, parsed] : imports) { + if (parsed) { + spdlog::debug("Adding file to watch list: {}", file); + result.push_back(file); + } + } + + return result; +} + +void waybar::CssReloadHelper::parseImports(const std::string& cssFile, + std::unordered_map& imports) { + // if the file has already been parsed, skip + if (imports.find(cssFile) != imports.end() && imports[cssFile]) { + return; + } + + auto contents = getFileContents(cssFile); + std::smatch matches; + while (std::regex_search(contents, matches, IMPORT_REGEX)) { + auto importFile = findPath({matches[1].str()}); + if (!importFile.empty() && imports.find(importFile) == imports.end()) { + imports[importFile] = false; + } + + contents = matches.suffix().str(); + } + + imports[cssFile] = true; +} diff --git a/src/util/enum.cpp b/src/util/enum.cpp new file mode 100644 index 00000000..dc3eae0c --- /dev/null +++ b/src/util/enum.cpp @@ -0,0 +1,45 @@ +#include "util/enum.hpp" + +#include // for std::transform +#include // for std::toupper +#include +#include +#include +#include + +#include "modules/hyprland/workspaces.hpp" +#include "util/string.hpp" + +namespace waybar::util { + +template +EnumParser::EnumParser() = default; + +template +EnumParser::~EnumParser() = default; + +template +EnumType EnumParser::parseStringToEnum(const std::string& str, + const std::map& enumMap) { + // Convert the input string to uppercase + std::string uppercaseStr = capitalize(str); + + // Capitalize the map keys before searching + std::map capitalizedEnumMap; + std::transform( + enumMap.begin(), enumMap.end(), std::inserter(capitalizedEnumMap, capitalizedEnumMap.end()), + [](const auto& pair) { return std::make_pair(capitalize(pair.first), pair.second); }); + + // Return enum match of string + auto it = capitalizedEnumMap.find(uppercaseStr); + if (it != capitalizedEnumMap.end()) return it->second; + + // Throw error if it doesn't return + throw std::invalid_argument("Invalid string representation for enum"); +} + +// Explicit instantiations for specific EnumType types you intend to use +// Add explicit instantiations for all relevant EnumType types +template struct EnumParser; + +} // namespace waybar::util diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp new file mode 100644 index 00000000..5fe3ba62 --- /dev/null +++ b/src/util/pipewire_backend.cpp @@ -0,0 +1,155 @@ +#include "util/pipewire/pipewire_backend.hpp" + +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +static void get_node_info(void *data_, const struct pw_node_info *info) { + PrivacyNodeInfo *p_node_info = static_cast(data_); + PipewireBackend *backend = (PipewireBackend *)p_node_info->data; + + p_node_info->state = info->state; + + const struct spa_dict_item *item; + spa_dict_for_each(item, info->props) { + if (strcmp(item->key, PW_KEY_CLIENT_ID) == 0) { + p_node_info->client_id = strtoul(item->value, NULL, 10); + } else if (strcmp(item->key, PW_KEY_MEDIA_NAME) == 0) { + p_node_info->media_name = item->value; + } else if (strcmp(item->key, PW_KEY_NODE_NAME) == 0) { + p_node_info->node_name = item->value; + } else if (strcmp(item->key, PW_KEY_APP_NAME) == 0) { + p_node_info->application_name = item->value; + } else if (strcmp(item->key, "pipewire.access.portal.app_id") == 0) { + p_node_info->pipewire_access_portal_app_id = item->value; + } else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) { + p_node_info->application_icon_name = item->value; + } + } + + backend->privacy_nodes_changed_signal_event.emit(); +} + +static const struct pw_node_events node_events = { + .version = PW_VERSION_NODE_EVENTS, + .info = get_node_info, +}; + +static void proxy_destroy(void *data) { + PrivacyNodeInfo *node = (PrivacyNodeInfo *)data; + + spa_hook_remove(&node->proxy_listener); + spa_hook_remove(&node->object_listener); +} + +static const struct pw_proxy_events proxy_events = { + .version = PW_VERSION_PROXY_EVENTS, + .destroy = proxy_destroy, +}; + +static void registry_event_global(void *_data, uint32_t id, uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) { + if (!props || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; + + const char *lookup_str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (!lookup_str) return; + std::string media_class = lookup_str; + enum PrivacyNodeType media_type = PRIVACY_NODE_TYPE_NONE; + if (media_class == "Stream/Input/Video") { + media_type = PRIVACY_NODE_TYPE_VIDEO_INPUT; + } else if (media_class == "Stream/Input/Audio") { + media_type = PRIVACY_NODE_TYPE_AUDIO_INPUT; + } else if (media_class == "Stream/Output/Audio") { + media_type = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; + } else { + return; + } + + PipewireBackend *backend = static_cast(_data); + struct pw_proxy *proxy = + (pw_proxy *)pw_registry_bind(backend->registry, id, type, version, sizeof(PrivacyNodeInfo)); + + if (!proxy) return; + + PrivacyNodeInfo *p_node_info = (PrivacyNodeInfo *)pw_proxy_get_user_data(proxy); + p_node_info->id = id; + p_node_info->data = backend; + p_node_info->type = media_type; + p_node_info->media_class = media_class; + + pw_proxy_add_listener(proxy, &p_node_info->proxy_listener, &proxy_events, p_node_info); + + pw_proxy_add_object_listener(proxy, &p_node_info->object_listener, &node_events, p_node_info); + + backend->privacy_nodes.insert_or_assign(id, p_node_info); +} + +static void registry_event_global_remove(void *_data, uint32_t id) { + auto backend = static_cast(_data); + + backend->mutex_.lock(); + auto iter = backend->privacy_nodes.find(id); + if (iter != backend->privacy_nodes.end()) { + backend->privacy_nodes.erase(id); + } + backend->mutex_.unlock(); + + backend->privacy_nodes_changed_signal_event.emit(); +} + +static const struct pw_registry_events registry_events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +PipewireBackend::PipewireBackend(private_constructor_tag tag) + : mainloop_(nullptr), context_(nullptr), core_(nullptr) { + pw_init(nullptr, nullptr); + mainloop_ = pw_thread_loop_new("waybar", nullptr); + if (mainloop_ == nullptr) { + throw std::runtime_error("pw_thread_loop_new() failed."); + } + context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0); + if (context_ == nullptr) { + throw std::runtime_error("pa_context_new() failed."); + } + core_ = pw_context_connect(context_, nullptr, 0); + if (core_ == nullptr) { + throw std::runtime_error("pw_context_connect() failed"); + } + registry = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0); + + spa_zero(registry_listener); + pw_registry_add_listener(registry, ®istry_listener, ®istry_events, this); + if (pw_thread_loop_start(mainloop_) < 0) { + throw std::runtime_error("pw_thread_loop_start() failed."); + } +} + +PipewireBackend::~PipewireBackend() { + if (registry != nullptr) { + pw_proxy_destroy((struct pw_proxy *)registry); + } + + spa_zero(registry_listener); + + if (core_ != nullptr) { + pw_core_disconnect(core_); + } + + if (context_ != nullptr) { + pw_context_destroy(context_); + } + + if (mainloop_ != nullptr) { + pw_thread_loop_stop(mainloop_); + pw_thread_loop_destroy(mainloop_); + } +} + +std::shared_ptr PipewireBackend::getInstance() { + private_constructor_tag tag; + return std::make_shared(tag); +} +} // namespace waybar::util::PipewireBackend diff --git a/src/util/portal.cpp b/src/util/portal.cpp new file mode 100644 index 00000000..50c646c5 --- /dev/null +++ b/src/util/portal.cpp @@ -0,0 +1,106 @@ +#include "util/portal.hpp" + +#include +#include +#include + +#include +#include + +#include "fmt/format.h" + +namespace waybar { +static constexpr const char* PORTAL_BUS_NAME = "org.freedesktop.portal.Desktop"; +static constexpr const char* PORTAL_OBJ_PATH = "/org/freedesktop/portal/desktop"; +static constexpr const char* PORTAL_INTERFACE = "org.freedesktop.portal.Settings"; +static constexpr const char* PORTAL_NAMESPACE = "org.freedesktop.appearance"; +static constexpr const char* PORTAL_KEY = "color-scheme"; +} // namespace waybar + +using namespace Gio; + +auto fmt::formatter::format(waybar::Appearance c, format_context& ctx) const { + string_view name; + switch (c) { + case waybar::Appearance::LIGHT: + name = "light"; + break; + case waybar::Appearance::DARK: + name = "dark"; + break; + default: + name = "unknown"; + break; + } + return formatter::format(name, ctx); +} + +waybar::Portal::Portal() + : DBus::Proxy(DBus::Connection::get_sync(DBus::BusType::BUS_TYPE_SESSION), PORTAL_BUS_NAME, + PORTAL_OBJ_PATH, PORTAL_INTERFACE), + currentMode(Appearance::UNKNOWN) { + refreshAppearance(); +}; + +void waybar::Portal::refreshAppearance() { + auto params = Glib::Variant>::create( + {PORTAL_NAMESPACE, PORTAL_KEY}); + Glib::VariantBase response; + try { + response = call_sync(std::string(PORTAL_INTERFACE) + ".Read", params); + } catch (const Glib::Error& e) { + spdlog::info("Unable to receive desktop appearance: {}", std::string(e.what())); + return; + } + + // unfortunately, the response is triple-nested, with type (v>), + // so we have cast thrice. This is a variation from the freedesktop standard + // (it should only be doubly nested) but all implementations appear to do so. + // + // xdg-desktop-portal 1.17 will fix this issue with a new `ReadOne` method, + // but this version is not yet released. + // TODO(xdg-desktop-portal v1.17): switch to ReadOne + auto container = Glib::VariantBase::cast_dynamic(response); + Glib::VariantBase modev; + container.get_child(modev, 0); + auto mode = + Glib::VariantBase::cast_dynamic>>>(modev) + .get() + .get() + .get(); + auto newMode = Appearance(mode); + if (newMode == currentMode) { + return; + } + spdlog::info("Discovered appearance '{}'", newMode); + currentMode = newMode; + m_signal_appearance_changed.emit(currentMode); +} + +waybar::Appearance waybar::Portal::getAppearance() { return currentMode; }; + +void waybar::Portal::on_signal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, + const Glib::VariantContainerBase& parameters) { + spdlog::debug("Received signal {}", (std::string)signal_name); + if (signal_name != "SettingChanged" || parameters.get_n_children() != 3) { + return; + } + Glib::VariantBase nspcv, keyv, valuev; + parameters.get_child(nspcv, 0); + parameters.get_child(keyv, 1); + parameters.get_child(valuev, 2); + auto nspc = Glib::VariantBase::cast_dynamic>(nspcv).get(); + auto key = Glib::VariantBase::cast_dynamic>(keyv).get(); + if (nspc != PORTAL_NAMESPACE || key != PORTAL_KEY) { + return; + } + auto value = + Glib::VariantBase::cast_dynamic>>(valuev).get().get(); + auto newMode = Appearance(value); + if (newMode == currentMode) { + return; + } + spdlog::info("Received new appearance '{}'", newMode); + currentMode = newMode; + m_signal_appearance_changed.emit(currentMode); +} diff --git a/src/util/prepare_for_sleep.cpp b/src/util/prepare_for_sleep.cpp index 218c1e29..661285a2 100644 --- a/src/util/prepare_for_sleep.cpp +++ b/src/util/prepare_for_sleep.cpp @@ -7,8 +7,7 @@ namespace { class PrepareForSleep { private: PrepareForSleep() { - GError *error = NULL; - login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL); if (!login1_connection) { spdlog::warn("Unable to connect to the SYSTEM Bus!..."); } else { diff --git a/src/util/regex_collection.cpp b/src/util/regex_collection.cpp new file mode 100644 index 00000000..704d6545 --- /dev/null +++ b/src/util/regex_collection.cpp @@ -0,0 +1,69 @@ +#include "util/regex_collection.hpp" + +#include +#include + +namespace waybar::util { + +int default_priority_function(std::string& key) { return 0; } + +RegexCollection::RegexCollection(const Json::Value& map, std::string default_repr, + std::function priority_function) + : default_repr(default_repr) { + if (!map.isObject()) { + spdlog::warn("Mapping is not an object"); + return; + } + + for (auto it = map.begin(); it != map.end(); ++it) { + if (it.key().isString() && it->isString()) { + std::string key = it.key().asString(); + int priority = priority_function(key); + try { + const std::regex rule{key, std::regex_constants::icase}; + rules.emplace_back(rule, it->asString(), priority); + } catch (const std::regex_error& e) { + spdlog::error("Invalid rule '{}': {}", key, e.what()); + } + } + } + + std::sort(rules.begin(), rules.end(), [](Rule& a, Rule& b) { return a.priority > b.priority; }); +} + +std::string& RegexCollection::find_match(std::string& value, bool& matched_any) { + for (auto& rule : rules) { + if (std::regex_search(value, rule.rule)) { + matched_any = true; + return rule.repr; + } + } + + return value; +} + +std::string& RegexCollection::get(std::string& value, bool& matched_any) { + if (regex_cache.contains(value)) { + return regex_cache[value]; + } + + // std::string repr = + // waybar::util::find_match(value, window_rewrite_rules_, matched_any); + + std::string repr = find_match(value, matched_any); + + if (!matched_any) { + repr = default_repr; + } + + regex_cache.emplace(value, repr); + + return regex_cache[value]; // Necessary in order to return a reference to the heap +} + +std::string& RegexCollection::get(std::string& value) { + bool matched_any = false; + return get(value, matched_any); +} + +} // namespace waybar::util diff --git a/src/util/rewrite_string.cpp b/src/util/rewrite_string.cpp index 40c71e99..3f6ae4ca 100644 --- a/src/util/rewrite_string.cpp +++ b/src/util/rewrite_string.cpp @@ -17,7 +17,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) { try { // malformated regexes will cause an exception. // in this case, log error and try the next rule. - const std::regex rule{it.key().asString()}; + const std::regex rule{it.key().asString(), std::regex_constants::icase}; if (std::regex_match(value, rule)) { res = std::regex_replace(res, rule, it->asString()); } diff --git a/subprojects/catch2.wrap b/subprojects/catch2.wrap index 4a6f836c..f2dfd57c 100644 --- a/subprojects/catch2.wrap +++ b/subprojects/catch2.wrap @@ -1,10 +1,10 @@ [wrap-file] -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 +directory = Catch2-3.5.1 +source_url = https://github.com/catchorg/Catch2/archive/v3.5.1.tar.gz +source_filename = Catch2-3.5.1.tar.gz +source_hash = 49c3ca7a68f1c8ec71307736bc6ed14fec21631707e1be9af45daf4037e75a08 +# source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/catch2_3.4.0-1/Catch2-3.4.0.tar.gz +# wrapdb_version = 3.4.0-1 [provide] catch2 = catch2_dep diff --git a/subprojects/cava.wrap b/subprojects/cava.wrap index f6973c83..19383d11 100644 --- a/subprojects/cava.wrap +++ b/subprojects/cava.wrap @@ -1,7 +1,7 @@ [wrap-file] -directory = cava-0.8.5 -source_url = https://github.com/LukashonakV/cava/archive/0.8.5.tar.gz -source_filename = cava-0.8.5.tar.gz -source_hash = 9ce3df7d374dc83ed0704fe3caef5e00600ce061d85608aad4142d2c59aa4647 +directory = cava-0.10.1 +source_url = https://github.com/LukashonakV/cava/archive/0.10.1.tar.gz +source_filename = cava-0.10.1.tar.gz +source_hash = ae8c7339908d6febeac5ab8df4576c03c9fdbca6c8e8975daf9ce68b57038bb5 [provide] cava = cava_dep diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap index 63869be1..9efe101e 100644 --- a/subprojects/fmt.wrap +++ b/subprojects/fmt.wrap @@ -1,12 +1,13 @@ [wrap-file] -directory = fmt-8.1.1 -source_url = https://github.com/fmtlib/fmt/archive/8.1.1.tar.gz -source_filename = fmt-8.1.1.tar.gz -source_hash = 3d794d3cf67633b34b2771eb9f073bde87e846e0d395d254df7b211ef1ec7346 -patch_filename = fmt_8.1.1-1_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/fmt_8.1.1-1/get_patch -patch_hash = 6035a67c7a8c90bed74c293c7265c769f47a69816125f7566bccb8e2543cee5e +directory = fmt-9.1.0 +source_url = https://github.com/fmtlib/fmt/archive/9.1.0.tar.gz +source_filename = fmt-9.1.0.tar.gz +source_hash = 5dea48d1fcddc3ec571ce2058e13910a0d4a6bab4cc09a809d8b1dd1c88ae6f2 +patch_filename = fmt_9.1.0-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/fmt_9.1.0-2/get_patch +patch_hash = 23e8c4829f3e63f509b5643fe6bb87cbed39eae9594c451b338475d14d051967 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_9.1.0-2/fmt-9.1.0.tar.gz +wrapdb_version = 9.1.0-2 [provide] fmt = fmt_dep - diff --git a/subprojects/gtk-layer-shell.wrap b/subprojects/gtk-layer-shell.wrap index 555fbcb6..cb730345 100644 --- a/subprojects/gtk-layer-shell.wrap +++ b/subprojects/gtk-layer-shell.wrap @@ -1,5 +1,5 @@ [wrap-file] -directory = gtk-layer-shell-0.4.0 -source_filename = gtk-layer-shell-0.4.0.tar.gz -source_hash = 52fd74d3161fefa5528585ca5a523c3150934961f2284ad010ae54336dad097e -source_url = https://github.com/wmww/gtk-layer-shell/archive/v0.4.0/gtk-layer-shell-0.4.0.tar.gz +directory = gtk-layer-shell-0.8.2 +source_filename = gtk-layer-shell-0.8.2.tar.gz +source_hash = 254dd246303127c5d5236ea640f01a82e35d2d652a48d139dd669c832a0f0dce +source_url = https://github.com/wmww/gtk-layer-shell/archive/v0.8.2/gtk-layer-shell-0.8.2.tar.gz diff --git a/subprojects/spdlog.wrap b/subprojects/spdlog.wrap index 03a6d4c3..69ef566f 100644 --- a/subprojects/spdlog.wrap +++ b/subprojects/spdlog.wrap @@ -1,13 +1,12 @@ [wrap-file] -directory = spdlog-1.10.0 -source_url = https://github.com/gabime/spdlog/archive/v1.10.0.tar.gz -source_filename = v1.10.0.tar.gz -source_hash = 697f91700237dbae2326b90469be32b876b2b44888302afbc7aceb68bcfe8224 -patch_filename = spdlog_1.10.0-3_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.10.0-3/get_patch -patch_hash = 5bb07b4af1e971817d4b886efbe077aaf6c36d72d3d7e461bbcf6631f3725704 -wrapdb_version = 1.10.0-3 +directory = spdlog-1.11.0 +source_url = https://github.com/gabime/spdlog/archive/v1.11.0.tar.gz +source_filename = v1.11.0.tar.gz +source_hash = ca5cae8d6cac15dae0ec63b21d6ad3530070650f68076f3a4a862ca293a858bb +patch_filename = spdlog_1.11.0-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.11.0-2/get_patch +patch_hash = db1364fe89502ac67f245a6c8c51290a52afd74a51eed26fa9ecb5b3443df57a +wrapdb_version = 1.11.0-2 [provide] spdlog = spdlog_dep - diff --git a/test/JsonParser.cpp b/test/JsonParser.cpp new file mode 100644 index 00000000..99a8649e --- /dev/null +++ b/test/JsonParser.cpp @@ -0,0 +1,45 @@ +#include "util/json.hpp" + +#if __has_include() +#include +#else +#include +#endif + +TEST_CASE("Simple json", "[json]") { + SECTION("Parse simple json") { + std::string stringToTest = R"({"number": 5, "string": "test"})"; + waybar::util::JsonParser parser; + Json::Value jsonValue = parser.parse(stringToTest); + REQUIRE(jsonValue["number"].asInt() == 5); + REQUIRE(jsonValue["string"].asString() == "test"); + } +} + +TEST_CASE("Json with unicode", "[json]") { + SECTION("Parse json with unicode") { + std::string stringToTest = R"({"test": "\xab"})"; + waybar::util::JsonParser parser; + Json::Value jsonValue = parser.parse(stringToTest); + // compare with "\u00ab" because "\xab" is replaced with "\u00ab" in the parser + REQUIRE(jsonValue["test"].asString() == "\u00ab"); + } +} + +TEST_CASE("Json with emoji", "[json]") { + SECTION("Parse json with emoji") { + std::string stringToTest = R"({"test": "😊"})"; + waybar::util::JsonParser parser; + Json::Value jsonValue = parser.parse(stringToTest); + REQUIRE(jsonValue["test"].asString() == "😊"); + } +} + +TEST_CASE("Json with chinese characters", "[json]") { + SECTION("Parse json with chinese characters") { + std::string stringToTest = R"({"test": "你好"})"; + waybar::util::JsonParser parser; + Json::Value jsonValue = parser.parse(stringToTest); + REQUIRE(jsonValue["test"].asString() == "你好"); + } +} \ No newline at end of file diff --git a/test/css_reload_helper.cpp b/test/css_reload_helper.cpp new file mode 100644 index 00000000..f3888a83 --- /dev/null +++ b/test/css_reload_helper.cpp @@ -0,0 +1,100 @@ +#include "util/css_reload_helper.hpp" + +#include + +#if __has_include() +#include +#else +#include +#endif + +class CssReloadHelperTest : public waybar::CssReloadHelper { + public: + CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this]() { callback(); }) {} + + void callback() { m_callbackCounter++; } + + protected: + std::string getFileContents(const std::string& filename) override { + return m_fileContents[filename]; + } + + std::string findPath(const std::string& filename) override { return filename; } + + void setFileContents(const std::string& filename, const std::string& contents) { + m_fileContents[filename] = contents; + } + + int getCallbackCounter() const { return m_callbackCounter; } + + private: + int m_callbackCounter{}; + std::map m_fileContents; +}; + +TEST_CASE_METHOD(CssReloadHelperTest, "parse_imports", "[util][css_reload_helper]") { + SECTION("no imports") { + setFileContents("/tmp/waybar_test.css", "body { color: red; }"); + auto files = parseImports("/tmp/waybar_test.css"); + REQUIRE(files.size() == 1); + CHECK(files[0] == "/tmp/waybar_test.css"); + } + + SECTION("single import") { + setFileContents("/tmp/waybar_test.css", "@import 'test.css';"); + setFileContents("test.css", "body { color: red; }"); + auto files = parseImports("/tmp/waybar_test.css"); + std::sort(files.begin(), files.end()); + REQUIRE(files.size() == 2); + CHECK(files[0] == "/tmp/waybar_test.css"); + CHECK(files[1] == "test.css"); + } + + SECTION("multiple imports") { + setFileContents("/tmp/waybar_test.css", "@import 'test.css'; @import 'test2.css';"); + setFileContents("test.css", "body { color: red; }"); + setFileContents("test2.css", "body { color: blue; }"); + auto files = parseImports("/tmp/waybar_test.css"); + std::sort(files.begin(), files.end()); + REQUIRE(files.size() == 3); + CHECK(files[0] == "/tmp/waybar_test.css"); + CHECK(files[1] == "test.css"); + CHECK(files[2] == "test2.css"); + } + + SECTION("nested imports") { + setFileContents("/tmp/waybar_test.css", "@import 'test.css';"); + setFileContents("test.css", "@import 'test2.css';"); + setFileContents("test2.css", "body { color: red; }"); + auto files = parseImports("/tmp/waybar_test.css"); + std::sort(files.begin(), files.end()); + REQUIRE(files.size() == 3); + CHECK(files[0] == "/tmp/waybar_test.css"); + CHECK(files[1] == "test.css"); + CHECK(files[2] == "test2.css"); + } + + SECTION("circular imports") { + setFileContents("/tmp/waybar_test.css", "@import 'test.css';"); + setFileContents("test.css", "@import 'test2.css';"); + setFileContents("test2.css", "@import 'test.css';"); + auto files = parseImports("/tmp/waybar_test.css"); + std::sort(files.begin(), files.end()); + REQUIRE(files.size() == 3); + CHECK(files[0] == "/tmp/waybar_test.css"); + CHECK(files[1] == "test.css"); + CHECK(files[2] == "test2.css"); + } + + SECTION("empty") { + setFileContents("/tmp/waybar_test.css", ""); + auto files = parseImports("/tmp/waybar_test.css"); + REQUIRE(files.size() == 1); + CHECK(files[0] == "/tmp/waybar_test.css"); + } + + SECTION("empty name") { + auto files = parseImports(""); + REQUIRE(files.empty()); + } +} diff --git a/test/date.cpp b/test/date.cpp index aa6d79b0..d317f98a 100644 --- a/test/date.cpp +++ b/test/date.cpp @@ -1,6 +1,5 @@ #include "util/date.hpp" -#include #include #include #include @@ -8,7 +7,7 @@ #if __has_include() #include -#include +#include #else #include #endif @@ -20,13 +19,13 @@ #endif using namespace std::literals::chrono_literals; - +namespace fmt_lib = waybar::util::date::format; /* * Check that the date/time formatter with locale and timezone support is working as expected. */ -const date::zoned_time TEST_TIME = date::zoned_time{ - "UTC", date::local_days{date::Monday[1] / date::January / 2022} + 13h + 4min + 5s}; +const zoned_time TEST_TIME{ + "UTC", local_days{Monday[1] / January / 2022} + 13h + 4min + 5s}; /* * Check if the date formatted with LC_TIME=en_US is within expectations. @@ -52,10 +51,11 @@ static const bool LC_TIME_is_sane = []() { 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 not HAVE_CHRONO_TIMEZONES + CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified +#endif + CHECK(fmt_lib::format(loc, "{:%c %Z}", tm) == "Mon Jan 3 13:04:05 2022 UTC"); + CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103130405"); if (!LC_TIME_is_sane) { SKIP("Locale support check failed, skip tests"); @@ -66,11 +66,15 @@ TEST_CASE("Format UTC time", "[clock][util]") { 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 +#if not HAVE_CHRONO_TIMEZONES + CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified + CHECK_THAT(fmt_lib::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"); + CHECK(fmt_lib::format(loc, "{:%x %X}", tm) == "01/03/2022 01:04:05 PM"); +#else + CHECK(fmt_lib::format(loc, "{:%F %r}", tm) == "2022-01-03 01:04:05 PM"); +#endif + CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103130405"); } catch (const std::runtime_error &) { WARN("Locale en_US not found, skip tests"); } @@ -79,11 +83,15 @@ TEST_CASE("Format UTC time", "[clock][util]") { 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 +#if not HAVE_CHRONO_TIMEZONES + CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified + CHECK_THAT(fmt_lib::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"); + CHECK(fmt_lib::format(loc, "{:%x %X}", tm) == "03/01/22 13:04:05"); +#else + CHECK(fmt_lib::format(loc, "{:%F %T}", tm) == "2022-01-03 13:04:05"); +#endif + CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103130405"); } catch (const std::runtime_error &) { WARN("Locale en_GB not found, skip tests"); } @@ -92,11 +100,15 @@ TEST_CASE("Format UTC time", "[clock][util]") { 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 +#if not HAVE_CHRONO_TIMEZONES + CHECK(fmt_lib::format("{}", tm).empty()); // no format specified + CHECK_THAT(fmt_lib::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"); + CHECK(fmt_lib::format("{:%x %X}", tm) == "01/03/2022 01:04:05 PM"); +#else + CHECK(fmt_lib::format("{:%F %r}", tm) == "2022-01-03 01:04:05 PM"); +#endif + CHECK(fmt_lib::format("{:%Y%m%d%H%M%S}", tm) == "20220103130405"); std::locale::global(loc); } catch (const std::runtime_error &) { @@ -107,11 +119,13 @@ TEST_CASE("Format UTC time", "[clock][util]") { TEST_CASE("Format zoned time", "[clock][util]") { const auto loc = std::locale("C"); - const auto tm = date::zoned_time{"America/New_York", TEST_TIME}; + const auto tm = 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 not HAVE_CHRONO_TIMEZONES + CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified +#endif + CHECK(fmt_lib::format(loc, "{:%c %Z}", tm) == "Mon Jan 3 08:04:05 2022 EST"); + CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103080405"); if (!LC_TIME_is_sane) { SKIP("Locale support check failed, skip tests"); @@ -122,11 +136,15 @@ TEST_CASE("Format zoned time", "[clock][util]") { 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 +#if not HAVE_CHRONO_TIMEZONES + CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified + CHECK_THAT(fmt_lib::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"); + CHECK(fmt_lib::format(loc, "{:%x %X}", tm) == "01/03/2022 08:04:05 AM"); +#else + CHECK(fmt_lib::format(loc, "{:%F %r}", tm) == "2022-01-03 08:04:05 AM"); +#endif + CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103080405"); } catch (const std::runtime_error &) { WARN("Locale en_US not found, skip tests"); } @@ -135,11 +153,15 @@ TEST_CASE("Format zoned time", "[clock][util]") { 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 +#if not HAVE_CHRONO_TIMEZONES + CHECK(fmt_lib::format(loc, "{}", tm).empty()); // no format specified + CHECK_THAT(fmt_lib::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"); + CHECK(fmt_lib::format(loc, "{:%x %X}", tm) == "03/01/22 08:04:05"); +#else + CHECK(fmt_lib::format(loc, "{:%F %T}", tm) == "2022-01-03 08:04:05"); +#endif + CHECK(fmt_lib::format(loc, "{:%Y%m%d%H%M%S}", tm) == "20220103080405"); } catch (const std::runtime_error &) { WARN("Locale en_GB not found, skip tests"); } @@ -148,11 +170,15 @@ TEST_CASE("Format zoned time", "[clock][util]") { 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 +#if not HAVE_CHRONO_TIMEZONES + CHECK(fmt_lib::format("{}", tm).empty()); // no format specified + CHECK_THAT(fmt_lib::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"); + CHECK(fmt_lib::format("{:%x %X}", tm) == "01/03/2022 08:04:05 AM"); +#else + CHECK(fmt_lib::format("{:%F %r}", tm) == "2022-01-03 08:04:05 AM"); +#endif + CHECK(fmt_lib::format("{:%Y%m%d%H%M%S}", tm) == "20220103080405"); std::locale::global(loc); } catch (const std::runtime_error &) { diff --git a/test/main.cpp b/test/main.cpp index daeee69e..15e17b0f 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -3,8 +3,9 @@ #include #include -#if __has_include() -#include +#if __has_include() +#include +#include #include #else #include diff --git a/test/meson.build b/test/meson.build index 02cbb2a4..7c922671 100644 --- a/test/meson.build +++ b/test/meson.build @@ -8,9 +8,12 @@ test_dep = [ ] test_src = files( 'main.cpp', + 'JsonParser.cpp', 'SafeSignal.cpp', 'config.cpp', + 'css_reload_helper.cpp', '../src/config.cpp', + '../src/util/css_reload_helper.cpp', ) if tz_dep.found() @@ -28,5 +31,5 @@ waybar_test = executable( test( 'waybar', waybar_test, - workdir: meson.source_root(), + workdir: meson.project_source_root(), )