diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..3d4cf260 --- /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..0fad47c4 --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,22 @@ +name: clang-format + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-format-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # TODO: bump to clang 19 release + # - uses: DoozyX/clang-format-lint-action@v0.18.2 + - uses: DoozyX/clang-format-lint-action@558090054b3f39e3d6af24f0cd73b319535da809 + name: clang-format + with: + source: "." + extensions: "hpp,h,cpp,c" + style: "file:.clang-format" + clangFormatVersion: 19 diff --git a/.github/workflows/clang-tidy.yml.bak b/.github/workflows/clang-tidy.yml.bak new file mode 100644 index 00000000..ec67fb7e --- /dev/null +++ b/.github/workflows/clang-tidy.yml.bak @@ -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: + lint: + 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/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..0e7e2944 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,34 @@ +name: Build and Push Docker Image + +on: + workflow_dispatch: + schedule: + # run monthly + - cron: '0 0 1 * *' + +jobs: + build-and-push: + runs-on: ubuntu-latest + if: github.event_name != 'schedule' || github.repository == 'Alexays/Waybar' + strategy: + fail-fast: false # don't fail the other jobs if one of the images fails to build + matrix: + os: [ 'alpine', 'archlinux', 'debian', 'fedora', 'gentoo', 'opensuse' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfiles/${{ matrix.os }} + push: true + tags: alexays/waybar:${{ matrix.os }} diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 550f9453..e45a8dc4 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -1,29 +1,35 @@ 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 + build: + # 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.25.0 + timeout-minutes: 180 + env: + CPPFLAGS: '-isystem/usr/local/include' + LDFLAGS: '-L/usr/local/lib' + with: + operating_system: freebsd + version: "14.3" + environment_variables: CPPFLAGS LDFLAGS + sync_files: runner-to-vm + run: | + 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 pipewire pulseaudio scdoc sndio spdlog wayland-protocols upower \ + libinotify + meson setup 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 1c00a5ed..c36f68e2 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -2,9 +2,14 @@ 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: + fail-fast: false matrix: distro: - alpine @@ -13,10 +18,7 @@ jobs: - fedora - opensuse - gentoo - cpp_std: [c++17] - include: - - distro: fedora - cpp_std: c++20 + cpp_std: [c++20] runs-on: ubuntu-latest container: @@ -25,7 +27,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: configure - run: meson -Dman-pages=enabled -Dcpp_std=${{matrix.cpp_std}} build + run: meson setup -Dman-pages=enabled -Dcpp_std=${{matrix.cpp_std}} build - name: build run: ninja -C build - name: test diff --git a/.github/workflows/nix-tests.yml b/.github/workflows/nix-tests.yml new file mode 100644 index 00000000..8859ecb5 --- /dev/null +++ b/.github/workflows/nix-tests.yml @@ -0,0 +1,17 @@ +name: "Nix-Tests" +on: + pull_request: + push: +jobs: + nix-flake-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - run: nix flake show + - run: nix flake check --print-build-logs + - run: nix build --print-build-logs diff --git a/.github/workflows/nix-update-flake-lock.yml b/.github/workflows/nix-update-flake-lock.yml new file mode 100644 index 00000000..a1679ead --- /dev/null +++ b/.github/workflows/nix-update-flake-lock.yml @@ -0,0 +1,22 @@ +name: update-flake-lock +on: + workflow_dispatch: # allows manual triggering + schedule: + - cron: '0 0 1 * *' # Run monthly + push: + paths: + - 'flake.nix' +jobs: + lockfile: + runs-on: ubuntu-latest + if: github.event_name != 'schedule' || github.repository == 'Alexays/Waybar' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Nix + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} + - name: Update flake.lock + uses: DeterminateSystems/update-flake-lock@v21 diff --git a/.gitignore b/.gitignore index 4d7babf3..b486237e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,9 @@ packagecache *.out *.app /.direnv/ + +# Nix +result +result-* + +.ccls-cache diff --git a/Dockerfiles/archlinux b/Dockerfiles/archlinux index cab4146b..d4274a46 100644 --- a/Dockerfiles/archlinux +++ b/Dockerfiles/archlinux @@ -3,5 +3,5 @@ FROM archlinux:base-devel RUN pacman -Syu --noconfirm && \ - pacman -S --noconfirm git meson base-devel libinput wayland wayland-protocols pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient gobject-introspection libxkbcommon playerctl && \ + pacman -S --noconfirm git meson base-devel libinput wayland wayland-protocols glib2-devel pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient gobject-introspection libxkbcommon playerctl iniparser fftw && \ sed -Ei 's/#(en_(US|GB)\.UTF)/\1/' /etc/locale.gen && locale-gen diff --git a/Dockerfiles/debian b/Dockerfiles/debian index 578588c7..c2584ccf 100644 --- a/Dockerfiles/debian +++ b/Dockerfiles/debian @@ -1,7 +1,49 @@ # vim: ft=Dockerfile -FROM debian:sid +FROM debian:sid-slim -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.5-dev \ + libxkbcommon-dev \ + libxkbregistry-dev \ + locales \ + meson \ + ninja-build \ + pkg-config \ + python3-pip \ + python3-venv \ + scdoc \ + sudo \ + wayland-protocols \ + && apt clean diff --git a/Dockerfiles/fedora b/Dockerfiles/fedora index 5892159c..9dc0337b 100644 --- a/Dockerfiles/fedora +++ b/Dockerfiles/fedora @@ -29,6 +29,6 @@ RUN dnf install -y @c-development \ 'pkgconfig(wayland-client)' \ 'pkgconfig(wayland-cursor)' \ 'pkgconfig(wayland-protocols)' \ - 'pkgconfig(wireplumber-0.4)' \ + 'pkgconfig(wireplumber-0.5)' \ 'pkgconfig(xkbregistry)' && \ dnf clean all -y diff --git a/Dockerfiles/gentoo b/Dockerfiles/gentoo index f2ec0dc9..f7023825 100644 --- a/Dockerfiles/gentoo +++ b/Dockerfiles/gentoo @@ -6,6 +6,6 @@ RUN export FEATURES="-ipc-sandbox -network-sandbox -pid-sandbox -sandbox -usersa emerge --sync && \ eselect news read --quiet new 1>/dev/null 2>&1 && \ emerge --verbose --update --deep --with-bdeps=y --backtrack=30 --newuse @world && \ - USE="wayland gtk3 gtk -doc X pulseaudio minimal" emerge dev-vcs/git dev-libs/wayland dev-libs/wayland-protocols =dev-cpp/gtkmm-3.24.6 x11-libs/libxkbcommon \ + USE="wayland gtk3 gtk -doc X pulseaudio minimal" emerge dev-vcs/git dev-libs/wayland dev-libs/wayland-protocols dev-cpp/gtkmm:3.0 x11-libs/libxkbcommon \ x11-libs/gtk+:3 dev-libs/libdbusmenu dev-libs/libnl sys-power/upower media-libs/libpulse dev-libs/libevdev media-libs/libmpdclient \ media-sound/sndio gui-libs/gtk-layer-shell app-text/scdoc media-sound/playerctl dev-libs/iniparser sci-libs/fftw diff --git a/Dockerfiles/opensuse b/Dockerfiles/opensuse index bdb42fbf..6ac3e058 100644 --- a/Dockerfiles/opensuse +++ b/Dockerfiles/opensuse @@ -6,4 +6,4 @@ RUN zypper -n up && \ zypper addrepo https://download.opensuse.org/repositories/X11:Wayland/openSUSE_Tumbleweed/X11:Wayland.repo | echo 'a' && \ zypper -n refresh && \ zypper -n install -t pattern devel_C_C++ && \ - zypper -n install git meson clang libinput10 libinput-devel pugixml-devel libwayland-client0 libwayland-cursor0 wayland-protocols-devel wayland-devel Mesa-libEGL-devel Mesa-libGLESv2-devel libgbm-devel libxkbcommon-devel libudev-devel libpixman-1-0-devel gtkmm3-devel jsoncpp-devel libxkbregistry-devel scdoc playerctl-devel + zypper -n install git meson clang libinput10 libinput-devel pugixml-devel libwayland-client0 libwayland-cursor0 wayland-protocols-devel wayland-devel Mesa-libEGL-devel Mesa-libGLESv2-devel libgbm-devel libxkbcommon-devel libudev-devel libpixman-1-0-devel gtkmm3-devel jsoncpp-devel libxkbregistry-devel scdoc playerctl-devel python3-packaging diff --git a/LICENSE b/LICENSE index 41eb81d8..d1bad1b4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Alex +Copyright (c) 2025 Alex Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index b1dbfc6e..3bb11199 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,11 @@ default: build build: - meson build + meson setup build ninja -C build build-debug: - meson build --buildtype=debug + meson setup build --buildtype=debug ninja -C build install: build diff --git a/README.md b/README.md index 718ceb44..5266e916 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ # 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) -- DWL (Tags) [requires dwl ipc patch](https://github.com/djpohly/dwl/wiki/ipc) +- Hyprland (Window Icons, Workspaces, Focused window name) +- Niri (Workspaces, Focused window name, Language) +- DWL (Tags, Focused window name) [requires dwl ipc patch](https://codeberg.org/dwl/dwl-patches/src/branch/main/patches/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 @@ -36,7 +38,7 @@ Waybar is available from a number of Linux distributions: -[![Packaging status](https://repology.org/badge/vertical-allrepos/waybar.svg)](https://repology.org/project/waybar/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/waybar.svg?columns=3&header=Waybar%20Downstream%20Packaging)](https://repology.org/project/waybar/versions) An Ubuntu PPA with more recent versions is available [here](https://launchpad.net/~nschloe/+archive/ubuntu/waybar). @@ -47,7 +49,7 @@ An Ubuntu PPA with more recent versions is available ```bash $ git clone https://github.com/Alexays/Waybar $ cd Waybar -$ meson build +$ meson setup build $ ninja -C build $ ./build/waybar # If you want to install it diff --git a/default.nix b/default.nix index 2cccff28..6466507b 100644 --- a/default.nix +++ b/default.nix @@ -1,10 +1,9 @@ -(import - ( - let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } - ) - { src = ./.; } -).defaultNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).defaultNix diff --git a/flake.lock b/flake.lock index b10c9bf7..34545ed4 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": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "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": 1748460289, + "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=", "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": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102", "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..7c7a2281 100644 --- a/flake.nix +++ b/flake.nix @@ -1,93 +1,128 @@ { - 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 [ - "x86_64-linux" - ]; + genSystems = + func: + lib.genAttrs + [ + "x86_64-linux" + "aarch64-linux" + ] + ( + system: + func ( + import nixpkgs { + inherit system; + overlays = with self.overlays; [ + waybar + ]; + } + ) + ); - pkgsFor = genSystems (system: - import nixpkgs { - inherit system; - }); - - mkDate = longDate: (lib.concatStringsSep "-" [ - (builtins.substring 0 4 longDate) - (builtins.substring 4 2 longDate) - (builtins.substring 6 2 longDate) - ]); + mkDate = + longDate: + (lib.concatStringsSep "-" [ + (builtins.substring 0 4 longDate) + (builtins.substring 4 2 longDate) + (builtins.substring 6 2 longDate) + ]); in { - overlays.default = _: prev: { - waybar = prev.callPackage ./nix/default.nix { - version = prev.waybar.version + "+date=" + (mkDate (self.lastModifiedDate or "19700101")) + "_" + (self.shortRev or "dirty"); - }; - }; - packages = genSystems - (system: - (self.overlays.default null pkgsFor.${system}) - // { - default = self.packages.${system}.waybar; - }); - } // - flake-utils.lib.eachDefaultSystem (system: { - devShell = - let pkgs = import nixpkgs { - inherit system; + devShells = genSystems (pkgs: { + default = pkgs.mkShell { + name = "waybar-shell"; - overlays = [ devshell.overlay ]; + # 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; [ + nixfmt-rfc-style + clang-tools + gdb + ]); }; - in - pkgs.devshell.mkShell { - imports = [ "${pkgs.devshell.extraModulesDir}/language/c.nix" ]; - commands = [ + }); + + formatter = genSystems ( + pkgs: + pkgs.treefmt.withConfig { + settings = [ { - package = pkgs.devshell.cli; - help = "Per project developer environments"; + formatter = { + clang-format = { + options = [ "-i" ]; + command = lib.getExe' pkgs.clang-tools "clang-format"; + excludes = [ ]; + includes = [ + "*.c" + "*.cpp" + "*.h" + "*.hpp" + ]; + }; + nixfmt = { + command = lib.getExe pkgs.nixfmt-rfc-style; + includes = [ "*.nix" ]; + }; + }; + tree-root-file = ".git/index"; } ]; - 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"; } - ]; + overlays = { + default = self.overlays.waybar; + waybar = final: prev: { + waybar = final.callPackage ./nix/default.nix { + waybar = prev.waybar; + # take the first "version: '...'" from meson.build + version = + (builtins.head ( + builtins.split "'" ( + builtins.elemAt (builtins.split " version: '" (builtins.readFile ./meson.build)) 2 + ) + )) + + "+date=" + + (mkDate (self.lastModifiedDate or "19700101")) + + "_" + + (self.shortRev or "dirty"); + }; }; - }); + }; + + packages = genSystems (pkgs: { + default = self.packages.${pkgs.stdenv.hostPlatform.system}.waybar; + inherit (pkgs) waybar; + }); + }; } diff --git a/include/AAppIconLabel.hpp b/include/AAppIconLabel.hpp new file mode 100644 index 00000000..d09ab14a --- /dev/null +++ b/include/AAppIconLabel.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "AIconLabel.hpp" + +namespace waybar { + +class AAppIconLabel : public AIconLabel { + public: + AAppIconLabel(const Json::Value &config, const std::string &name, const std::string &id, + const std::string &format, uint16_t interval = 0, bool ellipsize = false, + bool enable_click = false, bool enable_scroll = false); + virtual ~AAppIconLabel() = default; + auto update() -> void override; + + protected: + void updateAppIconName(const std::string &app_identifier, + const std::string &alternative_app_identifier); + void updateAppIcon(); + unsigned app_icon_size_{24}; + bool update_app_icon_{true}; + std::string app_icon_name_; +}; + +} // namespace waybar diff --git a/include/ALabel.hpp b/include/ALabel.hpp index 888c65a8..a1aae9da 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -27,6 +27,10 @@ class ALabel : public AModule { bool handleToggle(GdkEventButton *const &e) override; virtual std::string getState(uint8_t value, bool lesser = false); + + std::map submenus_; + std::map menuActionsMap_; + static void handleGtkMenuEvent(GtkMenuItem *menuitem, gpointer data); }; } // namespace waybar diff --git a/include/AModule.hpp b/include/AModule.hpp index 03bf25e1..2fcbfc23 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -11,14 +12,19 @@ namespace waybar { class AModule : public IModule { public: - virtual ~AModule(); + static constexpr const char *MODULE_CLASS = "module"; + + ~AModule() override; auto update() -> void override; - virtual auto refresh(int) -> void{}; + virtual auto refresh(int shouldRefresh) -> void {}; operator Gtk::Widget &() override; auto doAction(const std::string &name) -> void override; + /// Emitting on this dispatcher triggers a update() call Glib::Dispatcher dp; + bool expandEnabled() const; + protected: // Don't need to make an object directly // Derived classes are able to use it @@ -28,34 +34,49 @@ class AModule : public IModule { enum SCROLL_DIR { NONE, UP, DOWN, LEFT, RIGHT }; SCROLL_DIR getScrollDir(GdkEventScroll *e); - bool tooltipEnabled(); + bool tooltipEnabled() const; std::vector pid_children_; const std::string name_; const Json::Value &config_; Gtk::EventBox event_box_; + virtual void setCursor(Gdk::CursorType const &c); + virtual bool handleToggle(GdkEventButton *const &ev); + virtual bool handleMouseEnter(GdkEventCrossing *const &ev); + virtual bool handleMouseLeave(GdkEventCrossing *const &ev); virtual bool handleScroll(GdkEventScroll *); + virtual bool handleRelease(GdkEventButton *const &ev); + GObject *menu_; private: + bool handleUserEvent(GdkEventButton *const &ev); + const bool isTooltip; + const bool isExpand; + bool hasUserEvents_; 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..9b407abf 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -9,9 +9,11 @@ #include #include +#include #include #include "AModule.hpp" +#include "group.hpp" #include "xdg-output-unstable-v1-client-protocol.h" namespace waybar { @@ -52,35 +54,19 @@ class BarIpcClient; } #endif // HAVE_SWAY -class BarSurface { - protected: - BarSurface() = default; - +class Bar : public sigc::trackable { 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 setVisible(bool visible); + void setMode(const std::string &mode); + void setVisible(bool value); void toggle(); void handleSignal(int); @@ -88,8 +74,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 +88,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/config.hpp b/include/config.hpp index 66945542..5256bb46 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -20,6 +20,9 @@ class Config { static std::optional findConfigPath( const std::vector &names, const std::vector &dirs = CONFIG_DIRS); + static std::vector tryExpandPath(const std::string &base, + const std::string &filename); + Config() = default; void load(const std::string &config); 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..5ce331a8 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -5,18 +5,33 @@ #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; + Group(const std::string &, const std::string &, const Json::Value &, bool); + ~Group() override = default; auto update() -> void override; - operator Gtk::Widget&() override; + operator Gtk::Widget &() override; + + virtual Gtk::Box &getBox(); + void addWidget(Gtk::Widget &widget); + + protected: Gtk::Box box; + Gtk::Box revealer_box; + Gtk::Revealer revealer; + bool is_first_widget = true; + bool is_drawer = false; + bool click_to_reveal = false; + std::string add_class_to_drawer_children; + bool handleMouseEnter(GdkEventCrossing *const &ev) override; + bool handleMouseLeave(GdkEventCrossing *const &ev) override; + bool handleToggle(GdkEventButton *const &ev) override; + void show_group(); + void hide_group(); }; } // 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..8e1a2ad2 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; @@ -38,8 +32,9 @@ class Battery : public ALabel { void refreshBatteries(); void worker(); const std::string getAdapterStatus(uint8_t capacity) const; - const std::tuple getInfos(); + std::tuple getInfos(); const std::string formatTimeRemaining(float hoursRemaining); + void setBarClass(std::string&); 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..b89383a0 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -49,6 +49,9 @@ class Bluetooth : public ALabel { auto update() -> void override; private: + static auto onObjectAdded(GDBusObjectManager*, GDBusObject*, gpointer) -> void; + static auto onObjectRemoved(GDBusObjectManager*, GDBusObject*, gpointer) -> void; + static auto onInterfaceAddedOrRemoved(GDBusObjectManager*, GDBusObject*, GDBusInterface*, gpointer) -> void; static auto onInterfaceProxyPropertiesChanged(GDBusObjectManagerClient*, GDBusObjectProxy*, @@ -59,7 +62,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 +72,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..1a88c7b7 100644 --- a/include/modules/cava.hpp +++ b/include/modules/cava.hpp @@ -3,9 +3,20 @@ #include "ALabel.hpp" #include "util/sleeper_thread.hpp" +namespace cava { extern "C" { +// Need sdl_glsl output feature to be enabled on libcava +#ifndef SDL_GLSL +#define SDL_GLSL +#endif + #include + +#ifdef SDL_GLSL +#undef SDL_GLSL +#endif } +} // namespace cava namespace waybar::modules { using namespace std::literals::chrono_literals; @@ -21,13 +32,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,11 +47,13 @@ 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}; + std::string format_silent_{""}; int sleep_counter_{0}; // Cava method void pause_resume(); // ModuleActionMap - static inline std::map actionMap_{ + static inline std::map actionMap_{ {"mode", &waybar::modules::Cava::pause_resume}}; }; } // namespace waybar::modules 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 fab38111..e34b7a8e 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -6,67 +6,86 @@ 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(); - bool is_timezone_fixed(); - auto timezones_text(std::chrono::system_clock::time_point* now) -> std::string; - - /*Calendar properties*/ - WeeksSide cldWPos_{WeeksSide::HIDDEN}; + const std::locale m_locale_; + // tooltip + const std::string m_tlpFmt_; + std::string m_tlpText_{""}; // tooltip text to print + const Glib::RefPtr m_tooltip_; // tooltip as a separate Gtk::Label + bool query_tlp_cb(int, int, bool, const Glib::RefPtr& tooltip); + // Calendar + const bool cldInTooltip_; // calendar in tooltip + /* + 0 - calendar.format.months + 1 - calendar.format.weekdays + 2 - calendar.format.days + 3 - calendar.format.today + 4 - calendar.format.weeks + 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 + date::months cldCurrShift_{0}; // calendar months shift + int cldShift_{1}; // calendar months shift factor + date::year_month_day cldYearShift_; // calendar Year mode. Cached ymd + std::string cldYearCached_; // calendar Year mode. Cached calendar + date::year_month cldMonShift_; // calendar Month mode. Cached ym + std::string cldMonCached_; // calendar Month mode. Cached calendar + date::day cldBaseDay_{0}; // calendar Cached day. Is used when today is changing(midnight) + std::string cldText_{""}; // calendar text to print CldMode cldMode_{CldMode::MONTH}; - uint cldMonCols_{3}; // Count of the month in the row - int cldMonColLen_{20}; // Length of the month column - int cldWnLen_{3}; // Length of the week number - date::year_month_day cldYearShift_; - date::year_month cldMonShift_; - date::months cldCurrShift_{0}; - date::months cldShift_{0}; - std::string cldYearCached_{}; - std::string cldMonCached_{}; - date::day cldBaseDay_{0}; - /*Calendar functions*/ - auto get_calendar(const date::zoned_seconds& now, const date::zoned_seconds& wtime) - -> std::string; - /*Clock actions*/ + auto get_calendar(const date::year_month_day& today, const date::year_month_day& ymd, + const date::time_zone* tz) -> const std::string; + + // get local time zone + auto local_zone() -> const date::time_zone*; + + // time zoned time in tooltip + const bool tzInTooltip_; // if need to print time zones text + std::vector tzList_; // time zones list + int tzCurrIdx_; // current time zone index for tzList_ + std::string tzText_{""}; // time zones text to print + util::SleeperThread thread_; + + // ordinal date in tooltip + const bool ordInTooltip_; + std::string ordText_{""}; + auto get_ordinal_date(const date::year_month_day& today) -> std::string; + + auto getTZtext(date::sys_seconds now) -> std::string; + auto first_day_of_week() -> date::weekday; + // Module actions void cldModeSwitch(); void cldShift_up(); void cldShift_down(); + void cldShift_reset(); void tz_up(); void tz_down(); - - // ModuleActionMap - static inline std::map actionMap_{ + // Module Action Map + static inline std::map actionMap_{ {"mode", &waybar::modules::Clock::cldModeSwitch}, {"shift_up", &waybar::modules::Clock::cldShift_up}, {"shift_down", &waybar::modules::Clock::cldShift_down}, + {"shift_reset", &waybar::modules::Clock::cldShift_reset}, {"tz_up", &waybar::modules::Clock::tz_up}, {"tz_down", &waybar::modules::Clock::tz_down}}; }; + } // 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..6c17c6e4 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,10 +30,12 @@ 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_; std::string tooltip_; + const bool tooltip_format_enabled_; std::vector class_; int percentage_; FILE* fp_; 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/dwl/window.hpp b/include/modules/dwl/window.hpp new file mode 100644 index 00000000..43586399 --- /dev/null +++ b/include/modules/dwl/window.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "dwl-ipc-unstable-v2-client-protocol.h" +#include "util/json.hpp" + +namespace waybar::modules::dwl { + +class Window : public AAppIconLabel, public sigc::trackable { + public: + Window(const std::string &, const waybar::Bar &, const Json::Value &); + ~Window(); + + void handle_layout(const uint32_t layout); + void handle_title(const char *title); + void handle_appid(const char *ppid); + void handle_layout_symbol(const char *layout_symbol); + void handle_frame(); + + struct zdwl_ipc_manager_v2 *status_manager_; + + private: + const Bar &bar_; + + std::string title_; + std::string appid_; + std::string layout_symbol_; + uint32_t layout_; + + struct zdwl_ipc_output_v2 *output_status_; +}; + +} // namespace waybar::modules::dwl diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index e23b1582..cfd0b258 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -1,10 +1,11 @@ #pragma once -#include + +#include #include -#include #include #include #include +#include #include "util/json.hpp" @@ -18,23 +19,31 @@ class EventHandler { class IPC { public: - IPC() { startIPC(); } + IPC(); + ~IPC(); + static IPC& inst(); - 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); + static std::filesystem::path getSocketFolder(const char* instanceSig); + + protected: + static std::filesystem::path socketFolder_; private: - void startIPC(); + void socketListener(); void parseIPC(const std::string&); - std::mutex callbackMutex; + std::thread ipcThread_; + std::mutex callbackMutex_; util::JsonParser parser_; - std::list> callbacks; + std::list> callbacks_; + int socketfd_; // the hyprland socket file descriptor + bool running_ = true; }; -inline std::unique_ptr gIPC; inline bool modulesReady = false; }; // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index 30789d06..ec59e5c3 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,13 +30,15 @@ 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_; util::JsonParser parser_; Layout layout_; + + IPC& m_ipc; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/submap.hpp b/include/modules/hyprland/submap.hpp index e2a84981..7e3425ef 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,17 +14,22 @@ namespace waybar::modules::hyprland { class Submap : public waybar::ALabel, public EventHandler { public: Submap(const std::string&, const waybar::Bar&, const Json::Value&); - virtual ~Submap(); + ~Submap() override; auto update() -> void override; private: - void onEvent(const std::string&) override; + auto parseConfig(const Json::Value&) -> void; + void onEvent(const std::string& ev) override; std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; std::string submap_; + bool always_on_ = false; + std::string default_submap_ = "Default"; + + IPC& m_ipc; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index 950be05f..2be64594 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -1,16 +1,20 @@ +#pragma once + #include -#include "ALabel.hpp" +#include + +#include "AAppIconLabel.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" #include "util/json.hpp" namespace waybar::modules::hyprland { -class Window : public waybar::ALabel, public EventHandler { +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,26 +25,43 @@ class Window : public waybar::ALabel, 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; }; - auto getActiveWorkspace(const std::string&) -> Workspace; - auto getActiveWorkspace() -> Workspace; - void onEvent(const std::string&) override; + struct WindowData { + bool floating; + int monitor = -1; + std::string class_name; + std::string initial_class_name; + std::string title; + std::string initial_title; + bool fullscreen; + bool grouped; + + static auto parse(const Json::Value&) -> WindowData; + }; + + 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_; - std::string last_title_; + 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_; + + IPC& m_ipc; }; } // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/windowcreationpayload.hpp b/include/modules/hyprland/windowcreationpayload.hpp new file mode 100644 index 00000000..e4180ed9 --- /dev/null +++ b/include/modules/hyprland/windowcreationpayload.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "util/enum.hpp" +#include "util/regex_collection.hpp" + +using WindowAddress = std::string; + +namespace waybar::modules::hyprland { + +class Workspaces; + +class WindowCreationPayload { + public: + WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + std::string window_repr); + WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + std::string window_class, std::string window_title); + WindowCreationPayload(Json::Value const& client_data); + + int incrementTimeSpentUncreated(); + bool isEmpty(Workspaces& workspace_manager); + bool reprIsReady() const { return std::holds_alternative(m_window); } + std::string repr(Workspaces& workspace_manager); + + std::string getWorkspaceName() const { return m_workspaceName; } + WindowAddress getAddress() const { return m_windowAddress; } + + void moveToWorkspace(std::string& new_workspace_name); + + private: + void clearAddr(); + void clearWorkspaceName(); + + using Repr = std::string; + using ClassAndTitle = std::pair; + std::variant m_window; + + WindowAddress m_windowAddress; + std::string m_workspaceName; + + int m_timeSpentUncreated = 0; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp new file mode 100644 index 00000000..a257ddef --- /dev/null +++ b/include/modules/hyprland/workspace.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/hyprland/backend.hpp" +#include "modules/hyprland/windowcreationpayload.hpp" +#include "util/enum.hpp" +#include "util/regex_collection.hpp" + +using WindowAddress = std::string; + +namespace waybar::modules::hyprland { + +class Workspaces; +class Workspace { + public: + explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, + const Json::Value& clients_data = Json::Value::nullRef); + std::string& selectIcon(std::map& icons_map); + Gtk::Button& button() { return m_button; }; + + int id() const { return m_id; }; + std::string name() const { return m_name; }; + std::string output() const { return m_output; }; + bool isActive() const { return m_isActive; }; + bool isSpecial() const { return m_isSpecial; }; + bool isPersistent() const { return m_isPersistentRule || m_isPersistentConfig; }; + bool isPersistentConfig() const { return m_isPersistentConfig; }; + bool isPersistentRule() const { return m_isPersistentRule; }; + bool isVisible() const { return m_isVisible; }; + bool isEmpty() const { return m_windows == 0; }; + bool isUrgent() const { return m_isUrgent; }; + + bool handleClicked(GdkEventButton* bt) const; + void setActive(bool value = true) { m_isActive = value; }; + void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; + void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; + void setUrgent(bool value = true) { m_isUrgent = value; }; + void setVisible(bool value = true) { m_isVisible = value; }; + void setWindows(uint value) { m_windows = value; }; + void setName(std::string const& value) { m_name = value; }; + void setOutput(std::string const& value) { m_output = value; }; + bool containsWindow(WindowAddress const& addr) const { return m_windowMap.contains(addr); } + void insertWindow(WindowCreationPayload create_window_payload); + std::string removeWindow(WindowAddress const& addr); + void initializeWindowMap(const Json::Value& clients_data); + + bool onWindowOpened(WindowCreationPayload const& create_window_payload); + std::optional closeWindow(WindowAddress const& addr); + + void update(const std::string& format, const std::string& icon); + + private: + Workspaces& m_workspaceManager; + + int m_id; + std::string m_name; + std::string m_output; + uint m_windows; + bool m_isActive = false; + bool m_isSpecial = false; + bool m_isPersistentRule = false; // represents the persistent state in hyprland + bool m_isPersistentConfig = false; // represents the persistent state in the Waybar config + bool m_isUrgent = false; + bool m_isVisible = false; + + std::map m_windowMap; + + Gtk::Button m_button; + Gtk::Box m_content; + Gtk::Label m_label; + IPC& m_ipc; +}; + +} // namespace waybar::modules::hyprland diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 500bbe36..302648d0 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -1,64 +1,165 @@ +#pragma once + #include #include +#include +#include +#include #include +#include +#include +#include +#include #include "AModule.hpp" #include "bar.hpp" #include "modules/hyprland/backend.hpp" +#include "modules/hyprland/windowcreationpayload.hpp" +#include "modules/hyprland/workspace.hpp" +#include "util/enum.hpp" +#include "util/regex_collection.hpp" + +using WindowAddress = std::string; namespace waybar::modules::hyprland { -struct WorkspaceDto { - int id; - - static WorkspaceDto parse(const Json::Value& value); -}; - -class Workspace { - public: - Workspace(int id); - Workspace(WorkspaceDto dto); - int id() const { return id_; }; - int active() const { return active_; }; - std::string& select_icon(std::map& icons_map); - void set_active(bool value = true) { active_ = value; }; - Gtk::Button& button() { return button_; }; - - void update(const std::string& format, const std::string& icon); - - private: - int id_; - bool active_; - - Gtk::Button button_; - Gtk::Box content_; - Gtk::Label label_; -}; +class Workspaces; class Workspaces : public AModule, public EventHandler { public: Workspaces(const std::string&, const waybar::Bar&, const Json::Value&); - virtual ~Workspaces(); + ~Workspaces() override; void update() override; void init(); - private: - void onEvent(const std::string&) override; - void sort_workspaces(); - void create_workspace(int id); - void remove_workspace(int id); + auto allOutputs() const -> bool { return m_allOutputs; } + auto showSpecial() const -> bool { return m_showSpecial; } + auto activeOnly() const -> bool { return m_activeOnly; } + auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } + auto moveToMonitor() const -> bool { return m_moveToMonitor; } - std::string format_; - std::map icons_map_; - bool with_icon_; - int active_workspace_id; - std::vector> workspaces_; - std::vector workspaces_to_create_; - std::vector workspaces_to_remove_; - std::mutex mutex_; - const Bar& bar_; - Gtk::Box box_; + 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& e) override; + void updateWindowCount(); + void sortWorkspaces(); + void createWorkspace(Json::Value const& workspace_data, + Json::Value const& clients_data = Json::Value::nullRef); + + static Json::Value createMonitorWorkspaceData(std::string const& name, + std::string const& monitor); + void removeWorkspace(std::string const& workspaceString); + void setUrgentWorkspace(std::string const& windowaddress); + + // Config + void parseConfig(const Json::Value& config); + auto populateIconsMap(const Json::Value& formatIcons) -> void; + static auto populateBoolConfig(const Json::Value& config, const std::string& key, bool& member) + -> void; + auto populateSortByConfig(const Json::Value& config) -> void; + auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; + auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; + auto populateWindowRewriteConfig(const Json::Value& config) -> void; + + void registerIpc(); + + // workspace events + void onWorkspaceActivated(std::string const& payload); + void onSpecialWorkspaceActivated(std::string const& payload); + void onWorkspaceDestroyed(std::string const& payload); + void onWorkspaceCreated(std::string const& payload, + Json::Value const& clientsData = Json::Value::nullRef); + void onWorkspaceMoved(std::string const& payload); + void onWorkspaceRenamed(std::string const& payload); + static std::optional parseWorkspaceId(std::string const& workspaceIdStr); + + // monitor events + void onMonitorFocused(std::string const& payload); + + // 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); + + // event payload management + template + static std::string makePayload(Args const&... args); + static std::pair splitDoublePayload(std::string const& payload); + static std::tuple splitTriplePayload( + std::string const& payload); + + // Update methods + void doUpdate(); + void removeWorkspacesToRemove(); + void createWorkspacesToCreate(); + static std::vector getVisibleWorkspaces(); + void updateWorkspaceStates(); + bool updateWindowsToCreate(); + + void extendOrphans(int workspaceId, Json::Value const& clientsJson); + void registerOrphanWindow(WindowCreationPayload create_window_payload); + + 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; + bool m_specialVisibleOnly = false; + bool m_moveToMonitor = false; + Json::Value m_persistentWorkspaceConfig; + + // Map for windows stored in workspaces not present in the current bar. + // This happens when the user has multiple monitors (hence, multiple bars) + // and doesn't share windows across 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; + int m_activeWorkspaceId; + 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; + IPC& m_ipc; }; } // 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/mpris/mpris.hpp b/include/modules/mpris/mpris.hpp index a0aee3b2..ad4dac1e 100644 --- a/include/modules/mpris/mpris.hpp +++ b/include/modules/mpris/mpris.hpp @@ -66,9 +66,9 @@ class Mpris : public ALabel { int album_len_; int title_len_; int dynamic_len_; - std::string dynamic_separator_; - std::vector dynamic_order_; std::vector dynamic_prio_; + std::vector dynamic_order_; + std::string dynamic_separator_; bool truncate_hours_; bool tooltip_len_limits_; std::string ellipsis_; diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 47701b4e..5fd0c180 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -16,6 +16,8 @@ #include "util/rfkill.hpp" #endif +enum ip_addr_pref : uint8_t { IPV4, IPV6, IPV4_6 }; + namespace waybar::modules { class Network : public ALabel { @@ -40,6 +42,7 @@ class Network : public ALabel { void parseEssid(struct nlattr**); void parseSignal(struct nlattr**); void parseFreq(struct nlattr**); + void parseBssid(struct nlattr**); bool associatedOrJoined(struct nlattr**); bool checkInterface(std::string name); auto getInfo() -> void; @@ -49,7 +52,7 @@ class Network : public ALabel { std::optional> readBandwidthUsage(); int ifid_; - sa_family_t family_; + ip_addr_pref addr_pref_; struct sockaddr_nl nladdr_ = {0}; struct nl_sock* sock_ = nullptr; struct nl_sock* ev_sock_ = nullptr; @@ -69,12 +72,16 @@ class Network : public ALabel { std::string state_; std::string essid_; + std::string bssid_; bool carrier_; std::string ifname_; std::string ipaddr_; + std::string ipaddr6_; std::string gwaddr_; std::string netmask_; + std::string netmask6_; int cidr_; + int cidr6_; int32_t signal_strength_dbm_; uint8_t signal_strength_; std::string signal_strength_app_; diff --git a/include/modules/niri/backend.hpp b/include/modules/niri/backend.hpp new file mode 100644 index 00000000..42b9ff7f --- /dev/null +++ b/include/modules/niri/backend.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +#include "util/json.hpp" + +namespace waybar::modules::niri { + +class EventHandler { + public: + virtual void onEvent(const Json::Value& ev) = 0; + virtual ~EventHandler() = default; +}; + +class IPC { + public: + IPC() { startIPC(); } + + void registerForIPC(const std::string& ev, EventHandler* ev_handler); + void unregisterForIPC(EventHandler* handler); + + static Json::Value send(const Json::Value& request); + + // The data members are only safe to access while dataMutex_ is locked. + std::lock_guard lockData() { return std::lock_guard(dataMutex_); } + const std::vector& workspaces() const { return workspaces_; } + const std::vector& windows() const { return windows_; } + const std::vector& keyboardLayoutNames() const { return keyboardLayoutNames_; } + unsigned keyboardLayoutCurrent() const { return keyboardLayoutCurrent_; } + + private: + void startIPC(); + static int connectToSocket(); + void parseIPC(const std::string&); + + std::mutex dataMutex_; + std::vector workspaces_; + std::vector windows_; + std::vector keyboardLayoutNames_; + unsigned keyboardLayoutCurrent_; + + util::JsonParser parser_; + std::mutex callbackMutex_; + std::list> callbacks_; +}; + +inline std::unique_ptr gIPC; + +}; // namespace waybar::modules::niri diff --git a/include/modules/niri/language.hpp b/include/modules/niri/language.hpp new file mode 100644 index 00000000..42b90ac4 --- /dev/null +++ b/include/modules/niri/language.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/niri/backend.hpp" + +namespace waybar::modules::niri { + +class Language : public ALabel, public EventHandler { + public: + Language(const std::string &, const Bar &, const Json::Value &); + ~Language() override; + void update() override; + + private: + void updateFromIPC(); + void onEvent(const Json::Value &ev) override; + void doUpdate(); + + struct Layout { + std::string full_name; + std::string short_name; + std::string variant; + std::string short_description; + }; + + static Layout getLayout(const std::string &fullName); + + std::mutex mutex_; + const Bar &bar_; + + std::vector layouts_; + unsigned current_idx_; +}; + +} // namespace waybar::modules::niri diff --git a/include/modules/niri/window.hpp b/include/modules/niri/window.hpp new file mode 100644 index 00000000..909ae6f0 --- /dev/null +++ b/include/modules/niri/window.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "modules/niri/backend.hpp" + +namespace waybar::modules::niri { + +class Window : public AAppIconLabel, public EventHandler { + public: + Window(const std::string &, const Bar &, const Json::Value &); + ~Window() override; + void update() override; + + private: + void onEvent(const Json::Value &ev) override; + void doUpdate(); + void setClass(const std::string &className, bool enable); + + const Bar &bar_; + + std::string oldAppId_; +}; + +} // namespace waybar::modules::niri diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp new file mode 100644 index 00000000..a6850ed1 --- /dev/null +++ b/include/modules/niri/workspaces.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/niri/backend.hpp" + +namespace waybar::modules::niri { + +class Workspaces : public AModule, public EventHandler { + public: + Workspaces(const std::string &, const Bar &, const Json::Value &); + ~Workspaces() override; + void update() override; + + private: + void onEvent(const Json::Value &ev) override; + void doUpdate(); + Gtk::Button &addButton(const Json::Value &ws); + std::string getIcon(const std::string &value, const Json::Value &ws); + + const Bar &bar_; + Gtk::Box box_; + // Map from niri workspace id to button. + std::unordered_map buttons_; +}; + +} // namespace waybar::modules::niri diff --git a/include/modules/power_profiles_daemon.hpp b/include/modules/power_profiles_daemon.hpp new file mode 100644 index 00000000..a2bd3858 --- /dev/null +++ b/include/modules/power_profiles_daemon.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "ALabel.hpp" +#include "giomm/dbusproxy.h" + +namespace waybar::modules { + +struct Profile { + std::string name; + std::string driver; + + Profile(std::string n, std::string d) : name(std::move(n)), driver(std::move(d)) {} +}; + +class PowerProfilesDaemon : public ALabel { + public: + PowerProfilesDaemon(const std::string &, const Json::Value &); + auto update() -> void override; + void profileChangedCb(const Gio::DBus::Proxy::MapChangedProperties &, + const std::vector &); + void busConnectedCb(Glib::RefPtr &r); + void getAllPropsCb(Glib::RefPtr &r); + void setPropCb(Glib::RefPtr &r); + void populateInitState(); + bool handleToggle(GdkEventButton *const &e) override; + + private: + // True if we're connected to the dbug interface. False if we're + // not. + bool connected_; + // Look for a profile name in the list of available profiles and + // switch activeProfile_ to it. + void switchToProfile(std::string const &); + // Used to toggle/display the profiles + std::vector availableProfiles_; + // Points to the active profile in the profiles list + std::vector::iterator activeProfile_; + // Current CSS class applied to the label + std::string currentStyle_; + // Format string + std::string tooltipFormat_; + // DBus Proxy used to track the current active profile + Glib::RefPtr powerProfilesProxy_; +}; + +} // namespace waybar::modules diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp new file mode 100644 index 00000000..cb6a34da --- /dev/null +++ b/include/modules/privacy/privacy.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#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 &, Gtk::Orientation, 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::set> ignore; + bool ignore_monitor = true; + + 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..f5f572c0 --- /dev/null +++ b/include/modules/privacy/privacy_item.hpp @@ -0,0 +1,48 @@ +#pragma once + +#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, Gtk::Orientation orientation, + 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/icon_manager.hpp b/include/modules/sni/icon_manager.hpp new file mode 100644 index 00000000..614d42d9 --- /dev/null +++ b/include/modules/sni/icon_manager.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include +#include + +class IconManager { + public: + static IconManager& instance() { + static IconManager instance; + return instance; + } + + void setIconsConfig(const Json::Value& icons_config) { + if (icons_config.isObject()) { + for (const auto& key : icons_config.getMemberNames()) { + std::string app_name = key; + const Json::Value& icon_value = icons_config[key]; + + if (icon_value.isString()) { + std::string icon_path = icon_value.asString(); + icons_map_[app_name] = icon_path; + } + } + } else { + spdlog::warn("Invalid icon config format."); + } + } + + std::string getIconForApp(const std::string& app_name) const { + auto it = icons_map_.find(app_name); + if (it != icons_map_.end()) { + return it->second; + } + return ""; + } + + private: + IconManager() = default; + std::unordered_map icons_map_; +}; diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 423ec7c5..c5e86d37 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -62,6 +62,7 @@ class Item : public sigc::trackable { void proxyReady(Glib::RefPtr& result); void setProperty(const Glib::ustring& name, Glib::VariantBase& value); void setStatus(const Glib::ustring& value); + void setCustomIcon(const std::string& id); void getUpdatedProperties(); void processUpdatedProperties(Glib::RefPtr& result); void onSignal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, @@ -76,6 +77,8 @@ class Item : public sigc::trackable { void makeMenu(); bool handleClick(GdkEventButton* const& /*ev*/); bool handleScroll(GdkEventScroll* const&); + bool handleMouseEnter(GdkEventCrossing* const&); + bool handleMouseLeave(GdkEventCrossing* const&); // smooth scrolling threshold gdouble scroll_threshold_ = 0; @@ -84,6 +87,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..91aa181d 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -21,7 +21,7 @@ class Language : public ALabel, public sigc::trackable { auto update() -> void override; private: - enum class DispayedShortFlag { None = 0, ShortName = 1, ShortDescription = 1 << 1 }; + enum class DisplayedShortFlag { None = 0, ShortName = 1, ShortDescription = 1 << 1 }; struct Layout { std::string full_name; @@ -56,8 +56,9 @@ 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); + std::byte displayed_short_flag = static_cast(DisplayedShortFlag::None); util::JsonParser parser_; std::mutex mutex_; diff --git a/include/modules/sway/window.hpp b/include/modules/sway/window.hpp index 22f5a59c..427c2e81 100644 --- a/include/modules/sway/window.hpp +++ b/include/modules/sway/window.hpp @@ -4,7 +4,7 @@ #include -#include "AIconLabel.hpp" +#include "AAppIconLabel.hpp" #include "bar.hpp" #include "client.hpp" #include "modules/sway/ipc/client.hpp" @@ -12,7 +12,7 @@ namespace waybar::modules::sway { -class Window : public AIconLabel, public sigc::trackable { +class Window : public AAppIconLabel, public sigc::trackable { public: Window(const std::string&, const waybar::Bar&, const Json::Value&); virtual ~Window() = default; @@ -25,8 +25,6 @@ class Window : public AIconLabel, public sigc::trackable { std::tuple getFocusedNode(const Json::Value& nodes, std::string& output); void getTree(); - void updateAppIconName(); - void updateAppIcon(); const Bar& bar_; std::string window_; @@ -37,9 +35,6 @@ class Window : public AIconLabel, public sigc::trackable { std::string old_app_id_; std::size_t app_nb_; std::string shell_; - unsigned app_icon_size_{24}; - bool update_app_icon_{true}; - std::string app_icon_name_; int floating_count_; util::JsonParser parser_; std::mutex mutex_; diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index d07edb49..d8a9e18a 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -12,13 +12,14 @@ #include "client.hpp" #include "modules/sway/ipc/client.hpp" #include "util/json.hpp" +#include "util/regex_collection.hpp" namespace waybar::modules::sway { class Workspaces : public AModule, public sigc::trackable { public: Workspaces(const std::string&, const waybar::Bar&, const Json::Value&); - virtual ~Workspaces() = default; + ~Workspaces() override = default; auto update() -> void override; private: @@ -27,22 +28,28 @@ 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&); - const std::string getCycleWorkspace(std::vector::iterator, bool prev) const; + std::string getCycleWorkspace(std::vector::iterator, bool prev) const; uint16_t getWorkspaceIndex(const std::string& name) const; - std::string trimWorkspaceName(std::string); - bool handleScroll(GdkEventScroll*) override; + static std::string trimWorkspaceName(std::string); + bool handleScroll(GdkEventScroll* /*unused*/) override; const Bar& bar_; std::vector workspaces_; + std::vector high_priority_named_; std::vector workspaces_order_; Gtk::Box box_; + std::string m_formatWindowSeparator; + 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..48b0074e --- /dev/null +++ b/include/modules/systemd_failed_units.hpp @@ -0,0 +1,34 @@ +#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; + std::string system_state, user_state, overall_state; + uint32_t nr_failed_system, nr_failed_user, nr_failed; + std::string last_status; + Glib::RefPtr system_proxy, user_proxy; + + void notify_cb(const Glib::ustring &sender_name, const Glib::ustring &signal_name, + const Glib::VariantContainerBase &arguments); + void RequestFailedUnits(); + void RequestSystemState(); + void updateData(); +}; + +} // namespace waybar::modules diff --git a/include/modules/temperature.hpp b/include/modules/temperature.hpp index 5440df77..918281be 100644 --- a/include/modules/temperature.hpp +++ b/include/modules/temperature.hpp @@ -18,6 +18,7 @@ class Temperature : public ALabel { private: float getTemperature(); bool isCritical(uint16_t); + bool isWarning(uint16_t); std::string file_path_; util::SleeperThread thread_; diff --git a/include/modules/upower.hpp b/include/modules/upower.hpp new file mode 100644 index 00000000..60a276db --- /dev/null +++ b/include/modules/upower.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include + +#include + +#include "AIconLabel.hpp" + +namespace waybar::modules { + +class UPower final : public AIconLabel { + public: + UPower(const std::string &, const Json::Value &); + virtual ~UPower(); + auto update() -> void override; + + private: + const std::string NO_BATTERY{"battery-missing-symbolic"}; + + // Config + bool showIcon_{true}; + bool hideIfEmpty_{true}; + int iconSize_{20}; + int tooltip_spacing_{4}; + int tooltip_padding_{4}; + Gtk::Box contentBox_; // tooltip box + std::string tooltipFormat_; + + // UPower device info + struct upDevice_output { + UpDevice *upDevice{NULL}; + double percentage{0.0}; + double temperature{0.0}; + guint64 time_full{0u}; + guint64 time_empty{0u}; + gchar *icon_name{(char *)'\0'}; + bool upDeviceValid{false}; + UpDeviceState state; + UpDeviceKind kind; + char *nativePath{(char *)'\0'}; + char *model{(char *)'\0'}; + }; + + // Technical variables + std::string nativePath_; + std::string model_; + std::string lastStatus_; + Glib::ustring label_markup_; + std::mutex mutex_; + Glib::RefPtr gtkTheme_; + bool sleeping_; + + // Technical functions + void addDevice(UpDevice *); + void removeDevice(const gchar *); + void removeDevices(); + void resetDevices(); + void setDisplayDevice(); + const Glib::ustring getText(const upDevice_output &upDevice_, const std::string &format); + bool queryTooltipCb(int, int, bool, const Glib::RefPtr &); + + // DBUS variables + guint watcherID_; + Glib::RefPtr conn_; + guint subscrID_{0u}; + + // UPower variables + UpClient *upClient_; + upDevice_output upDevice_; // Device to display + typedef std::unordered_map Devices; + Devices devices_; + bool upRunning_{true}; + + // DBus callbacks + void getConn_cb(Glib::RefPtr &result); + void onAppear(const Glib::RefPtr &, const Glib::ustring &, + const Glib::ustring &); + void onVanished(const Glib::RefPtr &, const Glib::ustring &); + void prepareForSleep_cb(const Glib::RefPtr &connection, + const Glib::ustring &sender_name, const Glib::ustring &object_path, + const Glib::ustring &interface_name, const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters); + + // UPower callbacks + static void deviceAdded_cb(UpClient *client, UpDevice *device, gpointer data); + static void deviceRemoved_cb(UpClient *client, const gchar *objectPath, gpointer data); + static void deviceNotify_cb(UpDevice *device, GParamSpec *pspec, gpointer user_data); + // UPower secondary functions + void getUpDeviceInfo(upDevice_output &upDevice_); +}; + +} // namespace waybar::modules diff --git a/include/modules/upower/upower.hpp b/include/modules/upower/upower.hpp deleted file mode 100644 index 446d1f53..00000000 --- a/include/modules/upower/upower.hpp +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include - -#include "ALabel.hpp" -#include "glibconfig.h" -#include "gtkmm/box.h" -#include "gtkmm/image.h" -#include "gtkmm/label.h" -#include "modules/upower/upower_tooltip.hpp" - -namespace waybar::modules::upower { - -class UPower : public AModule { - public: - UPower(const std::string &, const Json::Value &); - virtual ~UPower(); - auto update() -> void override; - - private: - typedef std::unordered_map Devices; - - const std::string DEFAULT_FORMAT = "{percentage}"; - const std::string DEFAULT_FORMAT_ALT = "{percentage} {time}"; - - static void deviceAdded_cb(UpClient *client, UpDevice *device, gpointer data); - static void deviceRemoved_cb(UpClient *client, const gchar *objectPath, gpointer data); - static void deviceNotify_cb(UpDevice *device, GParamSpec *pspec, gpointer user_data); - static void prepareForSleep_cb(GDBusConnection *system_bus, const gchar *sender_name, - const gchar *object_path, const gchar *interface_name, - const gchar *signal_name, GVariant *parameters, - gpointer user_data); - static void upowerAppear(GDBusConnection *conn, const gchar *name, const gchar *name_owner, - gpointer data); - static void upowerDisappear(GDBusConnection *connection, const gchar *name, gpointer user_data); - - void removeDevice(const gchar *objectPath); - void addDevice(UpDevice *device); - void setDisplayDevice(); - void resetDevices(); - void removeDevices(); - bool show_tooltip_callback(int, int, bool, const Glib::RefPtr &tooltip); - bool handleToggle(GdkEventButton *const &) override; - std::string timeToString(gint64 time); - - const std::string getDeviceStatus(UpDeviceState &state); - - Gtk::Box box_; - Gtk::Image icon_; - Gtk::Label label_; - - // Config - bool hideIfEmpty = true; - bool tooltip_enabled = true; - uint tooltip_spacing = 4; - uint tooltip_padding = 4; - uint iconSize = 20; - std::string format = DEFAULT_FORMAT; - std::string format_alt = DEFAULT_FORMAT_ALT; - - Devices devices; - std::mutex m_Mutex; - UpClient *client; - UpDevice *displayDevice; - guint login1_id; - GDBusConnection *login1_connection; - UPowerTooltip *upower_tooltip; - std::string lastStatus; - bool showAltText; - bool upowerRunning; - guint upowerWatcher_id; - std::string nativePath_; -}; - -} // namespace waybar::modules::upower diff --git a/include/modules/upower/upower_tooltip.hpp b/include/modules/upower/upower_tooltip.hpp deleted file mode 100644 index 05e9dcb3..00000000 --- a/include/modules/upower/upower_tooltip.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -#include - -#include "gtkmm/box.h" -#include "gtkmm/label.h" -#include "gtkmm/window.h" - -namespace waybar::modules::upower { - -class UPowerTooltip : public Gtk::Window { - private: - typedef std::unordered_map Devices; - - const std::string getDeviceIcon(UpDeviceKind& kind); - - Gtk::Box* contentBox; - - uint iconSize; - uint tooltipSpacing; - uint tooltipPadding; - - public: - UPowerTooltip(uint iconSize, uint tooltipSpacing, uint tooltipPadding); - virtual ~UPowerTooltip(); - - uint updateTooltip(Devices& devices); -}; - -} // namespace waybar::modules::upower diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index 9bbf4d46..e0033e8a 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -17,18 +17,23 @@ class Wireplumber : public ALabel { auto update() -> void override; private: - void loadRequiredApiModules(); - void prepare(); + void asyncLoadRequiredApiModules(); + void prepare(waybar::modules::Wireplumber* self); void activatePlugins(); static void updateVolume(waybar::modules::Wireplumber* self, uint32_t id); static void updateNodeName(waybar::modules::Wireplumber* self, uint32_t id); static void onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); + static void onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, + waybar::modules::Wireplumber* self); + static void onMixerApiLoaded(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); static void onObjectManagerInstalled(waybar::modules::Wireplumber* self); static void onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id); static void onDefaultNodesApiChanged(waybar::modules::Wireplumber* self); bool handleScroll(GdkEventScroll* e) override; + static std::list modules; + WpCore* wp_core_; GPtrArray* apis_; WpObjectManager* om_; @@ -41,6 +46,7 @@ class Wireplumber : public ALabel { double min_step_; uint32_t node_id_{0}; std::string node_name_; + gchar* type_; }; } // namespace waybar::modules diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 4465dd06..07110dde 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -24,6 +24,10 @@ namespace waybar::modules::wlr { +struct widget_geometry { + int x, y, w, h; +}; + class Taskbar; class Task { @@ -42,6 +46,7 @@ class Task { }; // made public so TaskBar can reorder based on configuration. Gtk::Button button; + struct widget_geometry minimize_hint; private: static uint32_t global_id; @@ -82,6 +87,8 @@ class Task { private: std::string repr() const; std::string state_string(bool = false) const; + void set_minimize_hint(); + void on_button_size_allocated(Gtk::Allocation &alloc); void set_app_info_from_app_id_list(const std::string &app_id_list); bool image_load_icon(Gtk::Image &image, const Glib::RefPtr &icon_theme, Glib::RefPtr app_info, int size); diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp new file mode 100644 index 00000000..3737ae26 --- /dev/null +++ b/include/util/audio_backend.hpp @@ -0,0 +1,98 @@ +#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_; + std::string default_sink_name; + bool default_sink_running_; + 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..eb42d3cc --- /dev/null +++ b/include/util/backlight_backend.hpp @@ -0,0 +1,87 @@ +#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, 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(const std::string &preferred_device, ChangeType change_type, double step); + + void set_scaled_brightness(const std::string &preferred_device, int brightness); + int get_scaled_brightness(const std::string &preferred_device); + + bool is_login_proxy_initialized() const { return static_cast(login_proxy_); } + + static const BacklightDevice *best_device(const std::vector &devices, + std::string_view); + + std::vector devices_; + std::mutex udev_thread_mutex_; + + private: + void set_brightness_internal(const 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 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..d8653faf 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 std::format; +#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; @@ -50,7 +64,7 @@ struct fmt::formatter> { } template - auto format(const date::zoned_time& ztime, FormatContext& ctx) { + auto format(const date::zoned_time& ztime, FormatContext& ctx) const { if (ctx.locale()) { const auto loc = ctx.locale().template get(); return fmt::format_to(ctx.out(), "{}", date::format(loc, fmt::to_string(specs), ztime)); @@ -58,3 +72,4 @@ struct fmt::formatter> { return fmt::format_to(ctx.out(), "{}", date::format(fmt::to_string(specs), ztime)); } }; +#endif 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..c8ed837a 100644 --- a/include/util/format.hpp +++ b/include/util/format.hpp @@ -6,7 +6,7 @@ class pow_format { public: pow_format(long long val, std::string&& unit, bool binary = false) - : val_(val), unit_(unit), binary_(binary){}; + : val_(val), unit_(unit), binary_(binary) {}; long long val_; std::string unit_; @@ -45,7 +45,7 @@ struct formatter { } template - auto format(const pow_format& s, FormatContext& ctx) -> decltype(ctx.out()) { + auto format(const pow_format& s, FormatContext& ctx) const -> decltype(ctx.out()) { const char* units[] = {"", "k", "M", "G", "T", "P", nullptr}; auto base = s.binary_ ? 1024ull : 1000ll; @@ -92,8 +92,8 @@ struct formatter { template <> struct formatter : formatter { template - auto format(const Glib::ustring& value, FormatContext& ctx) { - return formatter::format(value, ctx); + auto format(const Glib::ustring& value, FormatContext& ctx) const { + return formatter::format(static_cast(value), ctx); } }; } // namespace fmt diff --git a/include/util/gtk_icon.hpp b/include/util/gtk_icon.hpp index 44555f65..06f15abe 100644 --- a/include/util/gtk_icon.hpp +++ b/include/util/gtk_icon.hpp @@ -10,5 +10,7 @@ class DefaultGtkIconThemeWrapper { public: static bool has_icon(const std::string&); - static Glib::RefPtr load_icon(const char*, int, Gtk::IconLookupFlags); + static Glib::RefPtr load_icon( + const char*, int, Gtk::IconLookupFlags, + Glib::RefPtr style = Glib::RefPtr()); }; diff --git a/include/util/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..90fb2bb2 --- /dev/null +++ b/include/util/pipewire/pipewire_backend.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#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_; + + pw_registry* registry_; + spa_hook registryListener_; + + /* Hack to keep constructor inaccessible but still public. + * This is required to be able to use std::make_shared. + * 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 PrivateConstructorTag {}; + + public: + sigc::signal privacy_nodes_changed_signal_event; + + std::unordered_map privacy_nodes; + std::mutex mutex_; + + static std::shared_ptr getInstance(); + + // Handlers for PipeWire events + void handleRegistryEventGlobal(uint32_t id, uint32_t permissions, const char* type, + uint32_t version, const struct spa_dict* props); + void handleRegistryEventGlobalRemove(uint32_t id); + + PipewireBackend(PrivateConstructorTag tag); + ~PipewireBackend(); +}; +} // namespace waybar::util::PipewireBackend diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp new file mode 100644 index 00000000..54da7d16 --- /dev/null +++ b/include/util/pipewire/privacy_node_info.hpp @@ -0,0 +1,46 @@ +#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; + bool is_monitor = false; + + 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 getName(); + std::string getIconName(); + + // Handlers for PipeWire events + void handleProxyEventDestroy(); + void handleNodeEventInfo(const struct pw_node_info *info); +}; + +} // namespace waybar::util::PipewireBackend diff --git a/include/util/portal.hpp b/include/util/portal.hpp new file mode 100644 index 00000000..bff74b11 --- /dev/null +++ b/include/util/portal.hpp @@ -0,0 +1,36 @@ +#include + +#include + +#include "fmt/format.h" + +namespace waybar { + +enum class Appearance { + UNKNOWN = 0, + DARK = 1, + LIGHT = 2, +}; +class Portal : private Gio::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/prepare_for_sleep.h b/include/util/prepare_for_sleep.h index 68db8d8e..82f3b627 100644 --- a/include/util/prepare_for_sleep.h +++ b/include/util/prepare_for_sleep.h @@ -4,6 +4,6 @@ namespace waybar::util { -// Get a signal emited with value true when entering sleep, and false when exiting +// Get a signal emitted with value true when entering sleep, and false when exiting SafeSignal& prepare_for_sleep(); } // namespace waybar::util diff --git a/include/util/regex_collection.hpp b/include/util/regex_collection.hpp new file mode 100644 index 00000000..30d26d4a --- /dev/null +++ b/include/util/regex_collection.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#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(std::move(rule)), repr(std::move(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 = "", + const std::function& priority_function = default_priority_function); + ~RegexCollection() = default; + + std::string& get(std::string& value, bool& matched_any); + std::string& get(std::string& value); +}; + +} // namespace waybar::util 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 a724b1e8..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( @@ -95,6 +107,7 @@ class SleeperThread { } ~SleeperThread() { + connection_.disconnect(); stop(); if (thread_.joinable()) { thread_.join(); 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..c6f027a1 --- /dev/null +++ b/man/waybar-backlight-slider.5.scd @@ -0,0 +1,93 @@ +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. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# 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 ca3d922b..20810051 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -25,16 +25,20 @@ The *backlight* module displays the current backlight level. The maximum length in characters the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *states*: ++ typeof: object ++ @@ -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,15 +79,38 @@ 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. + +*min-brightness*: ++ + typeof: double ++ + default: 0.0 ++ + The minimum brightness of the backlight. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # EXAMPLE: ``` "backlight": { - "device": "intel_backlight", - "format": "{percent}% {icon}", - "format-icons": ["", ""] + "device": "intel_backlight", + "format": "{percent}% {icon}", + "format-icons": ["", ""] } ``` diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index fabde59a..a3c21a20 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -23,9 +23,9 @@ The *battery* module displays the current capacity and state (eg. charging) of y Define the max percentage of the battery, for when you've set the battery to stop charging at a lower level to save it. For example, if you've set the battery to stop at 80% that will become the new 100%. *design-capacity*: ++ - typeof: bool ++ - default: false ++ - Option to use the battery design capacity instead of it's current maximal capacity. + typeof: bool ++ + default: false ++ + Option to use the battery design capacity instead of its current maximal capacity. *interval*: ++ typeof: integer ++ @@ -56,16 +56,20 @@ The *battery* module displays the current capacity and state (eg. charging) of y The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *rotate*: ++ typeof: integer++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *on-click*: ++ typeof: string ++ @@ -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,29 @@ 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. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{capacity}*: Capacity in percentage @@ -110,6 +137,10 @@ The *battery* module displays the current capacity and state (eg. charging) of y *{time}*: Estimate of time until full or empty. Note that this is based on the power draw at the last refresh time, not an average. +*{cycles}*: Amount of charge cycles the highest-capacity battery has seen. *(Linux only)* + +*{health}*: The percentage of the highest-capacity battery's original maximum charge it can still hold. + # TIME FORMAT The *battery* module allows you to define how time should be formatted via *format-time*. @@ -121,7 +152,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 +163,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*. @@ -141,15 +172,15 @@ The *battery* module allows one to define custom formats based on up to two fact ``` "battery": { - "bat": "BAT2", - "interval": 60, - "states": { - "warning": 30, - "critical": 15 - }, - "format": "{capacity}% {icon}", - "format-icons": ["", "", "", "", ""], - "max-length": 25 + "bat": "BAT2", + "interval": 60, + "states": { + "warning": 30, + "critical": 15 + }, + "format": "{capacity}% {icon}", + "format-icons": ["", "", "", "", ""], + "max-length": 25 } ``` @@ -162,3 +193,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 6a5e71a7..fd7d5fb5 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. ++ @@ -50,19 +54,23 @@ Addressed by *bluetooth* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -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,10 +121,32 @@ 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. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{status}*: Status of the bluetooth device. @@ -138,7 +168,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 new file mode 100644 index 00000000..7825c38a --- /dev/null +++ b/man/waybar-cava.5.scd @@ -0,0 +1,207 @@ +waybar-cava(5) "waybar-cava" "User Manual" + +# NAME + +waybar - cava module + +# DESCRIPTION + +*cava* module for karlstav/cava project. See it on github: https://github.com/karlstav/cava. + + +# FILES + +$XDG_CONFIG_HOME/waybar/config ++ + Per user configuration file + +# ADDITIONAL FILES + +libcava lives in: + +. /usr/lib/libcava.so or /usr/lib64/libcava.so +. /usr/lib/pkgconfig/cava.pc or /usr/lib64/pkgconfig/cava.pc +. /usr/include/cava + +# CONFIGURATION + +[- *Option* +:- *Typeof* +:- *Default* +:- *Description* +|[ *cava_config* +:[ string +:[ +:< Path where cava configuration file is placed to +|[ *framerate* +:[ integer +:[ 30 +:[ Frames per second. Is used as a replacement for *interval* +|[ *autosens* +:[ integer +:[ 1 +:[ Will attempt to decrease sensitivity if the bars peak +|[ *sensitivity* +:[ integer +:[ 100 +:[ Manual sensitivity in %. It's recommended to be omitted when *autosens* = 1 +|[ *bars* +:[ integer +:[ 12 +:[ The number of bars +|[ *lower_cutoff_freq* +:[ long integer +:[ 50 +:[ Lower cutoff frequencies for lowest bars the bandwidth of the visualizer +|[ *higher_cutoff_freq* +:[ long integer +:[ 10000 +:[ Higher cutoff frequencies for highest bars the bandwidth of the visualizer +|[ *sleep_timer* +:[ 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) +|[ *format_silent* +:[ string +:[ +:[ Widget's text after sleep_timer elapsed (hide_on_silence has to be false) +|[ *method* +:[ string +:[ pulse +:[ Audio capturing method. Possible methods are: pipewire, pulse, alsa, fifo, sndio or shmem +|[ *source* +:[ string +:[ auto +:[ See cava configuration +|[ *sample_rate* +:[ long integer +:[ 44100 +:[ See cava configuration +|[ *sample_bits* +:[ integer +:[ 16 +:[ See cava configuration +|[ *stereo* +:[ bool +:[ true +:[ Visual channels +|[ *reverse* +:[ bool +:[ false +:[ Displays frequencies the other way around +|[ *bar_delimiter* +:[ integer +:[ 0 +:[ Each bar is separated by a delimiter. Use decimal value in ascii table(i.e. 59 = ";"). 0 means no delimiter +|[ *monstercat* +:[ bool +:[ false +:[ Disables or enables the so-called "Monstercat smoothing" with or without "waves" +|[ *waves* +:[ bool +:[ false +:[ 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 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'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 +:[ It's impossible to set it directly. The value is dictated by the number of icons in the array *format-icons* +|[ *data_format* +:[ string +:[ asci +:[ It's impossible to set it. Waybar sets it to = asci for internal needs +|[ *raw_target* +:[ string +:[ /dev/stdout +:[ It's impossible to set it. Waybar sets it to = /dev/stdout for internal needs +|[ *menu* +:[ string +:[ +:[ Action that popups the menu. +|[ *menu-file* +:[ string +:[ +:[ Location of the menu descriptor file. There need to be an element of type GtkMenu with id *menu* +|[ *menu-actions* +:[ array +:[ +:[ The actions corresponding to the buttons of the menu. + +Configuration can be provided as: +- The only cava configuration file which is provided through *cava_config*. The rest configuration can be skipped +- 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 and then overrides particular options by the provided list of configuration options + +# ACTIONS + +[- *String* +:- *Action* +|[ *mode* +:< Switch main cava thread and fetch audio source thread from/to pause/resume + +# DEPENDENCIES + +- iniparser +- fftw3 + +# SOLVING ISSUES + +. 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 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 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 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. +In case when cava releases new version and you're wanna get it, it should be raised an issue to libcava(https://github.com/LukashonakV/cava) with title ++ +\[Bump\]x.x.x where x.x.x is cava release version. + +# EXAMPLES + +``` +"cava": { + //"cava_config": "$XDG_CONFIG_HOME/cava/cava.conf", + "framerate": 30, + "autosens": 1, + //"sensitivity": 100, + "bars": 14, + "lower_cutoff_freq": 50, + "higher_cutoff_freq": 10000, + "method": "pulse", + "source": "auto", + "stereo": true, + "reverse": false, + "bar_delimiter": 0, + "monstercat": false, + "waves": false, + "noise_reduction": 0.77, + "input_delay": 2, + "format-icons" : ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█" ], + "actions": { + "on-click-right": "mode" + } +}, +``` +# STYLE + +- *#cava* +- *#cava.silent* Applied after no sound has been detected for sleep_timer seconds +- *#cava.updated* Applied when a new frame is shown diff --git a/man/waybar-cffi.5.scd b/man/waybar-cffi.5.scd new file mode 100644 index 00000000..0203ef77 --- /dev/null +++ b/man/waybar-cffi.5.scd @@ -0,0 +1,42 @@ +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. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +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 3c670566..50a5fc07 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -2,7 +2,7 @@ waybar-clock(5) "waybar-clock" "User Manual" # NAME -clock +waybar - clock module # DESCRIPTION @@ -11,7 +11,7 @@ clock # FILES $XDG_CONFIG_HOME/waybar/config ++ - Per user configuration file + Per user configuration file # CONFIGURATION @@ -37,13 +37,13 @@ $XDG_CONFIG_HOME/waybar/config ++ :[ list of strings :[ :[ A list of timezones (as in *timezone*) to use for time display, changed using - the scroll wheel. Do not specify *timezone* option when *timezones* is specified. + the scroll wheel. Do not specify *timezone* option when *timezones* is specified. "" represents the system's local timezone |[ *locale* :[ string :[ :[ A locale to be used to display the time. Intended to render times in custom - timezones with the proper language and format + timezones with the proper language and format |[ *max-length* :[ integer :[ @@ -51,7 +51,7 @@ $XDG_CONFIG_HOME/waybar/config ++ |[ *rotate* :[ integer :[ -:[ Positive value to rotate the text label +:[ Positive value to rotate the text label (in 90 degree increments) |[ *on-click* :[ string :[ @@ -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 :[ @@ -84,8 +84,24 @@ $XDG_CONFIG_HOME/waybar/config ++ :[ string :[ same as format :[ Tooltip on hover +|[ *menu* +:[ string +:[ +:[ Action that popups the menu. +|[ *menu-file* +:[ string +:[ +:[ Location of the menu descriptor file. There need to be an element of type GtkMenu with id *menu* +|[ *menu-actions* +:[ array +:[ +:[ The actions corresponding to the buttons of the menu. +|[ *expand*: +:[ bool +:[ false +:[ Enables this module to consume all left over space dynamically. -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* @@ -101,17 +117,17 @@ View all valid format options in *strftime(3)* or have a look {calendar}", - "calendar": { - "mode" : "year", - "mode-mon-col" : 3, - "weeks-pos" : "right", - "on-scroll" : 1, - "on-click-right": "mode", - "format": { - "months": "{}", - "days": "{}", - "weeks": "W{}", - "weekdays": "{}", - "today": "{}" - } - }, - "actions": { - "on-click-right": "mode", - "on-click-forward": "tz_up", - "on-click-backward": "tz_down", - "on-scroll-up": "shift_up", - "on-scroll-down": "shift_down" - } + "format": "{:%H:%M}  ", + "format-alt": "{:%A, %B %d, %Y (%R)}  ", + "tooltip-format": "{calendar}", + "calendar": { + "mode" : "year", + "mode-mon-col" : 3, + "weeks-pos" : "right", + "on-scroll" : 1, + "on-click-right": "mode", + "format": { + "months": "{}", + "days": "{}", + "weeks": "W{}", + "weekdays": "{}", + "today": "{}" + } + }, + "actions": { + "on-click-right": "mode", + "on-click-forward": "tz_up", + "on-click-backward": "tz_down", + "on-scroll-up": "shift_up", + "on-scroll-down": "shift_down" + } }, ``` @@ -205,10 +222,10 @@ View all valid format options in *strftime(3)* or have a look {calendar}", - "calendar": { - "mode" : "year", - "mode-mon-col" : 3, - "weeks-pos" : "right", - "on-scroll" : 1, - "on-click-right": "mode", - "format": { - "months": "{}", - "days": "{}", - "weeks": "W{}", - "weekdays": "{}", - "today": "{}" - } - }, - "actions": { - "on-click-right": "mode", - "on-click-forward": "tz_up", - "on-click-backward": "tz_down", - "on-scroll-up": "shift_up", - "on-scroll-down": "shift_down" - } - }, + "format": "{:%H:%M}  ", + "format-alt": "{:%A, %B %d, %Y (%R)}  ", + "tooltip-format": "\n{calendar}", + "calendar": { + "mode" : "year", + "mode-mon-col" : 3, + "weeks-pos" : "right", + "on-scroll" : 1, + "on-click-right": "mode", + "format": { + "months": "{}", + "days": "{}", + "weeks": "W{}", + "weekdays": "{}", + "today": "{}" + } + }, + "actions": { + "on-click-right": "mode", + "on-click-forward": "tz_up", + "on-click-backward": "tz_down", + "on-scroll-up": "shift_up", + "on-scroll-down": "shift_down" + } +}, ``` # AUTHOR diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index e3545536..287bf123 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -6,7 +6,7 @@ waybar - cpu module # DESCRIPTION -The *cpu* module displays the current cpu utilization. +The *cpu* module displays the current CPU utilization. # CONFIGURATION @@ -30,20 +30,24 @@ The *cpu* module displays the current cpu utilization. The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *states*: ++ typeof: object ++ - A number of cpu usage states which get activated on certain usage levels. See *waybar-states(5)*. + A number of CPU usage states which get activated on certain usage levels. See *waybar-states(5)*. *on-click*: ++ typeof: string ++ @@ -55,7 +59,7 @@ The *cpu* module displays the current cpu utilization. *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,23 +82,28 @@ The *cpu* module displays the current cpu utilization. default: true ++ Option to disable tooltip on hover. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS -*{load}*: Current cpu load. +*{load}*: Current CPU load. -*{usage}*: Current overall cpu usage. +*{usage}*: Current overall CPU usage. -*{usage*{n}*}*: Current cpu core n usage. Cores are numbered from zero, so first core will be {usage0} and 4th will be {usage3}. +*{usage*{n}*}*: Current CPU core n usage. Cores are numbered from zero, so first core will be {usage0} and 4th will be {usage3}. -*{avg_frequency}*: Current cpu average frequency (based on all cores) in GHz. +*{avg_frequency}*: Current CPU average frequency (based on all cores) in GHz. -*{max_frequency}*: Current cpu max frequency (based on the core with the highest frequency) in GHz. +*{max_frequency}*: Current CPU max frequency (based on the core with the highest frequency) in GHz. -*{min_frequency}*: Current cpu min frequency (based on the core with the lowest frequency) in GHz. +*{min_frequency}*: Current CPU min frequency (based on the core with the lowest frequency) in GHz. -*{icon}*: Icon for overall cpu usage. +*{icon}*: Icon for overall CPU usage. -*{icon*{n}*}*: Icon for cpu core n usage. Use like {icon0}. +*{icon*{n}*}*: Icon for CPU core n usage. Use like {icon0}. # EXAMPLES @@ -108,7 +117,7 @@ Basic configuration: } ``` -Cpu usage per core rendered as icons: +CPU usage per core rendered as icons: ``` "cpu": { diff --git a/man/waybar-custom.5.scd b/man/waybar-custom.5.scd index 3d30859d..309fc184 100644 --- a/man/waybar-custom.5.scd +++ b/man/waybar-custom.5.scd @@ -1,5 +1,4 @@ waybar-custom(5) - # NAME waybar - custom module @@ -19,14 +18,17 @@ Addressed by *custom/* *exec-if*: ++ typeof: string ++ - The path to a script, which determines if the script in *exec* should be executed. + The path to a script, which determines if the script in *exec* should be executed. ++ *exec* will be executed if the exit code of *exec-if* equals 0. +*hide-empty-text*: ++ + typeof: bool ++ + Disables the module when output is empty, but format might contain additional static content. + *exec-on-event*: ++ typeof: bool ++ default: true ++ - If an event command is set (e.g. *on-click* or *on-scroll-up*) then re-execute the script after - executing the event command. + If an event command is set (e.g. *on-click* or *on-scroll-up*) then re-execute the script after executing the event command. *return-type*: ++ typeof: string ++ @@ -34,26 +36,27 @@ Addressed by *custom/* *interval*: ++ 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. + 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* 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*. + The restart interval (in seconds). ++ + Can't be used with the *interval* option, so only with continuous scripts. ++ + Once the script exits, it'll be re-executed after the *restart-interval*. *signal*: ++ typeof: integer ++ - The signal number used to update the module. - The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. + The signal number used to update the module. ++ + 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 ++ - default: {} ++ - The format, how information should be displayed. On {} data gets inserted. + default: {text} ++ + The format, how information should be displayed. On {text} data gets inserted. *format-icons*: ++ typeof: array ++ @@ -61,19 +64,23 @@ Addressed by *custom/* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -85,7 +92,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 ++ @@ -108,11 +115,34 @@ 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 ++ Option to enable escaping of script output. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # RETURN-TYPE When *return-type* is set to *json*, Waybar expects the *exec*-script to output its data in JSON format. @@ -135,9 +165,9 @@ $text\\n$tooltip\\n$class* # FORMAT REPLACEMENTS -*{}*: Output of the script. +*{text}*: 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. @@ -147,7 +177,7 @@ $text\\n$tooltip\\n$class* ``` "custom/spotify": { - "format": " {}", + "format": " {text}", "max-length": 40, "interval": 30, // Remove this if your script is endless and write in loop "exec": "$HOME/.config/waybar/mediaplayer.sh 2> /dev/null", // Script in resources folder @@ -160,7 +190,7 @@ $text\\n$tooltip\\n$class* ``` "custom/mpd": { - "format": "♪ {}", + "format": "♪ {text}", //"max-length": 15, "interval": 10, "exec": "mpc current", @@ -174,7 +204,7 @@ $text\\n$tooltip\\n$class* ``` "custom/cmus": { - "format": "♪ {}", + "format": "♪ {text}", //"max-length": 15, "interval": 10, "exec": "cmus-remote -C \"format_print '%a - %t'\"", // artist - title @@ -189,7 +219,7 @@ $text\\n$tooltip\\n$class* ``` "custom/pacman": { - "format": "{} ", + "format": "{text} ", "interval": "once", "exec": "pacman_packages", "on-click": "update-system", @@ -201,7 +231,7 @@ $text\\n$tooltip\\n$class* ``` "custom/pacman": { - "format": "{} ", + "format": "{text} ", "interval": 3600, // every hour "exec": "checkupdates | wc -l", // # of updates "exec-if": "exit 0", // always run; consider advanced run conditions diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index 586c9dbd..00af2b90 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -29,23 +29,27 @@ Addressed by *disk* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *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 ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -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,48 @@ 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. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # 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 +140,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 06fb577f..a2146dfd 100644 --- a/man/waybar-dwl-tags.5.scd +++ b/man/waybar-dwl-tags.5.scd @@ -13,24 +13,29 @@ The *tags* module displays the current state of tags in dwl. Addressed by *dwl/tags* *num-tags*: ++ - typeof: uint ++ - default: 9 ++ - The number of tags that should be displayed. Max 32. + typeof: uint ++ + default: 9 ++ + The number of tags that should be displayed. Max 32. *tag-labels*: ++ - typeof: array ++ - The label to display for each tag. + typeof: array ++ + The label to display for each tag. *disable-click*: ++ - typeof: bool ++ - default: false ++ - If set to false, you can left click to set focused tag. Right click to toggle tag focus. If set to true this behaviour is disabled. + 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. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # EXAMPLE ``` "dwl/tags": { - "num-tags": 5 + "num-tags": 5 } ``` diff --git a/man/waybar-dwl-window.5.scd b/man/waybar-dwl-window.5.scd new file mode 100644 index 00000000..9ac33d94 --- /dev/null +++ b/man/waybar-dwl-window.5.scd @@ -0,0 +1,123 @@ +waybar-dwl-window(5) + +# NAME + +waybar - dwl window module + +# DESCRIPTION + +The *window* module displays the title of the currently focused window in DWL + +# CONFIGURATION + +Addressed by *dwl/window* + +*format*: ++ + typeof: string ++ + default: {title} ++ + The format, how information should be displayed. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label (in 90 degree increments). + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should accept. + +*align*: ++ + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*rewrite*: ++ + typeof: object ++ + Rules to rewrite the module format output. See *rewrite rules*. + +*icon*: ++ + typeof: bool ++ + default: false ++ + Option to hide the application icon. + +*icon-size*: ++ + typeof: integer ++ + default: 24 ++ + Option to change the size of the application icon. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +*{title}*: The title of the focused window. + +*{app_id}*: The app_id of the focused window. + +*{layout}*: The layout of the focused window. + +# REWRITE RULES + +*rewrite* is an object where keys are regular expressions and values are +rewrite rules if the expression matches. Rules may contain references to +captures of the expression. + +Regular expression and replacement follow ECMA-script rules. + +If no expression matches, the format output is left unchanged. + +Invalid expressions (e.g., mismatched parentheses) are skipped. + +# EXAMPLES + +``` +"dwl/window": { + "format": "{}", + "max-length": 50, + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } +} +``` diff --git a/man/waybar-gamemode.5.scd b/man/waybar-gamemode.5.scd index 257c9c91..a6ca9af0 100644 --- a/man/waybar-gamemode.5.scd +++ b/man/waybar-gamemode.5.scd @@ -61,15 +61,20 @@ Feral Gamemode optimizations. default: 4 ++ Defines the spacing between the icon and the text. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{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-language.5.scd b/man/waybar-hyprland-language.5.scd index 3e92def8..5a7ba941 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -18,12 +18,30 @@ Addressed by *hyprland/language* The format, how information should be displayed. *format-* ++ - typeof: string++ - Provide an alternative name to display per language where is the language of your choosing. Can be passed multiple times with multiple languages as shown by the example below. + typeof: string++ + Provide an alternative name to display per language where is the language of your choosing. Can be passed multiple times with multiple languages as shown by the example below. *keyboard-name*: ++ - typeof: string ++ - Specifies which keyboard to use from hyprctl devices output. Using the option that begins with "at-translated-set..." is recommended. + typeof: string ++ + Specifies which keyboard to use from hyprctl devices output. Using the option that begins with "at-translated-set..." is recommended. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # FORMAT REPLACEMENTS @@ -41,10 +59,10 @@ Addressed by *hyprland/language* ``` "hyprland/language": { - "format": "Lang: {long}" - "format-en": "AMERICA, HELL YEAH!" - "format-tr": "As bayrakları" - "keyboard-name": "at-translated-set-2-keyboard" + "format": "Lang: {long}", + "format-en": "AMERICA, HELL YEAH!", + "format-tr": "As bayrakları", + "keyboard-name": "at-translated-set-2-keyboard" } ``` diff --git a/man/waybar-hyprland-submap.5.scd b/man/waybar-hyprland-submap.5.scd index a00a2762..f6cdff94 100644 --- a/man/waybar-hyprland-submap.5.scd +++ b/man/waybar-hyprland-submap.5.scd @@ -19,19 +19,23 @@ Addressed by *hyprland/submap* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -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 ++ @@ -66,17 +70,46 @@ Addressed by *hyprland/submap* default: true ++ Option to disable tooltip on hover. +*always-on*: ++ + typeof: bool ++ + default: false ++ + Option to display the widget even when there's no active submap. + +*default-submap* ++ + typeof: string ++ + default: Default ++ + Option to set the submap name to display when not in an active submap. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` "hyprland/submap": { - "format": "✌️ {}", - "max-length": 8, - "tooltip": false + "format": "✌️ {}", + "max-length": 8, + "tooltip": false } ``` # STYLE - *#submap* +- *#submap.* diff --git a/man/waybar-hyprland-window.5.scd b/man/waybar-hyprland-window.5.scd index 0135d7c9..34ebf89b 100644 --- a/man/waybar-hyprland-window.5.scd +++ b/man/waybar-hyprland-window.5.scd @@ -14,13 +14,43 @@ Addressed by *hyprland/window* *format*: ++ typeof: string ++ - default: {} ++ + default: {title} ++ The format, how information should be displayed. On {} the current window title is displayed. *rewrite*: ++ typeof: object ++ Rules to rewrite window title. See *rewrite rules*. +*separate-outputs*: ++ + typeof: bool ++ + Show the active window of the monitor the bar belongs to, instead of the focused window. + +*icon*: ++ + typeof: bool ++ + default: false ++ + Option to hide the application icon. + +*icon-size*: ++ + typeof: integer ++ + default: 24 ++ + Option to change the size of the application icon. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS +See the output of "hyprctl clients" for examples + +*{title}*: The current title of the focused window. + +*{initialTitle}*: The initial title of the focused window. + +*{class}*: The current class of the focused window. + +*{initialClass}*: The initial class of the focused window. + # REWRITE RULES *rewrite* is an object where keys are regular expressions and values are @@ -37,14 +67,28 @@ Invalid expressions (e.g., mismatched parentheses) are skipped. ``` "hyprland/window": { - "format": "{}", - "rewrite": { - "(.*) - Mozilla Firefox": "🌎 $1", - "(.*) - zsh": "> [$1]" - } + "format": "{}", + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } } ``` # STYLE - *#window* +- *window#waybar.empty #window* When no windows are in the workspace + +The following classes are applied to the entire Waybar rather than just the +window widget: + +- *window#waybar.empty* When no windows are in the workspace +- *window#waybar.solo* When one tiled window is visible in the workspace + (floating windows may be present) +- *window#waybar.* Where ** is the *class* (e.g. *chromium*) of + the solo tiled window in the workspace (use *hyprctl clients* to see classes) +- *window#waybar.floating* When there are only floating windows in the workspace +- *window#waybar.fullscreen* When there is a fullscreen window in the workspace; + useful with Hyprland's *fullscreen, 1* mode +- *window#waybar.swallowing* When there is a swallowed window in the workspace diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 0678fb22..18c39898 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -1,4 +1,4 @@ -waybar-wlr-workspaces(5) +waybar-hyprland-workspaces(5) # NAME @@ -19,25 +19,93 @@ 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. + You may assign an empty value to a rule to have it ignored from generating any representation in workspaces. + +*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. + +*special-visible-only*: ++ + typeof: bool ++ + default: false ++ + If this and show-special are to true, special workspaces will be shown only if visible. + +*all-outputs*: ++ + typeof: bool ++ + default: false ++ + 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. + +*move-to-monitor*: ++ + typeof: bool ++ + default: false ++ + If set to true, open the workspace on the current monitor when clicking on a workspace button. + Otherwise, the workspace will open on the monitor where it was previously assigned. + Analog to using `focusworkspaceoncurrentmonitor` dispatcher instead of `workspace` in Hyprland. + +*ignore-workspaces*: ++ + typeof: array ++ + default: [] ++ + 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. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # FORMAT REPLACEMENTS *{id}*: id of workspace assigned by compositor +*{name}*: workspace name assigned by compositor + *{icon}*: Icon, as defined in *format-icons*. # ICONS Additional to workspace name matching, the following *format-icons* can be set. -- *default*: Will be shown, when no string match is found. +- *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": "", @@ -48,7 +116,54 @@ Additional to workspace name matching, the following *format-icons* can be set. "active": "", "default": "" }, - "sort-by-number": true + "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": "󰨞", + "title<.* - (.*) - VSCodium>": "codium $1" // captures part of the window title and formats it into output + } +} +``` + +``` +"hyprland/workspaces": { + // Formatting omitted for brevity + "ignore-workspaces": [ + "(special:)?chrome-sharing-indicator" + ] } ``` @@ -57,3 +172,9 @@ 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* +- *#workspaces button.hosting-monitor* (gets applied if workspace-monitor == waybar-monitor) diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index f85456d8..405c8fc5 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 @@ -21,19 +21,23 @@ screensaving, also known as "presentation mode". *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -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 ++ @@ -64,13 +68,13 @@ screensaving, also known as "presentation mode". Threshold to be used when scrolling. *start-activated*: ++ - typeof: bool ++ - default: *false* ++ - Whether the inhibit should be activated when starting waybar. + typeof: bool ++ + default: *false* ++ + Whether the inhibit should be activated when starting waybar. *timeout*: ++ typeof: double ++ - The number of minutes the inhibit should last. + The number of minutes the inhibition should last. *tooltip*: ++ typeof: bool ++ @@ -85,6 +89,24 @@ screensaving, also known as "presentation mode". typeof: string ++ This format is used when the inhibit is deactivated. +*menu*: ++ + typeof: string ++ + Action that popups the menu. Cannot be "on-click". + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{status}*: status (*activated* or *deactivated*) @@ -97,9 +119,15 @@ screensaving, also known as "presentation mode". "idle_inhibitor": { "format": "{icon}", "format-icons": { - "activated": "", - "deactivated": "" + "activated": "", + "deactivated": "" }, "timeout": 30.5 } ``` + +# STYLE + +- *#idle_inhibitor* +- *#idle_inhibitor.activated* +- *#idle_inhibitor.deactivated* diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index d47dba39..a2dcc938 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -13,24 +13,26 @@ The *image* module displays an image from a path. *path*: ++ typeof: string ++ The path to the image. + *exec*: ++ typeof: string ++ - The path to the script, which should return image path file - it will only execute if the path is not set + The path to the script, which should return image path file. ++ + It will only execute if the path is not set + *size*: ++ typeof: integer ++ The width/height to render the image. *interval*: ++ typeof: integer ++ - The interval (in seconds) to re-render the image. - This is useful if the contents of *path* changes. + The interval (in seconds) to re-render the image. ++ + This is useful if the contents of *path* changes. ++ If no *interval* is defined, the image will only be rendered once. *signal*: ++ typeof: integer ++ - The signal number used to update the module. - This can be used instead of *interval* if the file changes irregularly. + The signal number used to update the module. ++ + This can be used instead of *interval* if the file changes irregularly. ++ The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. *on-click*: ++ @@ -62,9 +64,14 @@ The *image* module displays an image from a path. default: true ++ Option to enable tooltip on hover. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # 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: ``` @@ -85,3 +92,4 @@ $path\\n$tooltip # STYLE - *#image* +- *#image.empty* diff --git a/man/waybar-inhibitor.5.scd b/man/waybar-inhibitor.5.scd index 10d41bd5..fce6f4f8 100644 --- a/man/waybar-inhibitor.5.scd +++ b/man/waybar-inhibitor.5.scd @@ -25,7 +25,7 @@ See *systemd-inhibit*(1) for more information. *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ @@ -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 ++ @@ -72,6 +76,24 @@ See *systemd-inhibit*(1) for more information. default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. Cannot be "on-click". + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{status}*: status (*activated* or *deactivated*) @@ -85,8 +107,8 @@ See *systemd-inhibit*(1) for more information. "what": "handle-lid-switch", "format": "{icon}", "format-icons": { - "activated": "", - "deactivated": "" + "activated": "", + "deactivated": "" } } ``` diff --git a/man/waybar-jack.5.scd b/man/waybar-jack.5.scd index b8a4cebe..85ce7180 100644 --- a/man/waybar-jack.5.scd +++ b/man/waybar-jack.5.scd @@ -13,73 +13,95 @@ The *jack* module displays the current state of the JACK server. Addressed by *jack* *format*: ++ - typeof: string ++ - default: *{load}%* ++ - The format, how information should be displayed. This format is used when other formats aren't specified. + typeof: string ++ + default: *{load}%* ++ + The format, how information should be displayed. This format is used when other formats aren't specified. *format-connected*: ++ - typeof: string ++ - This format is used when the module is connected to the JACK server. + typeof: string ++ + This format is used when the module is connected to the JACK server. *format-disconnected*: ++ - typeof: string ++ - This format is used when the module is not connected to the JACK server. + typeof: string ++ + This format is used when the module is not connected to the JACK server. *format-xrun*: ++ - typeof: string ++ - This format is used for one polling interval, when the JACK server reports an xrun. + typeof: string ++ + This format is used for one polling interval when the JACK server reports an xrun. *realtime*: ++ - typeof: bool ++ - default: *true* ++ - Option to drop real-time privileges for the JACK client opened by Waybar. + typeof: bool ++ + default: *true* ++ + Option to drop real-time privileges for the JACK client opened by Waybar. *tooltip*: ++ - typeof: bool ++ - default: *true* ++ - Option to disable tooltip on hover. + typeof: bool ++ + default: *true* ++ + Option to disable tooltip on hover. *tooltip-format*: ++ - typeof: string ++ - default: *{bufsize}/{samplerate} {latency}ms* ++ - The format of information displayed in the tooltip. + typeof: string ++ + default: *{bufsize}/{samplerate} {latency}ms* ++ + The format of information displayed in the tooltip. *interval*: ++ - typeof: integer ++ - default: 1 ++ - The interval in which the information gets polled. + typeof: integer ++ + default: 1 ++ + The interval in which the information gets polled. *rotate*: ++ - typeof: integer ++ - Positive value to rotate the text label. + typeof: integer ++ + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ - typeof: integer ++ - The maximum length in character the module should display. + typeof: integer ++ + The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ - typeof: string ++ - Command to execute when clicked on the module. + typeof: string ++ + Command to execute when clicked on the module. *on-click-middle*: ++ - typeof: string ++ - Command to execute when middle-clicked on the module using mousewheel. + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. *on-click-right*: ++ - typeof: string ++ - Command to execute when you right clicked on the module. + typeof: string ++ + Command to execute when you right-click on the module. *on-update*: ++ - typeof: string ++ - Command to execute when the module is updated. + typeof: string ++ + Command to execute when the module is updated. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # FORMAT REPLACEMENTS @@ -87,7 +109,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. @@ -97,10 +119,10 @@ Addressed by *jack* ``` "jack": { - "format": "DSP {}%", - "format-xrun": "{xruns} xruns", - "format-disconnected": "DSP off", - "realtime": true + "format": "DSP {}%", + "format-xrun": "{xruns} xruns", + "format-disconnected": "DSP off", + "realtime": true } ``` diff --git a/man/waybar-keyboard-state.5.scd b/man/waybar-keyboard-state.5.scd index fef46709..79498414 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,16 @@ 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. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{name}*: Caps, Num, or Scroll. @@ -65,13 +75,13 @@ The following *format-icons* can be set. ``` "keyboard-state": { - "numlock": true, - "capslock": true, - "format": "{name} {icon}", - "format-icons": { - "locked": "", - "unlocked": "" - } + "numlock": true, + "capslock": true, + "format": "{name} {icon}", + "format-icons": { + "locked": "", + "unlocked": "" + } } ``` diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index 34e342f3..567c2c72 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -29,7 +29,7 @@ Addressed by *memory* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *states*: ++ typeof: object ++ @@ -40,12 +40,16 @@ Addressed by *memory* The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -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 ++ @@ -80,6 +84,24 @@ Addressed by *memory* default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{percentage}*: Percentage of memory in use. @@ -98,6 +120,8 @@ Addressed by *memory* *{swapAvail}*: Amount of available swap in GiB. +*{swapState}*: Signals if swap is activated or not + # EXAMPLES ``` diff --git a/man/waybar-menu.5.scd b/man/waybar-menu.5.scd new file mode 100644 index 00000000..47e10432 --- /dev/null +++ b/man/waybar-menu.5.scd @@ -0,0 +1,153 @@ +waybar-menu(5) + +# NAME + +waybar - menu property + +# OVERVIEW + + +Some modules support a 'menu', which allows to have a popup menu when a defined +click is done over the module. + +# PROPERTIES + +A module that implements a 'menu' needs 3 properties defined in its config : + +*menu*: ++ + typeof: string ++ + Action that popups the menu. The possibles actions are : + +[- *Option* +:- *Description* +|[ *on-click* +:< When you left-click on the module +|[ *on-click-release* +:< When you release left button on the module +|[ *on-double-click* +:< When you double left click on the module +|[ *on-triple-click* +:< When you triple left click on the module +|[ *on-click-middle* +:< When you middle click on the module using mousewheel +|[ *on-click-middle-release* +:< When you release mousewheel button on the module +|[ *on-double-click-middle* +:< When you double middle click on the module +|[ *on-triple-click-middle* +:< When you triple middle click on the module +|[ *on-click-right* +:< When you right click on the module using +|[ *on-click-right-release* +:< When you release right button on the module +|[ *on-double-click-right* +:< When you double right click on the module +|[ *on-triple-click-right* +:< When you triple middle click on the module +|[ *on-click-backward* +:< When you click on the module using mouse backward button +|[ *on-click-backward-release* +:< When you release mouse backward button on the module +|[ *on-double-click-backward* +:< When you double click on the module using mouse backward button +|[ *on-triple-click-backward* +:< When you triple click on the module using mouse backawrd button +|[ *on-click-forward* +:< When you click on the module using mouse forward button +|[ *on-click-forward-release* +:< When you release mouse forward button on the module +|[ *on-double-click-forward* +:< When you double click on the module using mouse forward button +|[ *on-triple-click-forward* +:< When you triple click on the module using mouse forward button + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu*. + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. The identifiers of + each actions needs to exists as an id in the 'menu-file' for it to be linked + properly. + +# MENU-FILE + +The menu-file is an `.xml` file representing a GtkBuilder. Documentation for it +can be found here : https://docs.gtk.org/gtk4/class.Builder.html + +Here, it needs to have an element of type GtkMenu with id "menu". Eeach actions +in *menu-actions* are linked to elements in the *menu-file* file by the id of +the elements. + +# EXAMPLE + +Module config : +``` +"custom/power": { + "format" : "⏻ ", + "tooltip": false, + "menu": "on-click", + "menu-file": "~/.config/waybar/power_menu.xml", + "menu-actions": { + "shutdown": "shutdown", + "reboot": "reboot", + "suspend": "systemctl suspend", + "hibernate": "systemctl hibernate", + }, +}, +``` + +~/.config/waybar/power_menu.xml : +``` + + + + + + Suspend + + + + + Hibernate + + + + + Shutdown + + + + + + + + Reboot + + + + +``` + +# STYLING MENUS + +- *menu* + Style for the menu + +- *menuitem* + Style for items in the menu + +# EXAMPLE: + +``` +menu { + border-radius: 15px; + background: #161320; + color: #B5E8E0; +} +menuitem { + border-radius: 15px; +} +``` diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index 1dde8f79..84abc2e8 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -91,7 +91,7 @@ Addressed by *mpd* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ @@ -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 ++ @@ -158,6 +162,24 @@ Addressed by *mpd* default: {} ++ Icon to show depending on the "single" option (*{ "on": "...", "off": "..." }*) +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS ## WHEN PLAYING/PAUSED @@ -182,7 +204,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 ad5c1df5..9c192bd1 100644 --- a/man/waybar-mpris.5.scd +++ b/man/waybar-mpris.5.scd @@ -13,8 +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 ++ @@ -22,6 +21,7 @@ The *mpris* module displays currently playing media via libplayerctl. *interval*: ++ typeof: integer ++ + default: 0 ++ Refresh MPRIS information on a timer. *format*: ++ @@ -49,18 +49,15 @@ The *mpris* module displays currently playing media via libplayerctl. *artist-len*: ++ typeof: integer ++ - Maximum length of the Artist tag (Wide/Fullwidth Unicode characters - count as two). Set to zero to hide the artist in `{dynamic}` tag. + Maximum length of the Artist tag (Wide/Fullwidth Unicode characters count as two). Set to zero to hide the artist in `{dynamic}` tag. *album-len*: ++ typeof: integer ++ - Maximum length of the Album tag (Wide/Fullwidth Unicode characters count - as two). Set to zero to hide the album in `{dynamic}` tag. + Maximum length of the Album tag (Wide/Fullwidth Unicode characters count as two). Set to zero to hide the album in `{dynamic}` tag. *title-len*: ++ typeof: integer ++ - Maximum length of the Title tag (Wide/Fullwidth Unicode characters count - as two). Set to zero to hide the title in `{dynamic}` tag. + Maximum length of the Title tag (Wide/Fullwidth Unicode characters count as two). Set to zero to hide the title in `{dynamic}` tag. *dynamic-len*: ++ typeof: integer ++ @@ -101,18 +98,16 @@ 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 ++ default: "…" ++ - This character will be used when any of the tags exceed their maximum - length. If you don't want to use an ellipsis, set this to empty string. + This character will be used when any of the tags exceed their maximum length. If you don't want to use an ellipsis, set this to empty string. *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ @@ -120,36 +115,43 @@ 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. *player-icons*: ++ typeof: map[string]string ++ - Allows setting _{player-icon}_ based on player-name property. + Allows setting _{player_icon}_ based on player-name property. *status-icons*: ++ typeof: map[string]string ++ - Allows setting _{status-icon}_ based on player status (playing, paused, - stopped). + Allows setting _{status_icon}_ based on player status (playing, paused, stopped). + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # FORMAT REPLACEMENTS @@ -167,7 +169,7 @@ The *mpris* module displays currently playing media via libplayerctl. *{length}*: Length of the track, formatted as HH:MM:SS *{dynamic}*: Use _{artist}_, _{album}_, _{title}_ and _{length}_, automatically omit++ - empty values + empty values *{player_icon}*: Chooses an icon from _player-icons_ based on _{player}_ diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index a80737d4..3b63e3ee 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 ++ @@ -24,7 +24,7 @@ Addressed by *network* *family*: ++ typeof: string ++ default: *ipv4* ++ - The address family that is used for the format replacement {ipaddr} and to determine if a network connection is present. + The address family that is used for the format replacement {ipaddr} and to determine if a network connection is present. Set it to ipv4_6 to display both. *format*: ++ typeof: string ++ @@ -41,7 +41,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 ++ @@ -58,19 +58,23 @@ Addressed by *network* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -82,7 +86,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 ++ @@ -125,6 +129,24 @@ Addressed by *network* typeof: string ++ This format is used when the displayed interface is disabled. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{ifname}*: Name of the network interface. @@ -133,17 +155,23 @@ Addressed by *network* *{gwaddr}*: The default gateway for the interface -*{netmask}*: The subnetmask corresponding to the IP. +*{netmask}*: The subnetmask corresponding to the IP(V4). -*{cidr}*: The subnetmask corresponding to the IP in CIDR notation. +*{netmask6}*: The subnetmask corresponding to the IP(V6). + +*{cidr}*: The subnetmask corresponding to the IP(V4) in CIDR notation. + +*{cidr6}*: The subnetmask corresponding to the IP(V6) in CIDR notation. *{essid}*: Name (SSID) of the wireless network. +*{bssid}*: MAC address (BSSID) of the wireless access point. + *{signalStrength}*: Signal strength of the wireless network. *{signaldBm}*: Signal strength of the wireless network in dBm. -*{frequency}*: Frequency of the wireless network in MHz. +*{frequency}*: Frequency of the wireless network in GHz. *{bandwidthUpBits}*: Instant up speed in bits/seconds. diff --git a/man/waybar-niri-language.5.scd b/man/waybar-niri-language.5.scd new file mode 100644 index 00000000..44876fd9 --- /dev/null +++ b/man/waybar-niri-language.5.scd @@ -0,0 +1,63 @@ +waybar-niri-language(5) + +# NAME + +waybar - niri language module + +# DESCRIPTION + +The *language* module displays the currently selected language in niri. + +# CONFIGURATION + +Addressed by *niri/language* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format, how information should be displayed. + +*format-* ++ + typeof: string++ + Provide an alternative name to display per language where is the language of your choosing. Can be passed multiple times with multiple languages as shown by the example below. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +*{short}*: Short name of layout (e.g. "us"). Equals to {}. + +*{shortDescription}*: Short description of layout (e.g. "en"). + +*{long}*: Long name of layout (e.g. "English (Dvorak)"). + +*{variant}*: Variant of layout (e.g. "dvorak"). + +# EXAMPLES + +``` +"niri/language": { + "format": "Lang: {long}" + "format-en": "AMERICA, HELL YEAH!" + "format-tr": "As bayrakları" +} +``` + +# STYLE + +- *#language* diff --git a/man/waybar-niri-window.5.scd b/man/waybar-niri-window.5.scd new file mode 100644 index 00000000..8e886c29 --- /dev/null +++ b/man/waybar-niri-window.5.scd @@ -0,0 +1,86 @@ +waybar-niri-window(5) + +# NAME + +waybar - niri window module + +# DESCRIPTION + +The *window* module displays the title of the currently focused window in niri. + +# CONFIGURATION + +Addressed by *niri/window* + +*format*: ++ + typeof: string ++ + default: {title} ++ + The format, how information should be displayed. On {} the current window title is displayed. + +*rewrite*: ++ + typeof: object ++ + Rules to rewrite window title. See *rewrite rules*. + +*separate-outputs*: ++ + typeof: bool ++ + Show the active window of the monitor the bar belongs to, instead of the focused window. + +*icon*: ++ + typeof: bool ++ + default: false ++ + Option to hide the application icon. + +*icon-size*: ++ + typeof: integer ++ + default: 24 ++ + Option to change the size of the application icon. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +See the output of "niri msg windows" for examples + +*{title}*: The current title of the focused window. + +*{app_id}*: The current app ID of the focused window. + +# REWRITE RULES + +*rewrite* is an object where keys are regular expressions and values are +rewrite rules if the expression matches. Rules may contain references to +captures of the expression. + +Regular expression and replacement follow ECMA-script rules. + +If no expression matches, the title is left unchanged. + +Invalid expressions (e.g., mismatched parentheses) are skipped. + +# EXAMPLES + +``` +"niri/window": { + "format": "{}", + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } +} +``` + +# STYLE + +- *#window* +- *window#waybar.empty #window* When no windows are on the workspace + +The following classes are applied to the entire Waybar rather than just the +window widget: + +- *window#waybar.empty* When no windows are in the workspace +- *window#waybar.solo* When only one window is on the workspace +- *window#waybar.* Where *app-id* is the app ID of the only window on + the workspace diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd new file mode 100644 index 00000000..0c0249ca --- /dev/null +++ b/man/waybar-niri-workspaces.5.scd @@ -0,0 +1,102 @@ +waybar-niri-workspaces(5) + +# NAME + +waybar - niri workspaces module + +# DESCRIPTION + +The *workspaces* module displays the currently used workspaces in niri. + +# CONFIGURATION + +Addressed by *niri/workspaces* + +*all-outputs*: ++ + typeof: bool ++ + default: false ++ + If set to false, workspaces will only be shown on the output they are on. If set to true all workspaces will be shown on every output. + +*format*: ++ + typeof: string ++ + default: {value} ++ + The format, how information should be displayed. + +*format-icons*: ++ + typeof: array ++ + Based on the workspace name, index and state, the corresponding icon gets selected. See *icons*. + +*disable-click*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can click to change workspace. If set to true this behaviour is disabled. + +*disable-markup*: ++ + typeof: bool ++ + default: false ++ + If set to true, button label will escape pango markup. + +*current-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only the active or focused workspace will be shown. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +*{value}*: Name of the workspace, or index for unnamed workspaces, +as defined by niri. + +*{name}*: Name of the workspace for named workspaces. + +*{icon}*: Icon, as defined in *format-icons*. + +*{index}*: Index of the workspace on its output. + +*{output}*: Output where the workspace is located. + +# ICONS + +Additional to workspace name matching, the following *format-icons* can be set. + +- *default*: Will be shown, when no string matches are found. +- *focused*: Will be shown, when workspace is focused. +- *active*: Will be shown, when workspace is active on its output. + +# EXAMPLES + +``` +"niri/workspaces": { + "format": "{icon}", + "format-icons": { + // Named workspaces + // (you need to configure them in niri) + "browser": "", + "discord": "", + "chat": "", + + // Icons by state + "active": "", + "default": "" + } +} +``` + +# Style + +- *#workspaces button* +- *#workspaces button.focused*: The single focused workspace. +- *#workspaces button.active*: The workspace is active (visible) on its output. +- *#workspaces button.empty*: The workspace is empty. +- *#workspaces button.current_output*: The workspace is from the same output as + the bar that it is displayed on. +- *#workspaces button#niri-workspace-*: Workspaces named this, or index + for unnamed workspaces. diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd new file mode 100644 index 00000000..6488767b --- /dev/null +++ b/man/waybar-power-profiles-daemon.5.scd @@ -0,0 +1,79 @@ +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. +|[ *expand*: +:[ bool +:[ false +:[ Enables this module to consume all left over space dynamically. + + + + + +# 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..1c4ef59d --- /dev/null +++ b/man/waybar-privacy.5.scd @@ -0,0 +1,120 @@ +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. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +*ignore-monitor* ++ + typeof: bool ++ + default: true ++ + Ignore streams with *stream.monitor* property. + +*ignore* ++ + typeof: array of objects ++ + default: [] ++ + Additional streams to be ignored. See *IGNORE CONFIGURATION* for++ + more information. + +# MODULES CONFIGURATION + +*type*: ++ + 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. + +# IGNORE CONFIGURATION + +*type*: ++ + typeof: string + +*name*: ++ + typeof: string + +# 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 + } + ], + "ignore-monitor": true, + "ignore": [ + { + "type": "audio-in", + "name": "cava" + }, + { + "type": "screenshare", + "name": "obs" + } + ] +}, +``` + +# 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..cb274826 --- /dev/null +++ b/man/waybar-pulseaudio-slider.5.scd @@ -0,0 +1,88 @@ +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`. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# 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..d47fc744 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,11 +36,11 @@ 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 ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *states*: ++ typeof: object ++ @@ -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,25 @@ 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. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # FORMAT REPLACEMENTS @@ -138,6 +160,8 @@ If they are found in the current PulseAudio port name, the corresponding icons w - *hifi* - *phone* +Additionally, suffixing a device name or port with *-muted* will cause the icon +to be selected when the corresponding audio device is muted. This applies to *default* as well. # EXAMPLES @@ -148,10 +172,12 @@ If they are found in the current PulseAudio port name, the corresponding icons w "format-muted": "", "format-icons": { "alsa_output.pci-0000_00_1f.3.analog-stereo": "", + "alsa_output.pci-0000_00_1f.3.analog-stereo-muted": "", "headphones": "", "handsfree": "", "headset": "", "phone": "", + "phone-muted": "", "portable": "", "car": "", "default": ["", ""] diff --git a/man/waybar-river-layout.5.scd b/man/waybar-river-layout.5.scd index 5b18eee8..78e03634 100644 --- a/man/waybar-river-layout.5.scd +++ b/man/waybar-river-layout.5.scd @@ -15,45 +15,67 @@ It may not be set until a layout is first applied. Addressed by *river/layout* *format*: ++ - typeof: string ++ - default: {} ++ - The format, how information should be displayed. On {} data gets inserted. + typeof: string ++ + default: {} ++ + The format, how information should be displayed. On {} data gets inserted. *rotate*: ++ - typeof: integer ++ - Positive value to rotate the text label. + typeof: integer ++ + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ - typeof: integer ++ - The maximum length in character the module should display. + typeof: integer ++ + The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ - typeof: string ++ - Command to execute when clicked on the module. + typeof: string ++ + Command to execute when clicked on the module. *on-click-middle*: ++ - typeof: string ++ - Command to execute when middle-clicked on the module using mousewheel. + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. *on-click-right*: ++ - typeof: string ++ - Command to execute when you right clicked on the module. + typeof: string ++ + Command to execute when you right-click on the module. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # EXAMPLE ``` "river/layout": { - "format": "{}", - "min-length": 4, - "align": "right" + "format": "{}", + "min-length": 4, + "align": "right" } ``` diff --git a/man/waybar-river-mode.5.scd b/man/waybar-river-mode.5.scd index 4dec7f55..5837411d 100644 --- a/man/waybar-river-mode.5.scd +++ b/man/waybar-river-mode.5.scd @@ -13,59 +13,81 @@ The *mode* module displays the current mapping mode of river. Addressed by *river/mode* *format*: ++ - typeof: string ++ - default: {} ++ - The format, how information should be displayed. On {} data gets inserted. + typeof: string ++ + default: {} ++ + The format, how information should be displayed. On {} data gets inserted. *rotate*: ++ - typeof: integer ++ - Positive value to rotate the text label. + typeof: integer ++ + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ - typeof: integer ++ - The maximum length in character the module should display. + typeof: integer ++ + The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ - typeof: string ++ - Command to execute when clicked on the module. + typeof: string ++ + Command to execute when clicked on the module. *on-click-middle*: ++ - typeof: string ++ - Command to execute when middle-clicked on the module using mousewheel. + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. *on-click-right*: ++ - typeof: string ++ - Command to execute when you right clicked on the module. + typeof: string ++ + Command to execute when you right-click on the module. *on-update*: ++ - typeof: string ++ - Command to execute when the module is updated. + typeof: string ++ + Command to execute when the module is updated. *on-scroll-up*: ++ - typeof: string ++ - Command to execute when scrolling up on the module. + typeof: string ++ + Command to execute when scrolling up on the module. *on-scroll-down*: ++ - typeof: string ++ - Command to execute when scrolling down on the module. + typeof: string ++ + Command to execute when scrolling down on the module. *smooth-scrolling-threshold*: ++ - typeof: double ++ - Threshold to be used when scrolling. + typeof: double ++ + Threshold to be used when scrolling. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # EXAMPLES ``` "river/mode": { - "format": " {}" + "format": " {}" } ``` diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index 7814ee49..64621229 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -13,24 +13,34 @@ The *tags* module displays the current state of tags in river. Addressed by *river/tags* *num-tags*: ++ - typeof: uint ++ - default: 9 ++ - The number of tags that should be displayed. Max 32. + typeof: uint ++ + default: 9 ++ + The number of tags that should be displayed. Max 32. *tag-labels*: ++ - typeof: array ++ - The label to display for each tag. + typeof: array ++ + The label to display for each tag. *disable-click*: ++ - typeof: bool ++ - default: false ++ - If set to false, you can left click to set focused tag. Right click to toggle tag focus. If set to true this behaviour is disabled. + 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. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +*hide-vacant*: ++ + typeof: bool ++ + default: false ++ + Only show relevant tags: tags that are either focused or have a window on them. # EXAMPLE ``` "river/tags": { - "num-tags": 5 + "num-tags": 5 } ``` diff --git a/man/waybar-river-window.5.scd b/man/waybar-river-window.5.scd index d0497a0f..82eee0a5 100644 --- a/man/waybar-river-window.5.scd +++ b/man/waybar-river-window.5.scd @@ -19,19 +19,23 @@ Addressed by *river/window* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -43,17 +47,35 @@ 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. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. # EXAMPLES ``` "river/window": { - "format": "{}" + "format": "{}" } ``` # STYLE - *#window* -- *#window.focused* Applied when the output this module's bar belongs to is focused. \ No newline at end of file +- *#window.focused* Applied when the output this module's bar belongs to is focused. diff --git a/man/waybar-sndio.5.scd b/man/waybar-sndio.5.scd index 90a73f48..f14d35e9 100644 --- a/man/waybar-sndio.5.scd +++ b/man/waybar-sndio.5.scd @@ -20,24 +20,28 @@ cursor is over the module, and clicking on the module toggles mute. *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *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 ++ @@ -58,18 +62,36 @@ cursor is over the module, and clicking on the module toggles mute. *on-scroll-up*: ++ typeof: string ++ - Command to execute when scrolling up on the module. + Command to execute when scrolling up on the module. ++ This replaces the default behaviour of volume control. *on-scroll-down*: ++ typeof: string ++ - Command to execute when scrolling down on the module. + Command to execute when scrolling down on the module. ++ This replaces the default behaviour of volume control. *smooth-scrolling-threshold*: ++ typeof: double ++ Threshold to be used when scrolling. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{volume}*: Volume in percentage. @@ -80,8 +102,8 @@ cursor is over the module, and clicking on the module toggles mute. ``` "sndio": { - "format": "{raw_value} 🎜", - "scroll-step": 3 + "format": "{raw_value} 🎜", + "scroll-step": 3 } ``` diff --git a/man/waybar-states.5.scd b/man/waybar-states.5.scd index c1b78391..dea1beea 100644 --- a/man/waybar-states.5.scd +++ b/man/waybar-states.5.scd @@ -1,5 +1,9 @@ waybar-states(5) +# NAME + +waybar - states property + # OVERVIEW Some modules support 'states' which allows percentage values to be used as styling triggers to diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in new file mode 100644 index 00000000..b11e15bd --- /dev/null +++ b/man/waybar-styles.5.scd.in @@ -0,0 +1,78 @@ +waybar-styles(5) + +# NAME + +waybar-styles - using stylesheets for waybar + +# DESCRIPTION + +Waybar uses Cascading Style Sheets (CSS) to configure its appearance. + +It uses the first file found in this search order: + +- *$XDG_CONFIG_HOME/waybar/style.css* +- *~/.config/waybar/style.css* +- *~/waybar/style.css* +- */etc/xdg/waybar/style.css* +- *@sysconfdir@/xdg/waybar/style.css* + +# EXAMPLE + +An example user-controlled stylesheet that just changes the color of the clock to be green on black, while keeping the rest of the system config the same would be: + +``` +@import url("file:///etc/xdg/waybar/style.css") + +#clock { + background: #000000; + color: #00ff00; +} +``` + +## Hover-effect + +You can apply special styling to any module for when the cursor hovers it. + +``` +#clock:hover { + background-color: #ffffff; +} +``` + +## Setting cursor style + +Most, if not all, module types support setting the `cursor` option. This is +configured in your `config.jsonc`. If set to `false`, when hovering the module a +"pointer"(as commonly known from web CSS styling `cursor: pointer`) style cursor +will not be shown. Default behavior is to indicate an interaction event is +available. + +There are more cursor types to choose from by setting the `cursor` option to +a number, see Gdk3 official docs for all possible cursor types: +https://docs.gtk.org/gdk3/enum.CursorType.html. +However, note that not all cursor options listed may be available on +your system. If you attempt to use a cursor which is not available, the +application will crash. + +Example of disabling pointer(`Gdk::Hand2`) cursor type on a custom module: + +``` +"custom/my-custom-module": { + ... + "cursor": false, +} +``` + +Example of setting cursor type to `Gdk::Boat`(according to +https://docs.gtk.org/gdk3/enum.CursorType.html#boat): + +``` +"custom/my-custom-module": { + ... + "cursor": 8, +} +``` + +# SEE ALSO + +- *waybar(5)* diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd index 1c88314c..5710e69d 100644 --- a/man/waybar-sway-language.5.scd +++ b/man/waybar-sway-language.5.scd @@ -17,6 +17,11 @@ Addressed by *sway/language* default: {} ++ The format, how layout should be displayed. +*hide-single-layout*: ++ + typeof: bool ++ + default: false ++ + Defines visibility of the module if a single layout is configured + *tooltip-format*: ++ typeof: string ++ default: {} ++ @@ -27,6 +32,24 @@ Addressed by *sway/language* default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{short}*: Short name of layout (e.g. "us"). Equals to {}. @@ -43,11 +66,11 @@ Addressed by *sway/language* ``` "sway/language": { - "format": "{}", + "format": "{}", }, "sway/language": { - "format": "{short} {variant}", + "format": "{short} {variant}", } ``` diff --git a/man/waybar-sway-mode.5.scd b/man/waybar-sway-mode.5.scd index 29eed4f5..52827376 100644 --- a/man/waybar-sway-mode.5.scd +++ b/man/waybar-sway-mode.5.scd @@ -19,19 +19,23 @@ Addressed by *sway/mode* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -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 ++ @@ -66,12 +70,30 @@ Addressed by *sway/mode* default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` "sway/mode": { - "format": " {}", - "max-length": 50 + "format": " {}", + "max-length": 50 } ``` diff --git a/man/waybar-sway-scratchpad.5.scd b/man/waybar-sway-scratchpad.5.scd index 11fc32c0..e51ad12a 100644 --- a/man/waybar-sway-scratchpad.5.scd +++ b/man/waybar-sway-scratchpad.5.scd @@ -36,6 +36,24 @@ Addressed by *sway/scratchpad* default: {app}: {title} ++ The format, how information in the tooltip should be displayed. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{icon}*: Icon, as defined in *format-icons*. @@ -50,11 +68,11 @@ Addressed by *sway/scratchpad* ``` "sway/scratchpad": { - "format": "{icon} {count}", - "show-empty": false, - "format-icons": ["", ""], - "tooltip": true, - "tooltip-format": "{app}: {title}" + "format": "{icon} {count}", + "show-empty": false, + "format-icons": ["", ""], + "tooltip": true, + "tooltip-format": "{app}: {title}" } ``` diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index 0dd16295..a7eb4f05 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -19,19 +19,23 @@ Addressed by *sway/window* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in character the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ @@ -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,13 +82,13 @@ 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 ++ default: false ++ If the workspace itself is focused and the workspace contains nodes or floating_nodes, show the workspace name. If not set, text remains empty but styles according to nodes in the workspace are still applied. - + *rewrite*: ++ typeof: object ++ Rules to rewrite the module format output. See *rewrite rules*. @@ -99,6 +103,11 @@ Addressed by *sway/window* default: 24 ++ Option to change the size of the application icon. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{title}*: The title of the focused window. @@ -106,7 +115,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 @@ -124,18 +133,22 @@ Invalid expressions (e.g., mismatched parentheses) are skipped. ``` "sway/window": { - "format": "{}", - "max-length": 50, - "rewrite": { - "(.*) - Mozilla Firefox": "🌎 $1", - "(.*) - zsh": "> [$1]" - } + "format": "{}", + "max-length": 50, + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + } } ``` # STYLE - *#window* + +The following classes are applied to the entire Waybar rather than just the +window widget: + - *window#waybar.empty* When no windows are in the workspace, or screen is not focused and offscreen-css option is not set - *window#waybar.solo* When one tiled window is in the workspace - *window#waybar.floating* When there are only floating windows in the workspace diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 1e5f45db..1c4360e9 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -13,74 +13,98 @@ The *workspaces* module displays the currently used workspaces in Sway. Addressed by *sway/workspaces* *all-outputs*: ++ - typeof: bool ++ - default: false ++ - If set to false, workspaces will only be shown on the output they are on. If set to true all workspaces will be shown on every output. + typeof: bool ++ + default: false ++ + If set to false, workspaces will only be shown on the output they are on. If set to true all workspaces will be shown on every output. *format*: ++ - typeof: string ++ - default: {value} ++ - The format, how information should be displayed. + typeof: string ++ + default: {value} ++ + The format, how information should be displayed. *format-icons*: ++ - typeof: array ++ - Based on the workspace name and state, the corresponding icon gets selected. See *icons*. + typeof: array ++ + Based on the workspace name and state, the corresponding icon gets selected. See *icons*. *disable-scroll*: ++ - typeof: bool ++ - default: false ++ - If set to false, you can scroll to cycle through workspaces. If set to true this behaviour is disabled. + typeof: bool ++ + default: false ++ + If set to false, you can scroll to cycle through workspaces. If set to true this behaviour is disabled. *disable-click*: ++ - typeof: bool ++ - default: false ++ - If set to false, you can click to change workspace. If set to true this behaviour is disabled. + typeof: bool ++ + default: false ++ + If set to false, you can click to change workspace. If set to true this behaviour is disabled. *smooth-scrolling-threshold*: ++ - typeof: double ++ - Threshold to be used when scrolling. + typeof: double ++ + Threshold to be used when scrolling. *disable-scroll-wraparound*: ++ - typeof: bool ++ - default: false ++ - If set to false, scrolling on the workspace indicator will wrap around to the first workspace when reading the end, and vice versa. If set to true this behavior is disabled. + typeof: bool ++ + default: false ++ + If set to false, scrolling on the workspace indicator will wrap around to the first workspace when reading the end, and vice versa. If set to true this behavior is disabled. *enable-bar-scroll*: ++ - typeof: bool ++ - default: false ++ - If set to false, you can't scroll to cycle throughout workspaces from the entire bar. If set to true this behaviour is enabled. + typeof: bool ++ + default: false ++ + If set to false, you can't scroll to cycle throughout workspaces from the entire bar. If set to true this behaviour is enabled. *disable-markup*: ++ - typeof: bool ++ - default: false ++ - If set to true, button label will escape pango markup. + typeof: bool ++ + default: false ++ + If set to true, button label will escape pango markup. *current-only*: ++ - typeof: bool ++ - default: false ++ - If set to true. Only focused workspaces will be shown. + typeof: bool ++ + default: false ++ + If set to true. Only focused workspaces will be shown. -*persistent_workspaces*: ++ - typeof: json (see below) ++ - default: empty ++ - Lists workspaces that should always be shown, even when non existent +*persistent-workspaces*: ++ + typeof: json (see below) ++ + default: empty ++ + Lists workspaces that should always be shown, even when non-existent *on-update*: ++ - typeof: string ++ - Command to execute when the module is updated. + typeof: string ++ + Command to execute when the module is updated. *disable-auto-back-and-forth*: ++ - typeof: bool ++ - Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration. + typeof: bool ++ + Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration. *alphabetical_sort*: ++ - typeof: bool ++ - Whether to sort workspaces alphabetically. Please note this can make "swaymsg workspace prev/next" move to workspaces inconsistent with the ordering shown in Waybar. + typeof: bool ++ + Whether to sort workspaces alphabetically. Please note this can make "swaymsg workspace prev/next" move to workspaces inconsistent with the ordering shown in Waybar. warp-on-scroll: ++ - typeof: bool ++ - default: true ++ - If set to false, you can scroll to cycle through workspaces without mouse warping being enabled. If set to true this behaviour is disabled. + typeof: bool ++ + default: true ++ + If set to false, you can scroll to cycle through workspaces without mouse warping being enabled. If set to true this behaviour is disabled. + +*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. + You may assign an empty value to a rule to have it ignored from generating any representation in workspaces. + For Wayland windows `class` is matched against the `app_id`, and for X11 windows against the `class` property. + +*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. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS @@ -94,14 +118,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. +- *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 @@ -111,11 +138,11 @@ 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 + } } ``` @@ -125,19 +152,34 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge ``` "sway/workspaces": { - "disable-scroll": true, - "all-outputs": true, - "format": "{name}: {icon}", - "format-icons": { - "1": "", - "2": "", - "3": "", - "4": "", - "5": "", - "urgent": "", - "focused": "", - "default": "" - } + "disable-scroll": true, + "all-outputs": true, + "format": "{name}: {icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "high-priority-named": [ "1", "2" ], + "urgent": "", + "focused": "", + "default": "" + } +} +``` + +``` +"sway/workspaces": { + "format": "{name} {windows}", + "format-window-separator": " | ", + "window-rewrite-default": "{name}", + "window-format": "{name}", + "window-rewrite": { + "class": "", + "class": "k", + "title<.* - (.*) - VSCodium>": "codium $1" // captures part of the window title and formats it into output + } } ``` @@ -148,5 +190,6 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge - *#workspaces button.focused* - *#workspaces button.urgent* - *#workspaces button.persistent* +- *#workspaces button.empty* - *#workspaces button.current_output* - *#workspaces button#sway-workspace-${name}* diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd new file mode 100644 index 00000000..8d7c980a --- /dev/null +++ b/man/waybar-systemd-failed-units.5.scd @@ -0,0 +1,87 @@ +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. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# 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. + +*{systemd_state}:* State of the systemd system session + +*{user_state}:* State of the systemd user session + +*{overall_state}:* Overall state of the systemd and user session. ("Ok" or "Degraded") + +# EXAMPLES + +``` +"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 cc689dc0..923d643d 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -25,11 +25,16 @@ Addressed by *temperature* *hwmon-path-abs*: ++ typeof: string ++ The path of the hwmon-directory of the device, e.g. */sys/devices/pci0000:00/0000:00:18.3/hwmon*. (Note that the subdirectory *hwmon/hwmon#*, where *#* is a number is not part of the path!) Has to be used together with *input-filename*. + This can also be an array of strings, for which, it just works like *hwmon-path*. *input-filename*: ++ typeof: string ++ The temperature filename of your *hwmon-path-abs*, e.g. *temp1_input* +*warning-threshold*: ++ + typeof: integer ++ + The threshold before it is considered warning (Celsius). + *critical-threshold*: ++ typeof: integer ++ The threshold before it is considered critical (Celsius). @@ -39,6 +44,10 @@ Addressed by *temperature* default: 10 ++ The interval in which the information gets polled. +*format-warning*: ++ + typeof: string ++ + The format to use when temperature is considered warning + *format-critical*: ++ typeof: string ++ The format to use when temperature is considered critical @@ -59,23 +68,27 @@ Addressed by *temperature* *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *max-length*: ++ typeof: integer ++ The maximum length in characters the module should display. *min-length*: ++ - typeof: integer ++ - The minimum length in characters the module should take up. + typeof: integer ++ + The minimum length in characters the module should 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. + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. *on-click*: ++ typeof: string ++ - Command to execute when you clicked on the module. + Command to execute when you click on the module. *on-click-middle*: ++ typeof: string ++ @@ -83,7 +96,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 ++ @@ -106,6 +119,24 @@ Addressed by *temperature* default: true ++ Option to disable tooltip on hover. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # FORMAT REPLACEMENTS *{temperatureC}*: Temperature in Celsius. diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index bfd1f296..dec5347f 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -6,43 +6,52 @@ waybar - tray module # DESCRIPTION -_WARNING_ *tray* is still in beta. There may me bugs. Breaking changes may occur. +_WARNING_ *tray* is still in beta. There may be bugs. Breaking changes may occur. # CONFIGURATION Addressed by *tray* *icon-size*: ++ - typeof: integer ++ - Defines the size of the tray icons. + typeof: integer ++ + Defines the size of the tray icons. *show-passive-items*: ++ - typeof: bool ++ - default: false ++ - Defines visibility of the tray icons with *Passive* status. + typeof: bool ++ + default: false ++ + Defines visibility of the tray icons with *Passive* status. *smooth-scrolling-threshold*: ++ typeof: double ++ Threshold to be used when scrolling. *spacing*: ++ - typeof: integer ++ - Defines the spacing between the tray icons. + typeof: integer ++ + Defines the spacing between the tray icons. *reverse-direction*: ++ - typeof: bool ++ - Defines if new app icons should be added in a reverse order + typeof: bool ++ + Defines if new app icons should be added in a reverse order *on-update*: ++ typeof: string ++ Command to execute when the module is updated. +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + # EXAMPLES ``` "tray": { - "icon-size": 21, - "spacing": 10 + "icon-size": 21, + "spacing": 10, + "icons": { + "blueman": "bluetooth", + "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png" + } } ``` diff --git a/man/waybar-upower.5.scd b/man/waybar-upower.5.scd index fc37b665..303ee65e 100644 --- a/man/waybar-upower.5.scd +++ b/man/waybar-upower.5.scd @@ -12,10 +12,16 @@ compatible devices in the tooltip. # CONFIGURATION *native-path*: ++ - typeof: string ++ - default: ++ - The battery to monitor. Refer to the https://upower.freedesktop.org/docs/UpDevice.html#UpDevice--native-path ++ - Can be obtained using `upower --dump` + typeof: string ++ + default: ++ + The battery to monitor. Refer to the https://upower.freedesktop.org/docs/UpDevice.html#UpDevice--native-path ++ + Can be obtained using `upower --dump` + +*model*: ++ + typeof: string ++ + default: ++ + The battery to monitor, based on the model. (this option is ignored if *native-path* is given). ++ + Can be obtained using `upower --dump` *icon-size*: ++ typeof: integer ++ @@ -57,6 +63,24 @@ 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. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{percentage}*: The battery capacity in percentage @@ -93,6 +117,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 473df926..ae78f184 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -19,6 +19,11 @@ The *wireplumber* module displays the current volume reported by WirePlumber. typeof: string ++ This format is used when the sound is muted. +*node-type*: ++ + typeof: string ++ + default: *Audio/Sink* ++ + The WirePlumber node type to attach to. Use *Audio/Source* to manage microphones etc. + *tooltip*: ++ typeof: bool ++ default: *true* ++ @@ -31,7 +36,7 @@ The *wireplumber* module displays the current volume reported by WirePlumber. *rotate*: ++ typeof: integer ++ - Positive value to rotate the text label. + Positive value to rotate the text label (in 90 degree increments). *states*: ++ typeof: object ++ @@ -43,16 +48,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 +73,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 ++ @@ -83,6 +92,19 @@ The *wireplumber* module displays the current volume reported by WirePlumber. default: 100 ++ The maximum volume that can be set, in percentage. +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + # FORMAT REPLACEMENTS *{volume}*: Volume in percentage. @@ -91,11 +113,33 @@ The *wireplumber* module displays the current volume reported by WirePlumber. # EXAMPLES +## Basic: + ``` "wireplumber": { - "format": "{volume}%", - "format-muted": "", - "on-click": "helvum" + "format": "{volume}%", + "format-muted": "", + "on-click": "helvum" +} +``` + +## Separate Sink and Source Widgets + +``` +"wireplumber#sink": { + "format": "{volume}% {icon}", + "format-muted": "", + "format-icons": ["", "", ""], + "on-click": "helvum", + "on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", + "scroll-step": 5 +}, +"wireplumber#source": { + "node-type": "Audio/Source", + "format": "{volume}% ", + "format-muted": "", + "on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle", + "scroll-step": 5 } ``` diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index 5626eaec..af1ba97f 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -2,7 +2,7 @@ waybar-wlr-taskbar(5) # NAME -wlroots - Taskbar module +waybar - wlr taskbar module # DESCRIPTION @@ -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. -*{title}*: 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 4a256f02..62d3f636 100644 --- a/man/waybar-wlr-workspaces.5.scd +++ b/man/waybar-wlr-workspaces.5.scd @@ -29,19 +29,18 @@ Addressed by *wlr/workspaces* *sort-by-coordinates*: ++ typeof: bool ++ default: true ++ - Should workspaces be sorted by coordinates. - Note that if both *sort-by-name* and *sort-by-coordinates* are true sort by name will be first. - If both are false - sort by id will be performed. + Should workspaces be sorted by coordinates. ++ + Note that if both *sort-by-name* and *sort-by-coordinates* are true sort-by name will be first. If both are false - sort by id will be performed. *sort-by-number*: ++ 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 ++ @@ -62,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 0b3dc748..6ca0aa99 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -6,21 +6,38 @@ 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. + +The visual display elements for waybar use a CSS stylesheet, see *waybar-styles(5)* for details. # BAR CONFIGURATION +*expand-center* ++ + typeof: bool ++ + default: false ++ + Enables the modules-center to consume all left over space dynamically. + +*expand-left* ++ + typeof: bool ++ + default: false ++ + Enables the modules-left to consume all left over space dynamically. + +*expand-right* ++ + typeof: bool ++ + default: false ++ + Enables the modules-left to consume all left over space dynamically. + *layer* ++ typeof: string ++ default: bottom ++ @@ -30,7 +47,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* ++ @@ -66,9 +83,14 @@ Also a minimal example configuration can be found on the at the bottom of this m typeof: integer ++ Margins value without units. +*no-center* ++ + typeof: bool ++ + default: false ++ + Option to disable the center modules fully useful together with expand-\*. + *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 +111,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 +130,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 +146,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 +163,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)*: @@ -180,6 +201,19 @@ A minimal *config* file could look like this: } ``` +# SIGNALS + +Waybar accepts the following signals: + +*SIGUSR1* + Toggles the bar visibility (hides if shown, shows if hidden) +*SIGUSR2* + Reloads (resets) the bar +*SIGINT* + Quits the bar + +For example, to toggle the bar programmatically, you can invoke `killall -SIGUSR1 waybar`. + # MULTI OUTPUT CONFIGURATION ## Limit a configuration to some outputs @@ -236,11 +270,22 @@ 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. + +## Swapping icon and label + +If a module displays both a label and an icon, it might be desirable to swap them (for instance, for panels on the left or right of the screen, or for user adopting a right-to-left script). This can be achieved with the "swap-icon-label" property, taking a boolean. Example: +``` +{ + "sway/window": { + "swap-icon-label": true + } +} +``` ## Grouping modules -Module groups allow stacking modules in 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,38 +308,97 @@ 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. + +*click-to-reveal*: ++ + typeof: bool ++ + default: false ++ + Whether left click should reveal the content rather than mouse over. Note that grouped modules may still process their own on-click events. + +*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)* - *waybar-battery(5)* - *waybar-bluetooth(5)* +- *waybar-cava(5)* - *waybar-clock(5)* - *waybar-cpu(5)* - *waybar-custom(5)* - *waybar-disk(5)* +- *waybar-dwl-tags(5)* +- *waybar-dwl-window(5)* +- *waybar-gamemode(5)* +- *waybar-hyprland-language(5)* +- *waybar-hyprland-submap(5)* +- *waybar-hyprland-window(5)* +- *waybar-hyprland-workspaces(5)* +- *waybar-niri-language(5)* +- *waybar-niri-window(5)* +- *waybar-niri-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 *sway-output(5)* +*waybar-styles(5)" diff --git a/meson.build b/meson.build index aa250b7f..7f9854d5 100644 --- a/meson.build +++ b/meson.build @@ -1,10 +1,10 @@ project( 'waybar', 'cpp', 'c', - version: '0.9.18', + version: '0.12.0', license: 'MIT', - meson_version: '>= 0.50.0', + meson_version: '>= 0.59.0', default_options : [ - 'cpp_std=c++17', + 'cpp_std=c++20', 'buildtype=release', 'default_library=static' ], @@ -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 @@ -80,16 +69,13 @@ is_openbsd = host_machine.system() == 'openbsd' thread_dep = dependency('threads') fmt = dependency('fmt', version : ['>=8.1.1'], fallback : ['fmt', 'fmt_dep']) -spdlog = dependency('spdlog', version : ['>=1.10.0'], fallback : ['spdlog', 'spdlog_dep'], default_options : ['external_fmt=enabled']) +spdlog = dependency('spdlog', version : ['>=1.10.0'], fallback : ['spdlog', 'spdlog_dep'], default_options : ['external_fmt=enabled', 'std_format=disabled', 'tests=disabled']) wayland_client = dependency('wayland-client') 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')) @@ -105,7 +92,7 @@ libevdev = dependency('libevdev', required: get_option('libevdev')) libmpdclient = dependency('libmpdclient', required: get_option('mpd')) xkbregistry = dependency('xkbregistry') libjack = dependency('jack', required: get_option('jack')) -libwireplumber = dependency('wireplumber-0.4', required: get_option('wireplumber')) +libwireplumber = dependency('wireplumber-0.5', required: get_option('wireplumber')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() @@ -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.9.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, @@ -159,22 +161,39 @@ src_files = files( 'src/AModule.cpp', 'src/ALabel.cpp', 'src/AIconLabel.cpp', + 'src/AAppIconLabel.cpp', 'src/modules/custom.cpp', '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-menu.5.scd', + 'man/waybar-temperature.5.scd', ) inc_dirs = ['include'] @@ -182,103 +201,207 @@ 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') + src_files += files('src/modules/dwl/window.cpp') + man_files += files('man/waybar-dwl-tags.5.scd') + man_files += files('man/waybar-dwl-window.5.scd') endif if true 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/workspace.cpp', + 'src/modules/hyprland/workspaces.cpp', + 'src/modules/hyprland/windowcreationpayload.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 get_option('niri') + add_project_arguments('-DHAVE_NIRI', language: 'cpp') + src_files += files( + 'src/modules/niri/backend.cpp', + 'src/modules/niri/language.cpp', + 'src/modules/niri/window.cpp', + 'src/modules/niri/workspaces.cpp', + ) + man_files += files( + 'man/waybar-niri-language.5.scd', + 'man/waybar-niri-window.5.scd', + 'man/waybar-niri-workspaces.5.scd', + ) +endif + +if get_option('login-proxy') + add_project_arguments('-DHAVE_LOGIN_PROXY', language: 'cpp') 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.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/pipewire_backend.cpp', + 'src/util/pipewire/privacy_node_info.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() @@ -289,38 +412,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 @@ -332,34 +463,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.4', + version : '>=0.10.4', required: get_option('cava'), fallback : ['cava', 'cava_dep'], not_found_message: 'cava is not found. Building waybar without cava') if cava.found() add_project_arguments('-DHAVE_LIBCAVA', language: 'cpp') - src_files += 'src/modules/cava.cpp' + 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, @@ -376,6 +521,7 @@ executable( libnl, libnlgen, upower_glib, + pipewire, playerctl, libpulse, libjack, @@ -396,73 +542,34 @@ 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)) + man_files += configure_file( + input: 'man/waybar-styles.5.scd.in', + output: 'waybar-styles.5.scd', + configuration: { + 'sysconfdir': prefix / sysconfdir + } + ) + fs = import('fs') mandir = get_option('mandir') - man_files = [ - main_manpage_path, - 'waybar-backlight.5.scd', - 'waybar-battery.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] @@ -470,12 +577,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) ) @@ -484,7 +590,7 @@ endif catch2 = dependency( 'catch2', - version: '>=2.0.0', + default_options: [ 'tests=false' ], fallback: ['catch2', 'catch2_dep'], required: get_option('tests'), ) @@ -500,7 +606,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..d83fe01f 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') @@ -19,3 +19,5 @@ option('experimental', type : 'boolean', value : false, description: 'Enable exp option('jack', type: 'feature', value: 'auto', description: 'Enable support for JACK') option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') option('cava', type: 'feature', value: 'auto', description: 'Enable support for Cava') +option('niri', type: 'boolean', description: 'Enable support for niri') +option('login-proxy', type: 'boolean', description: 'Enable interfacing with dbus login interface') diff --git a/nix/default.nix b/nix/default.nix index fc77225d..4cfd75c0 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,20 +1,43 @@ -{ lib -, waybar -, version +{ + lib, + pkgs, + waybar, + version, }: - -waybar.overrideAttrs (prev: { +let + libcava = rec { + version = "0.10.4"; + src = pkgs.fetchFromGitHub { + owner = "LukashonakV"; + repo = "cava"; + tag = version; + hash = "sha256-9eTDqM+O1tA/3bEfd1apm8LbEcR9CVgELTIspSVPMKM="; + }; + }; +in +(waybar.overrideAttrs (oldAttrs: { inherit version; - # version = "0.9.17"; src = lib.cleanSourceWith { - filter = name: type: - let - baseName = baseNameOf (toString name); - in - ! ( - lib.hasSuffix ".nix" baseName - ); + filter = name: type: type != "regular" || !lib.hasSuffix ".nix" name; src = lib.cleanSource ../.; }; -}) + + mesonFlags = lib.remove "-Dgtk-layer-shell=enabled" oldAttrs.mesonFlags; + + # downstream patch should not affect upstream + patches = [ ]; + # nixpkgs checks version, no need when building locally + nativeInstallCheckInputs = [ ]; + + buildInputs = (builtins.filter (p: p.pname != "wireplumber") oldAttrs.buildInputs) ++ [ + pkgs.wireplumber + ]; + + postUnpack = '' + pushd "$sourceRoot" + cp -R --no-preserve=mode,ownership ${libcava.src} subprojects/cava-${libcava.version} + patchShebangs . + popd + ''; +})) 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 77% rename from resources/config rename to resources/config.jsonc index daad8ab1..67d5ff5b 100644 --- a/resources/config +++ b/resources/config.jsonc @@ -1,3 +1,4 @@ +// -*- mode: jsonc -*- { // "layer": "top", // Waybar at top layer // "position": "bottom", // Waybar position (top|bottom|left|right) @@ -5,9 +6,33 @@ // "width": 1280, // Waybar width "spacing": 4, // Gaps between modules (4px) // Choose the order of the modules - "modules-left": ["sway/workspaces", "sway/mode", "sway/scratchpad", "custom/media"], - "modules-center": ["sway/window"], - "modules-right": ["mpd", "idle_inhibitor", "pulseaudio", "network", "cpu", "memory", "temperature", "backlight", "keyboard-state", "sway/language", "battery", "battery#bat2", "clock", "tray"], + "modules-left": [ + "sway/workspaces", + "sway/mode", + "sway/scratchpad", + "custom/media" + ], + "modules-center": [ + "sway/window" + ], + "modules-right": [ + "mpd", + "idle_inhibitor", + "pulseaudio", + "network", + "power-profiles-daemon", + "cpu", + "memory", + "temperature", + "backlight", + "keyboard-state", + "sway/language", + "battery", + "battery#bat2", + "clock", + "tray", + "custom/power" + ], // Modules configuration // "sway/workspaces": { // "disable-scroll": true, @@ -49,7 +74,7 @@ "format-disconnected": "Disconnected ", "format-stopped": "{consumeIcon}{randomIcon}{repeatIcon}{singleIcon}Stopped ", "unknown-tag": "N/A", - "interval": 2, + "interval": 5, "consume-icons": { "on": " " }, @@ -79,7 +104,11 @@ }, "tray": { // "icon-size": 21, - "spacing": 10 + "spacing": 10, + // "icons": { + // "blueman": "bluetooth", + // "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png" + // } }, "clock": { // "timezone": "America/New_York", @@ -113,6 +142,7 @@ "critical": 15 }, "format": "{capacity}% {icon}", + "format-full": "{capacity}% {icon}", "format-charging": "{capacity}% ", "format-plugged": "{capacity}% ", "format-alt": "{time} {icon}", @@ -123,6 +153,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}%) ", @@ -152,7 +193,7 @@ "on-click": "pavucontrol" }, "custom/media": { - "format": "{icon} {}", + "format": "{icon} {text}", "return-type": "json", "max-length": 40, "format-icons": { @@ -162,6 +203,17 @@ "escape": true, "exec": "$HOME/.config/waybar/mediaplayer.py 2> /dev/null" // Script in resources folder // "exec": "$HOME/.config/waybar/mediaplayer.py --player spotify 2> /dev/null" // Filter player based on name + }, + "custom/power": { + "format" : "⏻ ", + "tooltip": false, + "menu": "on-click", + "menu-file": "$HOME/.config/waybar/power_menu.xml", // Menu file in resources folder + "menu-actions": { + "shutdown": "shutdown", + "reboot": "reboot", + "suspend": "systemctl suspend", + "hibernate": "systemctl hibernate" + } } } - diff --git a/resources/custom_modules/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..7618de58 --- /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 = 2; + +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); +} 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..c1a82f59 --- /dev/null +++ b/resources/custom_modules/cffi_example/waybar_cffi_module.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Waybar ABI version. 2 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 + /// + /// In ABI version 1, this may be either a bare string if the value is a + /// string, or the JSON representation of any other JSON object as a string. + /// + /// From ABI version 2 onwards, this is always the JSON representation of the + /// value as a string. + 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 1630d97c..524d4d2a 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -1,89 +1,169 @@ #!/usr/bin/env python3 +import gi +gi.require_version("Playerctl", "2.0") +from gi.repository import Playerctl, GLib +from gi.repository.Playerctl import Player import argparse import logging import sys import signal import gi import json -gi.require_version('Playerctl', '2.0') -from gi.repository import Playerctl, GLib +import os +from typing import List logger = logging.getLogger(__name__) - -def write_output(text, player): - logger.info('Writing output') - - output = {'text': text, - 'class': 'custom-' + player.props.player_name, - 'alt': player.props.player_name} - - sys.stdout.write(json.dumps(output) + '\n') - sys.stdout.flush() - - -def on_play(player, status, manager): - logger.info('Received new playback status') - on_metadata(player, player.props.metadata, manager) - - -def on_metadata(player, metadata, manager): - logger.info('Received new metadata') - track_info = '' - - if player.props.player_name == 'spotify' and \ - 'mpris:trackid' in metadata.keys() and \ - ':ad:' in player.props.metadata['mpris:trackid']: - track_info = 'AD PLAYING' - elif player.get_artist() != '' and player.get_title() != '': - track_info = '{artist} - {title}'.format(artist=player.get_artist(), - title=player.get_title()) - else: - track_info = player.get_title() - - if player.props.status != 'Playing' and track_info: - track_info = ' ' + track_info - write_output(track_info, player) - - -def on_player_appeared(manager, player, selected_player=None): - if player is not None and (selected_player is None or player.name == selected_player): - init_player(manager, player) - else: - logger.debug("New player appeared, but it's not the selected player, skipping") - - -def on_player_vanished(manager, player): - logger.info('Player has vanished') - sys.stdout.write('\n') - sys.stdout.flush() - - -def init_player(manager, name): - logger.debug('Initialize player: {player}'.format(player=name.name)) - player = Playerctl.Player.new_from_name(name) - player.connect('playback-status', on_play, manager) - player.connect('metadata', on_metadata, manager) - manager.manage_player(player) - on_metadata(player, player.props.metadata, manager) - - def signal_handler(sig, frame): - logger.debug('Received signal to stop, exiting') - sys.stdout.write('\n') + logger.info("Received signal to stop, exiting") + sys.stdout.write("\n") sys.stdout.flush() # loop.quit() sys.exit(0) +class PlayerManager: + def __init__(self, selected_player=None, excluded_player=[]): + self.manager = Playerctl.PlayerManager() + self.loop = GLib.MainLoop() + self.manager.connect( + "name-appeared", lambda *args: self.on_player_appeared(*args)) + self.manager.connect( + "player-vanished", lambda *args: self.on_player_vanished(*args)) + + signal.signal(signal.SIGINT, signal_handler) + 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 + self.init_player(player) + + def run(self): + logger.info("Starting main loop") + self.loop.run() + + def init_player(self, player): + logger.info(f"Initialize new player: {player.name}") + player = Playerctl.Player.new_from_name(player) + player.connect("playback-status", + self.on_playback_status_changed, None) + player.connect("metadata", self.on_metadata_changed, None) + self.manager.manage_player(player) + self.on_metadata_changed(player, player.props.metadata) + + def get_players(self) -> List[Player]: + return self.manager.props.players + + def write_output(self, text, player): + logger.debug(f"Writing output: {text}") + + output = {"text": text, + "class": "custom-" + player.props.player_name, + "alt": player.props.player_name} + + sys.stdout.write(json.dumps(output) + "\n") + sys.stdout.flush() + + def clear_output(self): + sys.stdout.write("\n") + sys.stdout.flush() + + def on_playback_status_changed(self, player, status, _=None): + logger.debug(f"Playback status changed for player {player.props.player_name}: {status}") + self.on_metadata_changed(player, player.props.metadata) + + def get_first_playing_player(self): + players = self.get_players() + logger.debug(f"Getting first playing player from {len(players)} players") + if len(players) > 0: + # if any are playing, show the first one that is playing + # reverse order, so that the most recently added ones are preferred + for player in players[::-1]: + if player.props.status == "Playing": + return player + # if none are playing, show the first one + return players[0] + else: + logger.debug("No players found") + return None + + def show_most_important_player(self): + logger.debug("Showing most important player") + # show the currently playing player + # or else show the first paused player + # or else show nothing + current_player = self.get_first_playing_player() + if current_player is not None: + self.on_metadata_changed(current_player, current_player.props.metadata) + else: + self.clear_output() + + def on_metadata_changed(self, player, metadata, _=None): + logger.debug(f"Metadata changed for player {player.props.player_name}") + player_name = player.props.player_name + artist = player.get_artist() + artist = artist.replace("&", "&") + title = player.get_title() + title = title.replace("&", "&") + + track_info = "" + if player_name == "spotify" and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]: + track_info = "Advertisement" + elif artist is not None and title is not None: + track_info = f"{artist} - {title}" + else: + track_info = title + + if track_info: + if player.props.status == "Playing": + track_info = " " + track_info + else: + track_info = " " + track_info + # only print output if no other player is playing + current_playing = self.get_first_playing_player() + if current_playing is None or current_playing.props.player_name == player.props.player_name: + self.write_output(track_info, player) + else: + logger.debug(f"Other player {current_playing.props.player_name} is playing, skipping") + + def on_player_appeared(self, _, player): + logger.info(f"Player has appeared: {player.name}") + if player.name in self.excluded_player: + logger.debug( + "New player appeared, but it's in exclude player list, skipping") + return + if player is not None and (self.selected_player is None or player.name == self.selected_player): + self.init_player(player) + else: + logger.debug( + "New player appeared, but it's not the selected player, skipping") + + def on_player_vanished(self, _, player): + logger.info(f"Player {player.props.player_name} has vanished") + self.show_most_important_player() + def parse_arguments(): parser = argparse.ArgumentParser() # Increase verbosity with every occurrence of -v - parser.add_argument('-v', '--verbose', action='count', default=0) + parser.add_argument("-v", "--verbose", action="count", default=0) - # Define for which player we're listening - parser.add_argument('--player') + parser.add_argument("-x", "--exclude", "- Comma-separated list of excluded player") + + # Define for which player we"re listening + parser.add_argument("--player") + + parser.add_argument("--enable-logging", action="store_true") return parser.parse_args() @@ -92,37 +172,25 @@ def main(): arguments = parse_arguments() # Initialize logging - logging.basicConfig(stream=sys.stderr, level=logging.DEBUG, - format='%(name)s %(levelname)s %(message)s') + if arguments.enable_logging: + logfile = os.path.join(os.path.dirname( + os.path.realpath(__file__)), "media-player.log") + logging.basicConfig(filename=logfile, level=logging.DEBUG, + format="%(asctime)s %(name)s %(levelname)s:%(lineno)d %(message)s") # Logging is set by default to WARN and higher. # With every occurrence of -v it's lowered by one logger.setLevel(max((3 - arguments.verbose) * 10, 0)) - # Log the sent command line arguments - logger.debug('Arguments received {}'.format(vars(arguments))) + logger.info("Creating player manager") + if arguments.player: + logger.info(f"Filtering for player: {arguments.player}") + if arguments.exclude: + logger.info(f"Exclude player {arguments.exclude}") - manager = Playerctl.PlayerManager() - loop = GLib.MainLoop() - - manager.connect('name-appeared', lambda *args: on_player_appeared(*args, arguments.player)) - manager.connect('player-vanished', on_player_vanished) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - - for player in manager.props.player_names: - if arguments.player is not None and arguments.player != player.name: - logger.debug('{player} is not the filtered player, skipping it' - .format(player=player.name) - ) - continue - - init_player(manager, player) - - loop.run() + player = PlayerManager(arguments.player, arguments.exclude) + player.run() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/resources/custom_modules/power_menu.xml b/resources/custom_modules/power_menu.xml new file mode 100644 index 00000000..aa2a42ca --- /dev/null +++ b/resources/custom_modules/power_menu.xml @@ -0,0 +1,28 @@ + + + + + + Suspend + + + + + Hibernate + + + + + Shutdown + + + + + + + + Reboot + + + + 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..7e830285 100644 --- a/resources/style.css +++ b/resources/style.css @@ -48,6 +48,11 @@ button:hover { box-shadow: inset 0 -3px #ffffff; } +/* you can set a style on hover for any module like this */ +#pulseaudio:hover { + background-color: #a37800; +} + #workspaces button { padding: 0 5px; background-color: transparent; @@ -69,7 +74,7 @@ button:hover { #mode { background-color: #64727D; - border-bottom: 3px solid #ffffff; + box-shadow: inset 0 -3px #ffffff; } #clock, @@ -87,6 +92,7 @@ button:hover { #mode, #idle_inhibitor, #scratchpad, +#power-profiles-daemon, #mpd { padding: 0 10px; color: #ffffff; @@ -128,16 +134,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 +304,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/resources/waybar.service.in b/resources/waybar.service.in index 81ac6779..18bac54c 100644 --- a/resources/waybar.service.in +++ b/resources/waybar.service.in @@ -1,5 +1,5 @@ [Unit] -Description=Highly customizable Wayland bar for Sway and Wlroots based compositors. +Description=Highly customizable Wayland bar for Sway and Wlroots based compositors Documentation=https://github.com/Alexays/Waybar/wiki/ PartOf=graphical-session.target After=graphical-session.target diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp new file mode 100644 index 00000000..3f47eff1 --- /dev/null +++ b/src/AAppIconLabel.cpp @@ -0,0 +1,178 @@ +#include "AAppIconLabel.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include "util/gtk_icon.hpp" + +namespace waybar { + +AAppIconLabel::AAppIconLabel(const Json::Value& config, const std::string& name, + const std::string& id, const std::string& format, uint16_t interval, + bool ellipsize, bool enable_click, bool enable_scroll) + : AIconLabel(config, name, id, format, interval, ellipsize, enable_click, enable_scroll) { + // Icon size + if (config["icon-size"].isUInt()) { + app_icon_size_ = config["icon-size"].asUInt(); + } + 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_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_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; + } + } + } + return {}; +} + +std::optional getIconName(const std::string& app_identifier, + const std::string& alternative_app_identifier) { + const auto desktop_file_path = getDesktopFilePath(app_identifier, alternative_app_identifier); + if (!desktop_file_path.has_value()) { + // Try some heuristics to find a matching icon + + if (DefaultGtkIconThemeWrapper::has_icon(app_identifier)) { + return app_identifier; + } + + auto app_identifier_desktop = app_identifier + "-desktop"; + if (DefaultGtkIconThemeWrapper::has_icon(app_identifier_desktop)) { + return app_identifier_desktop; + } + + auto first_space = app_identifier.find_first_of(' '); + if (first_space != std::string::npos) { + auto first_word = toLowerCase(app_identifier.substr(0, first_space)); + if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { + return first_word; + } + } + + const auto first_dash = app_identifier.find_first_of('-'); + if (first_dash != std::string::npos) { + auto first_word = toLowerCase(app_identifier.substr(0, first_dash)); + if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { + return first_word; + } + } + + return {}; + } + + try { + Glib::KeyFile desktop_file; + desktop_file.load_from_file(desktop_file_path.value()); + return desktop_file.get_string("Desktop Entry", "Icon"); + } catch (Glib::FileError& error) { + spdlog::warn("Error while loading desktop file {}: {}", desktop_file_path.value(), + error.what().c_str()); + } catch (Glib::KeyFileError& error) { + spdlog::warn("Error while loading desktop file {}: {}", desktop_file_path.value(), + error.what().c_str()); + } + return {}; +} + +void AAppIconLabel::updateAppIconName(const std::string& app_identifier, + const std::string& alternative_app_identifier) { + if (!iconEnabled()) { + return; + } + + const auto icon_name = getIconName(app_identifier, alternative_app_identifier); + if (icon_name.has_value()) { + app_icon_name_ = icon_name.value(); + } else { + app_icon_name_ = ""; + } + update_app_icon_ = true; +} + +void AAppIconLabel::updateAppIcon() { + if (update_app_icon_) { + update_app_icon_ = false; + if (app_icon_name_.empty()) { + image_.set_visible(false); + } else if (app_icon_name_.front() == '/') { + auto pixbuf = Gdk::Pixbuf::create_from_file(app_icon_name_); + int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); + pixbuf = Gdk::Pixbuf::create_from_file(app_icon_name_, scaled_icon_size, scaled_icon_size); + + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), + image_.get_window()); + image_.set(surface); + image_.set_visible(true); + } else { + image_.set_from_icon_name(app_icon_name_, Gtk::ICON_SIZE_INVALID); + image_.set_visible(true); + } + } +} + +auto AAppIconLabel::update() -> void { + updateAppIcon(); + AIconLabel::update(); +} + +} // namespace waybar diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index a7e2380a..130ba60c 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -1,6 +1,7 @@ #include "AIconLabel.hpp" #include +#include namespace waybar { @@ -9,10 +10,46 @@ 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(); - box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); - box_.set_spacing(8); - box_.add(image_); - box_.add(label_); + 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); + } + + int rot = 0; + + if (config_["rotate"].isUInt()) { + rot = config["rotate"].asUInt() % 360; + if ((rot % 90) != 00) + rot = 0; + rot /= 90; + } + + if ((rot % 2) == 0) + box_.set_orientation(Gtk::Orientation::ORIENTATION_HORIZONTAL); + else + box_.set_orientation(Gtk::Orientation::ORIENTATION_VERTICAL); + box_.set_name(name); + + int spacing = config_["icon-spacing"].isInt() ? config_["icon-spacing"].asInt() : 8; + box_.set_spacing(spacing); + + bool swap_icon_label = false; + if (not config_["swap-icon-label"].isBool()) + spdlog::warn("'swap-icon-label' must be a bool."); + else + swap_icon_label = config_["swap-icon-label"].asBool(); + + if ( (rot == 0 || rot == 3) ^ swap_icon_label ) { + box_.add(image_); + box_.add(label_); + } else { + box_.add(label_); + box_.add(image_); + } + event_box_.add(box_); } diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 4d8b2218..c218e402 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -2,17 +2,23 @@ #include +#include +#include #include +#include "config.hpp" + namespace waybar { ALabel::ALabel(const Json::Value& config, const std::string& name, const std::string& id, const std::string& format, uint16_t interval, bool ellipsize, bool enable_click, bool enable_scroll) - : AModule(config, name, id, config["format-alt"].isString() || enable_click, enable_scroll), + : AModule(config, name, id, + config["format-alt"].isString() || config["menu"].isString() || enable_click, + enable_scroll), format_(config_["format"].isString() ? config_["format"].asString() : format), interval_(config_["interval"] == "once" - ? std::chrono::seconds(100000000) + ? std::chrono::seconds::max() : std::chrono::seconds( config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)), default_format_(format_) { @@ -20,6 +26,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()); @@ -38,6 +45,8 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st if (config_["rotate"].isUInt()) { rotate = config["rotate"].asUInt(); + if (not(rotate == 0 || rotate == 90 || rotate == 180 || rotate == 270)) + spdlog::warn("'rotate' is only supported in 90 degree increments {} is not valid.", rotate); label_.set_angle(rotate); } @@ -49,6 +58,66 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st label_.set_xalign(align); } } + + // If a GTKMenu is requested in the config + if (config_["menu"].isString()) { + // Create the GTKMenu widget + try { + // Check that the file exists + std::string menuFile = config_["menu-file"].asString(); + + // there might be "~" or "$HOME" in original path, try to expand it. + auto result = Config::tryExpandPath(menuFile, ""); + if (result.empty()) { + throw std::runtime_error("Failed to expand file: " + menuFile); + } + + menuFile = result.front(); + // Read the menu descriptor file + std::ifstream file(menuFile); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + menuFile); + } + std::stringstream fileContent; + fileContent << file.rdbuf(); + GtkBuilder* builder = gtk_builder_new(); + + // Make the GtkBuilder and check for errors in his parsing + if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) { + throw std::runtime_error("Error found in the file " + menuFile); + } + + menu_ = gtk_builder_get_object(builder, "menu"); + if (menu_ == nullptr) { + throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); + } + submenus_ = std::map(); + menuActionsMap_ = std::map(); + + // Linking actions to the GTKMenu based on + for (Json::Value::const_iterator it = config_["menu-actions"].begin(); + it != config_["menu-actions"].end(); ++it) { + std::string key = it.key().asString(); + submenus_[key] = GTK_MENU_ITEM(gtk_builder_get_object(builder, key.c_str())); + menuActionsMap_[key] = it->asString(); + g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), + (gpointer)menuActionsMap_[key].c_str()); + } + } catch (std::runtime_error& e) { + spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what()); + } + } + + if (config_["justify"].isString()) { + auto justify_str = config_["justify"].asString(); + if (justify_str == "left") { + label_.set_justify(Gtk::Justification::JUSTIFY_LEFT); + } else if (justify_str == "right") { + label_.set_justify(Gtk::Justification::JUSTIFY_RIGHT); + } else if (justify_str == "center") { + label_.set_justify(Gtk::Justification::JUSTIFY_CENTER); + } + } } auto ALabel::update() -> void { AModule::update(); } @@ -64,7 +133,7 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ } if (format_icons.isArray()) { auto size = format_icons.size(); - if (size) { + if (size != 0U) { auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); format_icons = format_icons[idx]; } @@ -90,7 +159,7 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& } if (format_icons.isArray()) { auto size = format_icons.size(); - if (size) { + if (size != 0U) { auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); format_icons = format_icons[idx]; } @@ -113,6 +182,10 @@ bool waybar::ALabel::handleToggle(GdkEventButton* const& e) { return AModule::handleToggle(e); } +void ALabel::handleGtkMenuEvent(GtkMenuItem* /*menuitem*/, gpointer data) { + waybar::util::command::res res = waybar::util::command::exec((char*)data, "GtkMenu"); +} + std::string ALabel::getState(uint8_t value, bool lesser) { if (!config_["states"].isObject()) { return ""; diff --git a/src/AModule.cpp b/src/AModule.cpp index 6336eb5e..259c6a39 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -1,51 +1,85 @@ #include "AModule.hpp" #include +#include #include +#include "gdk/gdk.h" +#include "gdkmm/cursor.h" + namespace waybar { AModule::AModule(const Json::Value& config, const std::string& name, const std::string& id, bool enable_click, bool enable_scroll) - : name_(std::move(name)), - config_(std::move(config)), + : name_(name), + config_(config), + isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, + isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false}, distance_scrolled_y_(0.0), distance_scrolled_x_(0.0) { // Configure module action Map const Json::Value actions{config_["actions"]}; + for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) { if (it.key().isString() && it->isString()) - if (eventActionMap_.count(it.key().asString()) == 0) { + if (!eventActionMap_.contains(it.key().asString())) { eventActionMap_.insert({it.key().asString(), it->asString()}); enable_click = true; enable_scroll = true; } else - spdlog::warn("Dublicate action is ignored: {0}", it.key().asString()); + spdlog::warn("Duplicate action is ignored: {0}", it.key().asString()); else spdlog::warn("Wrong actions section configuration. See config by index: {}", it.index()); } + event_box_.signal_enter_notify_event().connect(sigc::mem_fun(*this, &AModule::handleMouseEnter)); + event_box_.signal_leave_notify_event().connect(sigc::mem_fun(*this, &AModule::handleMouseLeave)); + // configure events' user commands - if (enable_click) { + // hasUserEvents is true if any element from eventMap_ is satisfying the condition in the lambda + bool hasUserEvents = + std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) { + // True if there is any non-release type event + return eventEntry.first.second != GdkEventType::GDK_BUTTON_RELEASE && + config[eventEntry.second].isString(); + }) != eventMap_.cend(); + + if (enable_click || hasUserEvents) { + hasUserEvents_ = true; event_box_.add_events(Gdk::BUTTON_PRESS_MASK); event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &AModule::handleToggle)); } else { - 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; - } + hasUserEvents_ = false; } - if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() || enable_scroll) { + + 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() || + config_["on-scroll-left"].isString() || config_["on-scroll-right"].isString() || + enable_scroll) { event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &AModule::handleScroll)); } + + // Respect user configuration of cursor + if (config_.isMember("cursor")) { + if (config_["cursor"].isBool() && config_["cursor"].asBool()) { + setCursor(Gdk::HAND2); + } else if (config_["cursor"].isInt()) { + setCursor(Gdk::CursorType(config_["cursor"].asInt())); + } else { + spdlog::warn("unknown cursor option configured on module {}", name_); + } + } } AModule::~AModule() { @@ -63,25 +97,83 @@ auto AModule::update() -> void { } } // Get mapping between event name and module action name -// Then call overrided doAction in order to call appropriate module action +// Then call overridden doAction in order to call appropriate module action auto AModule::doAction(const std::string& name) -> void { if (!name.empty()) { const std::map::const_iterator& recA{eventActionMap_.find(name)}; - // Call overrided action if derrived class has implemented it + // Call overridden action if derived class has implemented it if (recA != eventActionMap_.cend() && name != recA->second) this->doAction(recA->second); } } -bool AModule::handleToggle(GdkEventButton* const& e) { +void AModule::setCursor(Gdk::CursorType const& c) { + auto gdk_window = event_box_.get_window(); + if (gdk_window) { + auto cursor = Gdk::Cursor::create(c); + gdk_window->set_cursor(cursor); + } else { + // window may not be accessible yet, in this case, + // schedule another call for setting the cursor in 1 sec + Glib::signal_timeout().connect_seconds( + [this, c]() { + setCursor(c); + return false; + }, + 1); + } +} + +bool AModule::handleMouseEnter(GdkEventCrossing* const& e) { + if (auto* module = event_box_.get_child(); module != nullptr) { + module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + } + + // Default behavior indicating event availability + if (hasUserEvents_ && !config_.isMember("cursor")) { + setCursor(Gdk::HAND2); + } + + return false; +} + +bool AModule::handleMouseLeave(GdkEventCrossing* const& e) { + if (auto* module = event_box_.get_child(); module != nullptr) { + module->unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + } + + // Default behavior indicating event availability + if (hasUserEvents_ && !config_.isMember("cursor")) { + setCursor(Gdk::ARROW); + } + + return false; +} + +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))}; + if (rec != eventMap_.cend()) { // First call module actions this->AModule::doAction(rec->second); format = rec->second; } + + // Check that a menu has been configured + if (config_["menu"].isString()) { + // Check if the event is the one specified for the "menu" option + if (rec->second == config_["menu"].asString()) { + // Popup the menu + gtk_widget_show_all(GTK_WIDGET(menu_)); + gtk_menu_popup_at_pointer(GTK_MENU(menu_), reinterpret_cast(e)); + } + } // Second call user scripts if (!format.empty()) { if (config_[format].isString()) @@ -103,7 +195,7 @@ AModule::SCROLL_DIR AModule::getScrollDir(GdkEventScroll* e) { // ignore reverse-scrolling if event comes from a mouse wheel GdkDevice* device = gdk_event_get_source_device((GdkEvent*)e); - if (device != NULL && gdk_device_get_source(device) == GDK_SOURCE_MOUSE) { + if (device != nullptr && gdk_device_get_source(device) == GDK_SOURCE_MOUSE) { reverse = reverse_mouse; } @@ -166,6 +258,10 @@ bool AModule::handleScroll(GdkEventScroll* e) { eventName = "on-scroll-up"; else if (dir == SCROLL_DIR::DOWN) eventName = "on-scroll-down"; + else if (dir == SCROLL_DIR::LEFT) + eventName = "on-scroll-left"; + else if (dir == SCROLL_DIR::RIGHT) + eventName = "on-scroll-right"; // First call module actions this->AModule::doAction(eventName); @@ -177,9 +273,8 @@ bool AModule::handleScroll(GdkEventScroll* e) { return true; } -bool AModule::tooltipEnabled() { - return config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true; -} +bool AModule::tooltipEnabled() const { return isTooltip; } +bool AModule::expandEnabled() const { return isExpand; } 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..3c3ab690 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 @@ -43,7 +37,7 @@ const Bar::bar_mode_map Bar::PRESET_MODES = { // .visible = true}}, {"hide", {// - .layer = bar_layer::TOP, + .layer = bar_layer::OVERLAY, .exclusive = false, .passthrough = false, .visible = true}}, @@ -55,13 +49,13 @@ const Bar::bar_mode_map Bar::PRESET_MODES = { // .visible = false}}, {"overlay", {// - .layer = bar_layer::TOP, + .layer = bar_layer::OVERLAY, .exclusive = false, .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 */ @@ -78,26 +72,53 @@ void from_json(const Json::Value& j, bar_layer& l) { /* Deserializer for struct bar_mode */ void from_json(const Json::Value& j, bar_mode& m) { if (j.isObject()) { - if (auto v = j["layer"]; v.isString()) { + if (const auto& v = j["layer"]; v.isString()) { from_json(v, m.layer); } - if (auto v = j["exclusive"]; v.isBool()) { + if (const auto& v = j["exclusive"]; v.isBool()) { m.exclusive = v.asBool(); } - if (auto v = j["passthrough"]; v.isBool()) { + if (const auto& v = j["passthrough"]; v.isBool()) { m.passthrough = v.asBool(); } - if (auto v = j["visible"]; v.isBool()) { + if (const auto& v = j["visible"]; v.isBool()) { m.visible = v.asBool(); } } } +/* 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>> 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,16 @@ 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), + surface(nullptr), 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 +146,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 +170,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 +218,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 +294,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 +315,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,15 +340,76 @@ void waybar::Bar::setMode(const struct bar_mode& mode) { window.get_style_context()->add_class("hidden"); window.set_opacity(0); } - surface_impl_->commit(); + /* + * All the changes above require `wl_surface_commit`. + * gtk-layer-shell schedules a commit on the next frame event in GTK, but this could fail in + * certain scenarios, such as fully occluded bar. + */ + gtk_layer_try_force_commit(gtk_window); + wl_display_flush(Client::inst()->wl_display); } -void waybar::Bar::onMap(GdkEventAny*) { +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* /*unused*/) { /* * Obtain a pointer to the custom layer surface for modules that require it (idle_inhibitor). */ - auto gdk_window = window.get_window()->gobj(); + auto* gdk_window = window.get_window()->gobj(); surface = gdk_wayland_window_get_wl_surface(gdk_window); + configureGlobalOffset(gdk_window_get_width(gdk_window), gdk_window_get_height(gdk_window)); + + setPassThrough(passthrough_); } void waybar::Bar::setVisible(bool value) { @@ -721,7 +457,17 @@ void waybar::Bar::setupAltFormatKeyForModuleList(const char* module_list_name) { Json::Value& modules = config[module_list_name]; for (const Json::Value& module_name : modules) { if (module_name.isString()) { - setupAltFormatKeyForModule(module_name.asString()); + auto ref = module_name.asString(); + if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) { + Json::Value& group_modules = config[ref]["modules"]; + for (const Json::Value& module_name : group_modules) { + if (module_name.isString()) { + setupAltFormatKeyForModule(module_name.asString()); + } + } + } else { + setupAltFormatKeyForModule(ref); + } } } } @@ -734,8 +480,8 @@ void waybar::Bar::handleSignal(int signal) { } void waybar::Bar::getModules(const Factory& factory, const std::string& pos, - Gtk::Box* group = nullptr) { - auto module_list = group ? config[pos]["modules"] : config[pos]; + waybar::Group* group = nullptr) { + auto module_list = group != nullptr ? config[pos]["modules"] : config[pos]; if (module_list.isArray()) { for (const auto& name : module_list) { try { @@ -747,19 +493,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 group_module = new waybar::Group(id_name, class_name, config[ref], vertical); - getModules(factory, ref, &group_module->box); + auto vertical = (group != nullptr ? group->getBox().get_orientation() + : box_.get_orientation()) == Gtk::ORIENTATION_VERTICAL; + + auto* group_module = new waybar::Group(id_name, class_name, config[ref], vertical); + 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); + if (group != nullptr) { + group->addWidget(*module); } else { if (pos == "modules-left") { modules_left_.emplace_back(module_sp); @@ -787,13 +534,21 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, auto waybar::Bar::setupWidgets() -> void { window.add(box_); - box_.pack_start(left_, false, false); - if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) { - box_.set_center_widget(center_); - } else { - box_.pack_start(center_, true, false); + + bool expand_left = config["expand-left"].isBool() ? config["expand-left"].asBool() : false; + bool expand_center = config["expand-center"].isBool() ? config["expand-center"].asBool() : false; + bool expand_right = config["expand-right"].isBool() ? config["expand-right"].asBool() : false; + bool no_center = config["no-center"].isBool() ? config["no-center"].asBool() : false; + + box_.pack_start(left_, expand_left, expand_left); + if (!no_center) { + if (config["fixed-center"].isBool() ? config["fixed-center"].asBool() : true) { + box_.set_center_widget(center_); + } else { + box_.pack_start(center_, true, expand_center); + } } - box_.pack_end(right_, false, false); + box_.pack_end(right_, expand_right, expand_right); // Convert to button code for every module that is used. setupAltFormatKeyForModuleList("modules-left"); @@ -802,16 +557,91 @@ auto waybar::Bar::setupWidgets() -> void { Factory factory(*this, config); getModules(factory, "modules-left"); - getModules(factory, "modules-center"); + if (!no_center) { + getModules(factory, "modules-center"); + } getModules(factory, "modules-right"); + for (auto const& module : modules_left_) { - left_.pack_start(*module, false, false); + left_.pack_start(*module, module->expandEnabled(), module->expandEnabled()); } - for (auto const& module : modules_center_) { - center_.pack_start(*module, false, false); + + if (!no_center) { + for (auto const& module : modules_center_) { + center_.pack_start(*module, module->expandEnabled(), module->expandEnabled()); + } } + std::reverse(modules_right_.begin(), modules_right_.end()); for (auto const& module : modules_right_) { - right_.pack_end(*module, false, false); + right_.pack_end(*module, module->expandEnabled(), module->expandEnabled()); } } + +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..e363f236 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -1,29 +1,26 @@ #include "client.hpp" +#include #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(); + static auto *c = new Client(); return c; } void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { - auto client = static_cast(data); - 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) { + auto *client = static_cast(data); + if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && + version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) { client->xdg_output_manager = static_cast(wl_registry_bind( 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) { @@ -46,7 +43,7 @@ void waybar::Client::handleOutput(struct waybar_output &output) { .description = &handleOutputDescription, }; // owned by output->monitor; no need to destroy - auto wl_output = gdk_wayland_monitor_get_wl_output(output.monitor->gobj()); + auto *wl_output = gdk_wayland_monitor_get_wl_output(output.monitor->gobj()); output.xdg_output.reset(zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, wl_output)); zxdg_output_v1_add_listener(output.xdg_output.get(), &xdgOutputListener, &output); } @@ -65,7 +62,7 @@ std::vector waybar::Client::getOutputConfigs(struct waybar_output & } void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_output*/) { - auto client = waybar::Client::inst(); + auto *client = waybar::Client::inst(); try { auto &output = client->getOutput(data); /** @@ -89,44 +86,45 @@ void waybar::Client::handleOutputDone(void *data, struct zxdg_output_v1 * /*xdg_ } } } catch (const std::exception &e) { - std::cerr << e.what() << std::endl; + spdlog::warn("caught exception in zxdg_output_v1_listener::done: {}", e.what()); } } void waybar::Client::handleOutputName(void *data, struct zxdg_output_v1 * /*xdg_output*/, const char *name) { - auto client = waybar::Client::inst(); + auto *client = waybar::Client::inst(); try { auto &output = client->getOutput(data); output.name = name; } catch (const std::exception &e) { - std::cerr << e.what() << std::endl; + spdlog::warn("caught exception in zxdg_output_v1_listener::name: {}", e.what()); } } void waybar::Client::handleOutputDescription(void *data, struct zxdg_output_v1 * /*xdg_output*/, const char *description) { - auto client = waybar::Client::inst(); + auto *client = waybar::Client::inst(); try { auto &output = client->getOutput(data); - const char *open_paren = strrchr(description, '('); // Description format: "identifier (name)" - size_t identifier_length = open_paren - description; - output.identifier = std::string(description, identifier_length - 1); + auto s = std::string(description); + auto pos = s.find(" ("); + output.identifier = pos != std::string::npos ? s.substr(0, pos) : s; } catch (const std::exception &e) { - std::cerr << e.what() << std::endl; + spdlog::warn("caught exception in zxdg_output_v1_listener::description: {}", e.what()); } } void waybar::Client::handleMonitorAdded(Glib::RefPtr monitor) { auto &output = outputs_.emplace_back(); - output.monitor = monitor; + output.monitor = std::move(monitor); handleOutput(output); } void waybar::Client::handleMonitorRemoved(Glib::RefPtr monitor) { - spdlog::debug("Output removed: {} {}", monitor->get_manufacturer(), monitor->get_model()); + spdlog::debug("Output removed: {} {}", monitor->get_manufacturer().c_str(), + monitor->get_model().c_str()); /* This event can be triggered from wl_display_roundtrip called by GTK or our code. * Defer destruction of bars for the output to the next iteration of the event loop to avoid * deleting objects referenced by currently executed code. @@ -151,8 +149,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.emplace_back("style-light.css"); + break; + case waybar::Appearance::DARK: + search_files.emplace_back("style-dark.css"); + break; + case waybar::Appearance::UNKNOWN: + break; + } + search_files.emplace_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 +197,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() == 0) { + 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 @@ -214,11 +235,11 @@ int waybar::Client::main(int argc, char *argv[]) { return 1; } if (show_help) { - std::cout << cli << std::endl; + std::cout << cli << '\n'; return 0; } if (show_version) { - std::cout << "Waybar v" << VERSION << std::endl; + std::cout << "Waybar v" << VERSION << '\n'; return 0; } if (!log_level.empty()) { @@ -226,6 +247,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 +261,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/config.cpp b/src/config.cpp index 45f5ee38..7096ba89 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -21,10 +21,11 @@ const std::vector Config::CONFIG_DIRS = { const char *Config::CONFIG_PATH_ENV = "WAYBAR_CONFIG_DIR"; -std::optional tryExpandPath(const std::string base, const std::string filename) { +std::vector Config::tryExpandPath(const std::string &base, + const std::string &filename) { fs::path path; - if (filename != "") { + if (!filename.empty()) { path = fs::path(base) / fs::path(filename); } else { path = fs::path(base); @@ -32,33 +33,35 @@ std::optional tryExpandPath(const std::string base, const std::stri spdlog::debug("Try expanding: {}", path.string()); + std::vector results; wordexp_t p; if (wordexp(path.c_str(), &p, 0) == 0) { - if (access(*p.we_wordv, F_OK) == 0) { - std::string result = *p.we_wordv; - wordfree(&p); - spdlog::debug("Found config file: {}", path.string()); - return result; + for (size_t i = 0; i < p.we_wordc; i++) { + if (access(p.we_wordv[i], F_OK) == 0) { + results.emplace_back(p.we_wordv[i]); + spdlog::debug("Found config file: {}", p.we_wordv[i]); + } } wordfree(&p); } - return std::nullopt; + + return results; } std::optional Config::findConfigPath(const std::vector &names, const std::vector &dirs) { if (const char *dir = std::getenv(Config::CONFIG_PATH_ENV)) { for (const auto &name : names) { - if (auto res = tryExpandPath(dir, name); res) { - return res; + if (auto res = tryExpandPath(dir, name); !res.empty()) { + return res.front(); } } } for (const auto &dir : dirs) { for (const auto &name : names) { - if (auto res = tryExpandPath(dir, name); res) { - return res; + if (auto res = tryExpandPath(dir, name); !res.empty()) { + return res.front(); } } } @@ -91,11 +94,15 @@ void Config::resolveConfigIncludes(Json::Value &config, int depth) { if (includes.isArray()) { for (const auto &include : includes) { spdlog::info("Including resource file: {}", include.asString()); - setupConfig(config, tryExpandPath(include.asString(), "").value_or(""), ++depth); + for (const auto &match : tryExpandPath(include.asString(), "")) { + setupConfig(config, match, depth + 1); + } } } else if (includes.isString()) { spdlog::info("Including resource file: {}", includes.asString()); - setupConfig(config, tryExpandPath(includes.asString(), "").value_or(""), ++depth); + for (const auto &match : tryExpandPath(includes.asString(), "")) { + setupConfig(config, match, depth + 1); + } } } @@ -129,9 +136,9 @@ bool isValidOutput(const Json::Value &config, const std::string &name, if (config_output.substr(0, 1) == "!") { if (config_output.substr(1) == name || config_output.substr(1) == identifier) { return false; - } else { - continue; } + + continue; } if (config_output == name || config_output == identifier) { return true; @@ -142,7 +149,9 @@ bool isValidOutput(const Json::Value &config, const std::string &name, } } return false; - } else if (config["output"].isString()) { + } + + if (config["output"].isString()) { auto config_output = config["output"].asString(); if (!config_output.empty()) { if (config_output.substr(0, 1) == "!") { @@ -162,6 +171,7 @@ void Config::load(const std::string &config) { } config_file_ = file.value(); spdlog::info("Using configuration file {}", config_file_); + config_ = Json::Value(); setupConfig(config_, config_file_, 0); } diff --git a/src/factory.cpp b/src/factory.cpp index 1d7a00b5..1483397d 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -1,15 +1,131 @@ #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" +#include "modules/dwl/window.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 +#ifdef HAVE_NIRI +#include "modules/niri/language.hpp" +#include "modules/niri/window.hpp" +#include "modules/niri/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.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 @@ -19,7 +135,12 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { #endif #ifdef HAVE_UPOWER if (ref == "upower") { - return new waybar::modules::upower::UPower(id, config_[name]); + return new waybar::modules::UPower(id, config_[name]); + } +#endif +#ifdef HAVE_PIPEWIRE + if (ref == "privacy") { + return new waybar::modules::privacy::Privacy(id, config_[name], bar_.orientation, pos); } #endif #ifdef HAVE_MPRIS @@ -44,16 +165,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]); @@ -72,6 +193,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "dwl/tags") { return new waybar::modules::dwl::Tags(id, bar_, config_[name]); } + if (ref == "dwl/window") { + return new waybar::modules::dwl::Window(id, bar_, config_[name]); + } #endif #ifdef HAVE_HYPRLAND if (ref == "hyprland/window") { @@ -86,6 +210,17 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "hyprland/workspaces") { return new waybar::modules::hyprland::Workspaces(id, bar_, config_[name]); } +#endif +#ifdef HAVE_NIRI + if (ref == "niri/language") { + return new waybar::modules::niri::Language(id, bar_, config_[name]); + } + if (ref == "niri/window") { + return new waybar::modules::niri::Window(id, bar_, config_[name]); + } + if (ref == "niri/workspaces") { + return new waybar::modules::niri::Workspaces(id, bar_, config_[name]); + } #endif if (ref == "idle_inhibitor") { return new waybar::modules::IdleInhibitor(id, bar_, config_[name]); @@ -99,6 +234,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 +272,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 +285,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 +299,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 +326,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..50841efd 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -4,12 +4,31 @@ #include +#include "gtkmm/enums.h" +#include "gtkmm/widget.h" + namespace waybar { +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; + } + + 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,94 @@ 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); + click_to_reveal = drawer_config["click-to-reveal"].asBool(); + + 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); + } + } + + event_box_.add(box); +} + +void Group::show_group() { + box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + revealer.set_reveal_child(true); +} + +void Group::hide_group() { + box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + revealer.set_reveal_child(false); +} + +bool Group::handleMouseEnter(GdkEventCrossing* const& e) { + if (!click_to_reveal) { + show_group(); + } + return false; +} + +bool Group::handleMouseLeave(GdkEventCrossing* const& e) { + if (!click_to_reveal && e->detail != GDK_NOTIFY_INFERIOR) { + hide_group(); + } + return false; +} + +bool Group::handleToggle(GdkEventButton* const& e) { + if (!click_to_reveal || e->button != 1) { + return false; + } + if ((box.get_state_flags() & Gtk::StateFlags::STATE_FLAG_PRELIGHT) != 0U) { + hide_group(); + } else { + show_group(); + } + return true; } auto Group::update() -> void { // noop } -Group::operator Gtk::Widget&() { return box; } +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 && !is_first_widget) { + widget.get_style_context()->add_class(add_class_to_drawer_children); + } + + is_first_widget = false; +} + +Group::operator Gtk::Widget&() { return event_box_; } } // namespace waybar diff --git a/src/main.cpp b/src/main.cpp index ff446ffc..6e7650a9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -7,98 +8,132 @@ #include #include "client.hpp" +#include "util/SafeSignal.hpp" std::mutex reap_mtx; std::list reap; -volatile bool reload; -void* signalThread(void* args) { - int err, signum; - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGCHLD); +static int signal_pipe_write_fd; + +// Write a single signal to `signal_pipe_write_fd`. +// This function is set as a signal handler, so it must be async-signal-safe. +static void writeSignalToPipe(int signum) { + ssize_t amt = write(signal_pipe_write_fd, &signum, sizeof(int)); + + // There's not much we can safely do inside of a signal handler. + // Let's just ignore any errors. + (void)amt; +} + +// This initializes `signal_pipe_write_fd`, and sets up signal handlers. +// +// This function will run forever, emitting every `SIGUSR1`, `SIGUSR2`, +// `SIGINT`, `SIGCHLD`, and `SIGRTMIN + 1`...`SIGRTMAX` signal received +// to `signal_handler`. +static void catchSignals(waybar::SafeSignal& signal_handler) { + int fd[2]; + pipe(fd); + + int signal_pipe_read_fd = fd[0]; + signal_pipe_write_fd = fd[1]; + + // This pipe should be able to buffer ~thousands of signals. If it fills up, + // we'll drop signals instead of blocking. + + // We can't allow the write end to block because we'll be writing to it in a + // signal handler, which could interrupt the loop that's reading from it and + // deadlock. + + fcntl(signal_pipe_write_fd, F_SETFL, O_NONBLOCK); + + std::signal(SIGUSR1, writeSignalToPipe); + std::signal(SIGUSR2, writeSignalToPipe); + std::signal(SIGINT, writeSignalToPipe); + std::signal(SIGCHLD, writeSignalToPipe); + + for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { + std::signal(sig, writeSignalToPipe); + } while (true) { - err = sigwait(&mask, &signum); - if (err != 0) { - spdlog::error("sigwait failed: {}", strerror(errno)); + int signum; + ssize_t amt = read(signal_pipe_read_fd, &signum, sizeof(int)); + if (amt < 0) { + spdlog::error("read from signal pipe failed with error {}, closing thread", strerror(errno)); + break; + } + + if (amt != sizeof(int)) { continue; } - switch (signum) { - case SIGCHLD: - spdlog::debug("Received SIGCHLD in signalThread"); - if (!reap.empty()) { - reap_mtx.lock(); - for (auto it = reap.begin(); it != reap.end(); ++it) { - if (waitpid(*it, nullptr, WNOHANG) == *it) { - spdlog::debug("Reaped child with PID: {}", *it); - it = reap.erase(it); - } - } - reap_mtx.unlock(); - } - break; - default: - spdlog::debug("Received signal with number {}, but not handling", signum); - break; - } + signal_handler.emit(signum); } } -void startSignalThread(void) { - int err; - sigset_t mask; - sigemptyset(&mask); - sigaddset(&mask, SIGCHLD); +// Must be called on the main thread. +// +// If this signal should restart or close the bar, this function will write +// `true` or `false`, respectively, into `reload`. +static void handleSignalMainThread(int signum, bool& reload) { + if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) { + for (auto& bar : waybar::Client::inst()->bars) { + bar->handleSignal(signum); + } - // Block SIGCHLD so it can be handled by the signal thread - // Any threads created by this one (the main thread) should not - // modify their signal mask to unblock SIGCHLD - err = pthread_sigmask(SIG_BLOCK, &mask, nullptr); - if (err != 0) { - spdlog::error("pthread_sigmask failed in startSignalThread: {}", strerror(err)); - exit(1); + return; } - pthread_t thread_id; - err = pthread_create(&thread_id, nullptr, signalThread, nullptr); - if (err != 0) { - spdlog::error("pthread_create failed in startSignalThread: {}", strerror(err)); - exit(1); + switch (signum) { + case SIGUSR1: + spdlog::debug("Visibility toggled"); + for (auto& bar : waybar::Client::inst()->bars) { + bar->toggle(); + } + break; + case SIGUSR2: + spdlog::info("Reloading..."); + reload = true; + waybar::Client::inst()->reset(); + break; + case SIGINT: + spdlog::info("Quitting."); + reload = false; + waybar::Client::inst()->reset(); + break; + case SIGCHLD: + spdlog::debug("Received SIGCHLD in signalThread"); + if (!reap.empty()) { + reap_mtx.lock(); + for (auto it = reap.begin(); it != reap.end(); ++it) { + if (waitpid(*it, nullptr, WNOHANG) == *it) { + spdlog::debug("Reaped child with PID: {}", *it); + it = reap.erase(it); + } + } + reap_mtx.unlock(); + } + break; + default: + spdlog::debug("Received signal with number {}, but not handling", signum); + break; } } int main(int argc, char* argv[]) { try { - auto client = waybar::Client::inst(); + auto* client = waybar::Client::inst(); - std::signal(SIGUSR1, [](int /*signal*/) { - for (auto& bar : waybar::Client::inst()->bars) { - bar->toggle(); - } - }); + bool reload; - std::signal(SIGUSR2, [](int /*signal*/) { - spdlog::info("Reloading..."); - reload = true; - waybar::Client::inst()->reset(); - }); + waybar::SafeSignal posix_signal_received; + posix_signal_received.connect([&](int signum) { handleSignalMainThread(signum, reload); }); - std::signal(SIGINT, [](int /*signal*/) { - spdlog::info("Quitting."); - reload = false; - waybar::Client::inst()->reset(); - }); + std::thread signal_thread([&]() { catchSignals(posix_signal_received); }); - for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { - std::signal(sig, [](int sig) { - for (auto& bar : waybar::Client::inst()->bars) { - bar->handleSignal(sig); - } - }); - } - startSignalThread(); + // Every `std::thread` must be joined or detached. + // This thread should run forever, so detach it. + signal_thread.detach(); auto ret = 0; do { @@ -106,6 +141,10 @@ int main(int argc, char* argv[]) { ret = client->main(argc, argv); } while (reload); + std::signal(SIGUSR1, SIG_IGN); + std::signal(SIGUSR2, SIG_IGN); + std::signal(SIGINT, SIG_IGN); + delete client; return ret; } catch (const std::exception& e) { diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 58d14dde..ff58951c 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; } @@ -190,9 +38,8 @@ auto waybar::modules::Backlight::update() -> void { event_box_.show(); const uint8_t percent = best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max()); - std::string desc = - fmt::format(fmt::runtime(format_), fmt::arg("percent", std::to_string(percent)), - fmt::arg("icon", getIcon(percent))); + std::string desc = fmt::format(fmt::runtime(format_), fmt::arg("percent", percent), + fmt::arg("icon", getIcon(percent))); label_.set_markup(desc); getState(percent); if (tooltipEnabled()) { @@ -202,7 +49,7 @@ auto waybar::modules::Backlight::update() -> void { } if (!tooltip_format.empty()) { label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format), - fmt::arg("percent", std::to_string(percent)), + fmt::arg("percent", percent), fmt::arg("icon", getIcon(percent)))); } else { label_.set_tooltip_text(desc); @@ -212,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()) { @@ -295,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 @@ -312,38 +112,15 @@ 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_; + double min_brightness = 0; + if (config_["min-brightness"].isDouble()) { + min_brightness = config_["min-brightness"].asDouble(); } - const auto best = best_device(devices.cbegin(), devices.cend(), preferred_device_); - - if (best == nullptr) { + if (backend.get_scaled_brightness(preferred_device_) <= min_brightness && + ct == util::ChangeType::Decrease) { 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..44481448 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; @@ -177,7 +181,8 @@ static bool status_gt(const std::string& a, const std::string& b) { return false; } -const std::tuple waybar::modules::Battery::getInfos() { +std::tuple +waybar::modules::Battery::getInfos() { std::lock_guard guard(battery_list_mutex_); try { @@ -230,7 +235,7 @@ const std::tuple waybar::modules::Battery::g } // spdlog::info("{} {} {} {}", capacity,time,status,rate); - return {capacity, time / 60.0, status, rate}; + return {capacity, time / 60.0, status, rate, 0, 0.0F}; #elif defined(__linux__) uint32_t total_power = 0; // μW @@ -248,31 +253,38 @@ const std::tuple waybar::modules::Battery::g uint32_t time_to_full_now = 0; bool time_to_full_now_exists = false; + uint32_t largestDesignCapacity = 0; + uint16_t mainBatCycleCount = 0; + float mainBatHealthPercent = 0.0F; + std::string status = "Unknown"; for (auto const& item : batteries_) { auto bat = item.first; 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. - uint32_t capacity = 0; - bool capacity_exists = false; - if (fs::exists(bat / "capacity")) { - capacity_exists = true; - std::ifstream(bat / "capacity") >> capacity; - } - uint32_t current_now = 0; + int32_t _current_now_int = 0; bool current_now_exists = false; if (fs::exists(bat / "current_now")) { current_now_exists = true; - std::ifstream(bat / "current_now") >> current_now; + std::ifstream(bat / "current_now") >> _current_now_int; } else if (fs::exists(bat / "current_avg")) { current_now_exists = true; - std::ifstream(bat / "current_avg") >> current_now; + std::ifstream(bat / "current_avg") >> _current_now_int; } + // Documentation ABI allows a negative value when discharging, positive + // value when charging. + current_now = std::abs(_current_now_int); if (fs::exists(bat / "time_to_empty_now")) { time_to_empty_now_exists = true; @@ -316,11 +328,15 @@ const std::tuple waybar::modules::Battery::g } uint32_t power_now = 0; + int32_t _power_now_int = 0; bool power_now_exists = false; if (fs::exists(bat / "power_now")) { power_now_exists = true; - std::ifstream(bat / "power_now") >> power_now; + std::ifstream(bat / "power_now") >> _power_now_int; } + // Some drivers (example: Qualcomm) exposes use a negative value when + // discharging, positive value when charging. + power_now = std::abs(_power_now_int); uint32_t energy_now = 0; bool energy_now_exists = false; @@ -343,6 +359,43 @@ const std::tuple waybar::modules::Battery::g std::ifstream(bat / "energy_full_design") >> energy_full_design; } + uint16_t cycleCount = 0; + if (fs::exists(bat / "cycle_count")) { + std::ifstream(bat / "cycle_count") >> cycleCount; + } + if (charge_full_design >= largestDesignCapacity) { + largestDesignCapacity = charge_full_design; + + if (cycleCount > mainBatCycleCount) { + mainBatCycleCount = cycleCount; + } + + if (charge_full_exists && charge_full_design_exists) { + float batHealthPercent = ((float)charge_full / charge_full_design) * 100; + if (mainBatHealthPercent == 0.0F || batHealthPercent < mainBatHealthPercent) { + mainBatHealthPercent = batHealthPercent; + } + } else if (energy_full_exists && energy_full_design_exists) { + float batHealthPercent = ((float)energy_full / energy_full_design) * 100; + if (mainBatHealthPercent == 0.0F || batHealthPercent < mainBatHealthPercent) { + mainBatHealthPercent = batHealthPercent; + } + } + } + + uint32_t capacity = 0; + bool capacity_exists = false; + if (charge_now_exists && charge_full_exists && charge_full != 0) { + capacity_exists = true; + capacity = 100 * (uint64_t)charge_now / (uint64_t)charge_full; + } else if (energy_now_exists && energy_full_exists && energy_full != 0) { + capacity_exists = true; + capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; + } else if (fs::exists(bat / "capacity")) { + capacity_exists = true; + std::ifstream(bat / "capacity") >> capacity; + } + if (!voltage_now_exists) { if (power_now_exists && current_now_exists && current_now != 0) { voltage_now_exists = true; @@ -383,13 +436,7 @@ const std::tuple waybar::modules::Battery::g } if (!capacity_exists) { - if (charge_now_exists && charge_full_exists && charge_full != 0) { - capacity_exists = true; - capacity = 100 * (uint64_t)charge_now / (uint64_t)charge_full; - } else if (energy_now_exists && energy_full_exists && energy_full != 0) { - capacity_exists = true; - capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; - } else if (charge_now_exists && energy_full_exists && voltage_now_exists) { + if (charge_now_exists && energy_full_exists && voltage_now_exists) { if (!charge_full_exists && voltage_now != 0) { charge_full_exists = true; charge_full = 1000000 * (uint64_t)energy_full / (uint64_t)voltage_now; @@ -534,6 +581,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) { @@ -556,11 +610,12 @@ const std::tuple waybar::modules::Battery::g // still charging but not yet done if (cap == 100 && status == "Charging") status = "Full"; - return {cap, time_remaining, status, total_power / 1e6}; + return { + cap, time_remaining, status, total_power / 1e6, mainBatCycleCount, mainBatHealthPercent}; #endif } catch (const std::exception& e) { spdlog::error("Battery: {}", e.what()); - return {0, 0, "Unknown", 0}; + return {0, 0, "Unknown", 0, 0, 0.0f}; } } @@ -616,7 +671,7 @@ auto waybar::modules::Battery::update() -> void { return; } #endif - auto [capacity, time_remaining, status, power] = getInfos(); + auto [capacity, time_remaining, status, power, cycles, health] = getInfos(); if (status == "Unknown") { status = getAdapterStatus(capacity); } @@ -626,6 +681,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; @@ -645,10 +701,11 @@ auto waybar::modules::Battery::update() -> void { } else if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format), - fmt::arg("timeTo", tooltip_text_default), - fmt::arg("power", power), fmt::arg("capacity", capacity), - fmt::arg("time", time_remaining_formatted))); + label_.set_tooltip_text( + fmt::format(fmt::runtime(tooltip_format), fmt::arg("timeTo", tooltip_text_default), + fmt::arg("power", power), fmt::arg("capacity", capacity), + fmt::arg("time", time_remaining_formatted), fmt::arg("cycles", cycles), + fmt::arg("health", fmt::format("{:.3}", health)))); } if (!old_status_.empty()) { label_.get_style_context()->remove_class(old_status_); @@ -669,8 +726,44 @@ auto waybar::modules::Battery::update() -> void { auto icons = std::vector{status + "-" + state, status, state}; label_.set_markup(fmt::format( fmt::runtime(format), fmt::arg("capacity", capacity), fmt::arg("power", power), - fmt::arg("icon", getIcon(capacity, icons)), fmt::arg("time", time_remaining_formatted))); + fmt::arg("icon", getIcon(capacity, icons)), fmt::arg("time", time_remaining_formatted), + fmt::arg("cycles", cycles), fmt::arg("health", fmt::format("{:.3}", health)))); } // Call parent update ALabel::update(); } + +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..06475a2e 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,25 +98,27 @@ 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 - // 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()); + spdlog::warn("no bluetooth controller found with alias '{}'", + config_["controller-alias"].asString()); } else { - spdlog::error("findCurController() failed: no bluetooth controller found"); + spdlog::warn("no bluetooth controller found"); } - event_box_.hide(); - return; + update(); + } else { + // This call only make sense if a controller could be found + findConnectedDevices(cur_controller_->path, connected_devices_); } - findConnectedDevices(cur_controller_.path, connected_devices_); + g_signal_connect(manager_.get(), "object-added", G_CALLBACK(onObjectAdded), this); + g_signal_connect(manager_.get(), "object-removed", G_CALLBACK(onObjectRemoved), this); g_signal_connect(manager_.get(), "interface-proxy-properties-changed", G_CALLBACK(onInterfaceProxyPropertiesChanged), this); g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), this); g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), this); + #ifdef WANT_RFKILL rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); #endif @@ -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), @@ -264,6 +282,46 @@ auto waybar::modules::Bluetooth::update() -> void { ALabel::update(); } +auto waybar::modules::Bluetooth::onObjectAdded(GDBusObjectManager* manager, GDBusObject* object, + gpointer user_data) -> void { + ControllerInfo info; + Bluetooth* bt = static_cast(user_data); + + if (!bt->cur_controller_.has_value() && bt->getControllerProperties(object, info) && + (!bt->config_["controller-alias"].isString() || + bt->config_["controller-alias"].asString() == info.alias)) { + bt->cur_controller_ = std::move(info); + bt->dp.emit(); + } +} + +auto waybar::modules::Bluetooth::onObjectRemoved(GDBusObjectManager* manager, GDBusObject* object, + gpointer user_data) -> void { + Bluetooth* bt = static_cast(user_data); + GDBusProxy* proxy_controller; + + if (!bt->cur_controller_.has_value()) { + return; + } + + proxy_controller = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Adapter1")); + + if (proxy_controller != NULL) { + std::string object_path = g_dbus_object_get_object_path(object); + + if (object_path == bt->cur_controller_->path) { + bt->cur_controller_ = bt->findCurController(); + if (bt->cur_controller_.has_value()) { + bt->connected_devices_.clear(); + bt->findConnectedDevices(bt->cur_controller_->path, bt->connected_devices_); + } + bt->dp.emit(); + } + + g_object_unref(proxy_controller); + } +} + // NOTE: only for when the org.bluez.Battery1 interface is added/removed after/before a device is // connected/disconnected auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager* manager, @@ -274,11 +332,13 @@ auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager* m std::string object_path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(interface)); if (interface_name == "org.bluez.Battery1") { Bluetooth* bt = static_cast(user_data); - auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(), - [object_path](auto d) { return d.path == object_path; }); - if (device != bt->connected_devices_.end()) { - device->battery_percentage = bt->getDeviceBatteryPercentage(object); - bt->dp.emit(); + if (bt->cur_controller_.has_value()) { + auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(), + [object_path](auto d) { return d.path == object_path; }); + if (device != bt->connected_devices_.end()) { + device->battery_percentage = bt->getDeviceBatteryPercentage(object); + bt->dp.emit(); + } } } } @@ -291,9 +351,14 @@ auto waybar::modules::Bluetooth::onInterfaceProxyPropertiesChanged( std::string object_path = g_dbus_object_get_object_path(G_DBUS_OBJECT(object_proxy)); Bluetooth* bt = static_cast(user_data); + + if (!bt->cur_controller_.has_value()) { + return; + } + if (interface_name == "org.bluez.Adapter1") { - if (object_path == bt->cur_controller_.path) { - bt->getControllerProperties(G_DBUS_OBJECT(object_proxy), bt->cur_controller_); + 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 +443,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 +470,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..405a351a 100644 --- a/src/modules/cava.cpp +++ b/src/modules/cava.cpp @@ -8,13 +8,7 @@ waybar::modules::Cava::Cava(const std::string& id, const Json::Value& config) char cfgPath[PATH_MAX]; cfgPath[0] = '\0'; - if (config_["cava_config"].isString()) { - std::string strPath{config_["cava_config"].asString()}; - const std::string fnd{"XDG_CONFIG_HOME"}; - const std::string::size_type npos{strPath.find("$" + fnd)}; - if (npos != std::string::npos) strPath.replace(npos, fnd.length() + 1, getenv(fnd.c_str())); - strcpy(cfgPath, strPath.data()); - } + if (config_["cava_config"].isString()) strcpy(cfgPath, config_["cava_config"].asString().data()); // Load cava config error_.length = 0; @@ -25,7 +19,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 +28,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 +45,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 +58,10 @@ 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(); + if (config_["format_silent"].isString()) format_silent_ = config_["format_silent"].asString(); // Make cava parameters configuration - plan_ = new cava_plan{}; + plan_ = new cava::cava_plan{}; audio_raw_.height = prm_.ascii_range; audio_data_.format = -1; @@ -143,7 +139,7 @@ auto waybar::modules::Cava::update() -> void { } } - if (silence_ && prm_.sleep_timer) { + if (silence_ && prm_.sleep_timer != 0) { if (sleep_counter_ <= (int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { ++sleep_counter_; @@ -151,16 +147,17 @@ auto waybar::modules::Cava::update() -> void { } } - if (!silence_) { + if (!silence_ || prm_.sleep_timer == 0) { 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 +171,22 @@ auto waybar::modules::Cava::update() -> void { } label_.set_markup(text_); + label_.show(); ALabel::update(); + label_.get_style_context()->add_class("updated"); } - } else + + label_.get_style_context()->remove_class("silent"); + } else { upThreadDelay(frame_time_milsec_, suspend_silence_delay_); + if (hide_on_silence_) + label_.hide(); + else if (config_["format_silent"].isString()) + label_.set_markup(format_silent_); + + label_.get_style_context()->add_class("silent"); + label_.get_style_context()->remove_class("updated"); + } } auto waybar::modules::Cava::doAction(const std::string& name) -> void { diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp new file mode 100644 index 00000000..5c095f46 --- /dev/null +++ b/src/modules/cffi.cpp @@ -0,0 +1,123 @@ +#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 || *wbcffi_version == 2) { + // 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 (*wbcffi_version == 1) { + if (value.isConvertibleTo(Json::ValueType::stringValue)) { + config_entries_stringstor.push_back(value.asString()); + } else { + config_entries_stringstor.push_back(value.toStyledString()); + } + } else { + config_entries_stringstor.push_back(value.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 17cfd8da..a7d57437 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -1,345 +1,316 @@ #include "modules/clock.hpp" -#include +#include +#include #include -#include +#include #include #include #include -#include #include "util/ustring_clen.hpp" + #ifdef HAVE_LANGINFO_1STDAY #include -#include + +#include #endif +using namespace date; +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) { + m_locale_{std::locale(config_["locale"].isString() ? config_["locale"].asString() : "")}, + m_tlpFmt_{(config_["tooltip-format"].isString()) ? config_["tooltip-format"].asString() : ""}, + m_tooltip_{new Gtk::Label()}, + cldInTooltip_{m_tlpFmt_.find("{" + kCldPlaceholder + "}") != std::string::npos}, + cldYearShift_{January / 1 / 1900}, + cldMonShift_{year(1900) / January}, + tzInTooltip_{m_tlpFmt_.find("{" + kTZPlaceholder + "}") != std::string::npos}, + tzCurrIdx_{0}, + ordInTooltip_{m_tlpFmt_.find("{" + kOrdPlaceholder + "}") != std::string::npos} { + m_tlpText_ = m_tlpFmt_; + if (config_["timezones"].isArray() && !config_["timezones"].empty()) { for (const auto& zone_name : config_["timezones"]) { if (!zone_name.isString()) continue; if (zone_name.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(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, add current time zone. nullptr in timezones - // vector means that local time should be shown - 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(); - 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{local_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()) { + cldShift_ = config_[kCldPlaceholder]["on-scroll"].asInt(); 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(""); + if (tooltipEnabled()) { + label_.set_has_tooltip(true); + label_.signal_query_tooltip().connect(sigc::mem_fun(*this, &Clock::query_tlp_cb)); + } 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_] ? time_zones_[current_time_zone_idx_] - : date::current_zone(); -} - -bool waybar::modules::Clock::is_timezone_fixed() { - return time_zones_[current_time_zone_idx_] != nullptr; +bool waybar::modules::Clock::query_tlp_cb(int, int, bool, + const Glib::RefPtr& tooltip) { + tooltip->set_custom(*m_tooltip_.get()); + return true; } auto waybar::modules::Clock::update() -> void { - const auto* time_zone = current_timezone(); - auto now = std::chrono::system_clock::now(); - auto ztime = date::zoned_time{time_zone, date::floor(now)}; + const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : local_zone(); + const zoned_time now{tz, floor(system_clock::now())}; - auto shifted_date = date::year_month_day{date::floor(now)} + cldCurrShift_; - if (cldCurrShift_.count()) { - shifted_date = date::year_month_day(shifted_date.year(), shifted_date.month(), date::day(1)); - } - auto now_shifted = date::sys_days{shifted_date} + (now - date::floor(now)); - auto shifted_ztime = date::zoned_time{time_zone, date::floor(now_shifted)}; - - std::string text{""}; - if (!is_timezone_fixed()) { - // As date dep is not fully compatible, prefer fmt - tzset(); - auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now)); - text = fmt::format(locale_, fmt::runtime(format_), localtime); - } else { - text = fmt::format(locale_, fmt::runtime(format_), ztime); - } - label_.set_markup(text); + label_.set_markup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now))); if (tooltipEnabled()) { - if (config_["tooltip-format"].isString()) { - std::string calendar_lines{""}; - std::string timezoned_time_lines{""}; - if (is_calendar_in_tooltip_) { - calendar_lines = get_calendar(ztime, shifted_ztime); - } - if (is_timezoned_list_in_tooltip_) { - timezoned_time_lines = timezones_text(&now); - } - auto tooltip_format = config_["tooltip-format"].asString(); - text = fmt::format(locale_, fmt::runtime(tooltip_format), shifted_ztime, - fmt::arg(kCalendarPlaceholder.c_str(), calendar_lines), - fmt::arg(KTimezonedTimeListPlaceholder.c_str(), timezoned_time_lines)); - label_.set_tooltip_markup(text); + 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()))}; + + 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. + m_tlpText_ = + std::regex_replace(m_tlpFmt_, std::regex("\\{" + kTZPlaceholder + "\\}"), tzText_); + m_tlpText_ = std::regex_replace( + m_tlpText_, std::regex("\\{" + kCldPlaceholder + "\\}"), + fmt_lib::vformat(m_locale_, cldText_, fmt_lib::make_format_args(shiftedNow))); + m_tlpText_ = + std::regex_replace(m_tlpText_, std::regex("\\{" + kOrdPlaceholder + "\\}"), ordText_); + } else { + m_tlpText_ = m_tlpFmt_; } + + m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(now)); + m_tooltip_->set_markup(m_tlpText_); + label_.trigger_tooltip_query(); } - // 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] : local_zone(); + auto zt{zoned_time{tz, now}}; + os << fmt_lib::vformat(m_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 -unsigned cldRowsInMonth(date::year_month const ym, date::weekday const firstdow) { - using namespace date; - return static_cast( - ceil((weekday{ym / 1} - firstdow) + ((ym / last).day() - day{0})).count()) + - 2; +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(date::year_month const ym, date::weekday const firstdow, unsigned const line) - -> const date::year_month_weekday { - unsigned index = line - 2; - auto sd = date::sys_days{ym / 1}; - if (date::weekday{sd} == firstdow) ++index; - auto ymdw = ym / firstdow[index]; - return ymdw; +auto 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(date::year_month_day const currDate, date::year_month const ym, - unsigned const line, date::weekday const firstdow, - const std::locale* const locale_) -> std::string { - using namespace date::literals; - std::ostringstream res; +auto getCalendarLine(const year_month_day& currDate, const year_month ym, const unsigned line, + const weekday& firstdow, const std::locale* const m_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(*m_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(*m_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(*m_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(*m_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(*m_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(*m_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::zoned_seconds& now, - const date::zoned_seconds& wtime) -> std::string { - auto daypoint = date::floor(wtime.get_local_time()); - const auto ymd{date::year_month_day{daypoint}}; +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; - // get currdate - daypoint = date::floor(now.get_local_time()); - const auto currDate{date::year_month_day{daypoint}}; if (cldMode_ == CldMode::YEAR) { - if (y / date::month{1} / 1 == cldYearShift_) + if (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_) @@ -350,64 +321,88 @@ auto waybar::modules::Clock::get_calendar(const date::zoned_seconds& now, 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::sys_days{ymTmp / 1} - : date::sys_days{cldGetWeekForLine(ymTmp, firstdow, line)}) + if (line < ml[(unsigned)ymTmp.month() - 1u]) { + os << fmt_lib::vformat( + m_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(currDate, ymTmp, line, firstdow, &locale_), - (cldMonColLen_ + ((line < 2) ? cldWnLen_ : 0))); + // Count wide characters to avoid extra padding + size_t wideCharCount = 0; + std::string calendarLine = getCalendarLine(today, ymTmp, line, firstdow, &m_locale_); + if (line < 2) { + for (gchar *data = calendarLine.data(), *end = data + calendarLine.size(); + data != nullptr;) { + gunichar c = g_utf8_get_char_validated(data, end - data); + if (g_unichar_iswide(c)) { + wideCharCount++; + } + data = g_utf8_find_next_char(data, end); + } + } + os << Glib::ustring::format( + (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(L' '), + std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), + calendarLine); // Week numbers on the right - if (cldWPos_ == 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::sys_days{ymTmp / 1} - : date::sys_days{cldGetWeekForLine(ymTmp, firstdow, line)}); + << fmt_lib::vformat( + m_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( + m_locale_, fmtMap_[line], + fmt_lib::make_format_args(static_cast(os.str()))); else tmp << os.str(); // Clear ostringstream @@ -417,10 +412,13 @@ auto waybar::modules::Clock::get_calendar(const date::zoned_seconds& now, if (row + 1u != maxRows && cldMode_ == CldMode::YEAR) tmp << '\n'; } - os << fmt::format( // Apply days format - fmt::runtime(fmt::format(fmt::runtime(fmtMap_[2]), tmp.str())), - // Apply today format - fmt::arg("today", fmt::format(fmt::runtime(fmtMap_[3]), date::format("%e", ymd.day())))); + os << std::regex_replace( + fmt_lib::vformat(m_locale_, fmtMap_[2], + fmt_lib::make_format_args(static_cast(tmp.str()))), + std::regex("\\{today\\}"), + fmt_lib::vformat(m_locale_, fmtMap_[3], + fmt_lib::make_format_args( + static_cast(date::format("{:L%e}", d))))); if (cldMode_ == CldMode::YEAR) cldYearCached_ = os.str(); @@ -430,50 +428,47 @@ auto waybar::modules::Clock::get_calendar(const date::zoned_seconds& now, return os.str(); } -/*Clock actions*/ +auto waybar::modules::Clock::local_zone() -> const time_zone* { + const char* tz_name = getenv("TZ"); + if (tz_name) { + try { + return locate_zone(tz_name); + } catch (const std::runtime_error& e) { + spdlog::warn("Timezone: {0}. {1}", tz_name, e.what()); + } + } + return current_zone(); +} + +// Actions handler +auto waybar::modules::Clock::doAction(const std::string& name) -> void { + if (actionMap_[name]) { + (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) * cldShift_; } void waybar::modules::Clock::cldShift_down() { - cldCurrShift_ -= ((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_; + cldCurrShift_ -= (months)((cldMode_ == CldMode::YEAR) ? 12 : 1) * cldShift_; } +void waybar::modules::Clock::cldShift_reset() { cldCurrShift_ = (months)0; } 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 @@ -485,17 +480,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)}; + newlocale(LC_ALL, m_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(); } 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..e3947967 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,45 @@ 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; + + if (curr_times.size() != prev_times.size()) { + // The number of CPUs has changed, eg. due to CPU hotplug + // We don't know which CPU came up or went down + // so only give total usage (if we can) + if (!curr_times.empty() && !prev_times.empty()) { + auto [curr_idle, curr_total] = curr_times[0]; + auto [prev_idle, prev_total] = prev_times[0]; + 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); + tooltip = fmt::format("Total: {}%\nCores: (pending)", tmp); + usage.push_back(tmp); + } else { + tooltip = "(pending)"; + usage.push_back(0); + } + prev_times = curr_times; + return {usage, tooltip}; + } + 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]; + if (i > 0 && (curr_total == 0 || prev_total == 0)) { + // This CPU is offline + tooltip = tooltip + fmt::format("\nCore{}: offline", i - 1); + usage.push_back(0); + continue; + } 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 +101,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..6fbd659b --- /dev/null +++ b/src/modules/cpu_usage/linux.cpp @@ -0,0 +1,66 @@ +#include + +#include "modules/cpu_usage.hpp" + +std::vector> waybar::modules::CpuUsage::parseCpuinfo() { + // Get the "existing CPU count" from /sys/devices/system/cpu/present + // Probably this is what the user wants the offline CPUs accounted from + // For further details see: + // https://www.kernel.org/doc/html/latest/core-api/cpu_hotplug.html + const std::string sys_cpu_present_path = "/sys/devices/system/cpu/present"; + size_t cpu_present_last = 0; + std::ifstream cpu_present_file(sys_cpu_present_path); + std::string cpu_present_text; + if (cpu_present_file.is_open()) { + getline(cpu_present_file, cpu_present_text); + // This is a comma-separated list of ranges, eg. 0,2-4,7 + size_t last_separator = cpu_present_text.find_last_of("-,"); + if (last_separator < cpu_present_text.size()) { + std::stringstream(cpu_present_text.substr(last_separator + 1)) >> cpu_present_last; + } + } + + 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; + size_t current_cpu_number = -1; // First line is total, second line is cpu 0 + while (getline(info, line)) { + if (line.substr(0, 3).compare("cpu") != 0) { + break; + } + size_t line_cpu_number; + if (current_cpu_number >= 0) { + std::stringstream(line.substr(3)) >> line_cpu_number; + while (line_cpu_number > current_cpu_number) { + // Fill in 0 for offline CPUs missing inside the lines of /proc/stat + cpuinfo.emplace_back(0, 0); + current_cpu_number++; + } + } + 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); + current_cpu_number++; + } + + while (cpu_present_last >= current_cpu_number) { + // Fill in 0 for offline CPUs missing after the lines of /proc/stat + cpuinfo.emplace_back(0, 0); + current_cpu_number++; + } + + return cpuinfo; +} diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index b4c77d1b..3179e2bc 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -2,16 +2,23 @@ #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), + tooltip_format_enabled_{config_["tooltip-format"].isString()}, 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(); @@ -46,7 +53,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(); } @@ -57,12 +64,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; @@ -78,7 +90,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); } @@ -96,7 +108,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(); }; } @@ -135,35 +166,53 @@ 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_)); - if (str.empty()) { - event_box_.hide(); - } else { - label_.set_markup(str); - if (tooltipEnabled()) { - if (text_ == tooltip_) { - if (label_.get_tooltip_markup() != str) { - label_.set_tooltip_markup(str); - } - } else { - if (label_.get_tooltip_markup() != tooltip_) { - label_.set_tooltip_markup(tooltip_); + + try { + auto str = fmt::format(fmt::runtime(format_), fmt::arg("text", text_), fmt::arg("alt", alt_), + fmt::arg("icon", getIcon(percentage_, alt_)), + fmt::arg("percentage", percentage_)); + if ((config_["hide-empty-text"].asBool() && text_.empty()) || str.empty()) { + event_box_.hide(); + } else { + label_.set_markup(str); + if (tooltipEnabled()) { + if (tooltip_format_enabled_) { + auto tooltip = config_["tooltip-format"].asString(); + tooltip = fmt::format( + fmt::runtime(tooltip), fmt::arg("text", text_), fmt::arg("alt", alt_), + fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); + label_.set_tooltip_markup(tooltip); + } else if (text_ == tooltip_) { + if (label_.get_tooltip_markup() != str) { + label_.set_tooltip_markup(str); + } + } else { + if (label_.get_tooltip_markup() != tooltip_) { + label_.set_tooltip_markup(tooltip_); + } } } + auto style = label_.get_style_context(); + auto classes = style->list_classes(); + for (auto const& c : classes) { + if (c == id_) continue; + style->remove_class(c); + } + for (auto const& c : class_) { + style->add_class(c); + } + style->add_class("flat"); + style->add_class("text-button"); + style->add_class(MODULE_CLASS); + event_box_.show(); } - auto classes = label_.get_style_context()->list_classes(); - for (auto const& c : classes) { - if (c == id_) continue; - label_.get_style_context()->remove_class(c); - } - for (auto const& c : class_) { - label_.get_style_context()->add_class(c); - } - label_.get_style_context()->add_class("flat"); - label_.get_style_context()->add_class("text-button"); - event_box_.show(); + } catch (const fmt::format_error& e) { + if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) + throw; + + throw fmt::format_error( + "mixing manual and automatic argument indexing is no longer supported; " + "try replacing \"{}\" with \"{text}\" in your format specifier"); } } // Call parent update @@ -175,18 +224,29 @@ 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); + tooltip_ = Glib::Markup::escape_text(validated_line); } else { - text_ = line; + text_ = validated_line; + tooltip_ = validated_line; } - tooltip_ = line; + tooltip_ = validated_line; class_.clear(); } else if (i == 1) { - tooltip_ = line; + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(validated_line); + } else { + tooltip_ = validated_line; + } } else if (i == 2) { - class_.push_back(line); + class_.push_back(validated_line); } else { break; } @@ -210,7 +270,11 @@ void waybar::modules::Custom::parseOutputJson() { } else { alt_ = parsed["alt"].asString(); } - tooltip_ = parsed["tooltip"].asString(); + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString()); + } else { + tooltip_ = parsed["tooltip"].asString(); + } if (parsed["class"].isString()) { class_.push_back(parsed["class"].asString()); } else if (parsed["class"].isArray()) { diff --git a/src/modules/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..f8b250c8 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -21,11 +21,11 @@ wl_array tags, layouts; static uint num_tags = 0; -void toggle_visibility(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { +static void toggle_visibility(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { // Intentionally empty } -void active(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t active) { +static void active(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t active) { // Intentionally empty } @@ -37,15 +37,15 @@ static void set_tag(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t tag : num_tags & ~(1 << tag); } -void set_layout_symbol(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *layout) { +static void set_layout_symbol(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *layout) { // Intentionally empty } -void title(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *title) { +static void title(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *title) { // Intentionally empty } -void dwl_frame(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { +static void dwl_frame(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { // Intentionally empty } @@ -53,8 +53,8 @@ static void set_layout(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t // Intentionally empty } -static void appid(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *appid){ - // Intentionally empty +static void appid(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *appid) { + // Intentionally empty }; static const zdwl_ipc_output_v2_listener output_status_listener_impl{ @@ -93,10 +93,11 @@ 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); + wl_registry_add_listener(registry, ®istry_listener_impl, this); wl_display_roundtrip(display); @@ -113,6 +114,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 @@ -154,6 +156,9 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con } Tags::~Tags() { + if (output_status_) { + zdwl_ipc_output_v2_destroy(output_status_); + } if (status_manager_) { zdwl_ipc_manager_v2_destroy(status_manager_); } diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp new file mode 100644 index 00000000..a960a1f0 --- /dev/null +++ b/src/modules/dwl/window.cpp @@ -0,0 +1,123 @@ +#include "modules/dwl/window.hpp" + +#include +#include +#include +#include +#include +#include + +#include "client.hpp" +#include "dwl-ipc-unstable-v2-client-protocol.h" +#include "glibmm/markup.h" +#include "util/rewrite_string.hpp" + +namespace waybar::modules::dwl { + +static void toggle_visibility(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { + // Intentionally empty +} + +static void active(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t active) { + // Intentionally empty +} + +static void set_tag(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t tag, uint32_t state, + uint32_t clients, uint32_t focused) { + // Intentionally empty +} + +static void set_layout_symbol(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *layout) { + static_cast(data)->handle_layout_symbol(layout); +} + +static void title(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *title) { + static_cast(data)->handle_title(title); +} + +static void dwl_frame(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { + static_cast(data)->handle_frame(); +} + +static void set_layout(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t layout) { + static_cast(data)->handle_layout(layout); +} + +static void appid(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, const char *appid) { + static_cast(data)->handle_appid(appid); +}; + +static const zdwl_ipc_output_v2_listener output_status_listener_impl{ + .toggle_visibility = toggle_visibility, + .active = active, + .tag = set_tag, + .layout = set_layout, + .title = title, + .appid = appid, + .layout_symbol = set_layout_symbol, + .frame = dwl_frame, +}; + +static void handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) { + if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) { + static_cast(data)->status_manager_ = static_cast( + (zdwl_ipc_manager_v2 *)wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1)); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { + /* Ignore event */ +} + +static const wl_registry_listener registry_listener_impl = {.global = handle_global, + .global_remove = handle_global_remove}; + +Window::Window(const std::string &id, const Bar &bar, const Json::Value &config) + : AAppIconLabel(config, "window", id, "{}", 0, true), bar_(bar) { + struct wl_display *display = Client::inst()->wl_display; + struct wl_registry *registry = wl_display_get_registry(display); + + wl_registry_add_listener(registry, ®istry_listener_impl, this); + wl_display_roundtrip(display); + + if (status_manager_ == nullptr) { + spdlog::error("dwl_status_manager_v2 not advertised"); + return; + } + + struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + output_status_ = zdwl_ipc_manager_v2_get_output(status_manager_, output); + zdwl_ipc_output_v2_add_listener(output_status_, &output_status_listener_impl, this); + zdwl_ipc_manager_v2_destroy(status_manager_); +} + +Window::~Window() { + if (output_status_ != nullptr) { + zdwl_ipc_output_v2_destroy(output_status_); + } +} + +void Window::handle_title(const char *title) { title_ = Glib::Markup::escape_text(title); } + +void Window::handle_appid(const char *appid) { appid_ = Glib::Markup::escape_text(appid); } + +void Window::handle_layout_symbol(const char *layout_symbol) { + layout_symbol_ = Glib::Markup::escape_text(layout_symbol); +} + +void Window::handle_layout(const uint32_t layout) { layout_ = layout; } + +void Window::handle_frame() { + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", title_), + fmt::arg("layout", layout_symbol_), fmt::arg("app_id", appid_)), + config_["rewrite"])); + updateAppIconName(appid_, ""); + updateAppIcon(); + if (tooltipEnabled()) { + label_.set_tooltip_text(title_); + } +} + +} // namespace waybar::modules::dwl diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 79bc6375..2bd3b509 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -1,99 +1,145 @@ #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 { -void IPC::startIPC() { +std::filesystem::path IPC::socketFolder_; + +std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { + static std::mutex folderMutex; + std::unique_lock lock(folderMutex); + + // socket path, specified by EventManager of Hyprland + if (!socketFolder_.empty()) { + return socketFolder_; + } + + const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR"); + std::filesystem::path xdgRuntimeDir; + // Only set path if env variable is set + if (xdgRuntimeDirEnv != nullptr) { + xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv); + } + + if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) { + socketFolder_ = xdgRuntimeDir / "hypr"; + } else { + spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr"); + socketFolder_ = std::filesystem::path("/tmp") / "hypr"; + } + + socketFolder_ = socketFolder_ / instanceSig; + return socketFolder_; +} + +IPC::IPC() { // will start IPC and relay events to parseIPC + ipcThread_ = std::thread([this]() { socketListener(); }); +} - std::thread([&]() { - // check for hyprland - const char* HIS = getenv("HYPRLAND_INSTANCE_SIGNATURE"); - - if (!HIS) { - spdlog::warn("Hyprland is not running, Hyprland IPC will not be available."); - return; +IPC::~IPC() { + running_ = false; + spdlog::info("Hyprland IPC stopping..."); + if (socketfd_ != -1) { + spdlog::trace("Shutting down socket"); + if (shutdown(socketfd_, SHUT_RDWR) == -1) { + spdlog::error("Hyprland IPC: Couldn't shutdown socket"); } - - if (!modulesReady) return; - - spdlog::info("Hyprland IPC starting"); - - struct sockaddr_un addr; - int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); - - if (socketfd == -1) { - spdlog::error("Hyprland IPC: socketfd failed"); - return; + spdlog::trace("Closing socket"); + if (close(socketfd_) == -1) { + spdlog::error("Hyprland IPC: Couldn't close socket"); } + } + ipcThread_.join(); +} - addr.sun_family = AF_UNIX; +IPC& IPC::inst() { + static IPC ipc; + return ipc; +} - // socket path - std::string socketPath = "/tmp/hypr/" + std::string(HIS) + "/.socket2.sock"; +void IPC::socketListener() { + // check for hyprland + const char* his = getenv("HYPRLAND_INSTANCE_SIGNATURE"); - strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); + if (his == nullptr) { + spdlog::warn("Hyprland is not running, Hyprland IPC will not be available."); + return; + } - addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + if (!modulesReady) return; - int l = sizeof(struct sockaddr_un); + spdlog::info("Hyprland IPC starting"); - if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { - spdlog::error("Hyprland IPC: Unable to connect?"); - return; - } + struct sockaddr_un addr; + socketfd_ = socket(AF_UNIX, SOCK_STREAM, 0); - auto file = fdopen(socketfd, "r"); + if (socketfd_ == -1) { + spdlog::error("Hyprland IPC: socketfd failed"); + return; + } - while (1) { - // read + addr.sun_family = AF_UNIX; - char buffer[1024]; // Hyprland socket2 events are max 1024 bytes - auto recievedCharPtr = fgets(buffer, 1024, file); + auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock"; + strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); - if (!recievedCharPtr) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - continue; - } + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; - callbackMutex.lock(); + int l = sizeof(struct sockaddr_un); - std::string messageRecieved(buffer); + if (connect(socketfd_, (struct sockaddr*)&addr, l) == -1) { + spdlog::error("Hyprland IPC: Unable to connect?"); + return; + } + auto* file = fdopen(socketfd_, "r"); + if (file == nullptr) { + spdlog::error("Hyprland IPC: Couldn't open file descriptor"); + return; + } + while (running_) { + std::array buffer; // Hyprland socket2 events are max 1024 bytes - messageRecieved = messageRecieved.substr(0, messageRecieved.find_first_of('\n')); - - spdlog::debug("hyprland IPC received {}", messageRecieved); - - parseIPC(messageRecieved); - - callbackMutex.unlock(); + auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file); + if (receivedCharPtr == nullptr) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; } - }).detach(); + + std::string messageReceived(buffer.data()); + messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n')); + spdlog::debug("hyprland IPC received {}", messageReceived); + + try { + parseIPC(messageReceived); + } catch (std::exception& e) { + spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what()); + } catch (...) { + throw; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + spdlog::debug("Hyprland IPC stopped"); } 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,105 +147,97 @@ 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); + const auto serverSocket = socket(AF_UNIX, SOCK_STREAM, 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; - - if (getaddrinfo("localhost", NULL, &ai_hints, &ai_res) != 0) { - spdlog::error("Hyprland IPC: Couldn't get host (2)"); - return ""; + if (serverSocket < 0) { + throw std::runtime_error("Hyprland IPC: Couldn't open a socket (1)"); } // get the instance signature - auto instanceSig = getenv("HYPRLAND_INSTANCE_SIGNATURE"); + auto* instanceSig = getenv("HYPRLAND_INSTANCE_SIGNATURE"); - if (!instanceSig) { - spdlog::error("Hyprland IPC: HYPRLAND_INSTANCE_SIGNATURE was not set! (Is Hyprland running?)"); - return ""; + if (instanceSig == nullptr) { + throw std::runtime_error( + "Hyprland IPC: HYPRLAND_INSTANCE_SIGNATURE was not set! (Is Hyprland running?)"); } - std::string instanceSigStr = std::string(instanceSig); - sockaddr_un serverAddress = {0}; serverAddress.sun_family = AF_UNIX; - std::string socketPath = "/tmp/hypr/" + instanceSigStr + "/.socket.sock"; + std::string socketPath = IPC::getSocketFolder(instanceSig) / ".socket.sock"; - strcpy(serverAddress.sun_path, socketPath.c_str()); - - if (connect(SERVERSOCKET, (sockaddr*)&serverAddress, SUN_LEN(&serverAddress)) < 0) { - spdlog::error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); - return ""; + // 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) { + throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)"); } - auto sizeWritten = write(SERVERSOCKET, rq.c_str(), rq.length()); + if (connect(serverSocket, reinterpret_cast(&serverAddress), sizeof(serverAddress)) < + 0) { + throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); + } + + 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); - } while (sizeWritten == 8192); + response.append(buffer.data(), sizeWritten); + } while (sizeWritten > 0); - close(SERVERSOCKET); + close(serverSocket); return response; } Json::Value IPC::getSocket1JsonReply(const std::string& rq) { - return parser_.parse(getSocket1Reply("j/" + rq)); + std::string reply = getSocket1Reply("j/" + rq); + + if (reply.empty()) { + return {}; + } + + return parser_.parse(reply); } } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index aa22a482..da56e578 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -4,20 +4,15 @@ #include #include -#include - +#include "util/sanitize_str.hpp" #include "util/string.hpp" namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { + : ALabel(config, "language", id, "{}", 0, true), bar_(bar), m_ipc(IPC::inst()) { modulesReady = true; - if (!gIPC.get()) { - gIPC = std::make_unique(); - } - // get the active layout when open initLanguage(); @@ -25,11 +20,11 @@ Language::Language(const std::string& id, const Bar& bar, const Json::Value& con update(); // register for hyprland ipc - gIPC->registerForIPC("activelayout", this); + m_ipc.registerForIPC("activelayout", this); } Language::~Language() { - gIPC->unregisterForIPC(this); + m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(mutex_); } @@ -37,8 +32,16 @@ Language::~Language() { auto Language::update() -> void { std::lock_guard lg(mutex_); + spdlog::debug("hyprland language update with full name {}", layout_.full_name); + spdlog::debug("hyprland language update with short name {}", layout_.short_name); + spdlog::debug("hyprland language update with short description {}", layout_.short_description); + spdlog::debug("hyprland language update with variant {}", layout_.variant); + std::string layoutName = std::string{}; - if (config_.isMember("format-" + layout_.short_description)) { + if (config_.isMember("format-" + layout_.short_description + "-" + layout_.variant)) { + const auto propName = "format-" + layout_.short_description + "-" + layout_.variant; + layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString()); + } else if (config_.isMember("format-" + layout_.short_description)) { const auto propName = "format-" + layout_.short_description; layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString()); } else { @@ -48,6 +51,8 @@ auto Language::update() -> void { fmt::arg("variant", layout_.variant))); } + spdlog::debug("hyprland language formatted layout name {}", layoutName); + if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); @@ -61,7 +66,7 @@ auto Language::update() -> void { void Language::onEvent(const std::string& ev) { std::lock_guard lg(mutex_); std::string kbName(begin(ev) + ev.find_last_of('>') + 1, begin(ev) + ev.find_first_of(',')); - auto layoutName = ev.substr(ev.find_first_of(',') + 1); + auto layoutName = ev.substr(ev.find_last_of(',') + 1); if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) return; // ignore @@ -76,7 +81,7 @@ void Language::onEvent(const std::string& ev) { } void Language::initLanguage() { - const auto inputDevices = gIPC->getSocket1Reply("devices"); + const auto inputDevices = m_ipc.getSocket1Reply("devices"); const auto kbName = config_["keyboard-name"].asString(); @@ -94,18 +99,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) { @@ -114,21 +118,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..d1060447 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -2,32 +2,49 @@ #include -#include +#include "util/sanitize_str.hpp" namespace waybar::modules::hyprland { Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "submap", id, "{}", 0, true), bar_(bar) { + : ALabel(config, "submap", id, "{}", 0, true), bar_(bar), m_ipc(IPC::inst()) { modulesReady = true; - if (!gIPC.get()) { - gIPC = std::make_unique(); - } + parseConfig(config); label_.hide(); ALabel::update(); + // Displays widget immediately if always_on_ assuming default submap + // Needs an actual way to retrieve current submap on startup + if (always_on_) { + submap_ = default_submap_; + label_.get_style_context()->add_class(submap_); + } + // register for hyprland ipc - gIPC->registerForIPC("submap", this); + m_ipc.registerForIPC("submap", this); dp.emit(); } Submap::~Submap() { - gIPC->unregisterForIPC(this); + m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(mutex_); } +auto Submap::parseConfig(const Json::Value& config) -> void { + auto const& alwaysOn = config["always-on"]; + if (alwaysOn.isBool()) { + always_on_ = alwaysOn.asBool(); + } + + auto const& defaultSubmap = config["default-submap"]; + if (defaultSubmap.isString()) { + default_submap_ = defaultSubmap.asString(); + } +} + auto Submap::update() -> void { std::lock_guard lg(mutex_); @@ -51,11 +68,20 @@ void Submap::onEvent(const std::string& ev) { return; } - auto submapName = ev.substr(ev.find_last_of('>') + 1); - submapName = waybar::util::sanitize_string(submapName); + auto submapName = ev.substr(ev.find_first_of('>') + 2 ); + + if (!submap_.empty()) { + label_.get_style_context()->remove_class(submap_); + } submap_ = submapName; + if (submap_.empty() && always_on_) { + submap_ = default_submap_; + } + + 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 cb820bcd..815fbad8 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -1,161 +1,238 @@ #include "modules/hyprland/window.hpp" +#include +#include +#include #include #include -#include -#include +#include #include #include "modules/hyprland/backend.hpp" -#include "util/json.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) - : ALabel(config, "window", id, "{}", 0, true), bar_(bar) { - modulesReady = true; - separate_outputs = config["separate-outputs"].asBool(); +std::shared_mutex windowIpcSmtx; - if (!gIPC.get()) { - gIPC = std::make_unique(); - } +Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) + : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), m_ipc(IPC::inst()) { + std::unique_lock windowIpcUniqueLock(windowIpcSmtx); + + modulesReady = true; + separateOutputs_ = config["separate-outputs"].asBool(); + + // register for hyprland ipc + m_ipc.registerForIPC("activewindow", this); + m_ipc.registerForIPC("closewindow", this); + m_ipc.registerForIPC("movewindow", this); + m_ipc.registerForIPC("changefloatingmode", this); + m_ipc.registerForIPC("fullscreen", this); + + windowIpcUniqueLock.unlock(); queryActiveWorkspace(); update(); - - // register for hyprland ipc - gIPC->registerForIPC("activewindow", this); - gIPC->registerForIPC("closewindow", this); - gIPC->registerForIPC("movewindow", this); - gIPC->registerForIPC("changefloatingmode", this); - gIPC->registerForIPC("fullscreen", this); + dp.emit(); } Window::~Window() { - gIPC->unregisterForIPC(this); - // wait for possible event handler to finish - std::lock_guard lg(mutex_); + std::unique_lock windowIpcUniqueLock(windowIpcSmtx); + m_ipc.unregisterForIPC(this); } auto Window::update() -> void { - // fix ampersands - std::lock_guard lg(mutex_); + std::shared_lock windowIpcShareLock(windowIpcSmtx); - std::string window_name = waybar::util::sanitize_string(workspace_.last_window_title); + std::string windowName = waybar::util::sanitize_string(workspace_.last_window_title); + std::string windowAddress = workspace_.last_window; - if (window_name != last_title_) { - if (window_name.empty()) { - label_.get_style_context()->add_class("empty"); - } else { - label_.get_style_context()->remove_class("empty"); - } - last_title_ = window_name; - } + windowData_.title = windowName; + std::string label_text; if (!format_.empty()) { label_.show(); - label_.set_markup(fmt::format(fmt::runtime(format_), - waybar::util::rewriteString(window_name, config_["rewrite"]))); + label_text = waybar::util::rewriteString( + 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"]); + label_.set_markup(label_text); } else { label_.hide(); } - setClass("empty", workspace_.windows == 0); - setClass("solo", solo_); - setClass("fullscreen", fullscreen_); - setClass("floating", all_floating_); - - if (!last_solo_class_.empty() && solo_class_ != last_solo_class_) { - if (bar_.window.get_style_context()->has_class(last_solo_class_)) { - bar_.window.get_style_context()->remove_class(last_solo_class_); - spdlog::trace("Removing solo class: {}", last_solo_class_); + if (tooltipEnabled()) { + std::string tooltip_format; + if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + label_.set_tooltip_text( + fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName), + fmt::arg("initialTitle", windowData_.initial_title), + fmt::arg("class", windowData_.class_name), + fmt::arg("initialClass", windowData_.initial_class_name))); + } else if (!label_text.empty()) { + label_.set_tooltip_text(label_text); } } - 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 (focused_) { + image_.show(); + } else { + image_.hide(); } - last_solo_class_ = solo_class_; - ALabel::update(); + setClass("empty", workspace_.windows == 0); + setClass("solo", solo_); + setClass("floating", allFloating_); + setClass("swallowing", swallowing_); + setClass("fullscreen", fullscreen_); + + 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 (!soloClass_.empty() && soloClass_ != lastSoloClass_) { + bar_.window.get_style_context()->add_class(soloClass_); + spdlog::trace("Adding solo class: {}", soloClass_); + } + lastSoloClass_ = soloClass_; + + AAppIconLabel::update(); } auto Window::getActiveWorkspace() -> Workspace { - const auto workspace = gIPC->getSocket1JsonReply("activeworkspace"); - assert(workspace.isObject()); - return Workspace::parse(workspace); + const auto workspace = IPC::inst().getSocket1JsonReply("activeworkspace"); + + if (workspace.isObject()) { + return Workspace::parse(workspace); + } + + return {}; } auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { - const auto monitors = gIPC->getSocket1JsonReply("monitors"); - assert(monitors.isArray()); - auto monitor = std::find_if(monitors.begin(), monitors.end(), - [&](Json::Value monitor) { return monitor["name"] == monitorName; }); - if (monitor == std::end(monitors)) { - spdlog::warn("Monitor not found: {}", monitorName); - return Workspace{-1, 0, "", ""}; - } - const int id = (*monitor)["activeWorkspace"]["id"].asInt(); + const auto monitors = IPC::inst().getSocket1JsonReply("monitors"); + if (monitors.isArray()) { + auto monitor = std::ranges::find_if( + monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + if (monitor == std::end(monitors)) { + spdlog::warn("Monitor not found: {}", monitorName); + return Workspace{ + .id = -1, + .windows = 0, + .last_window = "", + .last_window_title = "", + }; + } + const int id = (*monitor)["activeWorkspace"]["id"].asInt(); - const auto workspaces = gIPC->getSocket1JsonReply("workspaces"); - assert(workspaces.isArray()); - auto workspace = std::find_if(monitors.begin(), monitors.end(), - [&](Json::Value workspace) { return workspace["id"] == id; }); - if (workspace == std::end(monitors)) { - spdlog::warn("No workspace with id {}", id); - return Workspace{-1, 0, "", ""}; - } - return Workspace::parse(*workspace); + const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces"); + if (workspaces.isArray()) { + auto workspace = std::ranges::find_if( + workspaces, [&](Json::Value workspace) { return workspace["id"] == id; }); + if (workspace == std::end(workspaces)) { + spdlog::warn("No workspace with id {}", id); + return Workspace{ + .id = -1, + .windows = 0, + .last_window = "", + .last_window_title = "", + }; + } + return Workspace::parse(*workspace); + }; + }; + + return {}; } 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{ + .id = value["id"].asInt(), + .windows = value["windows"].asInt(), + .last_window = value["lastwindow"].asString(), + .last_window_title = value["lastwindowtitle"].asString(), + }; +} + +auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData { + return WindowData{.floating = value["floating"].asBool(), + .monitor = value["monitor"].asInt(), + .class_name = value["class"].asString(), + .initial_class_name = value["initialClass"].asString(), + .title = value["title"].asString(), + .initial_title = value["initialTitle"].asString(), + .fullscreen = value["fullscreen"].asBool(), + .grouped = !value["grouped"].empty()}; } void Window::queryActiveWorkspace() { - std::lock_guard lg(mutex_); + std::shared_lock windowIpcShareLock(windowIpcSmtx); - if (separate_outputs) { + if (separateOutputs_) { workspace_ = getActiveWorkspace(this->bar_.output->name); } else { workspace_ = getActiveWorkspace(); } + focused_ = true; if (workspace_.windows > 0) { - const auto clients = gIPC->getSocket1Reply("j/clients"); - Json::Value json = parser_.parse(clients); - assert(json.isArray()); - auto active_window = std::find_if(json.begin(), json.end(), [&](Json::Value window) { - return window["address"] == workspace_.last_window; - }); - if (active_window == std::end(json)) { - return; - } + const auto clients = m_ipc.getSocket1JsonReply("clients"); + if (clients.isArray()) { + auto activeWindow = std::ranges::find_if( + clients, [&](Json::Value window) { return window["address"] == workspace_.last_window; }); - if (workspace_.windows == 1 && !(*active_window)["floating"].asBool()) { - solo_class_ = (*active_window)["class"].asString(); - } else { - solo_class_ = ""; + if (activeWindow == std::end(clients)) { + focused_ = false; + return; + } + + windowData_ = WindowData::parse(*activeWindow); + updateAppIconName(windowData_.class_name, windowData_.initial_class_name); + std::vector workspaceWindows; + std::ranges::copy_if(clients, std::back_inserter(workspaceWindows), [&](Json::Value window) { + return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); + }); + swallowing_ = std::ranges::any_of(workspaceWindows, [&](Json::Value window) { + return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; + }); + std::vector visibleWindows; + std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), + [&](Json::Value window) { return !window["hidden"].asBool(); }); + solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(), + [&](Json::Value window) { return !window["floating"].asBool(); }); + allFloating_ = std::ranges::all_of( + visibleWindows, [&](Json::Value window) { return window["floating"].asBool(); }); + fullscreen_ = windowData_.fullscreen; + + // Fullscreen windows look like they are solo + if (fullscreen_) { + solo_ = true; + } + + if (solo_) { + soloClass_ = windowData_.class_name; + } else { + soloClass_ = ""; + } } - std::vector workspace_windows; - std::copy_if(json.begin(), json.end(), std::back_inserter(workspace_windows), - [&](Json::Value window) { - return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); - }); - solo_ = 1 == std::count_if(workspace_windows.begin(), workspace_windows.end(), - [&](Json::Value window) { return !window["floating"].asBool(); }); - all_floating_ = std::all_of(workspace_windows.begin(), workspace_windows.end(), - [&](Json::Value window) { return window["floating"].asBool(); }); - fullscreen_ = (*active_window)["fullscreen"].asBool(); } else { - solo_class_ = ""; - solo_ = false; - all_floating_ = false; + focused_ = false; + windowData_ = WindowData{}; + allFloating_ = false; + swallowing_ = false; fullscreen_ = false; + solo_ = false; + soloClass_ = ""; } } diff --git a/src/modules/hyprland/windowcreationpayload.cpp b/src/modules/hyprland/windowcreationpayload.cpp new file mode 100644 index 00000000..2a62ac12 --- /dev/null +++ b/src/modules/hyprland/windowcreationpayload.cpp @@ -0,0 +1,108 @@ +#include "modules/hyprland/windowcreationpayload.hpp" + +#include +#include + +#include +#include +#include + +#include "modules/hyprland/workspaces.hpp" + +namespace waybar::modules::hyprland { + +WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data) + : m_window(std::make_pair(client_data["class"].asString(), client_data["title"].asString())), + m_windowAddress(client_data["address"].asString()), + m_workspaceName(client_data["workspace"]["name"].asString()) { + clearAddr(); + clearWorkspaceName(); +} + +WindowCreationPayload::WindowCreationPayload(std::string workspace_name, + WindowAddress window_address, std::string window_repr) + : m_window(std::move(window_repr)), + m_windowAddress(std::move(window_address)), + m_workspaceName(std::move(workspace_name)) { + clearAddr(); + clearWorkspaceName(); +} + +WindowCreationPayload::WindowCreationPayload(std::string workspace_name, + WindowAddress window_address, std::string window_class, + std::string window_title) + : m_window(std::make_pair(std::move(window_class), std::move(window_title))), + m_windowAddress(std::move(window_address)), + m_workspaceName(std::move(workspace_name)) { + clearAddr(); + clearWorkspaceName(); +} + +void WindowCreationPayload::clearAddr() { + // substr(2, ...) is necessary because Hyprland's JSON follows this format: + // 0x{ADDR} + // While Hyprland's IPC follows this format: + // {ADDR} + static const std::string ADDR_PREFIX = "0x"; + static const int ADDR_PREFIX_LEN = ADDR_PREFIX.length(); + + if (m_windowAddress.starts_with(ADDR_PREFIX)) { + m_windowAddress = + m_windowAddress.substr(ADDR_PREFIX_LEN, m_windowAddress.length() - ADDR_PREFIX_LEN); + } +} + +void WindowCreationPayload::clearWorkspaceName() { + // The workspace name may optionally feature "special:" at the beginning. + // If so, we need to remove it because the workspace is saved WITHOUT the + // special qualifier. The reasoning is that not all of Hyprland's IPC events + // use this qualifier, so it's better to be consistent about our uses. + + static const std::string SPECIAL_QUALIFIER_PREFIX = "special:"; + static const int SPECIAL_QUALIFIER_PREFIX_LEN = SPECIAL_QUALIFIER_PREFIX.length(); + + if (m_workspaceName.starts_with(SPECIAL_QUALIFIER_PREFIX)) { + m_workspaceName = m_workspaceName.substr( + SPECIAL_QUALIFIER_PREFIX_LEN, m_workspaceName.length() - SPECIAL_QUALIFIER_PREFIX_LEN); + } + + std::size_t spaceFound = m_workspaceName.find(' '); + if (spaceFound != std::string::npos) { + m_workspaceName.erase(m_workspaceName.begin() + spaceFound, m_workspaceName.end()); + } +} + +bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) { + if (std::holds_alternative(m_window)) { + return std::get(m_window).empty(); + } + if (std::holds_alternative(m_window)) { + auto [window_class, window_title] = std::get(m_window); + return (window_class.empty() && + (!workspace_manager.windowRewriteConfigUsesTitle() || window_title.empty())); + } + // Unreachable + spdlog::error("WorkspaceWindow::isEmpty: Unreachable"); + throw std::runtime_error("WorkspaceWindow::isEmpty: Unreachable"); +} + +int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; } + +void WindowCreationPayload::moveToWorkspace(std::string &new_workspace_name) { + m_workspaceName = new_workspace_name; +} + +std::string WindowCreationPayload::repr(Workspaces &workspace_manager) { + if (std::holds_alternative(m_window)) { + return std::get(m_window); + } + if (std::holds_alternative(m_window)) { + auto [window_class, window_title] = std::get(m_window); + return workspace_manager.getRewrite(window_class, window_title); + } + // Unreachable + spdlog::error("WorkspaceWindow::repr: Unreachable"); + throw std::runtime_error("WorkspaceWindow::repr: Unreachable"); +} + +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp new file mode 100644 index 00000000..658c511d --- /dev/null +++ b/src/modules/hyprland/workspace.cpp @@ -0,0 +1,220 @@ +#include +#include + +#include +#include +#include + +#include "modules/hyprland/workspaces.hpp" + +namespace waybar::modules::hyprland { + +Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_manager, + const Json::Value &clients_data) + : m_workspaceManager(workspace_manager), + m_id(workspace_data["id"].asInt()), + m_name(workspace_data["name"].asString()), + m_output(workspace_data["monitor"].asString()), // TODO:allow using monitor desc + m_windows(workspace_data["windows"].asInt()), + m_isActive(true), + m_isPersistentRule(workspace_data["persistent-rule"].asBool()), + m_isPersistentConfig(workspace_data["persistent-config"].asBool()), + m_ipc(IPC::inst()) { + if (m_name.starts_with("name:")) { + m_name = m_name.substr(5); + } else if (m_name.starts_with("special")) { + m_name = m_id == -99 ? m_name : m_name.substr(8); + m_isSpecial = true; + } + + m_button.add_events(Gdk::BUTTON_PRESS_MASK); + m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), + false); + + m_button.set_relief(Gtk::RELIEF_NONE); + m_content.set_center_widget(m_label); + m_button.add(m_content); + + initializeWindowMap(clients_data); +} + +void addOrRemoveClass(const Glib::RefPtr &context, bool condition, + const std::string &class_name) { + if (condition) { + context->add_class(class_name); + } else { + context->remove_class(class_name); + } +} + +std::optional Workspace::closeWindow(WindowAddress const &addr) { + if (m_windowMap.contains(addr)) { + return removeWindow(addr); + } + return std::nullopt; +} + +bool Workspace::handleClicked(GdkEventButton *bt) const { + if (bt->type == GDK_BUTTON_PRESS) { + try { + if (id() > 0) { // normal + if (m_workspaceManager.moveToMonitor()) { + m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); + } else { + m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id())); + } + } else if (!isSpecial()) { // named (this includes persistent) + if (m_workspaceManager.moveToMonitor()) { + m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); + } else { + m_ipc.getSocket1Reply("dispatch workspace name:" + name()); + } + } else if (id() != -99) { // named special + m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name()); + } else { // special + m_ipc.getSocket1Reply("dispatch togglespecialworkspace"); + } + return true; + } catch (const std::exception &e) { + spdlog::error("Failed to dispatch workspace: {}", e.what()); + } + } + return false; +} + +void Workspace::initializeWindowMap(const Json::Value &clients_data) { + m_windowMap.clear(); + for (auto client : clients_data) { + if (client["workspace"]["id"].asInt() == id()) { + insertWindow({client}); + } + } +} + +void Workspace::insertWindow(WindowCreationPayload create_window_payload) { + if (!create_window_payload.isEmpty(m_workspaceManager)) { + auto repr = create_window_payload.repr(m_workspaceManager); + + if (!repr.empty()) { + m_windowMap[create_window_payload.getAddress()] = repr; + } + } +}; + +bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_payload) { + if (create_window_payload.getWorkspaceName() == name()) { + insertWindow(create_window_payload); + return true; + } + return false; +} + +std::string Workspace::removeWindow(WindowAddress const &addr) { + std::string windowRepr = m_windowMap[addr]; + m_windowMap.erase(addr); + return windowRepr; +} + +std::string &Workspace::selectIcon(std::map &icons_map) { + spdlog::trace("Selecting icon for workspace {}", name()); + if (isUrgent()) { + auto urgentIconIt = icons_map.find("urgent"); + if (urgentIconIt != icons_map.end()) { + return urgentIconIt->second; + } + } + + if (isActive()) { + auto activeIconIt = icons_map.find("active"); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; + } + } + + if (isSpecial()) { + auto specialIconIt = icons_map.find("special"); + if (specialIconIt != icons_map.end()) { + return specialIconIt->second; + } + } + + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; + } + + if (isVisible()) { + auto visibleIconIt = icons_map.find("visible"); + if (visibleIconIt != icons_map.end()) { + return visibleIconIt->second; + } + } + + if (isEmpty()) { + auto emptyIconIt = icons_map.find("empty"); + if (emptyIconIt != icons_map.end()) { + return emptyIconIt->second; + } + } + + if (isPersistent()) { + auto persistentIconIt = icons_map.find("persistent"); + if (persistentIconIt != icons_map.end()) { + return persistentIconIt->second; + } + } + + auto defaultIconIt = icons_map.find("default"); + if (defaultIconIt != icons_map.end()) { + return defaultIconIt->second; + } + + return m_name; +} + +void Workspace::update(const std::string &format, const std::string &icon) { + // clang-format off + if (this->m_workspaceManager.activeOnly() && \ + !this->isActive() && \ + !this->isPersistent() && \ + !this->isVisible() && \ + !this->isSpecial()) { + // clang-format on + // if activeOnly is true, hide if not active, persistent, visible or special + m_button.hide(); + return; + } + if (this->m_workspaceManager.specialVisibleOnly() && this->isSpecial() && !this->isVisible()) { + m_button.hide(); + return; + } + m_button.show(); + + auto styleContext = m_button.get_style_context(); + addOrRemoveClass(styleContext, isActive(), "active"); + addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isEmpty(), "empty"); + addOrRemoveClass(styleContext, isPersistent(), "persistent"); + addOrRemoveClass(styleContext, isUrgent(), "urgent"); + addOrRemoveClass(styleContext, isVisible(), "visible"); + addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); + + std::string windows; + auto windowSeparator = m_workspaceManager.getWindowSeparator(); + + bool isNotFirst = false; + + for (auto &[_pid, window_repr] : m_windowMap) { + if (isNotFirst) { + windows.append(windowSeparator); + } + isNotFirst = true; + windows.append(window_repr); + } + + m_label.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), + fmt::arg("name", name()), fmt::arg("icon", icon), + fmt::arg("windows", windows))); +} + +} // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index e169f916..bd0b1c5a 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -4,196 +4,986 @@ #include #include -#include #include +#include #include +#include + +#include "util/regex_collection.hpp" namespace waybar::modules::hyprland { Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) : AModule(config, "workspaces", id, false, false), - bar_(bar), - box_(bar.vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0) { - Json::Value config_format = config["format"]; - - format_ = config_format.isString() ? config_format.asString() : "{id}"; - with_icon_ = format_.find("{icon}") != std::string::npos; - - if (with_icon_ && icons_map_.empty()) { - Json::Value format_icons = config["format-icons"]; - for (std::string &name : format_icons.getMemberNames()) { - icons_map_.emplace(name, format_icons[name].asString()); - } - - icons_map_.emplace("", ""); - } - - box_.set_name("workspaces"); - if (!id.empty()) { - box_.get_style_context()->add_class(id); - } - event_box_.add(box_); + m_bar(bar), + m_box(bar.orientation, 0), + m_ipc(IPC::inst()) { modulesReady = true; - if (!gIPC.get()) { - gIPC = std::make_unique(); - } + 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); + + setCurrentMonitorId(); init(); - - gIPC->registerForIPC("workspace", this); - gIPC->registerForIPC("createworkspace", this); - gIPC->registerForIPC("destroyworkspace", this); -} - -auto Workspaces::update() -> void { - for (int &workspace_to_remove : workspaces_to_remove_) { - remove_workspace(workspace_to_remove); - } - - workspaces_to_remove_.clear(); - - for (int &workspace_to_create : workspaces_to_create_) { - create_workspace(workspace_to_create); - } - - workspaces_to_create_.clear(); - - for (std::unique_ptr &workspace : workspaces_) { - workspace->set_active(workspace->id() == active_workspace_id); - - std::string &workspace_icon = icons_map_[""]; - if (with_icon_) { - workspace_icon = workspace->select_icon(icons_map_); - } - - workspace->update(format_, workspace_icon); - } - - AModule::update(); -} - -void Workspaces::onEvent(const std::string &ev) { - std::lock_guard lock(mutex_); - std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); - std::string payload = ev.substr(eventName.size() + 2); - if (eventName == "workspace") { - std::from_chars(payload.data(), payload.data() + payload.size(), active_workspace_id); - } else if (eventName == "destroyworkspace") { - int deleted_workspace_id; - std::from_chars(payload.data(), payload.data() + payload.size(), deleted_workspace_id); - workspaces_to_remove_.push_back(deleted_workspace_id); - } else if (eventName == "createworkspace") { - int new_workspace_id; - std::from_chars(payload.data(), payload.data() + payload.size(), new_workspace_id); - workspaces_to_create_.push_back(new_workspace_id); - } - - dp.emit(); -} - -void Workspaces::create_workspace(int id) { - workspaces_.push_back(std::make_unique(id)); - Gtk::Button &new_workspace_button = workspaces_.back()->button(); - box_.pack_start(new_workspace_button, false, false); - sort_workspaces(); - new_workspace_button.show_all(); -} - -void Workspaces::remove_workspace(int id) { - auto workspace = std::find_if(workspaces_.begin(), workspaces_.end(), - [&](std::unique_ptr &x) { return x->id() == id; }); - - if (workspace == workspaces_.end()) { - spdlog::warn("Can't find workspace with id {}", id); - return; - } - - box_.remove(workspace->get()->button()); - workspaces_.erase(workspace); -} - -void Workspaces::init() { - const auto activeWorkspace = WorkspaceDto::parse(gIPC->getSocket1JsonReply("activeworkspace")); - active_workspace_id = activeWorkspace.id; - const Json::Value workspaces_json = gIPC->getSocket1JsonReply("workspaces"); - for (const Json::Value &workspace_json : workspaces_json) { - workspaces_.push_back( - std::make_unique(Workspace(WorkspaceDto::parse(workspace_json)))); - } - - for (auto &workspace : workspaces_) { - box_.pack_start(workspace->button(), false, false); - } - - sort_workspaces(); - - dp.emit(); + registerIpc(); } Workspaces::~Workspaces() { - gIPC->unregisterForIPC(this); + m_ipc.unregisterForIPC(this); // wait for possible event handler to finish - std::lock_guard lg(mutex_); + std::lock_guard lg(m_mutex); } -WorkspaceDto WorkspaceDto::parse(const Json::Value &value) { - return WorkspaceDto{value["id"].asInt()}; +void Workspaces::init() { + m_activeWorkspaceId = m_ipc.getSocket1JsonReply("activeworkspace")["id"].asInt(); + + initializeWorkspaces(); + dp.emit(); } -Workspace::Workspace(WorkspaceDto dto) : Workspace(dto.id){}; +Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name, + std::string const &monitor) { + spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor); + Json::Value workspaceData; -Workspace::Workspace(int id) : id_(id) { - button_.set_relief(Gtk::RELIEF_NONE); - content_.set_center_widget(label_); - button_.add(content_); -}; + auto workspaceId = parseWorkspaceId(name); + if (!workspaceId.has_value()) { + workspaceId = 0; + } + workspaceData["id"] = *workspaceId; + workspaceData["name"] = name; + workspaceData["monitor"] = monitor; + workspaceData["windows"] = 0; + return workspaceData; +} -void add_or_remove_class(Glib::RefPtr context, bool condition, - const std::string &class_name) { - if (condition) { - context->add_class(class_name); +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::ranges::find_if(m_workspaces, [workspaceName](std::unique_ptr const &w) { + return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || + workspaceName == w->name(); + }); + + if (workspace != m_workspaces.end()) { + // don't recreate workspace, but update persistency if necessary + const auto keys = workspace_data.getMemberNames(); + + const auto *k = "persistent-rule"; + if (std::ranges::find(keys, 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::ranges::find(keys, 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 + 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::createWorkspacesToCreate() { + for (const auto &[workspaceData, clientsData] : m_workspacesToCreate) { + createWorkspace(workspaceData, clientsData); + } + if (!m_workspacesToCreate.empty()) { + updateWindowCount(); + sortWorkspaces(); + } + m_workspacesToCreate.clear(); +} + +/** + * 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); + + removeWorkspacesToRemove(); + createWorkspacesToCreate(); + updateWorkspaceStates(); + updateWindowCount(); + sortWorkspaces(); + + bool anyWindowCreated = updateWindowsToCreate(); + + if (anyWindowCreated) { + dp.emit(); + } +} + +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}); + } + } +} + +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 { - context->remove_class(class_name); + 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)); } -void Workspace::update(const std::string &format, const std::string &icon) { - Glib::RefPtr style_context = button_.get_style_context(); - add_or_remove_class(style_context, active(), "active"); - - label_.set_markup( - fmt::format(fmt::runtime(format), fmt::arg("id", id()), fmt::arg("icon", icon))); -} - -void Workspaces::sort_workspaces() { - std::sort(workspaces_.begin(), workspaces_.end(), - [](std::unique_ptr &lhs, std::unique_ptr &rhs) { - return lhs->id() < rhs->id(); - }); - - for (size_t i = 0; i < workspaces_.size(); ++i) { - box_.reorder_child(workspaces_[i]->button(), i); +std::vector Workspaces::getVisibleWorkspaces() { + std::vector visibleWorkspaces; + auto monitors = IPC::inst().getSocket1JsonReply("monitors"); + for (const auto &monitor : monitors) { + auto ws = monitor["activeWorkspace"]; + if (ws.isObject() && ws["id"].isInt()) { + visibleWorkspaces.push_back(ws["id"].asInt()); + } + auto sws = monitor["specialWorkspace"]; + auto name = sws["name"].asString(); + if (sws.isObject() && sws["id"].isInt() && !name.empty()) { + visibleWorkspaces.push_back(sws["id"].asInt()); + } } + return visibleWorkspaces; } -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; +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(std::to_string(workspace->id())); + } + + // get all current workspaces + auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); + auto const clientsJson = m_ipc.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); } } - auto named_icon_it = icons_map.find(std::to_string(id())); - if (named_icon_it != icons_map.end()) { - return named_icon_it->second; + spdlog::debug("Initializing persistent workspaces"); + if (m_persistentWorkspaceConfig.isObject()) { + // a persistent workspace config is defined, so use that instead of workspace rules + loadPersistentWorkspacesFromConfig(clientsJson); } - - auto default_icon_it = icons_map.find("default"); - if (default_icon_it != icons_map.end()) { - return default_icon_it->second; - } - - return icons_map[""]; + // load Hyprland's workspace rules + loadPersistentWorkspacesFromWorkspaceRules(clientsJson); } + +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::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::ranges::find(keys, currentMonitor) != keys.end(); + for (const std::string &key : keys) { + // only add if either: + // 1. key is the current monitor name + // 2. key is "*" and this monitor is not already defined in the config + bool canCreate = key == currentMonitor || (key == "*" && !monitorInConfig); + const Json::Value &value = m_persistentWorkspaceConfig[key]; + spdlog::trace("Parsing persistent workspace config: {} => {}", key, value.toStyledString()); + + if (value.isInt()) { + // value is a number => create that many workspaces for this monitor + if (canCreate) { + int amount = value.asInt(); + spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, currentMonitor); + for (int i = 0; i < amount; i++) { + persistentWorkspacesToCreate.emplace_back(std::to_string((m_monitorId * amount) + i + 1)); + } + } + } else if (value.isArray() && !value.empty()) { + // value is an array => create defined workspaces for this monitor + if (canCreate) { + for (const Json::Value &workspace : value) { + if (workspace.isInt()) { + spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor); + persistentWorkspacesToCreate.emplace_back(std::to_string(workspace.asInt())); + } + } + } else { + // key is the workspace and value is array of monitors to create on + for (const Json::Value &monitor : value) { + if (monitor.isString() && monitor.asString() == currentMonitor) { + persistentWorkspacesToCreate.emplace_back(currentMonitor); + break; + } + } + } + } else { + // this workspace should be displayed on all monitors + persistentWorkspacesToCreate.emplace_back(key); + } + } + + for (auto const &workspace : persistentWorkspacesToCreate) { + auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); + workspaceData["persistent-config"] = true; + m_workspacesToCreate.emplace_back(workspaceData, clientsJson); + } +} + +void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &clientsJson) { + spdlog::info("Loading persistent workspaces from Hyprland workspace rules"); + + auto const workspaceRules = m_ipc.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.isMember("defaultName") ? rule["defaultName"].asString() + : 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 { + // This can be any workspace selector. + m_workspacesToRemove.emplace_back(workspace); + } + } +} + +void Workspaces::onEvent(const std::string &ev) { + std::lock_guard lock(m_mutex); + std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); + std::string payload = ev.substr(eventName.size() + 2); + + if (eventName == "workspacev2") { + onWorkspaceActivated(payload); + } else if (eventName == "activespecial") { + onSpecialWorkspaceActivated(payload); + } else if (eventName == "destroyworkspacev2") { + onWorkspaceDestroyed(payload); + } else if (eventName == "createworkspacev2") { + onWorkspaceCreated(payload); + } else if (eventName == "focusedmonv2") { + onMonitorFocused(payload); + } else if (eventName == "moveworkspacev2") { + onWorkspaceMoved(payload); + } else if (eventName == "openwindow") { + onWindowOpened(payload); + } else if (eventName == "closewindow") { + onWindowClosed(payload); + } else if (eventName == "movewindowv2") { + onWindowMoved(payload); + } else if (eventName == "urgent") { + setUrgentWorkspace(payload); + } else if (eventName == "renameworkspace") { + onWorkspaceRenamed(payload); + } else if (eventName == "windowtitlev2") { + onWindowTitleEvent(payload); + } else if (eventName == "configreloaded") { + onConfigReloaded(); + } + + dp.emit(); +} + +void Workspaces::onWorkspaceActivated(std::string const &payload) { + const auto [workspaceIdStr, workspaceName] = splitDoublePayload(payload); + const auto workspaceId = parseWorkspaceId(workspaceIdStr); + if (workspaceId.has_value()) { + m_activeWorkspaceId = *workspaceId; + } +} + +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) { + const auto [workspaceId, workspaceName] = splitDoublePayload(payload); + if (!isDoubleSpecial(workspaceName)) { + m_workspacesToRemove.push_back(workspaceId); + } +} + +void Workspaces::onWorkspaceCreated(std::string const &payload, Json::Value const &clientsData) { + spdlog::debug("Workspace created: {}", payload); + + const auto [workspaceIdStr, _] = splitDoublePayload(payload); + + const auto workspaceId = parseWorkspaceId(workspaceIdStr); + if (!workspaceId.has_value()) { + return; + } + + auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules"); + auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); + + for (Json::Value workspaceJson : workspacesJson) { + const auto currentId = workspaceJson["id"].asInt(); + if (currentId == *workspaceId) { + std::string workspaceName = workspaceJson["name"].asString(); + // This workspace name is more up-to-date than the one in the event payload. + if (isWorkspaceIgnored(workspaceName)) { + spdlog::trace("Not creating workspace because it is ignored: id={} name={}", *workspaceId, + workspaceName); + break; + } + + if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && + (showSpecial() || !workspaceName.starts_with("special")) && + !isDoubleSpecial(workspaceName)) { + for (Json::Value const &rule : workspaceRules) { + auto ruleWorkspaceName = rule.isMember("defaultName") + ? rule["defaultName"].asString() + : rule["workspaceString"].asString(); + if (ruleWorkspaceName == workspaceName) { + workspaceJson["persistent-rule"] = rule["persistent"].asBool(); + break; + } + } + + m_workspacesToCreate.emplace_back(workspaceJson, clientsData); + break; + } + } else { + extendOrphans(*workspaceId, clientsData); + } + } +} + +void Workspaces::onWorkspaceMoved(std::string const &payload) { + spdlog::debug("Workspace moved: {}", payload); + + // Update active workspace + m_activeWorkspaceId = (m_ipc.getSocket1JsonReply("activeworkspace"))["id"].asInt(); + + if (allOutputs()) return; + + const auto [workspaceIdStr, workspaceName, monitorName] = splitTriplePayload(payload); + + const auto subPayload = makePayload(workspaceIdStr, workspaceName); + + if (m_bar.output->name == monitorName) { + Json::Value clientsData = m_ipc.getSocket1JsonReply("clients"); + onWorkspaceCreated(subPayload, clientsData); + } else { + spdlog::debug("Removing workspace because it was moved to another monitor: {}", subPayload); + onWorkspaceDestroyed(subPayload); + } +} + +void Workspaces::onWorkspaceRenamed(std::string const &payload) { + spdlog::debug("Workspace renamed: {}", payload); + const auto [workspaceIdStr, newName] = splitDoublePayload(payload); + + const auto workspaceId = parseWorkspaceId(workspaceIdStr); + if (!workspaceId.has_value()) { + return; + } + + for (auto &workspace : m_workspaces) { + if (workspace->id() == *workspaceId) { + workspace->setName(newName); + break; + } + } + sortWorkspaces(); +} + +void Workspaces::onMonitorFocused(std::string const &payload) { + spdlog::trace("Monitor focused: {}", payload); + + const auto [monitorName, workspaceIdStr] = splitDoublePayload(payload); + + const auto workspaceId = parseWorkspaceId(workspaceIdStr); + if (!workspaceId.has_value()) { + return; + } + + m_activeWorkspaceId = *workspaceId; + + for (Json::Value &monitor : m_ipc.getSocket1JsonReply("monitors")) { + if (monitor["name"].asString() == monitorName) { + const auto name = monitor["specialWorkspace"]["name"].asString(); + m_activeSpecialWorkspaceName = !name.starts_with("special:") ? name : name.substr(8); + } + } +} + +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(); + auto [windowAddress, _, workspaceName] = splitTriplePayload(payload); + + 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.moveToWorkspace(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; + + const auto [windowAddress, _] = splitDoublePayload(payload); + + // If the window was an orphan, rename it at the orphan's vector + if (m_orphanWindowMap.contains(windowAddress)) { + inserter = [this](WindowCreationPayload wcp) { this->registerOrphanWindow(std::move(wcp)); }; + } else { + auto windowWorkspace = std::ranges::find_if(m_workspaces, [windowAddress](auto &workspace) { + return workspace->containsWindow(windowAddress); + }); + + // 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::ranges::find_if(m_windowsToCreate, [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 = m_ipc.getSocket1JsonReply("clients"); + std::string jsonWindowAddress = fmt::format("0x{}", payload); + + auto client = std::ranges::find_if(clientsData, [jsonWindowAddress](auto &client) { + return client["address"].asString() == jsonWindowAddress; + }); + + if (client != clientsData.end() && !client->empty()) { + (*inserter)({*client}); + } + } +} + +void Workspaces::onConfigReloaded() { + spdlog::info("Hyprland config reloaded, reinitializing hyprland/workspaces module..."); + init(); +} + +auto Workspaces::parseConfig(const Json::Value &config) -> void { + const auto &configFormat = config["format"]; + m_format = configFormat.isString() ? configFormat.asString() : "{name}"; + m_withIcon = m_format.find("{icon}") != std::string::npos; + + if (m_withIcon && m_iconsMap.empty()) { + populateIconsMap(config["format-icons"]); + } + + populateBoolConfig(config, "all-outputs", m_allOutputs); + populateBoolConfig(config, "show-special", m_showSpecial); + populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly); + populateBoolConfig(config, "active-only", m_activeOnly); + populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); + + m_persistentWorkspaceConfig = config.get("persistent-workspaces", Json::Value()); + populateSortByConfig(config); + populateIgnoreWorkspacesConfig(config); + populateFormatWindowSeparatorConfig(config); + populateWindowRewriteConfig(config); +} + +auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void { + for (const auto &name : formatIcons.getMemberNames()) { + m_iconsMap.emplace(name, formatIcons[name].asString()); + } + m_iconsMap.emplace("", ""); +} + +auto Workspaces::populateBoolConfig(const Json::Value &config, const std::string &key, bool &member) + -> void { + const auto &configValue = config[key]; + if (configValue.isBool()) { + member = configValue.asBool(); + } +} + +auto Workspaces::populateSortByConfig(const Json::Value &config) -> void { + const auto &configSortBy = config["sort-by"]; + if (configSortBy.isString()) { + auto sortByStr = configSortBy.asString(); + try { + m_sortBy = m_enumParser.parseStringToEnum(sortByStr, m_sortMap); + } catch (const std::invalid_argument &e) { + m_sortBy = SortMethod::DEFAULT; + spdlog::warn( + "Invalid string representation for sort-by. Falling back to default sort method."); + } + } +} + +auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value &config) -> void { + auto ignoreWorkspaces = config["ignore-workspaces"]; + if (ignoreWorkspaces.isArray()) { + for (const auto &workspaceRegex : ignoreWorkspaces) { + if (workspaceRegex.isString()) { + std::string ruleString = workspaceRegex.asString(); + try { + const std::regex rule{ruleString, std::regex_constants::icase}; + m_ignoreWorkspaces.emplace_back(rule); + } catch (const std::regex_error &e) { + spdlog::error("Invalid rule {}: {}", ruleString, e.what()); + } + } else { + spdlog::error("Not a string: '{}'", workspaceRegex); + } + } + } +} + +auto Workspaces::populateFormatWindowSeparatorConfig(const Json::Value &config) -> void { + const auto &formatWindowSeparator = config["format-window-separator"]; + m_formatWindowSeparator = + formatWindowSeparator.isString() ? formatWindowSeparator.asString() : " "; +} + +auto Workspaces::populateWindowRewriteConfig(const Json::Value &config) -> void { + const auto &windowRewrite = config["window-rewrite"]; + if (!windowRewrite.isObject()) { + spdlog::debug("window-rewrite is not defined or is not an object, using default rules."); + return; + } + + const auto &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 { + m_ipc.registerForIPC("workspacev2", this); + m_ipc.registerForIPC("activespecial", this); + m_ipc.registerForIPC("createworkspacev2", this); + m_ipc.registerForIPC("destroyworkspacev2", this); + m_ipc.registerForIPC("focusedmonv2", this); + m_ipc.registerForIPC("moveworkspacev2", this); + m_ipc.registerForIPC("renameworkspace", this); + m_ipc.registerForIPC("openwindow", this); + m_ipc.registerForIPC("closewindow", this); + m_ipc.registerForIPC("movewindowv2", this); + m_ipc.registerForIPC("urgent", this); + m_ipc.registerForIPC("configreloaded", this); + + if (windowRewriteConfigUsesTitle()) { + spdlog::info( + "Registering for Hyprland's 'windowtitlev2' events because a user-defined window " + "rewrite rule uses the 'title' field."); + m_ipc.registerForIPC("windowtitlev2", this); + } +} + +void Workspaces::removeWorkspacesToRemove() { + for (const auto &workspaceString : m_workspacesToRemove) { + removeWorkspace(workspaceString); + } + m_workspacesToRemove.clear(); +} + +void Workspaces::removeWorkspace(std::string const &workspaceString) { + spdlog::debug("Removing workspace {}", workspaceString); + + // If this succeeds, we have a workspace ID. + const auto workspaceId = parseWorkspaceId(workspaceString); + + std::string name; + // TODO: At some point we want to support all workspace selectors + // This is just a subset. + // https://wiki.hyprland.org/Configuring/Workspace-Rules/#workspace-selectors + if (workspaceString.starts_with("special:")) { + name = workspaceString.substr(8); + } else if (workspaceString.starts_with("name:")) { + name = workspaceString.substr(5); + } else { + name = workspaceString; + } + + const auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr &x) { + if (workspaceId.has_value()) { + return *workspaceId == x->id(); + } + return name == x->name(); + }); + + if (workspace == m_workspaces.end()) { + // happens when a workspace on another monitor is destroyed + return; + } + + if ((*workspace)->isPersistentConfig()) { + spdlog::trace("Not removing config persistent workspace id={} name={}", (*workspace)->id(), + (*workspace)->name()); + return; + } + + m_box.remove(workspace->get()->button()); + m_workspaces.erase(workspace); +} + +void Workspaces::setCurrentMonitorId() { + // get monitor ID from name (used by persistent workspaces) + m_monitorId = 0; + auto monitors = m_ipc.getSocket1JsonReply("monitors"); + auto currentMonitor = std::ranges::find_if(monitors, [this](const Json::Value &m) { + return m["name"].asString() == m_bar.output->name; + }); + if (currentMonitor == monitors.end()) { + spdlog::error("Monitor '{}' does not have an ID? Using 0", m_bar.output->name); + } else { + m_monitorId = (*currentMonitor)["id"].asInt(); + spdlog::trace("Current monitor ID: {}", m_monitorId); + } +} + +void Workspaces::sortWorkspaces() { + std::ranges::sort( // + m_workspaces, [&](std::unique_ptr &a, std::unique_ptr &b) { + // Helper comparisons + auto isIdLess = a->id() < b->id(); + auto isNameLess = a->name() < b->name(); + + 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; + } + + // 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 < m_workspaces.size(); ++i) { + m_box.reorder_child(m_workspaces[i]->button(), i); + } +} + +void Workspaces::setUrgentWorkspace(std::string const &windowaddress) { + const Json::Value clientsJson = m_ipc.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::ranges::find_if(m_workspaces, [workspaceId](std::unique_ptr &x) { + return x->id() == workspaceId; + }); + if (workspace != m_workspaces.end()) { + workspace->get()->setUrgent(); + } +} + +auto Workspaces::update() -> void { + doUpdate(); + AModule::update(); +} + +void Workspaces::updateWindowCount() { + const Json::Value workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); + for (auto &workspace : m_workspaces) { + auto workspaceJson = std::ranges::find_if(workspacesJson, [&](Json::Value const &x) { + return x["name"].asString() == workspace->name() || + (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); + }); + 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); + } +} + +bool Workspaces::updateWindowsToCreate() { + bool anyWindowCreated = false; + std::vector notCreated; + for (auto &windowPayload : m_windowsToCreate) { + bool created = false; + for (auto &workspace : m_workspaces) { + if (workspace->onWindowOpened(windowPayload)) { + created = true; + anyWindowCreated = true; + break; + } + } + if (!created) { + static auto const WINDOW_CREATION_TIMEOUT = 2; + if (windowPayload.incrementTimeSpentUncreated() < WINDOW_CREATION_TIMEOUT) { + notCreated.push_back(windowPayload); + } else { + registerOrphanWindow(windowPayload); + } + } + } + m_windowsToCreate.clear(); + m_windowsToCreate = notCreated; + return anyWindowCreated; +} + +void Workspaces::updateWorkspaceStates() { + const std::vector visibleWorkspaces = getVisibleWorkspaces(); + auto updatedWorkspaces = m_ipc.getSocket1JsonReply("workspaces"); + for (auto &workspace : m_workspaces) { + workspace->setActive( + workspace->id() == m_activeWorkspaceId || + (workspace->isSpecial() && workspace->name() == m_activeSpecialWorkspaceName)); + if (workspace->isActive() && workspace->isUrgent()) { + workspace->setUrgent(false); + } + workspace->setVisible(std::ranges::find(visibleWorkspaces, workspace->id()) != + visibleWorkspaces.end()); + std::string &workspaceIcon = m_iconsMap[""]; + if (m_withIcon) { + workspaceIcon = workspace->selectIcon(m_iconsMap); + } + auto updatedWorkspace = std::ranges::find_if(updatedWorkspaces, [&workspace](const auto &w) { + auto wNameRaw = w["name"].asString(); + auto wName = wNameRaw.starts_with("special:") ? wNameRaw.substr(8) : wNameRaw; + return wName == workspace->name(); + }); + if (updatedWorkspace != updatedWorkspaces.end()) { + workspace->setOutput((*updatedWorkspace)["monitor"].asString()); + } + workspace->update(m_format, workspaceIcon); + } +} + +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; +} + +template +std::string Workspaces::makePayload(Args const &...args) { + std::ostringstream result; + bool first = true; + ((result << (first ? "" : ",") << args, first = false), ...); + return result.str(); +} + +std::pair Workspaces::splitDoublePayload(std::string const &payload) { + const std::string part1 = payload.substr(0, payload.find(',')); + const std::string part2 = payload.substr(part1.size() + 1); + return {part1, part2}; +} + +std::tuple Workspaces::splitTriplePayload( + std::string const &payload) { + const size_t firstComma = payload.find(','); + const size_t secondComma = payload.find(',', firstComma + 1); + + const std::string part1 = payload.substr(0, firstComma); + const std::string part2 = payload.substr(firstComma + 1, secondComma - (firstComma + 1)); + const std::string part3 = payload.substr(secondComma + 1); + + return {part1, part2, part3}; +} + +std::optional Workspaces::parseWorkspaceId(std::string const &workspaceIdStr) { + try { + return workspaceIdStr == "special" ? -99 : std::stoi(workspaceIdStr); + } catch (std::exception const &e) { + spdlog::error("Failed to parse workspace ID: {}", e.what()); + return std::nullopt; + } +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 843cd954..71e93b94 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(); @@ -41,31 +42,37 @@ void waybar::modules::Image::refresh(int sig) { } auto waybar::modules::Image::update() -> void { - Glib::RefPtr pixbuf; if (config_["path"].isString()) { path_ = config_["path"].asString(); } else if (config_["exec"].isString()) { - output_ = util::command::exec(config_["exec"].asString()); + output_ = util::command::exec(config_["exec"].asString(), ""); parseOutputRaw(); } else { path_ = ""; } - if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) - pixbuf = Gdk::Pixbuf::create_from_file(path_, size_, size_); - else - pixbuf = {}; - if (pixbuf) { + if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) { + Glib::RefPtr pixbuf; + + int scaled_icon_size = size_ * image_.get_scale_factor(); + pixbuf = Gdk::Pixbuf::create_from_file(path_, scaled_icon_size, scaled_icon_size); + + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), + image_.get_window()); + image_.set(surface); + image_.show(); + if (tooltipEnabled() && !tooltip_.empty()) { if (box_.get_tooltip_markup() != tooltip_) { box_.set_tooltip_markup(tooltip_); } } - 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/memory/bsd.cpp b/src/modules/memory/bsd.cpp index 67f9fed7..1d970e8a 100644 --- a/src/modules/memory/bsd.cpp +++ b/src/modules/memory/bsd.cpp @@ -21,13 +21,13 @@ static uint64_t get_total_memory() { u_long physmem; #endif int mib[] = { - CTL_HW, + CTL_HW, #if defined(HW_MEMSIZE) - HW_MEMSIZE, + HW_MEMSIZE, #elif defined(HW_PHYSMEM64) - HW_PHYSMEM64, + HW_PHYSMEM64, #else - HW_PHYSMEM, + HW_PHYSMEM, #endif }; u_int miblen = sizeof(mib) / sizeof(mib[0]); diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 544d7814..18600cd2 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -60,6 +60,7 @@ auto waybar::modules::Memory::update() -> void { fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("percentage", used_ram_percentage), + fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), fmt::arg("swapAvail", available_swap_gigabytes))); @@ -72,6 +73,7 @@ auto waybar::modules::Memory::update() -> void { fmt::runtime(tooltip_format), used_ram_percentage, fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), fmt::arg("percentage", used_ram_percentage), + fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), fmt::arg("swapAvail", available_swap_gigabytes))); diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 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 a5621758..47bb9c05 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), @@ -83,7 +85,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) // "dynamic-priority" has been kept for backward compatibility if (config_["dynamic-importance-order"].isArray() || config_["dynamic-priority"].isArray()) { dynamic_prio_.clear(); - const auto& dynamic_priority = config_["dynamic-importance-order"].isArray() ? config_["dynamic-importance-order"] : config_["dynamic-priority"]; + const auto& dynamic_priority = config_["dynamic-importance-order"].isArray() + ? config_["dynamic-importance-order"] + : config_["dynamic-priority"]; for (const auto& value : dynamic_priority) { if (value.isString()) { dynamic_prio_.push_back(value.asString()); @@ -92,9 +96,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } if (config_["dynamic-order"].isArray()) { dynamic_order_.clear(); - for (auto it = config_["dynamic-order"].begin(); it != config_["dynamic-order"].end(); ++it) { - if (it->isString()) { - dynamic_order_.push_back(it->asString()); + for (const auto& item : config_["dynamic-order"]) { + if (item.isString()) { + dynamic_order_.push_back(item.asString()); } } } @@ -106,15 +110,19 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) player_ = config_["player"].asString(); } if (config_["ignored-players"].isArray()) { - for (auto it = config_["ignored-players"].begin(); it != config_["ignored-players"].end(); - ++it) { - if (it->isString()) { - ignored_players_.push_back(it->asString()); + for (const auto& item : config_["ignored-players"]) { + if (item.isString()) { + ignored_players_.push_back(item.asString()); } } } 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)); @@ -134,13 +142,11 @@ 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) { - auto pn = static_cast(p->data); + for (auto* p = players; p != nullptr; p = p->next) { + auto* pn = static_cast(p->data); if (strcmp(pn->name, player_.c_str()) == 0) { player = playerctl_player_new_from_name(pn, &error); break; @@ -173,17 +179,14 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } Mpris::~Mpris() { - if (manager != NULL) g_object_unref(manager); - if (player != NULL) g_object_unref(player); + if (manager != nullptr) g_object_unref(manager); + if (player != nullptr) g_object_unref(player); } auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) -> std::string { if (icons.isObject()) { - if (icons[key].isString()) { - return icons[key].asString(); - } else if (icons["default"].isString()) { - return icons["default"].asString(); - } + if (icons[key].isString()) return icons[key].asString(); + if (icons["default"].isString()) return icons["default"].asString(); } return ""; } @@ -198,7 +201,7 @@ size_t utf8_truncate(std::string& str, size_t width = std::string::npos) { size_t total_width = 0; - for (gchar *data = str.data(), *end = data + str.size(); data;) { + for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) { gunichar c = g_utf8_get_char_validated(data, end - data); if (c == -1U || c == -2U) { // invalid unicode, treat string as ascii @@ -262,7 +265,7 @@ auto Mpris::getLengthStr(const PlayerInfo& info, bool truncated) -> std::string auto length = info.length.value(); return (truncated && length.substr(0, 3) == "00:") ? length.substr(3) : length; } - return std::string(); + return {}; } auto Mpris::getPositionStr(const PlayerInfo& info, bool truncated) -> std::string { @@ -270,7 +273,7 @@ auto Mpris::getPositionStr(const PlayerInfo& info, bool truncated) -> std::strin auto position = info.position.value(); return (truncated && position.substr(0, 3) == "00:") ? position.substr(3) : position; } - return std::string(); + return {}; } auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> std::string { @@ -299,9 +302,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> "position") != dynamic_order_.end()); if (truncated && dynamic_len_ >= 0) { - //Since the first element doesn't present a separator and we don't know a priori which one - //it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the - //separatorLen to all the other lengths. + // Since the first element doesn't present a separator and we don't know a priori which one + // it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the + // separatorLen to all the other lengths. size_t separatorLen = utf8_width(dynamic_separator_); size_t dynamicLen = dynamic_len_ + separatorLen; if (showArtist) artistLen += separatorLen; @@ -312,33 +315,33 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> size_t totalLen = 0; - for (auto it = dynamic_prio_.begin(); it != dynamic_prio_.end(); ++it) { - if (*it == "artist") { + for (const auto& item : dynamic_prio_) { + if (item == "artist") { if (totalLen + artistLen > dynamicLen) { showArtist = false; } else if (showArtist) { totalLen += artistLen; } - } else if (*it == "album") { + } else if (item == "album") { if (totalLen + albumLen > dynamicLen) { showAlbum = false; } else if (showAlbum) { totalLen += albumLen; } - } else if (*it == "title") { + } else if (item == "title") { if (totalLen + titleLen > dynamicLen) { showTitle = false; } else if (showTitle) { totalLen += titleLen; } - } else if (*it == "length") { + } else if (item == "length") { if (totalLen + lengthLen > dynamicLen) { showLength = false; } else if (showLength) { totalLen += lengthLen; posLen = std::max((size_t)2, posLen) - 2; } - } else if (*it == "position") { + } else if (item == "position") { if (totalLen + posLen > dynamicLen) { showPos = false; } else if (showPos) { @@ -361,12 +364,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> std::string previousOrder = ""; for (const std::string& order : dynamic_order_) { - if ((order == "artist" && showArtist) || - (order == "album" && showAlbum) || + if ((order == "artist" && showArtist) || (order == "album" && showAlbum) || (order == "title" && showTitle)) { - if (previousShown && - previousOrder != "length" && - previousOrder != "position") { + if (previousShown && previousOrder != "length" && previousOrder != "position") { dynamic << dynamic_separator_; } @@ -402,7 +402,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: name-appeared callback: {}", player_name->name); @@ -411,8 +411,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, nullptr); g_object_connect(mpris->player, "signal::play", G_CALLBACK(onPlayerPlay), mpris, "signal::pause", G_CALLBACK(onPlayerPause), mpris, "signal::stop", G_CALLBACK(onPlayerStop), mpris, "signal::stop", G_CALLBACK(onPlayerStop), mpris, "signal::metadata", @@ -423,19 +422,22 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye auto Mpris::onPlayerNameVanished(PlayerctlPlayerManager* manager, PlayerctlPlayerName* player_name, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; - spdlog::debug("mpris: player-vanished callback: {}", player_name->name); + spdlog::debug("mpris: name-vanished callback: {}", player_name->name); - if (std::string(player_name->name) == mpris->player_) { + if (mpris->player_ == "playerctld") { + mpris->dp.emit(); + } else if (mpris->player_ == player_name->name) { mpris->player = nullptr; + mpris->event_box_.set_visible(false); mpris->dp.emit(); } } auto Mpris::onPlayerPlay(PlayerctlPlayer* player, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-play callback"); @@ -444,7 +446,7 @@ auto Mpris::onPlayerPlay(PlayerctlPlayer* player, gpointer data) -> void { } auto Mpris::onPlayerPause(PlayerctlPlayer* player, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-pause callback"); @@ -453,7 +455,7 @@ auto Mpris::onPlayerPause(PlayerctlPlayer* player, gpointer data) -> void { } auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-stop callback"); @@ -465,7 +467,7 @@ auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void { } auto Mpris::onPlayerMetadata(PlayerctlPlayer* player, GVariant* metadata, gpointer data) -> void { - Mpris* mpris = static_cast(data); + auto* mpris = static_cast(data); if (!mpris) return; spdlog::debug("mpris: player-metadata callback"); @@ -479,6 +481,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; @@ -488,14 +495,13 @@ 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 players = g_list_first(players); if (players) player_name = static_cast(players->data)->name; + else return std::nullopt; // no players found, hide the widget } if (std::any_of(ignored_players_.begin(), ignored_players_.end(), @@ -517,30 +523,30 @@ auto Mpris::getPlayerInfo() -> std::optional { .length = std::nullopt, }; - if (auto artist_ = playerctl_player_get_artist(player, &error)) { + if (auto* artist_ = playerctl_player_get_artist(player, &error)) { spdlog::debug("mpris[{}]: artist = {}", info.name, artist_); info.artist = artist_; g_free(artist_); } if (error) goto errorexit; - if (auto album_ = playerctl_player_get_album(player, &error)) { + if (auto* album_ = playerctl_player_get_album(player, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); info.album = album_; g_free(album_); } if (error) goto errorexit; - if (auto title_ = playerctl_player_get_title(player, &error)) { + if (auto* title_ = playerctl_player_get_title(player, &error)) { spdlog::debug("mpris[{}]: title = {}", info.name, title_); info.title = title_; g_free(title_); } if (error) goto errorexit; - if (auto length_ = playerctl_player_print_metadata_prop(player, "mpris:length", &error)) { + if (auto* length_ = playerctl_player_print_metadata_prop(player, "mpris:length", &error)) { spdlog::debug("mpris[{}]: mpris:length = {}", info.name, length_); - std::chrono::microseconds len = std::chrono::microseconds(std::strtol(length_, nullptr, 10)); + auto len = std::chrono::microseconds(std::strtol(length_, nullptr, 10)); auto len_h = std::chrono::duration_cast(len); auto len_m = std::chrono::duration_cast(len - len_h); auto len_s = std::chrono::duration_cast(len - len_h - len_m); @@ -557,7 +563,7 @@ auto Mpris::getPlayerInfo() -> std::optional { error = nullptr; } else { spdlog::debug("mpris[{}]: position = {}", info.name, position_); - std::chrono::microseconds len = std::chrono::microseconds(position_); + auto len = std::chrono::microseconds(position_); auto len_h = std::chrono::duration_cast(len); auto len_m = std::chrono::duration_cast(len - len_h); auto len_s = std::chrono::duration_cast(len - len_h - len_m); @@ -568,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; @@ -588,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); @@ -604,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 5eef1661..955f9f1d 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -80,7 +80,7 @@ waybar::modules::Network::readBandwidthUsage() { waybar::modules::Network::Network(const std::string &id, const Json::Value &config) : ALabel(config, "network", id, DEFAULT_FORMAT, 60), ifid_(-1), - family_(config["family"] == "ipv6" ? AF_INET6 : AF_INET), + addr_pref_(IPV4), efd_(-1), ev_fd_(-1), want_route_dump_(false), @@ -89,6 +89,7 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf dump_in_progress_(false), is_p2p_(false), cidr_(0), + cidr6_(0), signal_strength_dbm_(0), signal_strength_(0), #ifdef WANT_RFKILL @@ -102,6 +103,12 @@ waybar::modules::Network::Network(const std::string &id, const Json::Value &conf // the module start with no text, but the event_box_ is shown. label_.set_markup(""); + if (config_["family"] == "ipv6") { + addr_pref_ = IPV6; + } else if (config["family"] == "ipv4_6") { + addr_pref_ = IPV4_6; + } + auto bandwidth = readBandwidthUsage(); if (bandwidth.has_value()) { bandwidth_down_total_ = (*bandwidth).first; @@ -141,12 +148,7 @@ waybar::modules::Network::~Network() { close(efd_); } if (ev_sock_ != nullptr) { - nl_socket_drop_membership(ev_sock_, RTNLGRP_LINK); - if (family_ == AF_INET) { - nl_socket_drop_membership(ev_sock_, RTNLGRP_IPV4_IFADDR); - } else { - nl_socket_drop_membership(ev_sock_, RTNLGRP_IPV6_IFADDR); - } + nl_socket_drop_memberships(ev_sock_, RTNLGRP_LINK, RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR); nl_close(ev_sock_); nl_socket_free(ev_sock_); } @@ -161,7 +163,7 @@ void waybar::modules::Network::createEventSocket() { nl_socket_disable_seq_check(ev_sock_); nl_socket_modify_cb(ev_sock_, NL_CB_VALID, NL_CB_CUSTOM, handleEvents, this); nl_socket_modify_cb(ev_sock_, NL_CB_FINISH, NL_CB_CUSTOM, handleEventsDone, this); - auto groups = RTMGRP_LINK | (family_ == AF_INET ? RTMGRP_IPV4_IFADDR : RTMGRP_IPV6_IFADDR); + auto groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR; nl_join_groups(ev_sock_, groups); // Deprecated if (nl_connect(ev_sock_, NETLINK_ROUTE) != 0) { throw std::runtime_error("Can't connect network socket"); @@ -169,18 +171,9 @@ void waybar::modules::Network::createEventSocket() { if (nl_socket_set_nonblocking(ev_sock_)) { throw std::runtime_error("Can't set non-blocking on network socket"); } - nl_socket_add_membership(ev_sock_, RTNLGRP_LINK); - if (family_ == AF_INET) { - nl_socket_add_membership(ev_sock_, RTNLGRP_IPV4_IFADDR); - } else { - nl_socket_add_membership(ev_sock_, RTNLGRP_IPV6_IFADDR); - } + nl_socket_add_memberships(ev_sock_, RTNLGRP_LINK, RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR, 0); if (!config_["interface"].isString()) { - if (family_ == AF_INET) { - nl_socket_add_membership(ev_sock_, RTNLGRP_IPV4_ROUTE); - } else { - nl_socket_add_membership(ev_sock_, RTNLGRP_IPV6_ROUTE); - } + nl_socket_add_memberships(ev_sock_, RTNLGRP_IPV4_ROUTE, RTNLGRP_IPV6_ROUTE, 0); } efd_ = epoll_create1(EPOLL_CLOEXEC); @@ -230,8 +223,8 @@ void waybar::modules::Network::worker() { std::lock_guard lock(mutex_); if (ifid_ > 0) { getInfo(); - dp.emit(); } + dp.emit(); } thread_timer_.sleep_for(interval_); }; @@ -278,14 +271,14 @@ void waybar::modules::Network::worker() { } const std::string waybar::modules::Network::getNetworkState() const { - if (ifid_ == -1) { #ifdef WANT_RFKILL - if (rfkill_.getState()) return "disabled"; + if (rfkill_.getState()) return "disabled"; #endif + if (ifid_ == -1) { return "disconnected"; } if (!carrier_) return "disconnected"; - if (ipaddr_.empty()) return "linked"; + if (ipaddr_.empty() && ipaddr6_.empty()) return "linked"; if (essid_.empty()) return "ethernet"; return "wifi"; } @@ -331,12 +324,24 @@ auto waybar::modules::Network::update() -> void { } getState(signal_strength_); + std::string final_ipaddr_; + if (addr_pref_ == ip_addr_pref::IPV4) { + final_ipaddr_ = ipaddr_; + } else if (addr_pref_ == ip_addr_pref::IPV6) { + final_ipaddr_ = ipaddr6_; + } else if (addr_pref_ == ip_addr_pref::IPV4_6) { + final_ipaddr_ = ipaddr_; + final_ipaddr_ += '\n'; + final_ipaddr_ += ipaddr6_; + } + auto text = fmt::format( - fmt::runtime(format_), fmt::arg("essid", essid_), fmt::arg("signaldBm", signal_strength_dbm_), - fmt::arg("signalStrength", signal_strength_), + fmt::runtime(format_), fmt::arg("essid", essid_), fmt::arg("bssid", bssid_), + fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), - fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_), - fmt::arg("cidr", cidr_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), + fmt::arg("netmask", netmask_), fmt::arg("netmask6", netmask6_), + fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), + fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")), @@ -364,11 +369,12 @@ auto waybar::modules::Network::update() -> void { } if (!tooltip_format.empty()) { auto tooltip_text = fmt::format( - fmt::runtime(tooltip_format), fmt::arg("essid", essid_), + fmt::runtime(tooltip_format), fmt::arg("essid", essid_), fmt::arg("bssid", bssid_), fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), - fmt::arg("netmask", netmask_), fmt::arg("ipaddr", ipaddr_), fmt::arg("gwaddr", gwaddr_), - fmt::arg("cidr", cidr_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), + fmt::arg("netmask", netmask_), fmt::arg("netmask6", netmask6_), + fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), + fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), @@ -407,11 +413,15 @@ void waybar::modules::Network::clearIface() { ifid_ = -1; ifname_.clear(); essid_.clear(); + bssid_.clear(); ipaddr_.clear(); + ipaddr6_.clear(); gwaddr_.clear(); netmask_.clear(); + netmask6_.clear(); carrier_ = false; cidr_ = 0; + cidr6_ = 0; signal_strength_dbm_ = 0; signal_strength_ = 0; signal_strength_app_.clear(); @@ -481,6 +491,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { } else { // clear state related to WiFi connection net->essid_.clear(); + net->bssid_.clear(); net->signal_strength_dbm_ = 0; net->signal_strength_ = 0; net->signal_strength_app_.clear(); @@ -529,16 +540,11 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { return NL_OK; } - if (ifa->ifa_family != net->family_) { - return NL_OK; - } - // We ignore address mark as scope for the link or host, // which should leave scope global addresses. if (ifa->ifa_scope >= RT_SCOPE_LINK) { return NL_OK; } - for (; RTA_OK(ifa_rta, attrlen); ifa_rta = RTA_NEXT(ifa_rta, attrlen)) { switch (ifa_rta->rta_type) { case IFA_ADDRESS: @@ -546,8 +552,20 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { case IFA_LOCAL: char ipaddr[INET6_ADDRSTRLEN]; if (!is_del_event) { - net->ipaddr_ = inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); - net->cidr_ = ifa->ifa_prefixlen; + if ((net->addr_pref_ == ip_addr_pref::IPV4 || + net->addr_pref_ == ip_addr_pref::IPV4_6) && + net->cidr_ == 0 && ifa->ifa_family == AF_INET) { + net->ipaddr_ = + inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); + net->cidr_ = ifa->ifa_prefixlen; + } else if ((net->addr_pref_ == ip_addr_pref::IPV6 || + net->addr_pref_ == ip_addr_pref::IPV4_6) && + net->cidr6_ == 0 && ifa->ifa_family == AF_INET6) { + net->ipaddr6_ = + inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); + net->cidr6_ = ifa->ifa_prefixlen; + } + switch (ifa->ifa_family) { case AF_INET: { struct in_addr netmask; @@ -555,21 +573,24 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { net->netmask_ = inet_ntop(ifa->ifa_family, &netmask, ipaddr, sizeof(ipaddr)); } case AF_INET6: { - struct in6_addr netmask; + struct in6_addr netmask6; for (int i = 0; i < 16; i++) { int v = (i + 1) * 8 - ifa->ifa_prefixlen; if (v < 0) v = 0; if (v > 8) v = 8; - netmask.s6_addr[i] = ~0 << v; + netmask6.s6_addr[i] = ~0 << v; } - net->netmask_ = inet_ntop(ifa->ifa_family, &netmask, ipaddr, sizeof(ipaddr)); + net->netmask6_ = inet_ntop(ifa->ifa_family, &netmask6, ipaddr, sizeof(ipaddr)); } } spdlog::debug("network: {}, new addr {}/{}", net->ifname_, net->ipaddr_, net->cidr_); } else { net->ipaddr_.clear(); + net->ipaddr6_.clear(); net->cidr_ = 0; + net->cidr6_ = 0; net->netmask_.clear(); + net->netmask6_.clear(); spdlog::debug("network: {} addr deleted {}/{}", net->ifname_, inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)), ifa->ifa_prefixlen); @@ -589,6 +610,7 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { // to find the interface used to reach the outside world struct rtmsg *rtm = static_cast(NLMSG_DATA(nh)); + int family = rtm->rtm_family; ssize_t attrlen = RTM_PAYLOAD(nh); struct rtattr *attr = RTM_RTA(rtm); bool has_gateway = false; @@ -616,14 +638,14 @@ int waybar::modules::Network::handleEvents(struct nl_msg *msg, void *data) { * If someone ever needs to figure out the gateway address as well, * it's here as the attribute payload. */ - inet_ntop(net->family_, RTA_DATA(attr), temp_gw_addr, sizeof(temp_gw_addr)); + inet_ntop(family, RTA_DATA(attr), temp_gw_addr, sizeof(temp_gw_addr)); has_gateway = true; break; case RTA_DST: { /* The destination address. * Should be either missing, or maybe all 0s. Accept both. */ - const uint32_t nr_zeroes = (net->family_ == AF_INET) ? 4 : 16; + const uint32_t nr_zeroes = (family == AF_INET) ? 4 : 16; unsigned char c = 0; size_t dstlen = RTA_PAYLOAD(attr); if (dstlen != nr_zeroes) { @@ -655,8 +677,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(); @@ -716,7 +737,6 @@ void waybar::modules::Network::askForStateDump(void) { }; if (want_route_dump_) { - rt_hdr.rtgen_family = family_; nl_send_simple(ev_sock_, RTM_GETROUTE, NLM_F_DUMP, &rt_hdr, sizeof(rt_hdr)); want_route_dump_ = false; dump_in_progress_ = true; @@ -727,7 +747,6 @@ void waybar::modules::Network::askForStateDump(void) { dump_in_progress_ = true; } else if (want_addr_dump_) { - rt_hdr.rtgen_family = family_; nl_send_simple(ev_sock_, RTM_GETADDR, NLM_F_DUMP, &rt_hdr, sizeof(rt_hdr)); want_addr_dump_ = false; dump_in_progress_ = true; @@ -773,6 +792,7 @@ int waybar::modules::Network::handleScan(struct nl_msg *msg, void *data) { net->parseEssid(bss); net->parseSignal(bss); net->parseFreq(bss); + net->parseBssid(bss); return NL_OK; } @@ -838,6 +858,17 @@ void waybar::modules::Network::parseFreq(struct nlattr **bss) { } } +void waybar::modules::Network::parseBssid(struct nlattr **bss) { + if (bss[NL80211_BSS_BSSID] != nullptr) { + auto bssid = static_cast(nla_data(bss[NL80211_BSS_BSSID])); + auto bssid_len = nla_len(bss[NL80211_BSS_BSSID]); + if (bssid_len == 6) { + bssid_ = fmt::format("{:x}:{:x}:{:x}:{:x}:{:x}:{:x}", bssid[0], bssid[1], bssid[2], bssid[3], + bssid[4], bssid[5]); + } + } +} + bool waybar::modules::Network::associatedOrJoined(struct nlattr **bss) { if (bss[NL80211_BSS_STATUS] == nullptr) { return false; diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp new file mode 100644 index 00000000..383bf113 --- /dev/null +++ b/src/modules/niri/backend.cpp @@ -0,0 +1,261 @@ +#include "modules/niri/backend.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "giomm/datainputstream.h" +#include "giomm/dataoutputstream.h" +#include "giomm/unixinputstream.h" +#include "giomm/unixoutputstream.h" + +namespace waybar::modules::niri { + +int IPC::connectToSocket() { + const char *socket_path = getenv("NIRI_SOCKET"); + + if (socket_path == nullptr) { + spdlog::warn("Niri is not running, niri IPC will not be available."); + return -1; + } + + struct sockaddr_un addr; + int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); + + if (socketfd == -1) { + throw std::runtime_error("socketfd failed"); + } + + addr.sun_family = AF_UNIX; + + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + + int l = sizeof(struct sockaddr_un); + + if (connect(socketfd, (struct sockaddr *)&addr, l) == -1) { + close(socketfd); + throw std::runtime_error("unable to connect"); + } + + return socketfd; +} + +void IPC::startIPC() { + // will start IPC and relay events to parseIPC + + std::thread([&]() { + int socketfd; + try { + socketfd = connectToSocket(); + } catch (std::exception &e) { + spdlog::error("Niri IPC: failed to start, reason: {}", e.what()); + return; + } + if (socketfd == -1) return; + + spdlog::info("Niri IPC starting"); + + auto unix_istream = Gio::UnixInputStream::create(socketfd, true); + auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false); + auto istream = Gio::DataInputStream::create(unix_istream); + auto ostream = Gio::DataOutputStream::create(unix_ostream); + + if (!ostream->put_string("\"EventStream\"\n") || !ostream->flush()) { + spdlog::error("Niri IPC: failed to start event stream"); + return; + } + + std::string line; + if (!istream->read_line(line) || line != R"({"Ok":"Handled"})") { + spdlog::error("Niri IPC: failed to start event stream"); + return; + } + + while (istream->read_line(line)) { + spdlog::debug("Niri IPC: received {}", line); + + try { + parseIPC(line); + } catch (std::exception &e) { + spdlog::warn("Failed to parse IPC message: {}, reason: {}", line, e.what()); + } catch (...) { + throw; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }).detach(); +} + +void IPC::parseIPC(const std::string &line) { + const auto ev = parser_.parse(line); + const auto members = ev.getMemberNames(); + if (members.size() != 1) throw std::runtime_error("Event must have a single member"); + + { + auto lock = lockData(); + + if (const auto &payload = ev["WorkspacesChanged"]) { + workspaces_.clear(); + const auto &values = payload["workspaces"]; + std::copy(values.begin(), values.end(), std::back_inserter(workspaces_)); + + std::sort(workspaces_.begin(), workspaces_.end(), [](const auto &a, const auto &b) { + const auto &aOutput = a["output"].asString(); + const auto &bOutput = b["output"].asString(); + const auto aIdx = a["idx"].asUInt(); + const auto bIdx = b["idx"].asUInt(); + if (aOutput == bOutput) return aIdx < bIdx; + return aOutput < bOutput; + }); + } else if (const auto &payload = ev["WorkspaceActivated"]) { + const auto id = payload["id"].asUInt64(); + const auto focused = payload["focused"].asBool(); + auto it = std::find_if(workspaces_.begin(), workspaces_.end(), + [id](const auto &ws) { return ws["id"].asUInt64() == id; }); + if (it != workspaces_.end()) { + const auto &ws = *it; + const auto &output = ws["output"].asString(); + for (auto &ws : workspaces_) { + const auto got_activated = (ws["id"].asUInt64() == id); + if (ws["output"] == output) ws["is_active"] = got_activated; + + if (focused) ws["is_focused"] = got_activated; + } + } else { + spdlog::error("Activated unknown workspace"); + } + } else if (const auto &payload = ev["WorkspaceActiveWindowChanged"]) { + const auto workspaceId = payload["workspace_id"].asUInt64(); + auto it = std::find_if(workspaces_.begin(), workspaces_.end(), [workspaceId](const auto &ws) { + return ws["id"].asUInt64() == workspaceId; + }); + if (it != workspaces_.end()) { + auto &ws = *it; + ws["active_window_id"] = payload["active_window_id"]; + } else { + spdlog::error("Active window changed on unknown workspace"); + } + } else if (const auto &payload = ev["KeyboardLayoutsChanged"]) { + const auto &layouts = payload["keyboard_layouts"]; + const auto &names = layouts["names"]; + keyboardLayoutCurrent_ = layouts["current_idx"].asUInt(); + + keyboardLayoutNames_.clear(); + for (const auto &fullName : names) keyboardLayoutNames_.push_back(fullName.asString()); + } else if (const auto &payload = ev["KeyboardLayoutSwitched"]) { + keyboardLayoutCurrent_ = payload["idx"].asUInt(); + } else if (const auto &payload = ev["WindowsChanged"]) { + windows_.clear(); + const auto &values = payload["windows"]; + std::copy(values.begin(), values.end(), std::back_inserter(windows_)); + } else if (const auto &payload = ev["WindowOpenedOrChanged"]) { + const auto &window = payload["window"]; + const auto id = window["id"].asUInt64(); + auto it = std::find_if(windows_.begin(), windows_.end(), + [id](const auto &win) { return win["id"].asUInt64() == id; }); + if (it == windows_.end()) { + windows_.push_back(window); + + if (window["is_focused"].asBool()) { + for (auto &win : windows_) { + win["is_focused"] = win["id"].asUInt64() == id; + } + } + } else { + *it = window; + } + } else if (const auto &payload = ev["WindowClosed"]) { + const auto id = payload["id"].asUInt64(); + auto it = std::find_if(windows_.begin(), windows_.end(), + [id](const auto &win) { return win["id"].asUInt64() == id; }); + if (it != windows_.end()) { + windows_.erase(it); + } else { + spdlog::error("Unknown window closed"); + } + } else if (const auto &payload = ev["WindowFocusChanged"]) { + const auto focused = !payload["id"].isNull(); + const auto id = payload["id"].asUInt64(); + for (auto &win : windows_) { + win["is_focused"] = focused && win["id"].asUInt64() == id; + } + } + } + + std::unique_lock lock(callbackMutex_); + + for (auto &[eventname, handler] : callbacks_) { + if (eventname == members[0]) { + handler->onEvent(ev); + } + } +} + +void IPC::registerForIPC(const std::string &ev, EventHandler *ev_handler) { + if (ev_handler == nullptr) { + return; + } + + std::unique_lock lock(callbackMutex_); + callbacks_.emplace_back(ev, ev_handler); +} + +void IPC::unregisterForIPC(EventHandler *ev_handler) { + if (ev_handler == nullptr) { + return; + } + + std::unique_lock lock(callbackMutex_); + + for (auto it = callbacks_.begin(); it != callbacks_.end();) { + auto &[eventname, handler] = *it; + if (handler == ev_handler) { + it = callbacks_.erase(it); + } else { + ++it; + } + } +} + +Json::Value IPC::send(const Json::Value &request) { + int socketfd = connectToSocket(); + if (socketfd == -1) throw std::runtime_error("Niri is not running"); + + auto unix_istream = Gio::UnixInputStream::create(socketfd, true); + auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false); + auto istream = Gio::DataInputStream::create(unix_istream); + auto ostream = Gio::DataOutputStream::create(unix_ostream); + + // Niri needs the request on a single line. + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; + std::unique_ptr writer(builder.newStreamWriter()); + std::ostringstream oss; + writer->write(request, &oss); + oss << '\n'; + + if (!ostream->put_string(oss.str()) || !ostream->flush()) + throw std::runtime_error("error writing to niri socket"); + + std::string line; + if (!istream->read_line(line)) throw std::runtime_error("error reading from niri socket"); + + std::istringstream iss(std::move(line)); + Json::Value response; + iss >> response; + return response; +} + +} // namespace waybar::modules::niri diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp new file mode 100644 index 00000000..3b55ff24 --- /dev/null +++ b/src/modules/niri/language.cpp @@ -0,0 +1,136 @@ +#include "modules/niri/language.hpp" + +#include +#include +#include + +#include "util/string.hpp" + +namespace waybar::modules::niri { + +Language::Language(const std::string &id, const Bar &bar, const Json::Value &config) + : ALabel(config, "language", id, "{}", 0, false), bar_(bar) { + label_.hide(); + + if (!gIPC) gIPC = std::make_unique(); + + gIPC->registerForIPC("KeyboardLayoutsChanged", this); + gIPC->registerForIPC("KeyboardLayoutSwitched", this); + + updateFromIPC(); + dp.emit(); +} + +Language::~Language() { + gIPC->unregisterForIPC(this); + // wait for possible event handler to finish + std::lock_guard lock(mutex_); +} + +void Language::updateFromIPC() { + std::lock_guard lock(mutex_); + auto ipcLock = gIPC->lockData(); + + layouts_.clear(); + for (const auto &fullName : gIPC->keyboardLayoutNames()) layouts_.push_back(getLayout(fullName)); + + current_idx_ = gIPC->keyboardLayoutCurrent(); +} + +/** + * Language::doUpdate - update workspaces in UI thread. + * + * Note: some member fields are modified by both UI thread and event listener thread, use mutex_ to + * protect these member fields, and lock should released before calling ALabel::update(). + */ +void Language::doUpdate() { + std::lock_guard lock(mutex_); + + if (layouts_.size() <= current_idx_) { + spdlog::error("niri language layout index out of bounds"); + label_.hide(); + return; + } + const auto &layout = layouts_[current_idx_]; + + spdlog::debug("niri language update with full name {}", layout.full_name); + spdlog::debug("niri language update with short name {}", layout.short_name); + spdlog::debug("niri language update with short description {}", layout.short_description); + spdlog::debug("niri language update with variant {}", layout.variant); + + std::string layoutName = std::string{}; + if (config_.isMember("format-" + layout.short_description + "-" + layout.variant)) { + const auto propName = "format-" + layout.short_description + "-" + layout.variant; + layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString()); + } else if (config_.isMember("format-" + layout.short_description)) { + const auto propName = "format-" + layout.short_description; + layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString()); + } else { + layoutName = trim(fmt::format(fmt::runtime(format_), fmt::arg("long", layout.full_name), + fmt::arg("short", layout.short_name), + fmt::arg("shortDescription", layout.short_description), + fmt::arg("variant", layout.variant))); + } + + spdlog::debug("niri language formatted layout name {}", layoutName); + + if (!format_.empty()) { + label_.show(); + label_.set_markup(layoutName); + } else { + label_.hide(); + } +} + +void Language::update() { + doUpdate(); + ALabel::update(); +} + +void Language::onEvent(const Json::Value &ev) { + if (ev["KeyboardLayoutsChanged"]) { + updateFromIPC(); + } else if (ev["KeyboardLayoutSwitched"]) { + std::lock_guard lock(mutex_); + auto ipcLock = gIPC->lockData(); + current_idx_ = gIPC->keyboardLayoutCurrent(); + } + + dp.emit(); +} + +Language::Layout Language::getLayout(const std::string &fullName) { + 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 != nullptr) { + std::string nameOfLayout = rxkb_layout_get_description(layout); + + if (nameOfLayout != fullName) { + layout = rxkb_layout_next(layout); + continue; + } + + auto name = std::string(rxkb_layout_get_name(layout)); + const auto *variantPtr = rxkb_layout_get_variant(layout); + std::string variant = variantPtr == nullptr ? "" : std::string(variantPtr); + + const auto *descriptionPtr = rxkb_layout_get_brief(layout); + std::string description = descriptionPtr == nullptr ? "" : std::string(descriptionPtr); + + Layout info = Layout{nameOfLayout, name, variant, description}; + + rxkb_context_unref(context); + + return info; + } + + rxkb_context_unref(context); + + spdlog::debug("niri language didn't find matching layout for {}", fullName); + + return Layout{"", "", "", ""}; +} + +} // namespace waybar::modules::niri diff --git a/src/modules/niri/window.cpp b/src/modules/niri/window.cpp new file mode 100644 index 00000000..6e6fd36f --- /dev/null +++ b/src/modules/niri/window.cpp @@ -0,0 +1,109 @@ +#include "modules/niri/window.hpp" + +#include +#include +#include + +#include "util/rewrite_string.hpp" +#include "util/sanitize_str.hpp" + +namespace waybar::modules::niri { + +Window::Window(const std::string &id, const Bar &bar, const Json::Value &config) + : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) { + if (!gIPC) gIPC = std::make_unique(); + + gIPC->registerForIPC("WindowsChanged", this); + gIPC->registerForIPC("WindowOpenedOrChanged", this); + gIPC->registerForIPC("WindowClosed", this); + gIPC->registerForIPC("WindowFocusChanged", this); + + dp.emit(); +} + +Window::~Window() { gIPC->unregisterForIPC(this); } + +void Window::onEvent(const Json::Value &ev) { dp.emit(); } + +void Window::doUpdate() { + auto ipcLock = gIPC->lockData(); + + const auto &windows = gIPC->windows(); + const auto &workspaces = gIPC->workspaces(); + + const auto separateOutputs = config_["separate-outputs"].asBool(); + const auto ws_it = std::find_if(workspaces.cbegin(), workspaces.cend(), [&](const auto &ws) { + if (separateOutputs) { + return ws["is_active"].asBool() && ws["output"].asString() == bar_.output->name; + } + + return ws["is_focused"].asBool(); + }); + + std::vector::const_iterator it; + if (ws_it == workspaces.cend() || (*ws_it)["active_window_id"].isNull()) { + it = windows.cend(); + } else { + const auto id = (*ws_it)["active_window_id"].asUInt64(); + it = std::find_if(windows.cbegin(), windows.cend(), + [id](const auto &win) { return win["id"].asUInt64() == id; }); + } + + setClass("empty", ws_it == workspaces.cend() || (*ws_it)["active_window_id"].isNull()); + + if (it != windows.cend()) { + const auto &window = *it; + + const auto title = window["title"].asString(); + const auto appId = window["app_id"].asString(); + const auto sanitizedTitle = waybar::util::sanitize_string(title); + const auto sanitizedAppId = waybar::util::sanitize_string(appId); + + label_.show(); + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", sanitizedTitle), + fmt::arg("app_id", sanitizedAppId)), + config_["rewrite"])); + + updateAppIconName(appId, ""); + + if (tooltipEnabled()) label_.set_tooltip_text(title); + + const auto id = window["id"].asUInt64(); + const auto workspaceId = window["workspace_id"].asUInt64(); + const auto isSolo = std::none_of(windows.cbegin(), windows.cend(), [&](const auto &win) { + return win["id"].asUInt64() != id && win["workspace_id"].asUInt64() == workspaceId; + }); + setClass("solo", isSolo); + if (!appId.empty()) setClass(appId, isSolo); + + if (oldAppId_ != appId) { + if (!oldAppId_.empty()) setClass(oldAppId_, false); + oldAppId_ = appId; + } + } else { + label_.hide(); + updateAppIconName("", ""); + setClass("solo", false); + if (!oldAppId_.empty()) setClass(oldAppId_, false); + oldAppId_.clear(); + } +} + +void Window::update() { + doUpdate(); + AAppIconLabel::update(); +} + +void Window::setClass(const std::string &className, bool enable) { + auto styleContext = bar_.window.get_style_context(); + if (enable) { + if (!styleContext->has_class(className)) { + styleContext->add_class(className); + } + } else { + styleContext->remove_class(className); + } +} + +} // namespace waybar::modules::niri diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp new file mode 100644 index 00000000..d2fcad5d --- /dev/null +++ b/src/modules/niri/workspaces.cpp @@ -0,0 +1,186 @@ +#include "modules/niri/workspaces.hpp" + +#include +#include +#include + +namespace waybar::modules::niri { + +Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) + : AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) { + box_.set_name("workspaces"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + box_.get_style_context()->add_class(MODULE_CLASS); + event_box_.add(box_); + + if (!gIPC) gIPC = std::make_unique(); + + gIPC->registerForIPC("WorkspacesChanged", this); + gIPC->registerForIPC("WorkspaceActivated", this); + gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); + + dp.emit(); +} + +Workspaces::~Workspaces() { gIPC->unregisterForIPC(this); } + +void Workspaces::onEvent(const Json::Value &ev) { dp.emit(); } + +void Workspaces::doUpdate() { + auto ipcLock = gIPC->lockData(); + + const auto alloutputs = config_["all-outputs"].asBool(); + std::vector my_workspaces; + const auto &workspaces = gIPC->workspaces(); + std::copy_if(workspaces.cbegin(), workspaces.cend(), std::back_inserter(my_workspaces), + [&](const auto &ws) { + if (alloutputs) return true; + return ws["output"].asString() == bar_.output->name; + }); + + // Remove buttons for removed workspaces. + for (auto it = buttons_.begin(); it != buttons_.end();) { + auto ws = std::find_if(my_workspaces.begin(), my_workspaces.end(), + [it](const auto &ws) { return ws["id"].asUInt64() == it->first; }); + if (ws == my_workspaces.end()) { + it = buttons_.erase(it); + } else { + ++it; + } + } + + // Add buttons for new workspaces, update existing ones. + for (const auto &ws : my_workspaces) { + auto bit = buttons_.find(ws["id"].asUInt64()); + auto &button = bit == buttons_.end() ? addButton(ws) : bit->second; + auto style_context = button.get_style_context(); + + if (ws["is_focused"].asBool()) + style_context->add_class("focused"); + else + style_context->remove_class("focused"); + + if (ws["is_active"].asBool()) + style_context->add_class("active"); + else + style_context->remove_class("active"); + + if (ws["output"]) { + if (ws["output"].asString() == bar_.output->name) + style_context->add_class("current_output"); + else + style_context->remove_class("current_output"); + } else { + style_context->remove_class("current_output"); + } + + if (ws["active_window_id"].isNull()) + style_context->add_class("empty"); + else + style_context->remove_class("empty"); + + std::string name; + if (ws["name"]) { + name = ws["name"].asString(); + } else { + name = std::to_string(ws["idx"].asUInt()); + } + button.set_name("niri-workspace-" + name); + + if (config_["format"].isString()) { + auto format = config_["format"].asString(); + name = fmt::format(fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), + fmt::arg("value", name), fmt::arg("name", ws["name"].asString()), + fmt::arg("index", ws["idx"].asUInt()), + fmt::arg("output", ws["output"].asString())); + } + if (!config_["disable-markup"].asBool()) { + static_cast(button.get_children()[0])->set_markup(name); + } else { + button.set_label(name); + } + + if (config_["current-only"].asBool()) { + const auto *property = alloutputs ? "is_focused" : "is_active"; + if (ws[property].asBool()) + button.show(); + else + button.hide(); + } else { + button.show(); + } + } + + // Refresh the button order. + for (auto it = my_workspaces.cbegin(); it != my_workspaces.cend(); ++it) { + const auto &ws = *it; + + auto pos = ws["idx"].asUInt() - 1; + if (alloutputs) pos = it - my_workspaces.cbegin(); + + auto &button = buttons_[ws["id"].asUInt64()]; + box_.reorder_child(button, pos); + } +} + +void Workspaces::update() { + doUpdate(); + AModule::update(); +} + +Gtk::Button &Workspaces::addButton(const Json::Value &ws) { + std::string name; + if (ws["name"]) { + name = ws["name"].asString(); + } else { + name = std::to_string(ws["idx"].asUInt()); + } + + auto pair = buttons_.emplace(ws["id"].asUInt64(), name); + auto &&button = pair.first->second; + box_.pack_start(button, false, false, 0); + button.set_relief(Gtk::RELIEF_NONE); + if (!config_["disable-click"].asBool()) { + const auto id = ws["id"].asUInt64(); + button.signal_pressed().connect([=] { + try { + // {"Action":{"FocusWorkspace":{"reference":{"Id":1}}}} + Json::Value request(Json::objectValue); + auto &action = (request["Action"] = Json::Value(Json::objectValue)); + auto &focusWorkspace = (action["FocusWorkspace"] = Json::Value(Json::objectValue)); + auto &reference = (focusWorkspace["reference"] = Json::Value(Json::objectValue)); + reference["Id"] = id; + + IPC::send(request); + } catch (const std::exception &e) { + spdlog::error("Error switching workspace: {}", e.what()); + } + }); + } + return button; +} + +std::string Workspaces::getIcon(const std::string &value, const Json::Value &ws) { + const auto &icons = config_["format-icons"]; + if (!icons) return value; + + if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].asString(); + + if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); + + if (ws["name"]) { + const auto &name = ws["name"].asString(); + if (icons[name]) return icons[name].asString(); + } + + const auto idx = ws["idx"].asString(); + if (icons[idx]) return icons[idx].asString(); + + if (icons["default"]) return icons["default"].asString(); + + return value; +} + +} // namespace waybar::modules::niri diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp new file mode 100644 index 00000000..abad763d --- /dev/null +++ b/src/modules/power_profiles_daemon.cpp @@ -0,0 +1,213 @@ +#include "modules/power_profiles_daemon.hpp" + +#include +#include +#include +#include + +namespace waybar::modules { + +PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Value& config) + : ALabel(config, "power-profiles-daemon", id, "{icon}", 0, false, true), connected_(false) { + if (config_["tooltip-format"].isString()) { + tooltipFormat_ = config_["tooltip-format"].asString(); + } else { + tooltipFormat_ = "Power profile: {profile}\nDriver: {driver}"; + } + // Fasten your seatbelt, we're up for quite a ride. The rest of the + // init is performed asynchronously. There's 2 callbacks involved. + // Here's the overall idea: + // 1. Async connect to the system bus. + // 2. In the system bus connect callback, try to call + // org.freedesktop.DBus.Properties.GetAll to see if + // power-profiles-daemon is able to respond. + // 3. In the GetAll callback, connect the activeProfile monitoring + // callback, consider the init to be successful. Meaning start + // drawing the module. + // + // There's sadly no other way around that, we have to try to call a + // method on the proxy to see whether or not something's responding + // on the other side. + + // NOTE: the DBus addresses are under migration. They should be + // changed to org.freedesktop.UPower.PowerProfiles at some point. + // + // See + // https://gitlab.freedesktop.org/upower/power-profiles-daemon/-/releases/0.20 + // + // The old name is still announced for now. Let's rather use the old + // addresses for compatibility sake. + // + // Revisit this in 2026, systems should be updated by then. + + Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::BUS_TYPE_SYSTEM, "net.hadess.PowerProfiles", + "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", + sigc::mem_fun(*this, &PowerProfilesDaemon::busConnectedCb)); + // Schedule update to set the initial visibility + dp.emit(); +} + +void PowerProfilesDaemon::busConnectedCb(Glib::RefPtr& r) { + try { + powerProfilesProxy_ = Gio::DBus::Proxy::create_for_bus_finish(r); + using GetAllProfilesVar = Glib::Variant>; + auto callArgs = GetAllProfilesVar::create(std::make_tuple("net.hadess.PowerProfiles")); + powerProfilesProxy_->call("org.freedesktop.DBus.Properties.GetAll", + sigc::mem_fun(*this, &PowerProfilesDaemon::getAllPropsCb), callArgs); + // Connect active profile callback + } catch (const std::exception& e) { + spdlog::error("Failed to create the power profiles daemon DBus proxy: {}", e.what()); + } catch (const Glib::Error& e) { + spdlog::error("Failed to create the power profiles daemon DBus proxy: {}", + std::string(e.what())); + } +} + +// Callback for the GetAll call. +// +// We're abusing this call to make sure power-profiles-daemon is +// available on the host. We're not really using +void PowerProfilesDaemon::getAllPropsCb(Glib::RefPtr& r) { + try { + auto _ = powerProfilesProxy_->call_finish(r); + // Power-profiles-daemon responded something, we can assume it's + // available, we can safely attach the activeProfile monitoring + // now. + connected_ = true; + powerProfilesProxy_->signal_properties_changed().connect( + sigc::mem_fun(*this, &PowerProfilesDaemon::profileChangedCb)); + populateInitState(); + } catch (const std::exception& err) { + spdlog::error("Failed to query power-profiles-daemon via dbus: {}", err.what()); + } catch (const Glib::Error& err) { + spdlog::error("Failed to query power-profiles-daemon via dbus: {}", std::string(err.what())); + } +} + +void PowerProfilesDaemon::populateInitState() { + // Retrieve current active profile + Glib::Variant profileStr; + powerProfilesProxy_->get_cached_property(profileStr, "ActiveProfile"); + + // Retrieve profiles list, it's aa{sv}. + using ProfilesType = std::vector>>; + Glib::Variant profilesVariant; + powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles"); + for (auto& variantDict : profilesVariant.get()) { + Glib::ustring name; + Glib::ustring driver; + if (auto p = variantDict.find("Profile"); p != variantDict.end()) { + name = p->second.get(); + } + if (auto d = variantDict.find("Driver"); d != variantDict.end()) { + driver = d->second.get(); + } + if (!name.empty()) { + availableProfiles_.emplace_back(std::move(name), std::move(driver)); + } else { + spdlog::error( + "Power profiles daemon: power-profiles-daemon sent us an empty power profile name. " + "Something is wrong."); + } + } + + // Find the index of the current activated mode (to toggle) + std::string str = profileStr.get(); + switchToProfile(str); +} + +void PowerProfilesDaemon::profileChangedCb( + const Gio::DBus::Proxy::MapChangedProperties& changedProperties, + const std::vector& invalidatedProperties) { + // We're likely connected if this callback gets triggered. + // But better be safe than sorry. + if (connected_) { + if (auto activeProfileVariant = changedProperties.find("ActiveProfile"); + activeProfileVariant != changedProperties.end()) { + std::string activeProfile = + Glib::VariantBase::cast_dynamic>(activeProfileVariant->second) + .get(); + switchToProfile(activeProfile); + } + } +} + +// Look for the profile str in our internal profiles list. Using a +// vector to store the profiles ain't the smartest move +// complexity-wise, but it makes toggling between the mode easy. This +// vector is 3 elements max, we'll be fine :P +void PowerProfilesDaemon::switchToProfile(std::string const& str) { + auto pred = [str](Profile const& p) { return p.name == str; }; + this->activeProfile_ = std::find_if(availableProfiles_.begin(), availableProfiles_.end(), pred); + if (activeProfile_ == availableProfiles_.end()) { + spdlog::error( + "Power profile daemon: can't find the active profile {} in the available profiles list", + str); + } + dp.emit(); +} + +auto PowerProfilesDaemon::update() -> void { + if (connected_ && activeProfile_ != availableProfiles_.end()) { + auto profile = (*activeProfile_); + // Set label + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("profile", profile.name)); + store.push_back(fmt::arg("driver", profile.driver)); + store.push_back(fmt::arg("icon", getIcon(0, profile.name))); + label_.set_markup(fmt::vformat(format_, store)); + if (tooltipEnabled()) { + label_.set_tooltip_text(fmt::vformat(tooltipFormat_, store)); + } + + // Set CSS class + if (!currentStyle_.empty()) { + label_.get_style_context()->remove_class(currentStyle_); + } + label_.get_style_context()->add_class(profile.name); + currentStyle_ = profile.name; + event_box_.set_visible(true); + } else { + event_box_.set_visible(false); + } + + ALabel::update(); +} + +bool PowerProfilesDaemon::handleToggle(GdkEventButton* const& e) { + if (e->type == GdkEventType::GDK_BUTTON_PRESS && connected_) { + if (e->button == 1) /* left click */ { + activeProfile_++; + if (activeProfile_ == availableProfiles_.end()) { + activeProfile_ = availableProfiles_.begin(); + } + } else { + if (activeProfile_ == availableProfiles_.begin()) { + activeProfile_ = availableProfiles_.end(); + } + activeProfile_--; + } + + using VarStr = Glib::Variant; + using SetPowerProfileVar = Glib::Variant>; + VarStr activeProfileVariant = VarStr::create(activeProfile_->name); + auto callArgs = SetPowerProfileVar::create( + std::make_tuple("net.hadess.PowerProfiles", "ActiveProfile", activeProfileVariant)); + powerProfilesProxy_->call("org.freedesktop.DBus.Properties.Set", + sigc::mem_fun(*this, &PowerProfilesDaemon::setPropCb), callArgs); + } + return true; +} + +void PowerProfilesDaemon::setPropCb(Glib::RefPtr& r) { + try { + auto _ = powerProfilesProxy_->call_finish(r); + dp.emit(); + } catch (const std::exception& e) { + spdlog::error("Failed to set the 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..904c8fd9 --- /dev/null +++ b/src/modules/privacy/privacy.cpp @@ -0,0 +1,210 @@ +#include "modules/privacy/privacy.hpp" + +#include +#include + +#include + +#include "AModule.hpp" +#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, Gtk::Orientation orientation, + const std::string& pos) + : AModule(config, "privacy", id), + nodes_screenshare(), + nodes_audio_in(), + nodes_audio_out(), + visibility_conn(), + box_(orientation, 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.empty()) { + modules = Json::Value(Json::arrayValue); + for (const auto& type : {"screenshare", "audio-in"}) { + Json::Value obj = Json::Value(Json::objectValue); + obj["type"] = type; + modules.append(obj); + } + } + + std::map > typeMap = { + {"screenshare", {&nodes_screenshare, PRIVACY_NODE_TYPE_VIDEO_INPUT}}, + {"audio-in", {&nodes_audio_in, PRIVACY_NODE_TYPE_AUDIO_INPUT}}, + {"audio-out", {&nodes_audio_out, PRIVACY_NODE_TYPE_AUDIO_OUTPUT}}, + }; + + for (const auto& module : modules) { + if (!module.isObject() || !module["type"].isString()) continue; + const std::string type = module["type"].asString(); + + auto iter = typeMap.find(type); + if (iter != typeMap.end()) { + auto& [nodePtr, nodeType] = iter->second; + auto* item = Gtk::make_managed(module, nodeType, nodePtr, orientation, pos, + iconSize, transition_duration); + box_.add(*item); + } + } + + for (const auto& ignore_item : config_["ignore"]) { + if (!ignore_item.isObject() || !ignore_item["type"].isString() || + !ignore_item["name"].isString()) + continue; + const std::string type = ignore_item["type"].asString(); + const std::string name = ignore_item["name"].asString(); + + auto iter = typeMap.find(type); + if (iter != typeMap.end()) { + auto& [_, nodeType] = iter->second; + ignore.emplace(nodeType, std::move(name)); + } + } + + if (config_["ignore-monitor"].isBool()) { + ignore_monitor = config_["ignore-monitor"].asBool(); + } + + backend = util::PipewireBackend::PipewireBackend::getInstance(); + backend->privacy_nodes_changed_signal_event.connect( + sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); + + dp.emit(); +} + +void Privacy::onPrivacyNodesChanged() { + mutex_.lock(); + nodes_audio_out.clear(); + nodes_audio_in.clear(); + nodes_screenshare.clear(); + + for (auto& node : backend->privacy_nodes) { + if (ignore_monitor && node.second->is_monitor) continue; + + auto iter = ignore.find(std::pair(node.second->type, node.second->node_name)); + if (iter != ignore.end()) continue; + + switch (node.second->state) { + case PW_NODE_STATE_RUNNING: + switch (node.second->type) { + 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 { + // set in modules or not + bool setScreenshare = false; + bool setAudioIn = false; + bool setAudioOut = false; + + // used or not + bool useScreenshare = false; + bool useAudioIn = false; + bool useAudioOut = false; + + mutex_.lock(); + for (Gtk::Widget* widget : box_.get_children()) { + auto* module = dynamic_cast(widget); + if (module == nullptr) continue; + switch (module->privacy_type) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + setScreenshare = true; + useScreenshare = !nodes_screenshare.empty(); + module->set_in_use(useScreenshare); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + setAudioIn = true; + useAudioIn = !nodes_audio_in.empty(); + module->set_in_use(useAudioIn); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + setAudioOut = true; + useAudioOut = !nodes_audio_out.empty(); + module->set_in_use(useAudioOut); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + break; + } + } + mutex_.unlock(); + + // Hide the whole widget if none are in use + bool isVisible = (setScreenshare && useScreenshare) || (setAudioIn && useAudioIn) || + (setAudioOut && useAudioOut); + + if (isVisible != event_box_.get_visible()) { + // Disconnect any previous connection so that it doesn't get activated in + // the future, hiding the module when it should be visible + visibility_conn.disconnect(); + if (isVisible) { + event_box_.set_visible(true); + } else { + // Hides the widget when all of the privacy_item revealers animations + // have finished animating + visibility_conn = Glib::signal_timeout().connect( + sigc::track_obj( + [this, setScreenshare, setAudioOut, setAudioIn]() { + mutex_.lock(); + bool visible = false; + visible |= setScreenshare && !nodes_screenshare.empty(); + visible |= setAudioIn && !nodes_audio_in.empty(); + visible |= setAudioOut && !nodes_audio_out.empty(); + mutex_.unlock(); + event_box_.set_visible(visible); + 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..6424da9e --- /dev/null +++ b/src/modules/privacy/privacy_item.cpp @@ -0,0 +1,164 @@ +#include "modules/privacy/privacy_item.hpp" + +#include + +#include + +#include "glibmm/main.h" +#include "gtkmm/label.h" +#include "gtkmm/revealer.h" +#include "gtkmm/tooltip.h" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::modules::privacy { + +PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, + std::list *nodes_, Gtk::Orientation orientation, + 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(orientation == Gtk::ORIENTATION_HORIZONTAL + ? Gtk::REVEALER_TRANSITION_TYPE_SLIDE_RIGHT + : Gtk::REVEALER_TRANSITION_TYPE_SLIDE_DOWN); + } else if (pos == "modules-center") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_CROSSFADE); + } else if (pos == "modules-right") { + set_transition_type(orientation == Gtk::ORIENTATION_HORIZONTAL + ? Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT + : Gtk::REVEALER_TRANSITION_TYPE_SLIDE_UP); + } + set_transition_duration(transition_duration); + + box_.set_name("privacy-item"); + + // We use `set_center_widget` instead of `add` to make sure the icon is + // centered even if the orientation is vertical + box_.set_center_widget(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()) { + tooltip_window.remove(*child); + // despite the remove, still needs a delete to prevent memory leak. Speculating that this might + // work differently in GTK4. + delete child; + } + for (auto *node : *nodes) { + auto *box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL, 4); + + // Set device icon + auto *node_icon = Gtk::make_managed(); + node_icon->set_pixel_size(tooltipIconSize); + node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID); + box->add(*node_icon); + + // Set model + auto *nodeName = Gtk::make_managed(node->getName()); + box->add(*nodeName); + + 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..255ca571 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,176 +26,59 @@ 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; + auto sink_muted = backend->getSinkMuted(); + if (sink_muted) { + res.emplace_back(backend->getCurrentSinkName() + "-muted"); + } + res.push_back(backend->getCurrentSinkName()); + res.push_back(backend->getDefaultSourceName()); + std::string nameLC = backend->getSinkPortName() + backend->getFormFactor(); std::transform(nameLC.begin(), nameLC.end(), nameLC.begin(), ::tolower); for (auto const &port : ports) { if (nameLC.find(port) != std::string::npos) { + if (sink_muted) { + res.emplace_back(port + "-muted"); + } res.push_back(port); - return res; + break; } } + if (sink_muted) { + res.emplace_back("default-muted"); + } return res; } 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 +90,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,22 +99,27 @@ 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(); } } else { label_.get_style_context()->remove_class("source-muted"); - if (config_["format-source-muted"].isString()) { + if (config_["format-source"].isString()) { 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 +133,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..359e5a23 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 @@ -188,10 +189,20 @@ bool Tags::handle_button_press(GdkEventButton *event_button, uint32_t tag) { } void Tags::handle_focused_tags(uint32_t tags) { + auto hide_vacant = config_["hide-vacant"].asBool(); for (size_t i = 0; i < buttons_.size(); ++i) { + bool visible = buttons_[i].is_visible(); + bool occupied = buttons_[i].get_style_context()->has_class("occupied"); + bool urgent = buttons_[i].get_style_context()->has_class("urgent"); if ((1 << i) & tags) { + if (hide_vacant && !visible) { + buttons_[i].set_visible(true); + } buttons_[i].get_style_context()->add_class("focused"); } else { + if (hide_vacant && !(occupied || urgent)) { + buttons_[i].set_visible(false); + } buttons_[i].get_style_context()->remove_class("focused"); } } @@ -204,20 +215,40 @@ void Tags::handle_view_tags(struct wl_array *view_tags) { for (; view_tag < end; ++view_tag) { tags |= *view_tag; } + auto hide_vacant = config_["hide-vacant"].asBool(); for (size_t i = 0; i < buttons_.size(); ++i) { + bool visible = buttons_[i].is_visible(); + bool focused = buttons_[i].get_style_context()->has_class("focused"); + bool urgent = buttons_[i].get_style_context()->has_class("urgent"); if ((1 << i) & tags) { + if (hide_vacant && !visible) { + buttons_[i].set_visible(true); + } buttons_[i].get_style_context()->add_class("occupied"); } else { + if (hide_vacant && !(focused || urgent)) { + buttons_[i].set_visible(false); + } buttons_[i].get_style_context()->remove_class("occupied"); } } } void Tags::handle_urgent_tags(uint32_t tags) { + auto hide_vacant = config_["hide-vacant"].asBool(); for (size_t i = 0; i < buttons_.size(); ++i) { + bool visible = buttons_[i].is_visible(); + bool occupied = buttons_[i].get_style_context()->has_class("occupied"); + bool focused = buttons_[i].get_style_context()->has_class("focused"); if ((1 << i) & tags) { + if (hide_vacant && !visible) { + buttons_[i].set_visible(true); + } buttons_[i].get_style_context()->add_class("urgent"); } else { + if (hide_vacant && !(occupied || focused)) { + buttons_[i].set_visible(false); + } buttons_[i].get_style_context()->remove_class("urgent"); } } 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 fff8e019..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, @@ -19,9 +21,13 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, Host::~Host() { if (bus_name_id_ > 0) { - Gio::DBus::unwatch_name(bus_name_id_); + Gio::DBus::unown_name(bus_name_id_); bus_name_id_ = 0; } + if (watcher_id_ > 0) { + Gio::DBus::unwatch_name(watcher_id_); + watcher_id_ = 0; + } g_cancellable_cancel(cancellable_); g_clear_object(&cancellable_); g_clear_object(&watcher_); @@ -53,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_, @@ -72,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..407d7e72 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -5,24 +5,27 @@ #include #include +#include #include #include +#include "gdk/gdk.h" +#include "modules/sni/icon_manager.hpp" #include "util/format.hpp" #include "util/gtk_icon.hpp" template <> struct fmt::formatter : formatter { - bool is_printable(const Glib::VariantBase& value) { + bool is_printable(const Glib::VariantBase& value) const { auto type = value.get_type_string(); /* Print only primitive (single character excluding 'v') and short complex types */ return (type.length() == 1 && islower(type[0]) && type[0] != 'v') || value.get_size() <= 32; } template - auto format(const Glib::VariantBase& value, FormatContext& ctx) { + auto format(const Glib::VariantBase& value, FormatContext& ctx) const { 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 +42,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(); } @@ -56,6 +60,8 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf event_box.add_events(Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); event_box.signal_button_press_event().connect(sigc::mem_fun(*this, &Item::handleClick)); event_box.signal_scroll_event().connect(sigc::mem_fun(*this, &Item::handleScroll)); + event_box.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseEnter)); + event_box.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Item::handleMouseLeave)); // initial visibility event_box.show_all(); event_box.set_visible(show_passive_); @@ -68,6 +74,16 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf cancellable_, interface); } +bool Item::handleMouseEnter(GdkEventCrossing* const& e) { + event_box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + return false; +} + +bool Item::handleMouseLeave(GdkEventCrossing* const& e) { + event_box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + return false; +} + void Item::onConfigure(GdkEventConfigure* ev) { this->updateImage(); } void Item::proxyReady(Glib::RefPtr& result) { @@ -110,7 +126,8 @@ ToolTip get_variant(const Glib::VariantBase& value) { result.text = get_variant(container.get_child(2)); auto description = get_variant(container.get_child(3)); if (!description.empty()) { - result.text = fmt::format("{}\n{}", result.text, description); + auto escapedDescription = Glib::Markup::escape_text(description); + result.text = fmt::format("{}\n{}", result.text, escapedDescription); } return result; } @@ -123,6 +140,7 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { category = get_variant(value); } else if (name == "Id") { id = get_variant(value); + setCustomIcon(id); } else if (name == "Title") { title = get_variant(value); if (tooltip.text.empty()) { @@ -184,6 +202,19 @@ void Item::setStatus(const Glib::ustring& value) { style->add_class(lower); } +void Item::setCustomIcon(const std::string& id) { + std::string custom_icon = IconManager::instance().getIconForApp(id); + if (!custom_icon.empty()) { + if (std::filesystem::exists(custom_icon)) { + Glib::RefPtr custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon); + icon_name = ""; // icon_name has priority over pixmap + icon_pixmap = custom_pixbuf; + } else { // if file doesn't exist it's most likely an icon_name + icon_name = custom_icon; + } + } +} + void Item::getUpdatedProperties() { auto params = Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(SNI_INTERFACE_NAME)}); @@ -355,33 +386,19 @@ 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 (!icon_theme_path.empty()) { + auto icon_info = icon_theme->lookup_icon(name.c_str(), request_size, + Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + if (icon_info) { + bool is_sym = false; + return icon_info.load_symbolic(event_box.get_style_context(), is_sym); } } - if (tmp_size == 0) { - tmp_size = request_size; - } - if (!icon_theme_path.empty() && - icon_theme->lookup_icon(name.c_str(), tmp_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE)) { - return icon_theme->load_icon(name.c_str(), tmp_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); - } - return DefaultGtkIconThemeWrapper::load_icon(name.c_str(), tmp_size, - Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); + return DefaultGtkIconThemeWrapper::load_icon(name.c_str(), request_size, + Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE, + event_box.get_style_context()); } double Item::getScaledIconSize() { @@ -410,7 +427,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..f657c855 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -2,11 +2,13 @@ #include +#include "modules/sni/icon_manager.hpp" + 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,10 +17,14 @@ 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()); } nb_hosts_ += 1; + if (config_["icons"].isObject()) { + IconManager::instance().setIconsConfig(config_["icons"]); + } dp.emit(); } @@ -38,7 +44,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 663fdcdc..324bd9f5 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() @@ -14,6 +16,10 @@ Watcher::Watcher() watcher_(sn_watcher_skeleton_new()) {} Watcher::~Watcher() { + if (hosts_ != nullptr) { + g_slist_free_full(hosts_, gfWatchFree); + hosts_ = nullptr; + } if (items_ != nullptr) { g_slist_free_full(items_, gfWatchFree); items_ = nullptr; @@ -25,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) { @@ -32,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", @@ -57,10 +67,9 @@ gboolean Watcher::handleRegisterHost(Watcher* obj, GDBusMethodInvocation* invoca } auto watch = gfWatchFind(obj->hosts_, bus_name, object_path); if (watch != nullptr) { - g_dbus_method_invocation_return_error( - invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, - "Status Notifier Host with bus name '%s' and object path '%s' is already registered", - bus_name, object_path); + g_warning("Status Notifier Host with bus name '%s' and object path '%s' is already registered", + bus_name, object_path); + sn_watcher_complete_register_item(obj->watcher_, invocation); return TRUE; } watch = gfWatchNew(GF_WATCH_TYPE_HOST, service, bus_name, object_path, obj); diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index 5c3df7b2..4139a53b 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -1,6 +1,7 @@ #include "modules/sway/ipc/client.hpp" #include +#include #include @@ -17,12 +18,16 @@ Ipc::~Ipc() { if (fd_ > 0) { // To fail the IPC header - write(fd_, "close-sway-ipc", 14); + if (write(fd_, "close-sway-ipc", 14) == -1) { + spdlog::error("Failed to close sway IPC"); + } close(fd_); fd_ = -1; } if (fd_event_ > 0) { - write(fd_event_, "close-sway-ipc", 14); + if (write(fd_event_, "close-sway-ipc", 14) == -1) { + spdlog::error("Failed to close sway IPC event handler"); + } close(fd_event_); fd_event_ = -1; } diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index a5860bd0..f4cfa6c3 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -19,12 +19,13 @@ 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); + displayed_short_flag |= static_cast(DisplayedShortFlag::ShortName); } if (format_.find("{shortDescription}") != std::string::npos) { - displayed_short_flag |= static_cast(DispayedShortFlag::ShortDescription); + displayed_short_flag |= static_cast(DisplayedShortFlag::ShortDescription); } if (config.isMember("tooltip-format")) { tooltip_format_ = config["tooltip-format"].asString(); @@ -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/window.cpp b/src/modules/sway/window.cpp index 50aea602..25e430a7 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -17,13 +17,7 @@ namespace waybar::modules::sway { Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) - : AIconLabel(config, "window", id, "{}", 0, true), bar_(bar), windowId_(-1) { - // Icon size - if (config_["icon-size"].isUInt()) { - app_icon_size_ = config["icon-size"].asUInt(); - } - image_.set_pixel_size(app_icon_size_); - + : AAppIconLabel(config, "window", id, "{}", 0, true), bar_(bar), windowId_(-1) { ipc_.subscribe(R"(["window","workspace"])"); ipc_.signal_event.connect(sigc::mem_fun(*this, &Window::onEvent)); ipc_.signal_cmd.connect(sigc::mem_fun(*this, &Window::onCmd)); @@ -49,7 +43,7 @@ void Window::onCmd(const struct Ipc::ipc_response& res) { auto output = payload["output"].isString() ? payload["output"].asString() : ""; std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_) = getFocusedNode(payload["nodes"], output); - updateAppIconName(); + updateAppIconName(app_id_, app_class_); dp.emit(); } catch (const std::exception& e) { spdlog::error("Window: {}", e.what()); @@ -57,105 +51,6 @@ void Window::onCmd(const struct Ipc::ipc_response& res) { } } -std::optional getDesktopFilePath(const std::string& app_id, - const std::string& app_class) { - 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_id + ".desktop"; - if (std::filesystem::exists(desktop_file_path)) { - return desktop_file_path; - } - if (!app_class.empty()) { - desktop_file_path = data_app_dir + app_class + ".desktop"; - if (std::filesystem::exists(desktop_file_path)) { - return desktop_file_path; - } - } - } - return {}; -} - -std::optional getIconName(const std::string& app_id, const std::string& app_class) { - const auto desktop_file_path = getDesktopFilePath(app_id, app_class); - if (!desktop_file_path.has_value()) { - // Try some heuristics to find a matching icon - - if (DefaultGtkIconThemeWrapper::has_icon(app_id)) { - return app_id; - } - - const auto app_id_desktop = app_id + "-desktop"; - if (DefaultGtkIconThemeWrapper::has_icon(app_id_desktop)) { - return app_id_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_id.find_first_of(' '); - if (first_space != std::string::npos) { - const auto first_word = to_lower(app_id.substr(0, first_space)); - if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { - return first_word; - } - } - - const auto first_dash = app_id.find_first_of('-'); - if (first_dash != std::string::npos) { - const auto first_word = to_lower(app_id.substr(0, first_dash)); - if (DefaultGtkIconThemeWrapper::has_icon(first_word)) { - return first_word; - } - } - - return {}; - } - - try { - Glib::KeyFile desktop_file; - desktop_file.load_from_file(desktop_file_path.value()); - return desktop_file.get_string("Desktop Entry", "Icon"); - } catch (Glib::FileError& error) { - spdlog::warn("Error while loading desktop file {}: {}", desktop_file_path.value(), - error.what().c_str()); - } catch (Glib::KeyFileError& error) { - spdlog::warn("Error while loading desktop file {}: {}", desktop_file_path.value(), - error.what().c_str()); - } - return {}; -} - -void Window::updateAppIconName() { - if (!iconEnabled()) { - return; - } - - const auto icon_name = getIconName(app_id_, app_class_); - if (icon_name.has_value()) { - app_icon_name_ = icon_name.value(); - } else { - app_icon_name_ = ""; - } - update_app_icon_ = true; -} - -void Window::updateAppIcon() { - if (update_app_icon_) { - update_app_icon_ = false; - if (app_icon_name_.empty()) { - image_.set_visible(false); - } else { - image_.set_from_icon_name(app_icon_name_, Gtk::ICON_SIZE_INVALID); - image_.set_visible(true); - } - } -} - auto Window::update() -> void { spdlog::trace("workspace layout {}, tiled count {}, floating count {}", layout_, app_nb_, floating_count_); @@ -210,7 +105,7 @@ auto Window::update() -> void { updateAppIcon(); // Call parent update - AIconLabel::update(); + AAppIconLabel::update(); } void Window::setClass(std::string classname, bool enable) { @@ -250,6 +145,40 @@ std::pair leafNodesInWorkspace(const Json::Value& node) { return {sum, floating_sum}; } +std::optional> getSingleChildNode( + const Json::Value& node) { + auto const& nodes = node["nodes"]; + if (nodes.empty()) { + if (node["type"].asString() == "workspace") + return {}; + else if (node["type"].asString() == "floating_con") { + return {}; + } else { + return {std::cref(node)}; + } + } + auto it = std::cbegin(nodes); + if (it == std::cend(nodes)) { + return {}; + } + auto const& child = *it; + ++it; + if (it != std::cend(nodes)) { + return {}; + } + return {getSingleChildNode(child)}; +} + +std::tuple getWindowInfo(const Json::Value& node) { + const auto app_id = node["app_id"].isString() ? node["app_id"].asString() + : node["window_properties"]["instance"].asString(); + const auto app_class = node["window_properties"]["class"].isString() + ? node["window_properties"]["class"].asString() + : ""; + const auto shell = node["shell"].isString() ? node["shell"].asString() : ""; + return {app_id, app_class, shell}; +} + std::tuple gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Value& config_, const Bar& bar_, Json::Value& parentWorkspace, @@ -286,12 +215,7 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu // found node spdlog::trace("actual output {}, output found {}, node (focused) found {}", bar_.output->name, output, node["name"].asString()); - auto app_id = node["app_id"].isString() ? node["app_id"].asString() - : node["window_properties"]["instance"].asString(); - const auto app_class = node["window_properties"]["class"].isString() - ? node["window_properties"]["class"].asString() - : ""; - const auto shell = node["shell"].isString() ? node["shell"].asString() : ""; + const auto [app_id, app_class, shell] = getWindowInfo(node); int nb = node.size(); int floating_count = 0; std::string workspace_layout = ""; @@ -331,15 +255,24 @@ gfnWithWorkspace(const Json::Value& nodes, std::string& output, const Json::Valu std::pair all_leaf_nodes = leafNodesInWorkspace(immediateParent); // using an empty string as default ensures that no window depending styles are set due to the // checks above for !name.empty() + std::string app_id = ""; + std::string app_class = ""; + std::string workspace_layout = ""; + if (all_leaf_nodes.first == 1) { + const auto single_child = getSingleChildNode(immediateParent); + if (single_child.has_value()) { + std::tie(app_id, app_class, workspace_layout) = getWindowInfo(single_child.value()); + } + } return {all_leaf_nodes.first, all_leaf_nodes.second, 0, (all_leaf_nodes.first > 0 || all_leaf_nodes.second > 0) ? config_["offscreen-css-text"].asString() : "", - "", - "", - "", + app_id, + app_class, + workspace_layout, immediateParent["layout"].asString()}; } diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index c1cfd5a4..7a8ce571 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -11,32 +11,69 @@ namespace waybar::modules::sway { // Helper function to assign a number to a workspace, just like sway. In fact // this is taken quite verbatim from `sway/ipc-json.c`. int Workspaces::convertWorkspaceNameToNum(std::string name) { - if (isdigit(name[0])) { + if (isdigit(name[0]) != 0) { errno = 0; - char *endptr = NULL; + char *endptr = nullptr; long long parsed_num = strtoll(name.c_str(), &endptr, 10); if (errno != 0 || parsed_num > INT32_MAX || parsed_num < 0 || endptr == name.c_str()) { return -1; - } else { - return (int)parsed_num; } + return (int)parsed_num; } 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 (const auto &it : config["format-icons"]["high-priority-named"]) { + high_priority_named_.push_back(it.asString()); + } + } box_.set_name("workspaces"); if (!id.empty()) { box_.get_style_context()->add_class(id); } + box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); + if (config_["format-window-separator"].isString()) { + m_formatWindowSeparator = config_["format-window-separator"].asString(); + } else { + m_formatWindowSeparator = " "; + } + const Json::Value &windowRewrite = config["window-rewrite"]; + if (windowRewrite.isObject()) { + const Json::Value &windowRewriteDefaultConfig = config["window-rewrite-default"]; + std::string windowRewriteDefault = + windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?"; + m_windowRewriteRules = waybar::util::RegexCollection( + windowRewrite, std::move(windowRewriteDefault), windowRewritePriorityFunction); + } 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); @@ -54,39 +91,52 @@ 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; }); + 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_)); + } + // adding persistent workspaces (as per the config file) - if (config_["persistent_workspaces"].isObject()) { - const Json::Value &p_workspaces = config_["persistent_workspaces"]; + if (config_["persistent-workspaces"].isObject()) { + const Json::Value &p_workspaces = config_["persistent-workspaces"]; const std::vector p_workspaces_names = p_workspaces.getMemberNames(); for (const std::string &p_w_name : p_workspaces_names) { const Json::Value &p_w = p_workspaces[p_w_name]; - auto it = - std::find_if(payload.begin(), payload.end(), [&p_w_name](const Json::Value &node) { - return node["name"].asString() == p_w_name; - }); + auto it = std::find_if(workspaces_.begin(), workspaces_.end(), + [&p_w_name](const Json::Value &node) { + return node["name"].asString() == p_w_name; + }); - if (it != payload.end()) { + if (it != workspaces_.end()) { continue; // already displayed by some bar } @@ -189,6 +239,49 @@ 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; + } + if (std::any_of(node["floating_nodes"].begin(), node["floating_nodes"].end(), + [&](auto const &e) { return hasFlag(e, flag); })) { + return true; + } + return false; +} + +void Workspaces::updateWindows(const Json::Value &node, std::string &windows) { + 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"].isString() + ? node["app_id"].asString() + : node["window_properties"]["class"].asString(); + + // Only add window rewrites that can be looked up + if (!windowClass.empty()) { + 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_formatWindowSeparator); + } + } + 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(); @@ -198,17 +291,21 @@ 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()); + } + bool noNodes = (*it)["nodes"].empty() && (*it)["floating_nodes"].empty(); + if (hasFlag((*it), "focused")) { button.get_style_context()->add_class("focused"); } else { button.get_style_context()->remove_class("focused"); } - if ((*it)["visible"].asBool()) { + if (hasFlag((*it), "visible") || ((*it)["output"].isString() && noNodes)) { 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"); @@ -218,6 +315,11 @@ auto Workspaces::update() -> void { } else { button.get_style_context()->remove_class("persistent"); } + if (noNodes) { + button.get_style_context()->add_class("empty"); + } else { + button.get_style_context()->remove_class("empty"); + } if ((*it)["output"].isString()) { if (((*it)["output"].asString()) == bar_.output->name) { button.get_style_context()->add_class("current_output"); @@ -227,16 +329,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_formatWindowSeparator.length())), + fmt::arg("output", (*it)["output"].asString())); } if (!config_["disable-markup"].asBool()) { static_cast(button.get_children()[0])->set_markup(output); @@ -279,13 +384,28 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { } std::string Workspaces::getIcon(const std::string &name, const Json::Value &node) { - std::vector keys = {"urgent", "focused", name, "visible", "default"}; + std::vector keys = {"high-priority-named", "urgent", "focused", name, "default"}; for (auto const &key : keys) { - if (key == "focused" || key == "visible" || key == "urgent") { - if (config_["format-icons"][key].isString() && node[key].asBool()) { + if (key == "high-priority-named") { + auto it = std::find_if(high_priority_named_.begin(), high_priority_named_.end(), + [&](const std::string &member) { return member == name; }); + if (it != high_priority_named_.end()) { + return config_["format-icons"][name].asString(); + } + + it = std::find_if(high_priority_named_.begin(), high_priority_named_.end(), + [&](const std::string &member) { + return trimWorkspaceName(member) == trimWorkspaceName(name); + }); + if (it != high_priority_named_.end()) { + return config_["format-icons"][trimWorkspaceName(name)].asString(); + } + } + if (key == "focused" || key == "urgent") { + if (config_["format-icons"][key].isString() && hasFlag(node, key)) { 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()) { @@ -298,7 +418,7 @@ std::string Workspaces::getIcon(const std::string &name, const Json::Value &node } bool Workspaces::handleScroll(GdkEventScroll *e) { - if (gdk_event_get_pointer_emulated((GdkEvent *)e)) { + if (gdk_event_get_pointer_emulated((GdkEvent *)e) != 0) { /** * Ignore emulated scroll events on window */ @@ -310,9 +430,16 @@ bool Workspaces::handleScroll(GdkEventScroll *e) { } std::string name; { + bool alloutputs = config_["all-outputs"].asBool(); std::lock_guard lock(mutex_); - auto it = std::find_if(workspaces_.begin(), workspaces_.end(), - [](const auto &workspace) { return workspace["focused"].asBool(); }); + auto it = + std::find_if(workspaces_.begin(), workspaces_.end(), [alloutputs](const auto &workspace) { + if (alloutputs) { + return hasFlag(workspace, "focused"); + } + bool noNodes = workspace["nodes"].empty() && workspace["floating_nodes"].empty(); + return hasFlag(workspace, "visible") || (workspace["output"].isString() && noNodes); + }); if (it == workspaces_.end()) { return true; } @@ -327,22 +454,21 @@ bool Workspaces::handleScroll(GdkEventScroll *e) { return true; } } - if (!config_["warp-on-scroll"].asBool()) { - ipc_.sendCmd(IPC_COMMAND, fmt::format("mouse_warping none")); + if (!config_["warp-on-scroll"].isNull() && !config_["warp-on-scroll"].asBool()) { + ipc_.sendCmd(IPC_COMMAND, fmt::format("mouse_warping none")); } try { ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, "--no-auto-back-and-forth", name)); } catch (const std::exception &e) { spdlog::error("Workspaces: {}", e.what()); } - if (!config_["warp-on-scroll"].asBool()) { - ipc_.sendCmd(IPC_COMMAND, fmt::format("mouse_warping container")); + if (!config_["warp-on-scroll"].isNull() && !config_["warp-on-scroll"].asBool()) { + ipc_.sendCmd(IPC_COMMAND, fmt::format("mouse_warping container")); } return true; } -const std::string Workspaces::getCycleWorkspace(std::vector::iterator it, - bool prev) const { +std::string Workspaces::getCycleWorkspace(std::vector::iterator it, bool prev) const { if (prev && it == workspaces_.begin() && !config_["disable-scroll-wraparound"].asBool()) { return (*(--workspaces_.end()))["name"].asString(); } @@ -368,9 +494,34 @@ std::string Workspaces::trimWorkspaceName(std::string name) { return name; } +bool is_focused_recursive(const Json::Value &node) { + // If a workspace has a focused container then get_tree will say + // that the workspace itself isn't focused. Therefore we need to + // check if any of its nodes are focused as well. + // some layouts like tabbed have many nested nodes + // all nested nodes must be checked for focused flag + if (node["focused"].asBool()) { + return true; + } + + for (const auto &child : node["nodes"]) { + if (is_focused_recursive(child)) { + return true; + } + } + + for (const auto &child : node["floating_nodes"]) { + if (is_focused_recursive(child)) { + return true; + } + } + + return false; +} + void Workspaces::onButtonReady(const Json::Value &node, Gtk::Button &button) { if (config_["current-only"].asBool()) { - if (node["focused"].asBool()) { + if (is_focused_recursive(node)) { button.show(); } else { button.hide(); diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp new file mode 100644 index 00000000..90f33be7 --- /dev/null +++ b/src/modules/systemd_failed_units.cpp @@ -0,0 +1,162 @@ +#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), + nr_failed(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::RequestSystemState() { + auto load = [](const char* kind, Glib::RefPtr& proxy) -> std::string { + try { + if (!proxy) return "unknown"; + auto parameters = Glib::VariantContainerBase( + g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState")); + Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); + if (data && data.is_of_type(Glib::VariantType("(v)"))) { + Glib::VariantBase variant; + g_variant_get(data.gobj_copy(), "(v)", &variant); + if (variant && variant.is_of_type(Glib::VARIANT_TYPE_STRING)) { + return g_variant_get_string(variant.gobj_copy(), NULL); + } + } + } catch (Glib::Error& e) { + spdlog::error("Failed to get {} state: {}", kind, e.what().c_str()); + } + return "unknown"; + }; + + system_state = load("systemwide", system_proxy); + user_state = load("user", user_proxy); + if (system_state == "running" && user_state == "running") + overall_state = "ok"; + else + overall_state = "degraded"; +} + +void SystemdFailedUnits::RequestFailedUnits() { + auto load = [](const char* kind, Glib::RefPtr& proxy) -> uint32_t { + try { + if (!proxy) return 0; + auto parameters = Glib::VariantContainerBase( + g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "NFailedUnits")); + Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); + 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)) { + return g_variant_get_uint32(variant.gobj_copy()); + } + } + } catch (Glib::Error& e) { + spdlog::error("Failed to get {} failed units: {}", kind, e.what().c_str()); + } + return 0; + }; + + nr_failed_system = load("systemwide", system_proxy); + nr_failed_user = load("user", user_proxy); + nr_failed = nr_failed_system + nr_failed_user; +} + +void SystemdFailedUnits::updateData() { + update_pending = false; + + RequestSystemState(); + if (overall_state == "degraded") RequestFailedUnits(); + + dp.emit(); +} + +auto SystemdFailedUnits::update() -> void { + if (last_status == overall_state) return; + + // Hide if needed. + if (overall_state == "ok" && hide_on_ok) { + event_box_.set_visible(false); + return; + } + + event_box_.set_visible(true); + + // Set state class. + 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(overall_state)) { + label_.get_style_context()->add_class(overall_state); + } + + last_status = overall_state; + + label_.set_markup(fmt::format( + fmt::runtime(nr_failed == 0 ? format_ok : format_), fmt::arg("nr_failed", nr_failed), + fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user), + fmt::arg("system_state", system_state), fmt::arg("user_state", user_state), + fmt::arg("overall_state", overall_state))); + ALabel::update(); +} + +} // namespace waybar::modules diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index 5ef2f4c9..b1241ba3 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -1,6 +1,7 @@ #include "modules/temperature.hpp" #include +#include #if defined(__FreeBSD__) #include @@ -9,34 +10,53 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Value& config) : ALabel(config, "temperature", id, "{temperatureC}°C", 10) { #if defined(__FreeBSD__) -// try to read sysctl? +// FreeBSD uses sysctlbyname instead of read from a file #else - auto& hwmon_path = config_["hwmon-path"]; - if (hwmon_path.isString()) { - file_path_ = hwmon_path.asString(); - } else if (hwmon_path.isArray()) { - // if hwmon_path is an array, loop to find first valid item - for (auto& item : hwmon_path) { - auto path = item.asString(); - if (std::filesystem::exists(path)) { - file_path_ = path; - break; - } - } - } else if (config_["hwmon-path-abs"].isString() && config_["input-filename"].isString()) { - file_path_ = (*std::filesystem::directory_iterator(config_["hwmon-path-abs"].asString())) - .path() - .string() + - "/" + config_["input-filename"].asString(); - } else { + auto traverseAsArray = [](const Json::Value& value, auto&& check_set_path) { + if (value.isString()) + check_set_path(value.asString()); + else if (value.isArray()) + for (const auto& item : value) + if (check_set_path(item.asString())) break; + }; + + // if hwmon_path is an array, loop to find first valid item + traverseAsArray(config_["hwmon-path"], [this](const std::string& path) { + if (!std::filesystem::exists(path)) return false; + file_path_ = path; + return true; + }); + + if (file_path_.empty() && config_["input-filename"].isString()) { + // fallback to hwmon_paths-abs + traverseAsArray(config_["hwmon-path-abs"], [this](const std::string& path) { + if (!std::filesystem::is_directory(path)) return false; + return std::ranges::any_of( + std::filesystem::directory_iterator(path), [this](const auto& hwmon) { + if (!hwmon.path().filename().string().starts_with("hwmon")) return false; + file_path_ = hwmon.path().string() + "/" + config_["input-filename"].asString(); + return true; + }); + }); + } + + if (file_path_.empty()) { auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); } + + // check if file_path_ can be used to retrieve the temperature std::ifstream temp(file_path_); if (!temp.is_open()) { throw std::runtime_error("Can't open " + file_path_); } + if (!temp.good()) { + temp.close(); + throw std::runtime_error("Can't read from " + file_path_); + } + temp.close(); #endif + thread_ = [this] { dp.emit(); thread_.sleep_for(interval_); @@ -49,21 +69,26 @@ auto waybar::modules::Temperature::update() -> void { uint16_t temperature_f = std::round(temperature * 1.8 + 32); uint16_t temperature_k = std::round(temperature + 273.15); auto critical = isCritical(temperature_c); + auto warning = isWarning(temperature_c); auto format = format_; if (critical) { format = config_["format-critical"].isString() ? config_["format-critical"].asString() : format; label_.get_style_context()->add_class("critical"); + } else if (warning) { + format = config_["format-warning"].isString() ? config_["format-warning"].asString() : format; + label_.get_style_context()->add_class("warning"); } else { label_.get_style_context()->remove_class("critical"); + label_.get_style_context()->remove_class("warning"); } if (format.empty()) { event_box_.hide(); return; - } else { - event_box_.show(); } + event_box_.show(); + auto max_temp = config_["critical-threshold"].isInt() ? config_["critical-threshold"].asInt() : 0; label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("temperatureC", temperature_c), fmt::arg("temperatureF", temperature_f), @@ -88,14 +113,18 @@ float waybar::modules::Temperature::getTemperature() { size_t size = sizeof temp; auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; - auto sysctl_thermal = fmt::format("hw.acpi.thermal.tz{}.temperature", zone); - if (sysctlbyname("hw.acpi.thermal.tz0.temperature", &temp, &size, NULL, 0) != 0) { - throw std::runtime_error( - "sysctl hw.acpi.thermal.tz0.temperature or dev.cpu.0.temperature failed"); + // First, try with dev.cpu + if ((sysctlbyname(fmt::format("dev.cpu.{}.temperature", zone).c_str(), &temp, &size, NULL, 0) == + 0) || + (sysctlbyname(fmt::format("hw.acpi.thermal.tz{}.temperature", zone).c_str(), &temp, &size, + NULL, 0) == 0)) { + auto temperature_c = ((float)temp - 2732) / 10; + return temperature_c; } - auto temperature_c = ((float)temp - 2732) / 10; - return temperature_c; + + throw std::runtime_error(fmt::format( + "sysctl hw.acpi.thermal.tz{}.temperature and dev.cpu.{}.temperature failed", zone, zone)); #else // Linux std::ifstream temp(file_path_); @@ -105,6 +134,9 @@ float waybar::modules::Temperature::getTemperature() { std::string line; if (temp.good()) { getline(temp, line); + } else { + temp.close(); + throw std::runtime_error("Can't read from " + file_path_); } temp.close(); auto temperature_c = std::strtol(line.c_str(), nullptr, 10) / 1000.0; @@ -112,6 +144,11 @@ float waybar::modules::Temperature::getTemperature() { #endif } +bool waybar::modules::Temperature::isWarning(uint16_t temperature_c) { + return config_["warning-threshold"].isInt() && + temperature_c >= config_["warning-threshold"].asInt(); +} + bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) { return config_["critical-threshold"].isInt() && temperature_c >= config_["critical-threshold"].asInt(); diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp new file mode 100644 index 00000000..4b832b7e --- /dev/null +++ b/src/modules/upower.cpp @@ -0,0 +1,498 @@ +#include "modules/upower.hpp" + +#include +#include +#include + +namespace waybar::modules { + +UPower::UPower(const std::string &id, const Json::Value &config) + : AIconLabel(config, "upower", id, "{percentage}", 0, true, true, true), sleeping_{false} { + box_.set_name(name_); + box_.set_spacing(0); + box_.set_has_tooltip(AModule::tooltipEnabled()); + // Tooltip box + contentBox_.set_orientation((box_.get_orientation() == Gtk::ORIENTATION_HORIZONTAL) + ? Gtk::ORIENTATION_VERTICAL + : Gtk::ORIENTATION_HORIZONTAL); + // Get current theme + gtkTheme_ = Gtk::IconTheme::get_default(); + + // Icon Size + if (config_["icon-size"].isInt()) { + iconSize_ = config_["icon-size"].asInt(); + } + image_.set_pixel_size(iconSize_); + + // Show icon only when "show-icon" isn't set to false + if (config_["show-icon"].isBool()) showIcon_ = config_["show-icon"].asBool(); + if (!showIcon_) box_.remove(image_); + // Device user wants + if (config_["native-path"].isString()) nativePath_ = config_["native-path"].asString(); + // Device model user wants + if (config_["model"].isString()) model_ = config_["model"].asString(); + + // Hide If Empty + if (config_["hide-if-empty"].isBool()) hideIfEmpty_ = config_["hide-if-empty"].asBool(); + + // Tooltip Spacing + if (config_["tooltip-spacing"].isInt()) tooltip_spacing_ = config_["tooltip-spacing"].asInt(); + + // Tooltip Padding + if (config_["tooltip-padding"].isInt()) { + tooltip_padding_ = config_["tooltip-padding"].asInt(); + contentBox_.set_margin_top(tooltip_padding_); + contentBox_.set_margin_bottom(tooltip_padding_); + contentBox_.set_margin_left(tooltip_padding_); + contentBox_.set_margin_right(tooltip_padding_); + } + + // Tooltip Format + if (config_["tooltip-format"].isString()) tooltipFormat_ = config_["tooltip-format"].asString(); + + // Start watching DBUS + watcherID_ = Gio::DBus::watch_name( + Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.UPower", + sigc::mem_fun(*this, &UPower::onAppear), sigc::mem_fun(*this, &UPower::onVanished), + Gio::DBus::BusNameWatcherFlags::BUS_NAME_WATCHER_FLAGS_AUTO_START); + // Get DBus async connect + Gio::DBus::Connection::get(Gio::DBus::BusType::BUS_TYPE_SYSTEM, + sigc::mem_fun(*this, &UPower::getConn_cb)); + + // Make UPower client + GError **gErr = NULL; + upClient_ = up_client_new_full(NULL, gErr); + if (upClient_ == NULL) + spdlog::error("Upower. UPower client connection error. {}", (*gErr)->message); + + // Subscribe UPower events + g_signal_connect(upClient_, "device-added", G_CALLBACK(deviceAdded_cb), this); + g_signal_connect(upClient_, "device-removed", G_CALLBACK(deviceRemoved_cb), this); + + // Subscribe tooltip query events + box_.set_has_tooltip(); + box_.signal_query_tooltip().connect(sigc::mem_fun(*this, &UPower::queryTooltipCb), false); + + resetDevices(); + setDisplayDevice(); + // Update the widget + dp.emit(); +} + +UPower::~UPower() { + if (upDevice_.upDevice != NULL) g_object_unref(upDevice_.upDevice); + if (upClient_ != NULL) g_object_unref(upClient_); + if (subscrID_ > 0u) { + conn_->signal_unsubscribe(subscrID_); + subscrID_ = 0u; + } + Gio::DBus::unwatch_name(watcherID_); + watcherID_ = 0u; + removeDevices(); +} + +static const std::string getDeviceStatus(UpDeviceState &state) { + switch (state) { + case UP_DEVICE_STATE_CHARGING: + case UP_DEVICE_STATE_PENDING_CHARGE: + return "charging"; + case UP_DEVICE_STATE_DISCHARGING: + case UP_DEVICE_STATE_PENDING_DISCHARGE: + return "discharging"; + case UP_DEVICE_STATE_FULLY_CHARGED: + return "full"; + case UP_DEVICE_STATE_EMPTY: + return "empty"; + default: + return "unknown-status"; + } +} + +static const std::string getDeviceIcon(UpDeviceKind &kind) { + switch (kind) { + case UP_DEVICE_KIND_LINE_POWER: + return "ac-adapter-symbolic"; + case UP_DEVICE_KIND_BATTERY: + return "battery-symbolic"; + case UP_DEVICE_KIND_UPS: + return "uninterruptible-power-supply-symbolic"; + case UP_DEVICE_KIND_MONITOR: + return "video-display-symbolic"; + case UP_DEVICE_KIND_MOUSE: + return "input-mouse-symbolic"; + case UP_DEVICE_KIND_KEYBOARD: + return "input-keyboard-symbolic"; + case UP_DEVICE_KIND_PDA: + return "pda-symbolic"; + case UP_DEVICE_KIND_PHONE: + return "phone-symbolic"; + case UP_DEVICE_KIND_MEDIA_PLAYER: + return "multimedia-player-symbolic"; + case UP_DEVICE_KIND_TABLET: + return "computer-apple-ipad-symbolic"; + case UP_DEVICE_KIND_COMPUTER: + return "computer-symbolic"; + case UP_DEVICE_KIND_GAMING_INPUT: + return "input-gaming-symbolic"; + case UP_DEVICE_KIND_PEN: + return "input-tablet-symbolic"; + case UP_DEVICE_KIND_TOUCHPAD: + return "input-touchpad-symbolic"; + case UP_DEVICE_KIND_MODEM: + return "modem-symbolic"; + case UP_DEVICE_KIND_NETWORK: + return "network-wired-symbolic"; + case UP_DEVICE_KIND_HEADSET: + return "audio-headset-symbolic"; + case UP_DEVICE_KIND_HEADPHONES: + return "audio-headphones-symbolic"; + case UP_DEVICE_KIND_OTHER_AUDIO: + case UP_DEVICE_KIND_SPEAKERS: + return "audio-speakers-symbolic"; + case UP_DEVICE_KIND_VIDEO: + return "camera-web-symbolic"; + case UP_DEVICE_KIND_PRINTER: + return "printer-symbolic"; + case UP_DEVICE_KIND_SCANNER: + return "scanner-symbolic"; + case UP_DEVICE_KIND_CAMERA: + return "camera-photo-symbolic"; + case UP_DEVICE_KIND_BLUETOOTH_GENERIC: + return "bluetooth-active-symbolic"; + case UP_DEVICE_KIND_TOY: + case UP_DEVICE_KIND_REMOTE_CONTROL: + case UP_DEVICE_KIND_WEARABLE: + case UP_DEVICE_KIND_LAST: + default: + return "battery-symbolic"; + } +} + +static std::string secondsToString(const std::chrono::seconds sec) { + const auto ds{std::chrono::duration_cast(sec)}; + const auto hrs{std::chrono::duration_cast(sec - ds)}; + const auto min{std::chrono::duration_cast(sec - ds - hrs)}; + std::string_view strRet{(ds.count() > 0) ? "{D}d {H}h {M}min" + : (hrs.count() > 0) ? "{H}h {M}min" + : (min.count() > 0) ? "{M}min" + : ""}; + spdlog::debug( + "UPower::secondsToString(). seconds: \"{0}\", minutes: \"{1}\", hours: \"{2}\", \ +days: \"{3}\", strRet: \"{4}\"", + sec.count(), min.count(), hrs.count(), ds.count(), strRet); + return fmt::format(fmt::runtime(strRet), fmt::arg("D", ds.count()), fmt::arg("H", hrs.count()), + fmt::arg("M", min.count())); +} + +auto UPower::update() -> void { + std::lock_guard guard{mutex_}; + // Don't update widget if the UPower service isn't running + if (!upRunning_ || sleeping_) { + if (hideIfEmpty_) box_.hide(); + return; + } + + getUpDeviceInfo(upDevice_); + + if (upDevice_.upDevice == NULL && hideIfEmpty_) { + box_.hide(); + return; + } + /* Every Device which is handled by Upower and which is not + * UP_DEVICE_KIND_UNKNOWN (0) or UP_DEVICE_KIND_LINE_POWER (1) is a Battery + */ + const bool upDeviceValid{upDevice_.kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN && + upDevice_.kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER}; + // Get CSS status + const auto status{getDeviceStatus(upDevice_.state)}; + // Remove last status if it exists + if (!lastStatus_.empty() && box_.get_style_context()->has_class(lastStatus_)) + box_.get_style_context()->remove_class(lastStatus_); + if (!box_.get_style_context()->has_class(status)) box_.get_style_context()->add_class(status); + lastStatus_ = status; + + if (devices_.size() == 0 && !upDeviceValid && hideIfEmpty_) { + box_.hide(); + // Call parent update + AModule::update(); + return; + } + + label_.set_markup(getText(upDevice_, format_)); + // Set icon + if (upDevice_.icon_name == NULL || !gtkTheme_->has_icon(upDevice_.icon_name)) + upDevice_.icon_name = (char *)NO_BATTERY.c_str(); + image_.set_from_icon_name(upDevice_.icon_name, Gtk::ICON_SIZE_INVALID); + + box_.show(); + + // Call parent update + ALabel::update(); +} + +void UPower::getConn_cb(Glib::RefPtr &result) { + try { + conn_ = Gio::DBus::Connection::get_finish(result); + // Subscribe DBUs events + subscrID_ = conn_->signal_subscribe(sigc::mem_fun(*this, &UPower::prepareForSleep_cb), + "org.freedesktop.login1", "org.freedesktop.login1.Manager", + "PrepareForSleep", "/org/freedesktop/login1"); + + } catch (const Glib::Error &e) { + spdlog::error("Upower. DBus connection error. {}", e.what().c_str()); + } +} + +void UPower::onAppear(const Glib::RefPtr &conn, const Glib::ustring &name, + const Glib::ustring &name_owner) { + upRunning_ = true; +} + +void UPower::onVanished(const Glib::RefPtr &conn, + const Glib::ustring &name) { + upRunning_ = false; +} + +void UPower::prepareForSleep_cb(const Glib::RefPtr &connection, + const Glib::ustring &sender_name, const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &signal_name, + const Glib::VariantContainerBase ¶meters) { + if (parameters.is_of_type(Glib::VariantType("(b)"))) { + Glib::Variant sleeping; + parameters.get_child(sleeping, 0); + if (!sleeping.get()) { + resetDevices(); + setDisplayDevice(); + sleeping_ = false; + // Update the widget + dp.emit(); + } else + sleeping_ = true; + } +} + +void UPower::deviceAdded_cb(UpClient *client, UpDevice *device, gpointer data) { + UPower *up{static_cast(data)}; + up->addDevice(device); + up->setDisplayDevice(); + // Update the widget + up->dp.emit(); +} + +void UPower::deviceRemoved_cb(UpClient *client, const gchar *objectPath, gpointer data) { + UPower *up{static_cast(data)}; + up->removeDevice(objectPath); + up->setDisplayDevice(); + // Update the widget + up->dp.emit(); +} + +void UPower::deviceNotify_cb(UpDevice *device, GParamSpec *pspec, gpointer data) { + UPower *up{static_cast(data)}; + // Update the widget + up->dp.emit(); +} + +void UPower::addDevice(UpDevice *device) { + std::lock_guard guard{mutex_}; + + if (G_IS_OBJECT(device)) { + const gchar *objectPath{up_device_get_object_path(device)}; + + // Due to the device getting cleared after this event is fired, we + // create a new object pointing to its objectPath + device = up_device_new(); + upDevice_output upDevice{.upDevice = device}; + gboolean ret{up_device_set_object_path_sync(device, objectPath, NULL, NULL)}; + if (!ret) { + g_object_unref(G_OBJECT(device)); + return; + } + + if (devices_.find(objectPath) != devices_.cend()) { + auto upDevice{devices_[objectPath]}; + if (G_IS_OBJECT(upDevice.upDevice)) g_object_unref(upDevice.upDevice); + devices_.erase(objectPath); + } + + g_signal_connect(device, "notify", G_CALLBACK(deviceNotify_cb), this); + devices_.emplace(Devices::value_type(objectPath, upDevice)); + } +} + +void UPower::removeDevice(const gchar *objectPath) { + std::lock_guard guard{mutex_}; + if (devices_.find(objectPath) != devices_.cend()) { + auto upDevice{devices_[objectPath]}; + if (G_IS_OBJECT(upDevice.upDevice)) g_object_unref(upDevice.upDevice); + devices_.erase(objectPath); + } +} + +void UPower::removeDevices() { + std::lock_guard guard{mutex_}; + if (!devices_.empty()) { + auto it{devices_.cbegin()}; + while (it != devices_.cend()) { + if (G_IS_OBJECT(it->second.upDevice)) g_object_unref(it->second.upDevice); + devices_.erase(it++); + } + } +} + +// Removes all devices and adds the current devices +void UPower::resetDevices() { + // Remove all devices + removeDevices(); + + // Adds all devices + GPtrArray *newDevices = up_client_get_devices2(upClient_); + if (newDevices != NULL) + for (guint i{0}; i < newDevices->len; ++i) { + UpDevice *device{(UpDevice *)g_ptr_array_index(newDevices, i)}; + if (device && G_IS_OBJECT(device)) addDevice(device); + } +} + +void UPower::setDisplayDevice() { + std::lock_guard guard{mutex_}; + + if (upDevice_.upDevice != NULL) { + g_object_unref(upDevice_.upDevice); + upDevice_.upDevice = NULL; + } + + if (nativePath_.empty() && model_.empty()) { + upDevice_.upDevice = up_client_get_display_device(upClient_); + getUpDeviceInfo(upDevice_); + } else { + g_ptr_array_foreach( + up_client_get_devices2(upClient_), + [](gpointer data, gpointer user_data) { + upDevice_output upDevice; + auto thisPtr{static_cast(user_data)}; + upDevice.upDevice = static_cast(data); + thisPtr->getUpDeviceInfo(upDevice); + upDevice_output displayDevice{NULL}; + if (!thisPtr->nativePath_.empty()) { + if (upDevice.nativePath == nullptr) return; + if (0 == std::strcmp(upDevice.nativePath, thisPtr->nativePath_.c_str())) { + displayDevice = upDevice; + } + } else { + if (upDevice.model == nullptr) return; + if (0 == std::strcmp(upDevice.model, thisPtr->model_.c_str())) { + displayDevice = upDevice; + } + } + // Unref current upDevice if it exists + if (displayDevice.upDevice != NULL) { + thisPtr->upDevice_ = displayDevice; + } + }, + this); + } + + if (upDevice_.upDevice != NULL) + g_signal_connect(upDevice_.upDevice, "notify", G_CALLBACK(deviceNotify_cb), this); +} + +void UPower::getUpDeviceInfo(upDevice_output &upDevice_) { + if (upDevice_.upDevice != NULL && G_IS_OBJECT(upDevice_.upDevice)) { + g_object_get(upDevice_.upDevice, "kind", &upDevice_.kind, "state", &upDevice_.state, + "percentage", &upDevice_.percentage, "icon-name", &upDevice_.icon_name, + "time-to-empty", &upDevice_.time_empty, "time-to-full", &upDevice_.time_full, + "temperature", &upDevice_.temperature, "native-path", &upDevice_.nativePath, + "model", &upDevice_.model, NULL); + spdlog::debug( + "UPower. getUpDeviceInfo. kind: \"{0}\". state: \"{1}\". percentage: \"{2}\". \ +icon_name: \"{3}\". time-to-empty: \"{4}\". time-to-full: \"{5}\". temperature: \"{6}\". \ +native_path: \"{7}\". model: \"{8}\"", + fmt::format_int(upDevice_.kind).str(), fmt::format_int(upDevice_.state).str(), + upDevice_.percentage, upDevice_.icon_name, upDevice_.time_empty, upDevice_.time_full, + upDevice_.temperature, upDevice_.nativePath, upDevice_.model); + } +} + +const Glib::ustring UPower::getText(const upDevice_output &upDevice_, const std::string &format) { + Glib::ustring ret{""}; + if (upDevice_.upDevice != NULL) { + std::string timeStr{""}; + switch (upDevice_.state) { + case UP_DEVICE_STATE_CHARGING: + case UP_DEVICE_STATE_PENDING_CHARGE: + timeStr = secondsToString(std::chrono::seconds(upDevice_.time_full)); + break; + case UP_DEVICE_STATE_DISCHARGING: + case UP_DEVICE_STATE_PENDING_DISCHARGE: + timeStr = secondsToString(std::chrono::seconds(upDevice_.time_empty)); + break; + default: + break; + } + + ret = fmt::format( + fmt::runtime(format), + fmt::arg("percentage", std::to_string((int)std::round(upDevice_.percentage)) + '%'), + fmt::arg("time", timeStr), + fmt::arg("temperature", fmt::format("{:-.2g}C", upDevice_.temperature)), + fmt::arg("model", upDevice_.model), fmt::arg("native-path", upDevice_.nativePath)); + } + + return ret; +} + +bool UPower::queryTooltipCb(int x, int y, bool keyboard_tooltip, + const Glib::RefPtr &tooltip) { + std::lock_guard guard{mutex_}; + + // Clear content box + contentBox_.forall([this](Gtk::Widget &wg) { contentBox_.remove(wg); }); + + // Fill content box with the content + for (auto pairDev : devices_) { + // Get device info + getUpDeviceInfo(pairDev.second); + + if (pairDev.second.kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN && + pairDev.second.kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER) { + // Make box record + Gtk::Box *boxRec{new Gtk::Box{box_.get_orientation(), tooltip_spacing_}}; + contentBox_.add(*boxRec); + Gtk::Box *boxDev{new Gtk::Box{box_.get_orientation()}}; + Gtk::Box *boxUsr{new Gtk::Box{box_.get_orientation()}}; + boxRec->add(*boxDev); + boxRec->add(*boxUsr); + // Construct device box + // Set icon from kind + std::string iconNameDev{getDeviceIcon(pairDev.second.kind)}; + if (!gtkTheme_->has_icon(iconNameDev)) iconNameDev = (char *)NO_BATTERY.c_str(); + Gtk::Image *iconDev{new Gtk::Image{}}; + iconDev->set_from_icon_name(iconNameDev, Gtk::ICON_SIZE_INVALID); + iconDev->set_pixel_size(iconSize_); + boxDev->add(*iconDev); + // Set label from model + Gtk::Label *labelDev{new Gtk::Label{pairDev.second.model}}; + boxDev->add(*labelDev); + // Construct user box + // Set icon from icon state + if (pairDev.second.icon_name == NULL || !gtkTheme_->has_icon(pairDev.second.icon_name)) + pairDev.second.icon_name = (char *)NO_BATTERY.c_str(); + Gtk::Image *iconTooltip{new Gtk::Image{}}; + iconTooltip->set_from_icon_name(pairDev.second.icon_name, Gtk::ICON_SIZE_INVALID); + iconTooltip->set_pixel_size(iconSize_); + boxUsr->add(*iconTooltip); + // Set markup text + Gtk::Label *labelTooltip{new Gtk::Label{}}; + labelTooltip->set_markup(getText(pairDev.second, tooltipFormat_)); + boxUsr->add(*labelTooltip); + } + } + tooltip->set_custom(contentBox_); + contentBox_.show_all(); + + return true; +} + +} // namespace waybar::modules diff --git a/src/modules/upower/upower.cpp b/src/modules/upower/upower.cpp deleted file mode 100644 index 1262d0a1..00000000 --- a/src/modules/upower/upower.cpp +++ /dev/null @@ -1,384 +0,0 @@ -#include "modules/upower/upower.hpp" - -#include - -#include -#include - -#include "gtkmm/tooltip.h" -#include "util/gtk_icon.hpp" - -namespace waybar::modules::upower { -UPower::UPower(const std::string& id, const Json::Value& config) - : AModule(config, "upower", id), - box_(Gtk::ORIENTATION_HORIZONTAL, 0), - icon_(), - label_(), - devices(), - m_Mutex(), - client(), - showAltText(false) { - box_.pack_start(icon_); - box_.pack_start(label_); - box_.set_name(name_); - event_box_.add(box_); - - // Device user wants - if (config_["native-path"].isString()) nativePath_ = config_["native-path"].asString(); - // Icon Size - if (config_["icon-size"].isUInt()) { - iconSize = config_["icon-size"].asUInt(); - } - icon_.set_pixel_size(iconSize); - - // Hide If Empty - if (config_["hide-if-empty"].isBool()) { - hideIfEmpty = config_["hide-if-empty"].asBool(); - } - - // Format - if (config_["format"].isString()) { - format = config_["format"].asString(); - } - - // Format Alt - if (config_["format-alt"].isString()) { - format_alt = config_["format-alt"].asString(); - } - - // Tooltip Spacing - if (config_["tooltip-spacing"].isUInt()) { - tooltip_spacing = config_["tooltip-spacing"].asUInt(); - } - - // Tooltip Padding - if (config_["tooltip-padding"].isUInt()) { - tooltip_padding = config_["tooltip-padding"].asUInt(); - } - - // Tooltip - if (config_["tooltip"].isBool()) { - tooltip_enabled = config_["tooltip"].asBool(); - } - box_.set_has_tooltip(tooltip_enabled); - if (tooltip_enabled) { - // Sets the window to use when showing the tooltip - upower_tooltip = new UPowerTooltip(iconSize, tooltip_spacing, tooltip_padding); - box_.set_tooltip_window(*upower_tooltip); - box_.signal_query_tooltip().connect(sigc::mem_fun(*this, &UPower::show_tooltip_callback)); - } - - upowerWatcher_id = g_bus_watch_name(G_BUS_TYPE_SYSTEM, "org.freedesktop.UPower", - G_BUS_NAME_WATCHER_FLAGS_AUTO_START, upowerAppear, - upowerDisappear, this, NULL); - - GError* error = NULL; - client = up_client_new_full(NULL, &error); - 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); - if (!login1_connection) { - throw std::runtime_error("Unable to connect to the SYSTEM Bus!..."); - } else { - login1_id = g_dbus_connection_signal_subscribe( - login1_connection, "org.freedesktop.login1", "org.freedesktop.login1.Manager", - "PrepareForSleep", "/org/freedesktop/login1", NULL, G_DBUS_SIGNAL_FLAGS_NONE, - prepareForSleep_cb, this, NULL); - } - - event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &UPower::handleToggle)); - - g_signal_connect(client, "device-added", G_CALLBACK(deviceAdded_cb), this); - g_signal_connect(client, "device-removed", G_CALLBACK(deviceRemoved_cb), this); - - resetDevices(); - setDisplayDevice(); -} - -UPower::~UPower() { - if (client != NULL) g_object_unref(client); - if (login1_id > 0) { - g_dbus_connection_signal_unsubscribe(login1_connection, login1_id); - login1_id = 0; - } - g_bus_unwatch_name(upowerWatcher_id); - removeDevices(); -} - -void UPower::deviceAdded_cb(UpClient* client, UpDevice* device, gpointer data) { - UPower* up = static_cast(data); - up->addDevice(device); - up->setDisplayDevice(); - // Update the widget - up->dp.emit(); -} -void UPower::deviceRemoved_cb(UpClient* client, const gchar* objectPath, gpointer data) { - UPower* up = static_cast(data); - up->removeDevice(objectPath); - up->setDisplayDevice(); - // Update the widget - up->dp.emit(); -} -void UPower::deviceNotify_cb(UpDevice* device, GParamSpec* pspec, gpointer data) { - UPower* up = static_cast(data); - // Update the widget - up->dp.emit(); -} -void UPower::prepareForSleep_cb(GDBusConnection* system_bus, const gchar* sender_name, - const gchar* object_path, const gchar* interface_name, - const gchar* signal_name, GVariant* parameters, gpointer data) { - if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)"))) { - gboolean sleeping; - g_variant_get(parameters, "(b)", &sleeping); - - if (!sleeping) { - UPower* up = static_cast(data); - up->resetDevices(); - up->setDisplayDevice(); - } - } -} -void UPower::upowerAppear(GDBusConnection* conn, const gchar* name, const gchar* name_owner, - gpointer data) { - UPower* up = static_cast(data); - up->upowerRunning = true; - up->event_box_.set_visible(true); -} -void UPower::upowerDisappear(GDBusConnection* conn, const gchar* name, gpointer data) { - UPower* up = static_cast(data); - up->upowerRunning = false; - up->event_box_.set_visible(false); -} - -void UPower::removeDevice(const gchar* objectPath) { - std::lock_guard guard(m_Mutex); - if (devices.find(objectPath) != devices.end()) { - UpDevice* device = devices[objectPath]; - if (G_IS_OBJECT(device)) { - g_object_unref(device); - } - devices.erase(objectPath); - } -} - -void UPower::addDevice(UpDevice* device) { - if (G_IS_OBJECT(device)) { - const gchar* objectPath = up_device_get_object_path(device); - - // Due to the device getting cleared after this event is fired, we - // create a new object pointing to its objectPath - gboolean ret; - device = up_device_new(); - ret = up_device_set_object_path_sync(device, objectPath, NULL, NULL); - if (!ret) { - g_object_unref(G_OBJECT(device)); - return; - } - - std::lock_guard guard(m_Mutex); - - if (devices.find(objectPath) != devices.end()) { - UpDevice* device = devices[objectPath]; - if (G_IS_OBJECT(device)) { - g_object_unref(device); - } - devices.erase(objectPath); - } - - g_signal_connect(device, "notify", G_CALLBACK(deviceNotify_cb), this); - devices.emplace(Devices::value_type(objectPath, device)); - } -} - -void UPower::setDisplayDevice() { - std::lock_guard guard(m_Mutex); - - if (nativePath_.empty()) - displayDevice = up_client_get_display_device(client); - else { - g_ptr_array_foreach( - up_client_get_devices2(client), - [](gpointer data, gpointer user_data) { - UpDevice* device{static_cast(data)}; - UPower* thisPtr{static_cast(user_data)}; - gchar* nativePath; - if (!thisPtr->displayDevice) { - g_object_get(device, "native-path", &nativePath, NULL); - if (!std::strcmp(nativePath, thisPtr->nativePath_.c_str())) - thisPtr->displayDevice = device; - } - }, - this); - } - - if (displayDevice) g_signal_connect(displayDevice, "notify", G_CALLBACK(deviceNotify_cb), this); -} - -void UPower::removeDevices() { - std::lock_guard guard(m_Mutex); - if (!devices.empty()) { - auto it = devices.cbegin(); - while (it != devices.cend()) { - if (G_IS_OBJECT(it->second)) { - g_object_unref(it->second); - } - devices.erase(it++); - } - } -} - -/** Removes all devices and adds the current devices */ -void UPower::resetDevices() { - // Removes all devices - removeDevices(); - - // Adds all devices - GPtrArray* newDevices = up_client_get_devices2(client); - for (guint i = 0; i < newDevices->len; i++) { - UpDevice* device = (UpDevice*)g_ptr_array_index(newDevices, i); - if (device && G_IS_OBJECT(device)) addDevice(device); - } - - // Update the widget - dp.emit(); -} - -bool UPower::show_tooltip_callback(int, int, bool, const Glib::RefPtr& tooltip) { - return true; -} - -const std::string UPower::getDeviceStatus(UpDeviceState& state) { - switch (state) { - case UP_DEVICE_STATE_CHARGING: - case UP_DEVICE_STATE_PENDING_CHARGE: - return "charging"; - case UP_DEVICE_STATE_DISCHARGING: - case UP_DEVICE_STATE_PENDING_DISCHARGE: - return "discharging"; - case UP_DEVICE_STATE_FULLY_CHARGED: - return "full"; - case UP_DEVICE_STATE_EMPTY: - return "empty"; - default: - return "unknown-status"; - } -} - -bool UPower::handleToggle(GdkEventButton* const& event) { - std::lock_guard guard(m_Mutex); - showAltText = !showAltText; - return AModule::handleToggle(event); -} - -std::string UPower::timeToString(gint64 time) { - if (time == 0) return ""; - float hours = (float)time / 3600; - float hours_fixed = static_cast(static_cast(hours * 10)) / 10; - float minutes = static_cast(static_cast(hours * 60 * 10)) / 10; - if (hours_fixed >= 1) { - return fmt::format("{H} h", fmt::arg("H", hours_fixed)); - } else { - return fmt::format("{M} min", fmt::arg("M", minutes)); - } -} - -auto UPower::update() -> void { - std::lock_guard guard(m_Mutex); - - // Don't update widget if the UPower service isn't running - if (!upowerRunning) return; - - UpDeviceKind kind; - UpDeviceState state; - double percentage; - gint64 time_empty; - gint64 time_full; - gchar* icon_name{(char*)'\0'}; - std::string percentString{""}; - std::string time_format{""}; - - bool displayDeviceValid{false}; - - if (displayDevice) { - g_object_get(displayDevice, "kind", &kind, "state", &state, "percentage", &percentage, - "icon-name", &icon_name, "time-to-empty", &time_empty, "time-to-full", &time_full, - NULL); - /* Every Device which is handled by Upower and which is not - * UP_DEVICE_KIND_UNKNOWN (0) or UP_DEVICE_KIND_LINE_POWER (1) is a Battery - */ - displayDeviceValid = (kind != UpDeviceKind::UP_DEVICE_KIND_UNKNOWN && - kind != UpDeviceKind::UP_DEVICE_KIND_LINE_POWER); - } - - // CSS status class - const std::string status = getDeviceStatus(state); - // Remove last status if it exists - if (!lastStatus.empty() && box_.get_style_context()->has_class(lastStatus)) { - box_.get_style_context()->remove_class(lastStatus); - } - // Add the new status class to the Box - if (!box_.get_style_context()->has_class(status)) { - box_.get_style_context()->add_class(status); - } - lastStatus = status; - - if (devices.size() == 0 && !displayDeviceValid && hideIfEmpty) { - event_box_.set_visible(false); - // Call parent update - AModule::update(); - return; - } - - event_box_.set_visible(true); - - if (displayDeviceValid) { - // Tooltip - if (tooltip_enabled) { - uint tooltipCount = upower_tooltip->updateTooltip(devices); - // Disable the tooltip if there aren't any devices in the tooltip - box_.set_has_tooltip(!devices.empty() && tooltipCount > 0); - } - - // Set percentage - percentString = std::to_string(int(percentage + 0.5)) + "%"; - - // Label format - switch (state) { - case UP_DEVICE_STATE_CHARGING: - case UP_DEVICE_STATE_PENDING_CHARGE: - time_format = timeToString(time_full); - break; - case UP_DEVICE_STATE_DISCHARGING: - case UP_DEVICE_STATE_PENDING_DISCHARGE: - time_format = timeToString(time_empty); - break; - default: - break; - } - } - std::string label_format = - fmt::format(fmt::runtime(showAltText ? format_alt : format), - fmt::arg("percentage", percentString), fmt::arg("time", time_format)); - // Only set the label text if it doesn't only contain spaces - bool onlySpaces = true; - for (auto& character : label_format) { - if (character == ' ') continue; - onlySpaces = false; - break; - } - label_.set_markup(onlySpaces ? "" : label_format); - - // Set icon - if (icon_name == NULL || !DefaultGtkIconThemeWrapper::has_icon(icon_name)) { - icon_name = (char*)"battery-missing-symbolic"; - } - icon_.set_from_icon_name(icon_name, Gtk::ICON_SIZE_INVALID); - - // Call parent update - AModule::update(); -} - -} // namespace waybar::modules::upower diff --git a/src/modules/upower/upower_tooltip.cpp b/src/modules/upower/upower_tooltip.cpp deleted file mode 100644 index 45544bbc..00000000 --- a/src/modules/upower/upower_tooltip.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "modules/upower/upower_tooltip.hpp" - -#include "gtkmm/box.h" -#include "gtkmm/enums.h" -#include "gtkmm/image.h" -#include "gtkmm/label.h" -#include "util/gtk_icon.hpp" - -namespace waybar::modules::upower { -UPowerTooltip::UPowerTooltip(uint iconSize_, uint tooltipSpacing_, uint tooltipPadding_) - : Gtk::Window(), - 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); - contentBox->set_margin_left(tooltipPadding); - contentBox->set_margin_right(tooltipPadding); - - add(*contentBox); - contentBox->show(); -} - -UPowerTooltip::~UPowerTooltip() {} - -uint UPowerTooltip::updateTooltip(Devices& devices) { - // Removes all old devices - for (auto child : contentBox->get_children()) { - delete child; - } - - uint deviceCount = 0; - // Adds all valid devices - for (auto pair : devices) { - UpDevice* device = pair.second; - std::string objectPath = pair.first; - - if (!G_IS_OBJECT(device)) continue; - - Gtk::Box* box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, tooltipSpacing); - - UpDeviceKind kind; - double percentage; - gchar* native_path; - gchar* model; - gchar* icon_name; - - g_object_get(device, "kind", &kind, "percentage", &percentage, "native-path", &native_path, - "model", &model, "icon-name", &icon_name, NULL); - - // Skip Line_Power and BAT0 devices - if (kind == UP_DEVICE_KIND_LINE_POWER || native_path == NULL || strlen(native_path) == 0 || - strcmp(native_path, "BAT0") == 0) - continue; - - Gtk::Box* modelBox = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); - box->add(*modelBox); - // Set device icon - std::string deviceIconName = getDeviceIcon(kind); - Gtk::Image* deviceIcon = new Gtk::Image(); - deviceIcon->set_pixel_size(iconSize); - if (!DefaultGtkIconThemeWrapper::has_icon(deviceIconName)) { - deviceIconName = "battery-missing-symbolic"; - } - deviceIcon->set_from_icon_name(deviceIconName, Gtk::ICON_SIZE_INVALID); - modelBox->add(*deviceIcon); - - // Set model - if (model == NULL) model = (gchar*)""; - Gtk::Label* modelLabel = new Gtk::Label(model); - modelBox->add(*modelLabel); - - Gtk::Box* chargeBox = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); - box->add(*chargeBox); - - // Set icon - Gtk::Image* icon = new Gtk::Image(); - icon->set_pixel_size(iconSize); - if (icon_name == NULL || !DefaultGtkIconThemeWrapper::has_icon(icon_name)) { - icon_name = (char*)"battery-missing-symbolic"; - } - icon->set_from_icon_name(icon_name, Gtk::ICON_SIZE_INVALID); - chargeBox->add(*icon); - - // Set percentage - std::string percentString = std::to_string(int(percentage + 0.5)) + "%"; - Gtk::Label* percentLabel = new Gtk::Label(percentString); - chargeBox->add(*percentLabel); - - contentBox->add(*box); - - deviceCount++; - } - - contentBox->show_all(); - return deviceCount; -} - -const std::string UPowerTooltip::getDeviceIcon(UpDeviceKind& kind) { - switch (kind) { - case UP_DEVICE_KIND_LINE_POWER: - return "ac-adapter-symbolic"; - case UP_DEVICE_KIND_BATTERY: - return "battery"; - case UP_DEVICE_KIND_UPS: - return "uninterruptible-power-supply-symbolic"; - case UP_DEVICE_KIND_MONITOR: - return "video-display-symbolic"; - case UP_DEVICE_KIND_MOUSE: - return "input-mouse-symbolic"; - case UP_DEVICE_KIND_KEYBOARD: - return "input-keyboard-symbolic"; - case UP_DEVICE_KIND_PDA: - return "pda-symbolic"; - case UP_DEVICE_KIND_PHONE: - return "phone-symbolic"; - case UP_DEVICE_KIND_MEDIA_PLAYER: - return "multimedia-player-symbolic"; - case UP_DEVICE_KIND_TABLET: - return "computer-apple-ipad-symbolic"; - case UP_DEVICE_KIND_COMPUTER: - return "computer-symbolic"; - case UP_DEVICE_KIND_GAMING_INPUT: - return "input-gaming-symbolic"; - case UP_DEVICE_KIND_PEN: - return "input-tablet-symbolic"; - case UP_DEVICE_KIND_TOUCHPAD: - return "input-touchpad-symbolic"; - case UP_DEVICE_KIND_MODEM: - return "modem-symbolic"; - case UP_DEVICE_KIND_NETWORK: - return "network-wired-symbolic"; - case UP_DEVICE_KIND_HEADSET: - return "audio-headset-symbolic"; - case UP_DEVICE_KIND_HEADPHONES: - return "audio-headphones-symbolic"; - case UP_DEVICE_KIND_OTHER_AUDIO: - case UP_DEVICE_KIND_SPEAKERS: - return "audio-speakers-symbolic"; - case UP_DEVICE_KIND_VIDEO: - return "camera-web-symbolic"; - case UP_DEVICE_KIND_PRINTER: - return "printer-symbolic"; - case UP_DEVICE_KIND_SCANNER: - return "scanner-symbolic"; - case UP_DEVICE_KIND_CAMERA: - return "camera-photo-symbolic"; - case UP_DEVICE_KIND_BLUETOOTH_GENERIC: - return "bluetooth-active-symbolic"; - case UP_DEVICE_KIND_TOY: - case UP_DEVICE_KIND_REMOTE_CONTROL: - case UP_DEVICE_KIND_WEARABLE: - case UP_DEVICE_KIND_LAST: - default: - return "battery-symbolic"; - } -} -} // namespace waybar::modules::upower diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index b2d9b39d..106ca403 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -4,6 +4,8 @@ bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; } +std::list waybar::modules::Wireplumber::modules; + waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Value& config) : ALabel(config, "wireplumber", id, "{volume}%"), wp_core_(nullptr), @@ -16,87 +18,95 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val muted_(false), volume_(0.0), min_step_(0.0), - node_id_(0) { + node_id_(0), + type_(nullptr) { + waybar::modules::Wireplumber::modules.push_back(this); + wp_init(WP_INIT_PIPEWIRE); - wp_core_ = wp_core_new(NULL, NULL); + wp_core_ = wp_core_new(nullptr, nullptr, nullptr); apis_ = g_ptr_array_new_with_free_func(g_object_unref); om_ = wp_object_manager_new(); - prepare(); + type_ = g_strdup(config_["node-type"].isString() ? config_["node-type"].asString().c_str() + : "Audio/Sink"); - loadRequiredApiModules(); + prepare(this); - spdlog::debug("[{}]: connecting to pipewire...", this->name_); + spdlog::debug("[{}]: connecting to pipewire: '{}'...", name_, type_); - if (!wp_core_connect(wp_core_)) { - spdlog::error("[{}]: Could not connect to PipeWire", this->name_); + if (wp_core_connect(wp_core_) == 0) { + spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_); throw std::runtime_error("Could not connect to PipeWire\n"); } - spdlog::debug("[{}]: connected!", this->name_); + spdlog::debug("[{}]: {} connected!", name_, type_); g_signal_connect_swapped(om_, "installed", (GCallback)onObjectManagerInstalled, this); - activatePlugins(); - - dp.emit(); - - event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); - event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Wireplumber::handleScroll)); + asyncLoadRequiredApiModules(); } waybar::modules::Wireplumber::~Wireplumber() { + waybar::modules::Wireplumber::modules.remove(this); + wp_core_disconnect(wp_core_); g_clear_pointer(&apis_, g_ptr_array_unref); g_clear_object(&om_); g_clear_object(&wp_core_); g_clear_object(&mixer_api_); g_clear_object(&def_nodes_api_); g_free(default_node_name_); + g_free(type_); } void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* self, uint32_t id) { - spdlog::debug("[{}]: updating node name with node.id {}", self->name_, id); + spdlog::debug("[{}]: updating '{}' node name with node.id {}", self->name_, self->type_, id); if (!isValidNodeId(id)) { - spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node name update.", self->name_, id); + spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring '{}' node name update.", self->name_, + id, self->type_); return; } - auto proxy = static_cast(wp_object_manager_lookup( - self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, NULL)); + auto* proxy = static_cast(wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY, + WP_CONSTRAINT_TYPE_G_PROPERTY, + "bound-id", "=u", id, nullptr)); - if (!proxy) { + if (proxy == nullptr) { auto err = fmt::format("Object '{}' not found\n", id); spdlog::error("[{}]: {}", self->name_, err); throw std::runtime_error(err); } g_autoptr(WpProperties) properties = - WP_IS_PIPEWIRE_OBJECT(proxy) ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy)) - : wp_properties_new_empty(); - g_autoptr(WpProperties) global_p = wp_global_proxy_get_global_properties(WP_GLOBAL_PROXY(proxy)); + WP_IS_PIPEWIRE_OBJECT(proxy) != 0 + ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy)) + : wp_properties_new_empty(); + g_autoptr(WpProperties) globalP = wp_global_proxy_get_global_properties(WP_GLOBAL_PROXY(proxy)); properties = wp_properties_ensure_unique_owner(properties); - wp_properties_add(properties, global_p); - wp_properties_set(properties, "object.id", NULL); - auto nick = wp_properties_get(properties, "node.nick"); - auto description = wp_properties_get(properties, "node.description"); + wp_properties_add(properties, globalP); + wp_properties_set(properties, "object.id", nullptr); + const auto* nick = wp_properties_get(properties, "node.nick"); + const auto* description = wp_properties_get(properties, "node.description"); - self->node_name_ = nick ? nick : description; - spdlog::debug("[{}]: Updating node name to: {}", self->name_, self->node_name_); + self->node_name_ = nick != nullptr ? nick + : description != nullptr ? description + : "Unknown node name"; + spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_); } void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self, uint32_t id) { spdlog::debug("[{}]: updating volume", self->name_); - GVariant* variant = NULL; + GVariant* variant = nullptr; if (!isValidNodeId(id)) { - spdlog::error("[{}]: '{}' is not a valid node ID. Ignoring volume update.", self->name_, id); + spdlog::error("[{}]: '{}' is not a valid '{}' node ID. Ignoring volume update.", self->name_, + id, self->type_); return; } g_signal_emit_by_name(self->mixer_api_, "get-volume", id, &variant); - if (!variant) { + if (variant == nullptr) { auto err = fmt::format("Node {} does not support volume\n", id); spdlog::error("[{}]: {}", self->name_, err); throw std::runtime_error(err); @@ -111,13 +121,22 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se } void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id) { - spdlog::debug("[{}]: (onMixerChanged) - id: {}", self->name_, id); - g_autoptr(WpNode) node = static_cast(wp_object_manager_lookup( - self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, NULL)); + self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", id, nullptr)); - if (!node) { - spdlog::warn("[{}]: (onMixerChanged) - Object with id {} not found", self->name_, id); + if (node == nullptr) { + // log a warning only if no other widget is targeting the id. + // this reduces log spam when multiple instances of the module are used on different node types. + if (id != self->node_id_) { + for (auto const& module : waybar::modules::Wireplumber::modules) { + if (module->node_id_ == id) { + return; + } + } + } + + spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_, + self->type_, id); return; } @@ -125,63 +144,66 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* if (self->node_id_ != id) { spdlog::debug( - "[{}]: (onMixerChanged) - ignoring mixer update for node: id: {}, name: {} as it is not " - "the default node: {} with id: {}", - self->name_, id, name, self->default_node_name_, self->node_id_); + "[{}]: (onMixerChanged: {}) - ignoring mixer update for node: id: {}, name: {} as it is " + "not the default node: {} with id: {}", + self->name_, self->type_, id, name, self->default_node_name_, self->node_id_); return; } - spdlog::debug("[{}]: (onMixerChanged) - Need to update volume for node with id {} and name {}", - self->name_, id, name); + spdlog::debug( + "[{}]: (onMixerChanged: {}) - Need to update volume for node with id {} and name {}", + self->name_, self->type_, id, name); updateVolume(self, id); } void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wireplumber* self) { - spdlog::debug("[{}]: (onDefaultNodesApiChanged)", self->name_); + spdlog::debug("[{}]: (onDefaultNodesApiChanged: {})", self->name_, self->type_); - uint32_t default_node_id; - g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &default_node_id); + uint32_t defaultNodeId; + g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &defaultNodeId); - if (!isValidNodeId(default_node_id)) { - spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring node change.", self->name_, - default_node_id); + if (!isValidNodeId(defaultNodeId)) { + spdlog::warn("[{}]: '{}' is not a valid node ID. Ignoring '{}' node change.", self->name_, + defaultNodeId, self->type_); return; } g_autoptr(WpNode) node = static_cast( wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", - "=u", default_node_id, NULL)); + "=u", defaultNodeId, nullptr)); - if (!node) { - spdlog::warn("[{}]: (onDefaultNodesApiChanged) - Object with id {} not found", self->name_, - default_node_id); + if (node == nullptr) { + spdlog::warn("[{}]: (onDefaultNodesApiChanged: {}) - Object with id {} not found", self->name_, + self->type_, defaultNodeId); return; } - const gchar* default_node_name = + const gchar* defaultNodeName = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name"); spdlog::debug( - "[{}]: (onDefaultNodesApiChanged) - got the following default node: Node(name: {}, id: {})", - self->name_, default_node_name, default_node_id); + "[{}]: (onDefaultNodesApiChanged: {}) - got the following default node: Node(name: {}, id: " + "{})", + self->name_, self->type_, defaultNodeName, defaultNodeId); - if (g_strcmp0(self->default_node_name_, default_node_name) == 0) { + if (g_strcmp0(self->default_node_name_, defaultNodeName) == 0 && + self->node_id_ == defaultNodeId) { spdlog::debug( - "[{}]: (onDefaultNodesApiChanged) - Default node has not changed. Node(name: {}, id: {}). " - "Ignoring.", - self->name_, self->default_node_name_, default_node_id); + "[{}]: (onDefaultNodesApiChanged: {}) - Default node has not changed. Node(name: {}, id: " + "{}). Ignoring.", + self->name_, self->type_, self->default_node_name_, defaultNodeId); return; } spdlog::debug( - "[{}]: (onDefaultNodesApiChanged) - Default node changed to -> Node(name: {}, id: {})", - self->name_, default_node_name, default_node_id); + "[{}]: (onDefaultNodesApiChanged: {}) - Default node changed to -> Node(name: {}, id: {})", + self->name_, self->type_, defaultNodeName, defaultNodeId); g_free(self->default_node_name_); - self->default_node_name_ = g_strdup(default_node_name); - self->node_id_ = default_node_id; - updateVolume(self, default_node_id); - updateNodeName(self, default_node_id); + self->default_node_name_ = g_strdup(defaultNodeName); + self->node_id_ = defaultNodeId; + updateVolume(self, defaultNodeId); + updateNodeName(self, defaultNodeId); } void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wireplumber* self) { @@ -189,25 +211,26 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir self->def_nodes_api_ = wp_plugin_find(self->wp_core_, "default-nodes-api"); - if (!self->def_nodes_api_) { + if (self->def_nodes_api_ == nullptr) { spdlog::error("[{}]: default nodes api is not loaded.", self->name_); throw std::runtime_error("Default nodes API is not loaded\n"); } self->mixer_api_ = wp_plugin_find(self->wp_core_, "mixer-api"); - if (!self->mixer_api_) { + if (self->mixer_api_ == nullptr) { spdlog::error("[{}]: mixer api is not loaded.", self->name_); throw std::runtime_error("Mixer api is not loaded\n"); } - g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Sink", + g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", self->type_, &self->default_node_name_); - g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", "Audio/Sink", &self->node_id_); + g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &self->node_id_); - if (self->default_node_name_) { - spdlog::debug("[{}]: (onObjectManagerInstalled) - default configured node name: {} and id: {}", - self->name_, self->default_node_name_, self->node_id_); + if (self->default_node_name_ != nullptr) { + spdlog::debug( + "[{}]: (onObjectManagerInstalled: {}) - default configured node name: {} and id: {}", + self->name_, self->type_, self->default_node_name_, self->node_id_); } updateVolume(self, self->node_id_); @@ -220,11 +243,11 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self) { - auto plugin_name = wp_plugin_get_name(WP_PLUGIN(p)); - spdlog::debug("[{}]: onPluginActivated: {}", self->name_, plugin_name); - g_autoptr(GError) error = NULL; + const auto* pluginName = wp_plugin_get_name(WP_PLUGIN(p)); + spdlog::debug("[{}]: onPluginActivated: {}", self->name_, pluginName); + g_autoptr(GError) error = nullptr; - if (!wp_object_activate_finish(p, res, &error)) { + if (wp_object_activate_finish(p, res, &error) == 0) { spdlog::error("[{}]: error activating plugin: {}", self->name_, error->message); throw std::runtime_error(error->message); } @@ -239,42 +262,75 @@ void waybar::modules::Wireplumber::activatePlugins() { for (uint16_t i = 0; i < apis_->len; i++) { WpPlugin* plugin = static_cast(g_ptr_array_index(apis_, i)); pending_plugins_++; - wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, NULL, + wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, nullptr, (GAsyncReadyCallback)onPluginActivated, this); } } -void waybar::modules::Wireplumber::prepare() { - spdlog::debug("[{}]: preparing object manager", name_); +void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { + spdlog::debug("[{}]: preparing object manager: '{}'", name_, self->type_); wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", - "=s", "Audio/Sink", NULL); + "=s", self->type_, nullptr); } -void waybar::modules::Wireplumber::loadRequiredApiModules() { - spdlog::debug("[{}]: loading required modules", name_); - g_autoptr(GError) error = NULL; +void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, + waybar::modules::Wireplumber* self) { + gboolean success = FALSE; + g_autoptr(GError) error = nullptr; - if (!wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", NULL, - &error)) { + spdlog::debug("[{}]: callback loading default node api module", self->name_); + + success = wp_core_load_component_finish(self->wp_core_, res, &error); + + if (success == FALSE) { + spdlog::error("[{}]: default nodes API load failed", self->name_); + throw std::runtime_error(error->message); + } + spdlog::debug("[{}]: loaded default nodes api", self->name_); + g_ptr_array_add(self->apis_, wp_plugin_find(self->wp_core_, "default-nodes-api")); + + spdlog::debug("[{}]: loading mixer api module", self->name_); + wp_core_load_component(self->wp_core_, "libwireplumber-module-mixer-api", "module", nullptr, + "mixer-api", nullptr, (GAsyncReadyCallback)onMixerApiLoaded, self); +} + +void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* res, + waybar::modules::Wireplumber* self) { + gboolean success = FALSE; + g_autoptr(GError) error = nullptr; + + success = wp_core_load_component_finish(self->wp_core_, res, &error); + + if (success == FALSE) { + spdlog::error("[{}]: mixer API load failed", self->name_); throw std::runtime_error(error->message); } - if (!wp_core_load_component(wp_core_, "libwireplumber-module-mixer-api", "module", NULL, - &error)) { - throw std::runtime_error(error->message); - } - - g_ptr_array_add(apis_, wp_plugin_find(wp_core_, "default-nodes-api")); - g_ptr_array_add(apis_, ({ - WpPlugin* p = wp_plugin_find(wp_core_, "mixer-api"); - g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, NULL); + spdlog::debug("[{}]: loaded mixer API", self->name_); + g_ptr_array_add(self->apis_, ({ + WpPlugin* p = wp_plugin_find(self->wp_core_, "mixer-api"); + g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, nullptr); p; })); + + self->activatePlugins(); + + self->dp.emit(); + + self->event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + self->event_box_.signal_scroll_event().connect(sigc::mem_fun(*self, &Wireplumber::handleScroll)); +} + +void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { + spdlog::debug("[{}]: loading default nodes api module", name_); + wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", nullptr, + "default-nodes-api", nullptr, (GAsyncReadyCallback)onDefaultNodesApiLoaded, + this); } auto waybar::modules::Wireplumber::update() -> void { auto format = format_; - std::string tooltip_format; + std::string tooltipFormat; if (muted_) { format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; @@ -291,12 +347,12 @@ auto waybar::modules::Wireplumber::update() -> void { getState(vol); if (tooltipEnabled()) { - if (tooltip_format.empty() && config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); + if (tooltipFormat.empty() && config_["tooltip-format"].isString()) { + tooltipFormat = config_["tooltip-format"].asString(); } - if (!tooltip_format.empty()) { - label_.set_tooltip_text(fmt::format(fmt::runtime(tooltip_format), + if (!tooltipFormat.empty()) { + label_.set_tooltip_text(fmt::format(fmt::runtime(tooltipFormat), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol)))); } else { @@ -316,38 +372,31 @@ 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 maxVolume = 1; double step = 1.0 / 100.0; if (config_["scroll-step"].isDouble()) { step = config_["scroll-step"].asDouble() / 100.0; } if (config_["max-volume"].isDouble()) { - max_volume = config_["max-volume"].asDouble() / 100.0; + maxVolume = config_["max-volume"].asDouble() / 100.0; } if (step < min_step_) step = min_step_; - double new_vol = volume_; + double newVol = volume_; if (dir == SCROLL_DIR::UP) { - if (volume_ < max_volume) { - new_vol = volume_ + step; - if (new_vol > max_volume) new_vol = max_volume; + if (volume_ < maxVolume) { + newVol = volume_ + step; + if (newVol > maxVolume) newVol = maxVolume; } } else if (dir == SCROLL_DIR::DOWN) { if (volume_ > 0) { - new_vol = volume_ - step; - if (new_vol < 0) new_vol = 0; + newVol = volume_ - step; + if (newVol < 0) newVol = 0; } } - if (new_vol != volume_) { - GVariant* variant = g_variant_new_double(new_vol); + if (newVol != volume_) { + GVariant* variant = g_variant_new_double(newVol); gboolean ret; g_signal_emit_by_name(mixer_api_, "set-volume", node_id_, variant, &ret); } diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 9e09d7a9..8e3b2542 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" @@ -29,6 +30,9 @@ namespace waybar::modules::wlr { static std::vector search_prefix() { std::vector prefixes = {""}; + std::string home_dir = std::getenv("HOME"); + prefixes.push_back(home_dir + "/.local/share/"); + auto xdg_data_dirs = std::getenv("XDG_DATA_DIRS"); if (!xdg_data_dirs) { prefixes.emplace_back("/usr/share/"); @@ -46,9 +50,6 @@ static std::vector search_prefix() { } while (end != std::string::npos); } - std::string home_dir = std::getenv("HOME"); - prefixes.push_back(home_dir + "/.local/share/"); - for (auto &p : prefixes) spdlog::debug("Using 'desktop' search path prefix: {}", p); return prefixes; @@ -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()) { @@ -319,9 +334,7 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, } button.add_events(Gdk::BUTTON_PRESS_MASK); - button.signal_button_press_event().connect(sigc::mem_fun(*this, &Task::handle_clicked), false); - button.signal_button_release_event().connect(sigc::mem_fun(*this, &Task::handle_button_release), - false); + button.signal_button_release_event().connect(sigc::mem_fun(*this, &Task::handle_clicked), false); button.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Task::handle_motion_notify), false); @@ -370,8 +383,43 @@ std::string Task::state_string(bool shortened) const { } void Task::handle_title(const char *title) { + if (title_.empty()) { + spdlog::debug(fmt::format("Task ({}) setting title to {}", id_, title_)); + } else { + spdlog::debug(fmt::format("Task ({}) overwriting title '{}' with '{}'", id_, title_, title)); + } title_ = title; hide_if_ignored(); + + if (!with_icon_ && !with_name_ || app_info_) { + return; + } + + set_app_info_from_app_id_list(title_); + name_ = app_info_ ? app_info_->get_display_name() : title; + + if (!with_icon_) { + return; + } + + int icon_size = config_["icon-size"].isInt() ? config_["icon-size"].asInt() : 16; + bool found = false; + for (auto &icon_theme : tbar_->icon_themes()) { + if (image_load_icon(icon_, icon_theme, app_info_, icon_size)) { + found = true; + break; + } + } + + if (found) + icon_.show(); + else + spdlog::debug("Couldn't find icon for {}", title_); +} + +void Task::set_minimize_hint() { + zwlr_foreign_toplevel_handle_v1_set_rectangle(handle_, bar_.surface, minimize_hint.x, + minimize_hint.y, minimize_hint.w, minimize_hint.h); } void Task::hide_if_ignored() { @@ -392,6 +440,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(); @@ -429,6 +482,13 @@ void Task::handle_app_id(const char *app_id) { spdlog::debug("Couldn't find icon for {}", app_id_); } +void Task::on_button_size_allocated(Gtk::Allocation &alloc) { + gtk_widget_translate_coordinates(GTK_WIDGET(button.gobj()), GTK_WIDGET(bar_.window.gobj()), 0, 0, + &minimize_hint.x, &minimize_hint.y); + minimize_hint.w = button.get_width(); + minimize_hint.h = button.get_height(); +} + void Task::handle_output_enter(struct wl_output *output) { if (ignored_) { spdlog::debug("{} is ignored", repr()); @@ -439,6 +499,8 @@ void Task::handle_output_enter(struct wl_output *output) { if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) { /* The task entered the output of the current bar make the button visible */ + button.signal_size_allocate().connect_notify( + sigc::mem_fun(this, &Task::on_button_size_allocated)); tbar_->add_button(button); button.show(); button_visible_ = true; @@ -507,17 +569,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; @@ -535,9 +597,11 @@ bool Task::handle_clicked(GdkEventButton *bt) { return true; else if (action == "activate") activate(); - else if (action == "minimize") + else if (action == "minimize") { + set_minimize_hint(); minimize(!minimized()); - else if (action == "minimize-raise") { + } else if (action == "minimize-raise") { + set_minimize_hint(); if (minimized()) minimize(false); else if (active()) @@ -553,12 +617,8 @@ bool Task::handle_clicked(GdkEventButton *bt) { else spdlog::warn("Unknown action {}", action); - return true; -} - -bool Task::handle_button_release(GdkEventButton *bt) { drag_start_button = -1; - return false; + return true; } bool Task::handle_motion_notify(GdkEventMotion *mn) { @@ -710,13 +770,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_); @@ -773,6 +834,10 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu } icon_themes_.push_back(Gtk::IconTheme::get_default()); + + for (auto &t : tasks_) { + t->handle_app_id(t->app_id().c_str()); + } } Taskbar::~Taskbar() { @@ -879,7 +944,7 @@ void Taskbar::move_button(Gtk::Button &bt, int pos) { box_.reorder_child(bt, pos void Taskbar::remove_button(Gtk::Button &bt) { box_.remove(bt); - if (tasks_.empty()) { + if (box_.get_children().empty()) { box_.get_style_context()->add_class("empty"); } } diff --git a/src/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..860168fd --- /dev/null +++ b/src/util/audio_backend.cpp @@ -0,0 +1,405 @@ +#include "util/audio_backend.hpp" + +#include +#include +#include +#include +#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), + volume_(0), + muted_(false), + source_volume_(0), + source_muted_(false), + on_updated_cb_(std::move(on_updated_cb)) { + // Initialize pa_volume_ with safe defaults + pa_cvolume_init(&pa_volume_); + 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) { + if ((backend->context_ != nullptr) && + pa_context_get_state(backend->context_) == PA_CONTEXT_READY) { + pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data); + } + } else { + spdlog::debug("Volume modification failed"); + } +} + +/* + * 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 running = i->state == PA_SINK_RUNNING; + auto idle = i->state == PA_SINK_IDLE; + spdlog::trace("Sink name {} Running:[{}] Idle:[{}]", i->name, running, idle); + + auto *backend = static_cast(data); + + if (!backend->ignored_sinks_.empty()) { + for (const auto &ignored_sink : backend->ignored_sinks_) { + if (ignored_sink == i->description) { + if (i->name == backend->current_sink_name_) { + // If the current sink happens to be ignored it is never considered running + // so it will be replaced with another sink. + backend->current_sink_running_ = false; + } + + return; + } + } + } + + backend->default_sink_running_ = backend->default_sink_name == i->name && + (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); + + if (i->name != backend->default_sink_name && !backend->default_sink_running_) { + return; + } + + if (backend->current_sink_name_ == i->name) { + backend->current_sink_running_ = (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); + } + + if (!backend->current_sink_running_ && + (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE)) { + backend->current_sink_name_ = i->name; + backend->current_sink_running_ = true; + } + + if (backend->current_sink_name_ == i->name) { + // Safely copy the volume structure + if (pa_cvolume_valid(&i->volume) != 0) { + 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); + } else { + spdlog::error("Invalid volume structure received from PulseAudio"); + // Initialize with safe defaults + pa_cvolume_init(&backend->pa_volume_); + backend->volume_ = 0; + } + + 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 (const 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_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) { + // Early return if context is not ready + if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { + spdlog::error("PulseAudio context not ready"); + return; + } + + // Prepare volume structure + pa_cvolume pa_volume; + + pa_cvolume_init(&pa_volume); + + // Use existing volume structure if valid, otherwise create a safe default + if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) { + pa_volume = pa_volume_; + } else { + // Set stereo as a safe default + pa_volume.channels = 2; + spdlog::debug("Using default stereo volume structure"); + } + + // Set the volume safely + volume = std::clamp(volume, min_volume, max_volume); + pa_volume_t vol = volume * (static_cast(PA_VOLUME_NORM) / 100); + + // Set all channels to the same volume manually to avoid pa_cvolume_set + for (uint8_t i = 0; i < pa_volume.channels; i++) { + pa_volume.values[i] = vol; + } + + // Apply the volume change + pa_threaded_mainloop_lock(mainloop_); + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + pa_threaded_mainloop_unlock(mainloop_); +} + +void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume) { + // Early return if context is not ready + if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { + spdlog::error("PulseAudio context not ready"); + return; + } + + // Prepare volume structure + pa_cvolume pa_volume; + pa_cvolume_init(&pa_volume); + + // Use existing volume structure if valid, otherwise create a safe default + if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) { + pa_volume = pa_volume_; + } else { + // Set stereo as a safe default + pa_volume.channels = 2; + spdlog::debug("Using default stereo volume structure"); + + // Initialize all channels to current volume level + double volume_tick = static_cast(PA_VOLUME_NORM) / 100; + pa_volume_t vol = volume_ * volume_tick; + for (uint8_t i = 0; i < pa_volume.channels; i++) { + pa_volume.values[i] = vol; + } + + // No need to continue with volume change if we had to create a new structure + pa_threaded_mainloop_lock(mainloop_); + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + pa_threaded_mainloop_unlock(mainloop_); + return; + } + + // Calculate volume change + double volume_tick = static_cast(PA_VOLUME_NORM) / 100; + pa_volume_t change; + max_volume = std::min(max_volume, static_cast(PA_VOLUME_UI_MAX)); + + if (change_type == ChangeType::Increase && volume_ < max_volume) { + // Calculate how much to increase + if (volume_ + step > max_volume) { + change = round((max_volume - volume_) * volume_tick); + } else { + change = round(step * volume_tick); + } + + // Manually increase each channel's volume + for (uint8_t i = 0; i < pa_volume.channels; i++) { + pa_volume.values[i] = std::min(pa_volume.values[i] + change, PA_VOLUME_MAX); + } + } else if (change_type == ChangeType::Decrease && volume_ > 0) { + // Calculate how much to decrease + if (volume_ - step < 0) { + change = round(volume_ * volume_tick); + } else { + change = round(step * volume_tick); + } + + // Manually decrease each channel's volume + for (uint8_t i = 0; i < pa_volume.channels; i++) { + pa_volume.values[i] = (pa_volume.values[i] > change) ? (pa_volume.values[i] - change) : 0; + } + } else { + // No change needed + return; + } + + // Apply the volume change + pa_threaded_mainloop_lock(mainloop_); + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + pa_threaded_mainloop_unlock(mainloop_); +} + +void AudioBackend::toggleSinkMute() { + muted_ = !muted_; + pa_threaded_mainloop_lock(mainloop_); + pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, + nullptr); + pa_threaded_mainloop_unlock(mainloop_); +} + +void AudioBackend::toggleSinkMute(bool mute) { + muted_ = mute; + pa_threaded_mainloop_lock(mainloop_); + pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, + nullptr); + pa_threaded_mainloop_unlock(mainloop_); +} + +void AudioBackend::toggleSourceMute() { + source_muted_ = !muted_; + pa_threaded_mainloop_lock(mainloop_); + pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), + nullptr, nullptr); + pa_threaded_mainloop_unlock(mainloop_); +} + +void AudioBackend::toggleSourceMute(bool mute) { + source_muted_ = mute; + pa_threaded_mainloop_lock(mainloop_); + pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), + nullptr, nullptr); + pa_threaded_mainloop_unlock(mainloop_); +} + +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 diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp new file mode 100644 index 00000000..863896d5 --- /dev/null +++ b/src/util/backlight_backend.cpp @@ -0,0 +1,294 @@ +#include "util/backlight_backend.hpp" + +#include +#include +#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 { + +static void upsert_device(std::vector &devices, udev_device *dev) { + const char *name = udev_device_get_sysname(dev); + check_nn(name); + + const char *actual_brightness_attr = + strncmp(name, "amdgpu_bl", 9) == 0 || strcmp(name, "apple-panel-bl") == 0 + ? "brightness" + : "actual_brightness"; + + const char *actual = udev_device_get_sysattr_value(dev, actual_brightness_attr); + const char *max = udev_device_get_sysattr_value(dev, "max_brightness"); + const char *power = udev_device_get_sysattr_value(dev, "bl_power"); + + auto found = std::find_if(devices.begin(), devices.end(), [name](const BacklightDevice &device) { + return device.name() == name; + }); + if (found != devices.end()) { + if (actual != nullptr) { + found->set_actual(std::stoi(actual)); + } + if (max != nullptr) { + found->set_max(std::stoi(max)); + } + if (power != nullptr) { + found->set_powered(std::stoi(power) == 0); + } + } else { + const int actual_int = actual == nullptr ? 0 : std::stoi(actual); + const int max_int = max == nullptr ? 0 : std::stoi(max); + const bool power_bool = power == nullptr ? true : std::stoi(power) == 0; + devices.emplace_back(name, actual_int, max_int, power_bool); + } +} + +static void enumerate_devices(std::vector &devices, udev *udev) { + std::unique_ptr enumerate{udev_enumerate_new(udev)}; + udev_enumerate_add_match_subsystem(enumerate.get(), "backlight"); + udev_enumerate_scan_devices(enumerate.get()); + udev_list_entry *enum_devices = udev_enumerate_get_list_entry(enumerate.get()); + udev_list_entry *dev_list_entry; + udev_list_entry_foreach(dev_list_entry, enum_devices) { + const char *path = udev_list_entry_get_name(dev_list_entry); + std::unique_ptr dev{udev_device_new_from_syspath(udev, path)}; + check_nn(dev.get(), "dev new failed"); + upsert_device(devices, dev.get()); + } +} + +BacklightDevice::BacklightDevice(std::string name, int actual, int max, bool powered) + : name_(std::move(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_(std::move(on_updated_cb)), polling_interval_(interval), previous_best_({}) { + std::unique_ptr udev_check{udev_new()}; + check_nn(udev_check.get(), "Udev check new failed"); + enumerate_devices(devices_, udev_check.get()); + if (devices_.empty()) { + throw std::runtime_error("No backlight found"); + } + +#ifdef HAVE_LOGIN_PROXY + // 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/auto", "org.freedesktop.login1.Session"); + + if (!login_proxy_) { + 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"); + } +#endif + + 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, dev.get()); + } + + // Refresh state if timed out + if (event_count == 0) { + enumerate_devices(devices, udev.get()); + } + { + std::scoped_lock lock(udev_thread_mutex_); + devices_ = devices; + } + this->on_updated_cb_(); + } + }; +} + +const BacklightDevice *BacklightBackend::best_device(const std::vector &devices, + std::string_view preferred_device) { + const auto found = std::find_if( + devices.begin(), devices.end(), + [preferred_device](const BacklightDevice &dev) { return dev.name() == preferred_device; }); + if (found != devices.end()) { + return &(*found); + } + + const auto max = std::max_element( + devices.begin(), devices.end(), + [](const BacklightDevice &l, const BacklightDevice &r) { return l.get_max() < r.get_max(); }); + + return max == devices.end() ? 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(const std::string &preferred_device, int brightness) { + GET_BEST_DEVICE(best, (*this), preferred_device); + + if (best != nullptr) { + const auto max = best->get_max(); + const auto abs_val = static_cast(std::round(brightness * max / 100.0F)); + set_brightness_internal(best->name(), abs_val, best->get_max()); + } +} + +void BacklightBackend::set_brightness(const std::string &preferred_device, ChangeType change_type, + double step) { + GET_BEST_DEVICE(best, (*this), preferred_device); + + if (best != nullptr) { + const auto max = best->get_max(); + + const auto abs_step = static_cast(round(step * max / 100.0F)); + + const 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(const 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(const std::string &preferred_device) { + GET_BEST_DEVICE(best, (*this), preferred_device); + + if (best != nullptr) { + return best->get_actual() * 100 / best->get_max(); + } + + return 0; +} + +} // namespace waybar::util diff --git a/src/util/css_reload_helper.cpp b/src/util/css_reload_helper.cpp new file mode 100644 index 00000000..274bdeed --- /dev/null +++ b/src/util/css_reload_helper.cpp @@ -0,0 +1,150 @@ +#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 + std::string original = result; + while (std::filesystem::is_symlink(result)) { + result = std::filesystem::read_symlink(result); + + // prevent infinite cycle + if (result == original) { + break; + } + } + + 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/gtk_icon.cpp b/src/util/gtk_icon.cpp index 5dd741f9..4b4d3d69 100644 --- a/src/util/gtk_icon.cpp +++ b/src/util/gtk_icon.cpp @@ -15,11 +15,20 @@ bool DefaultGtkIconThemeWrapper::has_icon(const std::string& value) { return Gtk::IconTheme::get_default()->has_icon(value); } -Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon(const char* name, int tmp_size, - Gtk::IconLookupFlags flags) { +Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon( + const char* name, int tmp_size, Gtk::IconLookupFlags flags, + Glib::RefPtr style) { const std::lock_guard lock(default_theme_mutex); auto default_theme = Gtk::IconTheme::get_default(); default_theme->rescan_if_needed(); - return default_theme->load_icon(name, tmp_size, flags); + + auto icon_info = default_theme->lookup_icon(name, tmp_size, flags); + + if (style.get() == nullptr) { + return icon_info.load_icon(); + } + + bool is_sym = false; + return icon_info.load_symbolic(style, is_sym); } diff --git a/src/util/pipewire/pipewire_backend.cpp b/src/util/pipewire/pipewire_backend.cpp new file mode 100644 index 00000000..6d859a91 --- /dev/null +++ b/src/util/pipewire/pipewire_backend.cpp @@ -0,0 +1,154 @@ +#include "util/pipewire/pipewire_backend.hpp" + +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +static void getNodeInfo(void *data_, const struct pw_node_info *info) { + auto *pNodeInfo = static_cast(data_); + pNodeInfo->handleNodeEventInfo(info); + + static_cast(pNodeInfo->data)->privacy_nodes_changed_signal_event.emit(); +} + +static const struct pw_node_events NODE_EVENTS = { + .version = PW_VERSION_NODE_EVENTS, + .info = getNodeInfo, +}; + +static void proxyDestroy(void *data) { + static_cast(data)->handleProxyEventDestroy(); +} + +static const struct pw_proxy_events PROXY_EVENTS = { + .version = PW_VERSION_PROXY_EVENTS, + .destroy = proxyDestroy, +}; + +static void registryEventGlobal(void *_data, uint32_t id, uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) { + static_cast(_data)->handleRegistryEventGlobal(id, permissions, type, version, + props); +} + +static void registryEventGlobalRemove(void *_data, uint32_t id) { + static_cast(_data)->handleRegistryEventGlobalRemove(id); +} + +static const struct pw_registry_events REGISTRY_EVENTS = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = registryEventGlobal, + .global_remove = registryEventGlobalRemove, +}; + +PipewireBackend::PipewireBackend(PrivateConstructorTag tag) + : mainloop_(nullptr), context_(nullptr), core_(nullptr) { + pw_init(nullptr, nullptr); + mainloop_ = pw_thread_loop_new("waybar", nullptr); + if (mainloop_ == nullptr) { + throw std::runtime_error("pw_thread_loop_new() failed."); + } + + pw_thread_loop_lock(mainloop_); + + context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0); + if (context_ == nullptr) { + pw_thread_loop_unlock(mainloop_); + throw std::runtime_error("pa_context_new() failed."); + } + core_ = pw_context_connect(context_, nullptr, 0); + if (core_ == nullptr) { + pw_thread_loop_unlock(mainloop_); + throw std::runtime_error("pw_context_connect() failed"); + } + registry_ = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0); + + spa_zero(registryListener_); + pw_registry_add_listener(registry_, ®istryListener_, ®ISTRY_EVENTS, this); + if (pw_thread_loop_start(mainloop_) < 0) { + pw_thread_loop_unlock(mainloop_); + throw std::runtime_error("pw_thread_loop_start() failed."); + } + pw_thread_loop_unlock(mainloop_); +} + +PipewireBackend::~PipewireBackend() { + if (mainloop_ != nullptr) { + pw_thread_loop_lock(mainloop_); + } + + if (registry_ != nullptr) { + pw_proxy_destroy((struct pw_proxy *)registry_); + } + + spa_zero(registryListener_); + + if (core_ != nullptr) { + pw_core_disconnect(core_); + } + + if (context_ != nullptr) { + pw_context_destroy(context_); + } + + if (mainloop_ != nullptr) { + pw_thread_loop_unlock(mainloop_); + pw_thread_loop_stop(mainloop_); + pw_thread_loop_destroy(mainloop_); + } +} + +std::shared_ptr PipewireBackend::getInstance() { + PrivateConstructorTag tag; + return std::make_shared(tag); +} + +void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) { + if (props == nullptr || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; + + const char *lookupStr = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (lookupStr == nullptr) return; + std::string mediaClass = lookupStr; + enum PrivacyNodeType mediaType = PRIVACY_NODE_TYPE_NONE; + if (mediaClass == "Stream/Input/Video") { + mediaType = PRIVACY_NODE_TYPE_VIDEO_INPUT; + } else if (mediaClass == "Stream/Input/Audio") { + mediaType = PRIVACY_NODE_TYPE_AUDIO_INPUT; + } else if (mediaClass == "Stream/Output/Audio") { + mediaType = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; + } else { + return; + } + + auto *proxy = (pw_proxy *)pw_registry_bind(registry_, id, type, version, sizeof(PrivacyNodeInfo)); + + if (proxy == nullptr) return; + + auto *pNodeInfo = (PrivacyNodeInfo *)pw_proxy_get_user_data(proxy); + new (pNodeInfo) PrivacyNodeInfo{}; + pNodeInfo->id = id; + pNodeInfo->data = this; + pNodeInfo->type = mediaType; + pNodeInfo->media_class = mediaClass; + + pw_proxy_add_listener(proxy, &pNodeInfo->proxy_listener, &PROXY_EVENTS, pNodeInfo); + + pw_proxy_add_object_listener(proxy, &pNodeInfo->object_listener, &NODE_EVENTS, pNodeInfo); + + privacy_nodes.insert_or_assign(id, pNodeInfo); +} + +void PipewireBackend::handleRegistryEventGlobalRemove(uint32_t id) { + mutex_.lock(); + auto iter = privacy_nodes.find(id); + if (iter != privacy_nodes.end()) { + privacy_nodes[id]->~PrivacyNodeInfo(); + privacy_nodes.erase(id); + } + mutex_.unlock(); + + privacy_nodes_changed_signal_event.emit(); +} + +} // namespace waybar::util::PipewireBackend diff --git a/src/util/pipewire/privacy_node_info.cpp b/src/util/pipewire/privacy_node_info.cpp new file mode 100644 index 00000000..ec110b86 --- /dev/null +++ b/src/util/pipewire/privacy_node_info.cpp @@ -0,0 +1,58 @@ +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +std::string PrivacyNodeInfo::getName() { + const std::vector names{&application_name, &node_name}; + std::string name = "Unknown Application"; + for (const auto &item : names) { + if (item != nullptr && !item->empty()) { + name = *item; + name[0] = toupper(name[0]); + break; + } + } + return name; +} + +std::string PrivacyNodeInfo::getIconName() { + const std::vector names{&application_icon_name, &pipewire_access_portal_app_id, + &application_name, &node_name}; + std::string name = "application-x-executable-symbolic"; + for (const auto &item : names) { + if (item != nullptr && !item->empty() && DefaultGtkIconThemeWrapper::has_icon(*item)) { + return *item; + } + } + return name; +} + +void PrivacyNodeInfo::handleProxyEventDestroy() { + spa_hook_remove(&proxy_listener); + spa_hook_remove(&object_listener); +} + +void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info *info) { + state = info->state; + + const struct spa_dict_item *item; + spa_dict_for_each(item, info->props) { + if (strcmp(item->key, PW_KEY_CLIENT_ID) == 0) { + client_id = strtoul(item->value, nullptr, 10); + } else if (strcmp(item->key, PW_KEY_MEDIA_NAME) == 0) { + media_name = item->value; + } else if (strcmp(item->key, PW_KEY_NODE_NAME) == 0) { + node_name = item->value; + } else if (strcmp(item->key, PW_KEY_APP_NAME) == 0) { + application_name = item->value; + } else if (strcmp(item->key, "pipewire.access.portal.app_id") == 0) { + pipewire_access_portal_app_id = item->value; + } else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) { + application_icon_name = item->value; + } else if (strcmp(item->key, "stream.monitor") == 0) { + is_monitor = strcmp(item->value, "true") == 0; + } + } +} + +} // namespace waybar::util::PipewireBackend diff --git a/src/util/portal.cpp b/src/util/portal.cpp new file mode 100644 index 00000000..6df2a6b6 --- /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 + +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() + : Gio::DBus::Proxy(Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION), + PORTAL_BUS_NAME, PORTAL_OBJ_PATH, PORTAL_INTERFACE), + currentMode(Appearance::UNKNOWN) { + refreshAppearance(); +}; + +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; + Glib::VariantBase keyv; + Glib::VariantBase 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 221497e8..3adcdf67 100644 --- a/src/util/prepare_for_sleep.cpp +++ b/src/util/prepare_for_sleep.cpp @@ -1,20 +1,20 @@ #include "util/prepare_for_sleep.h" #include +#include namespace { class PrepareForSleep { private: PrepareForSleep() { - GError *error = NULL; - login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); - if (!login1_connection) { - throw std::runtime_error("Unable to connect to the SYSTEM Bus!..."); + login1_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr); + if (login1_connection == nullptr) { + spdlog::warn("Unable to connect to the SYSTEM Bus!..."); } else { login1_id = g_dbus_connection_signal_subscribe( login1_connection, "org.freedesktop.login1", "org.freedesktop.login1.Manager", - "PrepareForSleep", "/org/freedesktop/login1", NULL, G_DBUS_SIGNAL_FLAGS_NONE, - prepareForSleep_cb, this, NULL); + "PrepareForSleep", "/org/freedesktop/login1", nullptr, G_DBUS_SIGNAL_FLAGS_NONE, + prepareForSleep_cb, this, nullptr); } } @@ -22,11 +22,11 @@ class PrepareForSleep { const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { - if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)"))) { + if (g_variant_is_of_type(parameters, G_VARIANT_TYPE("(b)")) != 0) { gboolean sleeping; g_variant_get(parameters, "(b)", &sleeping); - PrepareForSleep *self = static_cast(user_data); + auto *self = static_cast(user_data); self->signal.emit(sleeping); } } diff --git a/src/util/regex_collection.cpp b/src/util/regex_collection.cpp new file mode 100644 index 00000000..51dd6ff7 --- /dev/null +++ b/src/util/regex_collection.cpp @@ -0,0 +1,73 @@ +#include "util/regex_collection.hpp" + +#include +#include + +#include +#include + +namespace waybar::util { + +int default_priority_function(std::string& key) { return 0; } + +RegexCollection::RegexCollection(const Json::Value& map, std::string default_repr, + const std::function& priority_function) + : default_repr(std::move(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) { + std::smatch match; + if (std::regex_search(value, match, rule.rule)) { + matched_any = true; + return match.format(rule.repr.data()); + } + } + + 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/src/util/sanitize_str.cpp b/src/util/sanitize_str.cpp index 131b9f28..ae9a9e37 100644 --- a/src/util/sanitize_str.cpp +++ b/src/util/sanitize_str.cpp @@ -10,9 +10,8 @@ std::string sanitize_string(std::string str) { const std::pair replacement_table[] = { {'&', "&"}, {'<', "<"}, {'>', ">"}, {'"', """}, {'\'', "'"}}; size_t startpoint; - for (size_t i = 0; i < (sizeof(replacement_table) / sizeof(replacement_table[0])); ++i) { + for (const auto& pair : replacement_table) { startpoint = 0; - std::pair pair = replacement_table[i]; while ((startpoint = str.find(pair.first, startpoint)) != std::string::npos) { str.replace(startpoint, 1, pair.second); startpoint += pair.second.length(); diff --git a/src/util/ustring_clen.cpp b/src/util/ustring_clen.cpp index 374df0d6..a8b9c9af 100644 --- a/src/util/ustring_clen.cpp +++ b/src/util/ustring_clen.cpp @@ -2,8 +2,8 @@ int ustring_clen(const Glib::ustring &str) { int total = 0; - for (auto i = str.begin(); i != str.end(); ++i) { - total += g_unichar_iswide(*i) + 1; + for (unsigned int i : str) { + total += g_unichar_iswide(i) + 1; } return total; -} \ No newline at end of file +} diff --git a/subprojects/catch2.wrap b/subprojects/catch2.wrap index 4a6f836c..489db6c6 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.7.0 +source_url = https://github.com/catchorg/Catch2/archive/v3.7.0.tar.gz +source_filename = Catch2-3.7.0.tar.gz +source_hash = 5b10cd536fa3818112a82820ce0787bd9f2a906c618429e7c4dea639983c8e88 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/catch2_3.7.0-1/Catch2-3.7.0.tar.gz +wrapdb_version = 3.7.0-1 [provide] catch2 = catch2_dep diff --git a/subprojects/cava.wrap b/subprojects/cava.wrap index 59044b14..f220207c 100644 --- a/subprojects/cava.wrap +++ b/subprojects/cava.wrap @@ -1,7 +1,7 @@ [wrap-file] -directory = cava-0.8.4 -source_url = https://github.com/LukashonakV/cava/archive/0.8.4.tar.gz -source_filename = cava-0.8.4.tar.gz -source_hash = 523353f446570277d40b8e1efb84468d70fdec53e1356a555c14bf466557a3ed +directory = cava-0.10.4 +source_url = https://github.com/LukashonakV/cava/archive/0.10.4.tar.gz +source_filename = cava-0.10.4.tar.gz +source_hash =7bc1c1f9535f2bcc5cd2ae8a2434a2e3a05f5670b1c96316df304137ffc65756 [provide] cava = cava_dep diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap index 63869be1..fd508477 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-11.0.2 +source_url = https://github.com/fmtlib/fmt/archive/11.0.2.tar.gz +source_filename = fmt-11.0.2.tar.gz +source_hash = 6cb1e6d37bdcb756dbbe59be438790db409cdb4868c66e888d5df9f13f7c027f +patch_filename = fmt_11.0.2-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/fmt_11.0.2-1/get_patch +patch_hash = 90c9e3b8e8f29713d40ca949f6f93ad115d78d7fb921064112bc6179e6427c5e +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_11.0.2-1/fmt-11.0.2.tar.gz +wrapdb_version = 11.0.2-1 [provide] fmt = fmt_dep - diff --git a/subprojects/gtk-layer-shell.wrap b/subprojects/gtk-layer-shell.wrap index 555fbcb6..fc0ddf74 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.9.0 +source_filename = gtk-layer-shell-0.9.0.tar.gz +source_hash = 3809e5565d9ed02e44bb73787ff218523e8760fef65830afe60ea7322e22da1c +source_url = https://github.com/wmww/gtk-layer-shell/archive/v0.9.0/gtk-layer-shell-0.9.0.tar.gz diff --git a/subprojects/spdlog.wrap b/subprojects/spdlog.wrap index 03a6d4c3..af00d5a7 100644 --- a/subprojects/spdlog.wrap +++ b/subprojects/spdlog.wrap @@ -1,13 +1,13 @@ [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.14.1 +source_url = https://github.com/gabime/spdlog/archive/refs/tags/v1.14.1.tar.gz +source_filename = spdlog-1.14.1.tar.gz +source_hash = 1586508029a7d0670dfcb2d97575dcdc242d3868a259742b69f100801ab4e16b +patch_filename = spdlog_1.14.1-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/spdlog_1.14.1-1/get_patch +patch_hash = ae878e732330ea1048f90d7e117c40c0cd2a6fb8ae5492c7955818ce3aaade6c +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/spdlog_1.14.1-1/spdlog-1.14.1.tar.gz +wrapdb_version = 1.14.1-1 [provide] spdlog = spdlog_dep - diff --git a/test/config.cpp b/test/config.cpp index ad3df065..c60519ce 100644 --- a/test/config.cpp +++ b/test/config.cpp @@ -117,3 +117,42 @@ TEST_CASE("Load multiple bar config with include", "[config]") { REQUIRE(data.size() == 4); REQUIRE(data[0]["output"].asString() == "OUT-0"); } + +TEST_CASE("Load Hyprland Workspaces bar config", "[config]") { + waybar::Config conf; + conf.load("test/config/hyprland-workspaces.json"); + + auto& data = conf.getConfig(); + auto hyprland = data[0]["hyprland/workspaces"]; + auto hyprland_window_rewrite = data[0]["hyprland/workspaces"]["window-rewrite"]; + auto hyprland_format_icons = data[0]["hyprland/workspaces"]["format-icons"]; + auto hyprland_persistent_workspaces = data[0]["hyprland/workspaces"]["persistent-workspaces"]; + + REQUIRE(data.isArray()); + REQUIRE(data.size() == 1); + REQUIRE(data[0]["height"].asInt() == 20); + REQUIRE(data[0]["layer"].asString() == "bottom"); + REQUIRE(data[0]["output"].isArray()); + REQUIRE(data[0]["output"][0].asString() == "HDMI-0"); + REQUIRE(data[0]["output"][1].asString() == "DP-0"); + + REQUIRE(hyprland["active-only"].asBool() == true); + REQUIRE(hyprland["all-outputs"].asBool() == false); + REQUIRE(hyprland["move-to-monitor"].asBool() == true); + REQUIRE(hyprland["format"].asString() == "{icon} {windows}"); + REQUIRE(hyprland["format-window-separator"].asString() == " "); + REQUIRE(hyprland["on-scroll-down"].asString() == "hyprctl dispatch workspace e-1"); + REQUIRE(hyprland["on-scroll-up"].asString() == "hyprctl dispatch workspace e+1"); + REQUIRE(hyprland["show-special"].asBool() == true); + REQUIRE(hyprland["window-rewrite-default"].asString() == ""); + REQUIRE(hyprland["window-rewrite-separator"].asString() == " "); + REQUIRE(hyprland_format_icons["1"].asString() == "󰎤"); + REQUIRE(hyprland_format_icons["2"].asString() == "󰎧"); + REQUIRE(hyprland_format_icons["3"].asString() == "󰎪"); + REQUIRE(hyprland_format_icons["default"].asString() == ""); + REQUIRE(hyprland_format_icons["empty"].asString() == "󱓼"); + REQUIRE(hyprland_format_icons["urgent"].asString() == "󱨇"); + REQUIRE(hyprland_persistent_workspaces["1"].asString() == "HDMI-0"); + REQUIRE(hyprland_window_rewrite["title"].asString() == ""); + REQUIRE(hyprland["sort-by"].asString() == "number"); +} diff --git a/test/config/hyprland-workspaces.json b/test/config/hyprland-workspaces.json new file mode 100644 index 00000000..dd733897 --- /dev/null +++ b/test/config/hyprland-workspaces.json @@ -0,0 +1,37 @@ +[ + { + "height": 20, + "layer": "bottom", + "output": [ + "HDMI-0", + "DP-0" + ], + "hyprland/workspaces": { + "active-only": true, + "all-outputs": false, + "show-special": true, + "move-to-monitor": true, + "format": "{icon} {windows}", + "format-window-separator": " ", + "format-icons": { + "1": "󰎤", + "2": "󰎧", + "3": "󰎪", + "default": "", + "empty": "󱓼", + "urgent": "󱨇" + }, + "persistent-workspaces": { + "1": "HDMI-0" + }, + "on-scroll-down": "hyprctl dispatch workspace e-1", + "on-scroll-up": "hyprctl dispatch workspace e+1", + "window-rewrite": { + "title": "" + }, + "window-rewrite-default": "", + "window-rewrite-separator": " ", + "sort-by": "number" + } + } +] diff --git a/test/date.cpp b/test/date.cpp deleted file mode 100644 index aa6d79b0..00000000 --- a/test/date.cpp +++ /dev/null @@ -1,162 +0,0 @@ -#include "util/date.hpp" - -#include -#include -#include -#include -#include - -#if __has_include() -#include -#include -#else -#include -#endif - -#ifndef SKIP -#define SKIP(...) \ - WARN(__VA_ARGS__); \ - return -#endif - -using namespace std::literals::chrono_literals; - -/* - * Check that the date/time formatter with locale and timezone support is working as expected. - */ - -const date::zoned_time TEST_TIME = date::zoned_time{ - "UTC", date::local_days{date::Monday[1] / date::January / 2022} + 13h + 4min + 5s}; - -/* - * Check if the date formatted with LC_TIME=en_US is within expectations. - * - * The check expects Glibc output style and will fail with FreeBSD (different implementation) - * or musl (no implementation). - */ -static const bool LC_TIME_is_sane = []() { - try { - std::stringstream ss; - ss.imbue(std::locale("en_US.UTF-8")); - - time_t t = 1641211200; - std::tm tm = *std::gmtime(&t); - - ss << std::put_time(&tm, "%x %X"); - return ss.str() == "01/03/2022 12:00:00 PM"; - } catch (std::exception &) { - return false; - } -}(); - -TEST_CASE("Format UTC time", "[clock][util]") { - const auto loc = std::locale("C"); - const auto tm = TEST_TIME; - - CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified - CHECK(fmt::format(loc, "{:%c %Z}", tm) == "Mon Jan 3 13:04:05 2022 UTC"); - CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); - - if (!LC_TIME_is_sane) { - SKIP("Locale support check failed, skip tests"); - } - - /* Test a few locales that are most likely to be present */ - SECTION("US locale") { - try { - const auto loc = std::locale("en_US.UTF-8"); - - CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified - CHECK_THAT(fmt::format(loc, "{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 01:04:05 PM")); - CHECK(fmt::format(loc, "{:%x %X}", tm) == "01/03/2022 01:04:05 PM"); - CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); - } catch (const std::runtime_error &) { - WARN("Locale en_US not found, skip tests"); - } - } - SECTION("GB locale") { - try { - const auto loc = std::locale("en_GB.UTF-8"); - - CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified - CHECK_THAT(fmt::format(loc, "{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 13:04:05")); - CHECK(fmt::format(loc, "{:%x %X}", tm) == "03/01/22 13:04:05"); - CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); - } catch (const std::runtime_error &) { - WARN("Locale en_GB not found, skip tests"); - } - } - SECTION("Global locale") { - try { - const auto loc = std::locale::global(std::locale("en_US.UTF-8")); - - CHECK(fmt::format("{}", tm).empty()); // no format specified - CHECK_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 01:04:05 PM")); - CHECK(fmt::format("{:%x %X}", tm) == "01/03/2022 01:04:05 PM"); - CHECK(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103130405"); - - std::locale::global(loc); - } catch (const std::runtime_error &) { - WARN("Locale en_US not found, skip tests"); - } - } -} - -TEST_CASE("Format zoned time", "[clock][util]") { - const auto loc = std::locale("C"); - const auto tm = date::zoned_time{"America/New_York", TEST_TIME}; - - CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified - CHECK(fmt::format(loc, "{:%c %Z}", tm) == "Mon Jan 3 08:04:05 2022 EST"); - CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); - - if (!LC_TIME_is_sane) { - SKIP("Locale support check failed, skip tests"); - } - - /* Test a few locales that are most likely to be present */ - SECTION("US locale") { - try { - const auto loc = std::locale("en_US.UTF-8"); - - CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified - CHECK_THAT(fmt::format(loc, "{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05 AM")); - CHECK(fmt::format(loc, "{:%x %X}", tm) == "01/03/2022 08:04:05 AM"); - CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); - } catch (const std::runtime_error &) { - WARN("Locale en_US not found, skip tests"); - } - } - SECTION("GB locale") { - try { - const auto loc = std::locale("en_GB.UTF-8"); - - CHECK(fmt::format(loc, "{}", tm).empty()); // no format specified - CHECK_THAT(fmt::format(loc, "{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05")); - CHECK(fmt::format(loc, "{:%x %X}", tm) == "03/01/22 08:04:05"); - CHECK(fmt::format(loc, "{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); - } catch (const std::runtime_error &) { - WARN("Locale en_GB not found, skip tests"); - } - } - SECTION("Global locale") { - try { - const auto loc = std::locale::global(std::locale("en_US.UTF-8")); - - CHECK(fmt::format("{}", tm).empty()); // no format specified - CHECK_THAT(fmt::format("{:%c}", tm), // HowardHinnant/date#704 - Catch::Matchers::StartsWith("Mon 03 Jan 2022 08:04:05 AM")); - CHECK(fmt::format("{:%x %X}", tm) == "01/03/2022 08:04:05 AM"); - CHECK(fmt::format("{arg:%Y%m%d%H%M%S}", fmt::arg("arg", tm)) == "20220103080405"); - - std::locale::global(loc); - } catch (const std::runtime_error &) { - WARN("Locale en_US not found, skip tests"); - } - } -} diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp new file mode 100644 index 00000000..b83b839c --- /dev/null +++ b/test/hyprland/backend.cpp @@ -0,0 +1,59 @@ +#if __has_include() +#include +#else +#include +#endif + +#include "fixtures/IPCTestFixture.hpp" + +namespace fs = std::filesystem; +namespace hyprland = waybar::modules::hyprland; + +TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirExists", "[getSocketFolder]") { + // Test case: XDG_RUNTIME_DIR exists and contains "hypr" directory + // Arrange + tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + fs::path expectedPath = tempDir / "hypr" / instanceSig; + fs::create_directories(tempDir / "hypr" / instanceSig); + setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); + + // Act + fs::path actualPath = getSocketFolder(instanceSig); + + // Assert expected result + REQUIRE(actualPath == expectedPath); +} + +TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirDoesNotExist", "[getSocketFolder]") { + // Test case: XDG_RUNTIME_DIR does not exist + // Arrange + unsetenv("XDG_RUNTIME_DIR"); + fs::path expectedPath = fs::path("/tmp") / "hypr" / instanceSig; + + // Act + fs::path actualPath = getSocketFolder(instanceSig); + + // Assert expected result + REQUIRE(actualPath == expectedPath); +} + +TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirExistsNoHyprDir", "[getSocketFolder]") { + // Test case: XDG_RUNTIME_DIR exists but does not contain "hypr" directory + // Arrange + fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + fs::create_directories(tempDir); + setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); + fs::path expectedPath = fs::path("/tmp") / "hypr" / instanceSig; + + // Act + fs::path actualPath = getSocketFolder(instanceSig); + + // Assert expected result + REQUIRE(actualPath == expectedPath); +} + +TEST_CASE_METHOD(IPCTestFixture, "getSocket1Reply throws on no socket", "[getSocket1Reply]") { + std::string request = "test_request"; + + CHECK_THROWS(getSocket1Reply(request)); +} diff --git a/test/hyprland/fixtures/IPCTestFixture.hpp b/test/hyprland/fixtures/IPCTestFixture.hpp new file mode 100644 index 00000000..caa92975 --- /dev/null +++ b/test/hyprland/fixtures/IPCTestFixture.hpp @@ -0,0 +1,25 @@ +#include "modules/hyprland/backend.hpp" + +namespace fs = std::filesystem; +namespace hyprland = waybar::modules::hyprland; + +class IPCTestFixture : public hyprland::IPC { + public: + IPCTestFixture() : IPC() { IPC::socketFolder_ = ""; } + ~IPCTestFixture() { fs::remove_all(tempDir); } + + protected: + const char* instanceSig = "instance_sig"; + fs::path tempDir = fs::temp_directory_path() / "hypr_test"; + + private: +}; + +class IPCMock : public IPCTestFixture { + public: + // Mock getSocket1Reply to return an empty string + static std::string getSocket1Reply(const std::string& rq) { return ""; } + + protected: + const char* instanceSig = "instance_sig"; +}; diff --git a/test/hyprland/meson.build b/test/hyprland/meson.build new file mode 100644 index 00000000..533022fc --- /dev/null +++ b/test/hyprland/meson.build @@ -0,0 +1,28 @@ +test_inc = include_directories('../../include') + +test_dep = [ + catch2, + fmt, + gtkmm, + jsoncpp, + spdlog, +] + +test_src = files( + '../main.cpp', + 'backend.cpp', + '../../src/modules/hyprland/backend.cpp' +) + +hyprland_test = executable( + 'hyprland_test', + test_src, + dependencies: test_dep, + include_directories: test_inc, +) + +test( + 'hyprland', + hyprland_test, + workdir: meson.project_source_root(), +) diff --git a/test/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..ea430c50 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,4 +1,5 @@ test_inc = include_directories('../include') + test_dep = [ catch2, fmt, @@ -6,18 +7,13 @@ test_dep = [ jsoncpp, spdlog, ] + test_src = files( 'main.cpp', - 'SafeSignal.cpp', 'config.cpp', '../src/config.cpp', ) -if tz_dep.found() - test_dep += tz_dep - test_src += files('date.cpp') -endif - waybar_test = executable( 'waybar_test', test_src, @@ -28,5 +24,8 @@ waybar_test = executable( test( 'waybar', waybar_test, - workdir: meson.source_root(), + workdir: meson.project_source_root(), ) + +subdir('utils') +subdir('hyprland') diff --git a/test/utils/JsonParser.cpp b/test/utils/JsonParser.cpp new file mode 100644 index 00000000..99a8649e --- /dev/null +++ b/test/utils/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/SafeSignal.cpp b/test/utils/SafeSignal.cpp similarity index 97% rename from test/SafeSignal.cpp rename to test/utils/SafeSignal.cpp index f496d7ab..e7e096b0 100644 --- a/test/SafeSignal.cpp +++ b/test/utils/SafeSignal.cpp @@ -10,7 +10,7 @@ #include #include -#include "GlibTestsFixture.hpp" +#include "fixtures/GlibTestsFixture.hpp" using namespace waybar; @@ -71,7 +71,7 @@ struct TestObject { unsigned copied = 0; unsigned moved = 0; - TestObject(const T& v) : value(v){}; + TestObject(const T& v) : value(v) {}; ~TestObject() = default; TestObject(const TestObject& other) diff --git a/test/utils/css_reload_helper.cpp b/test/utils/css_reload_helper.cpp new file mode 100644 index 00000000..f3888a83 --- /dev/null +++ b/test/utils/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/utils/date.cpp b/test/utils/date.cpp new file mode 100644 index 00000000..576a4799 --- /dev/null +++ b/test/utils/date.cpp @@ -0,0 +1,190 @@ +#include "util/date.hpp" + +#include +#include +#include +#include + +#if __has_include() +#include +#include +#else +#include +#endif + +#ifndef SKIP +#define SKIP(...) \ + WARN(__VA_ARGS__); \ + return +#endif + +using namespace date; +using namespace std::literals::chrono_literals; +namespace fmt_lib = waybar::util::date::format; + +/* + * Check that the date/time formatter with locale and timezone support is working as expected. + */ + +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. + * + * The check expects Glibc output style and will fail with FreeBSD (different implementation) + * or musl (no implementation). + */ +static const bool LC_TIME_is_sane = []() { + try { + std::stringstream ss; + ss.imbue(std::locale("en_US.UTF-8")); + + time_t t = 1641211200; + std::tm tm = *std::gmtime(&t); + + ss << std::put_time(&tm, "%x %X"); + return ss.str() == "01/03/2022 12:00:00 PM"; + } catch (std::exception &) { + return false; + } +}(); + +TEST_CASE("Format UTC time", "[clock][util]") { + const auto loc = std::locale("C"); + const auto tm = TEST_TIME; +#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"); + } + + /* Test a few locales that are most likely to be present */ + SECTION("US locale") { + try { + const auto loc = std::locale("en_US.UTF-8"); + +#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_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"); + } + } + SECTION("GB locale") { + try { + const auto loc = std::locale("en_GB.UTF-8"); + +#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_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"); + } + } + SECTION("Global locale") { + try { + const auto loc = std::locale::global(std::locale("en_US.UTF-8")); + +#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_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 &) { + WARN("Locale en_US not found, skip tests"); + } + } +} + +TEST_CASE("Format zoned time", "[clock][util]") { + const auto loc = std::locale("C"); + const auto tm = zoned_time{"America/New_York", TEST_TIME}; + +#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"); + } + + /* Test a few locales that are most likely to be present */ + SECTION("US locale") { + try { + const auto loc = std::locale("en_US.UTF-8"); + +#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_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"); + } + } + SECTION("GB locale") { + try { + const auto loc = std::locale("en_GB.UTF-8"); + +#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_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"); + } + } + SECTION("Global locale") { + try { + const auto loc = std::locale::global(std::locale("en_US.UTF-8")); + +#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_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 &) { + WARN("Locale en_US not found, skip tests"); + } + } +} diff --git a/test/GlibTestsFixture.hpp b/test/utils/fixtures/GlibTestsFixture.hpp similarity index 100% rename from test/GlibTestsFixture.hpp rename to test/utils/fixtures/GlibTestsFixture.hpp diff --git a/test/utils/meson.build b/test/utils/meson.build new file mode 100644 index 00000000..b7b3665a --- /dev/null +++ b/test/utils/meson.build @@ -0,0 +1,36 @@ +test_inc = include_directories('../../include') + +test_dep = [ + catch2, + fmt, + gtkmm, + jsoncpp, + spdlog, +] +test_src = files( + '../main.cpp', + '../config.cpp', + '../../src/config.cpp', + 'JsonParser.cpp', + 'SafeSignal.cpp', + 'css_reload_helper.cpp', + '../../src/util/css_reload_helper.cpp', +) + +if tz_dep.found() + test_dep += tz_dep + test_src += files('date.cpp') +endif + +utils_test = executable( + 'utils_test', + test_src, + dependencies: test_dep, + include_directories: test_inc, +) + +test( + 'utils', + utils_test, + workdir: meson.project_source_root(), +)